]> git.donarmstrong.com Git - roundcube.git/blob - program/include/rcube_imap.php
Fix symlink mess
[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 5952 2012-03-03 13:20:14Z 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                     $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                 // we can do this only when LIST response is available
3044                 if (is_array($a_folders) && $name == '*' && !empty($this->conn->data['LIST'])) {
3045                     foreach ($a_folders as $idx => $folder) {
3046                         if (($opts = $this->conn->data['LIST'][$folder])
3047                             && in_array('\\NonExistent', $opts)
3048                         ) {
3049                             $this->conn->unsubscribe($folder);
3050                             unset($a_folders[$idx]);
3051                         }
3052                     }
3053                 }
3054             }
3055             // retrieve list of folders from IMAP server using LSUB
3056             else {
3057                 $a_folders = $this->conn->listSubscribed($root, $name);
3058
3059                 // unsubscribe non-existent folders, remove them from the list,
3060                 // we can do this only when LIST response is available
3061                 if (is_array($a_folders) && $name == '*' && !empty($this->conn->data['LIST'])) {
3062                     foreach ($a_folders as $idx => $folder) {
3063                         if (!isset($this->conn->data['LIST'][$folder])
3064                             || in_array('\\Noselect', $this->conn->data['LIST'][$folder])
3065                         ) {
3066                             // Some servers returns \Noselect for existing folders
3067                             if (!$this->mailbox_exists($folder)) {
3068                                 $this->conn->unsubscribe($folder);
3069                                 unset($a_folders[$idx]);
3070                             }
3071                         }
3072                     }
3073                 }
3074             }
3075         }
3076
3077         if (!is_array($a_folders) || !sizeof($a_folders)) {
3078             $a_folders = array();
3079         }
3080
3081         return $a_folders;
3082     }
3083
3084
3085     /**
3086      * Get a list of all folders available on the IMAP server
3087      *
3088      * @param string  $root      IMAP root dir
3089      * @param string  $name      Optional name pattern
3090      * @param mixed   $filter    Optional filter
3091      * @param string  $rights    Optional ACL requirements
3092      * @param bool    $skip_sort Enable to return unsorted list (for better performance)
3093      *
3094      * @return array Indexed array with folder names
3095      */
3096     function list_unsubscribed($root='', $name='*', $filter=null, $rights=null, $skip_sort=false)
3097     {
3098         $cache_key = $root.':'.$name;
3099         if (!empty($filter)) {
3100             $cache_key .= ':'.(is_string($filter) ? $filter : serialize($filter));
3101         }
3102         $cache_key .= ':'.$rights;
3103         $cache_key = 'mailboxes.list.'.md5($cache_key);
3104
3105         // get cached folder list
3106         $a_mboxes = $this->get_cache($cache_key);
3107         if (is_array($a_mboxes)) {
3108             return $a_mboxes;
3109         }
3110
3111         // Give plugins a chance to provide a list of mailboxes
3112         $data = rcmail::get_instance()->plugins->exec_hook('mailboxes_list',
3113             array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LIST'));
3114
3115         if (isset($data['folders'])) {
3116             $a_mboxes = $data['folders'];
3117         }
3118         else {
3119             // retrieve list of folders from IMAP server
3120             $a_mboxes = $this->_list_unsubscribed($root, $name);
3121         }
3122
3123         if (!is_array($a_mboxes)) {
3124             $a_mboxes = array();
3125         }
3126
3127         // INBOX should always be available
3128         if ((!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)) {
3129             array_unshift($a_mboxes, 'INBOX');
3130         }
3131
3132         // cache folder attributes
3133         if ($root == '' && $name == '*' && empty($filter)) {
3134             $this->update_cache('mailboxes.attributes', $this->conn->data['LIST']);
3135         }
3136
3137         // filter folders list according to rights requirements
3138         if ($rights && $this->get_capability('ACL')) {
3139             $a_folders = $this->filter_rights($a_folders, $rights);
3140         }
3141
3142         // filter folders and sort them
3143         if (!$skip_sort) {
3144             $a_mboxes = $this->_sort_mailbox_list($a_mboxes);
3145         }
3146
3147         // write mailboxlist to cache
3148         $this->update_cache($cache_key, $a_mboxes);
3149
3150         return $a_mboxes;
3151     }
3152
3153
3154     /**
3155      * Private method for mailbox listing (LIST)
3156      *
3157      * @param   string  $root   Optional root folder
3158      * @param   string  $name   Optional name pattern
3159      *
3160      * @return  array   List of folders
3161      * @see     rcube_imap::list_unsubscribed()
3162      */
3163     private function _list_unsubscribed($root='', $name='*')
3164     {
3165         $result = $this->conn->listMailboxes($root, $name);
3166
3167         if (!is_array($result)) {
3168             return array();
3169         }
3170
3171         // #1486796: some server configurations doesn't
3172         // return folders in all namespaces, we'll try to detect that situation
3173         // and ask for these namespaces separately
3174         if ($root == '' && $name == '*') {
3175             $delim     = $this->get_hierarchy_delimiter();
3176             $namespace = $this->get_namespace();
3177             $search    = array();
3178
3179             // build list of namespace prefixes
3180             foreach ((array)$namespace as $ns) {
3181                 if (is_array($ns)) {
3182                     foreach ($ns as $ns_data) {
3183                         if (strlen($ns_data[0])) {
3184                             $search[] = $ns_data[0];
3185                         }
3186                     }
3187                 }
3188             }
3189
3190             if (!empty($search)) {
3191                 // go through all folders detecting namespace usage
3192                 foreach ($result as $folder) {
3193                     foreach ($search as $idx => $prefix) {
3194                         if (strpos($folder, $prefix) === 0) {
3195                             unset($search[$idx]);
3196                         }
3197                     }
3198                     if (empty($search)) {
3199                         break;
3200                     }
3201                 }
3202
3203                 // get folders in hidden namespaces and add to the result
3204                 foreach ($search as $prefix) {
3205                     $list = $this->conn->listMailboxes($prefix, $name);
3206
3207                     if (!empty($list)) {
3208                         $result = array_merge($result, $list);
3209                     }
3210                 }
3211             }
3212         }
3213
3214         return $result;
3215     }
3216
3217
3218     /**
3219      * Filter the given list of folders according to access rights
3220      */
3221     private function filter_rights($a_folders, $rights)
3222     {
3223         $regex = '/('.$rights.')/';
3224         foreach ($a_folders as $idx => $folder) {
3225             $myrights = join('', (array)$this->my_rights($folder));
3226             if ($myrights !== null && !preg_match($regex, $myrights))
3227                 unset($a_folders[$idx]);
3228         }
3229
3230         return $a_folders;
3231     }
3232
3233
3234     /**
3235      * Get mailbox quota information
3236      * added by Nuny
3237      *
3238      * @return mixed Quota info or False if not supported
3239      */
3240     function get_quota()
3241     {
3242         if ($this->get_capability('QUOTA'))
3243             return $this->conn->getQuota();
3244
3245         return false;
3246     }
3247
3248
3249     /**
3250      * Get mailbox size (size of all messages in a mailbox)
3251      *
3252      * @param string $mailbox Mailbox name
3253      *
3254      * @return int Mailbox size in bytes, False on error
3255      */
3256     function get_mailbox_size($mailbox)
3257     {
3258         // @TODO: could we try to use QUOTA here?
3259         $result = $this->conn->fetchHeaderIndex($mailbox, '1:*', 'SIZE', false);
3260
3261         if (is_array($result))
3262             $result = array_sum($result);
3263
3264         return $result;
3265     }
3266
3267
3268     /**
3269      * Subscribe to a specific mailbox(es)
3270      *
3271      * @param array $a_mboxes Mailbox name(s)
3272      * @return boolean True on success
3273      */
3274     function subscribe($a_mboxes)
3275     {
3276         if (!is_array($a_mboxes))
3277             $a_mboxes = array($a_mboxes);
3278
3279         // let this common function do the main work
3280         return $this->_change_subscription($a_mboxes, 'subscribe');
3281     }
3282
3283
3284     /**
3285      * Unsubscribe mailboxes
3286      *
3287      * @param array $a_mboxes Mailbox name(s)
3288      * @return boolean True on success
3289      */
3290     function unsubscribe($a_mboxes)
3291     {
3292         if (!is_array($a_mboxes))
3293             $a_mboxes = array($a_mboxes);
3294
3295         // let this common function do the main work
3296         return $this->_change_subscription($a_mboxes, 'unsubscribe');
3297     }
3298
3299
3300     /**
3301      * Create a new mailbox on the server and register it in local cache
3302      *
3303      * @param string  $mailbox   New mailbox name
3304      * @param boolean $subscribe True if the new mailbox should be subscribed
3305      *
3306      * @return boolean True on success
3307      */
3308     function create_mailbox($mailbox, $subscribe=false)
3309     {
3310         $result = $this->conn->createFolder($mailbox);
3311
3312         // try to subscribe it
3313         if ($result) {
3314             // clear cache
3315             $this->clear_cache('mailboxes', true);
3316
3317             if ($subscribe)
3318                 $this->subscribe($mailbox);
3319         }
3320
3321         return $result;
3322     }
3323
3324
3325     /**
3326      * Set a new name to an existing mailbox
3327      *
3328      * @param string $mailbox  Mailbox to rename
3329      * @param string $new_name New mailbox name
3330      *
3331      * @return boolean True on success
3332      */
3333     function rename_mailbox($mailbox, $new_name)
3334     {
3335         if (!strlen($new_name)) {
3336             return false;
3337         }
3338
3339         $delm = $this->get_hierarchy_delimiter();
3340
3341         // get list of subscribed folders
3342         if ((strpos($mailbox, '%') === false) && (strpos($mailbox, '*') === false)) {
3343             $a_subscribed = $this->_list_mailboxes('', $mailbox . $delm . '*');
3344             $subscribed   = $this->mailbox_exists($mailbox, true);
3345         }
3346         else {
3347             $a_subscribed = $this->_list_mailboxes();
3348             $subscribed   = in_array($mailbox, $a_subscribed);
3349         }
3350
3351         $result = $this->conn->renameFolder($mailbox, $new_name);
3352
3353         if ($result) {
3354             // unsubscribe the old folder, subscribe the new one
3355             if ($subscribed) {
3356                 $this->conn->unsubscribe($mailbox);
3357                 $this->conn->subscribe($new_name);
3358             }
3359
3360             // check if mailbox children are subscribed
3361             foreach ($a_subscribed as $c_subscribed) {
3362                 if (preg_match('/^'.preg_quote($mailbox.$delm, '/').'/', $c_subscribed)) {
3363                     $this->conn->unsubscribe($c_subscribed);
3364                     $this->conn->subscribe(preg_replace('/^'.preg_quote($mailbox, '/').'/',
3365                         $new_name, $c_subscribed));
3366
3367                     // clear cache
3368                     $this->clear_message_cache($c_subscribed);
3369                 }
3370             }
3371
3372             // clear cache
3373             $this->clear_message_cache($mailbox);
3374             $this->clear_cache('mailboxes', true);
3375         }
3376
3377         return $result;
3378     }
3379
3380
3381     /**
3382      * Remove mailbox from server
3383      *
3384      * @param string $mailbox Mailbox name
3385      *
3386      * @return boolean True on success
3387      */
3388     function delete_mailbox($mailbox)
3389     {
3390         $delm = $this->get_hierarchy_delimiter();
3391
3392         // get list of folders
3393         if ((strpos($mailbox, '%') === false) && (strpos($mailbox, '*') === false))
3394             $sub_mboxes = $this->list_unsubscribed('', $mailbox . $delm . '*');
3395         else
3396             $sub_mboxes = $this->list_unsubscribed();
3397
3398         // send delete command to server
3399         $result = $this->conn->deleteFolder($mailbox);
3400
3401         if ($result) {
3402             // unsubscribe mailbox
3403             $this->conn->unsubscribe($mailbox);
3404
3405             foreach ($sub_mboxes as $c_mbox) {
3406                 if (preg_match('/^'.preg_quote($mailbox.$delm, '/').'/', $c_mbox)) {
3407                     $this->conn->unsubscribe($c_mbox);
3408                     if ($this->conn->deleteFolder($c_mbox)) {
3409                             $this->clear_message_cache($c_mbox);
3410                     }
3411                 }
3412             }
3413
3414             // clear mailbox-related cache
3415             $this->clear_message_cache($mailbox);
3416             $this->clear_cache('mailboxes', true);
3417         }
3418
3419         return $result;
3420     }
3421
3422
3423     /**
3424      * Create all folders specified as default
3425      */
3426     function create_default_folders()
3427     {
3428         // create default folders if they do not exist
3429         foreach ($this->default_folders as $folder) {
3430             if (!$this->mailbox_exists($folder))
3431                 $this->create_mailbox($folder, true);
3432             else if (!$this->mailbox_exists($folder, true))
3433                 $this->subscribe($folder);
3434         }
3435     }
3436
3437
3438     /**
3439      * Checks if folder exists and is subscribed
3440      *
3441      * @param string   $mailbox      Folder name
3442      * @param boolean  $subscription Enable subscription checking
3443      *
3444      * @return boolean TRUE or FALSE
3445      */
3446     function mailbox_exists($mailbox, $subscription=false)
3447     {
3448         if ($mailbox == 'INBOX') {
3449             return true;
3450         }
3451
3452         $key  = $subscription ? 'subscribed' : 'existing';
3453
3454         if (is_array($this->icache[$key]) && in_array($mailbox, $this->icache[$key]))
3455             return true;
3456
3457         if ($subscription) {
3458             $a_folders = $this->conn->listSubscribed('', $mailbox);
3459         }
3460         else {
3461             $a_folders = $this->conn->listMailboxes('', $mailbox);
3462         }
3463
3464         if (is_array($a_folders) && in_array($mailbox, $a_folders)) {
3465             $this->icache[$key][] = $mailbox;
3466             return true;
3467         }
3468
3469         return false;
3470     }
3471
3472
3473     /**
3474      * Returns the namespace where the folder is in
3475      *
3476      * @param string $mailbox Folder name
3477      *
3478      * @return string One of 'personal', 'other' or 'shared'
3479      * @access public
3480      */
3481     function mailbox_namespace($mailbox)
3482     {
3483         if ($mailbox == 'INBOX') {
3484             return 'personal';
3485         }
3486
3487         foreach ($this->namespace as $type => $namespace) {
3488             if (is_array($namespace)) {
3489                 foreach ($namespace as $ns) {
3490                     if ($len = strlen($ns[0])) {
3491                         if (($len > 1 && $mailbox == substr($ns[0], 0, -1))
3492                             || strpos($mailbox, $ns[0]) === 0
3493                         ) {
3494                             return $type;
3495                         }
3496                     }
3497                 }
3498             }
3499         }
3500
3501         return 'personal';
3502     }
3503
3504
3505     /**
3506      * Modify folder name according to namespace.
3507      * For output it removes prefix of the personal namespace if it's possible.
3508      * For input it adds the prefix. Use it before creating a folder in root
3509      * of the folders tree.
3510      *
3511      * @param string $mailbox Folder name
3512      * @param string $mode    Mode name (out/in)
3513      *
3514      * @return string Folder name
3515      */
3516     function mod_mailbox($mailbox, $mode = 'out')
3517     {
3518         if (!strlen($mailbox)) {
3519             return $mailbox;
3520         }
3521
3522         $prefix     = $this->namespace['prefix']; // see set_env()
3523         $prefix_len = strlen($prefix);
3524
3525         if (!$prefix_len) {
3526             return $mailbox;
3527         }
3528
3529         // remove prefix for output
3530         if ($mode == 'out') {
3531             if (substr($mailbox, 0, $prefix_len) === $prefix) {
3532                 return substr($mailbox, $prefix_len);
3533             }
3534         }
3535         // add prefix for input (e.g. folder creation)
3536         else {
3537             return $prefix . $mailbox;
3538         }
3539
3540         return $mailbox;
3541     }
3542
3543
3544     /**
3545      * Gets folder attributes from LIST response, e.g. \Noselect, \Noinferiors
3546      *
3547      * @param string $mailbox Folder name
3548      * @param bool   $force   Set to True if attributes should be refreshed
3549      *
3550      * @return array Options list
3551      */
3552     function mailbox_attributes($mailbox, $force=false)
3553     {
3554         // get attributes directly from LIST command
3555         if (!empty($this->conn->data['LIST']) && is_array($this->conn->data['LIST'][$mailbox])) {
3556             $opts = $this->conn->data['LIST'][$mailbox];
3557         }
3558         // get cached folder attributes
3559         else if (!$force) {
3560             $opts = $this->get_cache('mailboxes.attributes');
3561             $opts = $opts[$mailbox];
3562         }
3563
3564         if (!is_array($opts)) {
3565             $this->conn->listMailboxes('', $mailbox);
3566             $opts = $this->conn->data['LIST'][$mailbox];
3567         }
3568
3569         return is_array($opts) ? $opts : array();
3570     }
3571
3572
3573     /**
3574      * Gets connection (and current mailbox) data: UIDVALIDITY, EXISTS, RECENT,
3575      * PERMANENTFLAGS, UIDNEXT, UNSEEN
3576      *
3577      * @param string $mailbox Folder name
3578      *
3579      * @return array Data
3580      */
3581     function mailbox_data($mailbox)
3582     {
3583         if (!strlen($mailbox))
3584             $mailbox = $this->mailbox !== null ? $this->mailbox : 'INBOX';
3585
3586         if ($this->conn->selected != $mailbox) {
3587             if ($this->conn->select($mailbox))
3588                 $this->mailbox = $mailbox;
3589             else
3590                 return null;
3591         }
3592
3593         $data = $this->conn->data;
3594
3595         // add (E)SEARCH result for ALL UNDELETED query
3596         if (!empty($this->icache['undeleted_idx']) && $this->icache['undeleted_idx'][0] == $mailbox) {
3597             $data['ALL_UNDELETED']   = $this->icache['undeleted_idx'][1];
3598             $data['COUNT_UNDELETED'] = $this->icache['undeleted_idx'][2];
3599         }
3600
3601         return $data;
3602     }
3603
3604
3605     /**
3606      * Returns extended information about the folder
3607      *
3608      * @param string $mailbox Folder name
3609      *
3610      * @return array Data
3611      */
3612     function mailbox_info($mailbox)
3613     {
3614         if ($this->icache['options'] && $this->icache['options']['name'] == $mailbox) {
3615             return $this->icache['options'];
3616         }
3617
3618         $acl       = $this->get_capability('ACL');
3619         $namespace = $this->get_namespace();
3620         $options   = array();
3621
3622         // check if the folder is a namespace prefix
3623         if (!empty($namespace)) {
3624             $mbox = $mailbox . $this->delimiter;
3625             foreach ($namespace as $ns) {
3626                 if (!empty($ns)) {
3627                     foreach ($ns as $item) {
3628                         if ($item[0] === $mbox) {
3629                             $options['is_root'] = true;
3630                             break 2;
3631                         }
3632                     }
3633                 }
3634             }
3635         }
3636         // check if the folder is other user virtual-root
3637         if (!$options['is_root'] && !empty($namespace) && !empty($namespace['other'])) {
3638             $parts = explode($this->delimiter, $mailbox);
3639             if (count($parts) == 2) {
3640                 $mbox = $parts[0] . $this->delimiter;
3641                 foreach ($namespace['other'] as $item) {
3642                     if ($item[0] === $mbox) {
3643                         $options['is_root'] = true;
3644                         break;
3645                     }
3646                 }
3647             }
3648         }
3649
3650         $options['name']       = $mailbox;
3651         $options['attributes'] = $this->mailbox_attributes($mailbox, true);
3652         $options['namespace']  = $this->mailbox_namespace($mailbox);
3653         $options['rights']     = $acl && !$options['is_root'] ? (array)$this->my_rights($mailbox) : array();
3654         $options['special']    = in_array($mailbox, $this->default_folders);
3655
3656         // Set 'noselect' and 'norename' flags
3657         if (is_array($options['attributes'])) {
3658             foreach ($options['attributes'] as $attrib) {
3659                 $attrib = strtolower($attrib);
3660                 if ($attrib == '\noselect' || $attrib == '\nonexistent') {
3661                     $options['noselect'] = true;
3662                 }
3663             }
3664         }
3665         else {
3666             $options['noselect'] = true;
3667         }
3668
3669         if (!empty($options['rights'])) {
3670             $options['norename'] = !in_array('x', $options['rights']) && !in_array('d', $options['rights']);
3671
3672             if (!$options['noselect']) {
3673                 $options['noselect'] = !in_array('r', $options['rights']);
3674             }
3675         }
3676         else {
3677             $options['norename'] = $options['is_root'] || $options['namespace'] != 'personal';
3678         }
3679
3680         $this->icache['options'] = $options;
3681
3682         return $options;
3683     }
3684
3685
3686     /**
3687      * Synchronizes messages cache.
3688      *
3689      * @param string $mailbox Folder name
3690      */
3691     public function mailbox_sync($mailbox)
3692     {
3693         if ($mcache = $this->get_mcache_engine()) {
3694             $mcache->synchronize($mailbox);
3695         }
3696     }
3697
3698
3699     /**
3700      * Get message header names for rcube_imap_generic::fetchHeader(s)
3701      *
3702      * @return string Space-separated list of header names
3703      */
3704     private function get_fetch_headers()
3705     {
3706         $headers = explode(' ', $this->fetch_add_headers);
3707         $headers = array_map('strtoupper', $headers);
3708
3709         if ($this->messages_caching || $this->get_all_headers)
3710             $headers = array_merge($headers, $this->all_headers);
3711
3712         return implode(' ', array_unique($headers));
3713     }
3714
3715
3716     /* -----------------------------------------
3717      *   ACL and METADATA/ANNOTATEMORE methods
3718      * ----------------------------------------*/
3719
3720     /**
3721      * Changes the ACL on the specified mailbox (SETACL)
3722      *
3723      * @param string $mailbox Mailbox name
3724      * @param string $user    User name
3725      * @param string $acl     ACL string
3726      *
3727      * @return boolean True on success, False on failure
3728      *
3729      * @access public
3730      * @since 0.5-beta
3731      */
3732     function set_acl($mailbox, $user, $acl)
3733     {
3734         if ($this->get_capability('ACL'))
3735             return $this->conn->setACL($mailbox, $user, $acl);
3736
3737         return false;
3738     }
3739
3740
3741     /**
3742      * Removes any <identifier,rights> pair for the
3743      * specified user from the ACL for the specified
3744      * mailbox (DELETEACL)
3745      *
3746      * @param string $mailbox Mailbox name
3747      * @param string $user    User name
3748      *
3749      * @return boolean True on success, False on failure
3750      *
3751      * @access public
3752      * @since 0.5-beta
3753      */
3754     function delete_acl($mailbox, $user)
3755     {
3756         if ($this->get_capability('ACL'))
3757             return $this->conn->deleteACL($mailbox, $user);
3758
3759         return false;
3760     }
3761
3762
3763     /**
3764      * Returns the access control list for mailbox (GETACL)
3765      *
3766      * @param string $mailbox Mailbox name
3767      *
3768      * @return array User-rights array on success, NULL on error
3769      * @access public
3770      * @since 0.5-beta
3771      */
3772     function get_acl($mailbox)
3773     {
3774         if ($this->get_capability('ACL'))
3775             return $this->conn->getACL($mailbox);
3776
3777         return NULL;
3778     }
3779
3780
3781     /**
3782      * Returns information about what rights can be granted to the
3783      * user (identifier) in the ACL for the mailbox (LISTRIGHTS)
3784      *
3785      * @param string $mailbox Mailbox name
3786      * @param string $user    User name
3787      *
3788      * @return array List of user rights
3789      * @access public
3790      * @since 0.5-beta
3791      */
3792     function list_rights($mailbox, $user)
3793     {
3794         if ($this->get_capability('ACL'))
3795             return $this->conn->listRights($mailbox, $user);
3796
3797         return NULL;
3798     }
3799
3800
3801     /**
3802      * Returns the set of rights that the current user has to
3803      * mailbox (MYRIGHTS)
3804      *
3805      * @param string $mailbox Mailbox name
3806      *
3807      * @return array MYRIGHTS response on success, NULL on error
3808      * @access public
3809      * @since 0.5-beta
3810      */
3811     function my_rights($mailbox)
3812     {
3813         if ($this->get_capability('ACL'))
3814             return $this->conn->myRights($mailbox);
3815
3816         return NULL;
3817     }
3818
3819
3820     /**
3821      * Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
3822      *
3823      * @param string $mailbox Mailbox name (empty for server metadata)
3824      * @param array  $entries Entry-value array (use NULL value as NIL)
3825      *
3826      * @return boolean True on success, False on failure
3827      * @access public
3828      * @since 0.5-beta
3829      */
3830     function set_metadata($mailbox, $entries)
3831     {
3832         if ($this->get_capability('METADATA') ||
3833             (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
3834         ) {
3835             return $this->conn->setMetadata($mailbox, $entries);
3836         }
3837         else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
3838             foreach ((array)$entries as $entry => $value) {
3839                 list($ent, $attr) = $this->md2annotate($entry);
3840                 $entries[$entry] = array($ent, $attr, $value);
3841             }
3842             return $this->conn->setAnnotation($mailbox, $entries);
3843         }
3844
3845         return false;
3846     }
3847
3848
3849     /**
3850      * Unsets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
3851      *
3852      * @param string $mailbox Mailbox name (empty for server metadata)
3853      * @param array  $entries Entry names array
3854      *
3855      * @return boolean True on success, False on failure
3856      *
3857      * @access public
3858      * @since 0.5-beta
3859      */
3860     function delete_metadata($mailbox, $entries)
3861     {
3862         if ($this->get_capability('METADATA') || 
3863             (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
3864         ) {
3865             return $this->conn->deleteMetadata($mailbox, $entries);
3866         }
3867         else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
3868             foreach ((array)$entries as $idx => $entry) {
3869                 list($ent, $attr) = $this->md2annotate($entry);
3870                 $entries[$idx] = array($ent, $attr, NULL);
3871             }
3872             return $this->conn->setAnnotation($mailbox, $entries);
3873         }
3874
3875         return false;
3876     }
3877
3878
3879     /**
3880      * Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION)
3881      *
3882      * @param string $mailbox Mailbox name (empty for server metadata)
3883      * @param array  $entries Entries
3884      * @param array  $options Command options (with MAXSIZE and DEPTH keys)
3885      *
3886      * @return array Metadata entry-value hash array on success, NULL on error
3887      *
3888      * @access public
3889      * @since 0.5-beta
3890      */
3891     function get_metadata($mailbox, $entries, $options=array())
3892     {
3893         if ($this->get_capability('METADATA') || 
3894             (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
3895         ) {
3896             return $this->conn->getMetadata($mailbox, $entries, $options);
3897         }
3898         else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
3899             $queries = array();
3900             $res     = array();
3901
3902             // Convert entry names
3903             foreach ((array)$entries as $entry) {
3904                 list($ent, $attr) = $this->md2annotate($entry);
3905                 $queries[$attr][] = $ent;
3906             }
3907
3908             // @TODO: Honor MAXSIZE and DEPTH options
3909             foreach ($queries as $attrib => $entry)
3910                 if ($result = $this->conn->getAnnotation($mailbox, $entry, $attrib))
3911                     $res = array_merge_recursive($res, $result);
3912
3913             return $res;
3914         }
3915
3916         return NULL;
3917     }
3918
3919
3920     /**
3921      * Converts the METADATA extension entry name into the correct
3922      * entry-attrib names for older ANNOTATEMORE version.
3923      *
3924      * @param string $entry Entry name
3925      *
3926      * @return array Entry-attribute list, NULL if not supported (?)
3927      */
3928     private function md2annotate($entry)
3929     {
3930         if (substr($entry, 0, 7) == '/shared') {
3931             return array(substr($entry, 7), 'value.shared');
3932         }
3933         else if (substr($entry, 0, 8) == '/private') {
3934             return array(substr($entry, 8), 'value.priv');
3935         }
3936
3937         // @TODO: log error
3938         return NULL;
3939     }
3940
3941
3942     /* --------------------------------
3943      *   internal caching methods
3944      * --------------------------------*/
3945
3946     /**
3947      * Enable or disable indexes caching
3948      *
3949      * @param string $type Cache type (@see rcmail::get_cache)
3950      * @access public
3951      */
3952     function set_caching($type)
3953     {
3954         if ($type) {
3955             $this->caching = $type;
3956         }
3957         else {
3958             if ($this->cache)
3959                 $this->cache->close();
3960             $this->cache   = null;
3961             $this->caching = false;
3962         }
3963     }
3964
3965     /**
3966      * Getter for IMAP cache object
3967      */
3968     private function get_cache_engine()
3969     {
3970         if ($this->caching && !$this->cache) {
3971             $rcmail = rcmail::get_instance();
3972             $this->cache = $rcmail->get_cache('IMAP', $this->caching);
3973         }
3974
3975         return $this->cache;
3976     }
3977
3978     /**
3979      * Returns cached value
3980      *
3981      * @param string $key Cache key
3982      * @return mixed
3983      * @access public
3984      */
3985     function get_cache($key)
3986     {
3987         if ($cache = $this->get_cache_engine()) {
3988             return $cache->get($key);
3989         }
3990     }
3991
3992     /**
3993      * Update cache
3994      *
3995      * @param string $key  Cache key
3996      * @param mixed  $data Data
3997      * @access public
3998      */
3999     function update_cache($key, $data)
4000     {
4001         if ($cache = $this->get_cache_engine()) {
4002             $cache->set($key, $data);
4003         }
4004     }
4005
4006     /**
4007      * Clears the cache.
4008      *
4009      * @param string  $key         Cache key name or pattern
4010      * @param boolean $prefix_mode Enable it to clear all keys starting
4011      *                             with prefix specified in $key
4012      * @access public
4013      */
4014     function clear_cache($key=null, $prefix_mode=false)
4015     {
4016         if ($cache = $this->get_cache_engine()) {
4017             $cache->remove($key, $prefix_mode);
4018         }
4019     }
4020
4021
4022     /* --------------------------------
4023      *   message caching methods
4024      * --------------------------------*/
4025
4026     /**
4027      * Enable or disable messages caching
4028      *
4029      * @param boolean $set Flag
4030      */
4031     function set_messages_caching($set)
4032     {
4033         if ($set) {
4034             $this->messages_caching = true;
4035         }
4036         else {
4037             if ($this->mcache)
4038                 $this->mcache->close();
4039             $this->mcache = null;
4040             $this->messages_caching = false;
4041         }
4042     }
4043
4044     /**
4045      * Getter for messages cache object
4046      */
4047     private function get_mcache_engine()
4048     {
4049         if ($this->messages_caching && !$this->mcache) {
4050             $rcmail = rcmail::get_instance();
4051             if ($dbh = $rcmail->get_dbh()) {
4052                 $this->mcache = new rcube_imap_cache(
4053                     $dbh, $this, $rcmail->user->ID, $this->skip_deleted);
4054             }
4055         }
4056
4057         return $this->mcache;
4058     }
4059
4060     /**
4061      * Clears the messages cache.
4062      *
4063      * @param string $mailbox Folder name
4064      * @param array  $uids    Optional message UIDs to remove from cache
4065      */
4066     function clear_message_cache($mailbox = null, $uids = null)
4067     {
4068         if ($mcache = $this->get_mcache_engine()) {
4069             $mcache->clear($mailbox, $uids);
4070         }
4071     }
4072
4073
4074
4075     /* --------------------------------
4076      *   encoding/decoding methods
4077      * --------------------------------*/
4078
4079     /**
4080      * Split an address list into a structured array list
4081      *
4082      * @param string  $input  Input string
4083      * @param int     $max    List only this number of addresses
4084      * @param boolean $decode Decode address strings
4085      * @return array  Indexed list of addresses
4086      */
4087     function decode_address_list($input, $max=null, $decode=true)
4088     {
4089         $a = $this->_parse_address_list($input, $decode);
4090         $out = array();
4091         // Special chars as defined by RFC 822 need to in quoted string (or escaped).
4092         $special_chars = '[\(\)\<\>\\\.\[\]@,;:"]';
4093
4094         if (!is_array($a))
4095             return $out;
4096
4097         $c = count($a);
4098         $j = 0;
4099
4100         foreach ($a as $val) {
4101             $j++;
4102             $address = trim($val['address']);
4103             $name    = trim($val['name']);
4104
4105             if ($name && $address && $name != $address)
4106                 $string = sprintf('%s <%s>', preg_match("/$special_chars/", $name) ? '"'.addcslashes($name, '"').'"' : $name, $address);
4107             else if ($address)
4108                 $string = $address;
4109             else if ($name)
4110                 $string = $name;
4111
4112             $out[$j] = array(
4113                 'name'   => $name,
4114                 'mailto' => $address,
4115                 'string' => $string
4116             );
4117
4118             if ($max && $j==$max)
4119                 break;
4120         }
4121
4122         return $out;
4123     }
4124
4125
4126     /**
4127      * Decode a message header value
4128      *
4129      * @param string  $input         Header value
4130      * @param boolean $remove_quotas Remove quotes if necessary
4131      * @return string Decoded string
4132      */
4133     function decode_header($input, $remove_quotes=false)
4134     {
4135         $str = rcube_imap::decode_mime_string((string)$input, $this->default_charset);
4136         if ($str[0] == '"' && $remove_quotes)
4137             $str = str_replace('"', '', $str);
4138
4139         return $str;
4140     }
4141
4142
4143     /**
4144      * Decode a mime-encoded string to internal charset
4145      *
4146      * @param string $input    Header value
4147      * @param string $fallback Fallback charset if none specified
4148      *
4149      * @return string Decoded string
4150      * @static
4151      */
4152     public static function decode_mime_string($input, $fallback=null)
4153     {
4154         if (!empty($fallback)) {
4155             $default_charset = $fallback;
4156         }
4157         else {
4158             $default_charset = rcmail::get_instance()->config->get('default_charset', 'ISO-8859-1');
4159         }
4160
4161         // rfc: all line breaks or other characters not found
4162         // in the Base64 Alphabet must be ignored by decoding software
4163         // delete all blanks between MIME-lines, differently we can
4164         // receive unnecessary blanks and broken utf-8 symbols
4165         $input = preg_replace("/\?=\s+=\?/", '?==?', $input);
4166
4167         // encoded-word regexp
4168         $re = '/=\?([^?]+)\?([BbQq])\?([^\n]*?)\?=/';
4169
4170         // Find all RFC2047's encoded words
4171         if (preg_match_all($re, $input, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {
4172             // Initialize variables
4173             $tmp   = array();
4174             $out   = '';
4175             $start = 0;
4176
4177             foreach ($matches as $idx => $m) {
4178                 $pos      = $m[0][1];
4179                 $charset  = $m[1][0];
4180                 $encoding = $m[2][0];
4181                 $text     = $m[3][0];
4182                 $length   = strlen($m[0][0]);
4183
4184                 // Append everything that is before the text to be decoded
4185                 if ($start != $pos) {
4186                     $substr = substr($input, $start, $pos-$start);
4187                     $out   .= rcube_charset_convert($substr, $default_charset);
4188                     $start  = $pos;
4189                 }
4190                 $start += $length;
4191
4192                 // Per RFC2047, each string part "MUST represent an integral number
4193                 // of characters . A multi-octet character may not be split across
4194                 // adjacent encoded-words." However, some mailers break this, so we
4195                 // try to handle characters spanned across parts anyway by iterating
4196                 // through and aggregating sequential encoded parts with the same
4197                 // character set and encoding, then perform the decoding on the
4198                 // aggregation as a whole.
4199
4200                 $tmp[] = $text;
4201                 if ($next_match = $matches[$idx+1]) {
4202                     if ($next_match[0][1] == $start
4203                         && $next_match[1][0] == $charset
4204                         && $next_match[2][0] == $encoding
4205                     ) {
4206                         continue;
4207                     }
4208                 }
4209
4210                 $count = count($tmp);
4211                 $text  = '';
4212
4213                 // Decode and join encoded-word's chunks
4214                 if ($encoding == 'B' || $encoding == 'b') {
4215                     // base64 must be decoded a segment at a time
4216                     for ($i=0; $i<$count; $i++)
4217                         $text .= base64_decode($tmp[$i]);
4218                 }
4219                 else { //if ($encoding == 'Q' || $encoding == 'q') {
4220                     // quoted printable can be combined and processed at once
4221                     for ($i=0; $i<$count; $i++)
4222                         $text .= $tmp[$i];
4223
4224                     $text = str_replace('_', ' ', $text);
4225                     $text = quoted_printable_decode($text);
4226                 }
4227
4228                 $out .= rcube_charset_convert($text, $charset);
4229                 $tmp = array();
4230             }
4231
4232             // add the last part of the input string
4233             if ($start != strlen($input)) {
4234                 $out .= rcube_charset_convert(substr($input, $start), $default_charset);
4235             }
4236
4237             // return the results
4238             return $out;
4239         }
4240
4241         // no encoding information, use fallback
4242         return rcube_charset_convert($input, $default_charset);
4243     }
4244
4245
4246     /**
4247      * Decode a mime part
4248      *
4249      * @param string $input    Input string
4250      * @param string $encoding Part encoding
4251      * @return string Decoded string
4252      */
4253     function mime_decode($input, $encoding='7bit')
4254     {
4255         switch (strtolower($encoding)) {
4256         case 'quoted-printable':
4257             return quoted_printable_decode($input);
4258         case 'base64':
4259             return base64_decode($input);
4260         case 'x-uuencode':
4261         case 'x-uue':
4262         case 'uue':
4263         case 'uuencode':
4264             return convert_uudecode($input);
4265         case '7bit':
4266         default:
4267             return $input;
4268         }
4269     }
4270
4271
4272     /**
4273      * Convert body charset to RCMAIL_CHARSET according to the ctype_parameters
4274      *
4275      * @param string $body        Part body to decode
4276      * @param string $ctype_param Charset to convert from
4277      * @return string Content converted to internal charset
4278      */
4279     function charset_decode($body, $ctype_param)
4280     {
4281         if (is_array($ctype_param) && !empty($ctype_param['charset']))
4282             return rcube_charset_convert($body, $ctype_param['charset']);
4283
4284         // defaults to what is specified in the class header
4285         return rcube_charset_convert($body,  $this->default_charset);
4286     }
4287
4288
4289     /* --------------------------------
4290      *         private methods
4291      * --------------------------------*/
4292
4293     /**
4294      * Validate the given input and save to local properties
4295      *
4296      * @param string $sort_field Sort column
4297      * @param string $sort_order Sort order
4298      * @access private
4299      */
4300     private function _set_sort_order($sort_field, $sort_order)
4301     {
4302         if ($sort_field != null)
4303             $this->sort_field = asciiwords($sort_field);
4304         if ($sort_order != null)
4305             $this->sort_order = strtoupper($sort_order) == 'DESC' ? 'DESC' : 'ASC';
4306     }
4307
4308
4309     /**
4310      * Sort mailboxes first by default folders and then in alphabethical order
4311      *
4312      * @param array $a_folders Mailboxes list
4313      * @access private
4314      */
4315     private function _sort_mailbox_list($a_folders)
4316     {
4317         $a_out = $a_defaults = $folders = array();
4318
4319         $delimiter = $this->get_hierarchy_delimiter();
4320
4321         // find default folders and skip folders starting with '.'
4322         foreach ($a_folders as $i => $folder) {
4323             if ($folder[0] == '.')
4324                 continue;
4325
4326             if (($p = array_search($folder, $this->default_folders)) !== false && !$a_defaults[$p])
4327                 $a_defaults[$p] = $folder;
4328             else
4329                 $folders[$folder] = rcube_charset_convert($folder, 'UTF7-IMAP');
4330         }
4331
4332         // sort folders and place defaults on the top
4333         asort($folders, SORT_LOCALE_STRING);
4334         ksort($a_defaults);
4335         $folders = array_merge($a_defaults, array_keys($folders));
4336
4337         // finally we must rebuild the list to move
4338         // subfolders of default folders to their place...
4339         // ...also do this for the rest of folders because
4340         // asort() is not properly sorting case sensitive names
4341         while (list($key, $folder) = each($folders)) {
4342             // set the type of folder name variable (#1485527)
4343             $a_out[] = (string) $folder;
4344             unset($folders[$key]);
4345             $this->_rsort($folder, $delimiter, $folders, $a_out);
4346         }
4347
4348         return $a_out;
4349     }
4350
4351
4352     /**
4353      * @access private
4354      */
4355     private function _rsort($folder, $delimiter, &$list, &$out)
4356     {
4357         while (list($key, $name) = each($list)) {
4358                 if (strpos($name, $folder.$delimiter) === 0) {
4359                     // set the type of folder name variable (#1485527)
4360                 $out[] = (string) $name;
4361                     unset($list[$key]);
4362                     $this->_rsort($name, $delimiter, $list, $out);
4363                 }
4364         }
4365         reset($list);
4366     }
4367
4368
4369     /**
4370      * Finds message sequence ID for specified UID
4371      *
4372      * @param int    $uid      Message UID
4373      * @param string $mailbox  Mailbox name
4374      * @param bool   $force    True to skip cache
4375      *
4376      * @return int Message (sequence) ID
4377      */
4378     function uid2id($uid, $mailbox = null, $force = false)
4379     {
4380         if (!strlen($mailbox)) {
4381             $mailbox = $this->mailbox;
4382         }
4383
4384         if (!empty($this->uid_id_map[$mailbox][$uid])) {
4385             return $this->uid_id_map[$mailbox][$uid];
4386         }
4387
4388         if (!$force && ($mcache = $this->get_mcache_engine()))
4389             $id = $mcache->uid2id($mailbox, $uid);
4390
4391         if (empty($id))
4392             $id = $this->conn->UID2ID($mailbox, $uid);
4393
4394         $this->uid_id_map[$mailbox][$uid] = $id;
4395
4396         return $id;
4397     }
4398
4399
4400     /**
4401      * Find UID of the specified message sequence ID
4402      *
4403      * @param int    $id       Message (sequence) ID
4404      * @param string $mailbox  Mailbox name
4405      * @param bool   $force    True to skip cache
4406      *
4407      * @return int Message UID
4408      */
4409     function id2uid($id, $mailbox = null, $force = false)
4410     {
4411         if (!strlen($mailbox)) {
4412             $mailbox = $this->mailbox;
4413         }
4414
4415         if ($uid = array_search($id, (array)$this->uid_id_map[$mailbox])) {
4416             return $uid;
4417         }
4418
4419         if (!$force && ($mcache = $this->get_mcache_engine()))
4420             $uid = $mcache->id2uid($mailbox, $id);
4421
4422         if (empty($uid))
4423             $uid = $this->conn->ID2UID($mailbox, $id);
4424
4425         $this->uid_id_map[$mailbox][$uid] = $id;
4426
4427         return $uid;
4428     }
4429
4430
4431     /**
4432      * Subscribe/unsubscribe a list of mailboxes and update local cache
4433      * @access private
4434      */
4435     private function _change_subscription($a_mboxes, $mode)
4436     {
4437         $updated = false;
4438
4439         if (is_array($a_mboxes))
4440             foreach ($a_mboxes as $i => $mailbox) {
4441                 $a_mboxes[$i] = $mailbox;
4442
4443                 if ($mode == 'subscribe')
4444                     $updated = $this->conn->subscribe($mailbox);
4445                 else if ($mode == 'unsubscribe')
4446                     $updated = $this->conn->unsubscribe($mailbox);
4447             }
4448
4449         // clear cached mailbox list(s)
4450         if ($updated) {
4451             $this->clear_cache('mailboxes', true);
4452         }
4453
4454         return $updated;
4455     }
4456
4457
4458     /**
4459      * Increde/decrese messagecount for a specific mailbox
4460      * @access private
4461      */
4462     private function _set_messagecount($mailbox, $mode, $increment)
4463     {
4464         $mode = strtoupper($mode);
4465         $a_mailbox_cache = $this->get_cache('messagecount');
4466
4467         if (!is_array($a_mailbox_cache[$mailbox]) || !isset($a_mailbox_cache[$mailbox][$mode]) || !is_numeric($increment))
4468             return false;
4469
4470         // add incremental value to messagecount
4471         $a_mailbox_cache[$mailbox][$mode] += $increment;
4472
4473         // there's something wrong, delete from cache
4474         if ($a_mailbox_cache[$mailbox][$mode] < 0)
4475             unset($a_mailbox_cache[$mailbox][$mode]);
4476
4477         // write back to cache
4478         $this->update_cache('messagecount', $a_mailbox_cache);
4479
4480         return true;
4481     }
4482
4483
4484     /**
4485      * Remove messagecount of a specific mailbox from cache
4486      * @access private
4487      */
4488     private function _clear_messagecount($mailbox, $mode=null)
4489     {
4490         $a_mailbox_cache = $this->get_cache('messagecount');
4491
4492         if (is_array($a_mailbox_cache[$mailbox])) {
4493             if ($mode) {
4494                 unset($a_mailbox_cache[$mailbox][$mode]);
4495             }
4496             else {
4497                 unset($a_mailbox_cache[$mailbox]);
4498             }
4499             $this->update_cache('messagecount', $a_mailbox_cache);
4500         }
4501     }
4502
4503
4504     /**
4505      * Split RFC822 header string into an associative array
4506      * @access private
4507      */
4508     private function _parse_headers($headers)
4509     {
4510         $a_headers = array();
4511         $headers = preg_replace('/\r?\n(\t| )+/', ' ', $headers);
4512         $lines = explode("\n", $headers);
4513         $c = count($lines);
4514
4515         for ($i=0; $i<$c; $i++) {
4516             if ($p = strpos($lines[$i], ': ')) {
4517                 $field = strtolower(substr($lines[$i], 0, $p));
4518                 $value = trim(substr($lines[$i], $p+1));
4519                 if (!empty($value))
4520                     $a_headers[$field] = $value;
4521             }
4522         }
4523
4524         return $a_headers;
4525     }
4526
4527
4528     /**
4529      * @access private
4530      */
4531     private function _parse_address_list($str, $decode=true)
4532     {
4533         // remove any newlines and carriage returns before
4534         $str = preg_replace('/\r?\n(\s|\t)?/', ' ', $str);
4535
4536         // extract list items, remove comments
4537         $str = self::explode_header_string(',;', $str, true);
4538         $result = array();
4539
4540         // simplified regexp, supporting quoted local part
4541         $email_rx = '(\S+|("\s*(?:[^"\f\n\r\t\v\b\s]+\s*)+"))@\S+';
4542
4543         foreach ($str as $key => $val) {
4544             $name    = '';
4545             $address = '';
4546             $val     = trim($val);
4547
4548             if (preg_match('/(.*)<('.$email_rx.')>$/', $val, $m)) {
4549                 $address = $m[2];
4550                 $name    = trim($m[1]);
4551             }
4552             else if (preg_match('/^('.$email_rx.')$/', $val, $m)) {
4553                 $address = $m[1];
4554                 $name    = '';
4555             }
4556             else {
4557                 $name = $val;
4558             }
4559
4560             // dequote and/or decode name
4561             if ($name) {
4562                 if ($name[0] == '"' && $name[strlen($name)-1] == '"') {
4563                     $name = substr($name, 1, -1);
4564                     $name = stripslashes($name);
4565                 }
4566                 if ($decode) {
4567                     $name = $this->decode_header($name);
4568                 }
4569             }
4570
4571             if (!$address && $name) {
4572                 $address = $name;
4573             }
4574
4575             if ($address) {
4576                 $result[$key] = array('name' => $name, 'address' => $address);
4577             }
4578         }
4579
4580         return $result;
4581     }
4582
4583
4584     /**
4585      * Explodes header (e.g. address-list) string into array of strings
4586      * using specified separator characters with proper handling
4587      * of quoted-strings and comments (RFC2822)
4588      *
4589      * @param string $separator       String containing separator characters
4590      * @param string $str             Header string
4591      * @param bool   $remove_comments Enable to remove comments
4592      *
4593      * @return array Header items
4594      */
4595     static function explode_header_string($separator, $str, $remove_comments=false)
4596     {
4597         $length  = strlen($str);
4598         $result  = array();
4599         $quoted  = false;
4600         $comment = 0;
4601         $out     = '';
4602
4603         for ($i=0; $i<$length; $i++) {
4604             // we're inside a quoted string
4605             if ($quoted) {
4606                 if ($str[$i] == '"') {
4607                     $quoted = false;
4608                 }
4609                 else if ($str[$i] == '\\') {
4610                     if ($comment <= 0) {
4611                         $out .= '\\';
4612                     }
4613                     $i++;
4614                 }
4615             }
4616             // we're inside a comment string
4617             else if ($comment > 0) {
4618                     if ($str[$i] == ')') {
4619                         $comment--;
4620                     }
4621                     else if ($str[$i] == '(') {
4622                         $comment++;
4623                     }
4624                     else if ($str[$i] == '\\') {
4625                         $i++;
4626                     }
4627                     continue;
4628             }
4629             // separator, add to result array
4630             else if (strpos($separator, $str[$i]) !== false) {
4631                     if ($out) {
4632                         $result[] = $out;
4633                     }
4634                     $out = '';
4635                     continue;
4636             }
4637             // start of quoted string
4638             else if ($str[$i] == '"') {
4639                     $quoted = true;
4640             }
4641             // start of comment
4642             else if ($remove_comments && $str[$i] == '(') {
4643                     $comment++;
4644             }
4645
4646             if ($comment <= 0) {
4647                 $out .= $str[$i];
4648             }
4649         }
4650
4651         if ($out && $comment <= 0) {
4652             $result[] = $out;
4653         }
4654
4655         return $result;
4656     }
4657
4658
4659     /**
4660      * This is our own debug handler for the IMAP connection
4661      * @access public
4662      */
4663     public function debug_handler(&$imap, $message)
4664     {
4665         write_log('imap', $message);
4666     }
4667
4668 }  // end class rcube_imap
4669
4670
4671 /**
4672  * Class representing a message part
4673  *
4674  * @package Mail
4675  */
4676 class rcube_message_part
4677 {
4678     var $mime_id = '';
4679     var $ctype_primary = 'text';
4680     var $ctype_secondary = 'plain';
4681     var $mimetype = 'text/plain';
4682     var $disposition = '';
4683     var $filename = '';
4684     var $encoding = '8bit';
4685     var $charset = '';
4686     var $size = 0;
4687     var $headers = array();
4688     var $d_parameters = array();
4689     var $ctype_parameters = array();
4690
4691     function __clone()
4692     {
4693         if (isset($this->parts))
4694             foreach ($this->parts as $idx => $part)
4695                 if (is_object($part))
4696                         $this->parts[$idx] = clone $part;
4697     }
4698 }
4699
4700
4701 /**
4702  * Class for sorting an array of rcube_mail_header objects in a predetermined order.
4703  *
4704  * @package Mail
4705  * @author Eric Stadtherr
4706  */
4707 class rcube_header_sorter
4708 {
4709     private $seqs = array();
4710     private $uids = array();
4711
4712
4713     /**
4714      * Set the predetermined sort order.
4715      *
4716      * @param array $index  Numerically indexed array of IMAP ID or UIDs
4717      * @param bool  $is_uid Set to true if $index contains UIDs
4718      */
4719     function set_index($index, $is_uid = false)
4720     {
4721         $index = array_flip($index);
4722
4723         if ($is_uid)
4724             $this->uids = $index;
4725         else
4726             $this->seqs = $index;
4727     }
4728
4729     /**
4730      * Sort the array of header objects
4731      *
4732      * @param array $headers Array of rcube_mail_header objects indexed by UID
4733      */
4734     function sort_headers(&$headers)
4735     {
4736         if (!empty($this->uids))
4737             uksort($headers, array($this, "compare_uids"));
4738         else
4739             uasort($headers, array($this, "compare_seqnums"));
4740     }
4741
4742     /**
4743      * Sort method called by uasort()
4744      *
4745      * @param rcube_mail_header $a
4746      * @param rcube_mail_header $b
4747      */
4748     function compare_seqnums($a, $b)
4749     {
4750         // First get the sequence number from the header object (the 'id' field).
4751         $seqa = $a->id;
4752         $seqb = $b->id;
4753
4754         // then find each sequence number in my ordered list
4755         $posa = isset($this->seqs[$seqa]) ? intval($this->seqs[$seqa]) : -1;
4756         $posb = isset($this->seqs[$seqb]) ? intval($this->seqs[$seqb]) : -1;
4757
4758         // return the relative position as the comparison value
4759         return $posa - $posb;
4760     }
4761
4762     /**
4763      * Sort method called by uksort()
4764      *
4765      * @param int $a Array key (UID)
4766      * @param int $b Array key (UID)
4767      */
4768     function compare_uids($a, $b)
4769     {
4770         // then find each sequence number in my ordered list
4771         $posa = isset($this->uids[$a]) ? intval($this->uids[$a]) : -1;
4772         $posb = isset($this->uids[$b]) ? intval($this->uids[$b]) : -1;
4773
4774         // return the relative position as the comparison value
4775         return $posa - $posb;
4776     }
4777 }