source: subversion/trunk/roundcubemail/program/include/rcube_imap.php @ 5232

Last change on this file since 5232 was 5232, checked in by alec, 20 months ago
  • Fix locked folder rename option on servers supporting RFC2086 only (#1488089)
  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
File size: 144.2 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-2011, The Roundcube Dev Team                       |
9 | Copyright (C) 2011, Kolab Systems AG                                  |
10 | Licensed under the GNU GPL                                            |
11 |                                                                       |
12 | PURPOSE:                                                              |
13 |   IMAP Engine                                                         |
14 |                                                                       |
15 +-----------------------------------------------------------------------+
16 | Author: Thomas Bruederli <roundcube@gmail.com>                        |
17 | Author: Aleksander Machniak <alec@alec.pl>                            |
18 +-----------------------------------------------------------------------+
19
20 $Id$
21
22*/
23
24
25/**
26 * Interface class for accessing an IMAP server
27 *
28 * @package    Mail
29 * @author     Thomas Bruederli <roundcube@gmail.com>
30 * @author     Aleksander Machniak <alec@alec.pl>
31 * @version    2.0
32 */
33class rcube_imap
34{
35    public $debug_level = 1;
36    public $skip_deleted = false;
37    public $page_size = 10;
38    public $list_page = 1;
39    public $threading = false;
40    public $fetch_add_headers = '';
41    public $get_all_headers = false;
42
43    /**
44     * Instance of rcube_imap_generic
45     *
46     * @var rcube_imap_generic
47     */
48    public $conn;
49
50    /**
51     * Instance of rcube_imap_cache
52     *
53     * @var rcube_imap_cache
54     */
55    private $mcache;
56
57    /**
58     * Instance of rcube_cache
59     *
60     * @var rcube_cache
61     */
62    private $cache;
63
64    /**
65     * Internal (in-memory) cache
66     *
67     * @var array
68     */
69    private $icache = array();
70
71    private $mailbox = 'INBOX';
72    private $delimiter = NULL;
73    private $namespace = NULL;
74    private $sort_field = '';
75    private $sort_order = 'DESC';
76    private $default_charset = 'ISO-8859-1';
77    private $struct_charset = NULL;
78    private $default_folders = array('INBOX');
79    private $uid_id_map = array();
80    private $msg_headers = array();
81    public  $search_set = NULL;
82    public  $search_string = '';
83    private $search_charset = '';
84    private $search_sort_field = '';
85    private $search_threads = false;
86    private $search_sorted = false;
87    private $options = array('auth_method' => 'check');
88    private $host, $user, $pass, $port, $ssl;
89    private $caching = false;
90    private $messages_caching = false;
91
92    /**
93     * All (additional) headers used (in any way) by Roundcube
94     * Not listed here: DATE, FROM, TO, CC, REPLY-TO, SUBJECT, CONTENT-TYPE, LIST-POST
95     * (used for messages listing) are hardcoded in rcube_imap_generic::fetchHeaders()
96     *
97     * @var array
98     * @see rcube_imap::fetch_add_headers
99     */
100    private $all_headers = array(
101        'IN-REPLY-TO',
102        'BCC',
103        'MESSAGE-ID',
104        'CONTENT-TRANSFER-ENCODING',
105        'REFERENCES',
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, $sorted) = $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        // Remove NULL characters (#1486189)
2390        $body = str_replace("\x00", '', $body);
2391
2392        // convert charset (if text or message part)
2393        if ($body && !$skip_charset_conv &&
2394            preg_match('/^(text|message)$/', $o_part->ctype_primary)
2395        ) {
2396            if (!$o_part->charset || strtoupper($o_part->charset) == 'US-ASCII') {
2397                $o_part->charset = $this->default_charset;
2398            }
2399            $body = rcube_charset_convert($body, $o_part->charset);
2400        }
2401
2402        return $body;
2403    }
2404
2405
2406    /**
2407     * Fetch message body of a specific message from the server
2408     *
2409     * @param  int    $uid  Message UID
2410     * @return string $part Message/part body
2411     * @see    rcube_imap::get_message_part()
2412     */
2413    function &get_body($uid, $part=1)
2414    {
2415        $headers = $this->get_headers($uid);
2416        return rcube_charset_convert($this->get_message_part($uid, $part, NULL),
2417            $headers->charset ? $headers->charset : $this->default_charset);
2418    }
2419
2420
2421    /**
2422     * Returns the whole message source as string (or saves to a file)
2423     *
2424     * @param int      $uid Message UID
2425     * @param resource $fp  File pointer to save the message
2426     *
2427     * @return string Message source string
2428     */
2429    function &get_raw_body($uid, $fp=null)
2430    {
2431        return $this->conn->handlePartBody($this->mailbox, $uid,
2432            true, null, null, false, $fp);
2433    }
2434
2435
2436    /**
2437     * Returns the message headers as string
2438     *
2439     * @param int $uid  Message UID
2440     * @return string Message headers string
2441     */
2442    function &get_raw_headers($uid)
2443    {
2444        return $this->conn->fetchPartHeader($this->mailbox, $uid, true);
2445    }
2446
2447
2448    /**
2449     * Sends the whole message source to stdout
2450     *
2451     * @param int $uid Message UID
2452     */
2453    function print_raw_body($uid)
2454    {
2455        $this->conn->handlePartBody($this->mailbox, $uid, true, NULL, NULL, true);
2456    }
2457
2458
2459    /**
2460     * Set message flag to one or several messages
2461     *
2462     * @param mixed   $uids       Message UIDs as array or comma-separated string, or '*'
2463     * @param string  $flag       Flag to set: SEEN, UNDELETED, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
2464     * @param string  $mailbox    Folder name
2465     * @param boolean $skip_cache True to skip message cache clean up
2466     *
2467     * @return boolean  Operation status
2468     */
2469    function set_flag($uids, $flag, $mailbox=null, $skip_cache=false)
2470    {
2471        if (!strlen($mailbox)) {
2472            $mailbox = $this->mailbox;
2473        }
2474
2475        $flag = strtoupper($flag);
2476        list($uids, $all_mode) = $this->_parse_uids($uids, $mailbox);
2477
2478        if (strpos($flag, 'UN') === 0)
2479            $result = $this->conn->unflag($mailbox, $uids, substr($flag, 2));
2480        else
2481            $result = $this->conn->flag($mailbox, $uids, $flag);
2482
2483        if ($result) {
2484            // reload message headers if cached
2485            // @TODO: update flags instead removing from cache
2486            if (!$skip_cache && ($mcache = $this->get_mcache_engine())) {
2487                $status = strpos($flag, 'UN') !== 0;
2488                $mflag  = preg_replace('/^UN/', '', $flag);
2489                $mcache->change_flag($mailbox, $all_mode ? null : explode(',', $uids),
2490                    $mflag, $status);
2491            }
2492
2493            // clear cached counters
2494            if ($flag == 'SEEN' || $flag == 'UNSEEN') {
2495                $this->_clear_messagecount($mailbox, 'SEEN');
2496                $this->_clear_messagecount($mailbox, 'UNSEEN');
2497            }
2498            else if ($flag == 'DELETED') {
2499                $this->_clear_messagecount($mailbox, 'DELETED');
2500            }
2501        }
2502
2503        return $result;
2504    }
2505
2506
2507    /**
2508     * Remove message flag for one or several messages
2509     *
2510     * @param mixed  $uids    Message UIDs as array or comma-separated string, or '*'
2511     * @param string $flag    Flag to unset: SEEN, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
2512     * @param string $mailbox Folder name
2513     *
2514     * @return int   Number of flagged messages, -1 on failure
2515     * @see set_flag
2516     */
2517    function unset_flag($uids, $flag, $mailbox=null)
2518    {
2519        return $this->set_flag($uids, 'UN'.$flag, $mailbox);
2520    }
2521
2522
2523    /**
2524     * Append a mail message (source) to a specific mailbox
2525     *
2526     * @param string  $mailbox Target mailbox
2527     * @param string  $message The message source string or filename
2528     * @param string  $headers Headers string if $message contains only the body
2529     * @param boolean $is_file True if $message is a filename
2530     *
2531     * @return boolean True on success, False on error
2532     */
2533    function save_message($mailbox, &$message, $headers='', $is_file=false)
2534    {
2535        if (!strlen($mailbox)) {
2536            $mailbox = $this->mailbox;
2537        }
2538
2539        // make sure mailbox exists
2540        if ($this->mailbox_exists($mailbox)) {
2541            if ($is_file)
2542                $saved = $this->conn->appendFromFile($mailbox, $message, $headers);
2543            else
2544                $saved = $this->conn->append($mailbox, $message);
2545        }
2546
2547        if ($saved) {
2548            // increase messagecount of the target mailbox
2549            $this->_set_messagecount($mailbox, 'ALL', 1);
2550        }
2551
2552        return $saved;
2553    }
2554
2555
2556    /**
2557     * Move a message from one mailbox to another
2558     *
2559     * @param mixed  $uids      Message UIDs as array or comma-separated string, or '*'
2560     * @param string $to_mbox   Target mailbox
2561     * @param string $from_mbox Source mailbox
2562     * @return boolean True on success, False on error
2563     */
2564    function move_message($uids, $to_mbox, $from_mbox='')
2565    {
2566        if (!strlen($from_mbox)) {
2567            $from_mbox = $this->mailbox;
2568        }
2569
2570        if ($to_mbox === $from_mbox) {
2571            return false;
2572        }
2573
2574        list($uids, $all_mode) = $this->_parse_uids($uids, $from_mbox);
2575
2576        // exit if no message uids are specified
2577        if (empty($uids))
2578            return false;
2579
2580        // make sure mailbox exists
2581        if ($to_mbox != 'INBOX' && !$this->mailbox_exists($to_mbox)) {
2582            if (in_array($to_mbox, $this->default_folders))
2583                $this->create_mailbox($to_mbox, true);
2584            else
2585                return false;
2586        }
2587
2588        $config = rcmail::get_instance()->config;
2589        $to_trash = $to_mbox == $config->get('trash_mbox');
2590
2591        // flag messages as read before moving them
2592        if ($to_trash && $config->get('read_when_deleted')) {
2593            // don't flush cache (4th argument)
2594            $this->set_flag($uids, 'SEEN', $from_mbox, true);
2595        }
2596
2597        // move messages
2598        $moved = $this->conn->move($uids, $from_mbox, $to_mbox);
2599
2600        // send expunge command in order to have the moved message
2601        // really deleted from the source mailbox
2602        if ($moved) {
2603            $this->_expunge($from_mbox, false, $uids);
2604            $this->_clear_messagecount($from_mbox);
2605            $this->_clear_messagecount($to_mbox);
2606        }
2607        // moving failed
2608        else if ($to_trash && $config->get('delete_always', false)) {
2609            $moved = $this->delete_message($uids, $from_mbox);
2610        }
2611
2612        if ($moved) {
2613            // unset threads internal cache
2614            unset($this->icache['threads']);
2615
2616            // remove message ids from search set
2617            if ($this->search_set && $from_mbox == $this->mailbox) {
2618                // threads are too complicated to just remove messages from set
2619                if ($this->search_threads || $all_mode)
2620                    $this->refresh_search();
2621                else {
2622                    $a_uids = explode(',', $uids);
2623                    foreach ($a_uids as $uid)
2624                        $a_mids[] = $this->uid2id($uid, $from_mbox);
2625                    $this->search_set = array_diff($this->search_set, $a_mids);
2626                }
2627                unset($a_mids);
2628                unset($a_uids);
2629            }
2630
2631            // remove cached messages
2632            // @TODO: do cache update instead of clearing it
2633            $this->clear_message_cache($from_mbox, $all_mode ? null : explode(',', $uids));
2634        }
2635
2636        return $moved;
2637    }
2638
2639
2640    /**
2641     * Copy a message from one mailbox to another
2642     *
2643     * @param mixed  $uids      Message UIDs as array or comma-separated string, or '*'
2644     * @param string $to_mbox   Target mailbox
2645     * @param string $from_mbox Source mailbox
2646     * @return boolean True on success, False on error
2647     */
2648    function copy_message($uids, $to_mbox, $from_mbox='')
2649    {
2650        if (!strlen($from_mbox)) {
2651            $from_mbox = $this->mailbox;
2652        }
2653
2654        list($uids, $all_mode) = $this->_parse_uids($uids, $from_mbox);
2655
2656        // exit if no message uids are specified
2657        if (empty($uids)) {
2658            return false;
2659        }
2660
2661        // make sure mailbox exists
2662        if ($to_mbox != 'INBOX' && !$this->mailbox_exists($to_mbox)) {
2663            if (in_array($to_mbox, $this->default_folders))
2664                $this->create_mailbox($to_mbox, true);
2665            else
2666                return false;
2667        }
2668
2669        // copy messages
2670        $copied = $this->conn->copy($uids, $from_mbox, $to_mbox);
2671
2672        if ($copied) {
2673            $this->_clear_messagecount($to_mbox);
2674        }
2675
2676        return $copied;
2677    }
2678
2679
2680    /**
2681     * Mark messages as deleted and expunge mailbox
2682     *
2683     * @param mixed  $uids    Message UIDs as array or comma-separated string, or '*'
2684     * @param string $mailbox Source mailbox
2685     *
2686     * @return boolean True on success, False on error
2687     */
2688    function delete_message($uids, $mailbox='')
2689    {
2690        if (!strlen($mailbox)) {
2691            $mailbox = $this->mailbox;
2692        }
2693
2694        list($uids, $all_mode) = $this->_parse_uids($uids, $mailbox);
2695
2696        // exit if no message uids are specified
2697        if (empty($uids))
2698            return false;
2699
2700        $deleted = $this->conn->delete($mailbox, $uids);
2701
2702        if ($deleted) {
2703            // send expunge command in order to have the deleted message
2704            // really deleted from the mailbox
2705            $this->_expunge($mailbox, false, $uids);
2706            $this->_clear_messagecount($mailbox);
2707            unset($this->uid_id_map[$mailbox]);
2708
2709            // unset threads internal cache
2710            unset($this->icache['threads']);
2711
2712            // remove message ids from search set
2713            if ($this->search_set && $mailbox == $this->mailbox) {
2714                // threads are too complicated to just remove messages from set
2715                if ($this->search_threads || $all_mode)
2716                    $this->refresh_search();
2717                else {
2718                    $a_uids = explode(',', $uids);
2719                    foreach ($a_uids as $uid)
2720                        $a_mids[] = $this->uid2id($uid, $mailbox);
2721                    $this->search_set = array_diff($this->search_set, $a_mids);
2722                    unset($a_uids);
2723                    unset($a_mids);
2724                }
2725            }
2726
2727            // remove cached messages
2728            $this->clear_message_cache($mailbox, $all_mode ? null : explode(',', $uids));
2729        }
2730
2731        return $deleted;
2732    }
2733
2734
2735    /**
2736     * Clear all messages in a specific mailbox
2737     *
2738     * @param string $mailbox Mailbox name
2739     *
2740     * @return int Above 0 on success
2741     */
2742    function clear_mailbox($mailbox=null)
2743    {
2744        if (!strlen($mailbox)) {
2745            $mailbox = $this->mailbox;
2746        }
2747
2748        // SELECT will set messages count for clearFolder()
2749        if ($this->conn->select($mailbox)) {
2750            $cleared = $this->conn->clearFolder($mailbox);
2751        }
2752
2753        // make sure the cache is cleared as well
2754        if ($cleared) {
2755            $this->clear_message_cache($mailbox);
2756            $a_mailbox_cache = $this->get_cache('messagecount');
2757            unset($a_mailbox_cache[$mailbox]);
2758            $this->update_cache('messagecount', $a_mailbox_cache);
2759        }
2760
2761        return $cleared;
2762    }
2763
2764
2765    /**
2766     * Send IMAP expunge command and clear cache
2767     *
2768     * @param string  $mailbox     Mailbox name
2769     * @param boolean $clear_cache False if cache should not be cleared
2770     *
2771     * @return boolean True on success
2772     */
2773    function expunge($mailbox='', $clear_cache=true)
2774    {
2775        if (!strlen($mailbox)) {
2776            $mailbox = $this->mailbox;
2777        }
2778
2779        return $this->_expunge($mailbox, $clear_cache);
2780    }
2781
2782
2783    /**
2784     * Send IMAP expunge command and clear cache
2785     *
2786     * @param string  $mailbox     Mailbox name
2787     * @param boolean $clear_cache False if cache should not be cleared
2788     * @param mixed   $uids        Message UIDs as array or comma-separated string, or '*'
2789     * @return boolean True on success
2790     * @access private
2791     * @see rcube_imap::expunge()
2792     */
2793    private function _expunge($mailbox, $clear_cache=true, $uids=NULL)
2794    {
2795        if ($uids && $this->get_capability('UIDPLUS'))
2796            list($uids, $all_mode) = $this->_parse_uids($uids, $mailbox);
2797        else
2798            $uids = null;
2799
2800        // force mailbox selection and check if mailbox is writeable
2801        // to prevent a situation when CLOSE is executed on closed
2802        // or EXPUNGE on read-only mailbox
2803        $result = $this->conn->select($mailbox);
2804        if (!$result) {
2805            return false;
2806        }
2807        if (!$this->conn->data['READ-WRITE']) {
2808            $this->conn->setError(rcube_imap_generic::ERROR_READONLY, "Mailbox is read-only");
2809            return false;
2810        }
2811
2812        // CLOSE(+SELECT) should be faster than EXPUNGE
2813        if (empty($uids) || $all_mode)
2814            $result = $this->conn->close();
2815        else
2816            $result = $this->conn->expunge($mailbox, $uids);
2817
2818        if ($result && $clear_cache) {
2819            $this->clear_message_cache($mailbox, $all_mode ? null : explode(',', $uids));
2820            $this->_clear_messagecount($mailbox);
2821        }
2822
2823        return $result;
2824    }
2825
2826
2827    /**
2828     * Parse message UIDs input
2829     *
2830     * @param mixed  $uids    UIDs array or comma-separated list or '*' or '1:*'
2831     * @param string $mailbox Mailbox name
2832     * @return array Two elements array with UIDs converted to list and ALL flag
2833     * @access private
2834     */
2835    private function _parse_uids($uids, $mailbox)
2836    {
2837        if ($uids === '*' || $uids === '1:*') {
2838            if (empty($this->search_set)) {
2839                $uids = '1:*';
2840                $all = true;
2841            }
2842            // get UIDs from current search set
2843            // @TODO: skip fetchUIDs() and work with IDs instead of UIDs (?)
2844            else {
2845                if ($this->search_threads)
2846                    $uids = $this->conn->fetchUIDs($mailbox, array_keys($this->search_set['depth']));
2847                else
2848                    $uids = $this->conn->fetchUIDs($mailbox, $this->search_set);
2849
2850                // save ID-to-UID mapping in local cache
2851                if (is_array($uids))
2852                    foreach ($uids as $id => $uid)
2853                        $this->uid_id_map[$mailbox][$uid] = $id;
2854
2855                $uids = join(',', $uids);
2856            }
2857        }
2858        else {
2859            if (is_array($uids))
2860                $uids = join(',', $uids);
2861
2862            if (preg_match('/[^0-9,]/', $uids))
2863                $uids = '';
2864        }
2865
2866        return array($uids, (bool) $all);
2867    }
2868
2869
2870    /**
2871     * Translate UID to message ID
2872     *
2873     * @param int    $uid     Message UID
2874     * @param string $mailbox Mailbox name
2875     *
2876     * @return int   Message ID
2877     */
2878    function get_id($uid, $mailbox=null)
2879    {
2880        if (!strlen($mailbox)) {
2881            $mailbox = $this->mailbox;
2882        }
2883
2884        return $this->uid2id($uid, $mailbox);
2885    }
2886
2887
2888    /**
2889     * Translate message number to UID
2890     *
2891     * @param int    $id      Message ID
2892     * @param string $mailbox Mailbox name
2893     *
2894     * @return int   Message UID
2895     */
2896    function get_uid($id, $mailbox=null)
2897    {
2898        if (!strlen($mailbox)) {
2899            $mailbox = $this->mailbox;
2900        }
2901
2902        return $this->id2uid($id, $mailbox);
2903    }
2904
2905
2906
2907    /* --------------------------------
2908     *        folder managment
2909     * --------------------------------*/
2910
2911    /**
2912     * Public method for listing subscribed folders
2913     *
2914     * @param   string  $root   Optional root folder
2915     * @param   string  $name   Optional name pattern
2916     * @param   string  $filter Optional filter
2917     *
2918     * @return  array   List of mailboxes/folders
2919     * @access  public
2920     */
2921    function list_mailboxes($root='', $name='*', $filter=null)
2922    {
2923        $a_mboxes = $this->_list_mailboxes($root, $name, $filter);
2924
2925        // INBOX should always be available
2926        if ((!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)) {
2927            array_unshift($a_mboxes, 'INBOX');
2928        }
2929
2930        // sort mailboxes
2931        $a_mboxes = $this->_sort_mailbox_list($a_mboxes);
2932
2933        return $a_mboxes;
2934    }
2935
2936
2937    /**
2938     * Private method for mailbox listing
2939     *
2940     * @param   string  $root   Optional root folder
2941     * @param   string  $name   Optional name pattern
2942     * @param   mixed   $filter Optional filter
2943     *
2944     * @return  array   List of mailboxes/folders
2945     * @see     rcube_imap::list_mailboxes()
2946     * @access  private
2947     */
2948    private function _list_mailboxes($root='', $name='*', $filter=null)
2949    {
2950        $cache_key = $root.':'.$name;
2951        if (!empty($filter)) {
2952            $cache_key .= ':'.(is_string($filter) ? $filter : serialize($filter));
2953        }
2954
2955        $cache_key = 'mailboxes.'.md5($cache_key);
2956
2957        // get cached folder list
2958        $a_mboxes = $this->get_cache($cache_key);
2959        if (is_array($a_mboxes)) {
2960            return $a_mboxes;
2961        }
2962
2963        $a_defaults = $a_out = array();
2964
2965        // Give plugins a chance to provide a list of mailboxes
2966        $data = rcmail::get_instance()->plugins->exec_hook('mailboxes_list',
2967            array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LSUB'));
2968
2969        if (isset($data['folders'])) {
2970            $a_folders = $data['folders'];
2971        }
2972        else if (!$this->conn->connected()) {
2973           return array();
2974        }
2975        else {
2976            // Server supports LIST-EXTENDED, we can use selection options
2977            $config = rcmail::get_instance()->config;
2978            // #1486225: Some dovecot versions returns wrong result using LIST-EXTENDED
2979            if (!$config->get('imap_force_lsub') && $this->get_capability('LIST-EXTENDED')) {
2980                // This will also set mailbox options, LSUB doesn't do that
2981                $a_folders = $this->conn->listMailboxes($root, $name,
2982                    NULL, array('SUBSCRIBED'));
2983
2984                // unsubscribe non-existent folders, remove from the list
2985                if (is_array($a_folders) && $name == '*') {
2986                    foreach ($a_folders as $idx => $folder) {
2987                        if ($this->conn->data['LIST'] && ($opts = $this->conn->data['LIST'][$folder])
2988                            && in_array('\\NonExistent', $opts)
2989                        ) {
2990                            $this->conn->unsubscribe($folder);
2991                            unset($a_folders[$idx]);
2992                        }
2993                    }
2994                }
2995            }
2996            // retrieve list of folders from IMAP server using LSUB
2997            else {
2998                $a_folders = $this->conn->listSubscribed($root, $name);
2999
3000                // unsubscribe non-existent folders, remove from the list
3001                if (is_array($a_folders) && $name == '*') {
3002                    foreach ($a_folders as $idx => $folder) {
3003                        if ($this->conn->data['LIST'] && ($opts = $this->conn->data['LIST'][$folder])
3004                            && in_array('\\Noselect', $opts)
3005                        ) {
3006                            // Some servers returns \Noselect for existing folders
3007                            if (!$this->mailbox_exists($folder)) {
3008                                $this->conn->unsubscribe($folder);
3009                                unset($a_folders[$idx]);
3010                            }
3011                        }
3012                    }
3013                }
3014            }
3015        }
3016
3017        if (!is_array($a_folders) || !sizeof($a_folders)) {
3018            $a_folders = array();
3019        }
3020
3021        // write mailboxlist to cache
3022        $this->update_cache($cache_key, $a_folders);
3023
3024        return $a_folders;
3025    }
3026
3027
3028    /**
3029     * Get a list of all folders available on the IMAP server
3030     *
3031     * @param string $root   IMAP root dir
3032     * @param string  $name   Optional name pattern
3033     * @param mixed   $filter Optional filter
3034     *
3035     * @return array Indexed array with folder names
3036     */
3037    function list_unsubscribed($root='', $name='*', $filter=null)
3038    {
3039        // @TODO: caching
3040        // Give plugins a chance to provide a list of mailboxes
3041        $data = rcmail::get_instance()->plugins->exec_hook('mailboxes_list',
3042            array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LIST'));
3043
3044        if (isset($data['folders'])) {
3045            $a_mboxes = $data['folders'];
3046        }
3047        else {
3048            // retrieve list of folders from IMAP server
3049            $a_mboxes = $this->conn->listMailboxes($root, $name);
3050        }
3051
3052        if (!is_array($a_mboxes)) {
3053            $a_mboxes = array();
3054        }
3055
3056        // INBOX should always be available
3057        if ((!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)) {
3058            array_unshift($a_mboxes, 'INBOX');
3059        }
3060
3061        // filter folders and sort them
3062        $a_mboxes = $this->_sort_mailbox_list($a_mboxes);
3063
3064        return $a_mboxes;
3065    }
3066
3067
3068    /**
3069     * Get mailbox quota information
3070     * added by Nuny
3071     *
3072     * @return mixed Quota info or False if not supported
3073     */
3074    function get_quota()
3075    {
3076        if ($this->get_capability('QUOTA'))
3077            return $this->conn->getQuota();
3078
3079        return false;
3080    }
3081
3082
3083    /**
3084     * Get mailbox size (size of all messages in a mailbox)
3085     *
3086     * @param string $mailbox Mailbox name
3087     *
3088     * @return int Mailbox size in bytes, False on error
3089     */
3090    function get_mailbox_size($mailbox)
3091    {
3092        // @TODO: could we try to use QUOTA here?
3093        $result = $this->conn->fetchHeaderIndex($mailbox, '1:*', 'SIZE', false);
3094
3095        if (is_array($result))
3096            $result = array_sum($result);
3097
3098        return $result;
3099    }
3100
3101
3102    /**
3103     * Subscribe to a specific mailbox(es)
3104     *
3105     * @param array $a_mboxes Mailbox name(s)
3106     * @return boolean True on success
3107     */
3108    function subscribe($a_mboxes)
3109    {
3110        if (!is_array($a_mboxes))
3111            $a_mboxes = array($a_mboxes);
3112
3113        // let this common function do the main work
3114        return $this->_change_subscription($a_mboxes, 'subscribe');
3115    }
3116
3117
3118    /**
3119     * Unsubscribe mailboxes
3120     *
3121     * @param array $a_mboxes Mailbox name(s)
3122     * @return boolean True on success
3123     */
3124    function unsubscribe($a_mboxes)
3125    {
3126        if (!is_array($a_mboxes))
3127            $a_mboxes = array($a_mboxes);
3128
3129        // let this common function do the main work
3130        return $this->_change_subscription($a_mboxes, 'unsubscribe');
3131    }
3132
3133
3134    /**
3135     * Create a new mailbox on the server and register it in local cache
3136     *
3137     * @param string  $mailbox   New mailbox name
3138     * @param boolean $subscribe True if the new mailbox should be subscribed
3139     *
3140     * @return boolean True on success
3141     */
3142    function create_mailbox($mailbox, $subscribe=false)
3143    {
3144        $result = $this->conn->createFolder($mailbox);
3145
3146        // try to subscribe it
3147        if ($result) {
3148            // clear cache
3149            $this->clear_cache('mailboxes', true);
3150
3151            if ($subscribe)
3152                $this->subscribe($mailbox);
3153        }
3154
3155        return $result;
3156    }
3157
3158
3159    /**
3160     * Set a new name to an existing mailbox
3161     *
3162     * @param string $mailbox  Mailbox to rename
3163     * @param string $new_name New mailbox name
3164     *
3165     * @return boolean True on success
3166     */
3167    function rename_mailbox($mailbox, $new_name)
3168    {
3169        if (!strlen($new_name)) {
3170            return false;
3171        }
3172
3173        $delm = $this->get_hierarchy_delimiter();
3174
3175        // get list of subscribed folders
3176        if ((strpos($mailbox, '%') === false) && (strpos($mailbox, '*') === false)) {
3177            $a_subscribed = $this->_list_mailboxes('', $mailbox . $delm . '*');
3178            $subscribed   = $this->mailbox_exists($mailbox, true);
3179        }
3180        else {
3181            $a_subscribed = $this->_list_mailboxes();
3182            $subscribed   = in_array($mailbox, $a_subscribed);
3183        }
3184
3185        $result = $this->conn->renameFolder($mailbox, $new_name);
3186
3187        if ($result) {
3188            // unsubscribe the old folder, subscribe the new one
3189            if ($subscribed) {
3190                $this->conn->unsubscribe($mailbox);
3191                $this->conn->subscribe($new_name);
3192            }
3193
3194            // check if mailbox children are subscribed
3195            foreach ($a_subscribed as $c_subscribed) {
3196                if (preg_match('/^'.preg_quote($mailbox.$delm, '/').'/', $c_subscribed)) {
3197                    $this->conn->unsubscribe($c_subscribed);
3198                    $this->conn->subscribe(preg_replace('/^'.preg_quote($mailbox, '/').'/',
3199                        $new_name, $c_subscribed));
3200
3201                    // clear cache
3202                    $this->clear_message_cache($c_subscribed);
3203                }
3204            }
3205
3206            // clear cache
3207            $this->clear_message_cache($mailbox);
3208            $this->clear_cache('mailboxes', true);
3209        }
3210
3211        return $result;
3212    }
3213
3214
3215    /**
3216     * Remove mailbox from server
3217     *
3218     * @param string $mailbox Mailbox name
3219     *
3220     * @return boolean True on success
3221     */
3222    function delete_mailbox($mailbox)
3223    {
3224        $delm = $this->get_hierarchy_delimiter();
3225
3226        // get list of folders
3227        if ((strpos($mailbox, '%') === false) && (strpos($mailbox, '*') === false))
3228            $sub_mboxes = $this->list_unsubscribed('', $mailbox . $delm . '*');
3229        else
3230            $sub_mboxes = $this->list_unsubscribed();
3231
3232        // send delete command to server
3233        $result = $this->conn->deleteFolder($mailbox);
3234
3235        if ($result) {
3236            // unsubscribe mailbox
3237            $this->conn->unsubscribe($mailbox);
3238
3239            foreach ($sub_mboxes as $c_mbox) {
3240                if (preg_match('/^'.preg_quote($mailbox.$delm, '/').'/', $c_mbox)) {
3241                    $this->conn->unsubscribe($c_mbox);
3242                    if ($this->conn->deleteFolder($c_mbox)) {
3243                            $this->clear_message_cache($c_mbox);
3244                    }
3245                }
3246            }
3247
3248            // clear mailbox-related cache
3249            $this->clear_message_cache($mailbox);
3250            $this->clear_cache('mailboxes', true);
3251        }
3252
3253        return $result;
3254    }
3255
3256
3257    /**
3258     * Create all folders specified as default
3259     */
3260    function create_default_folders()
3261    {
3262        // create default folders if they do not exist
3263        foreach ($this->default_folders as $folder) {
3264            if (!$this->mailbox_exists($folder))
3265                $this->create_mailbox($folder, true);
3266            else if (!$this->mailbox_exists($folder, true))
3267                $this->subscribe($folder);
3268        }
3269    }
3270
3271
3272    /**
3273     * Checks if folder exists and is subscribed
3274     *
3275     * @param string   $mailbox      Folder name
3276     * @param boolean  $subscription Enable subscription checking
3277     *
3278     * @return boolean TRUE or FALSE
3279     */
3280    function mailbox_exists($mailbox, $subscription=false)
3281    {
3282        if ($mailbox == 'INBOX') {
3283            return true;
3284        }
3285
3286        $key  = $subscription ? 'subscribed' : 'existing';
3287
3288        if (is_array($this->icache[$key]) && in_array($mailbox, $this->icache[$key]))
3289            return true;
3290
3291        if ($subscription) {
3292            $a_folders = $this->conn->listSubscribed('', $mailbox);
3293        }
3294        else {
3295            $a_folders = $this->conn->listMailboxes('', $mailbox);
3296        }
3297
3298        if (is_array($a_folders) && in_array($mailbox, $a_folders)) {
3299            $this->icache[$key][] = $mailbox;
3300            return true;
3301        }
3302
3303        return false;
3304    }
3305
3306
3307    /**
3308     * Returns the namespace where the folder is in
3309     *
3310     * @param string $mailbox Folder name
3311     *
3312     * @return string One of 'personal', 'other' or 'shared'
3313     * @access public
3314     */
3315    function mailbox_namespace($mailbox)
3316    {
3317        if ($mailbox == 'INBOX') {
3318            return 'personal';
3319        }
3320
3321        foreach ($this->namespace as $type => $namespace) {
3322            if (is_array($namespace)) {
3323                foreach ($namespace as $ns) {
3324                    if (strlen($ns[0])) {
3325                        if ((strlen($ns[0])>1 && $mailbox == substr($ns[0], 0, -1))
3326                            || strpos($mailbox, $ns[0]) === 0
3327                        ) {
3328                            return $type;
3329                        }
3330                    }
3331                }
3332            }
3333        }
3334
3335        return 'personal';
3336    }
3337
3338
3339    /**
3340     * Modify folder name according to namespace.
3341     * For output it removes prefix of the personal namespace if it's possible.
3342     * For input it adds the prefix. Use it before creating a folder in root
3343     * of the folders tree.
3344     *
3345     * @param string $mailbox Folder name
3346     * @param string $mode    Mode name (out/in)
3347     *
3348     * @return string Folder name
3349     */
3350    function mod_mailbox($mailbox, $mode = 'out')
3351    {
3352        if (!strlen($mailbox)) {
3353            return $mailbox;
3354        }
3355
3356        $prefix     = $this->namespace['prefix']; // see set_env()
3357        $prefix_len = strlen($prefix);
3358
3359        if (!$prefix_len) {
3360            return $mailbox;
3361        }
3362
3363        // remove prefix for output
3364        if ($mode == 'out') {
3365            if (substr($mailbox, 0, $prefix_len) === $prefix) {
3366                return substr($mailbox, $prefix_len);
3367            }
3368        }
3369        // add prefix for input (e.g. folder creation)
3370        else {
3371            return $prefix . $mailbox;
3372        }
3373
3374        return $mailbox;
3375    }
3376
3377
3378    /**
3379     * Gets folder options from LIST response, e.g. \Noselect, \Noinferiors
3380     *
3381     * @param string $mailbox Folder name
3382     * @param bool   $force   Set to True if options should be refreshed
3383     *                        Options are available after LIST command only
3384     *
3385     * @return array Options list
3386     */
3387    function mailbox_options($mailbox, $force=false)
3388    {
3389        if ($mailbox == 'INBOX') {
3390            return array();
3391        }
3392
3393        if (!is_array($this->conn->data['LIST']) || !is_array($this->conn->data['LIST'][$mailbox])) {
3394            if ($force) {
3395                $this->conn->listMailboxes('', $mailbox);
3396            }
3397            else {
3398                return array();
3399            }
3400        }
3401
3402        $opts = $this->conn->data['LIST'][$mailbox];
3403
3404        return is_array($opts) ? $opts : array();
3405    }
3406
3407
3408    /**
3409     * Gets connection (and current mailbox) data: UIDVALIDITY, EXISTS, RECENT,
3410     * PERMANENTFLAGS, UIDNEXT, UNSEEN
3411     *
3412     * @param string $mailbox Folder name
3413     *
3414     * @return array Data
3415     */
3416    function mailbox_data($mailbox)
3417    {
3418        if (!strlen($mailbox))
3419            $mailbox = $this->mailbox !== null ? $this->mailbox : 'INBOX';
3420
3421        if ($this->conn->selected != $mailbox) {
3422            if ($this->conn->select($mailbox))
3423                $this->mailbox = $mailbox;
3424        }
3425
3426        $data = $this->conn->data;
3427
3428        // add (E)SEARCH result for ALL UNDELETED query
3429        if (!empty($this->icache['undeleted_idx']) && $this->icache['undeleted_idx'][0] == $mailbox) {
3430            $data['ALL_UNDELETED']   = $this->icache['undeleted_idx'][1];
3431            $data['COUNT_UNDELETED'] = $this->icache['undeleted_idx'][2];
3432        }
3433
3434        return $data;
3435    }
3436
3437
3438    /**
3439     * Returns extended information about the folder
3440     *
3441     * @param string $mailbox Folder name
3442     *
3443     * @return array Data
3444     */
3445    function mailbox_info($mailbox)
3446    {
3447        if ($this->icache['options'] && $this->icache['options']['name'] == $mailbox) {
3448            return $this->icache['options'];
3449        }
3450
3451        $acl       = $this->get_capability('ACL');
3452        $namespace = $this->get_namespace();
3453        $options   = array();
3454
3455        // check if the folder is a namespace prefix
3456        if (!empty($namespace)) {
3457            $mbox = $mailbox . $this->delimiter;
3458            foreach ($namespace as $ns) {
3459                if (!empty($ns)) {
3460                    foreach ($ns as $item) {
3461                        if ($item[0] === $mbox) {
3462                            $options['is_root'] = true;
3463                            break 2;
3464                        }
3465                    }
3466                }
3467            }
3468        }
3469        // check if the folder is other user virtual-root
3470        if (!$options['is_root'] && !empty($namespace) && !empty($namespace['other'])) {
3471            $parts = explode($this->delimiter, $mailbox);
3472            if (count($parts) == 2) {
3473                $mbox = $parts[0] . $this->delimiter;
3474                foreach ($namespace['other'] as $item) {
3475                    if ($item[0] === $mbox) {
3476                        $options['is_root'] = true;
3477                        break;
3478                    }
3479                }
3480            }
3481        }
3482
3483        $options['name']      = $mailbox;
3484        $options['options']   = $this->mailbox_options($mailbox, true);
3485        $options['namespace'] = $this->mailbox_namespace($mailbox);
3486        $options['rights']    = $acl && !$options['is_root'] ? (array)$this->my_rights($mailbox) : array();
3487        $options['special']   = in_array($mailbox, $this->default_folders);
3488
3489        // Set 'noselect' and 'norename' flags
3490        if (is_array($options['options'])) {
3491            foreach ($options['options'] as $opt) {
3492                $opt = strtolower($opt);
3493                if ($opt == '\noselect' || $opt == '\nonexistent') {
3494                    $options['noselect'] = true;
3495                }
3496            }
3497        }
3498        else {
3499            $options['noselect'] = true;
3500        }
3501
3502        if (!empty($options['rights'])) {
3503            $options['norename'] = !in_array('x', $options['rights']) && !in_array('d', $options['rights']);
3504
3505            if (!$options['noselect']) {
3506                $options['noselect'] = !in_array('r', $options['rights']);
3507            }
3508        }
3509        else {
3510            $options['norename'] = $options['is_root'] || $options['namespace'] != 'personal';
3511        }
3512
3513        $this->icache['options'] = $options;
3514
3515        return $options;
3516    }
3517
3518
3519    /**
3520     * Get message header names for rcube_imap_generic::fetchHeader(s)
3521     *
3522     * @return string Space-separated list of header names
3523     */
3524    private function get_fetch_headers()
3525    {
3526        $headers = explode(' ', $this->fetch_add_headers);
3527        $headers = array_map('strtoupper', $headers);
3528
3529        if ($this->messages_caching || $this->get_all_headers)
3530            $headers = array_merge($headers, $this->all_headers);
3531
3532        return implode(' ', array_unique($headers));
3533    }
3534
3535
3536    /* -----------------------------------------
3537     *   ACL and METADATA/ANNOTATEMORE methods
3538     * ----------------------------------------*/
3539
3540    /**
3541     * Changes the ACL on the specified mailbox (SETACL)
3542     *
3543     * @param string $mailbox Mailbox name
3544     * @param string $user    User name
3545     * @param string $acl     ACL string
3546     *
3547     * @return boolean True on success, False on failure
3548     *
3549     * @access public
3550     * @since 0.5-beta
3551     */
3552    function set_acl($mailbox, $user, $acl)
3553    {
3554        if ($this->get_capability('ACL'))
3555            return $this->conn->setACL($mailbox, $user, $acl);
3556
3557        return false;
3558    }
3559
3560
3561    /**
3562     * Removes any <identifier,rights> pair for the
3563     * specified user from the ACL for the specified
3564     * mailbox (DELETEACL)
3565     *
3566     * @param string $mailbox Mailbox name
3567     * @param string $user    User name
3568     *
3569     * @return boolean True on success, False on failure
3570     *
3571     * @access public
3572     * @since 0.5-beta
3573     */
3574    function delete_acl($mailbox, $user)
3575    {
3576        if ($this->get_capability('ACL'))
3577            return $this->conn->deleteACL($mailbox, $user);
3578
3579        return false;
3580    }
3581
3582
3583    /**
3584     * Returns the access control list for mailbox (GETACL)
3585     *
3586     * @param string $mailbox Mailbox name
3587     *
3588     * @return array User-rights array on success, NULL on error
3589     * @access public
3590     * @since 0.5-beta
3591     */
3592    function get_acl($mailbox)
3593    {
3594        if ($this->get_capability('ACL'))
3595            return $this->conn->getACL($mailbox);
3596
3597        return NULL;
3598    }
3599
3600
3601    /**
3602     * Returns information about what rights can be granted to the
3603     * user (identifier) in the ACL for the mailbox (LISTRIGHTS)
3604     *
3605     * @param string $mailbox Mailbox name
3606     * @param string $user    User name
3607     *
3608     * @return array List of user rights
3609     * @access public
3610     * @since 0.5-beta
3611     */
3612    function list_rights($mailbox, $user)
3613    {
3614        if ($this->get_capability('ACL'))
3615            return $this->conn->listRights($mailbox, $user);
3616
3617        return NULL;
3618    }
3619
3620
3621    /**
3622     * Returns the set of rights that the current user has to
3623     * mailbox (MYRIGHTS)
3624     *
3625     * @param string $mailbox Mailbox name
3626     *
3627     * @return array MYRIGHTS response on success, NULL on error
3628     * @access public
3629     * @since 0.5-beta
3630     */
3631    function my_rights($mailbox)
3632    {
3633        if ($this->get_capability('ACL'))
3634            return $this->conn->myRights($mailbox);
3635
3636        return NULL;
3637    }
3638
3639
3640    /**
3641     * Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
3642     *
3643     * @param string $mailbox Mailbox name (empty for server metadata)
3644     * @param array  $entries Entry-value array (use NULL value as NIL)
3645     *
3646     * @return boolean True on success, False on failure
3647     * @access public
3648     * @since 0.5-beta
3649     */
3650    function set_metadata($mailbox, $entries)
3651    {
3652        if ($this->get_capability('METADATA') ||
3653            (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
3654        ) {
3655            return $this->conn->setMetadata($mailbox, $entries);
3656        }
3657        else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
3658            foreach ((array)$entries as $entry => $value) {
3659                list($ent, $attr) = $this->md2annotate($entry);
3660                $entries[$entry] = array($ent, $attr, $value);
3661            }
3662            return $this->conn->setAnnotation($mailbox, $entries);
3663        }
3664
3665        return false;
3666    }
3667
3668
3669    /**
3670     * Unsets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
3671     *
3672     * @param string $mailbox Mailbox name (empty for server metadata)
3673     * @param array  $entries Entry names array
3674     *
3675     * @return boolean True on success, False on failure
3676     *
3677     * @access public
3678     * @since 0.5-beta
3679     */
3680    function delete_metadata($mailbox, $entries)
3681    {
3682        if ($this->get_capability('METADATA') || 
3683            (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
3684        ) {
3685            return $this->conn->deleteMetadata($mailbox, $entries);
3686        }
3687        else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
3688            foreach ((array)$entries as $idx => $entry) {
3689                list($ent, $attr) = $this->md2annotate($entry);
3690                $entries[$idx] = array($ent, $attr, NULL);
3691            }
3692            return $this->conn->setAnnotation($mailbox, $entries);
3693        }
3694
3695        return false;
3696    }
3697
3698
3699    /**
3700     * Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION)
3701     *
3702     * @param string $mailbox Mailbox name (empty for server metadata)
3703     * @param array  $entries Entries
3704     * @param array  $options Command options (with MAXSIZE and DEPTH keys)
3705     *
3706     * @return array Metadata entry-value hash array on success, NULL on error
3707     *
3708     * @access public
3709     * @since 0.5-beta
3710     */
3711    function get_metadata($mailbox, $entries, $options=array())
3712    {
3713        if ($this->get_capability('METADATA') || 
3714            (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
3715        ) {
3716            return $this->conn->getMetadata($mailbox, $entries, $options);
3717        }
3718        else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
3719            $queries = array();
3720            $res     = array();
3721
3722            // Convert entry names
3723            foreach ((array)$entries as $entry) {
3724                list($ent, $attr) = $this->md2annotate($entry);
3725                $queries[$attr][] = $ent;
3726            }
3727
3728            // @TODO: Honor MAXSIZE and DEPTH options
3729            foreach ($queries as $attrib => $entry)
3730                if ($result = $this->conn->getAnnotation($mailbox, $entry, $attrib))
3731                    $res = array_merge($res, $result);
3732
3733            return $res;
3734        }
3735
3736        return NULL;
3737    }
3738
3739
3740    /**
3741     * Converts the METADATA extension entry name into the correct
3742     * entry-attrib names for older ANNOTATEMORE version.
3743     *
3744     * @param string $entry Entry name
3745     *
3746     * @return array Entry-attribute list, NULL if not supported (?)
3747     */
3748    private function md2annotate($entry)
3749    {
3750        if (substr($entry, 0, 7) == '/shared') {
3751            return array(substr($entry, 7), 'value.shared');
3752        }
3753        else if (substr($entry, 0, 8) == '/private') {
3754            return array(substr($entry, 8), 'value.priv');
3755        }
3756
3757        // @TODO: log error
3758        return NULL;
3759    }
3760
3761
3762    /* --------------------------------
3763     *   internal caching methods
3764     * --------------------------------*/
3765
3766    /**
3767     * Enable or disable indexes caching
3768     *
3769     * @param string $type Cache type (@see rcmail::get_cache)
3770     * @access public
3771     */
3772    function set_caching($type)
3773    {
3774        if ($type) {
3775            $this->caching = $type;
3776        }
3777        else {
3778            if ($this->cache)
3779                $this->cache->close();
3780            $this->cache   = null;
3781            $this->caching = false;
3782        }
3783    }
3784
3785    /**
3786     * Getter for IMAP cache object
3787     */
3788    private function get_cache_engine()
3789    {
3790        if ($this->caching && !$this->cache) {
3791            $rcmail = rcmail::get_instance();
3792            $this->cache = $rcmail->get_cache('IMAP', $this->caching);
3793        }
3794
3795        return $this->cache;
3796    }
3797
3798    /**
3799     * Returns cached value
3800     *
3801     * @param string $key Cache key
3802     * @return mixed
3803     * @access public
3804     */
3805    function get_cache($key)
3806    {
3807        if ($cache = $this->get_cache_engine()) {
3808            return $cache->get($key);
3809        }
3810    }
3811
3812    /**
3813     * Update cache
3814     *
3815     * @param string $key  Cache key
3816     * @param mixed  $data Data
3817     * @access public
3818     */
3819    function update_cache($key, $data)
3820    {
3821        if ($cache = $this->get_cache_engine()) {
3822            $cache->set($key, $data);
3823        }
3824    }
3825
3826    /**
3827     * Clears the cache.
3828     *
3829     * @param string  $key         Cache key name or pattern
3830     * @param boolean $prefix_mode Enable it to clear all keys starting
3831     *                             with prefix specified in $key
3832     * @access public
3833     */
3834    function clear_cache($key=null, $prefix_mode=false)
3835    {
3836        if ($cache = $this->get_cache_engine()) {
3837            $cache->remove($key, $prefix_mode);
3838        }
3839    }
3840
3841
3842    /* --------------------------------
3843     *   message caching methods
3844     * --------------------------------*/
3845
3846    /**
3847     * Enable or disable messages caching
3848     *
3849     * @param boolean $set Flag
3850     */
3851    function set_messages_caching($set)
3852    {
3853        if ($set) {
3854            $this->messages_caching = true;
3855        }
3856        else {
3857            if ($this->mcache)
3858                $this->mcache->close();
3859            $this->mcache = null;
3860            $this->messages_caching = false;
3861        }
3862    }
3863
3864    /**
3865     * Getter for messages cache object
3866     */
3867    private function get_mcache_engine()
3868    {
3869        if ($this->messages_caching && !$this->mcache) {
3870            $rcmail = rcmail::get_instance();
3871            if ($dbh = $rcmail->get_dbh()) {
3872                $this->mcache = new rcube_imap_cache(
3873                    $dbh, $this, $rcmail->user->ID, $this->skip_deleted);
3874            }
3875        }
3876
3877        return $this->mcache;
3878    }
3879
3880    /**
3881     * Clears the messages cache.
3882     *
3883     * @param string $mailbox Folder name
3884     * @param array  $uids    Optional message UIDs to remove from cache
3885     */
3886    function clear_message_cache($mailbox = null, $uids = null)
3887    {
3888        if ($mcache = $this->get_mcache_engine()) {
3889            $mcache->clear($mailbox, $uids);
3890        }
3891    }
3892
3893
3894
3895    /* --------------------------------
3896     *   encoding/decoding methods
3897     * --------------------------------*/
3898
3899    /**
3900     * Split an address list into a structured array list
3901     *
3902     * @param string  $input  Input string
3903     * @param int     $max    List only this number of addresses
3904     * @param boolean $decode Decode address strings
3905     * @return array  Indexed list of addresses
3906     */
3907    function decode_address_list($input, $max=null, $decode=true)
3908    {
3909        $a = $this->_parse_address_list($input, $decode);
3910        $out = array();
3911        // Special chars as defined by RFC 822 need to in quoted string (or escaped).
3912        $special_chars = '[\(\)\<\>\\\.\[\]@,;:"]';
3913
3914        if (!is_array($a))
3915            return $out;
3916
3917        $c = count($a);
3918        $j = 0;
3919
3920        foreach ($a as $val) {
3921            $j++;
3922            $address = trim($val['address']);
3923            $name    = trim($val['name']);
3924
3925            if ($name && $address && $name != $address)
3926                $string = sprintf('%s <%s>', preg_match("/$special_chars/", $name) ? '"'.addcslashes($name, '"').'"' : $name, $address);
3927            else if ($address)
3928                $string = $address;
3929            else if ($name)
3930                $string = $name;
3931
3932            $out[$j] = array(
3933                'name'   => $name,
3934                'mailto' => $address,
3935                'string' => $string
3936            );
3937
3938            if ($max && $j==$max)
3939                break;
3940        }
3941
3942        return $out;
3943    }
3944
3945
3946    /**
3947     * Decode a message header value
3948     *
3949     * @param string  $input         Header value
3950     * @param boolean $remove_quotas Remove quotes if necessary
3951     * @return string Decoded string
3952     */
3953    function decode_header($input, $remove_quotes=false)
3954    {
3955        $str = rcube_imap::decode_mime_string((string)$input, $this->default_charset);
3956        if ($str[0] == '"' && $remove_quotes)
3957            $str = str_replace('"', '', $str);
3958
3959        return $str;
3960    }
3961
3962
3963    /**
3964     * Decode a mime-encoded string to internal charset
3965     *
3966     * @param string $input    Header value
3967     * @param string $fallback Fallback charset if none specified
3968     *
3969     * @return string Decoded string
3970     * @static
3971     */
3972    public static function decode_mime_string($input, $fallback=null)
3973    {
3974        if (!empty($fallback)) {
3975            $default_charset = $fallback;
3976        }
3977        else {
3978            $default_charset = rcmail::get_instance()->config->get('default_charset', 'ISO-8859-1');
3979        }
3980
3981        // rfc: all line breaks or other characters not found
3982        // in the Base64 Alphabet must be ignored by decoding software
3983        // delete all blanks between MIME-lines, differently we can
3984        // receive unnecessary blanks and broken utf-8 symbols
3985        $input = preg_replace("/\?=\s+=\?/", '?==?', $input);
3986
3987        // encoded-word regexp
3988        $re = '/=\?([^?]+)\?([BbQq])\?([^?\n]*)\?=/';
3989
3990        // Find all RFC2047's encoded words
3991        if (preg_match_all($re, $input, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {
3992            // Initialize variables
3993            $tmp   = array();
3994            $out   = '';
3995            $start = 0;
3996
3997            foreach ($matches as $idx => $m) {
3998                $pos      = $m[0][1];
3999                $charset  = $m[1][0];
4000                $encoding = $m[2][0];
4001                $text     = $m[3][0];
4002                $length   = strlen($m[0][0]);
4003
4004                // Append everything that is before the text to be decoded
4005                if ($start != $pos) {
4006                    $substr = substr($input, $start, $pos-$start);
4007                    $out   .= rcube_charset_convert($substr, $default_charset);
4008                    $start  = $pos;
4009                }
4010                $start += $length;
4011
4012                // Per RFC2047, each string part "MUST represent an integral number
4013                // of characters . A multi-octet character may not be split across
4014                // adjacent encoded-words." However, some mailers break this, so we
4015                // try to handle characters spanned across parts anyway by iterating
4016                // through and aggregating sequential encoded parts with the same
4017                // character set and encoding, then perform the decoding on the
4018                // aggregation as a whole.
4019
4020                $tmp[] = $text;
4021                if ($next_match = $matches[$idx+1]) {
4022                    if ($next_match[0][1] == $start
4023                        && $next_match[1][0] == $charset
4024                        && $next_match[2][0] == $encoding
4025                    ) {
4026                        continue;
4027                    }
4028                }
4029
4030                $count = count($tmp);
4031                $text  = '';
4032
4033                // Decode and join encoded-word's chunks
4034                if ($encoding == 'B' || $encoding == 'b') {
4035                    // base64 must be decoded a segment at a time
4036                    for ($i=0; $i<$count; $i++)
4037                        $text .= base64_decode($tmp[$i]);
4038                }
4039                else { //if ($encoding == 'Q' || $encoding == 'q') {
4040                    // quoted printable can be combined and processed at once
4041                    for ($i=0; $i<$count; $i++)
4042                        $text .= $tmp[$i];
4043
4044                    $text = str_replace('_', ' ', $text);
4045                    $text = quoted_printable_decode($text);
4046                }
4047
4048                $out .= rcube_charset_convert($text, $charset);
4049                $tmp = array();
4050            }
4051
4052            // add the last part of the input string
4053            if ($start != strlen($input)) {
4054                $out .= rcube_charset_convert(substr($input, $start), $default_charset);
4055            }
4056
4057            // return the results
4058            return $out;
4059        }
4060
4061        // no encoding information, use fallback
4062        return rcube_charset_convert($input, $default_charset);
4063    }
4064
4065
4066    /**
4067     * Decode a mime part
4068     *
4069     * @param string $input    Input string
4070     * @param string $encoding Part encoding
4071     * @return string Decoded string
4072     */
4073    function mime_decode($input, $encoding='7bit')
4074    {
4075        switch (strtolower($encoding)) {
4076        case 'quoted-printable':
4077            return quoted_printable_decode($input);
4078        case 'base64':
4079            return base64_decode($input);
4080        case 'x-uuencode':
4081        case 'x-uue':
4082        case 'uue':
4083        case 'uuencode':
4084            return convert_uudecode($input);
4085        case '7bit':
4086        default:
4087            return $input;
4088        }
4089    }
4090
4091
4092    /**
4093     * Convert body charset to RCMAIL_CHARSET according to the ctype_parameters
4094     *
4095     * @param string $body        Part body to decode
4096     * @param string $ctype_param Charset to convert from
4097     * @return string Content converted to internal charset
4098     */
4099    function charset_decode($body, $ctype_param)
4100    {
4101        if (is_array($ctype_param) && !empty($ctype_param['charset']))
4102            return rcube_charset_convert($body, $ctype_param['charset']);
4103
4104        // defaults to what is specified in the class header
4105        return rcube_charset_convert($body,  $this->default_charset);
4106    }
4107
4108
4109    /* --------------------------------
4110     *         private methods
4111     * --------------------------------*/
4112
4113    /**
4114     * Validate the given input and save to local properties
4115     *
4116     * @param string $sort_field Sort column
4117     * @param string $sort_order Sort order
4118     * @access private
4119     */
4120    private function _set_sort_order($sort_field, $sort_order)
4121    {
4122        if ($sort_field != null)
4123            $this->sort_field = asciiwords($sort_field);
4124        if ($sort_order != null)
4125            $this->sort_order = strtoupper($sort_order) == 'DESC' ? 'DESC' : 'ASC';
4126    }
4127
4128
4129    /**
4130     * Sort mailboxes first by default folders and then in alphabethical order
4131     *
4132     * @param array $a_folders Mailboxes list
4133     * @access private
4134     */
4135    private function _sort_mailbox_list($a_folders)
4136    {
4137        $a_out = $a_defaults = $folders = array();
4138
4139        $delimiter = $this->get_hierarchy_delimiter();
4140
4141        // find default folders and skip folders starting with '.'
4142        foreach ($a_folders as $i => $folder) {
4143            if ($folder[0] == '.')
4144                continue;
4145
4146            if (($p = array_search($folder, $this->default_folders)) !== false && !$a_defaults[$p])
4147                $a_defaults[$p] = $folder;
4148            else
4149                $folders[$folder] = rcube_charset_convert($folder, 'UTF7-IMAP');
4150        }
4151
4152        // sort folders and place defaults on the top
4153        asort($folders, SORT_LOCALE_STRING);
4154        ksort($a_defaults);
4155        $folders = array_merge($a_defaults, array_keys($folders));
4156
4157        // finally we must rebuild the list to move
4158        // subfolders of default folders to their place...
4159        // ...also do this for the rest of folders because
4160        // asort() is not properly sorting case sensitive names
4161        while (list($key, $folder) = each($folders)) {
4162            // set the type of folder name variable (#1485527)
4163            $a_out[] = (string) $folder;
4164            unset($folders[$key]);
4165            $this->_rsort($folder, $delimiter, $folders, $a_out);
4166        }
4167
4168        return $a_out;
4169    }
4170
4171
4172    /**
4173     * @access private
4174     */
4175    private function _rsort($folder, $delimiter, &$list, &$out)
4176    {
4177        while (list($key, $name) = each($list)) {
4178                if (strpos($name, $folder.$delimiter) === 0) {
4179                    // set the type of folder name variable (#1485527)
4180                $out[] = (string) $name;
4181                    unset($list[$key]);
4182                    $this->_rsort($name, $delimiter, $list, $out);
4183                }
4184        }
4185        reset($list);
4186    }
4187
4188
4189    /**
4190     * Finds message sequence ID for specified UID
4191     *
4192     * @param int    $uid      Message UID
4193     * @param string $mailbox  Mailbox name
4194     * @param bool   $force    True to skip cache
4195     *
4196     * @return int Message (sequence) ID
4197     */
4198    function uid2id($uid, $mailbox = null, $force = false)
4199    {
4200        if (!strlen($mailbox)) {
4201            $mailbox = $this->mailbox;
4202        }
4203
4204        if (!empty($this->uid_id_map[$mailbox][$uid])) {
4205            return $this->uid_id_map[$mailbox][$uid];
4206        }
4207
4208        if (!$force && ($mcache = $this->get_mcache_engine()))
4209            $id = $mcache->uid2id($mailbox, $uid);
4210
4211        if (empty($id))
4212            $id = $this->conn->UID2ID($mailbox, $uid);
4213
4214        $this->uid_id_map[$mailbox][$uid] = $id;
4215
4216        return $id;
4217    }
4218
4219
4220    /**
4221     * Find UID of the specified message sequence ID
4222     *
4223     * @param int    $id       Message (sequence) ID
4224     * @param string $mailbox  Mailbox name
4225     * @param bool   $force    True to skip cache
4226     *
4227     * @return int Message UID
4228     */
4229    function id2uid($id, $mailbox = null, $force = false)
4230    {
4231        if (!strlen($mailbox)) {
4232            $mailbox = $this->mailbox;
4233        }
4234
4235        if ($uid = array_search($id, (array)$this->uid_id_map[$mailbox])) {
4236            return $uid;
4237        }
4238
4239        if (!$force && ($mcache = $this->get_mcache_engine()))
4240            $uid = $mcache->id2uid($mailbox, $id);
4241
4242        if (empty($uid))
4243            $uid = $this->conn->ID2UID($mailbox, $id);
4244
4245        $this->uid_id_map[$mailbox][$uid] = $id;
4246
4247        return $uid;
4248    }
4249
4250
4251    /**
4252     * Subscribe/unsubscribe a list of mailboxes and update local cache
4253     * @access private
4254     */
4255    private function _change_subscription($a_mboxes, $mode)
4256    {
4257        $updated = false;
4258
4259        if (is_array($a_mboxes))
4260            foreach ($a_mboxes as $i => $mailbox) {
4261                $a_mboxes[$i] = $mailbox;
4262
4263                if ($mode == 'subscribe')
4264                    $updated = $this->conn->subscribe($mailbox);
4265                else if ($mode == 'unsubscribe')
4266                    $updated = $this->conn->unsubscribe($mailbox);
4267            }
4268
4269        // clear cached mailbox list(s)
4270        if ($updated) {
4271            $this->clear_cache('mailboxes', true);
4272        }
4273
4274        return $updated;
4275    }
4276
4277
4278    /**
4279     * Increde/decrese messagecount for a specific mailbox
4280     * @access private
4281     */
4282    private function _set_messagecount($mailbox, $mode, $increment)
4283    {
4284        $mode = strtoupper($mode);
4285        $a_mailbox_cache = $this->get_cache('messagecount');
4286
4287        if (!is_array($a_mailbox_cache[$mailbox]) || !isset($a_mailbox_cache[$mailbox][$mode]) || !is_numeric($increment))
4288            return false;
4289
4290        // add incremental value to messagecount
4291        $a_mailbox_cache[$mailbox][$mode] += $increment;
4292
4293        // there's something wrong, delete from cache
4294        if ($a_mailbox_cache[$mailbox][$mode] < 0)
4295            unset($a_mailbox_cache[$mailbox][$mode]);
4296
4297        // write back to cache
4298        $this->update_cache('messagecount', $a_mailbox_cache);
4299
4300        return true;
4301    }
4302
4303
4304    /**
4305     * Remove messagecount of a specific mailbox from cache
4306     * @access private
4307     */
4308    private function _clear_messagecount($mailbox, $mode=null)
4309    {
4310        $a_mailbox_cache = $this->get_cache('messagecount');
4311
4312        if (is_array($a_mailbox_cache[$mailbox])) {
4313            if ($mode) {
4314                unset($a_mailbox_cache[$mailbox][$mode]);
4315            }
4316            else {
4317                unset($a_mailbox_cache[$mailbox]);
4318            }
4319            $this->update_cache('messagecount', $a_mailbox_cache);
4320        }
4321    }
4322
4323
4324    /**
4325     * Split RFC822 header string into an associative array
4326     * @access private
4327     */
4328    private function _parse_headers($headers)
4329    {
4330        $a_headers = array();
4331        $headers = preg_replace('/\r?\n(\t| )+/', ' ', $headers);
4332        $lines = explode("\n", $headers);
4333        $c = count($lines);
4334
4335        for ($i=0; $i<$c; $i++) {
4336            if ($p = strpos($lines[$i], ': ')) {
4337                $field = strtolower(substr($lines[$i], 0, $p));
4338                $value = trim(substr($lines[$i], $p+1));
4339                if (!empty($value))
4340                    $a_headers[$field] = $value;
4341            }
4342        }
4343
4344        return $a_headers;
4345    }
4346
4347
4348    /**
4349     * @access private
4350     */
4351    private function _parse_address_list($str, $decode=true)
4352    {
4353        // remove any newlines and carriage returns before
4354        $str = preg_replace('/\r?\n(\s|\t)?/', ' ', $str);
4355
4356        // extract list items, remove comments
4357        $str = self::explode_header_string(',;', $str, true);
4358        $result = array();
4359
4360        // simplified regexp, supporting quoted local part
4361        $email_rx = '(\S+|("\s*(?:[^"\f\n\r\t\v\b\s]+\s*)+"))@\S+';
4362
4363        foreach ($str as $key => $val) {
4364            $name    = '';
4365            $address = '';
4366            $val     = trim($val);
4367
4368            if (preg_match('/(.*)<('.$email_rx.')>$/', $val, $m)) {
4369                $address = $m[2];
4370                $name    = trim($m[1]);
4371            }
4372            else if (preg_match('/^('.$email_rx.')$/', $val, $m)) {
4373                $address = $m[1];
4374                $name    = '';
4375            }
4376            else {
4377                $name = $val;
4378            }
4379
4380            // dequote and/or decode name
4381            if ($name) {
4382                if ($name[0] == '"' && $name[strlen($name)-1] == '"') {
4383                    $name = substr($name, 1, -1);
4384                    $name = stripslashes($name);
4385                }
4386                if ($decode) {
4387                    $name = $this->decode_header($name);
4388                }
4389            }
4390
4391            if (!$address && $name) {
4392                $address = $name;
4393            }
4394
4395            if ($address) {
4396                $result[$key] = array('name' => $name, 'address' => $address);
4397            }
4398        }
4399
4400        return $result;
4401    }
4402
4403
4404    /**
4405     * Explodes header (e.g. address-list) string into array of strings
4406     * using specified separator characters with proper handling
4407     * of quoted-strings and comments (RFC2822)
4408     *
4409     * @param string $separator       String containing separator characters
4410     * @param string $str             Header string
4411     * @param bool   $remove_comments Enable to remove comments
4412     *
4413     * @return array Header items
4414     */
4415    static function explode_header_string($separator, $str, $remove_comments=false)
4416    {
4417        $length  = strlen($str);
4418        $result  = array();
4419        $quoted  = false;
4420        $comment = 0;
4421        $out     = '';
4422
4423        for ($i=0; $i<$length; $i++) {
4424            // we're inside a quoted string
4425            if ($quoted) {
4426                if ($str[$i] == '"') {
4427                    $quoted = false;
4428                }
4429                else if ($str[$i] == '\\') {
4430                    if ($comment <= 0) {
4431                        $out .= '\\';
4432                    }
4433                    $i++;
4434                }
4435            }
4436            // we're inside a comment string
4437            else if ($comment > 0) {
4438                    if ($str[$i] == ')') {
4439                        $comment--;
4440                    }
4441                    else if ($str[$i] == '(') {
4442                        $comment++;
4443                    }
4444                    else if ($str[$i] == '\\') {
4445                        $i++;
4446                    }
4447                    continue;
4448            }
4449            // separator, add to result array
4450            else if (strpos($separator, $str[$i]) !== false) {
4451                    if ($out) {
4452                        $result[] = $out;
4453                    }
4454                    $out = '';
4455                    continue;
4456            }
4457            // start of quoted string
4458            else if ($str[$i] == '"') {
4459                    $quoted = true;
4460            }
4461            // start of comment
4462            else if ($remove_comments && $str[$i] == '(') {
4463                    $comment++;
4464            }
4465
4466            if ($comment <= 0) {
4467                $out .= $str[$i];
4468            }
4469        }
4470
4471        if ($out && $comment <= 0) {
4472            $result[] = $out;
4473        }
4474
4475        return $result;
4476    }
4477
4478
4479    /**
4480     * This is our own debug handler for the IMAP connection
4481     * @access public
4482     */
4483    public function debug_handler(&$imap, $message)
4484    {
4485        write_log('imap', $message);
4486    }
4487
4488}  // end class rcube_imap
4489
4490
4491/**
4492 * Class representing a message part
4493 *
4494 * @package Mail
4495 */
4496class rcube_message_part
4497{
4498    var $mime_id = '';
4499    var $ctype_primary = 'text';
4500    var $ctype_secondary = 'plain';
4501    var $mimetype = 'text/plain';
4502    var $disposition = '';
4503    var $filename = '';
4504    var $encoding = '8bit';
4505    var $charset = '';
4506    var $size = 0;
4507    var $headers = array();
4508    var $d_parameters = array();
4509    var $ctype_parameters = array();
4510
4511    function __clone()
4512    {
4513        if (isset($this->parts))
4514            foreach ($this->parts as $idx => $part)
4515                if (is_object($part))
4516                        $this->parts[$idx] = clone $part;
4517    }
4518}
4519
4520
4521/**
4522 * Class for sorting an array of rcube_mail_header objects in a predetermined order.
4523 *
4524 * @package Mail
4525 * @author Eric Stadtherr
4526 */
4527class rcube_header_sorter
4528{
4529    private $seqs = array();
4530    private $uids = array();
4531
4532
4533    /**
4534     * Set the predetermined sort order.
4535     *
4536     * @param array $index  Numerically indexed array of IMAP ID or UIDs
4537     * @param bool  $is_uid Set to true if $index contains UIDs
4538     */
4539    function set_index($index, $is_uid = false)
4540    {
4541        $index = array_flip($index);
4542
4543        if ($is_uid)
4544            $this->uids = $index;
4545        else
4546            $this->seqs = $index;
4547    }
4548
4549    /**
4550     * Sort the array of header objects
4551     *
4552     * @param array $headers Array of rcube_mail_header objects indexed by UID
4553     */
4554    function sort_headers(&$headers)
4555    {
4556        if (!empty($this->uids))
4557            uksort($headers, array($this, "compare_uids"));
4558        else
4559            uasort($headers, array($this, "compare_seqnums"));
4560    }
4561
4562    /**
4563     * Sort method called by uasort()
4564     *
4565     * @param rcube_mail_header $a
4566     * @param rcube_mail_header $b
4567     */
4568    function compare_seqnums($a, $b)
4569    {
4570        // First get the sequence number from the header object (the 'id' field).
4571        $seqa = $a->id;
4572        $seqb = $b->id;
4573
4574        // then find each sequence number in my ordered list
4575        $posa = isset($this->seqs[$seqa]) ? intval($this->seqs[$seqa]) : -1;
4576        $posb = isset($this->seqs[$seqb]) ? intval($this->seqs[$seqb]) : -1;
4577
4578        // return the relative position as the comparison value
4579        return $posa - $posb;
4580    }
4581
4582    /**
4583     * Sort method called by uksort()
4584     *
4585     * @param int $a Array key (UID)
4586     * @param int $b Array key (UID)
4587     */
4588    function compare_uids($a, $b)
4589    {
4590        // then find each sequence number in my ordered list
4591        $posa = isset($this->uids[$a]) ? intval($this->uids[$a]) : -1;
4592        $posb = isset($this->uids[$b]) ? intval($this->uids[$b]) : -1;
4593
4594        // return the relative position as the comparison value
4595        return $posa - $posb;
4596    }
4597}
Note: See TracBrowser for help on using the repository browser.