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