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