source: subversion/trunk/roundcubemail/program/include/rcube_result_index.php @ 5557

Last change on this file since 5557 was 5557, checked in by alec, 18 months ago
  • Fixed issues with big memory allocation of IMAP results, improved a lot of rcube_imap class
File size: 12.6 KB
Line 
1<?php
2
3/*
4 +-----------------------------------------------------------------------+
5 | program/include/rcube_result_index.php                                |
6 |                                                                       |
7 | This file is part of the Roundcube Webmail client                     |
8 | Copyright (C) 2005-2011, The Roundcube Dev Team                       |
9 | Copyright (C) 2011, Kolab Systems AG                                  |
10 | Licensed under the GNU GPL                                            |
11 |                                                                       |
12 | PURPOSE:                                                              |
13 |   SORT/SEARCH/ESEARCH response handler                                |
14 |                                                                       |
15 +-----------------------------------------------------------------------+
16 | Author: Thomas Bruederli <roundcube@gmail.com>                        |
17 | Author: Aleksander Machniak <alec@alec.pl>                            |
18 +-----------------------------------------------------------------------+
19
20 $Id: rcube_imap.php 5347 2011-10-19 06:35:29Z alec $
21
22*/
23
24
25/**
26 * Class for accessing IMAP's SORT/SEARCH/ESEARCH result
27 */
28class rcube_result_index
29{
30    private $raw_data;
31    private $mailbox;
32    private $meta = array();
33    private $params = array();
34    private $order = 'ASC';
35
36    const SEPARATOR_ELEMENT = ' ';
37
38
39    /**
40     * Object constructor.
41     */
42    public function __construct($mailbox = null, $data = null)
43    {
44        $this->mailbox = $mailbox;
45        $this->init($data);
46    }
47
48
49    /**
50     * Initializes object with SORT command response
51     *
52     * @param string $data IMAP response string
53     */
54    public function init($data = null)
55    {
56        $this->meta = array();
57
58        $data = explode('*', (string)$data);
59
60        // ...skip unilateral untagged server responses
61        for ($i=0, $len=count($data); $i<$len; $i++) {
62            $data_item = &$data[$i];
63            if (preg_match('/^ SORT/i', $data_item)) {
64                $data_item = substr($data_item, 5);
65                break;
66            }
67            else if (preg_match('/^ (E?SEARCH)/i', $data_item, $m)) {
68                $data_item = substr($data_item, strlen($m[0]));
69
70                if (strtoupper($m[1]) == 'ESEARCH') {
71                    $data_item = trim($data_item);
72                    // remove MODSEQ response
73                    if (preg_match('/\(MODSEQ ([0-9]+)\)$/i', $data_item, $m)) {
74                        $data_item = substr($data_item, 0, -strlen($m[0]));
75                        $this->params['MODSEQ'] = $m[1];
76                    }
77                    // remove TAG response part
78                    if (preg_match('/^\(TAG ["a-z0-9]+\)\s*/i', $data_item, $m)) {
79                        $data_item = substr($data_item, strlen($m[0]));
80                    }
81                    // remove UID
82                    $data_item = preg_replace('/^UID\s*/i', '', $data_item);
83
84                    // ESEARCH parameters
85                    while (preg_match('/^([a-z]+) ([0-9:,]+)\s*/i', $data_item, $m)) {
86                        $param = strtoupper($m[1]);
87                        $value = $m[2];
88
89                        $this->params[strtoupper($m[1])] = $value;
90                        $data_item = substr($data_item, strlen($m[0]));
91
92                        if (in_array($param, array('COUNT', 'MIN', 'MAX'))) {
93                            $this->meta[strtolower($param)] = (int) $m[2];
94                        }
95                    }
96
97// @TODO: Implement compression using compressMessageSet() in __sleep() and __wakeup() ?
98// @TODO: work with compressed result?!
99                    if (isset($this->params['ALL'])) {
100                        $data[$idx] = implode(self::SEPARATOR_ELEMENT,
101                            rcube_imap_generic::uncompressMessageSet($this->params['ALL']));
102                    }
103                }
104
105                break;
106            }
107
108            unset($data[$i]);
109        }
110
111        if (empty($data)) {
112            return;
113        }
114
115        $data = array_shift($data);
116        $data = trim($data);
117        $data = preg_replace('/[\r\n]/', '', $data);
118        $data = preg_replace('/\s+/', ' ', $data);
119
120        $this->raw_data = $data;
121    }
122
123
124    /**
125     * Checks the result from IMAP command
126     *
127     * @return bool True if the result is an error, False otherwise
128     */
129    public function isError()
130    {
131        return $this->raw_data === null ? true : false;
132    }
133
134
135    /**
136     * Checks if the result is empty
137     *
138     * @return bool True if the result is empty, False otherwise
139     */
140    public function isEmpty()
141    {
142        return empty($this->raw_data) ? true : false;
143    }
144
145
146    /**
147     * Returns number of elements in the result
148     *
149     * @return int Number of elements
150     */
151    public function count()
152    {
153        if ($this->meta['count'] !== null)
154            return $this->meta['count'];
155
156        if (empty($this->raw_data)) {
157            $this->meta['count']  = 0;
158            $this->meta['length'] = 0;
159        }
160        else
161            // @TODO: check performance substr_count() vs. explode()
162            $this->meta['count'] = 1 + substr_count($this->raw_data, self::SEPARATOR_ELEMENT);
163
164        return $this->meta['count'];
165    }
166
167
168    /**
169     * Returns number of elements in the result.
170     * Alias for count() for compatibility with rcube_result_thread
171     *
172     * @return int Number of elements
173     */
174    public function countMessages()
175    {
176        return $this->count();
177    }
178
179
180    /**
181     * Returns maximal message identifier in the result
182     *
183     * @return int Maximal message identifier
184     */
185    public function max()
186    {
187        if (!isset($this->meta['max'])) {
188            // @TODO: do it by parsing raw_data?
189            $this->meta['max'] = (int) @max($this->get());
190        }
191
192        return $this->meta['max'];
193    }
194
195
196    /**
197     * Returns minimal message identifier in the result
198     *
199     * @return int Minimal message identifier
200     */
201    public function min()
202    {
203        if (!isset($this->meta['min'])) {
204            // @TODO: do it by parsing raw_data?
205            $this->meta['min'] = (int) @min($this->get());
206        }
207
208        return $this->meta['min'];
209    }
210
211
212    /**
213     * Slices data set.
214     *
215     * @param $offset Offset (as for PHP's array_slice())
216     * @param $length Number of elements (as for PHP's array_slice())
217     *
218     */
219    public function slice($offset, $length)
220    {
221        $data = $this->get();
222        $data = array_slice($data, $offset, $length);
223
224        $this->meta          = array();
225        $this->meta['count'] = count($data);
226        $this->raw_data      = implode(self::SEPARATOR_ELEMENT, $data);
227    }
228
229
230    /**
231     * Filters data set. Removes elements listed in $ids list.
232     *
233     * @param array $ids List of IDs to remove.
234     */
235    public function filter($ids = array())
236    {
237        $data = $this->get();
238        $data = array_diff($data, $ids);
239
240        $this->meta          = array();
241        $this->meta['count'] = count($data);
242        $this->raw_data      = implode(self::SEPARATOR_ELEMENT, $data);
243    }
244
245
246    /**
247     * Filters data set. Removes elements not listed in $ids list.
248     *
249     * @param array $ids List of IDs to keep.
250     */
251    public function intersect($ids = array())
252    {
253        $data = $this->get();
254        $data = array_intersect($data, $ids);
255
256        $this->meta          = array();
257        $this->meta['count'] = count($data);
258        $this->raw_data      = implode(self::SEPARATOR_ELEMENT, $data);
259    }
260
261
262    /**
263     * Reverts order of elements in the result
264     */
265    public function revert()
266    {
267        $this->order = $this->order == 'ASC' ? 'DESC' : 'ASC';
268
269        if (empty($this->raw_data)) {
270            return;
271        }
272
273        // @TODO: maybe do this in chunks
274        $data = $this->get();
275        $data = array_reverse($data);
276        $this->raw_data = implode(self::SEPARATOR_ELEMENT, $data);
277
278        $this->meta['pos'] = array();
279    }
280
281
282    /**
283     * Check if the given message ID exists in the object
284     *
285     * @param int  $msgid     Message ID
286     * @param bool $get_index When enabled element's index will be returned.
287     *                        Elements are indexed starting with 0
288     *
289     * @return mixed False if message ID doesn't exist, True if exists or
290     *               index of the element if $get_index=true
291     */
292    public function exists($msgid, $get_index = false)
293    {
294        if (empty($this->raw_data)) {
295            return false;
296        }
297
298        $msgid = (int) $msgid;
299        $begin = implode('|', array('^', preg_quote(self::SEPARATOR_ELEMENT, '/')));
300        $end   = implode('|', array('$', preg_quote(self::SEPARATOR_ELEMENT, '/')));
301
302        if (preg_match("/($begin)$msgid($end)/", $this->raw_data, $m,
303            $get_index ? PREG_OFFSET_CAPTURE : null)
304        ) {
305            if ($get_index) {
306                $idx = 0;
307                if ($m[0][1]) {
308                    $idx = 1 + substr_count($this->raw_data, self::SEPARATOR_ELEMENT, 0, $m[0][1]);
309                }
310                // cache position of this element, so we can use it in getElement()
311                $this->meta['pos'][$idx] = (int)$m[0][1];
312
313                return $idx;
314            }
315            return true;
316        }
317
318        return false;
319    }
320
321
322    /**
323     * Return all messages in the result.
324     *
325     * @return array List of message IDs
326     */
327    public function get()
328    {
329        if (empty($this->raw_data)) {
330            return array();
331        }
332        return explode(self::SEPARATOR_ELEMENT, $this->raw_data);
333    }
334
335
336    /**
337     * Return all messages in the result.
338     *
339     * @return array List of message IDs
340     */
341    public function getCompressed()
342    {
343        if (empty($this->raw_data)) {
344            return '';
345        }
346
347        return rcube_imap_generic::compressMessageSet($this->get());
348    }
349
350
351    /**
352     * Return result element at specified index
353     *
354     * @param int|string  $index  Element's index or "FIRST" or "LAST"
355     *
356     * @return int Element value
357     */
358    public function getElement($index)
359    {
360        $count = $this->count();
361
362        if (!$count) {
363            return null;
364        }
365
366        // first element
367        if ($index === 0 || $index === '0' || $index === 'FIRST') {
368            $pos = strpos($this->raw_data, self::SEPARATOR_ELEMENT);
369            if ($pos === false)
370                $result = (int) $this->raw_data;
371            else
372                $result = (int) substr($this->raw_data, 0, $pos);
373
374            return $result;
375        }
376
377        // last element
378        if ($index === 'LAST' || $index == $count-1) {
379            $pos = strrpos($this->raw_data, self::SEPARATOR_ELEMENT);
380            if ($pos === false)
381                $result = (int) $this->raw_data;
382            else
383                $result = (int) substr($this->raw_data, $pos);
384
385            return $result;
386        }
387
388        // do we know the position of the element or the neighbour of it?
389        if (!empty($this->meta['pos'])) {
390            if (isset($this->meta['pos'][$index]))
391                $pos = $this->meta['pos'][$index];
392            else if (isset($this->meta['pos'][$index-1]))
393                $pos = strpos($this->raw_data, self::SEPARATOR_ELEMENT,
394                    $this->meta['pos'][$index-1] + 1);
395            else if (isset($this->meta['pos'][$index+1]))
396                $pos = strrpos($this->raw_data, self::SEPARATOR_ELEMENT,
397                    $this->meta['pos'][$index+1] - $this->length() - 1);
398
399            if (isset($pos) && preg_match('/([0-9]+)/', $this->raw_data, $m, null, $pos)) {
400                return (int) $m[1];
401            }
402        }
403
404        // Finally use less effective method
405        $data = explode(self::SEPARATOR_ELEMENT, $this->raw_data);
406
407        return $data[$index];
408    }
409
410
411    /**
412     * Returns response parameters, e.g. ESEARCH's MIN/MAX/COUNT/ALL/MODSEQ
413     * or internal data e.g. MAILBOX, ORDER
414     *
415     * @param string $param  Parameter name
416     *
417     * @return array|string Response parameters or parameter value
418     */
419    public function getParameters($param=null)
420    {
421        $params = $this->params;
422        $params['MAILBOX'] = $this->mailbox;
423        $params['ORDER']   = $this->order;
424
425        if ($param !== null) {
426            return $params[$param];
427        }
428
429        return $params;
430    }
431
432
433    /**
434     * Returns length of internal data representation
435     *
436     * @return int Data length
437     */
438    private function length()
439    {
440        if (!isset($this->meta['length'])) {
441            $this->meta['length'] = strlen($this->raw_data);
442        }
443
444        return $this->meta['length'];
445    }
446}
Note: See TracBrowser for help on using the repository browser.