source: github/program/include/rcube_imap.php @ dfc79b3

HEADcourier-fixdev-browser-capabilitiespdorelease-0.7release-0.8
Last change on this file since dfc79b3 was dfc79b3, checked in by thomascube <thomas@…>, 19 months ago

Find charset in HTML meta tags if not specified in content-type header (#1488125)

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