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

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