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

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