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