diff options
author | Thomas Bruederli <thomas@roundcube.net> | 2014-04-07 09:53:46 +0200 |
---|---|---|
committer | Thomas Bruederli <thomas@roundcube.net> | 2014-04-07 09:53:46 +0200 |
commit | 66536974fe12a02ca5ffcec4354bf5113282a0cc (patch) | |
tree | 034b848ec971008eab1003a2a9a401fb7020a517 /program/lib/Roundcube | |
parent | c77a8497e7c4b04dd881e55341c779f6fe5ffa34 (diff) | |
parent | e3857bb6c2a2f9a4331a72b0bd74e0d70ee8a8a1 (diff) |
Merge branch 'dev-multi-folder-search'
Diffstat (limited to 'program/lib/Roundcube')
-rw-r--r-- | program/lib/Roundcube/rcube_imap.php | 108 | ||||
-rw-r--r-- | program/lib/Roundcube/rcube_imap_search.php | 336 | ||||
-rw-r--r-- | program/lib/Roundcube/rcube_message_header.php | 9 | ||||
-rw-r--r-- | program/lib/Roundcube/rcube_result_multifolder.php | 250 |
4 files changed, 695 insertions, 8 deletions
diff --git a/program/lib/Roundcube/rcube_imap.php b/program/lib/Roundcube/rcube_imap.php index 628338a44..f60be620c 100644 --- a/program/lib/Roundcube/rcube_imap.php +++ b/program/lib/Roundcube/rcube_imap.php @@ -332,6 +332,10 @@ class rcube_imap extends rcube_storage $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); + } } @@ -945,6 +949,54 @@ class rcube_imap extends rcube_storage 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); @@ -1111,6 +1163,7 @@ class rcube_imap extends rcube_storage } foreach ($headers as $h) { + $h->folder = $folder; $a_msg_headers[$h->uid] = $h; } @@ -1234,8 +1287,13 @@ class rcube_imap extends rcube_storage return new rcube_result_index($folder, '* SORT'); } + 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? - if (($this->search_set instanceof rcube_result_index) + 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)) ) { @@ -1422,11 +1480,34 @@ class rcube_imap extends rcube_storage $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)); @@ -1500,7 +1581,7 @@ class rcube_imap extends rcube_storage // 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; @@ -1514,7 +1595,7 @@ class rcube_imap extends rcube_storage // 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()) { @@ -1529,7 +1610,7 @@ class rcube_imap extends rcube_storage // 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; @@ -1547,7 +1628,7 @@ class rcube_imap extends rcube_storage * * @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)) { @@ -1583,6 +1664,7 @@ class rcube_imap extends rcube_storage 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); } @@ -1605,6 +1687,11 @@ class rcube_imap extends rcube_storage $folder = $this->folder; } + // decode combined UID-folder identifier + if (preg_match('/^\d+-[^,]+$/', $uid)) { + list($uid, $folder) = explode('-', $uid); + } + // get cached headers if (!$force && $uid && ($mcache = $this->get_mcache_engine())) { $headers = $mcache->get_message($folder, $uid); @@ -1636,6 +1723,11 @@ class rcube_imap extends rcube_storage $folder = $this->folder; } + // decode combined UID-folder identifier + if (preg_match('/^\d+-[^,]+$/', $uid)) { + list($uid, $folder) = explode('-', $uid); + } + // Check internal cache if (!empty($this->icache['message'])) { if (($headers = $this->icache['message']) && $headers->uid == $uid) { @@ -2397,7 +2489,7 @@ class rcube_imap extends rcube_storage $this->refresh_search(); } else { - $this->search_set->filter(explode(',', $uids)); + $this->search_set->filter(explode(',', $uids), $this->folder); } } diff --git a/program/lib/Roundcube/rcube_imap_search.php b/program/lib/Roundcube/rcube_imap_search.php new file mode 100644 index 000000000..c88198140 --- /dev/null +++ b/program/lib/Roundcube/rcube_imap_search.php @@ -0,0 +1,336 @@ +<?php + +/* + +-----------------------------------------------------------------------+ + | This file is part of the Roundcube Webmail client | + | | + | Copyright (C) 2013, The Roundcube Dev Team | + | Copyright (C) 2013, Kolab Systems AG | + | | + | Licensed under the GNU General Public License version 3 or | + | any later version with exceptions for skins & plugins. | + | See the README file for a full license statement. | + | | + | PURPOSE: | + | Execute (multi-threaded) searches in multiple IMAP folders | + +-----------------------------------------------------------------------+ + | Author: Thomas Bruederli <roundcube@gmail.com> | + +-----------------------------------------------------------------------+ +*/ + +// create classes defined by the pthreads module if that isn't installed +if (!defined('PTHREADS_INHERIT_ALL')) { + class Worker { } + class Stackable { } +} + +/** + * Class to control search jobs on multiple IMAP folders. + * This implement a simple threads pool using the pthreads extension. + * + * @package Framework + * @subpackage Storage + * @author Thomas Bruederli <roundcube@gmail.com> + */ +class rcube_imap_search +{ + public $options = array(); + + private $size = 10; + private $next = 0; + private $workers = array(); + private $states = array(); + private $jobs = array(); + private $conn; + + /** + * Default constructor + */ + public function __construct($options, $conn) + { + $this->options = $options; + $this->conn = $conn; + } + + /** + * Invoke search request to IMAP server + * + * @param array $folders List of IMAP folders to search in + * @param string $str Search criteria + * @param string $charset Search charset + * @param string $sort_field Header field to sort by + * @param boolean $threading True if threaded listing is active + */ + public function exec($folders, $str, $charset = null, $sort_field = null, $threading=null) + { + $pthreads = defined('PTHREADS_INHERIT_ALL'); + + // start a search job for every folder to search in + foreach ($folders as $folder) { + $job = new rcube_imap_search_job($folder, $str, $charset, $sort_field, $threading); + if ($pthreads && $this->submit($job)) { + $this->jobs[] = $job; + } + else { + $job->worker = $this; + $job->run(); + $this->jobs[] = $job; + } + } + + // wait for all workers to be done + $this->shutdown(); + + // gather results + $results = new rcube_result_multifolder; + foreach ($this->jobs as $job) { + $results->add($job->get_result()); + } + + return $results; + } + + /** + * Assign the given job object to one of the worker threads for execution + */ + public function submit(Stackable $job) + { + if (count($this->workers) < $this->size) { + $id = count($this->workers); + $this->workers[$id] = new rcube_imap_search_worker($id, $this->options); + $this->workers[$id]->start(PTHREADS_INHERIT_ALL); + + if ($this->workers[$id]->stack($job)) { + return $job; + } + else { + // trigger_error(sprintf("Failed to push Stackable onto %s", $id), E_USER_WARNING); + } + } + if (($worker = $this->workers[$this->next])) { + $this->next = ($this->next+1) % $this->size; + if ($worker->stack($job)) { + return $job; + } + else { + // trigger_error(sprintf("Failed to stack onto selected worker %s", $worker->id), E_USER_WARNING); + } + } + else { + // trigger_error(sprintf("Failed to select a worker for Stackable"), E_USER_WARNING); + } + + return false; + } + + /** + * Shutdown the pool of threads cleanly, retaining exit status locally + */ + public function shutdown() + { + foreach ($this->workers as $worker) { + $this->states[$worker->getThreadId()] = $worker->shutdown(); + $worker->close(); + } + + # console('shutdown', $this->states); + } + + /** + * Get connection to the IMAP server + * (used for single-thread mode) + */ + public function get_imap() + { + return $this->conn; + } +} + + +/** + * Stackable item to run the search on a specific IMAP folder + */ +class rcube_imap_search_job extends Stackable +{ + private $folder; + private $search; + private $charset; + private $sort_field; + private $threading; + private $searchset; + private $result; + private $pagesize = 100; + + public function __construct($folder, $str, $charset = null, $sort_field = null, $threading=false) + { + $this->folder = $folder; + $this->search = $str; + $this->charset = $charset; + $this->sort_field = $sort_field; + $this->threading = $threading; + } + + public function run() + { + // trigger_error("Start search $this->folder", E_USER_NOTICE); + $this->result = $this->search_index(); + // trigger_error("End search $this->folder: " . $this->result->count(), E_USER_NOTICE); + } + + /** + * Copy of rcube_imap::search_index() + */ + protected function search_index() + { + $pthreads = defined('PTHREADS_INHERIT_ALL'); + $criteria = $this->search; + $charset = $this->charset; + + $imap = $this->worker->get_imap(); + + if (!$imap->connected()) { + trigger_error("No IMAP connection for $this->folder", E_USER_WARNING); + + if ($this->threading) { + return new rcube_result_thread(); + } + else { + return new rcube_result_index(); + } + } + + if ($this->worker->options['skip_deleted'] && !preg_match('/UNDELETED/', $criteria)) { + $criteria = 'UNDELETED '.$criteria; + } + + // unset CHARSET if criteria string is ASCII, this way + // SEARCH won't be re-sent after "unsupported charset" response + if ($charset && $charset != 'US-ASCII' && is_ascii($criteria)) { + $charset = 'US-ASCII'; + } + + if ($this->threading) { + $threads = $imap->thread($this->folder, $this->threading, $criteria, true, $charset); + + // Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8, + // but I've seen that Courier doesn't support UTF-8) + if ($threads->is_error() && $charset && $charset != 'US-ASCII') { + $threads = $imap->thread($this->folder, $this->threading, + rcube_imap::convert_criteria($criteria, $charset), true, 'US-ASCII'); + } + + // close IMAP connection again + if ($pthreads) + $imap->closeConnection(); + + return $threads; + } + + if ($this->sort_field) { + $messages = $imap->sort($this->folder, $this->sort_field, $criteria, true, $charset); + + // Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8, + // but I've seen Courier with disabled UTF-8 support) + if ($messages->is_error() && $charset && $charset != 'US-ASCII') { + $messages = $imap->sort($this->folder, $this->sort_field, + rcube_imap::convert_criteria($criteria, $charset), true, 'US-ASCII'); + } + } + + if (!$messages || $messages->is_error()) { + $messages = $imap->search($this->folder, + ($charset && $charset != 'US-ASCII' ? "CHARSET $charset " : '') . $criteria, true); + + // Error, try with US-ASCII (some servers may support only US-ASCII) + if ($messages->is_error() && $charset && $charset != 'US-ASCII') { + $messages = $imap->search($this->folder, + rcube_imap::convert_criteria($criteria, $charset), true); + } + } + + // close IMAP connection again + if ($pthreads) + $imap->closeConnection(); + + return $messages; + } + + public function get_search_set() + { + return array( + $this->search, + $this->result, + $this->charset, + $this->sort_field, + $this->threading, + ); + } + + public function get_result() + { + return $this->result; + } +} + + +/** + * Worker thread to run search jobs while maintaining a common context + */ +class rcube_imap_search_worker extends Worker +{ + public $id; + public $options; + + private $conn; + private $counts = 0; + + /** + * Default constructor + */ + public function __construct($id, $options) + { + $this->id = $id; + $this->options = $options; + } + + /** + * Get a dedicated connection to the IMAP server + */ + public function get_imap() + { + // TODO: make this connection persistent for several jobs + // This doesn't seem to work. Socket connections don't survive serialization which is used in pthreads + + $conn = new rcube_imap_generic(); + # $conn->setDebug(true, function($conn, $message){ trigger_error($message, E_USER_NOTICE); }); + + if ($this->options['user'] && $this->options['password']) { + $this->options['ident']['command'] = 'search-' . $this->id . 't' . ++$this->counts; + $conn->connect($this->options['host'], $this->options['user'], $this->options['password'], $this->options); + } + + if ($conn->error) + trigger_error($conn->error, E_USER_WARNING); + + return $conn; + } + + /** + * @override + */ + public function run() + { + + } + + /** + * Close IMAP connection + */ + public function close() + { + if ($this->conn) { + $this->conn->close(); + } + } +} + diff --git a/program/lib/Roundcube/rcube_message_header.php b/program/lib/Roundcube/rcube_message_header.php index 2c5e2b6c8..2bda930eb 100644 --- a/program/lib/Roundcube/rcube_message_header.php +++ b/program/lib/Roundcube/rcube_message_header.php @@ -167,6 +167,13 @@ class rcube_message_header public $mdn_to; /** + * IMAP folder this message is stored in + * + * @var string + */ + public $folder; + + /** * Other message headers * * @var array @@ -189,6 +196,8 @@ class rcube_message_header 'reply-to' => 'replyto', 'cc' => 'cc', 'bcc' => 'bcc', + 'mbox' => 'folder', + 'folder' => 'folder', 'content-transfer-encoding' => 'encoding', 'in-reply-to' => 'in_reply_to', 'content-type' => 'ctype', diff --git a/program/lib/Roundcube/rcube_result_multifolder.php b/program/lib/Roundcube/rcube_result_multifolder.php new file mode 100644 index 000000000..188fb26f2 --- /dev/null +++ b/program/lib/Roundcube/rcube_result_multifolder.php @@ -0,0 +1,250 @@ +<?php + +/* + +-----------------------------------------------------------------------+ + | This file is part of the Roundcube Webmail client | + | Copyright (C) 2005-2011, The Roundcube Dev Team | + | Copyright (C) 2011, Kolab Systems AG | + | | + | Licensed under the GNU General Public License version 3 or | + | any later version with exceptions for skins & plugins. | + | See the README file for a full license statement. | + | | + | PURPOSE: | + | SORT/SEARCH/ESEARCH response handler | + +-----------------------------------------------------------------------+ + | Author: Thomas Bruederli <roundcube@gmail.com> | + +-----------------------------------------------------------------------+ +*/ + +/** + * Class holding a set of rcube_result_index instances that together form a + * result set of a multi-folder search + * + * @package Framework + * @subpackage Storage + */ +class rcube_result_multifolder +{ + public $multi = true; + public $sets = array(); + public $folder; + + protected $meta = array(); + protected $index = array(); + protected $sorting; + protected $order = 'ASC'; + + + /** + * Object constructor. + */ + public function __construct() + { + $this->meta = array('count' => 0); + } + + + /** + * Initializes object with SORT command response + * + * @param string $data IMAP response string + */ + public function add($result) + { + if ($count = $result->count()) { + $this->sets[] = $result; + $this->meta['count'] += $count; + + // append UIDs to global index + $folder = $result->get_parameters('MAILBOX'); + $index = array_map(function($uid) use ($folder) { return $uid . '-' . $folder; }, $result->get()); + $this->index = array_merge($this->index, $index); + } + } + + /** + * Store a global index of (sorted) message UIDs + */ + public function set_message_index($headers, $sort_field, $sort_order) + { + $this->index = array(); + foreach ($headers as $header) { + $this->index[] = $header->uid . '-' . $header->folder; + } + + $this->sorting = $sort_field; + $this->order = $sort_order; + } + + /** + * Checks the result from IMAP command + * + * @return bool True if the result is an error, False otherwise + */ + public function is_error() + { + return false; + } + + + /** + * Checks if the result is empty + * + * @return bool True if the result is empty, False otherwise + */ + public function is_empty() + { + return empty($this->sets) || $this->meta['count'] == 0; + } + + + /** + * Returns number of elements in the result + * + * @return int Number of elements + */ + public function count() + { + return $this->meta['count']; + } + + + /** + * Returns number of elements in the result. + * Alias for count() for compatibility with rcube_result_thread + * + * @return int Number of elements + */ + public function count_messages() + { + return $this->count(); + } + + + /** + * Reverts order of elements in the result + */ + public function revert() + { + $this->order = $this->order == 'ASC' ? 'DESC' : 'ASC'; + } + + + /** + * Check if the given message ID exists in the object + * + * @param int $msgid Message ID + * @param bool $get_index When enabled element's index will be returned. + * Elements are indexed starting with 0 + * @return mixed False if message ID doesn't exist, True if exists or + * index of the element if $get_index=true + */ + public function exists($msgid, $get_index = false) + { + if (!empty($this->folder)) { + $msgid .= '-' . $this->folder; + } + return array_search($msgid, $this->index); + } + + + /** + * Filters data set. Removes elements listed in $ids list. + * + * @param array $ids List of IDs to remove. + * @param string $folder IMAP folder + */ + public function filter($ids = array(), $folder = null) + { + $this->meta['count'] = 0; + foreach ($this->sets as $set) { + if ($set->get_parameters('MAILBOX') == $folder) { + $set->filter($ids); + } + $this->meta['count'] += $set->count(); + } + } + + /** + * Filters data set. Removes elements not listed in $ids list. + * + * @param array $ids List of IDs to keep. + */ + public function intersect($ids = array()) + { + // not implemented + } + + /** + * Return all messages in the result. + * + * @return array List of message IDs + */ + public function get() + { + return $this->index; + } + + + /** + * Return all messages in the result. + * + * @return array List of message IDs + */ + public function get_compressed() + { + return ''; + } + + + /** + * Return result element at specified index + * + * @param int|string $index Element's index or "FIRST" or "LAST" + * + * @return int Element value + */ + public function get_element($idx) + { + switch ($idx) { + case 'FIRST': return $this->index[0]; + case 'LAST': return end($this->index); + default: return $this->index[$idx]; + } + } + + + /** + * Returns response parameters, e.g. ESEARCH's MIN/MAX/COUNT/ALL/MODSEQ + * or internal data e.g. MAILBOX, ORDER + * + * @param string $param Parameter name + * + * @return array|string Response parameters or parameter value + */ + public function get_parameters($param=null) + { + $params = array( + 'SORT' => $this->sorting, + 'ORDER' => $this->order, + ); + + if ($param !== null) { + return $params[$param]; + } + + return $params; + } + + + /** + * Returns length of internal data representation + * + * @return int Data length + */ + protected function length() + { + return $this->count(); + } +} |