program/js/app.js | ●●●●● patch | view | raw | blame | history | |
program/lib/Roundcube/rcube_imap.php | ●●●●● patch | view | raw | blame | history | |
program/lib/Roundcube/rcube_imap_search.php | ●●●●● patch | view | raw | blame | history | |
program/lib/Roundcube/rcube_result_index.php | ●●●●● patch | view | raw | blame | history | |
program/lib/Roundcube/rcube_result_multifolder.php | ●●●●● patch | view | raw | blame | history | |
program/lib/Roundcube/rcube_result_thread.php | ●●●●● patch | view | raw | blame | history | |
program/localization/en_US/messages.inc | ●●●●● patch | view | raw | blame | history | |
program/steps/mail/func.inc | ●●●●● patch | view | raw | blame | history | |
program/steps/mail/search.inc | ●●●●● patch | view | raw | blame | history |
program/js/app.js
@@ -1992,6 +1992,9 @@ else html = ' '; } else if (c == 'folder') { html = '<span onmouseover="rcube_webmail.long_subject_title(this)">' + cols[c] + '<span>'; } else html = cols[c]; @@ -4212,6 +4215,17 @@ } return false; }; this.continue_search = function(request_id) { var lock = ref.set_busy(true, 'stillsearching'); setTimeout(function(){ var url = ref.search_params(); url._continue = request_id; ref.env.qsearch = { lock: lock, request: ref.http_request('search', url, lock) }; }, 100); }; // build URL params for search @@ -7870,7 +7884,7 @@ { if (!elem.title) { var $elem = $(elem); if ($elem.width() + indent * 15 > $elem.parent().width()) if ($elem.width() + (indent || 0) * 15 > $elem.parent().width()) elem.title = $elem.text(); } }; program/lib/Roundcube/rcube_imap.php
@@ -1513,6 +1513,16 @@ $this->threading = false; $searcher = new rcube_imap_search($this->options, $this->conn); // set limit to not exceed the client's request timeout $searcher->set_timelimit(60); // continue existing incomplete search if (!empty($this->search_set) && $this->search_set->incomplete && $str == $this->search_string) { $searcher->set_results($this->search_set); } // execute the search $results = $searcher->exec( $folder, $str, program/lib/Roundcube/rcube_imap_search.php
@@ -5,7 +5,7 @@ | This file is part of the Roundcube Webmail client | | | | Copyright (C) 2013, The Roundcube Dev Team | | Copyright (C) 2013, Kolab Systems AG | | Copyright (C) 2014, Kolab Systems AG | | | | Licensed under the GNU General Public License version 3 or | | any later version with exceptions for skins & plugins. | @@ -18,15 +18,8 @@ +-----------------------------------------------------------------------+ */ // 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 @@ -36,12 +29,10 @@ { public $options = array(); private $size = 10; private $next = 0; private $workers = array(); private $states = array(); private $jobs = array(); private $conn; protected $jobs = array(); protected $timelimit = 0; protected $results; protected $conn; /** * Default constructor @@ -63,28 +54,32 @@ */ public function exec($folders, $str, $charset = null, $sort_field = null, $threading=null) { $pthreads = defined('PTHREADS_INHERIT_ALL'); $start = floor(microtime(true)); $results = new rcube_result_multifolder($folders); // 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; // a complete result for this folder already exists $result = $this->results ? $this->results->get_set($folder) : false; if ($result && !$result->incomplete) { $results->add($result); } else { $job = new rcube_imap_search_job($folder, $str, $charset, $sort_field, $threading); $job->worker = $this; $job->run(); $this->jobs[] = $job; } } // wait for all workers to be done $this->shutdown(); // gather results // execute jobs and gather results foreach ($this->jobs as $job) { // only run search if within the configured time limit // TODO: try to estimate the required time based on folder size and previous search performance if (!$this->timelimit || floor(microtime(true)) - $start < $this->timelimit) { $job->run(); } // add result (may have ->incomplete flag set) $results->add($job->get_result()); } @@ -92,49 +87,19 @@ } /** * Assign the given job object to one of the worker threads for execution * Setter for timelimt property */ public function submit(Stackable $job) public function set_timelimit($seconds) { 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; $this->timelimit = $seconds; } /** * Shutdown the pool of threads cleanly, retaining exit status locally * Setter for previous (potentially incomplete) search results */ public function shutdown() public function set_results($res) { foreach ($this->workers as $worker) { $this->states[$worker->getThreadId()] = $worker->shutdown(); $worker->close(); } # console('shutdown', $this->states); $this->results = $res; } /** @@ -151,7 +116,7 @@ /** * Stackable item to run the search on a specific IMAP folder */ class rcube_imap_search_job extends Stackable class rcube_imap_search_job /* extends Stackable */ { private $folder; private $search; @@ -169,13 +134,14 @@ $this->charset = $charset; $this->sort_field = $sort_field; $this->threading = $threading; $this->result = new rcube_result_index($folder); $this->result->incomplete = true; } 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); } /** @@ -183,7 +149,6 @@ */ protected function search_index() { $pthreads = defined('PTHREADS_INHERIT_ALL'); $criteria = $this->search; $charset = $this->charset; @@ -193,10 +158,10 @@ trigger_error("No IMAP connection for $this->folder", E_USER_WARNING); if ($this->threading) { return new rcube_result_thread(); return new rcube_result_thread($this->folder); } else { return new rcube_result_index(); return new rcube_result_index($this->folder); } } @@ -219,10 +184,6 @@ $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; } @@ -249,10 +210,6 @@ } } // close IMAP connection again if ($pthreads) $imap->closeConnection(); return $messages; } @@ -271,67 +228,7 @@ { 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(); } } } program/lib/Roundcube/rcube_result_index.php
@@ -26,6 +26,8 @@ */ class rcube_result_index { public $incomplete = false; protected $raw_data; protected $mailbox; protected $meta = array(); program/lib/Roundcube/rcube_result_multifolder.php
@@ -28,6 +28,7 @@ { public $multi = true; public $sets = array(); public $incomplete = false; public $folder; protected $meta = array(); @@ -54,14 +55,18 @@ */ public function add($result) { if ($count = $result->count()) { $this->sets[] = $result; if ($count = $result->count()) { $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); } else if ($result->incomplete) { $this->incomplete = true; } } @@ -266,6 +271,22 @@ return $params; } /** * Returns the stored result object for a particular folder * * @param string $folder Folder name * @return false|obejct rcube_result_* instance of false if none found */ public function get_set($folder) { foreach ($this->sets as $set) { if ($set->get_parameters('MAILBOX') == $folder) { return $set; } } return false; } /** * Returns length of internal data representation @@ -276,4 +297,33 @@ { return $this->count(); } /* Serialize magic methods */ public function __sleep() { return array('sets','folders','sorting','order'); } public function __wakeup() { // restore index from saved result sets $this->meta = array('count' => 0); foreach ($this->sets as $result) { if ($count = $result->count()) { $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); } else if ($result->incomplete) { $this->incomplete = true; } } } } program/lib/Roundcube/rcube_result_thread.php
@@ -26,6 +26,8 @@ */ class rcube_result_thread { public $incomplete = false; protected $raw_data; protected $mailbox; protected $meta = array(); program/localization/en_US/messages.inc
@@ -95,6 +95,7 @@ $messages['searchnomatch'] = 'Search returned no matches.'; $messages['searching'] = 'Searching...'; $messages['checking'] = 'Checking...'; $messages['stillsearching'] = 'Still searching...'; $messages['nospellerrors'] = 'No spelling errors found.'; $messages['folderdeleted'] = 'Folder successfully deleted.'; $messages['foldersubscribed'] = 'Folder successfully subscribed.'; program/steps/mail/func.inc
@@ -142,7 +142,7 @@ if (!$OUTPUT->ajax_call) { $OUTPUT->add_label('checkingmail', 'deletemessage', 'movemessagetotrash', 'movingmessage', 'copyingmessage', 'deletingmessage', 'markingmessage', 'copy', 'move', 'quota', 'replyall', 'replylist'); 'copy', 'move', 'quota', 'replyall', 'replylist', 'stillsearching'); } $pagetitle = $RCMAIL->localize_foldername($mbox_name, true); program/steps/mail/search.inc
@@ -38,6 +38,7 @@ $filter = rcube_utils::get_input_value('_filter', rcube_utils::INPUT_GET); $headers = rcube_utils::get_input_value('_headers', rcube_utils::INPUT_GET); $scope = rcube_utils::get_input_value('_scope', rcube_utils::INPUT_GET); $continue = rcube_utils::get_input_value('_continue', rcube_utils::INPUT_GET); $subject = array(); $filter = trim($filter); @@ -110,6 +111,12 @@ $search_str = trim($search_str); $sort_column = rcmail_sort_column(); // set message set for already stored (but incomplete) search request if (!empty($continue) && isset($_SESSION['search']) && $_SESSION['search_request'] == $continue) { $RCMAIL->storage->set_search_set($_SESSION['search']); $search_str = $_SESSION['search'][0]; } // execute IMAP search if ($search_str) { // search all, current or subfolders folders @@ -126,10 +133,6 @@ $result = $RCMAIL->storage->search($mboxes, $search_str, $imap_charset, $sort_column); } // Get the headers $result_h = $RCMAIL->storage->list_messages($mbox, 1, $sort_column, rcmail_sort_order()); $count = $RCMAIL->storage->count($mbox, $RCMAIL->storage->get_threading() ? 'THREADS' : 'ALL'); // save search results in session if (!is_array($_SESSION['search'])) { $_SESSION['search'] = array(); @@ -141,6 +144,13 @@ } $_SESSION['search_request'] = $search_request; $_SESSION['search_scope'] = $scope; // Get the headers if (!$result->incomplete) { $result_h = $RCMAIL->storage->list_messages($mbox, 1, $sort_column, rcmail_sort_order()); $count = $RCMAIL->storage->count($mbox, $RCMAIL->storage->get_threading() ? 'THREADS' : 'ALL'); } // Make sure we got the headers if (!empty($result_h)) { @@ -160,6 +170,12 @@ else if ($err_code = $RCMAIL->storage->get_error_code()) { $RCMAIL->display_server_error(); } // advice the client to re-send the (cross-folder) search request else if ($result->incomplete) { $count = 0; // keep UI locked $OUTPUT->command('continue_search', $search_request); console('search incomplete', strlen(serialize($result))); } else { $OUTPUT->show_message('searchnomatch', 'notice'); $OUTPUT->set_env('multifolder_listing', (bool)$result->multi);