Thomas Bruederli
2014-01-21 d93ce5cde23b7170b96fd9816e8d5e8cfdf6e0f6
commit | author | age
566747 1 <?php
T 2
3 /*
4  +-----------------------------------------------------------------------+
5  | This file is part of the Roundcube Webmail client                     |
6  |                                                                       |
7  | Copyright (C) 2013, The Roundcube Dev Team                            |
8  | Copyright (C) 2013, Kolab Systems AG                                  |
9  |                                                                       |
10  | Licensed under the GNU General Public License version 3 or            |
11  | any later version with exceptions for skins & plugins.                |
12  | See the README file for a full license statement.                     |
13  |                                                                       |
14  | PURPOSE:                                                              |
15  |   Execute (multi-threaded) searches in multiple IMAP folders          |
16  +-----------------------------------------------------------------------+
17  | Author: Thomas Bruederli <roundcube@gmail.com>                        |
18  +-----------------------------------------------------------------------+
19 */
20
21 // create classes defined by the pthreads module if that isn't installed
22 if (!defined('PTHREADS_INHERIT_ALL')) {
23     class Worker { }
24     class Stackable { }
25 }
26
27 /**
28  * Class to control search jobs on multiple IMAP folders.
29  * This implement a simple threads pool using the pthreads extension.
30  *
31  * @package    Framework
32  * @subpackage Storage
33  * @author     Thomas Bruederli <roundcube@gmail.com>
34  */
35 class rcube_imap_search
36 {
37     public $options = array();
38
39     private $size = 10;
40     private $next = 0;
41     private $workers = array();
42     private $states = array();
43     private $jobs = array();
44     private $conn;
45
46     /**
47      * Default constructor
48      */
49     public function __construct($options, $conn)
50     {
51         $this->options = $options;
52         $this->conn = $conn;
53     }
54
55     /**
56      * Invoke search request to IMAP server
57      *
58      * @param  array   $folders    List of IMAP folders to search in
59      * @param  string  $str        Search criteria
60      * @param  string  $charset    Search charset
61      * @param  string  $sort_field Header field to sort by
62      * @param  boolean $threading  True if threaded listing is active
63      */
64     public function exec($folders, $str, $charset = null, $sort_field = null, $threading=null)
65     {
66         $pthreads = defined('PTHREADS_INHERIT_ALL');
67
68         // start a search job for every folder to search in
69         foreach ($folders as $folder) {
70             $job = new rcube_imap_search_job($folder, $str, $charset, $sort_field, $threading);
71             if ($pthreads && $this->submit($job)) {
72                 $this->jobs[] = $job;
73             }
74             else {
75                 $job->worker = $this;
76                 $job->run();
77                 $this->jobs[] = $job;
78             }
79         }
80
81         // wait for all workers to be done
82         $this->shutdown();
83
84         // gather results
85         $results = new rcube_result_multifolder;
86         foreach ($this->jobs as $job) {
87             $results->add($job->get_result());
88         }
89
90         return $results;
91     }
92
93     /**
94      * Assign the given job object to one of the worker threads for execution
95      */
96     public function submit(Stackable $job)
97     {
98         if (count($this->workers) < $this->size) {
99             $id = count($this->workers);
100             $this->workers[$id] = new rcube_imap_search_worker($id, $this->options);
101             $this->workers[$id]->start(PTHREADS_INHERIT_ALL);
102
103             if ($this->workers[$id]->stack($job)) {
104                 return $job;
105             }
106             else {
107                 // trigger_error(sprintf("Failed to push Stackable onto %s", $id), E_USER_WARNING);
108             }
109         }
110         if (($worker = $this->workers[$this->next])) {
111             $this->next = ($this->next+1) % $this->size;
112             if ($worker->stack($job)) {
113                 return $job;
114             }
115             else {
116                 // trigger_error(sprintf("Failed to stack onto selected worker %s", $worker->id), E_USER_WARNING);
117             }
118         }
119         else {
120             // trigger_error(sprintf("Failed to select a worker for Stackable"), E_USER_WARNING);
121         }
122
123         return false;
124     }
125
126     /**
127      * Shutdown the pool of threads cleanly, retaining exit status locally
128      */
129     public function shutdown()
130     {
131         foreach ($this->workers as $worker) {
132             $this->states[$worker->getThreadId()] = $worker->shutdown();
133             $worker->close();
134         }
135
136         # console('shutdown', $this->states);
137     }
138     
139     /**
140      * Get connection to the IMAP server
141      * (used for single-thread mode)
142      */
143     public function get_imap()
144     {
145         return $this->conn;
146     }
147 }
148
149
150 /**
151  * Stackable item to run the search on a specific IMAP folder
152  */
153 class rcube_imap_search_job extends Stackable
154 {
155     private $folder;
156     private $search;
157     private $charset;
158     private $sort_field;
159     private $threading;
160     private $searchset;
161     private $result;
162     private $pagesize = 100;
163
164     public function __construct($folder, $str, $charset = null, $sort_field = null, $threading=false)
165     {
166         $this->folder = $folder;
167         $this->search = $str;
168         $this->charset = $charset;
169         $this->sort_field = $sort_field;
170         $this->threading = $threading;
171     }
172
173     public function run()
174     {
b6e24c 175         // trigger_error("Start search $this->folder", E_USER_NOTICE);
566747 176         $this->result = $this->search_index();
b6e24c 177         // trigger_error("End search $this->folder: " . $this->result->count(), E_USER_NOTICE);
566747 178     }
T 179
180     /**
181      * Copy of rcube_imap::search_index()
182      */
183     protected function search_index()
184     {
b6e24c 185         $pthreads = defined('PTHREADS_INHERIT_ALL');
566747 186         $criteria = $this->search;
T 187         $charset = $this->charset;
188
189         $imap = $this->worker->get_imap();
190
191         if (!$imap->connected()) {
d93ce5 192             trigger_error("No IMAP connection for $this->folder", E_USER_WARNING);
TB 193
566747 194             if ($this->threading) {
T 195                 return new rcube_result_thread();
196             }
197             else {
198                 return new rcube_result_index();
199             }
200         }
201
202         if ($this->worker->options['skip_deleted'] && !preg_match('/UNDELETED/', $criteria)) {
203             $criteria = 'UNDELETED '.$criteria;
204         }
205
206         // unset CHARSET if criteria string is ASCII, this way
207         // SEARCH won't be re-sent after "unsupported charset" response
208         if ($charset && $charset != 'US-ASCII' && is_ascii($criteria)) {
209             $charset = 'US-ASCII';
210         }
211
212         if ($this->threading) {
213             $threads = $imap->thread($this->folder, $this->threading, $criteria, true, $charset);
214
215             // Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
216             // but I've seen that Courier doesn't support UTF-8)
217             if ($threads->is_error() && $charset && $charset != 'US-ASCII') {
218                 $threads = $imap->thread($this->folder, $this->threading,
219                     rcube_imap::convert_criteria($criteria, $charset), true, 'US-ASCII');
220             }
221
b6e24c 222             // close IMAP connection again
TB 223             if ($pthreads)
224                 $imap->closeConnection();
225
566747 226             return $threads;
T 227         }
228
229         if ($this->sort_field) {
230             $messages = $imap->sort($this->folder, $this->sort_field, $criteria, true, $charset);
231
232             // Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
233             // but I've seen Courier with disabled UTF-8 support)
234             if ($messages->is_error() && $charset && $charset != 'US-ASCII') {
235                 $messages = $imap->sort($this->folder, $this->sort_field,
236                     rcube_imap::convert_criteria($criteria, $charset), true, 'US-ASCII');
237             }
b6e24c 238         }
566747 239
d53b60 240         if (!$messages || $messages->is_error()) {
b6e24c 241             $messages = $imap->search($this->folder,
TB 242                 ($charset && $charset != 'US-ASCII' ? "CHARSET $charset " : '') . $criteria, true);
243
244             // Error, try with US-ASCII (some servers may support only US-ASCII)
245             if ($messages->is_error() && $charset && $charset != 'US-ASCII') {
246                 $messages = $imap->search($this->folder,
247                     rcube_imap::convert_criteria($criteria, $charset), true);
566747 248             }
T 249         }
250
b6e24c 251         // close IMAP connection again
TB 252         if ($pthreads)
253             $imap->closeConnection();
566747 254
T 255         return $messages;
256     }
257
258     public function get_search_set()
259     {
260         return array(
261             $this->search,
262             $this->result,
263             $this->charset,
264             $this->sort_field,
265             $this->threading,
266         );
267     }
268
269     public function get_result()
270     {
271         return $this->result;
272     }
273 }
274
275
276 /**
d53b60 277  * Worker thread to run search jobs while maintaining a common context
566747 278  */
T 279 class rcube_imap_search_worker extends Worker
280 {
281     public $id;
282     public $options;
283
284     private $conn;
d93ce5 285     private $counts = 0;
566747 286
T 287     /**
288      * Default constructor
289      */
290     public function __construct($id, $options)
291     {
292         $this->id = $id;
293         $this->options = $options;
294     }
295
296     /**
297      * Get a dedicated connection to the IMAP server
298      */
299     public function get_imap()
300     {
301         // TODO: make this connection persistent for several jobs
d93ce5 302         // This doesn't seem to work. Socket connections don't survive serialization which is used in pthreads
566747 303
T 304         $conn = new rcube_imap_generic();
305         # $conn->setDebug(true, function($conn, $message){ trigger_error($message, E_USER_NOTICE); });
306
307         if ($this->options['user'] && $this->options['password']) {
d93ce5 308             $this->options['ident']['command'] = 'search-' . $this->id . 't' . ++$this->counts;
566747 309             $conn->connect($this->options['host'], $this->options['user'], $this->options['password'], $this->options);
T 310         }
311
312         if ($conn->error)
b6e24c 313             trigger_error($conn->error, E_USER_WARNING);
566747 314
T 315         return $conn;
316     }
317
318     /**
319      * @override
320      */
321     public function run()
322     {
323         
324     }
325
326     /**
327      * Close IMAP connection
328      */
329     public function close()
330     {
331         if ($this->conn) {
332             $this->conn->close();
333         }
334     }
335 }
336