source: subversion/branches/devel-mcache/roundcubemail/program/include/rcube_imap.php @ 5193

Last change on this file since 5193 was 5193, checked in by alec, 21 months ago
  • Merge r5192 from trunk, I'll continue QRESYNC implementation here
  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
File size: 142.8 KB
Line 
1<?php
2
3/*
4 +-----------------------------------------------------------------------+
5 | program/include/rcube_imap.php                                        |
6 |                                                                       |
7 | This file is part of the Roundcube Webmail client                     |
8 | Copyright (C) 2005-2010, The Roundcube Dev Team                       |
9 | Licensed under the GNU GPL                                            |
10 |                                                                       |
11 | PURPOSE:                                                              |
12 |   IMAP Engine                                                         |
13 |                                                                       |
14 +-----------------------------------------------------------------------+
15 | Author: Thomas Bruederli <roundcube@gmail.com>                        |
16 | Author: Aleksander Machniak <alec@alec.pl>                            |
17 +-----------------------------------------------------------------------+
18
19 $Id$
20
21*/
22
23
24/**
25 * Interface class for accessing an IMAP server
26 *
27 * @package    Mail
28 * @author     Thomas Bruederli <roundcube@gmail.com>
29 * @author     Aleksander Machniak <alec@alec.pl>
30 * @version    2.0
31 */
32class rcube_imap
33{
34    public $debug_level = 1;
35    public $skip_deleted = false;
36    public $page_size = 10;
37    public $list_page = 1;
38    public $threading = false;
39    public $fetch_add_headers = '';
40    public $get_all_headers = false;
41
42    /**
43     * Instance of rcube_imap_generic
44     *
45     * @var rcube_imap_generic
46     */
47    public $conn;
48
49    /**
50     * Instance of rcube_imap_cache
51     *
52     * @var rcube_imap_cache
53     */
54    private $mcache;
55
56    /**
57     * Instance of rcube_cache
58     *
59     * @var rcube_cache
60     */
61    private $cache;
62
63    /**
64     * Internal (in-memory) cache
65     *
66     * @var array
67     */
68    private $icache = array();
69
70    private $mailbox = 'INBOX';
71    private $delimiter = NULL;
72    private $namespace = NULL;
73    private $sort_field = '';
74    private $sort_order = 'DESC';
75    private $default_charset = 'ISO-8859-1';
76    private $struct_charset = NULL;
77    private $default_folders = array('INBOX');
78    private $uid_id_map = array();
79    private $msg_headers = array();
80    public  $search_set = NULL;
81    public  $search_string = '';
82    private $search_charset = '';
83    private $search_sort_field = '';
84    private $search_threads = false;
85    private $search_sorted = false;
86    private $options = array('auth_method' => 'check');
87    private $host, $user, $pass, $port, $ssl;
88    private $caching = false;
89    private $messages_caching = false;
90
91    /**
92     * All (additional) headers used (in any way) by Roundcube
93     * Not listed here: DATE, FROM, TO, CC, REPLY-TO, SUBJECT, CONTENT-TYPE, LIST-POST
94     * (used for messages listing) are hardcoded in rcube_imap_generic::fetchHeaders()
95     *
96     * @var array
97     * @see rcube_imap::fetch_add_headers
98     */
99    private $all_headers = array(
100        'IN-REPLY-TO',
101        'BCC',
102        'MESSAGE-ID',
103        'CONTENT-TRANSFER-ENCODING',
104        'REFERENCES',
105        'X-PRIORITY',
106        'X-DRAFT-INFO',
107        'MAIL-FOLLOWUP-TO',
108        'MAIL-REPLY-TO',
109        'RETURN-PATH',
110    );
111
112    const UNKNOWN       = 0;
113    const NOPERM        = 1;
114    const READONLY      = 2;
115    const TRYCREATE     = 3;
116    const INUSE         = 4;
117    const OVERQUOTA     = 5;
118    const ALREADYEXISTS = 6;
119    const NONEXISTENT   = 7;
120    const CONTACTADMIN  = 8;
121
122
123    /**
124     * Object constructor.
125     */
126    function __construct()
127    {
128        $this->conn = new rcube_imap_generic();
129
130        // Set namespace and delimiter from session,
131        // so some methods would work before connection
132        if (isset($_SESSION['imap_namespace']))
133            $this->namespace = $_SESSION['imap_namespace'];
134        if (isset($_SESSION['imap_delimiter']))
135            $this->delimiter = $_SESSION['imap_delimiter'];
136    }
137
138
139    /**
140     * Connect to an IMAP server
141     *
142     * @param  string   $host    Host to connect
143     * @param  string   $user    Username for IMAP account
144     * @param  string   $pass    Password for IMAP account
145     * @param  integer  $port    Port to connect to
146     * @param  string   $use_ssl SSL schema (either ssl or tls) or null if plain connection
147     * @return boolean  TRUE on success, FALSE on failure
148     * @access public
149     */
150    function connect($host, $user, $pass, $port=143, $use_ssl=null)
151    {
152        // check for OpenSSL support in PHP build
153        if ($use_ssl && extension_loaded('openssl'))
154            $this->options['ssl_mode'] = $use_ssl == 'imaps' ? 'ssl' : $use_ssl;
155        else if ($use_ssl) {
156            raise_error(array('code' => 403, 'type' => 'imap',
157                'file' => __FILE__, 'line' => __LINE__,
158                'message' => "OpenSSL not available"), true, false);
159            $port = 143;
160        }
161
162        $this->options['port'] = $port;
163
164        if ($this->options['debug']) {
165            $this->conn->setDebug(true, array($this, 'debug_handler'));
166
167            $this->options['ident'] = array(
168                'name' => 'Roundcube Webmail',
169                'version' => RCMAIL_VERSION,
170                'php' => PHP_VERSION,
171                'os' => PHP_OS,
172                'command' => $_SERVER['REQUEST_URI'],
173            );
174        }
175
176        $attempt = 0;
177        do {
178            $data = rcmail::get_instance()->plugins->exec_hook('imap_connect',
179                array('host' => $host, 'user' => $user, 'attempt' => ++$attempt));
180
181            if (!empty($data['pass']))
182                $pass = $data['pass'];
183
184            $this->conn->connect($data['host'], $data['user'], $pass, $this->options);
185        } while(!$this->conn->connected() && $data['retry']);
186
187        $this->host = $data['host'];
188        $this->user = $data['user'];
189        $this->pass = $pass;
190        $this->port = $port;
191        $this->ssl  = $use_ssl;
192
193        if ($this->conn->connected()) {
194            // get namespace and delimiter
195            $this->set_env();
196            return true;
197        }
198        // write error log
199        else if ($this->conn->error) {
200            if ($pass && $user) {
201                $message = sprintf("Login failed for %s from %s. %s",
202                    $user, rcmail_remote_ip(), $this->conn->error);
203
204                raise_error(array('code' => 403, 'type' => 'imap',
205                    'file' => __FILE__, 'line' => __LINE__,
206                    'message' => $message), true, false);
207            }
208        }
209
210        return false;
211    }
212
213
214    /**
215     * Close IMAP connection
216     * Usually done on script shutdown
217     *
218     * @access public
219     */
220    function close()
221    {
222        $this->conn->closeConnection();
223        if ($this->mcache)
224            $this->mcache->close();
225    }
226
227
228    /**
229     * Close IMAP connection and re-connect
230     * This is used to avoid some strange socket errors when talking to Courier IMAP
231     *
232     * @access public
233     */
234    function reconnect()
235    {
236        $this->conn->closeConnection();
237        $connected = $this->connect($this->host, $this->user, $this->pass, $this->port, $this->ssl);
238
239        // issue SELECT command to restore connection status
240        if ($connected && strlen($this->mailbox))
241            $this->conn->select($this->mailbox);
242    }
243
244
245    /**
246     * Returns code of last error
247     *
248     * @return int Error code
249     */
250    function get_error_code()
251    {
252        return $this->conn->errornum;
253    }
254
255
256    /**
257     * Returns message of last error
258     *
259     * @return string Error message
260     */
261    function get_error_str()
262    {
263        return $this->conn->error;
264    }
265
266
267    /**
268     * Returns code of last command response
269     *
270     * @return int Response code
271     */
272    function get_response_code()
273    {
274        switch ($this->conn->resultcode) {
275            case 'NOPERM':
276                return self::NOPERM;
277            case 'READ-ONLY':
278                return self::READONLY;
279            case 'TRYCREATE':
280                return self::TRYCREATE;
281            case 'INUSE':
282                return self::INUSE;
283            case 'OVERQUOTA':
284                return self::OVERQUOTA;
285            case 'ALREADYEXISTS':
286                return self::ALREADYEXISTS;
287            case 'NONEXISTENT':
288                return self::NONEXISTENT;
289            case 'CONTACTADMIN':
290                return self::CONTACTADMIN;
291            default:
292                return self::UNKNOWN;
293        }
294    }
295
296
297    /**
298     * Returns last command response
299     *
300     * @return string Response
301     */
302    function get_response_str()
303    {
304        return $this->conn->result;
305    }
306
307
308    /**
309     * Set options to be used in rcube_imap_generic::connect()
310     *
311     * @param array $opt Options array
312     */
313    function set_options($opt)
314    {
315        $this->options = array_merge($this->options, (array)$opt);
316    }
317
318
319    /**
320     * Set default message charset
321     *
322     * This will be used for message decoding if a charset specification is not available
323     *
324     * @param  string $cs Charset string
325     * @access public
326     */
327    function set_charset($cs)
328    {
329        $this->default_charset = $cs;
330    }
331
332
333    /**
334     * This list of folders will be listed above all other folders
335     *
336     * @param  array $arr Indexed list of folder names
337     * @access public
338     */
339    function set_default_mailboxes($arr)
340    {
341        if (is_array($arr)) {
342            $this->default_folders = $arr;
343
344            // add inbox if not included
345            if (!in_array('INBOX', $this->default_folders))
346                array_unshift($this->default_folders, 'INBOX');
347        }
348    }
349
350
351    /**
352     * Set internal mailbox reference.
353     *
354     * All operations will be perfomed on this mailbox/folder
355     *
356     * @param  string $mailbox Mailbox/Folder name
357     * @access public
358     */
359    function set_mailbox($mailbox)
360    {
361        if ($this->mailbox == $mailbox)
362            return;
363
364        $this->mailbox = $mailbox;
365
366        // clear messagecount cache for this mailbox
367        $this->_clear_messagecount($mailbox);
368    }
369
370
371    /**
372     * Forces selection of a mailbox
373     *
374     * @param  string $mailbox Mailbox/Folder name
375     * @access public
376     */
377    function select_mailbox($mailbox=null)
378    {
379        if (!strlen($mailbox)) {
380            $mailbox = $this->mailbox;
381        }
382
383        $selected = $this->conn->select($mailbox);
384
385        if ($selected && $this->mailbox != $mailbox) {
386            // clear messagecount cache for this mailbox
387            $this->_clear_messagecount($mailbox);
388            $this->mailbox = $mailbox;
389        }
390    }
391
392
393    /**
394     * Set internal list page
395     *
396     * @param  number $page Page number to list
397     * @access public
398     */
399    function set_page($page)
400    {
401        $this->list_page = (int)$page;
402    }
403
404
405    /**
406     * Set internal page size
407     *
408     * @param  number $size Number of messages to display on one page
409     * @access public
410     */
411    function set_pagesize($size)
412    {
413        $this->page_size = (int)$size;
414    }
415
416
417    /**
418     * Save a set of message ids for future message listing methods
419     *
420     * @param  string  IMAP Search query
421     * @param  array   List of message ids or NULL if empty
422     * @param  string  Charset of search string
423     * @param  string  Sorting field
424     * @param  string  True if set is sorted (SORT was used for searching)
425     */
426    function set_search_set($str=null, $msgs=null, $charset=null, $sort_field=null, $threads=false, $sorted=false)
427    {
428        if (is_array($str) && $msgs == null)
429            list($str, $msgs, $charset, $sort_field, $threads) = $str;
430        if ($msgs === false)
431            $msgs = array();
432        else if ($msgs != null && !is_array($msgs))
433            $msgs = explode(',', $msgs);
434
435        $this->search_string     = $str;
436        $this->search_set        = $msgs;
437        $this->search_charset    = $charset;
438        $this->search_sort_field = $sort_field;
439        $this->search_threads    = $threads;
440        $this->search_sorted     = $sorted;
441    }
442
443
444    /**
445     * Return the saved search set as hash array
446     * @return array Search set
447     */
448    function get_search_set()
449    {
450        return array($this->search_string,
451                $this->search_set,
452                $this->search_charset,
453                $this->search_sort_field,
454                $this->search_threads,
455                $this->search_sorted,
456            );
457    }
458
459
460    /**
461     * Returns the currently used mailbox name
462     *
463     * @return  string Name of the mailbox/folder
464     * @access  public
465     */
466    function get_mailbox_name()
467    {
468        return $this->conn->connected() ? $this->mailbox : '';
469    }
470
471
472    /**
473     * Returns the IMAP server's capability
474     *
475     * @param   string  $cap Capability name
476     * @return  mixed   Capability value or TRUE if supported, FALSE if not
477     * @access  public
478     */
479    function get_capability($cap)
480    {
481        return $this->conn->getCapability(strtoupper($cap));
482    }
483
484
485    /**
486     * Sets threading flag to the best supported THREAD algorithm
487     *
488     * @param  boolean  $enable TRUE to enable and FALSE
489     * @return string   Algorithm or false if THREAD is not supported
490     * @access public
491     */
492    function set_threading($enable=false)
493    {
494        $this->threading = false;
495
496        if ($enable && ($caps = $this->get_capability('THREAD'))) {
497            if (in_array('REFS', $caps))
498                $this->threading = 'REFS';
499            else if (in_array('REFERENCES', $caps))
500                $this->threading = 'REFERENCES';
501            else if (in_array('ORDEREDSUBJECT', $caps))
502                $this->threading = 'ORDEREDSUBJECT';
503        }
504
505        return $this->threading;
506    }
507
508
509    /**
510     * Checks the PERMANENTFLAGS capability of the current mailbox
511     * and returns true if the given flag is supported by the IMAP server
512     *
513     * @param   string  $flag Permanentflag name
514     * @return  boolean True if this flag is supported
515     * @access  public
516     */
517    function check_permflag($flag)
518    {
519        $flag = strtoupper($flag);
520        $imap_flag = $this->conn->flags[$flag];
521        return (in_array_nocase($imap_flag, $this->conn->data['PERMANENTFLAGS']));
522    }
523
524
525    /**
526     * Returns the delimiter that is used by the IMAP server for folder separation
527     *
528     * @return  string  Delimiter string
529     * @access  public
530     */
531    function get_hierarchy_delimiter()
532    {
533        return $this->delimiter;
534    }
535
536
537    /**
538     * Get namespace
539     *
540     * @param string $name Namespace array index: personal, other, shared, prefix
541     *
542     * @return  array  Namespace data
543     * @access  public
544     */
545    function get_namespace($name=null)
546    {
547        $ns = $this->namespace;
548
549        if ($name) {
550            return isset($ns[$name]) ? $ns[$name] : null;
551        }
552
553        unset($ns['prefix']);
554        return $ns;
555    }
556
557
558    /**
559     * Sets delimiter and namespaces
560     *
561     * @access private
562     */
563    private function set_env()
564    {
565        if ($this->delimiter !== null && $this->namespace !== null) {
566            return;
567        }
568
569        $config = rcmail::get_instance()->config;
570        $imap_personal  = $config->get('imap_ns_personal');
571        $imap_other     = $config->get('imap_ns_other');
572        $imap_shared    = $config->get('imap_ns_shared');
573        $imap_delimiter = $config->get('imap_delimiter');
574
575        if (!$this->conn->connected())
576            return;
577
578        $ns = $this->conn->getNamespace();
579
580        // Set namespaces (NAMESPACE supported)
581        if (is_array($ns)) {
582            $this->namespace = $ns;
583        }
584        else {
585            $this->namespace = array(
586                'personal' => NULL,
587                'other'    => NULL,
588                'shared'   => NULL,
589            );
590        }
591
592        if ($imap_delimiter) {
593            $this->delimiter = $imap_delimiter;
594        }
595        if (empty($this->delimiter)) {
596            $this->delimiter = $this->namespace['personal'][0][1];
597        }
598        if (empty($this->delimiter)) {
599            $this->delimiter = $this->conn->getHierarchyDelimiter();
600        }
601        if (empty($this->delimiter)) {
602            $this->delimiter = '/';
603        }
604
605        // Overwrite namespaces
606        if ($imap_personal !== null) {
607            $this->namespace['personal'] = NULL;
608            foreach ((array)$imap_personal as $dir) {
609                $this->namespace['personal'][] = array($dir, $this->delimiter);
610            }
611        }
612        if ($imap_other !== null) {
613            $this->namespace['other'] = NULL;
614            foreach ((array)$imap_other as $dir) {
615                if ($dir) {
616                    $this->namespace['other'][] = array($dir, $this->delimiter);
617                }
618            }
619        }
620        if ($imap_shared !== null) {
621            $this->namespace['shared'] = NULL;
622            foreach ((array)$imap_shared as $dir) {
623                if ($dir) {
624                    $this->namespace['shared'][] = array($dir, $this->delimiter);
625                }
626            }
627        }
628
629        // Find personal namespace prefix for mod_mailbox()
630        // Prefix can be removed when there is only one personal namespace
631        if (is_array($this->namespace['personal']) && count($this->namespace['personal']) == 1) {
632            $this->namespace['prefix'] = $this->namespace['personal'][0][0];
633        }
634
635        $_SESSION['imap_namespace'] = $this->namespace;
636        $_SESSION['imap_delimiter'] = $this->delimiter;
637    }
638
639
640    /**
641     * Get message count for a specific mailbox
642     *
643     * @param  string  $mailbox Mailbox/folder name
644     * @param  string  $mode    Mode for count [ALL|THREADS|UNSEEN|RECENT]
645     * @param  boolean $force   Force reading from server and update cache
646     * @param  boolean $status  Enables storing folder status info (max UID/count),
647     *                          required for mailbox_status()
648     * @return int     Number of messages
649     * @access public
650     */
651    function messagecount($mailbox='', $mode='ALL', $force=false, $status=true)
652    {
653        if (!strlen($mailbox)) {
654            $mailbox = $this->mailbox;
655        }
656
657        return $this->_messagecount($mailbox, $mode, $force, $status);
658    }
659
660
661    /**
662     * Private method for getting nr of messages
663     *
664     * @param string  $mailbox Mailbox name
665     * @param string  $mode    Mode for count [ALL|THREADS|UNSEEN|RECENT]
666     * @param boolean $force   Force reading from server and update cache
667     * @param boolean $status  Enables storing folder status info (max UID/count),
668     *                         required for mailbox_status()
669     * @return int Number of messages
670     * @access  private
671     * @see     rcube_imap::messagecount()
672     */
673    private function _messagecount($mailbox, $mode='ALL', $force=false, $status=true)
674    {
675        $mode = strtoupper($mode);
676
677        // count search set
678        if ($this->search_string && $mailbox == $this->mailbox && ($mode == 'ALL' || $mode == 'THREADS') && !$force) {
679            if ($this->search_threads)
680                return $mode == 'ALL' ? count((array)$this->search_set['depth']) : count((array)$this->search_set['tree']);
681            else
682                return count((array)$this->search_set);
683        }
684
685        $a_mailbox_cache = $this->get_cache('messagecount');
686
687        // return cached value
688        if (!$force && is_array($a_mailbox_cache[$mailbox]) && isset($a_mailbox_cache[$mailbox][$mode]))
689            return $a_mailbox_cache[$mailbox][$mode];
690
691        if (!is_array($a_mailbox_cache[$mailbox]))
692            $a_mailbox_cache[$mailbox] = array();
693
694        if ($mode == 'THREADS') {
695            $res   = $this->_threadcount($mailbox, $msg_count);
696            $count = $res['count'];
697
698            if ($status) {
699                $this->set_folder_stats($mailbox, 'cnt', $res['msgcount']);
700                $this->set_folder_stats($mailbox, 'maxuid', $res['maxuid'] ? $this->id2uid($res['maxuid'], $mailbox) : 0);
701            }
702        }
703        // RECENT count is fetched a bit different
704        else if ($mode == 'RECENT') {
705            $count = $this->conn->countRecent($mailbox);
706        }
707        // use SEARCH for message counting
708        else if ($this->skip_deleted) {
709            $search_str = "ALL UNDELETED";
710            $keys       = array('COUNT');
711            $need_uid   = false;
712
713            if ($mode == 'UNSEEN') {
714                $search_str .= " UNSEEN";
715            }
716            else {
717                if ($this->messages_caching) {
718                    $keys[] = 'ALL';
719                }
720                if ($status) {
721                    $keys[]   = 'MAX';
722                    $need_uid = true;
723                }
724            }
725
726            // get message count using (E)SEARCH
727            // not very performant but more precise (using UNDELETED)
728            $index = $this->conn->search($mailbox, $search_str, $need_uid, $keys);
729
730            $count = is_array($index) ? $index['COUNT'] : 0;
731
732            if ($mode == 'ALL') {
733                if ($this->messages_caching) {
734                    // Save additional info required by cache status check
735                    $this->icache['undeleted_idx'] = array($mailbox, $index['ALL'], $index['COUNT']);
736                }
737                if ($status) {
738                    $this->set_folder_stats($mailbox, 'cnt', $count);
739                    $this->set_folder_stats($mailbox, 'maxuid', is_array($index) ? $index['MAX'] : 0);
740                }
741            }
742        }
743        else {
744            if ($mode == 'UNSEEN')
745                $count = $this->conn->countUnseen($mailbox);
746            else {
747                $count = $this->conn->countMessages($mailbox);
748                if ($status) {
749                    $this->set_folder_stats($mailbox,'cnt', $count);
750                    $this->set_folder_stats($mailbox, 'maxuid', $count ? $this->id2uid($count, $mailbox) : 0);
751                }
752            }
753        }
754
755        $a_mailbox_cache[$mailbox][$mode] = (int)$count;
756
757        // write back to cache
758        $this->update_cache('messagecount', $a_mailbox_cache);
759
760        return (int)$count;
761    }
762
763
764    /**
765     * Private method for getting nr of threads
766     *
767     * @param string $mailbox   Folder name
768     *
769     * @returns array Array containing items: 'count' - threads count,
770     *                'msgcount' = messages count, 'maxuid' = max. UID in the set
771     * @access  private
772     */
773    private function _threadcount($mailbox)
774    {
775        $result = array();
776
777        if (!empty($this->icache['threads'])) {
778            $dcount = count($this->icache['threads']['depth']);
779            $result = array(
780                'count'    => count($this->icache['threads']['tree']),
781                'msgcount' => $dcount,
782                'maxuid'   => $dcount ? max(array_keys($this->icache['threads']['depth'])) : 0,
783            );
784        }
785        else if (is_array($result = $this->fetch_threads($mailbox))) {
786            $dcount = count($result[1]);
787            $result = array(
788                'count'    => count($result[0]),
789                'msgcount' => $dcount,
790                'maxuid'   => $dcount ? max(array_keys($result[1])) : 0,
791            );
792        }
793
794        return $result;
795    }
796
797
798    /**
799     * Public method for listing headers
800     * convert mailbox name with root dir first
801     *
802     * @param   string   $mailbox    Mailbox/folder name
803     * @param   int      $page       Current page to list
804     * @param   string   $sort_field Header field to sort by
805     * @param   string   $sort_order Sort order [ASC|DESC]
806     * @param   int      $slice      Number of slice items to extract from result array
807     * @return  array    Indexed array with message header objects
808     * @access  public
809     */
810    function list_headers($mailbox='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
811    {
812        if (!strlen($mailbox)) {
813            $mailbox = $this->mailbox;
814        }
815
816        return $this->_list_headers($mailbox, $page, $sort_field, $sort_order, false, $slice);
817    }
818
819
820    /**
821     * Private method for listing message headers
822     *
823     * @param   string   $mailbox    Mailbox name
824     * @param   int      $page       Current page to list
825     * @param   string   $sort_field Header field to sort by
826     * @param   string   $sort_order Sort order [ASC|DESC]
827     * @param   int      $slice      Number of slice items to extract from result array
828     *
829     * @return  array    Indexed array with message header objects
830     * @see     rcube_imap::list_headers
831     */
832    private function _list_headers($mailbox='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
833    {
834        if (!strlen($mailbox))
835            return array();
836
837        // use saved message set
838        if ($this->search_string && $mailbox == $this->mailbox)
839            return $this->_list_header_set($mailbox, $page, $sort_field, $sort_order, $slice);
840
841        if ($this->threading)
842            return $this->_list_thread_headers($mailbox, $page, $sort_field, $sort_order, $slice);
843
844        $this->_set_sort_order($sort_field, $sort_order);
845
846        $page = $page ? $page : $this->list_page;
847
848        // Use messages cache
849        if ($mcache = $this->get_mcache_engine()) {
850            $msg_index = $mcache->get_index($mailbox, $this->sort_field, $this->sort_order);
851
852            if (empty($msg_index))
853                return array();
854
855            $from      = ($page-1) * $this->page_size;
856            $to        = $from + $this->page_size;
857            $msg_index = array_values($msg_index); // UIDs
858            $is_uid    = true;
859            $sorted    = true;
860
861            if ($from || $to)
862                $msg_index = array_slice($msg_index, $from, $to - $from);
863
864            if ($slice)
865                $msg_index = array_slice($msg_index, -$slice, $slice);
866
867            $a_msg_headers = $mcache->get_messages($mailbox, $msg_index);
868        }
869        // retrieve headers from IMAP
870        // use message index sort as default sorting (for better performance)
871        else if (!$this->sort_field) {
872            if ($this->skip_deleted) {
873                // @TODO: this could be cached
874                if ($msg_index = $this->_search_index($mailbox, 'ALL UNDELETED')) {
875                    list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
876                    $msg_index = array_slice($msg_index, $begin, $end-$begin);
877                }
878            }
879            else if ($max = $this->conn->countMessages($mailbox)) {
880                list($begin, $end) = $this->_get_message_range($max, $page);
881                $msg_index = range($begin+1, $end);
882            }
883            else
884                $msg_index = array();
885
886            if ($slice && $msg_index)
887                $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
888
889            // fetch reqested headers from server
890            if ($msg_index)
891                $a_msg_headers = $this->fetch_headers($mailbox, $msg_index);
892        }
893        // use SORT command
894        else if ($this->get_capability('SORT') &&
895            // Courier-IMAP provides SORT capability but allows to disable it by admin (#1486959)
896            ($msg_index = $this->conn->sort($mailbox, $this->sort_field,
897                $this->skip_deleted ? 'UNDELETED' : '', true)) !== false
898        ) {
899            if (!empty($msg_index)) {
900                list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
901                $msg_index = array_slice($msg_index, $begin, $end-$begin);
902                $is_uid    = true;
903
904                if ($slice)
905                    $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
906
907                // fetch reqested headers from server
908                $a_msg_headers = $this->fetch_headers($mailbox, $msg_index, true);
909            }
910        }
911        // fetch specified header for all messages and sort
912        else if ($msg_index = $this->conn->fetchHeaderIndex($mailbox, "1:*",
913            $this->sort_field, $this->skip_deleted, true)
914        ) {
915            asort($msg_index); // ASC
916            $msg_index = array_keys($msg_index);
917            list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
918            $msg_index = array_slice($msg_index, $begin, $end-$begin);
919            $is_uid    = true;
920
921            if ($slice)
922                $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
923
924            // fetch reqested headers from server
925            $a_msg_headers = $this->fetch_headers($mailbox, $msg_index, true);
926        }
927
928        // return empty array if no messages found
929        if (!is_array($a_msg_headers) || empty($a_msg_headers))
930            return array();
931
932        // use this class for message sorting
933        $sorter = new rcube_header_sorter();
934        $sorter->set_index($msg_index, $is_uid);
935        $sorter->sort_headers($a_msg_headers);
936
937        if ($this->sort_order == 'DESC' && !$sorted)
938            $a_msg_headers = array_reverse($a_msg_headers);
939
940        return array_values($a_msg_headers);
941    }
942
943
944    /**
945     * Private method for listing message headers using threads
946     *
947     * @param   string   $mailbox    Mailbox/folder name
948     * @param   int      $page       Current page to list
949     * @param   string   $sort_field Header field to sort by
950     * @param   string   $sort_order Sort order [ASC|DESC]
951     * @param   int      $slice      Number of slice items to extract from result array
952     *
953     * @return  array    Indexed array with message header objects
954     * @see     rcube_imap::list_headers
955     */
956    private function _list_thread_headers($mailbox, $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
957    {
958        $this->_set_sort_order($sort_field, $sort_order);
959
960        $page   = $page ? $page : $this->list_page;
961        $mcache = $this->get_mcache_engine();
962
963        // get all threads (not sorted)
964        if ($mcache)
965            list ($thread_tree, $msg_depth, $has_children) = $mcache->get_thread($mailbox);
966        else
967            list ($thread_tree, $msg_depth, $has_children) = $this->fetch_threads($mailbox);
968
969        if (empty($thread_tree))
970            return array();
971
972        $msg_index = $this->sort_threads($mailbox, $thread_tree);
973
974        return $this->_fetch_thread_headers($mailbox,
975            $thread_tree, $msg_depth, $has_children, $msg_index, $page, $slice);
976    }
977
978
979    /**
980     * Method for fetching threads data
981     *
982     * @param  string $mailbox  Folder name
983     * @param  bool   $force    Use IMAP server, no cache
984     *
985     * @return  array    Array with thread data
986     */
987    function fetch_threads($mailbox, $force = false)
988    {
989        if (!$force && ($mcache = $this->get_mcache_engine())) {
990            // don't store in self's internal cache, cache has it's own internal cache
991            return $mcache->get_thread($mailbox);
992        }
993
994        if (empty($this->icache['threads'])) {
995            // get all threads
996            $result = $this->conn->thread($mailbox, $this->threading,
997                $this->skip_deleted ? 'UNDELETED' : '');
998
999            // add to internal (fast) cache
1000            $this->icache['threads'] = array();
1001            $this->icache['threads']['tree'] = is_array($result) ? $result[0] : array();
1002            $this->icache['threads']['depth'] = is_array($result) ? $result[1] : array();
1003            $this->icache['threads']['has_children'] = is_array($result) ? $result[2] : array();
1004        }
1005
1006        return array(
1007            $this->icache['threads']['tree'],
1008            $this->icache['threads']['depth'],
1009            $this->icache['threads']['has_children'],
1010        );
1011    }
1012
1013
1014    /**
1015     * Private method for fetching threaded messages headers
1016     *
1017     * @param string  $mailbox      Mailbox name
1018     * @param array   $thread_tree  Thread tree data
1019     * @param array   $msg_depth    Thread depth data
1020     * @param array   $has_children Thread children data
1021     * @param array   $msg_index    Messages index
1022     * @param int     $page         List page number
1023     * @param int     $slice        Number of threads to slice
1024     *
1025     * @return array  Messages headers
1026     * @access  private
1027     */
1028    private function _fetch_thread_headers($mailbox, $thread_tree, $msg_depth, $has_children, $msg_index, $page, $slice=0)
1029    {
1030        // now get IDs for current page
1031        list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
1032        $msg_index = array_slice($msg_index, $begin, $end-$begin);
1033
1034        if ($slice)
1035            $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
1036
1037        if ($this->sort_order == 'DESC')
1038            $msg_index = array_reverse($msg_index);
1039
1040        // flatten threads array
1041        // @TODO: fetch children only in expanded mode (?)
1042        $all_ids = array();
1043        foreach ($msg_index as $root) {
1044            $all_ids[] = $root;
1045            if (!empty($thread_tree[$root]))
1046                $all_ids = array_merge($all_ids, array_keys_recursive($thread_tree[$root]));
1047        }
1048
1049        // fetch reqested headers from server
1050        $a_msg_headers = $this->fetch_headers($mailbox, $all_ids);
1051
1052        // return empty array if no messages found
1053        if (!is_array($a_msg_headers) || empty($a_msg_headers))
1054            return array();
1055
1056        // use this class for message sorting
1057        $sorter = new rcube_header_sorter();
1058        $sorter->set_index($all_ids);
1059        $sorter->sort_headers($a_msg_headers);
1060
1061        // Set depth, has_children and unread_children fields in headers
1062        $this->_set_thread_flags($a_msg_headers, $msg_depth, $has_children);
1063
1064        return array_values($a_msg_headers);
1065    }
1066
1067
1068    /**
1069     * Private method for setting threaded messages flags:
1070     * depth, has_children and unread_children
1071     *
1072     * @param  array  $headers      Reference to headers array indexed by message ID
1073     * @param  array  $msg_depth    Array of messages depth indexed by message ID
1074     * @param  array  $msg_children Array of messages children flags indexed by message ID
1075     * @return array   Message headers array indexed by message ID
1076     * @access private
1077     */
1078    private function _set_thread_flags(&$headers, $msg_depth, $msg_children)
1079    {
1080        $parents = array();
1081
1082        foreach ($headers as $idx => $header) {
1083            $id = $header->id;
1084            $depth = $msg_depth[$id];
1085            $parents = array_slice($parents, 0, $depth);
1086
1087            if (!empty($parents)) {
1088                $headers[$idx]->parent_uid = end($parents);
1089                if (!$header->seen)
1090                    $headers[$parents[0]]->unread_children++;
1091            }
1092            array_push($parents, $header->uid);
1093
1094            $headers[$idx]->depth = $depth;
1095            $headers[$idx]->has_children = $msg_children[$id];
1096        }
1097    }
1098
1099
1100    /**
1101     * Private method for listing a set of message headers (search results)
1102     *
1103     * @param   string   $mailbox    Mailbox/folder name
1104     * @param   int      $page       Current page to list
1105     * @param   string   $sort_field Header field to sort by
1106     * @param   string   $sort_order Sort order [ASC|DESC]
1107     * @param   int  $slice      Number of slice items to extract from result array
1108     * @return  array    Indexed array with message header objects
1109     * @access  private
1110     * @see     rcube_imap::list_header_set()
1111     */
1112    private function _list_header_set($mailbox, $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
1113    {
1114        if (!strlen($mailbox) || empty($this->search_set))
1115            return array();
1116
1117        // use saved messages from searching
1118        if ($this->threading)
1119            return $this->_list_thread_header_set($mailbox, $page, $sort_field, $sort_order, $slice);
1120
1121        // search set is threaded, we need a new one
1122        if ($this->search_threads) {
1123            if (empty($this->search_set['tree']))
1124                return array();
1125            $this->search('', $this->search_string, $this->search_charset, $sort_field);
1126        }
1127
1128        $msgs = $this->search_set;
1129        $a_msg_headers = array();
1130        $page = $page ? $page : $this->list_page;
1131        $start_msg = ($page-1) * $this->page_size;
1132
1133        $this->_set_sort_order($sort_field, $sort_order);
1134
1135        // quickest method (default sorting)
1136        if (!$this->search_sort_field && !$this->sort_field) {
1137            if ($sort_order == 'DESC')
1138                $msgs = array_reverse($msgs);
1139
1140            // get messages uids for one page
1141            $msgs = array_slice(array_values($msgs), $start_msg, min(count($msgs)-$start_msg, $this->page_size));
1142
1143            if ($slice)
1144                $msgs = array_slice($msgs, -$slice, $slice);
1145
1146            // fetch headers
1147            $a_msg_headers = $this->fetch_headers($mailbox, $msgs);
1148
1149            // I didn't found in RFC that FETCH always returns messages sorted by index
1150            $sorter = new rcube_header_sorter();
1151            $sorter->set_index($msgs);
1152            $sorter->sort_headers($a_msg_headers);
1153
1154            return array_values($a_msg_headers);
1155        }
1156
1157        // sorted messages, so we can first slice array and then fetch only wanted headers
1158        if ($this->search_sorted) { // SORT searching result
1159            // reset search set if sorting field has been changed
1160            if ($this->sort_field && $this->search_sort_field != $this->sort_field)
1161                $msgs = $this->search('', $this->search_string, $this->search_charset, $this->sort_field);
1162
1163            // return empty array if no messages found
1164            if (empty($msgs))
1165                return array();
1166
1167            if ($sort_order == 'DESC')
1168                $msgs = array_reverse($msgs);
1169
1170            // get messages uids for one page
1171            $msgs = array_slice(array_values($msgs), $start_msg, min(count($msgs)-$start_msg, $this->page_size));
1172
1173            if ($slice)
1174                $msgs = array_slice($msgs, -$slice, $slice);
1175
1176            // fetch headers
1177            $a_msg_headers = $this->fetch_headers($mailbox, $msgs);
1178
1179            $sorter = new rcube_header_sorter();
1180            $sorter->set_index($msgs);
1181            $sorter->sort_headers($a_msg_headers);
1182
1183            return array_values($a_msg_headers);
1184        }
1185        else { // SEARCH result, need sorting
1186            $cnt = count($msgs);
1187            // 300: experimantal value for best result
1188            if (($cnt > 300 && $cnt > $this->page_size) || !$this->sort_field) {
1189                // use memory less expensive (and quick) method for big result set
1190                $a_index = $this->message_index('', $this->sort_field, $this->sort_order);
1191                // get messages uids for one page...
1192                $msgs = array_slice($a_index, $start_msg, min($cnt-$start_msg, $this->page_size));
1193                if ($slice)
1194                    $msgs = array_slice($msgs, -$slice, $slice);
1195                // ...and fetch headers
1196                $a_msg_headers = $this->fetch_headers($mailbox, $msgs);
1197
1198
1199                // return empty array if no messages found
1200                if (!is_array($a_msg_headers) || empty($a_msg_headers))
1201                    return array();
1202
1203                $sorter = new rcube_header_sorter();
1204                $sorter->set_index($msgs);
1205                $sorter->sort_headers($a_msg_headers);
1206
1207                return array_values($a_msg_headers);
1208            }
1209            else {
1210                // for small result set we can fetch all messages headers
1211                $a_msg_headers = $this->fetch_headers($mailbox, $msgs);
1212
1213                // return empty array if no messages found
1214                if (!is_array($a_msg_headers) || empty($a_msg_headers))
1215                    return array();
1216
1217                // if not already sorted
1218                $a_msg_headers = $this->conn->sortHeaders(
1219                    $a_msg_headers, $this->sort_field, $this->sort_order);
1220
1221                // only return the requested part of the set
1222                $a_msg_headers = array_slice(array_values($a_msg_headers),
1223                    $start_msg, min($cnt-$start_msg, $this->page_size));
1224
1225                if ($slice)
1226                    $a_msg_headers = array_slice($a_msg_headers, -$slice, $slice);
1227
1228                return $a_msg_headers;
1229            }
1230        }
1231    }
1232
1233
1234    /**
1235     * Private method for listing a set of threaded message headers (search results)
1236     *
1237     * @param   string   $mailbox    Mailbox/folder name
1238     * @param   int      $page       Current page to list
1239     * @param   string   $sort_field Header field to sort by
1240     * @param   string   $sort_order Sort order [ASC|DESC]
1241     * @param   int      $slice      Number of slice items to extract from result array
1242     * @return  array    Indexed array with message header objects
1243     * @access  private
1244     * @see     rcube_imap::list_header_set()
1245     */
1246    private function _list_thread_header_set($mailbox, $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
1247    {
1248        // update search_set if previous data was fetched with disabled threading
1249        if (!$this->search_threads) {
1250            if (empty($this->search_set))
1251                return array();
1252            $this->search('', $this->search_string, $this->search_charset, $sort_field);
1253        }
1254
1255        // empty result
1256        if (empty($this->search_set['tree']))
1257            return array();
1258
1259        $thread_tree = $this->search_set['tree'];
1260        $msg_depth = $this->search_set['depth'];
1261        $has_children = $this->search_set['children'];
1262        $a_msg_headers = array();
1263
1264        $page = $page ? $page : $this->list_page;
1265        $start_msg = ($page-1) * $this->page_size;
1266
1267        $this->_set_sort_order($sort_field, $sort_order);
1268
1269        $msg_index = $this->sort_threads($mailbox, $thread_tree, array_keys($msg_depth));
1270
1271        return $this->_fetch_thread_headers($mailbox,
1272            $thread_tree, $msg_depth, $has_children, $msg_index, $page, $slice=0);
1273    }
1274
1275
1276    /**
1277     * Helper function to get first and last index of the requested set
1278     *
1279     * @param  int     $max  Messages count
1280     * @param  mixed   $page Page number to show, or string 'all'
1281     * @return array   Array with two values: first index, last index
1282     * @access private
1283     */
1284    private function _get_message_range($max, $page)
1285    {
1286        $start_msg = ($page-1) * $this->page_size;
1287
1288        if ($page=='all') {
1289            $begin  = 0;
1290            $end    = $max;
1291        }
1292        else if ($this->sort_order=='DESC') {
1293            $begin  = $max - $this->page_size - $start_msg;
1294            $end    = $max - $start_msg;
1295        }
1296        else {
1297            $begin  = $start_msg;
1298            $end    = $start_msg + $this->page_size;
1299        }
1300
1301        if ($begin < 0) $begin = 0;
1302        if ($end < 0) $end = $max;
1303        if ($end > $max) $end = $max;
1304
1305        return array($begin, $end);
1306    }
1307
1308
1309    /**
1310     * Fetches messages headers
1311     *
1312     * @param  string  $mailbox  Mailbox name
1313     * @param  array   $msgs     Messages sequence numbers
1314     * @param  bool    $is_uid   Enable if $msgs numbers are UIDs
1315     * @param  bool    $force    Disables cache use
1316     *
1317     * @return array Messages headers indexed by UID
1318     * @access private
1319     */
1320    function fetch_headers($mailbox, $msgs, $is_uid = false, $force = false)
1321    {
1322        if (empty($msgs))
1323            return array();
1324
1325        if (!$force && ($mcache = $this->get_mcache_engine())) {
1326            return $mcache->get_messages($mailbox, $msgs, $is_uid);
1327        }
1328
1329        // fetch reqested headers from server
1330        $index = $this->conn->fetchHeaders(
1331            $mailbox, $msgs, $is_uid, false, $this->get_fetch_headers());
1332
1333        if (empty($index))
1334            return array();
1335
1336        foreach ($index as $headers) {
1337            $a_msg_headers[$headers->uid] = $headers;
1338        }
1339
1340        return $a_msg_headers;
1341    }
1342
1343
1344    /**
1345     * Returns current status of mailbox
1346     *
1347     * We compare the maximum UID to determine the number of
1348     * new messages because the RECENT flag is not reliable.
1349     *
1350     * @param string $mailbox Mailbox/folder name
1351     * @return int   Folder status
1352     */
1353    function mailbox_status($mailbox = null)
1354    {
1355        if (!strlen($mailbox)) {
1356            $mailbox = $this->mailbox;
1357        }
1358        $old = $this->get_folder_stats($mailbox);
1359
1360        // refresh message count -> will update
1361        $this->_messagecount($mailbox, 'ALL', true);
1362
1363        $result = 0;
1364        $new = $this->get_folder_stats($mailbox);
1365
1366        // got new messages
1367        if ($new['maxuid'] > $old['maxuid'])
1368            $result += 1;
1369        // some messages has been deleted
1370        if ($new['cnt'] < $old['cnt'])
1371            $result += 2;
1372
1373        // @TODO: optional checking for messages flags changes (?)
1374        // @TODO: UIDVALIDITY checking
1375
1376        return $result;
1377    }
1378
1379
1380    /**
1381     * Stores folder statistic data in session
1382     * @TODO: move to separate DB table (cache?)
1383     *
1384     * @param string $mailbox Mailbox name
1385     * @param string $name    Data name
1386     * @param mixed  $data    Data value
1387     */
1388    private function set_folder_stats($mailbox, $name, $data)
1389    {
1390        $_SESSION['folders'][$mailbox][$name] = $data;
1391    }
1392
1393
1394    /**
1395     * Gets folder statistic data
1396     *
1397     * @param string $mailbox Mailbox name
1398     *
1399     * @return array Stats data
1400     */
1401    private function get_folder_stats($mailbox)
1402    {
1403        if ($_SESSION['folders'][$mailbox])
1404            return (array) $_SESSION['folders'][$mailbox];
1405        else
1406            return array();
1407    }
1408
1409
1410    /**
1411     * Return sorted array of message IDs (not UIDs)
1412     *
1413     * @param string $mailbox    Mailbox to get index from
1414     * @param string $sort_field Sort column
1415     * @param string $sort_order Sort order [ASC, DESC]
1416     * @return array Indexed array with message IDs
1417     */
1418    function message_index($mailbox='', $sort_field=NULL, $sort_order=NULL)
1419    {
1420        if ($this->threading)
1421            return $this->thread_index($mailbox, $sort_field, $sort_order);
1422
1423        $this->_set_sort_order($sort_field, $sort_order);
1424
1425        if (!strlen($mailbox)) {
1426            $mailbox = $this->mailbox;
1427        }
1428        $key = "{$mailbox}:{$this->sort_field}:{$this->sort_order}:{$this->search_string}.msgi";
1429
1430        // we have a saved search result, get index from there
1431        if (!isset($this->icache[$key]) && $this->search_string
1432            && !$this->search_threads && $mailbox == $this->mailbox) {
1433            // use message index sort as default sorting
1434            if (!$this->sort_field) {
1435                $msgs = $this->search_set;
1436
1437                if ($this->search_sort_field != 'date')
1438                    sort($msgs);
1439
1440                if ($this->sort_order == 'DESC')
1441                    $this->icache[$key] = array_reverse($msgs);
1442                else
1443                    $this->icache[$key] = $msgs;
1444            }
1445            // sort with SORT command
1446            else if ($this->search_sorted) {
1447                if ($this->sort_field && $this->search_sort_field != $this->sort_field)
1448                    $this->search('', $this->search_string, $this->search_charset, $this->sort_field);
1449
1450                if ($this->sort_order == 'DESC')
1451                    $this->icache[$key] = array_reverse($this->search_set);
1452                else
1453                    $this->icache[$key] = $this->search_set;
1454            }
1455            else {
1456                $a_index = $this->conn->fetchHeaderIndex($mailbox,
1457                    join(',', $this->search_set), $this->sort_field, $this->skip_deleted);
1458
1459                if (is_array($a_index)) {
1460                    if ($this->sort_order=="ASC")
1461                        asort($a_index);
1462                    else if ($this->sort_order=="DESC")
1463                        arsort($a_index);
1464
1465                    $this->icache[$key] = array_keys($a_index);
1466                }
1467                else {
1468                    $this->icache[$key] = array();
1469                }
1470            }
1471        }
1472
1473        // have stored it in RAM
1474        if (isset($this->icache[$key]))
1475            return $this->icache[$key];
1476
1477        // check local cache
1478        if ($mcache = $this->get_mcache_engine()) {
1479            $a_index = $mcache->get_index($mailbox, $this->sort_field, $this->sort_order);
1480            $this->icache[$key] = array_keys($a_index);
1481        }
1482        // fetch from IMAP server
1483        else {
1484            $this->icache[$key] = $this->message_index_direct(
1485                $mailbox, $this->sort_field, $this->sort_order);
1486        }
1487
1488        return $this->icache[$key];
1489    }
1490
1491
1492    /**
1493     * Return sorted array of message IDs (not UIDs) directly from IMAP server.
1494     * Doesn't use cache and ignores current search settings.
1495     *
1496     * @param string $mailbox    Mailbox to get index from
1497     * @param string $sort_field Sort column
1498     * @param string $sort_order Sort order [ASC, DESC]
1499     *
1500     * @return array Indexed array with message IDs
1501     */
1502    function message_index_direct($mailbox, $sort_field = null, $sort_order = null)
1503    {
1504        // use message index sort as default sorting
1505        if (!$sort_field) {
1506            if ($this->skip_deleted) {
1507                $a_index = $this->_search_index($mailbox, 'ALL');
1508            } else if ($max = $this->_messagecount($mailbox)) {
1509                $a_index = range(1, $max);
1510            }
1511
1512            if ($a_index !== false && $sort_order == 'DESC')
1513                $a_index = array_reverse($a_index);
1514        }
1515        // fetch complete message index
1516        else if ($this->get_capability('SORT') &&
1517            ($a_index = $this->conn->sort($mailbox,
1518                $sort_field, $this->skip_deleted ? 'UNDELETED' : '')) !== false
1519        ) {
1520            if ($sort_order == 'DESC')
1521                $a_index = array_reverse($a_index);
1522        }
1523        else if ($a_index = $this->conn->fetchHeaderIndex(
1524            $mailbox, "1:*", $sort_field, $skip_deleted)) {
1525            if ($sort_order=="ASC")
1526                asort($a_index);
1527            else if ($sort_order=="DESC")
1528                arsort($a_index);
1529
1530            $a_index = array_keys($a_index);
1531        }
1532
1533        return $a_index !== false ? $a_index : array();
1534    }
1535
1536
1537    /**
1538     * Return sorted array of threaded message IDs (not UIDs)
1539     *
1540     * @param string $mailbox    Mailbox to get index from
1541     * @param string $sort_field Sort column
1542     * @param string $sort_order Sort order [ASC, DESC]
1543     * @return array Indexed array with message IDs
1544     */
1545    function thread_index($mailbox='', $sort_field=NULL, $sort_order=NULL)
1546    {
1547        $this->_set_sort_order($sort_field, $sort_order);
1548
1549        if (!strlen($mailbox)) {
1550            $mailbox = $this->mailbox;
1551        }
1552        $key = "{$mailbox}:{$this->sort_field}:{$this->sort_order}:{$this->search_string}.thi";
1553
1554        // we have a saved search result, get index from there
1555        if (!isset($this->icache[$key]) && $this->search_string
1556            && $this->search_threads && $mailbox == $this->mailbox) {
1557            // use message IDs for better performance
1558            $ids = array_keys_recursive($this->search_set['tree']);
1559            $this->icache[$key] = $this->_flatten_threads($mailbox, $this->search_set['tree'], $ids);
1560        }
1561
1562        // have stored it in RAM
1563        if (isset($this->icache[$key]))
1564            return $this->icache[$key];
1565
1566        // get all threads (default sort order)
1567        list ($thread_tree) = $this->fetch_threads($mailbox);
1568
1569        $this->icache[$key] = $this->_flatten_threads($mailbox, $thread_tree);
1570
1571        return $this->icache[$key];
1572    }
1573
1574
1575    /**
1576     * Return array of threaded messages (all, not only roots)
1577     *
1578     * @param string $mailbox     Mailbox to get index from
1579     * @param array  $thread_tree Threaded messages array (see fetch_threads())
1580     * @param array  $ids         Message IDs if we know what we need (e.g. search result)
1581     *                            for better performance
1582     * @return array Indexed array with message IDs
1583     *
1584     * @access private
1585     */
1586    private function _flatten_threads($mailbox, $thread_tree, $ids=null)
1587    {
1588        if (empty($thread_tree))
1589            return array();
1590
1591        $msg_index = $this->sort_threads($mailbox, $thread_tree, $ids);
1592
1593        if ($this->sort_order == 'DESC')
1594            $msg_index = array_reverse($msg_index);
1595
1596        // flatten threads array
1597        $all_ids = array();
1598        foreach ($msg_index as $root) {
1599            $all_ids[] = $root;
1600            if (!empty($thread_tree[$root])) {
1601                foreach (array_keys_recursive($thread_tree[$root]) as $val)
1602                    $all_ids[] = $val;
1603            }
1604        }
1605
1606        return $all_ids;
1607    }
1608
1609
1610    /**
1611     * Invoke search request to IMAP server
1612     *
1613     * @param  string  $mailbox    Mailbox name to search in
1614     * @param  string  $str        Search criteria
1615     * @param  string  $charset    Search charset
1616     * @param  string  $sort_field Header field to sort by
1617     * @return array   search results as list of message IDs
1618     * @access public
1619     */
1620    function search($mailbox='', $str=NULL, $charset=NULL, $sort_field=NULL)
1621    {
1622        if (!$str)
1623            return false;
1624
1625        if (!strlen($mailbox)) {
1626            $mailbox = $this->mailbox;
1627        }
1628
1629        $results = $this->_search_index($mailbox, $str, $charset, $sort_field);
1630
1631        $this->set_search_set($str, $results, $charset, $sort_field, (bool)$this->threading,
1632            $this->threading || $this->search_sorted ? true : false);
1633
1634        return $results;
1635    }
1636
1637
1638    /**
1639     * Private search method
1640     *
1641     * @param string $mailbox    Mailbox name
1642     * @param string $criteria   Search criteria
1643     * @param string $charset    Charset
1644     * @param string $sort_field Sorting field
1645     *
1646     * @return array   search results as list of message ids
1647     * @see rcube_imap::search()
1648     */
1649    private function _search_index($mailbox, $criteria='ALL', $charset=NULL, $sort_field=NULL)
1650    {
1651        $orig_criteria = $criteria;
1652
1653        if ($this->skip_deleted && !preg_match('/UNDELETED/', $criteria))
1654            $criteria = 'UNDELETED '.$criteria;
1655
1656        if ($this->threading) {
1657            $a_messages = $this->conn->thread($mailbox, $this->threading, $criteria, $charset);
1658
1659            // Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
1660            // but I've seen that Courier doesn't support UTF-8)
1661            if ($a_messages === false && $charset && $charset != 'US-ASCII')
1662                $a_messages = $this->conn->thread($mailbox, $this->threading,
1663                    $this->convert_criteria($criteria, $charset), 'US-ASCII');
1664
1665            if ($a_messages !== false) {
1666                list ($thread_tree, $msg_depth, $has_children) = $a_messages;
1667                $a_messages = array(
1668                    'tree' => $thread_tree,
1669                    'depth'=> $msg_depth,
1670                    'children' => $has_children
1671                );
1672            }
1673
1674            return $a_messages;
1675        }
1676
1677        if ($sort_field && $this->get_capability('SORT')) {
1678            $charset = $charset ? $charset : $this->default_charset;
1679            $a_messages = $this->conn->sort($mailbox, $sort_field, $criteria, false, $charset);
1680
1681            // Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
1682            // but I've seen Courier with disabled UTF-8 support)
1683            if ($a_messages === false && $charset && $charset != 'US-ASCII')
1684                $a_messages = $this->conn->sort($mailbox, $sort_field,
1685                    $this->convert_criteria($criteria, $charset), false, 'US-ASCII');
1686
1687            if ($a_messages !== false) {
1688                $this->search_sorted = true;
1689                return $a_messages;
1690            }
1691        }
1692
1693        if ($orig_criteria == 'ALL') {
1694            $max = $this->_messagecount($mailbox);
1695            $a_messages = $max ? range(1, $max) : array();
1696        }
1697        else {
1698            $a_messages = $this->conn->search($mailbox,
1699                ($charset ? "CHARSET $charset " : '') . $criteria);
1700
1701            // Error, try with US-ASCII (some servers may support only US-ASCII)
1702            if ($a_messages === false && $charset && $charset != 'US-ASCII')
1703                $a_messages = $this->conn->search($mailbox,
1704                    'CHARSET US-ASCII ' . $this->convert_criteria($criteria, $charset));
1705
1706            // I didn't found that SEARCH should return sorted IDs
1707            if (is_array($a_messages) && !$this->sort_field)
1708                sort($a_messages);
1709        }
1710
1711        $this->search_sorted = false;
1712
1713        return $a_messages;
1714    }
1715
1716
1717    /**
1718     * Direct (real and simple) SEARCH request to IMAP server,
1719     * without result sorting and caching
1720     *
1721     * @param  string  $mailbox Mailbox name to search in
1722     * @param  string  $str     Search string
1723     * @param  boolean $ret_uid True if UIDs should be returned
1724     *
1725     * @return array   Search results as list of message IDs or UIDs
1726     */
1727    function search_once($mailbox='', $str=NULL, $ret_uid=false)
1728    {
1729        if (!$str)
1730            return false;
1731
1732        if (!strlen($mailbox)) {
1733            $mailbox = $this->mailbox;
1734        }
1735
1736        return $this->conn->search($mailbox, $str, $ret_uid);
1737    }
1738
1739
1740    /**
1741     * Converts charset of search criteria string
1742     *
1743     * @param  string  $str          Search string
1744     * @param  string  $charset      Original charset
1745     * @param  string  $dest_charset Destination charset (default US-ASCII)
1746     * @return string  Search string
1747     * @access private
1748     */
1749    private function convert_criteria($str, $charset, $dest_charset='US-ASCII')
1750    {
1751        // convert strings to US_ASCII
1752        if (preg_match_all('/\{([0-9]+)\}\r\n/', $str, $matches, PREG_OFFSET_CAPTURE)) {
1753            $last = 0; $res = '';
1754            foreach ($matches[1] as $m) {
1755                $string_offset = $m[1] + strlen($m[0]) + 4; // {}\r\n
1756                $string = substr($str, $string_offset - 1, $m[0]);
1757                $string = rcube_charset_convert($string, $charset, $dest_charset);
1758                if (!$string)
1759                    continue;
1760                $res .= sprintf("%s{%d}\r\n%s", substr($str, $last, $m[1] - $last - 1), strlen($string), $string);
1761                $last = $m[0] + $string_offset - 1;
1762            }
1763            if ($last < strlen($str))
1764                $res .= substr($str, $last, strlen($str)-$last);
1765        }
1766        else // strings for conversion not found
1767            $res = $str;
1768
1769        return $res;
1770    }
1771
1772
1773    /**
1774     * Sort thread
1775     *
1776     * @param string $mailbox     Mailbox name
1777     * @param  array $thread_tree Unsorted thread tree (rcube_imap_generic::thread() result)
1778     * @param  array $ids         Message IDs if we know what we need (e.g. search result)
1779     *
1780     * @return array Sorted roots IDs
1781     */
1782    function sort_threads($mailbox, $thread_tree, $ids = null)
1783    {
1784        // THREAD=ORDEREDSUBJECT: sorting by sent date of root message
1785        // THREAD=REFERENCES:     sorting by sent date of root message
1786        // THREAD=REFS:           sorting by the most recent date in each thread
1787
1788        // default sorting
1789        if (!$this->sort_field || ($this->sort_field == 'date' && $this->threading == 'REFS')) {
1790            return array_keys((array)$thread_tree);
1791        }
1792        // here we'll implement REFS sorting
1793        else {
1794            if ($mcache = $this->get_mcache_engine()) {
1795                $a_index = $mcache->get_index($mailbox, $this->sort_field, 'ASC');
1796                if (is_array($a_index)) {
1797                    $a_index = array_keys($a_index);
1798                    // now we must remove IDs that doesn't exist in $ids
1799                    if (!empty($ids))
1800                        $a_index = array_intersect($a_index, $ids);
1801                }
1802            }
1803            // use SORT command
1804            else if ($this->get_capability('SORT') &&
1805                ($a_index = $this->conn->sort($mailbox, $this->sort_field,
1806                    !empty($ids) ? $ids : ($this->skip_deleted ? 'UNDELETED' : ''))) !== false
1807            ) {
1808                // do nothing
1809            }
1810            else {
1811                // fetch specified headers for all messages and sort them
1812                $a_index = $this->conn->fetchHeaderIndex($mailbox, !empty($ids) ? $ids : "1:*",
1813                    $this->sort_field, $this->skip_deleted);
1814
1815                // return unsorted tree if we've got no index data
1816                if (!empty($a_index)) {
1817                    asort($a_index); // ASC
1818                    $a_index = array_values($a_index);
1819                }
1820            }
1821
1822            if (empty($a_index))
1823                return array_keys((array)$thread_tree);
1824
1825            return $this->_sort_thread_refs($thread_tree, $a_index);
1826        }
1827    }
1828
1829
1830    /**
1831     * THREAD=REFS sorting implementation
1832     *
1833     * @param  array $tree   Thread tree array (message identifiers as keys)
1834     * @param  array $index  Array of sorted message identifiers
1835     *
1836     * @return array   Array of sorted roots messages
1837     */
1838    private function _sort_thread_refs($tree, $index)
1839    {
1840        if (empty($tree))
1841            return array();
1842
1843        $index = array_combine(array_values($index), $index);
1844
1845        // assign roots
1846        foreach ($tree as $idx => $val) {
1847            $index[$idx] = $idx;
1848            if (!empty($val)) {
1849                $idx_arr = array_keys_recursive($tree[$idx]);
1850                foreach ($idx_arr as $subidx)
1851                    $index[$subidx] = $idx;
1852            }
1853        }
1854
1855        $index = array_values($index);
1856
1857        // create sorted array of roots
1858        $msg_index = array();
1859        if ($this->sort_order != 'DESC') {
1860            foreach ($index as $idx)
1861                if (!isset($msg_index[$idx]))
1862                    $msg_index[$idx] = $idx;
1863            $msg_index = array_values($msg_index);
1864        }
1865        else {
1866            for ($x=count($index)-1; $x>=0; $x--)
1867                if (!isset($msg_index[$index[$x]]))
1868                    $msg_index[$index[$x]] = $index[$x];
1869            $msg_index = array_reverse($msg_index);
1870        }
1871
1872        return $msg_index;
1873    }
1874
1875
1876    /**
1877     * Refresh saved search set
1878     *
1879     * @return array Current search set
1880     */
1881    function refresh_search()
1882    {
1883        if (!empty($this->search_string))
1884            $this->search_set = $this->search('', $this->search_string, $this->search_charset,
1885                $this->search_sort_field, $this->search_threads, $this->search_sorted);
1886
1887        return $this->get_search_set();
1888    }
1889
1890
1891    /**
1892     * Check if the given message ID is part of the current search set
1893     *
1894     * @param string $msgid Message id
1895     * @return boolean True on match or if no search request is stored
1896     */
1897    function in_searchset($msgid)
1898    {
1899        if (!empty($this->search_string)) {
1900            if ($this->search_threads)
1901                return isset($this->search_set['depth']["$msgid"]);
1902            else
1903                return in_array("$msgid", (array)$this->search_set, true);
1904        }
1905        else
1906            return true;
1907    }
1908
1909
1910    /**
1911     * Return message headers object of a specific message
1912     *
1913     * @param int     $id       Message sequence ID or UID
1914     * @param string  $mailbox  Mailbox to read from
1915     * @param bool    $force    True to skip cache
1916     *
1917     * @return rcube_mail_header Message headers
1918     */
1919    function get_headers($uid, $mailbox = null, $force = false)
1920    {
1921        if (!strlen($mailbox)) {
1922            $mailbox = $this->mailbox;
1923        }
1924
1925        // get cached headers
1926        if (!$force && $uid && ($mcache = $this->get_mcache_engine())) {
1927            $headers = $mcache->get_message($mailbox, $uid);
1928        }
1929        else {
1930            $headers = $this->conn->fetchHeader(
1931                $mailbox, $uid, true, true, $this->get_fetch_headers());
1932        }
1933
1934        return $headers;
1935    }
1936
1937
1938    /**
1939     * Fetch message headers and body structure from the IMAP server and build
1940     * an object structure similar to the one generated by PEAR::Mail_mimeDecode
1941     *
1942     * @param int     $uid      Message UID to fetch
1943     * @param string  $mailbox  Mailbox to read from
1944     *
1945     * @return object rcube_mail_header Message data
1946     */
1947    function get_message($uid, $mailbox = null)
1948    {
1949        if (!strlen($mailbox)) {
1950            $mailbox = $this->mailbox;
1951        }
1952
1953        // Check internal cache
1954        if (!empty($this->icache['message'])) {
1955            if (($headers = $this->icache['message']) && $headers->uid == $uid) {
1956                return $headers;
1957            }
1958        }
1959
1960        $headers = $this->get_headers($uid, $mailbox);
1961
1962        // structure might be cached
1963        if (!empty($headers->structure))
1964            return $headers;
1965
1966        $this->_msg_uid = $uid;
1967
1968        if (empty($headers->bodystructure)) {
1969            $headers->bodystructure = $this->conn->getStructure($mailbox, $uid, true);
1970        }
1971
1972        $structure = $headers->bodystructure;
1973
1974        if (empty($structure))
1975            return $headers;
1976
1977        // set message charset from message headers
1978        if ($headers->charset)
1979            $this->struct_charset = $headers->charset;
1980        else
1981            $this->struct_charset = $this->_structure_charset($structure);
1982
1983        $headers->ctype = strtolower($headers->ctype);
1984
1985        // Here we can recognize malformed BODYSTRUCTURE and
1986        // 1. [@TODO] parse the message in other way to create our own message structure
1987        // 2. or just show the raw message body.
1988        // Example of structure for malformed MIME message:
1989        // ("text" "plain" NIL NIL NIL "7bit" 2154 70 NIL NIL NIL)
1990        if ($headers->ctype && !is_array($structure[0]) && $headers->ctype != 'text/plain'
1991            && strtolower($structure[0].'/'.$structure[1]) == 'text/plain') {
1992            // we can handle single-part messages, by simple fix in structure (#1486898)
1993            if (preg_match('/^(text|application)\/(.*)/', $headers->ctype, $m)) {
1994                $structure[0] = $m[1];
1995                $structure[1] = $m[2];
1996            }
1997            else
1998                return $headers;
1999        }
2000
2001        $struct = &$this->_structure_part($structure, 0, '', $headers);
2002
2003        // don't trust given content-type
2004        if (empty($struct->parts) && !empty($headers->ctype)) {
2005            $struct->mime_id = '1';
2006            $struct->mimetype = strtolower($headers->ctype);
2007            list($struct->ctype_primary, $struct->ctype_secondary) = explode('/', $struct->mimetype);
2008        }
2009
2010        $headers->structure = $struct;
2011
2012        return $this->icache['message'] = $headers;
2013    }
2014
2015
2016    /**
2017     * Build message part object
2018     *
2019     * @param array  $part
2020     * @param int    $count
2021     * @param string $parent
2022     * @access private
2023     */
2024    function &_structure_part($part, $count=0, $parent='', $mime_headers=null)
2025    {
2026        $struct = new rcube_message_part;
2027        $struct->mime_id = empty($parent) ? (string)$count : "$parent.$count";
2028
2029        // multipart
2030        if (is_array($part[0])) {
2031            $struct->ctype_primary = 'multipart';
2032
2033        /* RFC3501: BODYSTRUCTURE fields of multipart part
2034            part1 array
2035            part2 array
2036            part3 array
2037            ....
2038            1. subtype
2039            2. parameters (optional)
2040            3. description (optional)
2041            4. language (optional)
2042            5. location (optional)
2043        */
2044
2045            // find first non-array entry
2046            for ($i=1; $i<count($part); $i++) {
2047                if (!is_array($part[$i])) {
2048                    $struct->ctype_secondary = strtolower($part[$i]);
2049                    break;
2050                }
2051            }
2052
2053            $struct->mimetype = 'multipart/'.$struct->ctype_secondary;
2054
2055            // build parts list for headers pre-fetching
2056            for ($i=0; $i<count($part); $i++) {
2057                if (!is_array($part[$i]))
2058                    break;
2059                // fetch message headers if message/rfc822
2060                // or named part (could contain Content-Location header)
2061                if (!is_array($part[$i][0])) {
2062                    $tmp_part_id = $struct->mime_id ? $struct->mime_id.'.'.($i+1) : $i+1;
2063                    if (strtolower($part[$i][0]) == 'message' && strtolower($part[$i][1]) == 'rfc822') {
2064                        $mime_part_headers[] = $tmp_part_id;
2065                    }
2066                    else if (in_array('name', (array)$part[$i][2]) && empty($part[$i][3])) {
2067                        $mime_part_headers[] = $tmp_part_id;
2068                    }
2069                }
2070            }
2071
2072            // pre-fetch headers of all parts (in one command for better performance)
2073            // @TODO: we could do this before _structure_part() call, to fetch
2074            // headers for parts on all levels
2075            if ($mime_part_headers) {
2076                $mime_part_headers = $this->conn->fetchMIMEHeaders($this->mailbox,
2077                    $this->_msg_uid, $mime_part_headers);
2078            }
2079
2080            $struct->parts = array();
2081            for ($i=0, $count=0; $i<count($part); $i++) {
2082                if (!is_array($part[$i]))
2083                    break;
2084                $tmp_part_id = $struct->mime_id ? $struct->mime_id.'.'.($i+1) : $i+1;
2085                $struct->parts[] = $this->_structure_part($part[$i], ++$count, $struct->mime_id,
2086                    $mime_part_headers[$tmp_part_id]);
2087            }
2088
2089            return $struct;
2090        }
2091
2092        /* RFC3501: BODYSTRUCTURE fields of non-multipart part
2093            0. type
2094            1. subtype
2095            2. parameters
2096            3. id
2097            4. description
2098            5. encoding
2099            6. size
2100          -- text
2101            7. lines
2102          -- message/rfc822
2103            7. envelope structure
2104            8. body structure
2105            9. lines
2106          --
2107            x. md5 (optional)
2108            x. disposition (optional)
2109            x. language (optional)
2110            x. location (optional)
2111        */
2112
2113        // regular part
2114        $struct->ctype_primary = strtolower($part[0]);
2115        $struct->ctype_secondary = strtolower($part[1]);
2116        $struct->mimetype = $struct->ctype_primary.'/'.$struct->ctype_secondary;
2117
2118        // read content type parameters
2119        if (is_array($part[2])) {
2120            $struct->ctype_parameters = array();
2121            for ($i=0; $i<count($part[2]); $i+=2)
2122                $struct->ctype_parameters[strtolower($part[2][$i])] = $part[2][$i+1];
2123
2124            if (isset($struct->ctype_parameters['charset']))
2125                $struct->charset = $struct->ctype_parameters['charset'];
2126        }
2127
2128        // #1487700: workaround for lack of charset in malformed structure
2129        if (empty($struct->charset) && !empty($mime_headers) && $mime_headers->charset) {
2130            $struct->charset = $mime_headers->charset;
2131        }
2132
2133        // read content encoding
2134        if (!empty($part[5])) {
2135            $struct->encoding = strtolower($part[5]);
2136            $struct->headers['content-transfer-encoding'] = $struct->encoding;
2137        }
2138
2139        // get part size
2140        if (!empty($part[6]))
2141            $struct->size = intval($part[6]);
2142
2143        // read part disposition
2144        $di = 8;
2145        if ($struct->ctype_primary == 'text') $di += 1;
2146        else if ($struct->mimetype == 'message/rfc822') $di += 3;
2147
2148        if (is_array($part[$di]) && count($part[$di]) == 2) {
2149            $struct->disposition = strtolower($part[$di][0]);
2150
2151            if (is_array($part[$di][1]))
2152                for ($n=0; $n<count($part[$di][1]); $n+=2)
2153                    $struct->d_parameters[strtolower($part[$di][1][$n])] = $part[$di][1][$n+1];
2154        }
2155
2156        // get message/rfc822's child-parts
2157        if (is_array($part[8]) && $di != 8) {
2158            $struct->parts = array();
2159            for ($i=0, $count=0; $i<count($part[8]); $i++) {
2160                if (!is_array($part[8][$i]))
2161                    break;
2162                $struct->parts[] = $this->_structure_part($part[8][$i], ++$count, $struct->mime_id);
2163            }
2164        }
2165
2166        // get part ID
2167        if (!empty($part[3])) {
2168            $struct->content_id = $part[3];
2169            $struct->headers['content-id'] = $part[3];
2170
2171            if (empty($struct->disposition))
2172                $struct->disposition = 'inline';
2173        }
2174
2175        // fetch message headers if message/rfc822 or named part (could contain Content-Location header)
2176        if ($struct->ctype_primary == 'message' || ($struct->ctype_parameters['name'] && !$struct->content_id)) {
2177            if (empty($mime_headers)) {
2178                $mime_headers = $this->conn->fetchPartHeader(
2179                    $this->mailbox, $this->_msg_uid, true, $struct->mime_id);
2180            }
2181
2182            if (is_string($mime_headers))
2183                $struct->headers = $this->_parse_headers($mime_headers) + $struct->headers;
2184            else if (is_object($mime_headers))
2185                $struct->headers = get_object_vars($mime_headers) + $struct->headers;
2186
2187            // get real content-type of message/rfc822
2188            if ($struct->mimetype == 'message/rfc822') {
2189                // single-part
2190                if (!is_array($part[8][0]))
2191                    $struct->real_mimetype = strtolower($part[8][0] . '/' . $part[8][1]);
2192                // multi-part
2193                else {
2194                    for ($n=0; $n<count($part[8]); $n++)
2195                        if (!is_array($part[8][$n]))
2196                            break;
2197                    $struct->real_mimetype = 'multipart/' . strtolower($part[8][$n]);
2198                }
2199            }
2200
2201            if ($struct->ctype_primary == 'message' && empty($struct->parts)) {
2202                if (is_array($part[8]) && $di != 8)
2203                    $struct->parts[] = $this->_structure_part($part[8], ++$count, $struct->mime_id);
2204            }
2205        }
2206
2207        // normalize filename property
2208        $this->_set_part_filename($struct, $mime_headers);
2209
2210        return $struct;
2211    }
2212
2213
2214    /**
2215     * Set attachment filename from message part structure
2216     *
2217     * @param  rcube_message_part $part    Part object
2218     * @param  string             $headers Part's raw headers
2219     * @access private
2220     */
2221    private function _set_part_filename(&$part, $headers=null)
2222    {
2223        if (!empty($part->d_parameters['filename']))
2224            $filename_mime = $part->d_parameters['filename'];
2225        else if (!empty($part->d_parameters['filename*']))
2226            $filename_encoded = $part->d_parameters['filename*'];
2227        else if (!empty($part->ctype_parameters['name*']))
2228            $filename_encoded = $part->ctype_parameters['name*'];
2229        // RFC2231 value continuations
2230        // TODO: this should be rewrited to support RFC2231 4.1 combinations
2231        else if (!empty($part->d_parameters['filename*0'])) {
2232            $i = 0;
2233            while (isset($part->d_parameters['filename*'.$i])) {
2234                $filename_mime .= $part->d_parameters['filename*'.$i];
2235                $i++;
2236            }
2237            // some servers (eg. dovecot-1.x) have no support for parameter value continuations
2238            // we must fetch and parse headers "manually"
2239            if ($i<2) {
2240                if (!$headers) {
2241                    $headers = $this->conn->fetchPartHeader(
2242                        $this->mailbox, $this->_msg_uid, true, $part->mime_id);
2243                }
2244                $filename_mime = '';
2245                $i = 0;
2246                while (preg_match('/filename\*'.$i.'\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
2247                    $filename_mime .= $matches[1];
2248                    $i++;
2249                }
2250            }
2251        }
2252        else if (!empty($part->d_parameters['filename*0*'])) {
2253            $i = 0;
2254            while (isset($part->d_parameters['filename*'.$i.'*'])) {
2255                $filename_encoded .= $part->d_parameters['filename*'.$i.'*'];
2256                $i++;
2257            }
2258            if ($i<2) {
2259                if (!$headers) {
2260                    $headers = $this->conn->fetchPartHeader(
2261                            $this->mailbox, $this->_msg_uid, true, $part->mime_id);
2262                }
2263                $filename_encoded = '';
2264                $i = 0; $matches = array();
2265                while (preg_match('/filename\*'.$i.'\*\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
2266                    $filename_encoded .= $matches[1];
2267                    $i++;
2268                }
2269            }
2270        }
2271        else if (!empty($part->ctype_parameters['name*0'])) {
2272            $i = 0;
2273            while (isset($part->ctype_parameters['name*'.$i])) {
2274                $filename_mime .= $part->ctype_parameters['name*'.$i];
2275                $i++;
2276            }
2277            if ($i<2) {
2278                if (!$headers) {
2279                    $headers = $this->conn->fetchPartHeader(
2280                        $this->mailbox, $this->_msg_uid, true, $part->mime_id);
2281                }
2282                $filename_mime = '';
2283                $i = 0; $matches = array();
2284                while (preg_match('/\s+name\*'.$i.'\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
2285                    $filename_mime .= $matches[1];
2286                    $i++;
2287                }
2288            }
2289        }
2290        else if (!empty($part->ctype_parameters['name*0*'])) {
2291            $i = 0;
2292            while (isset($part->ctype_parameters['name*'.$i.'*'])) {
2293                $filename_encoded .= $part->ctype_parameters['name*'.$i.'*'];
2294                $i++;
2295            }
2296            if ($i<2) {
2297                if (!$headers) {
2298                    $headers = $this->conn->fetchPartHeader(
2299                        $this->mailbox, $this->_msg_uid, true, $part->mime_id);
2300                }
2301                $filename_encoded = '';
2302                $i = 0; $matches = array();
2303                while (preg_match('/\s+name\*'.$i.'\*\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
2304                    $filename_encoded .= $matches[1];
2305                    $i++;
2306                }
2307            }
2308        }
2309        // read 'name' after rfc2231 parameters as it may contains truncated filename (from Thunderbird)
2310        else if (!empty($part->ctype_parameters['name']))
2311            $filename_mime = $part->ctype_parameters['name'];
2312        // Content-Disposition
2313        else if (!empty($part->headers['content-description']))
2314            $filename_mime = $part->headers['content-description'];
2315        else
2316            return;
2317
2318        // decode filename
2319        if (!empty($filename_mime)) {
2320            $part->filename = rcube_imap::decode_mime_string($filename_mime,
2321                $part->charset ? $part->charset : ($this->struct_charset ? $this->struct_charset :
2322                rc_detect_encoding($filename_mime, $this->default_charset)));
2323        }
2324        else if (!empty($filename_encoded)) {
2325            // decode filename according to RFC 2231, Section 4
2326            if (preg_match("/^([^']*)'[^']*'(.*)$/", $filename_encoded, $fmatches)) {
2327                $filename_charset = $fmatches[1];
2328                $filename_encoded = $fmatches[2];
2329            }
2330            $part->filename = rcube_charset_convert(urldecode($filename_encoded), $filename_charset);
2331        }
2332    }
2333
2334
2335    /**
2336     * Get charset name from message structure (first part)
2337     *
2338     * @param  array $structure Message structure
2339     * @return string Charset name
2340     * @access private
2341     */
2342    private function _structure_charset($structure)
2343    {
2344        while (is_array($structure)) {
2345            if (is_array($structure[2]) && $structure[2][0] == 'charset')
2346                return $structure[2][1];
2347            $structure = $structure[0];
2348        }
2349    }
2350
2351
2352    /**
2353     * Fetch message body of a specific message from the server
2354     *
2355     * @param  int                $uid    Message UID
2356     * @param  string             $part   Part number
2357     * @param  rcube_message_part $o_part Part object created by get_structure()
2358     * @param  mixed              $print  True to print part, ressource to write part contents in
2359     * @param  resource           $fp     File pointer to save the message part
2360     * @param  boolean            $skip_charset_conv Disables charset conversion
2361     *
2362     * @return string Message/part body if not printed
2363     */
2364    function &get_message_part($uid, $part=1, $o_part=NULL, $print=NULL, $fp=NULL, $skip_charset_conv=false)
2365    {
2366        // get part encoding if not provided
2367        if (!is_object($o_part)) {
2368            $structure = $this->conn->getStructure($this->mailbox, $uid, true);
2369
2370            $o_part = new rcube_message_part;
2371            $o_part->ctype_primary = strtolower(rcube_imap_generic::getStructurePartType($structure, $part));
2372            $o_part->encoding      = strtolower(rcube_imap_generic::getStructurePartEncoding($structure, $part));
2373            $o_part->charset       = rcube_imap_generic::getStructurePartCharset($structure, $part);
2374        }
2375
2376        // TODO: Add caching for message parts
2377
2378        if (!$part) {
2379            $part = 'TEXT';
2380        }
2381
2382        $body = $this->conn->handlePartBody($this->mailbox, $uid, true, $part,
2383            $o_part->encoding, $print, $fp);
2384
2385        if ($fp || $print) {
2386            return true;
2387        }
2388
2389        // convert charset (if text or message part)
2390        if ($body && !$skip_charset_conv &&
2391            preg_match('/^(text|message)$/', $o_part->ctype_primary)
2392        ) {
2393            if (!$o_part->charset || strtoupper($o_part->charset) == 'US-ASCII') {
2394                $o_part->charset = $this->default_charset;
2395            }
2396            $body = rcube_charset_convert($body, $o_part->charset);
2397        }
2398
2399        return $body;
2400    }
2401
2402
2403    /**
2404     * Fetch message body of a specific message from the server
2405     *
2406     * @param  int    $uid  Message UID
2407     * @return string $part Message/part body
2408     * @see    rcube_imap::get_message_part()
2409     */
2410    function &get_body($uid, $part=1)
2411    {
2412        $headers = $this->get_headers($uid);
2413        return rcube_charset_convert($this->get_message_part($uid, $part, NULL),
2414            $headers->charset ? $headers->charset : $this->default_charset);
2415    }
2416
2417
2418    /**
2419     * Returns the whole message source as string (or saves to a file)
2420     *
2421     * @param int      $uid Message UID
2422     * @param resource $fp  File pointer to save the message
2423     *
2424     * @return string Message source string
2425     */
2426    function &get_raw_body($uid, $fp=null)
2427    {
2428        return $this->conn->handlePartBody($this->mailbox, $uid,
2429            true, null, null, false, $fp);
2430    }
2431
2432
2433    /**
2434     * Returns the message headers as string
2435     *
2436     * @param int $uid  Message UID
2437     * @return string Message headers string
2438     */
2439    function &get_raw_headers($uid)
2440    {
2441        return $this->conn->fetchPartHeader($this->mailbox, $uid, true);
2442    }
2443
2444
2445    /**
2446     * Sends the whole message source to stdout
2447     *
2448     * @param int $uid Message UID
2449     */
2450    function print_raw_body($uid)
2451    {
2452        $this->conn->handlePartBody($this->mailbox, $uid, true, NULL, NULL, true);
2453    }
2454
2455
2456    /**
2457     * Set message flag to one or several messages
2458     *
2459     * @param mixed   $uids       Message UIDs as array or comma-separated string, or '*'
2460     * @param string  $flag       Flag to set: SEEN, UNDELETED, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
2461     * @param string  $mailbox    Folder name
2462     * @param boolean $skip_cache True to skip message cache clean up
2463     *
2464     * @return boolean  Operation status
2465     */
2466    function set_flag($uids, $flag, $mailbox=null, $skip_cache=false)
2467    {
2468        if (!strlen($mailbox)) {
2469            $mailbox = $this->mailbox;
2470        }
2471
2472        $flag = strtoupper($flag);
2473        list($uids, $all_mode) = $this->_parse_uids($uids, $mailbox);
2474
2475        if (strpos($flag, 'UN') === 0)
2476            $result = $this->conn->unflag($mailbox, $uids, substr($flag, 2));
2477        else
2478            $result = $this->conn->flag($mailbox, $uids, $flag);
2479
2480        if ($result) {
2481            // reload message headers if cached
2482            // @TODO: update flags instead removing from cache
2483            if (!$skip_cache && ($mcache = $this->get_mcache_engine())) {
2484                $status = strpos($flag, 'UN') !== 0;
2485                $mflag  = preg_replace('/^UN/', '', $flag);
2486                $mcache->change_flag($mailbox, $all_mode ? null : explode(',', $uids),
2487                    $mflag, $status);
2488            }
2489
2490            // clear cached counters
2491            if ($flag == 'SEEN' || $flag == 'UNSEEN') {
2492                $this->_clear_messagecount($mailbox, 'SEEN');
2493                $this->_clear_messagecount($mailbox, 'UNSEEN');
2494            }
2495            else if ($flag == 'DELETED') {
2496                $this->_clear_messagecount($mailbox, 'DELETED');
2497            }
2498        }
2499
2500        return $result;
2501    }
2502
2503
2504    /**
2505     * Remove message flag for one or several messages
2506     *
2507     * @param mixed  $uids    Message UIDs as array or comma-separated string, or '*'
2508     * @param string $flag    Flag to unset: SEEN, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
2509     * @param string $mailbox Folder name
2510     *
2511     * @return int   Number of flagged messages, -1 on failure
2512     * @see set_flag
2513     */
2514    function unset_flag($uids, $flag, $mailbox=null)
2515    {
2516        return $this->set_flag($uids, 'UN'.$flag, $mailbox);
2517    }
2518
2519
2520    /**
2521     * Append a mail message (source) to a specific mailbox
2522     *
2523     * @param string  $mailbox Target mailbox
2524     * @param string  $message The message source string or filename
2525     * @param string  $headers Headers string if $message contains only the body
2526     * @param boolean $is_file True if $message is a filename
2527     *
2528     * @return boolean True on success, False on error
2529     */
2530    function save_message($mailbox, &$message, $headers='', $is_file=false)
2531    {
2532        if (!strlen($mailbox)) {
2533            $mailbox = $this->mailbox;
2534        }
2535
2536        // make sure mailbox exists
2537        if ($this->mailbox_exists($mailbox)) {
2538            if ($is_file)
2539                $saved = $this->conn->appendFromFile($mailbox, $message, $headers);
2540            else
2541                $saved = $this->conn->append($mailbox, $message);
2542        }
2543
2544        if ($saved) {
2545            // increase messagecount of the target mailbox
2546            $this->_set_messagecount($mailbox, 'ALL', 1);
2547        }
2548
2549        return $saved;
2550    }
2551
2552
2553    /**
2554     * Move a message from one mailbox to another
2555     *
2556     * @param mixed  $uids      Message UIDs as array or comma-separated string, or '*'
2557     * @param string $to_mbox   Target mailbox
2558     * @param string $from_mbox Source mailbox
2559     * @return boolean True on success, False on error
2560     */
2561    function move_message($uids, $to_mbox, $from_mbox='')
2562    {
2563        if (!strlen($from_mbox)) {
2564            $from_mbox = $this->mailbox;
2565        }
2566
2567        if ($to_mbox === $from_mbox) {
2568            return false;
2569        }
2570
2571        list($uids, $all_mode) = $this->_parse_uids($uids, $from_mbox);
2572
2573        // exit if no message uids are specified
2574        if (empty($uids))
2575            return false;
2576
2577        // make sure mailbox exists
2578        if ($to_mbox != 'INBOX' && !$this->mailbox_exists($to_mbox)) {
2579            if (in_array($to_mbox, $this->default_folders))
2580                $this->create_mailbox($to_mbox, true);
2581            else
2582                return false;
2583        }
2584
2585        $config = rcmail::get_instance()->config;
2586        $to_trash = $to_mbox == $config->get('trash_mbox');
2587
2588        // flag messages as read before moving them
2589        if ($to_trash && $config->get('read_when_deleted')) {
2590            // don't flush cache (4th argument)
2591            $this->set_flag($uids, 'SEEN', $from_mbox, true);
2592        }
2593
2594        // move messages
2595        $moved = $this->conn->move($uids, $from_mbox, $to_mbox);
2596
2597        // send expunge command in order to have the moved message
2598        // really deleted from the source mailbox
2599        if ($moved) {
2600            $this->_expunge($from_mbox, false, $uids);
2601            $this->_clear_messagecount($from_mbox);
2602            $this->_clear_messagecount($to_mbox);
2603        }
2604        // moving failed
2605        else if ($to_trash && $config->get('delete_always', false)) {
2606            $moved = $this->delete_message($uids, $from_mbox);
2607        }
2608
2609        if ($moved) {
2610            // unset threads internal cache
2611            unset($this->icache['threads']);
2612
2613            // remove message ids from search set
2614            if ($this->search_set && $from_mbox == $this->mailbox) {
2615                // threads are too complicated to just remove messages from set
2616                if ($this->search_threads || $all_mode)
2617                    $this->refresh_search();
2618                else {
2619                    $a_uids = explode(',', $uids);
2620                    foreach ($a_uids as $uid)
2621                        $a_mids[] = $this->uid2id($uid, $from_mbox);
2622                    $this->search_set = array_diff($this->search_set, $a_mids);
2623                }
2624                unset($a_mids);
2625                unset($a_uids);
2626            }
2627
2628            // remove cached messages
2629            // @TODO: do cache update instead of clearing it
2630            $this->clear_message_cache($from_mbox, $all_mode ? null : explode(',', $uids));
2631        }
2632
2633        return $moved;
2634    }
2635
2636
2637    /**
2638     * Copy a message from one mailbox to another
2639     *
2640     * @param mixed  $uids      Message UIDs as array or comma-separated string, or '*'
2641     * @param string $to_mbox   Target mailbox
2642     * @param string $from_mbox Source mailbox
2643     * @return boolean True on success, False on error
2644     */
2645    function copy_message($uids, $to_mbox, $from_mbox='')
2646    {
2647        if (!strlen($from_mbox)) {
2648            $from_mbox = $this->mailbox;
2649        }
2650
2651        list($uids, $all_mode) = $this->_parse_uids($uids, $from_mbox);
2652
2653        // exit if no message uids are specified
2654        if (empty($uids)) {
2655            return false;
2656        }
2657
2658        // make sure mailbox exists
2659        if ($to_mbox != 'INBOX' && !$this->mailbox_exists($to_mbox)) {
2660            if (in_array($to_mbox, $this->default_folders))
2661                $this->create_mailbox($to_mbox, true);
2662            else
2663                return false;
2664        }
2665
2666        // copy messages
2667        $copied = $this->conn->copy($uids, $from_mbox, $to_mbox);
2668
2669        if ($copied) {
2670            $this->_clear_messagecount($to_mbox);
2671        }
2672
2673        return $copied;
2674    }
2675
2676
2677    /**
2678     * Mark messages as deleted and expunge mailbox
2679     *
2680     * @param mixed  $uids    Message UIDs as array or comma-separated string, or '*'
2681     * @param string $mailbox Source mailbox
2682     *
2683     * @return boolean True on success, False on error
2684     */
2685    function delete_message($uids, $mailbox='')
2686    {
2687        if (!strlen($mailbox)) {
2688            $mailbox = $this->mailbox;
2689        }
2690
2691        list($uids, $all_mode) = $this->_parse_uids($uids, $mailbox);
2692
2693        // exit if no message uids are specified
2694        if (empty($uids))
2695            return false;
2696
2697        $deleted = $this->conn->delete($mailbox, $uids);
2698
2699        if ($deleted) {
2700            // send expunge command in order to have the deleted message
2701            // really deleted from the mailbox
2702            $this->_expunge($mailbox, false, $uids);
2703            $this->_clear_messagecount($mailbox);
2704            unset($this->uid_id_map[$mailbox]);
2705
2706            // unset threads internal cache
2707            unset($this->icache['threads']);
2708
2709            // remove message ids from search set
2710            if ($this->search_set && $mailbox == $this->mailbox) {
2711                // threads are too complicated to just remove messages from set
2712                if ($this->search_threads || $all_mode)
2713                    $this->refresh_search();
2714                else {
2715                    $a_uids = explode(',', $uids);
2716                    foreach ($a_uids as $uid)
2717                        $a_mids[] = $this->uid2id($uid, $mailbox);
2718                    $this->search_set = array_diff($this->search_set, $a_mids);
2719                    unset($a_uids);
2720                    unset($a_mids);
2721                }
2722            }
2723
2724            // remove cached messages
2725            $this->clear_message_cache($mailbox, $all_mode ? null : explode(',', $uids));
2726        }
2727
2728        return $deleted;
2729    }
2730
2731
2732    /**
2733     * Clear all messages in a specific mailbox
2734     *
2735     * @param string $mailbox Mailbox name
2736     *
2737     * @return int Above 0 on success
2738     */
2739    function clear_mailbox($mailbox=null)
2740    {
2741        if (!strlen($mailbox)) {
2742            $mailbox = $this->mailbox;
2743        }
2744
2745        // SELECT will set messages count for clearFolder()
2746        if ($this->conn->select($mailbox)) {
2747            $cleared = $this->conn->clearFolder($mailbox);
2748        }
2749
2750        // make sure the cache is cleared as well
2751        if ($cleared) {
2752            $this->clear_message_cache($mailbox);
2753            $a_mailbox_cache = $this->get_cache('messagecount');
2754            unset($a_mailbox_cache[$mailbox]);
2755            $this->update_cache('messagecount', $a_mailbox_cache);
2756        }
2757
2758        return $cleared;
2759    }
2760
2761
2762    /**
2763     * Send IMAP expunge command and clear cache
2764     *
2765     * @param string  $mailbox     Mailbox name
2766     * @param boolean $clear_cache False if cache should not be cleared
2767     *
2768     * @return boolean True on success
2769     */
2770    function expunge($mailbox='', $clear_cache=true)
2771    {
2772        if (!strlen($mailbox)) {
2773            $mailbox = $this->mailbox;
2774        }
2775
2776        return $this->_expunge($mailbox, $clear_cache);
2777    }
2778
2779
2780    /**
2781     * Send IMAP expunge command and clear cache
2782     *
2783     * @param string  $mailbox     Mailbox name
2784     * @param boolean $clear_cache False if cache should not be cleared
2785     * @param mixed   $uids        Message UIDs as array or comma-separated string, or '*'
2786     * @return boolean True on success
2787     * @access private
2788     * @see rcube_imap::expunge()
2789     */
2790    private function _expunge($mailbox, $clear_cache=true, $uids=NULL)
2791    {
2792        if ($uids && $this->get_capability('UIDPLUS'))
2793            list($uids, $all_mode) = $this->_parse_uids($uids, $mailbox);
2794        else
2795            $uids = null;
2796
2797        // force mailbox selection and check if mailbox is writeable
2798        // to prevent a situation when CLOSE is executed on closed
2799        // or EXPUNGE on read-only mailbox
2800        $result = $this->conn->select($mailbox);
2801        if (!$result) {
2802            return false;
2803        }
2804        if (!$this->conn->data['READ-WRITE']) {
2805            $this->conn->setError(rcube_imap_generic::ERROR_READONLY, "Mailbox is read-only");
2806            return false;
2807        }
2808
2809        // CLOSE(+SELECT) should be faster than EXPUNGE
2810        if (empty($uids) || $all_mode)
2811            $result = $this->conn->close();
2812        else
2813            $result = $this->conn->expunge($mailbox, $uids);
2814
2815        if ($result && $clear_cache) {
2816            $this->clear_message_cache($mailbox, $all_mode ? null : explode(',', $uids));
2817            $this->_clear_messagecount($mailbox);
2818        }
2819
2820        return $result;
2821    }
2822
2823
2824    /**
2825     * Parse message UIDs input
2826     *
2827     * @param mixed  $uids    UIDs array or comma-separated list or '*' or '1:*'
2828     * @param string $mailbox Mailbox name
2829     * @return array Two elements array with UIDs converted to list and ALL flag
2830     * @access private
2831     */
2832    private function _parse_uids($uids, $mailbox)
2833    {
2834        if ($uids === '*' || $uids === '1:*') {
2835            if (empty($this->search_set)) {
2836                $uids = '1:*';
2837                $all = true;
2838            }
2839            // get UIDs from current search set
2840            // @TODO: skip fetchUIDs() and work with IDs instead of UIDs (?)
2841            else {
2842                if ($this->search_threads)
2843                    $uids = $this->conn->fetchUIDs($mailbox, array_keys($this->search_set['depth']));
2844                else
2845                    $uids = $this->conn->fetchUIDs($mailbox, $this->search_set);
2846
2847                // save ID-to-UID mapping in local cache
2848                if (is_array($uids))
2849                    foreach ($uids as $id => $uid)
2850                        $this->uid_id_map[$mailbox][$uid] = $id;
2851
2852                $uids = join(',', $uids);
2853            }
2854        }
2855        else {
2856            if (is_array($uids))
2857                $uids = join(',', $uids);
2858
2859            if (preg_match('/[^0-9,]/', $uids))
2860                $uids = '';
2861        }
2862
2863        return array($uids, (bool) $all);
2864    }
2865
2866
2867    /**
2868     * Translate UID to message ID
2869     *
2870     * @param int    $uid     Message UID
2871     * @param string $mailbox Mailbox name
2872     *
2873     * @return int   Message ID
2874     */
2875    function get_id($uid, $mailbox=null)
2876    {
2877        if (!strlen($mailbox)) {
2878            $mailbox = $this->mailbox;
2879        }
2880
2881        return $this->uid2id($uid, $mailbox);
2882    }
2883
2884
2885    /**
2886     * Translate message number to UID
2887     *
2888     * @param int    $id      Message ID
2889     * @param string $mailbox Mailbox name
2890     *
2891     * @return int   Message UID
2892     */
2893    function get_uid($id, $mailbox=null)
2894    {
2895        if (!strlen($mailbox)) {
2896            $mailbox = $this->mailbox;
2897        }
2898
2899        return $this->id2uid($id, $mailbox);
2900    }
2901
2902
2903
2904    /* --------------------------------
2905     *        folder managment
2906     * --------------------------------*/
2907
2908    /**
2909     * Public method for listing subscribed folders
2910     *
2911     * @param   string  $root   Optional root folder
2912     * @param   string  $name   Optional name pattern
2913     * @param   string  $filter Optional filter
2914     *
2915     * @return  array   List of mailboxes/folders
2916     * @access  public
2917     */
2918    function list_mailboxes($root='', $name='*', $filter=null)
2919    {
2920        $a_mboxes = $this->_list_mailboxes($root, $name, $filter);
2921
2922        // INBOX should always be available
2923        if ((!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)) {
2924            array_unshift($a_mboxes, 'INBOX');
2925        }
2926
2927        // sort mailboxes
2928        $a_mboxes = $this->_sort_mailbox_list($a_mboxes);
2929
2930        return $a_mboxes;
2931    }
2932
2933
2934    /**
2935     * Private method for mailbox listing
2936     *
2937     * @param   string  $root   Optional root folder
2938     * @param   string  $name   Optional name pattern
2939     * @param   mixed   $filter Optional filter
2940     *
2941     * @return  array   List of mailboxes/folders
2942     * @see     rcube_imap::list_mailboxes()
2943     * @access  private
2944     */
2945    private function _list_mailboxes($root='', $name='*', $filter=null)
2946    {
2947        $cache_key = $root.':'.$name;
2948        if (!empty($filter)) {
2949            $cache_key .= ':'.(is_string($filter) ? $filter : serialize($filter));
2950        }
2951
2952        $cache_key = 'mailboxes.'.md5($cache_key);
2953
2954        // get cached folder list
2955        $a_mboxes = $this->get_cache($cache_key);
2956        if (is_array($a_mboxes)) {
2957            return $a_mboxes;
2958        }
2959
2960        $a_defaults = $a_out = array();
2961
2962        // Give plugins a chance to provide a list of mailboxes
2963        $data = rcmail::get_instance()->plugins->exec_hook('mailboxes_list',
2964            array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LSUB'));
2965
2966        if (isset($data['folders'])) {
2967            $a_folders = $data['folders'];
2968        }
2969        else {
2970            // Server supports LIST-EXTENDED, we can use selection options
2971            $config = rcmail::get_instance()->config;
2972            // #1486225: Some dovecot versions returns wrong result using LIST-EXTENDED
2973            if (!$config->get('imap_force_lsub') && $this->get_capability('LIST-EXTENDED')) {
2974                // This will also set mailbox options, LSUB doesn't do that
2975                $a_folders = $this->conn->listMailboxes($root, $name,
2976                    NULL, array('SUBSCRIBED'));
2977
2978                // remove non-existent folders
2979                if (is_array($a_folders)) {
2980                    foreach ($a_folders as $idx => $folder) {
2981                        if ($this->conn->data['LIST'] && ($opts = $this->conn->data['LIST'][$folder])
2982                            && in_array('\\NonExistent', $opts)
2983                        ) {
2984                            unset($a_folders[$idx]);
2985                        } 
2986                    }
2987                }
2988            }
2989            // retrieve list of folders from IMAP server using LSUB
2990            else {
2991                $a_folders = $this->conn->listSubscribed($root, $name);
2992            }
2993        }
2994
2995        if (!is_array($a_folders) || !sizeof($a_folders)) {
2996            $a_folders = array();
2997        }
2998
2999        // write mailboxlist to cache
3000        $this->update_cache($cache_key, $a_folders);
3001
3002        return $a_folders;
3003    }
3004
3005
3006    /**
3007     * Get a list of all folders available on the IMAP server
3008     *
3009     * @param string $root   IMAP root dir
3010     * @param string  $name   Optional name pattern
3011     * @param mixed   $filter Optional filter
3012     *
3013     * @return array Indexed array with folder names
3014     */
3015    function list_unsubscribed($root='', $name='*', $filter=null)
3016    {
3017        // @TODO: caching
3018        // Give plugins a chance to provide a list of mailboxes
3019        $data = rcmail::get_instance()->plugins->exec_hook('mailboxes_list',
3020            array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LIST'));
3021
3022        if (isset($data['folders'])) {
3023            $a_mboxes = $data['folders'];
3024        }
3025        else {
3026            // retrieve list of folders from IMAP server
3027            $a_mboxes = $this->conn->listMailboxes($root, $name);
3028        }
3029
3030        if (!is_array($a_mboxes)) {
3031            $a_mboxes = array();
3032        }
3033
3034        // INBOX should always be available
3035        if ((!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)) {
3036            array_unshift($a_mboxes, 'INBOX');
3037        }
3038
3039        // filter folders and sort them
3040        $a_mboxes = $this->_sort_mailbox_list($a_mboxes);
3041
3042        return $a_mboxes;
3043    }
3044
3045
3046    /**
3047     * Get mailbox quota information
3048     * added by Nuny
3049     *
3050     * @return mixed Quota info or False if not supported
3051     */
3052    function get_quota()
3053    {
3054        if ($this->get_capability('QUOTA'))
3055            return $this->conn->getQuota();
3056
3057        return false;
3058    }
3059
3060
3061    /**
3062     * Get mailbox size (size of all messages in a mailbox)
3063     *
3064     * @param string $mailbox Mailbox name
3065     *
3066     * @return int Mailbox size in bytes, False on error
3067     */
3068    function get_mailbox_size($mailbox)
3069    {
3070        // @TODO: could we try to use QUOTA here?
3071        $result = $this->conn->fetchHeaderIndex($mailbox, '1:*', 'SIZE', false);
3072
3073        if (is_array($result))
3074            $result = array_sum($result);
3075
3076        return $result;
3077    }
3078
3079
3080    /**
3081     * Subscribe to a specific mailbox(es)
3082     *
3083     * @param array $a_mboxes Mailbox name(s)
3084     * @return boolean True on success
3085     */
3086    function subscribe($a_mboxes)
3087    {
3088        if (!is_array($a_mboxes))
3089            $a_mboxes = array($a_mboxes);
3090
3091        // let this common function do the main work
3092        return $this->_change_subscription($a_mboxes, 'subscribe');
3093    }
3094
3095
3096    /**
3097     * Unsubscribe mailboxes
3098     *
3099     * @param array $a_mboxes Mailbox name(s)
3100     * @return boolean True on success
3101     */
3102    function unsubscribe($a_mboxes)
3103    {
3104        if (!is_array($a_mboxes))
3105            $a_mboxes = array($a_mboxes);
3106
3107        // let this common function do the main work
3108        return $this->_change_subscription($a_mboxes, 'unsubscribe');
3109    }
3110
3111
3112    /**
3113     * Create a new mailbox on the server and register it in local cache
3114     *
3115     * @param string  $mailbox   New mailbox name
3116     * @param boolean $subscribe True if the new mailbox should be subscribed
3117     *
3118     * @return boolean True on success
3119     */
3120    function create_mailbox($mailbox, $subscribe=false)
3121    {
3122        $result = $this->conn->createFolder($mailbox);
3123
3124        // try to subscribe it
3125        if ($result) {
3126            // clear cache
3127            $this->clear_cache('mailboxes', true);
3128
3129            if ($subscribe)
3130                $this->subscribe($mailbox);
3131        }
3132
3133        return $result;
3134    }
3135
3136
3137    /**
3138     * Set a new name to an existing mailbox
3139     *
3140     * @param string $mailbox  Mailbox to rename
3141     * @param string $new_name New mailbox name
3142     *
3143     * @return boolean True on success
3144     */
3145    function rename_mailbox($mailbox, $new_name)
3146    {
3147        if (!strlen($new_name)) {
3148            return false;
3149        }
3150
3151        $delm = $this->get_hierarchy_delimiter();
3152
3153        // get list of subscribed folders
3154        if ((strpos($mailbox, '%') === false) && (strpos($mailbox, '*') === false)) {
3155            $a_subscribed = $this->_list_mailboxes('', $mailbox . $delm . '*');
3156            $subscribed   = $this->mailbox_exists($mailbox, true);
3157        }
3158        else {
3159            $a_subscribed = $this->_list_mailboxes();
3160            $subscribed   = in_array($mailbox, $a_subscribed);
3161        }
3162
3163        $result = $this->conn->renameFolder($mailbox, $new_name);
3164
3165        if ($result) {
3166            // unsubscribe the old folder, subscribe the new one
3167            if ($subscribed) {
3168                $this->conn->unsubscribe($mailbox);
3169                $this->conn->subscribe($new_name);
3170            }
3171
3172            // check if mailbox children are subscribed
3173            foreach ($a_subscribed as $c_subscribed) {
3174                if (preg_match('/^'.preg_quote($mailbox.$delm, '/').'/', $c_subscribed)) {
3175                    $this->conn->unsubscribe($c_subscribed);
3176                    $this->conn->subscribe(preg_replace('/^'.preg_quote($mailbox, '/').'/',
3177                        $new_name, $c_subscribed));
3178
3179                    // clear cache
3180                    $this->clear_message_cache($c_subscribed);
3181                }
3182            }
3183
3184            // clear cache
3185            $this->clear_message_cache($mailbox);
3186            $this->clear_cache('mailboxes', true);
3187        }
3188
3189        return $result;
3190    }
3191
3192
3193    /**
3194     * Remove mailbox from server
3195     *
3196     * @param string $mailbox Mailbox name
3197     *
3198     * @return boolean True on success
3199     */
3200    function delete_mailbox($mailbox)
3201    {
3202        $delm = $this->get_hierarchy_delimiter();
3203
3204        // get list of folders
3205        if ((strpos($mailbox, '%') === false) && (strpos($mailbox, '*') === false))
3206            $sub_mboxes = $this->list_unsubscribed('', $mailbox . $delm . '*');
3207        else
3208            $sub_mboxes = $this->list_unsubscribed();
3209
3210        // send delete command to server
3211        $result = $this->conn->deleteFolder($mailbox);
3212
3213        if ($result) {
3214            // unsubscribe mailbox
3215            $this->conn->unsubscribe($mailbox);
3216
3217            foreach ($sub_mboxes as $c_mbox) {
3218                if (preg_match('/^'.preg_quote($mailbox.$delm, '/').'/', $c_mbox)) {
3219                    $this->conn->unsubscribe($c_mbox);
3220                    if ($this->conn->deleteFolder($c_mbox)) {
3221                            $this->clear_message_cache($c_mbox);
3222                    }
3223                }
3224            }
3225
3226            // clear mailbox-related cache
3227            $this->clear_message_cache($mailbox);
3228            $this->clear_cache('mailboxes', true);
3229        }
3230
3231        return $result;
3232    }
3233
3234
3235    /**
3236     * Create all folders specified as default
3237     */
3238    function create_default_folders()
3239    {
3240        // create default folders if they do not exist
3241        foreach ($this->default_folders as $folder) {
3242            if (!$this->mailbox_exists($folder))
3243                $this->create_mailbox($folder, true);
3244            else if (!$this->mailbox_exists($folder, true))
3245                $this->subscribe($folder);
3246        }
3247    }
3248
3249
3250    /**
3251     * Checks if folder exists and is subscribed
3252     *
3253     * @param string   $mailbox      Folder name
3254     * @param boolean  $subscription Enable subscription checking
3255     *
3256     * @return boolean TRUE or FALSE
3257     */
3258    function mailbox_exists($mailbox, $subscription=false)
3259    {
3260        if ($mailbox == 'INBOX') {
3261            return true;
3262        }
3263
3264        $key  = $subscription ? 'subscribed' : 'existing';
3265
3266        if (is_array($this->icache[$key]) && in_array($mailbox, $this->icache[$key]))
3267            return true;
3268
3269        if ($subscription) {
3270            $a_folders = $this->conn->listSubscribed('', $mailbox);
3271        }
3272        else {
3273            $a_folders = $this->conn->listMailboxes('', $mailbox);
3274        }
3275
3276        if (is_array($a_folders) && in_array($mailbox, $a_folders)) {
3277            $this->icache[$key][] = $mailbox;
3278            return true;
3279        }
3280
3281        return false;
3282    }
3283
3284
3285    /**
3286     * Returns the namespace where the folder is in
3287     *
3288     * @param string $mailbox Folder name
3289     *
3290     * @return string One of 'personal', 'other' or 'shared'
3291     * @access public
3292     */
3293    function mailbox_namespace($mailbox)
3294    {
3295        if ($mailbox == 'INBOX') {
3296            return 'personal';
3297        }
3298
3299        foreach ($this->namespace as $type => $namespace) {
3300            if (is_array($namespace)) {
3301                foreach ($namespace as $ns) {
3302                    if (strlen($ns[0])) {
3303                        if ((strlen($ns[0])>1 && $mailbox == substr($ns[0], 0, -1))
3304                            || strpos($mailbox, $ns[0]) === 0
3305                        ) {
3306                            return $type;
3307                        }
3308                    }
3309                }
3310            }
3311        }
3312
3313        return 'personal';
3314    }
3315
3316
3317    /**
3318     * Modify folder name according to namespace.
3319     * For output it removes prefix of the personal namespace if it's possible.
3320     * For input it adds the prefix. Use it before creating a folder in root
3321     * of the folders tree.
3322     *
3323     * @param string $mailbox Folder name
3324     * @param string $mode    Mode name (out/in)
3325     *
3326     * @return string Folder name
3327     */
3328    function mod_mailbox($mailbox, $mode = 'out')
3329    {
3330        if (!strlen($mailbox)) {
3331            return $mailbox;
3332        }
3333
3334        $prefix     = $this->namespace['prefix']; // see set_env()
3335        $prefix_len = strlen($prefix);
3336
3337        if (!$prefix_len) {
3338            return $mailbox;
3339        }
3340
3341        // remove prefix for output
3342        if ($mode == 'out') {
3343            if (substr($mailbox, 0, $prefix_len) === $prefix) {
3344                return substr($mailbox, $prefix_len);
3345            }
3346        }
3347        // add prefix for input (e.g. folder creation)
3348        else {
3349            return $prefix . $mailbox;
3350        }
3351
3352        return $mailbox;
3353    }
3354
3355
3356    /**
3357     * Gets folder options from LIST response, e.g. \Noselect, \Noinferiors
3358     *
3359     * @param string $mailbox Folder name
3360     * @param bool   $force   Set to True if options should be refreshed
3361     *                        Options are available after LIST command only
3362     *
3363     * @return array Options list
3364     */
3365    function mailbox_options($mailbox, $force=false)
3366    {
3367        if ($mailbox == 'INBOX') {
3368            return array();
3369        }
3370
3371        if (!is_array($this->conn->data['LIST']) || !is_array($this->conn->data['LIST'][$mailbox])) {
3372            if ($force) {
3373                $this->conn->listMailboxes('', $mailbox);
3374            }
3375            else {
3376                return array();
3377            }
3378        }
3379
3380        $opts = $this->conn->data['LIST'][$mailbox];
3381
3382        return is_array($opts) ? $opts : array();
3383    }
3384
3385
3386    /**
3387     * Gets connection (and current mailbox) data: UIDVALIDITY, EXISTS, RECENT,
3388     * PERMANENTFLAGS, UIDNEXT, UNSEEN
3389     *
3390     * @param string $mailbox Folder name
3391     *
3392     * @return array Data
3393     */
3394    function mailbox_data($mailbox)
3395    {
3396        if (!strlen($mailbox))
3397            $mailbox = $this->mailbox !== null ? $this->mailbox : 'INBOX';
3398
3399        if ($this->conn->selected != $mailbox) {
3400            if ($this->conn->select($mailbox))
3401                $this->mailbox = $mailbox;
3402        }
3403
3404        $data = $this->conn->data;
3405
3406        // add (E)SEARCH result for ALL UNDELETED query
3407        if (!empty($this->icache['undeleted_idx']) && $this->icache['undeleted_idx'][0] == $mailbox) {
3408            $data['ALL_UNDELETED']   = $this->icache['undeleted_idx'][1];
3409            $data['COUNT_UNDELETED'] = $this->icache['undeleted_idx'][2];
3410        }
3411
3412        return $data;
3413    }
3414
3415
3416    /**
3417     * Returns extended information about the folder
3418     *
3419     * @param string $mailbox Folder name
3420     *
3421     * @return array Data
3422     */
3423    function mailbox_info($mailbox)
3424    {
3425        $acl       = $this->get_capability('ACL');
3426        $namespace = $this->get_namespace();
3427        $options   = array();
3428
3429        // check if the folder is a namespace prefix
3430        if (!empty($namespace)) {
3431            $mbox = $mailbox . $this->delimiter;
3432            foreach ($namespace as $ns) {
3433                if (!empty($ns)) {
3434                    foreach ($ns as $item) {
3435                        if ($item[0] === $mbox) {
3436                            $options['is_root'] = true;
3437                            break 2;
3438                        }
3439                    }
3440                }
3441            }
3442        }
3443        // check if the folder is other user virtual-root
3444        if (!$options['is_root'] && !empty($namespace) && !empty($namespace['other'])) {
3445            $parts = explode($this->delimiter, $mailbox);
3446            if (count($parts) == 2) {
3447                $mbox = $parts[0] . $this->delimiter;
3448                foreach ($namespace['other'] as $item) {
3449                    if ($item[0] === $mbox) {
3450                        $options['is_root'] = true;
3451                        break;
3452                    }
3453                }
3454            }
3455        }
3456
3457        $options['name']      = $mailbox;
3458        $options['options']   = $this->mailbox_options($mailbox, true);
3459        $options['namespace'] = $this->mailbox_namespace($mailbox);
3460        $options['rights']    = $acl && !$options['is_root'] ? (array)$this->my_rights($mailbox) : array();
3461        $options['special']   = in_array($mailbox, $this->default_folders);
3462
3463        // Set 'noselect' and 'norename' flags
3464        if (is_array($options['options'])) {
3465            foreach ($options['options'] as $opt) {
3466                $opt = strtolower($opt);
3467                if ($opt == '\noselect' || $opt == '\nonexistent') {
3468                    $options['noselect'] = true;
3469                }
3470            }
3471        }
3472        else {
3473            $options['noselect'] = true;
3474        }
3475
3476        if (!empty($options['rights'])) {
3477            $options['norename'] = !in_array('x', $options['rights']);
3478            if (!$options['noselect']) {
3479                $options['noselect'] = !in_array('r', $options['rights']);
3480            }
3481        }
3482        else {
3483            $options['norename'] = $options['is_root'] || $options['namespace'] != 'personal';
3484        }
3485
3486        return $options;
3487    }
3488
3489
3490    /**
3491     * Get message header names for rcube_imap_generic::fetchHeader(s)
3492     *
3493     * @return string Space-separated list of header names
3494     */
3495    private function get_fetch_headers()
3496    {
3497        $headers = explode(' ', $this->fetch_add_headers);
3498        $headers = array_map('strtoupper', $headers);
3499
3500        if ($this->messages_caching || $this->get_all_headers)
3501            $headers = array_merge($headers, $this->all_headers);
3502
3503        return implode(' ', array_unique($headers));
3504    }
3505
3506
3507    /* -----------------------------------------
3508     *   ACL and METADATA/ANNOTATEMORE methods
3509     * ----------------------------------------*/
3510
3511    /**
3512     * Changes the ACL on the specified mailbox (SETACL)
3513     *
3514     * @param string $mailbox Mailbox name
3515     * @param string $user    User name
3516     * @param string $acl     ACL string
3517     *
3518     * @return boolean True on success, False on failure
3519     *
3520     * @access public
3521     * @since 0.5-beta
3522     */
3523    function set_acl($mailbox, $user, $acl)
3524    {
3525        if ($this->get_capability('ACL'))
3526            return $this->conn->setACL($mailbox, $user, $acl);
3527
3528        return false;
3529    }
3530
3531
3532    /**
3533     * Removes any <identifier,rights> pair for the
3534     * specified user from the ACL for the specified
3535     * mailbox (DELETEACL)
3536     *
3537     * @param string $mailbox Mailbox name
3538     * @param string $user    User name
3539     *
3540     * @return boolean True on success, False on failure
3541     *
3542     * @access public
3543     * @since 0.5-beta
3544     */
3545    function delete_acl($mailbox, $user)
3546    {
3547        if ($this->get_capability('ACL'))
3548            return $this->conn->deleteACL($mailbox, $user);
3549
3550        return false;
3551    }
3552
3553
3554    /**
3555     * Returns the access control list for mailbox (GETACL)
3556     *
3557     * @param string $mailbox Mailbox name
3558     *
3559     * @return array User-rights array on success, NULL on error
3560     * @access public
3561     * @since 0.5-beta
3562     */
3563    function get_acl($mailbox)
3564    {
3565        if ($this->get_capability('ACL'))
3566            return $this->conn->getACL($mailbox);
3567
3568        return NULL;
3569    }
3570
3571
3572    /**
3573     * Returns information about what rights can be granted to the
3574     * user (identifier) in the ACL for the mailbox (LISTRIGHTS)
3575     *
3576     * @param string $mailbox Mailbox name
3577     * @param string $user    User name
3578     *
3579     * @return array List of user rights
3580     * @access public
3581     * @since 0.5-beta
3582     */
3583    function list_rights($mailbox, $user)
3584    {
3585        if ($this->get_capability('ACL'))
3586            return $this->conn->listRights($mailbox, $user);
3587
3588        return NULL;
3589    }
3590
3591
3592    /**
3593     * Returns the set of rights that the current user has to
3594     * mailbox (MYRIGHTS)
3595     *
3596     * @param string $mailbox Mailbox name
3597     *
3598     * @return array MYRIGHTS response on success, NULL on error
3599     * @access public
3600     * @since 0.5-beta
3601     */
3602    function my_rights($mailbox)
3603    {
3604        if ($this->get_capability('ACL'))
3605            return $this->conn->myRights($mailbox);
3606
3607        return NULL;
3608    }
3609
3610
3611    /**
3612     * Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
3613     *
3614     * @param string $mailbox Mailbox name (empty for server metadata)
3615     * @param array  $entries Entry-value array (use NULL value as NIL)
3616     *
3617     * @return boolean True on success, False on failure
3618     * @access public
3619     * @since 0.5-beta
3620     */
3621    function set_metadata($mailbox, $entries)
3622    {
3623        if ($this->get_capability('METADATA') ||
3624            (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
3625        ) {
3626            return $this->conn->setMetadata($mailbox, $entries);
3627        }
3628        else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
3629            foreach ((array)$entries as $entry => $value) {
3630                list($ent, $attr) = $this->md2annotate($entry);
3631                $entries[$entry] = array($ent, $attr, $value);
3632            }
3633            return $this->conn->setAnnotation($mailbox, $entries);
3634        }
3635
3636        return false;
3637    }
3638
3639
3640    /**
3641     * Unsets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
3642     *
3643     * @param string $mailbox Mailbox name (empty for server metadata)
3644     * @param array  $entries Entry names array
3645     *
3646     * @return boolean True on success, False on failure
3647     *
3648     * @access public
3649     * @since 0.5-beta
3650     */
3651    function delete_metadata($mailbox, $entries)
3652    {
3653        if ($this->get_capability('METADATA') || 
3654            (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
3655        ) {
3656            return $this->conn->deleteMetadata($mailbox, $entries);
3657        }
3658        else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
3659            foreach ((array)$entries as $idx => $entry) {
3660                list($ent, $attr) = $this->md2annotate($entry);
3661                $entries[$idx] = array($ent, $attr, NULL);
3662            }
3663            return $this->conn->setAnnotation($mailbox, $entries);
3664        }
3665
3666        return false;
3667    }
3668
3669
3670    /**
3671     * Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION)
3672     *
3673     * @param string $mailbox Mailbox name (empty for server metadata)
3674     * @param array  $entries Entries
3675     * @param array  $options Command options (with MAXSIZE and DEPTH keys)
3676     *
3677     * @return array Metadata entry-value hash array on success, NULL on error
3678     *
3679     * @access public
3680     * @since 0.5-beta
3681     */
3682    function get_metadata($mailbox, $entries, $options=array())
3683    {
3684        if ($this->get_capability('METADATA') || 
3685            (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
3686        ) {
3687            return $this->conn->getMetadata($mailbox, $entries, $options);
3688        }
3689        else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
3690            $queries = array();
3691            $res     = array();
3692
3693            // Convert entry names
3694            foreach ((array)$entries as $entry) {
3695                list($ent, $attr) = $this->md2annotate($entry);
3696                $queries[$attr][] = $ent;
3697            }
3698
3699            // @TODO: Honor MAXSIZE and DEPTH options
3700            foreach ($queries as $attrib => $entry)
3701                if ($result = $this->conn->getAnnotation($mailbox, $entry, $attrib))
3702                    $res = array_merge($res, $result);
3703
3704            return $res;
3705        }
3706
3707        return NULL;
3708    }
3709
3710
3711    /**
3712     * Converts the METADATA extension entry name into the correct
3713     * entry-attrib names for older ANNOTATEMORE version.
3714     *
3715     * @param string $entry Entry name
3716     *
3717     * @return array Entry-attribute list, NULL if not supported (?)
3718     */
3719    private function md2annotate($entry)
3720    {
3721        if (substr($entry, 0, 7) == '/shared') {
3722            return array(substr($entry, 7), 'value.shared');
3723        }
3724        else if (substr($entry, 0, 8) == '/private') {
3725            return array(substr($entry, 8), 'value.priv');
3726        }
3727
3728        // @TODO: log error
3729        return NULL;
3730    }
3731
3732
3733    /* --------------------------------
3734     *   internal caching methods
3735     * --------------------------------*/
3736
3737    /**
3738     * Enable or disable indexes caching
3739     *
3740     * @param boolean $type Cache type (@see rcmail::get_cache)
3741     * @access public
3742     */
3743    function set_caching($type)
3744    {
3745        if ($type) {
3746            $this->caching = true;
3747        }
3748        else {
3749            if ($this->cache)
3750                $this->cache->close();
3751            $this->cache = null;
3752            $this->caching = false;
3753        }
3754    }
3755
3756    /**
3757     * Getter for IMAP cache object
3758     */
3759    private function get_cache_engine()
3760    {
3761        if ($this->caching && !$this->cache) {
3762            $rcmail = rcmail::get_instance();
3763            $this->cache = $rcmail->get_cache('IMAP', $type);
3764        }
3765
3766        return $this->cache;
3767    }
3768
3769    /**
3770     * Returns cached value
3771     *
3772     * @param string $key Cache key
3773     * @return mixed
3774     * @access public
3775     */
3776    function get_cache($key)
3777    {
3778        if ($cache = $this->get_cache_engine()) {
3779            return $cache->get($key);
3780        }
3781    }
3782
3783    /**
3784     * Update cache
3785     *
3786     * @param string $key  Cache key
3787     * @param mixed  $data Data
3788     * @access public
3789     */
3790    function update_cache($key, $data)
3791    {
3792        if ($cache = $this->get_cache_engine()) {
3793            $cache->set($key, $data);
3794        }
3795    }
3796
3797    /**
3798     * Clears the cache.
3799     *
3800     * @param string  $key         Cache key name or pattern
3801     * @param boolean $prefix_mode Enable it to clear all keys starting
3802     *                             with prefix specified in $key
3803     * @access public
3804     */
3805    function clear_cache($key=null, $prefix_mode=false)
3806    {
3807        if ($cache = $this->get_cache_engine()) {
3808            $cache->remove($key, $prefix_mode);
3809        }
3810    }
3811
3812
3813    /* --------------------------------
3814     *   message caching methods
3815     * --------------------------------*/
3816
3817    /**
3818     * Enable or disable messages caching
3819     *
3820     * @param boolean $set Flag
3821     */
3822    function set_messages_caching($set)
3823    {
3824        if ($set) {
3825            $this->messages_caching = true;
3826        }
3827        else {
3828            if ($this->mcache)
3829                $this->mcache->close();
3830            $this->mcache = null;
3831            $this->messages_caching = false;
3832        }
3833    }
3834
3835    /**
3836     * Getter for messages cache object
3837     */
3838    private function get_mcache_engine()
3839    {
3840        if ($this->messages_caching && !$this->mcache) {
3841            $rcmail = rcmail::get_instance();
3842            if ($dbh = $rcmail->get_dbh()) {
3843                $this->mcache = new rcube_imap_cache(
3844                    $dbh, $this, $rcmail->user->ID, $this->skip_deleted);
3845            }
3846        }
3847
3848        return $this->mcache;
3849    }
3850
3851    /**
3852     * Clears the messages cache.
3853     *
3854     * @param string $mailbox Folder name
3855     * @param array  $uids    Optional message UIDs to remove from cache
3856     */
3857    function clear_message_cache($mailbox = null, $uids = null)
3858    {
3859        if ($mcache = $this->get_mcache_engine()) {
3860            $mcache->clear($mailbox, $uids);
3861        }
3862    }
3863
3864
3865
3866    /* --------------------------------
3867     *   encoding/decoding methods
3868     * --------------------------------*/
3869
3870    /**
3871     * Split an address list into a structured array list
3872     *
3873     * @param string  $input  Input string
3874     * @param int     $max    List only this number of addresses
3875     * @param boolean $decode Decode address strings
3876     * @return array  Indexed list of addresses
3877     */
3878    function decode_address_list($input, $max=null, $decode=true)
3879    {
3880        $a = $this->_parse_address_list($input, $decode);
3881        $out = array();
3882        // Special chars as defined by RFC 822 need to in quoted string (or escaped).
3883        $special_chars = '[\(\)\<\>\\\.\[\]@,;:"]';
3884
3885        if (!is_array($a))
3886            return $out;
3887
3888        $c = count($a);
3889        $j = 0;
3890
3891        foreach ($a as $val) {
3892            $j++;
3893            $address = trim($val['address']);
3894            $name    = trim($val['name']);
3895
3896            if ($name && $address && $name != $address)
3897                $string = sprintf('%s <%s>', preg_match("/$special_chars/", $name) ? '"'.addcslashes($name, '"').'"' : $name, $address);
3898            else if ($address)
3899                $string = $address;
3900            else if ($name)
3901                $string = $name;
3902
3903            $out[$j] = array(
3904                'name'   => $name,
3905                'mailto' => $address,
3906                'string' => $string
3907            );
3908
3909            if ($max && $j==$max)
3910                break;
3911        }
3912
3913        return $out;
3914    }
3915
3916
3917    /**
3918     * Decode a message header value
3919     *
3920     * @param string  $input         Header value
3921     * @param boolean $remove_quotas Remove quotes if necessary
3922     * @return string Decoded string
3923     */
3924    function decode_header($input, $remove_quotes=false)
3925    {
3926        $str = rcube_imap::decode_mime_string((string)$input, $this->default_charset);
3927        if ($str[0] == '"' && $remove_quotes)
3928            $str = str_replace('"', '', $str);
3929
3930        return $str;
3931    }
3932
3933
3934    /**
3935     * Decode a mime-encoded string to internal charset
3936     *
3937     * @param string $input    Header value
3938     * @param string $fallback Fallback charset if none specified
3939     *
3940     * @return string Decoded string
3941     * @static
3942     */
3943    public static function decode_mime_string($input, $fallback=null)
3944    {
3945        if (!empty($fallback)) {
3946            $default_charset = $fallback;
3947        }
3948        else {
3949            $default_charset = rcmail::get_instance()->config->get('default_charset', 'ISO-8859-1');
3950        }
3951
3952        // rfc: all line breaks or other characters not found
3953        // in the Base64 Alphabet must be ignored by decoding software
3954        // delete all blanks between MIME-lines, differently we can
3955        // receive unnecessary blanks and broken utf-8 symbols
3956        $input = preg_replace("/\?=\s+=\?/", '?==?', $input);
3957
3958        // encoded-word regexp
3959        $re = '/=\?([^?]+)\?([BbQq])\?([^?\n]*)\?=/';
3960
3961        // Find all RFC2047's encoded words
3962        if (preg_match_all($re, $input, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {
3963            // Initialize variables
3964            $tmp   = array();
3965            $out   = '';
3966            $start = 0;
3967
3968            foreach ($matches as $idx => $m) {
3969                $pos      = $m[0][1];
3970                $charset  = $m[1][0];
3971                $encoding = $m[2][0];
3972                $text     = $m[3][0];
3973                $length   = strlen($m[0][0]);
3974
3975                // Append everything that is before the text to be decoded
3976                if ($start != $pos) {
3977                    $substr = substr($input, $start, $pos-$start);
3978                    $out   .= rcube_charset_convert($substr, $default_charset);
3979                    $start  = $pos;
3980                }
3981                $start += $length;
3982
3983                // Per RFC2047, each string part "MUST represent an integral number
3984                // of characters . A multi-octet character may not be split across
3985                // adjacent encoded-words." However, some mailers break this, so we
3986                // try to handle characters spanned across parts anyway by iterating
3987                // through and aggregating sequential encoded parts with the same
3988                // character set and encoding, then perform the decoding on the
3989                // aggregation as a whole.
3990
3991                $tmp[] = $text;
3992                if ($next_match = $matches[$idx+1]) {
3993                    if ($next_match[0][1] == $start
3994                        && $next_match[1][0] == $charset
3995                        && $next_match[2][0] == $encoding
3996                    ) {
3997                        continue;
3998                    }
3999                }
4000
4001                $count = count($tmp);
4002                $text  = '';
4003
4004                // Decode and join encoded-word's chunks
4005                if ($encoding == 'B' || $encoding == 'b') {
4006                    // base64 must be decoded a segment at a time
4007                    for ($i=0; $i<$count; $i++)
4008                        $text .= base64_decode($tmp[$i]);
4009                }
4010                else { //if ($encoding == 'Q' || $encoding == 'q') {
4011                    // quoted printable can be combined and processed at once
4012                    for ($i=0; $i<$count; $i++)
4013                        $text .= $tmp[$i];
4014
4015                    $text = str_replace('_', ' ', $text);
4016                    $text = quoted_printable_decode($text);
4017                }
4018
4019                $out .= rcube_charset_convert($text, $charset);
4020                $tmp = array();
4021            }
4022
4023            // add the last part of the input string
4024            if ($start != strlen($input)) {
4025                $out .= rcube_charset_convert(substr($input, $start), $default_charset);
4026            }
4027
4028            // return the results
4029            return $out;
4030        }
4031
4032        // no encoding information, use fallback
4033        return rcube_charset_convert($input, $default_charset);
4034    }
4035
4036
4037    /**
4038     * Decode a mime part
4039     *
4040     * @param string $input    Input string
4041     * @param string $encoding Part encoding
4042     * @return string Decoded string
4043     */
4044    function mime_decode($input, $encoding='7bit')
4045    {
4046        switch (strtolower($encoding)) {
4047        case 'quoted-printable':
4048            return quoted_printable_decode($input);
4049        case 'base64':
4050            return base64_decode($input);
4051        case 'x-uuencode':
4052        case 'x-uue':
4053        case 'uue':
4054        case 'uuencode':
4055            return convert_uudecode($input);
4056        case '7bit':
4057        default:
4058            return $input;
4059        }
4060    }
4061
4062
4063    /**
4064     * Convert body charset to RCMAIL_CHARSET according to the ctype_parameters
4065     *
4066     * @param string $body        Part body to decode
4067     * @param string $ctype_param Charset to convert from
4068     * @return string Content converted to internal charset
4069     */
4070    function charset_decode($body, $ctype_param)
4071    {
4072        if (is_array($ctype_param) && !empty($ctype_param['charset']))
4073            return rcube_charset_convert($body, $ctype_param['charset']);
4074
4075        // defaults to what is specified in the class header
4076        return rcube_charset_convert($body,  $this->default_charset);
4077    }
4078
4079
4080    /* --------------------------------
4081     *         private methods
4082     * --------------------------------*/
4083
4084    /**
4085     * Validate the given input and save to local properties
4086     *
4087     * @param string $sort_field Sort column
4088     * @param string $sort_order Sort order
4089     * @access private
4090     */
4091    private function _set_sort_order($sort_field, $sort_order)
4092    {
4093        if ($sort_field != null)
4094            $this->sort_field = asciiwords($sort_field);
4095        if ($sort_order != null)
4096            $this->sort_order = strtoupper($sort_order) == 'DESC' ? 'DESC' : 'ASC';
4097    }
4098
4099
4100    /**
4101     * Sort mailboxes first by default folders and then in alphabethical order
4102     *
4103     * @param array $a_folders Mailboxes list
4104     * @access private
4105     */
4106    private function _sort_mailbox_list($a_folders)
4107    {
4108        $a_out = $a_defaults = $folders = array();
4109
4110        $delimiter = $this->get_hierarchy_delimiter();
4111
4112        // find default folders and skip folders starting with '.'
4113        foreach ($a_folders as $i => $folder) {
4114            if ($folder[0] == '.')
4115                continue;
4116
4117            if (($p = array_search($folder, $this->default_folders)) !== false && !$a_defaults[$p])
4118                $a_defaults[$p] = $folder;
4119            else
4120                $folders[$folder] = rcube_charset_convert($folder, 'UTF7-IMAP');
4121        }
4122
4123        // sort folders and place defaults on the top
4124        asort($folders, SORT_LOCALE_STRING);
4125        ksort($a_defaults);
4126        $folders = array_merge($a_defaults, array_keys($folders));
4127
4128        // finally we must rebuild the list to move
4129        // subfolders of default folders to their place...
4130        // ...also do this for the rest of folders because
4131        // asort() is not properly sorting case sensitive names
4132        while (list($key, $folder) = each($folders)) {
4133            // set the type of folder name variable (#1485527)
4134            $a_out[] = (string) $folder;
4135            unset($folders[$key]);
4136            $this->_rsort($folder, $delimiter, $folders, $a_out);
4137        }
4138
4139        return $a_out;
4140    }
4141
4142
4143    /**
4144     * @access private
4145     */
4146    private function _rsort($folder, $delimiter, &$list, &$out)
4147    {
4148        while (list($key, $name) = each($list)) {
4149                if (strpos($name, $folder.$delimiter) === 0) {
4150                    // set the type of folder name variable (#1485527)
4151                $out[] = (string) $name;
4152                    unset($list[$key]);
4153                    $this->_rsort($name, $delimiter, $list, $out);
4154                }
4155        }
4156        reset($list);
4157    }
4158
4159
4160    /**
4161     * Finds message sequence ID for specified UID
4162     *
4163     * @param int    $uid      Message UID
4164     * @param string $mailbox  Mailbox name
4165     * @param bool   $force    True to skip cache
4166     *
4167     * @return int Message (sequence) ID
4168     */
4169    function uid2id($uid, $mailbox = null, $force = false)
4170    {
4171        if (!strlen($mailbox)) {
4172            $mailbox = $this->mailbox;
4173        }
4174
4175        if (!empty($this->uid_id_map[$mailbox][$uid])) {
4176            return $this->uid_id_map[$mailbox][$uid];
4177        }
4178
4179        if (!$force && ($mcache = $this->get_mcache_engine()))
4180            $id = $mcache->uid2id($mailbox, $uid);
4181
4182        if (empty($id))
4183            $id = $this->conn->UID2ID($mailbox, $uid);
4184
4185        $this->uid_id_map[$mailbox][$uid] = $id;
4186
4187        return $id;
4188    }
4189
4190
4191    /**
4192     * Find UID of the specified message sequence ID
4193     *
4194     * @param int    $id       Message (sequence) ID
4195     * @param string $mailbox  Mailbox name
4196     * @param bool   $force    True to skip cache
4197     *
4198     * @return int Message UID
4199     */
4200    function id2uid($id, $mailbox = null, $force = false)
4201    {
4202        if (!strlen($mailbox)) {
4203            $mailbox = $this->mailbox;
4204        }
4205
4206        if ($uid = array_search($id, (array)$this->uid_id_map[$mailbox])) {
4207            return $uid;
4208        }
4209
4210        if (!$force && ($mcache = $this->get_mcache_engine()))
4211            $uid = $mcache->id2uid($mailbox, $id);
4212
4213        if (empty($uid))
4214            $uid = $this->conn->ID2UID($mailbox, $id);
4215
4216        $this->uid_id_map[$mailbox][$uid] = $id;
4217
4218        return $uid;
4219    }
4220
4221
4222    /**
4223     * Subscribe/unsubscribe a list of mailboxes and update local cache
4224     * @access private
4225     */
4226    private function _change_subscription($a_mboxes, $mode)
4227    {
4228        $updated = false;
4229
4230        if (is_array($a_mboxes))
4231            foreach ($a_mboxes as $i => $mailbox) {
4232                $a_mboxes[$i] = $mailbox;
4233
4234                if ($mode == 'subscribe')
4235                    $updated = $this->conn->subscribe($mailbox);
4236                else if ($mode == 'unsubscribe')
4237                    $updated = $this->conn->unsubscribe($mailbox);
4238            }
4239
4240        // clear cached mailbox list(s)
4241        if ($updated) {
4242            $this->clear_cache('mailboxes', true);
4243        }
4244
4245        return $updated;
4246    }
4247
4248
4249    /**
4250     * Increde/decrese messagecount for a specific mailbox
4251     * @access private
4252     */
4253    private function _set_messagecount($mailbox, $mode, $increment)
4254    {
4255        $mode = strtoupper($mode);
4256        $a_mailbox_cache = $this->get_cache('messagecount');
4257
4258        if (!is_array($a_mailbox_cache[$mailbox]) || !isset($a_mailbox_cache[$mailbox][$mode]) || !is_numeric($increment))
4259            return false;
4260
4261        // add incremental value to messagecount
4262        $a_mailbox_cache[$mailbox][$mode] += $increment;
4263
4264        // there's something wrong, delete from cache
4265        if ($a_mailbox_cache[$mailbox][$mode] < 0)
4266            unset($a_mailbox_cache[$mailbox][$mode]);
4267
4268        // write back to cache
4269        $this->update_cache('messagecount', $a_mailbox_cache);
4270
4271        return true;
4272    }
4273
4274
4275    /**
4276     * Remove messagecount of a specific mailbox from cache
4277     * @access private
4278     */
4279    private function _clear_messagecount($mailbox, $mode=null)
4280    {
4281        $a_mailbox_cache = $this->get_cache('messagecount');
4282
4283        if (is_array($a_mailbox_cache[$mailbox])) {
4284            if ($mode) {
4285                unset($a_mailbox_cache[$mailbox][$mode]);
4286            }
4287            else {
4288                unset($a_mailbox_cache[$mailbox]);
4289            }
4290            $this->update_cache('messagecount', $a_mailbox_cache);
4291        }
4292    }
4293
4294
4295    /**
4296     * Split RFC822 header string into an associative array
4297     * @access private
4298     */
4299    private function _parse_headers($headers)
4300    {
4301        $a_headers = array();
4302        $headers = preg_replace('/\r?\n(\t| )+/', ' ', $headers);
4303        $lines = explode("\n", $headers);
4304        $c = count($lines);
4305
4306        for ($i=0; $i<$c; $i++) {
4307            if ($p = strpos($lines[$i], ': ')) {
4308                $field = strtolower(substr($lines[$i], 0, $p));
4309                $value = trim(substr($lines[$i], $p+1));
4310                if (!empty($value))
4311                    $a_headers[$field] = $value;
4312            }
4313        }
4314
4315        return $a_headers;
4316    }
4317
4318
4319    /**
4320     * @access private
4321     */
4322    private function _parse_address_list($str, $decode=true)
4323    {
4324        // remove any newlines and carriage returns before
4325        $str = preg_replace('/\r?\n(\s|\t)?/', ' ', $str);
4326
4327        // extract list items, remove comments
4328        $str = self::explode_header_string(',;', $str, true);
4329        $result = array();
4330
4331        foreach ($str as $key => $val) {
4332            $name    = '';
4333            $address = '';
4334            $val     = trim($val);
4335
4336            if (preg_match('/(.*)<(\S+@\S+)>$/', $val, $m)) {
4337                $address = $m[2];
4338                $name    = trim($m[1]);
4339            }
4340            else if (preg_match('/^(\S+@\S+)$/', $val, $m)) {
4341                $address = $m[1];
4342                $name    = '';
4343            }
4344            else {
4345                $name = $val;
4346            }
4347
4348            // dequote and/or decode name
4349            if ($name) {
4350                if ($name[0] == '"') {
4351                    $name = substr($name, 1, -1);
4352                    $name = stripslashes($name);
4353                }
4354                if ($decode) {
4355                    $name = $this->decode_header($name);
4356                }
4357            }
4358
4359            if (!$address && $name) {
4360                $address = $name;
4361            }
4362
4363            if ($address) {
4364                $result[$key] = array('name' => $name, 'address' => $address);
4365            }
4366        }
4367
4368        return $result;
4369    }
4370
4371
4372    /**
4373     * Explodes header (e.g. address-list) string into array of strings
4374     * using specified separator characters with proper handling
4375     * of quoted-strings and comments (RFC2822)
4376     *
4377     * @param string $separator       String containing separator characters
4378     * @param string $str             Header string
4379     * @param bool   $remove_comments Enable to remove comments
4380     *
4381     * @return array Header items
4382     */
4383    static function explode_header_string($separator, $str, $remove_comments=false)
4384    {
4385        $length  = strlen($str);
4386        $result  = array();
4387        $quoted  = false;
4388        $comment = 0;
4389        $out     = '';
4390
4391        for ($i=0; $i<$length; $i++) {
4392            // we're inside a quoted string
4393            if ($quoted) {
4394                if ($str[$i] == '"') {
4395                    $quoted = false;
4396                }
4397                else if ($str[$i] == '\\') {
4398                    if ($comment <= 0) {
4399                        $out .= '\\';
4400                    }
4401                    $i++;
4402                }
4403            }
4404            // we're inside a comment string
4405            else if ($comment > 0) {
4406                    if ($str[$i] == ')') {
4407                        $comment--;
4408                    }
4409                    else if ($str[$i] == '(') {
4410                        $comment++;
4411                    }
4412                    else if ($str[$i] == '\\') {
4413                        $i++;
4414                    }
4415                    continue;
4416            }
4417            // separator, add to result array
4418            else if (strpos($separator, $str[$i]) !== false) {
4419                    if ($out) {
4420                        $result[] = $out;
4421                    }
4422                    $out = '';
4423                    continue;
4424            }
4425            // start of quoted string
4426            else if ($str[$i] == '"') {
4427                    $quoted = true;
4428            }
4429            // start of comment
4430            else if ($remove_comments && $str[$i] == '(') {
4431                    $comment++;
4432            }
4433
4434            if ($comment <= 0) {
4435                $out .= $str[$i];
4436            }
4437        }
4438
4439        if ($out && $comment <= 0) {
4440            $result[] = $out;
4441        }
4442
4443        return $result;
4444    }
4445
4446
4447    /**
4448     * This is our own debug handler for the IMAP connection
4449     * @access public
4450     */
4451    public function debug_handler(&$imap, $message)
4452    {
4453        write_log('imap', $message);
4454    }
4455
4456}  // end class rcube_imap
4457
4458
4459/**
4460 * Class representing a message part
4461 *
4462 * @package Mail
4463 */
4464class rcube_message_part
4465{
4466    var $mime_id = '';
4467    var $ctype_primary = 'text';
4468    var $ctype_secondary = 'plain';
4469    var $mimetype = 'text/plain';
4470    var $disposition = '';
4471    var $filename = '';
4472    var $encoding = '8bit';
4473    var $charset = '';
4474    var $size = 0;
4475    var $headers = array();
4476    var $d_parameters = array();
4477    var $ctype_parameters = array();
4478
4479    function __clone()
4480    {
4481        if (isset($this->parts))
4482            foreach ($this->parts as $idx => $part)
4483                if (is_object($part))
4484                        $this->parts[$idx] = clone $part;
4485    }
4486}
4487
4488
4489/**
4490 * Class for sorting an array of rcube_mail_header objects in a predetermined order.
4491 *
4492 * @package Mail
4493 * @author Eric Stadtherr
4494 */
4495class rcube_header_sorter
4496{
4497    private $seqs = array();
4498    private $uids = array();
4499
4500
4501    /**
4502     * Set the predetermined sort order.
4503     *
4504     * @param array $index  Numerically indexed array of IMAP ID or UIDs
4505     * @param bool  $is_uid Set to true if $index contains UIDs
4506     */
4507    function set_index($index, $is_uid = false)
4508    {
4509        $index = array_flip($index);
4510
4511        if ($is_uid)
4512            $this->uids = $index;
4513        else
4514            $this->seqs = $index;
4515    }
4516
4517    /**
4518     * Sort the array of header objects
4519     *
4520     * @param array $headers Array of rcube_mail_header objects indexed by UID
4521     */
4522    function sort_headers(&$headers)
4523    {
4524        if (!empty($this->uids))
4525            uksort($headers, array($this, "compare_uids"));
4526        else
4527            uasort($headers, array($this, "compare_seqnums"));
4528    }
4529
4530    /**
4531     * Sort method called by uasort()
4532     *
4533     * @param rcube_mail_header $a
4534     * @param rcube_mail_header $b
4535     */
4536    function compare_seqnums($a, $b)
4537    {
4538        // First get the sequence number from the header object (the 'id' field).
4539        $seqa = $a->id;
4540        $seqb = $b->id;
4541
4542        // then find each sequence number in my ordered list
4543        $posa = isset($this->seqs[$seqa]) ? intval($this->seqs[$seqa]) : -1;
4544        $posb = isset($this->seqs[$seqb]) ? intval($this->seqs[$seqb]) : -1;
4545
4546        // return the relative position as the comparison value
4547        return $posa - $posb;
4548    }
4549
4550    /**
4551     * Sort method called by uksort()
4552     *
4553     * @param int $a Array key (UID)
4554     * @param int $b Array key (UID)
4555     */
4556    function compare_uids($a, $b)
4557    {
4558        // then find each sequence number in my ordered list
4559        $posa = isset($this->uids[$a]) ? intval($this->uids[$a]) : -1;
4560        $posb = isset($this->uids[$b]) ? intval($this->uids[$b]) : -1;
4561
4562        // return the relative position as the comparison value
4563        return $posa - $posb;
4564    }
4565}
Note: See TracBrowser for help on using the repository browser.