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

Last change on this file since 5396 was 5396, checked in by thomasb, 20 months ago

Remove unused cruft

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