From 9e63cd5f24defa521724dfe3dcbbaa4385761836 Mon Sep 17 00:00:00 2001 From: Thomas Date: Wed, 9 Oct 2013 17:12:30 +0200 Subject: Support globally unique message UIDs with IMAP folder name appended --- program/steps/mail/search.inc | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) (limited to 'program/steps/mail/search.inc') diff --git a/program/steps/mail/search.inc b/program/steps/mail/search.inc index fb1b48797..0632b042a 100644 --- a/program/steps/mail/search.inc +++ b/program/steps/mail/search.inc @@ -126,9 +126,23 @@ $_SESSION['search_request'] = $search_request; $result_h = $RCMAIL->storage->list_messages($mbox, 1, $sort_column, rcmail_sort_order()); $count = $RCMAIL->storage->count($mbox, $RCMAIL->storage->get_threading() ? 'THREADS' : 'ALL'); +// Add 'folder' column to list +if ($multi_folder_search) { + $a_show_cols = $_SESSION['list_attrib']['columns'] ? $_SESSION['list_attrib']['columns'] : (array)$CONFIG['list_cols']; + if (!in_array($a_show_cols)) + $a_show_cols[] = 'folder'; + + // make message UIDs unique by appending the folder name + foreach ($result_h as $i => $header) { + $header->uid .= '-'.$header->folder; + if ($header->parent_uid) + $header->parent_uid .= '-'.$header->folder; + } +} + // Make sure we got the headers if (!empty($result_h)) { - rcmail_js_message_list($result_h); + rcmail_js_message_list($result_h, false, $a_show_cols); if ($search_str) $OUTPUT->show_message('searchsuccessful', 'confirmation', array('nr' => $RCMAIL->storage->count(NULL, 'ALL'))); } -- cgit v1.2.3 From 7e3e3ef81ad48f161d01044dcdc2b8cf51811a4f Mon Sep 17 00:00:00 2001 From: Thomas Date: Mon, 14 Oct 2013 21:57:53 +0200 Subject: First attempt to search in multiple folders; do it multi-threaded using pthreads if available --- program/lib/Roundcube/rcube_imap.php | 80 ++++- program/lib/Roundcube/rcube_imap_search.php | 327 +++++++++++++++++++++ program/lib/Roundcube/rcube_result_multifolder.php | 211 +++++++++++++ program/steps/mail/search.inc | 12 +- 4 files changed, 620 insertions(+), 10 deletions(-) create mode 100644 program/lib/Roundcube/rcube_imap_search.php create mode 100644 program/lib/Roundcube/rcube_result_multifolder.php (limited to 'program/steps/mail/search.inc') diff --git a/program/lib/Roundcube/rcube_imap.php b/program/lib/Roundcube/rcube_imap.php index e0dce6f79..98c8f7a4b 100644 --- a/program/lib/Roundcube/rcube_imap.php +++ b/program/lib/Roundcube/rcube_imap.php @@ -910,6 +910,50 @@ 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 = 100; // limit to 100 messages per folder + + $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) { + $a_msg_headers = $this->conn->sortHeaders($a_msg_headers, $this->sort_field, $this->sort_order); + } + + // only return the requested part of the set + $slice_length = min($page_size, $cnt - ($to > $cnt ? $from : $to)); + $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); @@ -1388,11 +1432,33 @@ 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 + if (!defined('PTHREADS_INHERIT_ALL')) { + $this->check_connection(); + } + + $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)); @@ -1466,7 +1532,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; @@ -1480,7 +1546,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()) { @@ -1495,7 +1561,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; @@ -1513,7 +1579,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)) { @@ -2375,7 +2441,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..ed4face98 --- /dev/null +++ b/program/lib/Roundcube/rcube_imap_search.php @@ -0,0 +1,327 @@ + | + +-----------------------------------------------------------------------+ +*/ + +// 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 + */ +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() + { + $criteria = $this->search; + $charset = $this->charset; + + $imap = $this->worker->get_imap(); + + if (!$imap->connected()) { + 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'); + } + + 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->is_error()) { + return $messages; + } + } + + $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); + } + + 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; + } +} + + +/** + * Wrker thread to run search jobs while maintaining a common context + */ +class rcube_imap_search_worker extends Worker +{ + public $id; + public $options; + + private $conn; + + /** + * 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 + #if ($this->conn) + # return $this->conn; + + $conn = new rcube_imap_generic(); + # $conn->setDebug(true, function($conn, $message){ trigger_error($message, E_USER_NOTICE); }); + + if ($this->options['user'] && $this->options['password']) { + $conn->connect($this->options['host'], $this->options['user'], $this->options['password'], $this->options); + } + + if ($conn->error) + trigger_error($this->conn->error, E_USER_WARNING); + + #$this->conn = $conn; + 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_result_multifolder.php b/program/lib/Roundcube/rcube_result_multifolder.php new file mode 100644 index 000000000..8d7ae5de8 --- /dev/null +++ b/program/lib/Roundcube/rcube_result_multifolder.php @@ -0,0 +1,211 @@ + | + +-----------------------------------------------------------------------+ +*/ + +/** + * 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(); + + protected $meta = array(); + 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) + { + $this->sets[] = $result; + $this->meta['count'] += $result->count(); + } + + + /** + * 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) + { + return false; + } + + + /** + * 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 array(); + } + + + /** + * 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($index) + { + return null; + } + + + /** + * 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) + { + return $params; + } + + + /** + * Returns length of internal data representation + * + * @return int Data length + */ + protected function length() + { + return $this->count(); + } +} diff --git a/program/steps/mail/search.inc b/program/steps/mail/search.inc index 0632b042a..7d128c73c 100644 --- a/program/steps/mail/search.inc +++ b/program/steps/mail/search.inc @@ -107,9 +107,12 @@ if (!empty($subject)) { $search_str = trim($search_str); $sort_column = rcmail_sort_column(); +// TEMPORARY: search all folders +$mboxes = $RCMAIL->storage->list_folders_subscribed('', '*', 'mail'); + // execute IMAP search if ($search_str) - $RCMAIL->storage->search($mbox, $search_str, $imap_charset, $sort_column); + $RCMAIL->storage->search($mboxes, $search_str, $imap_charset, $sort_column); // save search results in session if (!is_array($_SESSION['search'])) @@ -127,17 +130,20 @@ $result_h = $RCMAIL->storage->list_messages($mbox, 1, $sort_column, rcmail_sort_ $count = $RCMAIL->storage->count($mbox, $RCMAIL->storage->get_threading() ? 'THREADS' : 'ALL'); // Add 'folder' column to list -if ($multi_folder_search) { +if ($_SESSION['search'][1]->multi) { $a_show_cols = $_SESSION['list_attrib']['columns'] ? $_SESSION['list_attrib']['columns'] : (array)$CONFIG['list_cols']; - if (!in_array($a_show_cols)) + if (!in_array('folder', $a_show_cols)) $a_show_cols[] = 'folder'; // make message UIDs unique by appending the folder name foreach ($result_h as $i => $header) { $header->uid .= '-'.$header->folder; + $header->flags['skip_mbox_check'] = true; if ($header->parent_uid) $header->parent_uid .= '-'.$header->folder; } + + $OUTPUT->command('select_folder', ''); } // Make sure we got the headers -- cgit v1.2.3