Merge branch 'dev-multi-folder-search'
commit
66536974fe
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue