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

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