]> git.donarmstrong.com Git - roundcube.git/blob - program/include/rcube_imap.php
Imported Upstream version 0.5.2+dfsg
[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 4643 2011-04-11 12:24:00Z 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      * @param  boolean            $skip_charset_conv Disables charset conversion
2430      *
2431      * @return string Message/part body if not printed
2432      */
2433     function &get_message_part($uid, $part=1, $o_part=NULL, $print=NULL, $fp=NULL, $skip_charset_conv=false)
2434     {
2435         // get part encoding if not provided
2436         if (!is_object($o_part)) {
2437             $structure_str = $this->conn->fetchStructureString($this->mailbox, $uid, true);
2438             $structure = new rcube_mime_struct();
2439             // error or message not found
2440             if (!$structure->loadStructure($structure_str)) {
2441                 return false;
2442             }
2443
2444             $o_part = new rcube_message_part;
2445             $o_part->ctype_primary = strtolower($structure->getPartType($part));
2446             $o_part->encoding      = strtolower($structure->getPartEncoding($part));
2447             $o_part->charset       = $structure->getPartCharset($part);
2448         }
2449
2450         // TODO: Add caching for message parts
2451
2452         if (!$part) {
2453             $part = 'TEXT';
2454         }
2455
2456         $body = $this->conn->handlePartBody($this->mailbox, $uid, true, $part,
2457             $o_part->encoding, $print, $fp);
2458
2459         if ($fp || $print) {
2460             return true;
2461         }
2462
2463         // convert charset (if text or message part)
2464         if ($body && !$skip_charset_conv &&
2465             preg_match('/^(text|message)$/', $o_part->ctype_primary)
2466         ) {
2467             if (!$o_part->charset || strtoupper($o_part->charset) == 'US-ASCII') {
2468                 $o_part->charset = $this->default_charset;
2469             }
2470             $body = rcube_charset_convert($body, $o_part->charset);
2471         }
2472
2473         return $body;
2474     }
2475
2476
2477     /**
2478      * Fetch message body of a specific message from the server
2479      *
2480      * @param  int    $uid  Message UID
2481      * @return string $part Message/part body
2482      * @see    rcube_imap::get_message_part()
2483      */
2484     function &get_body($uid, $part=1)
2485     {
2486         $headers = $this->get_headers($uid);
2487         return rcube_charset_convert($this->get_message_part($uid, $part, NULL),
2488             $headers->charset ? $headers->charset : $this->default_charset);
2489     }
2490
2491
2492     /**
2493      * Returns the whole message source as string
2494      *
2495      * @param int $uid Message UID
2496      * @return string Message source string
2497      */
2498     function &get_raw_body($uid)
2499     {
2500         return $this->conn->handlePartBody($this->mailbox, $uid, true);
2501     }
2502
2503
2504     /**
2505      * Returns the message headers as string
2506      *
2507      * @param int $uid  Message UID
2508      * @return string Message headers string
2509      */
2510     function &get_raw_headers($uid)
2511     {
2512         return $this->conn->fetchPartHeader($this->mailbox, $uid, true);
2513     }
2514
2515
2516     /**
2517      * Sends the whole message source to stdout
2518      *
2519      * @param int $uid Message UID
2520      */
2521     function print_raw_body($uid)
2522     {
2523         $this->conn->handlePartBody($this->mailbox, $uid, true, NULL, NULL, true);
2524     }
2525
2526
2527     /**
2528      * Set message flag to one or several messages
2529      *
2530      * @param mixed   $uids       Message UIDs as array or comma-separated string, or '*'
2531      * @param string  $flag       Flag to set: SEEN, UNDELETED, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
2532      * @param string  $mbox_name  Folder name
2533      * @param boolean $skip_cache True to skip message cache clean up
2534      * @return boolean  Operation status
2535      */
2536     function set_flag($uids, $flag, $mbox_name=NULL, $skip_cache=false)
2537     {
2538         $mailbox = strlen($mbox_name) ? $this->mod_mailbox($mbox_name) : $this->mailbox;
2539
2540         $flag = strtoupper($flag);
2541         list($uids, $all_mode) = $this->_parse_uids($uids, $mailbox);
2542
2543         if (strpos($flag, 'UN') === 0)
2544             $result = $this->conn->unflag($mailbox, $uids, substr($flag, 2));
2545         else
2546             $result = $this->conn->flag($mailbox, $uids, $flag);
2547
2548         if ($result) {
2549             // reload message headers if cached
2550             if ($this->caching_enabled && !$skip_cache) {
2551                 $cache_key = $mailbox.'.msg';
2552                 if ($all_mode)
2553                     $this->clear_message_cache($cache_key);
2554                 else
2555                     $this->remove_message_cache($cache_key, explode(',', $uids));
2556             }
2557
2558             // clear cached counters
2559             if ($flag == 'SEEN' || $flag == 'UNSEEN') {
2560                 $this->_clear_messagecount($mailbox, 'SEEN');
2561                 $this->_clear_messagecount($mailbox, 'UNSEEN');
2562             }
2563             else if ($flag == 'DELETED') {
2564                 $this->_clear_messagecount($mailbox, 'DELETED');
2565             }
2566         }
2567
2568         return $result;
2569     }
2570
2571
2572     /**
2573      * Remove message flag for one or several messages
2574      *
2575      * @param mixed  $uids      Message UIDs as array or comma-separated string, or '*'
2576      * @param string $flag      Flag to unset: SEEN, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
2577      * @param string $mbox_name Folder name
2578      * @return int   Number of flagged messages, -1 on failure
2579      * @see set_flag
2580      */
2581     function unset_flag($uids, $flag, $mbox_name=NULL)
2582     {
2583         return $this->set_flag($uids, 'UN'.$flag, $mbox_name);
2584     }
2585
2586
2587     /**
2588      * Append a mail message (source) to a specific mailbox
2589      *
2590      * @param string  $mbox_name Target mailbox
2591      * @param string  $message   The message source string or filename
2592      * @param string  $headers   Headers string if $message contains only the body
2593      * @param boolean $is_file   True if $message is a filename
2594      *
2595      * @return boolean True on success, False on error
2596      */
2597     function save_message($mbox_name, &$message, $headers='', $is_file=false)
2598     {
2599         $mailbox = $this->mod_mailbox($mbox_name);
2600
2601         // make sure mailbox exists
2602         if ($this->mailbox_exists($mbox_name)) {
2603             if ($is_file)
2604                 $saved = $this->conn->appendFromFile($mailbox, $message, $headers);
2605             else
2606                 $saved = $this->conn->append($mailbox, $message);
2607         }
2608
2609         if ($saved) {
2610             // increase messagecount of the target mailbox
2611             $this->_set_messagecount($mailbox, 'ALL', 1);
2612         }
2613
2614         return $saved;
2615     }
2616
2617
2618     /**
2619      * Move a message from one mailbox to another
2620      *
2621      * @param mixed  $uids      Message UIDs as array or comma-separated string, or '*'
2622      * @param string $to_mbox   Target mailbox
2623      * @param string $from_mbox Source mailbox
2624      * @return boolean True on success, False on error
2625      */
2626     function move_message($uids, $to_mbox, $from_mbox='')
2627     {
2628         $fbox = $from_mbox;
2629         $tbox = $to_mbox;
2630         $to_mbox = $this->mod_mailbox($to_mbox);
2631         $from_mbox = strlen($from_mbox) ? $this->mod_mailbox($from_mbox) : $this->mailbox;
2632
2633         if ($to_mbox === $from_mbox)
2634             return false;
2635
2636         list($uids, $all_mode) = $this->_parse_uids($uids, $from_mbox);
2637
2638         // exit if no message uids are specified
2639         if (empty($uids))
2640             return false;
2641
2642         // make sure mailbox exists
2643         if ($to_mbox != 'INBOX' && !$this->mailbox_exists($tbox)) {
2644             if (in_array($tbox, $this->default_folders))
2645                 $this->create_mailbox($tbox, true);
2646             else
2647                 return false;
2648         }
2649
2650         // flag messages as read before moving them
2651         $config = rcmail::get_instance()->config;
2652         if ($config->get('read_when_deleted') && $tbox == $config->get('trash_mbox')) {
2653             // don't flush cache (4th argument)
2654             $this->set_flag($uids, 'SEEN', $fbox, true);
2655         }
2656
2657         // move messages
2658         $moved = $this->conn->move($uids, $from_mbox, $to_mbox);
2659
2660         // send expunge command in order to have the moved message
2661         // really deleted from the source mailbox
2662         if ($moved) {
2663             $this->_expunge($from_mbox, false, $uids);
2664             $this->_clear_messagecount($from_mbox);
2665             $this->_clear_messagecount($to_mbox);
2666         }
2667         // moving failed
2668         else if ($config->get('delete_always', false) && $tbox == $config->get('trash_mbox')) {
2669             $moved = $this->delete_message($uids, $fbox);
2670         }
2671
2672         if ($moved) {
2673             // unset threads internal cache
2674             unset($this->icache['threads']);
2675
2676             // remove message ids from search set
2677             if ($this->search_set && $from_mbox == $this->mailbox) {
2678                 // threads are too complicated to just remove messages from set
2679                 if ($this->search_threads || $all_mode)
2680                     $this->refresh_search();
2681                 else {
2682                     $uids = explode(',', $uids);
2683                     foreach ($uids as $uid)
2684                         $a_mids[] = $this->_uid2id($uid, $from_mbox);
2685                     $this->search_set = array_diff($this->search_set, $a_mids);
2686                 }
2687             }
2688
2689             // update cached message headers
2690             $cache_key = $from_mbox.'.msg';
2691             if ($all_mode || ($start_index = $this->get_message_cache_index_min($cache_key, $uids))) {
2692                 // clear cache from the lowest index on
2693                 $this->clear_message_cache($cache_key, $all_mode ? 1 : $start_index);
2694             }
2695         }
2696
2697         return $moved;
2698     }
2699
2700
2701     /**
2702      * Copy a message from one mailbox to another
2703      *
2704      * @param mixed  $uids      Message UIDs as array or comma-separated string, or '*'
2705      * @param string $to_mbox   Target mailbox
2706      * @param string $from_mbox Source mailbox
2707      * @return boolean True on success, False on error
2708      */
2709     function copy_message($uids, $to_mbox, $from_mbox='')
2710     {
2711         $fbox = $from_mbox;
2712         $tbox = $to_mbox;
2713         $to_mbox = $this->mod_mailbox($to_mbox);
2714         $from_mbox = $from_mbox ? $this->mod_mailbox($from_mbox) : $this->mailbox;
2715
2716         list($uids, $all_mode) = $this->_parse_uids($uids, $from_mbox);
2717
2718         // exit if no message uids are specified
2719         if (empty($uids)) {
2720             return false;
2721         }
2722
2723         // make sure mailbox exists
2724         if ($to_mbox != 'INBOX' && !$this->mailbox_exists($tbox)) {
2725             if (in_array($tbox, $this->default_folders))
2726                 $this->create_mailbox($tbox, true);
2727             else
2728                 return false;
2729         }
2730
2731         // copy messages
2732         $copied = $this->conn->copy($uids, $from_mbox, $to_mbox);
2733
2734         if ($copied) {
2735             $this->_clear_messagecount($to_mbox);
2736         }
2737
2738         return $copied;
2739     }
2740
2741
2742     /**
2743      * Mark messages as deleted and expunge mailbox
2744      *
2745      * @param mixed  $uids      Message UIDs as array or comma-separated string, or '*'
2746      * @param string $mbox_name Source mailbox
2747      * @return boolean True on success, False on error
2748      */
2749     function delete_message($uids, $mbox_name='')
2750     {
2751         $mailbox = strlen($mbox_name) ? $this->mod_mailbox($mbox_name) : $this->mailbox;
2752
2753         list($uids, $all_mode) = $this->_parse_uids($uids, $mailbox);
2754
2755         // exit if no message uids are specified
2756         if (empty($uids))
2757             return false;
2758
2759         $deleted = $this->conn->delete($mailbox, $uids);
2760
2761         if ($deleted) {
2762             // send expunge command in order to have the deleted message
2763             // really deleted from the mailbox
2764             $this->_expunge($mailbox, false, $uids);
2765             $this->_clear_messagecount($mailbox);
2766             unset($this->uid_id_map[$mailbox]);
2767
2768             // unset threads internal cache
2769             unset($this->icache['threads']);
2770
2771             // remove message ids from search set
2772             if ($this->search_set && $mailbox == $this->mailbox) {
2773                 // threads are too complicated to just remove messages from set
2774                 if ($this->search_threads || $all_mode)
2775                     $this->refresh_search();
2776                 else {
2777                     $uids = explode(',', $uids);
2778                     foreach ($uids as $uid)
2779                         $a_mids[] = $this->_uid2id($uid, $mailbox);
2780                     $this->search_set = array_diff($this->search_set, $a_mids);
2781                 }
2782             }
2783
2784             // remove deleted messages from cache
2785             $cache_key = $mailbox.'.msg';
2786             if ($all_mode || ($start_index = $this->get_message_cache_index_min($cache_key, $uids))) {
2787                 // clear cache from the lowest index on
2788                 $this->clear_message_cache($cache_key, $all_mode ? 1 : $start_index);
2789             }
2790         }
2791
2792         return $deleted;
2793     }
2794
2795
2796     /**
2797      * Clear all messages in a specific mailbox
2798      *
2799      * @param string $mbox_name Mailbox name
2800      * @return int Above 0 on success
2801      */
2802     function clear_mailbox($mbox_name=NULL)
2803     {
2804         $mailbox = strlen($mbox_name) ? $this->mod_mailbox($mbox_name) : $this->mailbox;
2805
2806         // SELECT will set messages count for clearFolder()
2807         if ($this->conn->select($mailbox)) {
2808             $cleared = $this->conn->clearFolder($mailbox);
2809         }
2810
2811         // make sure the message count cache is cleared as well
2812         if ($cleared) {
2813             $this->clear_message_cache($mailbox.'.msg');
2814             $a_mailbox_cache = $this->get_cache('messagecount');
2815             unset($a_mailbox_cache[$mailbox]);
2816             $this->update_cache('messagecount', $a_mailbox_cache);
2817         }
2818
2819         return $cleared;
2820     }
2821
2822
2823     /**
2824      * Send IMAP expunge command and clear cache
2825      *
2826      * @param string  $mbox_name   Mailbox name
2827      * @param boolean $clear_cache False if cache should not be cleared
2828      * @return boolean True on success
2829      */
2830     function expunge($mbox_name='', $clear_cache=true)
2831     {
2832         $mailbox = strlen($mbox_name) ? $this->mod_mailbox($mbox_name) : $this->mailbox;
2833         return $this->_expunge($mailbox, $clear_cache);
2834     }
2835
2836
2837     /**
2838      * Send IMAP expunge command and clear cache
2839      *
2840      * @param string  $mailbox     Mailbox name
2841      * @param boolean $clear_cache False if cache should not be cleared
2842      * @param mixed   $uids        Message UIDs as array or comma-separated string, or '*'
2843      * @return boolean True on success
2844      * @access private
2845      * @see rcube_imap::expunge()
2846      */
2847     private function _expunge($mailbox, $clear_cache=true, $uids=NULL)
2848     {
2849         if ($uids && $this->get_capability('UIDPLUS'))
2850             $a_uids = is_array($uids) ? join(',', $uids) : $uids;
2851         else
2852             $a_uids = NULL;
2853
2854         // force mailbox selection and check if mailbox is writeable
2855         // to prevent a situation when CLOSE is executed on closed
2856         // or EXPUNGE on read-only mailbox
2857         $result = $this->conn->select($mailbox);
2858         if (!$result) {
2859             return false;
2860         }
2861         if (!$this->conn->data['READ-WRITE']) {
2862             $this->conn->setError(rcube_imap_generic::ERROR_READONLY, "Mailbox is read-only");
2863             return false;
2864         }
2865
2866         // CLOSE(+SELECT) should be faster than EXPUNGE
2867         if (empty($a_uids) || $a_uids == '1:*')
2868             $result = $this->conn->close();
2869         else
2870             $result = $this->conn->expunge($mailbox, $a_uids);
2871
2872         if ($result && $clear_cache) {
2873             $this->clear_message_cache($mailbox.'.msg');
2874             $this->_clear_messagecount($mailbox);
2875         }
2876
2877         return $result;
2878     }
2879
2880
2881     /**
2882      * Parse message UIDs input
2883      *
2884      * @param mixed  $uids    UIDs array or comma-separated list or '*' or '1:*'
2885      * @param string $mailbox Mailbox name
2886      * @return array Two elements array with UIDs converted to list and ALL flag
2887      * @access private
2888      */
2889     private function _parse_uids($uids, $mailbox)
2890     {
2891         if ($uids === '*' || $uids === '1:*') {
2892             if (empty($this->search_set)) {
2893                 $uids = '1:*';
2894                 $all = true;
2895             }
2896             // get UIDs from current search set
2897             // @TODO: skip fetchUIDs() and work with IDs instead of UIDs (?)
2898             else {
2899                 if ($this->search_threads)
2900                     $uids = $this->conn->fetchUIDs($mailbox, array_keys($this->search_set['depth']));
2901                 else
2902                     $uids = $this->conn->fetchUIDs($mailbox, $this->search_set);
2903
2904                 // save ID-to-UID mapping in local cache
2905                 if (is_array($uids))
2906                     foreach ($uids as $id => $uid)
2907                         $this->uid_id_map[$mailbox][$uid] = $id;
2908
2909                 $uids = join(',', $uids);
2910             }
2911         }
2912         else {
2913             if (is_array($uids))
2914                 $uids = join(',', $uids);
2915
2916             if (preg_match('/[^0-9,]/', $uids))
2917                 $uids = '';
2918         }
2919
2920         return array($uids, (bool) $all);
2921     }
2922
2923
2924     /**
2925      * Translate UID to message ID
2926      *
2927      * @param int    $uid       Message UID
2928      * @param string $mbox_name Mailbox name
2929      * @return int   Message ID
2930      */
2931     function get_id($uid, $mbox_name=NULL)
2932     {
2933         $mailbox = strlen($mbox_name) ? $this->mod_mailbox($mbox_name) : $this->mailbox;
2934         return $this->_uid2id($uid, $mailbox);
2935     }
2936
2937
2938     /**
2939      * Translate message number to UID
2940      *
2941      * @param int    $id        Message ID
2942      * @param string $mbox_name Mailbox name
2943      * @return int   Message UID
2944      */
2945     function get_uid($id, $mbox_name=NULL)
2946     {
2947         $mailbox = strlen($mbox_name) ? $this->mod_mailbox($mbox_name) : $this->mailbox;
2948         return $this->_id2uid($id, $mailbox);
2949     }
2950
2951
2952
2953     /* --------------------------------
2954      *        folder managment
2955      * --------------------------------*/
2956
2957     /**
2958      * Public method for listing subscribed folders
2959      *
2960      * Converts mailbox name with root dir first
2961      *
2962      * @param   string  $root   Optional root folder
2963      * @param   string  $filter Optional filter for mailbox listing
2964      * @return  array   List of mailboxes/folders
2965      * @access  public
2966      */
2967     function list_mailboxes($root='', $filter='*')
2968     {
2969         $a_out = array();
2970         $a_mboxes = $this->_list_mailboxes($root, $filter);
2971
2972         foreach ($a_mboxes as $idx => $mbox_row) {
2973             if (strlen($name = $this->mod_mailbox($mbox_row, 'out')))
2974                 $a_out[] = $name;
2975             unset($a_mboxes[$idx]);
2976         }
2977
2978         // INBOX should always be available
2979         if (!in_array('INBOX', $a_out))
2980             array_unshift($a_out, 'INBOX');
2981
2982         // sort mailboxes
2983         $a_out = $this->_sort_mailbox_list($a_out);
2984
2985         return $a_out;
2986     }
2987
2988
2989     /**
2990      * Private method for mailbox listing
2991      *
2992      * @param   string  $root   Optional root folder
2993      * @param   string  $filter Optional filter for mailbox listing
2994      * @return  array   List of mailboxes/folders
2995      * @see     rcube_imap::list_mailboxes()
2996      * @access  private
2997      */
2998     private function _list_mailboxes($root='', $filter='*')
2999     {
3000         // get cached folder list
3001         $a_mboxes = $this->get_cache('mailboxes');
3002         if (is_array($a_mboxes))
3003             return $a_mboxes;
3004
3005         $a_defaults = $a_out = array();
3006
3007         // Give plugins a chance to provide a list of mailboxes
3008         $data = rcmail::get_instance()->plugins->exec_hook('mailboxes_list',
3009             array('root' => $root, 'filter' => $filter, 'mode' => 'LSUB'));
3010
3011         if (isset($data['folders'])) {
3012             $a_folders = $data['folders'];
3013         }
3014         else {
3015             // Server supports LIST-EXTENDED, we can use selection options
3016             $config = rcmail::get_instance()->config;
3017             // #1486225: Some dovecot versions returns wrong result using LIST-EXTENDED
3018             if (!$config->get('imap_force_lsub') && $this->get_capability('LIST-EXTENDED')) {
3019                 // This will also set mailbox options, LSUB doesn't do that
3020                 $a_folders = $this->conn->listMailboxes($this->mod_mailbox($root), $filter,
3021                     NULL, array('SUBSCRIBED'));
3022
3023                 // remove non-existent folders
3024                 if (is_array($a_folders)) {
3025                     foreach ($a_folders as $idx => $folder) {
3026                         if ($this->conn->data['LIST'] && ($opts = $this->conn->data['LIST'][$folder])
3027                             && in_array('\\NonExistent', $opts)
3028                         ) {
3029                             unset($a_folders[$idx]);
3030                         } 
3031                     }
3032                 }
3033             }
3034             // retrieve list of folders from IMAP server using LSUB
3035             else {
3036                 $a_folders = $this->conn->listSubscribed($this->mod_mailbox($root), $filter);
3037             }
3038         }
3039
3040         if (!is_array($a_folders) || !sizeof($a_folders))
3041             $a_folders = array();
3042
3043         // write mailboxlist to cache
3044         $this->update_cache('mailboxes', $a_folders);
3045
3046         return $a_folders;
3047     }
3048
3049
3050     /**
3051      * Get a list of all folders available on the IMAP server
3052      *
3053      * @param string $root   IMAP root dir
3054      * @param string $filter Optional filter for mailbox listing
3055      * @return array Indexed array with folder names
3056      */
3057     function list_unsubscribed($root='', $filter='*')
3058     {
3059         // Give plugins a chance to provide a list of mailboxes
3060         $data = rcmail::get_instance()->plugins->exec_hook('mailboxes_list',
3061             array('root' => $root, 'filter' => $filter, 'mode' => 'LIST'));
3062
3063         if (isset($data['folders'])) {
3064             $a_mboxes = $data['folders'];
3065         }
3066         else {
3067             // retrieve list of folders from IMAP server
3068             $a_mboxes = $this->conn->listMailboxes($this->mod_mailbox($root), $filter);
3069         }
3070
3071         $a_folders = array();
3072         if (!is_array($a_mboxes))
3073             $a_mboxes = array();
3074
3075         // modify names with root dir
3076         foreach ($a_mboxes as $idx => $mbox_name) {
3077             if (strlen($name = $this->mod_mailbox($mbox_name, 'out')))
3078                 $a_folders[] = $name;
3079             unset($a_mboxes[$idx]);
3080         }
3081
3082         // INBOX should always be available
3083         if (!in_array('INBOX', $a_folders))
3084             array_unshift($a_folders, 'INBOX');
3085
3086         // filter folders and sort them
3087         $a_folders = $this->_sort_mailbox_list($a_folders);
3088         return $a_folders;
3089     }
3090
3091
3092     /**
3093      * Get mailbox quota information
3094      * added by Nuny
3095      *
3096      * @return mixed Quota info or False if not supported
3097      */
3098     function get_quota()
3099     {
3100         if ($this->get_capability('QUOTA'))
3101             return $this->conn->getQuota();
3102
3103         return false;
3104     }
3105
3106
3107     /**
3108      * Get mailbox size (size of all messages in a mailbox)
3109      *
3110      * @param string $name Mailbox name
3111      * @return int Mailbox size in bytes, False on error
3112      */
3113     function get_mailbox_size($name)
3114     {
3115         $name = $this->mod_mailbox($name);
3116
3117         // @TODO: could we try to use QUOTA here?
3118         $result = $this->conn->fetchHeaderIndex($name, '1:*', 'SIZE', false);
3119
3120         if (is_array($result))
3121             $result = array_sum($result);
3122
3123         return $result;
3124     }
3125
3126
3127     /**
3128      * Subscribe to a specific mailbox(es)
3129      *
3130      * @param array $a_mboxes Mailbox name(s)
3131      * @return boolean True on success
3132      */
3133     function subscribe($a_mboxes)
3134     {
3135         if (!is_array($a_mboxes))
3136             $a_mboxes = array($a_mboxes);
3137
3138         // let this common function do the main work
3139         return $this->_change_subscription($a_mboxes, 'subscribe');
3140     }
3141
3142
3143     /**
3144      * Unsubscribe mailboxes
3145      *
3146      * @param array $a_mboxes Mailbox name(s)
3147      * @return boolean True on success
3148      */
3149     function unsubscribe($a_mboxes)
3150     {
3151         if (!is_array($a_mboxes))
3152             $a_mboxes = array($a_mboxes);
3153
3154         // let this common function do the main work
3155         return $this->_change_subscription($a_mboxes, 'unsubscribe');
3156     }
3157
3158
3159     /**
3160      * Create a new mailbox on the server and register it in local cache
3161      *
3162      * @param string  $name      New mailbox name
3163      * @param boolean $subscribe True if the new mailbox should be subscribed
3164      * @param boolean True on success
3165      */
3166     function create_mailbox($name, $subscribe=false)
3167     {
3168         $result   = false;
3169         $abs_name = $this->mod_mailbox($name);
3170         $result   = $this->conn->createFolder($abs_name);
3171
3172         // try to subscribe it
3173         if ($result && $subscribe)
3174             $this->subscribe($name);
3175
3176         return $result;
3177     }
3178
3179
3180     /**
3181      * Set a new name to an existing mailbox
3182      *
3183      * @param string $mbox_name Mailbox to rename
3184      * @param string $new_name  New mailbox name
3185      *
3186      * @return boolean True on success
3187      */
3188     function rename_mailbox($mbox_name, $new_name)
3189     {
3190         $result = false;
3191
3192         // make absolute path
3193         $mailbox  = $this->mod_mailbox($mbox_name);
3194         $abs_name = $this->mod_mailbox($new_name);
3195         $delm     = $this->get_hierarchy_delimiter();
3196
3197         // get list of subscribed folders
3198         if ((strpos($mailbox, '%') === false) && (strpos($mailbox, '*') === false)) {
3199             $a_subscribed = $this->_list_mailboxes('', $mbox_name . $delm . '*');
3200             $subscribed   = $this->mailbox_exists($mbox_name, true);
3201         }
3202         else {
3203             $a_subscribed = $this->_list_mailboxes();
3204             $subscribed   = in_array($mailbox, $a_subscribed);
3205         }
3206
3207         if (strlen($abs_name))
3208             $result = $this->conn->renameFolder($mailbox, $abs_name);
3209
3210         if ($result) {
3211             // unsubscribe the old folder, subscribe the new one
3212             if ($subscribed) {
3213                 $this->conn->unsubscribe($mailbox);
3214                 $this->conn->subscribe($abs_name);
3215             }
3216
3217             // check if mailbox children are subscribed
3218             foreach ($a_subscribed as $c_subscribed) {
3219                 if (preg_match('/^'.preg_quote($mailbox.$delm, '/').'/', $c_subscribed)) {
3220                     $this->conn->unsubscribe($c_subscribed);
3221                     $this->conn->subscribe(preg_replace('/^'.preg_quote($mailbox, '/').'/',
3222                         $abs_name, $c_subscribed));
3223                 }
3224             }
3225
3226             // clear cache
3227             $this->clear_message_cache($mailbox.'.msg');
3228             $this->clear_cache('mailboxes');
3229         }
3230
3231         return $result;
3232     }
3233
3234
3235     /**
3236      * Remove mailbox from server
3237      *
3238      * @param string $mbox_name Mailbox name
3239      *
3240      * @return boolean True on success
3241      */
3242     function delete_mailbox($mbox_name)
3243     {
3244         $result  = false;
3245         $mailbox = $this->mod_mailbox($mbox_name);
3246         $delm    = $this->get_hierarchy_delimiter();
3247
3248         // get list of folders
3249         if ((strpos($mailbox, '%') === false) && (strpos($mailbox, '*') === false))
3250             $sub_mboxes = $this->list_unsubscribed('', $mailbox . $delm . '*');
3251         else
3252             $sub_mboxes = $this->list_unsubscribed();
3253
3254         // send delete command to server
3255         $result = $this->conn->deleteFolder($mailbox);
3256
3257         if ($result) {
3258             // unsubscribe mailbox
3259             $this->conn->unsubscribe($mailbox);
3260
3261             foreach ($sub_mboxes as $c_mbox) {
3262                 if (preg_match('/^'.preg_quote($mailbox.$delm, '/').'/', $c_mbox)) {
3263                     $this->conn->unsubscribe($c_mbox);
3264                     if ($this->conn->deleteFolder($c_mbox)) {
3265                             $this->clear_message_cache($c_mbox.'.msg');
3266                     }
3267                 }
3268             }
3269
3270             // clear mailbox-related cache
3271             $this->clear_message_cache($mailbox.'.msg');
3272             $this->clear_cache('mailboxes');
3273         }
3274
3275         return $result;
3276     }
3277
3278
3279     /**
3280      * Create all folders specified as default
3281      */
3282     function create_default_folders()
3283     {
3284         // create default folders if they do not exist
3285         foreach ($this->default_folders as $folder) {
3286             if (!$this->mailbox_exists($folder))
3287                 $this->create_mailbox($folder, true);
3288             else if (!$this->mailbox_exists($folder, true))
3289                 $this->subscribe($folder);
3290         }
3291     }
3292
3293
3294     /**
3295      * Checks if folder exists and is subscribed
3296      *
3297      * @param string   $mbox_name    Folder name
3298      * @param boolean  $subscription Enable subscription checking
3299      * @return boolean TRUE or FALSE
3300      */
3301     function mailbox_exists($mbox_name, $subscription=false)
3302     {
3303         if ($mbox_name == 'INBOX')
3304             return true;
3305
3306         $key  = $subscription ? 'subscribed' : 'existing';
3307         $mbox = $this->mod_mailbox($mbox_name);
3308
3309         if (is_array($this->icache[$key]) && in_array($mbox, $this->icache[$key]))
3310             return true;
3311
3312         if ($subscription) {
3313             $a_folders = $this->conn->listSubscribed('', $mbox);
3314         }
3315         else {
3316             $a_folders = $this->conn->listMailboxes('', $mbox);
3317         }
3318
3319         if (is_array($a_folders) && in_array($mbox, $a_folders)) {
3320             $this->icache[$key][] = $mbox;
3321             return true;
3322         }
3323
3324         return false;
3325     }
3326
3327
3328     /**
3329      * Modify folder name for input/output according to root dir and namespace
3330      *
3331      * @param string  $mbox_name Folder name
3332      * @param string  $mode      Mode
3333      * @return string Folder name
3334      */
3335     function mod_mailbox($mbox_name, $mode='in')
3336     {
3337         if (!strlen($mbox_name))
3338             return '';
3339
3340         if ($mode == 'in') {
3341             // If folder contains namespace prefix, don't modify it
3342             if (is_array($this->namespace['shared'])) {
3343                 foreach ($this->namespace['shared'] as $ns) {
3344                     if ($ns[0] && strpos($mbox_name, $ns[0]) === 0) {
3345                         return $mbox_name;
3346                     }
3347                 }
3348             }
3349             if (is_array($this->namespace['other'])) {
3350                 foreach ($this->namespace['other'] as $ns) {
3351                     if ($ns[0] && strpos($mbox_name, $ns[0]) === 0) {
3352                         return $mbox_name;
3353                     }
3354                 }
3355             }
3356             if (is_array($this->namespace['personal'])) {
3357                 foreach ($this->namespace['personal'] as $ns) {
3358                     if ($ns[0] && strpos($mbox_name, $ns[0]) === 0) {
3359                         return $mbox_name;
3360                     }
3361                 }
3362                 // Add prefix if first personal namespace is non-empty
3363                 if ($mbox_name != 'INBOX' && $this->namespace['personal'][0][0]) {
3364                     return $this->namespace['personal'][0][0].$mbox_name;
3365                 }
3366             }
3367         }
3368         else {
3369             // Remove prefix if folder is from first ("non-empty") personal namespace
3370             if (is_array($this->namespace['personal'])) {
3371                 if ($prefix = $this->namespace['personal'][0][0]) {
3372                     if (strpos($mbox_name, $prefix) === 0) {
3373                         return substr($mbox_name, strlen($prefix));
3374                     }
3375                 }
3376             }
3377         }
3378
3379         return $mbox_name;
3380     }
3381
3382
3383     /**
3384      * Gets folder options from LIST response, e.g. \Noselect, \Noinferiors
3385      *
3386      * @param string $mbox_name Folder name
3387      * @param bool   $force     Set to True if options should be refreshed
3388      *                          Options are available after LIST command only
3389      *
3390      * @return array Options list
3391      */
3392     function mailbox_options($mbox_name, $force=false)
3393     {
3394         $mbox = $this->mod_mailbox($mbox_name);
3395
3396         if ($mbox == 'INBOX') {
3397             return array();
3398         }
3399
3400         if (!is_array($this->conn->data['LIST']) || !is_array($this->conn->data['LIST'][$mbox])) {
3401             if ($force) {
3402                 $this->conn->listMailboxes('', $mbox_name);
3403             }
3404             else {
3405                 return array();
3406             }
3407         }
3408
3409         $opts = $this->conn->data['LIST'][$mbox];
3410
3411         return is_array($opts) ? $opts : array();
3412     }
3413
3414
3415     /**
3416      * Get message header names for rcube_imap_generic::fetchHeader(s)
3417      *
3418      * @return string Space-separated list of header names
3419      */
3420     private function get_fetch_headers()
3421     {
3422         $headers = explode(' ', $this->fetch_add_headers);
3423         $headers = array_map('strtoupper', $headers);
3424
3425         if ($this->caching_enabled || $this->get_all_headers)
3426             $headers = array_merge($headers, $this->all_headers);
3427
3428         return implode(' ', array_unique($headers));
3429     }
3430
3431
3432     /* -----------------------------------------
3433      *   ACL and METADATA/ANNOTATEMORE methods
3434      * ----------------------------------------*/
3435
3436     /**
3437      * Changes the ACL on the specified mailbox (SETACL)
3438      *
3439      * @param string $mailbox Mailbox name
3440      * @param string $user    User name
3441      * @param string $acl     ACL string
3442      *
3443      * @return boolean True on success, False on failure
3444      *
3445      * @access public
3446      * @since 0.5-beta
3447      */
3448     function set_acl($mailbox, $user, $acl)
3449     {
3450         $mailbox = $this->mod_mailbox($mailbox);
3451
3452         if ($this->get_capability('ACL'))
3453             return $this->conn->setACL($mailbox, $user, $acl);
3454
3455         return false;
3456     }
3457
3458
3459     /**
3460      * Removes any <identifier,rights> pair for the
3461      * specified user from the ACL for the specified
3462      * mailbox (DELETEACL)
3463      *
3464      * @param string $mailbox Mailbox name
3465      * @param string $user    User name
3466      *
3467      * @return boolean True on success, False on failure
3468      *
3469      * @access public
3470      * @since 0.5-beta
3471      */
3472     function delete_acl($mailbox, $user)
3473     {
3474         $mailbox = $this->mod_mailbox($mailbox);
3475
3476         if ($this->get_capability('ACL'))
3477             return $this->conn->deleteACL($mailbox, $user);
3478
3479         return false;
3480     }
3481
3482
3483     /**
3484      * Returns the access control list for mailbox (GETACL)
3485      *
3486      * @param string $mailbox Mailbox name
3487      *
3488      * @return array User-rights array on success, NULL on error
3489      * @access public
3490      * @since 0.5-beta
3491      */
3492     function get_acl($mailbox)
3493     {
3494         $mailbox = $this->mod_mailbox($mailbox);
3495
3496         if ($this->get_capability('ACL'))
3497             return $this->conn->getACL($mailbox);
3498
3499         return NULL;
3500     }
3501
3502
3503     /**
3504      * Returns information about what rights can be granted to the
3505      * user (identifier) in the ACL for the mailbox (LISTRIGHTS)
3506      *
3507      * @param string $mailbox Mailbox name
3508      * @param string $user    User name
3509      *
3510      * @return array List of user rights
3511      * @access public
3512      * @since 0.5-beta
3513      */
3514     function list_rights($mailbox, $user)
3515     {
3516         $mailbox = $this->mod_mailbox($mailbox);
3517
3518         if ($this->get_capability('ACL'))
3519             return $this->conn->listRights($mailbox, $user);
3520
3521         return NULL;
3522     }
3523
3524
3525     /**
3526      * Returns the set of rights that the current user has to
3527      * mailbox (MYRIGHTS)
3528      *
3529      * @param string $mailbox Mailbox name
3530      *
3531      * @return array MYRIGHTS response on success, NULL on error
3532      * @access public
3533      * @since 0.5-beta
3534      */
3535     function my_rights($mailbox)
3536     {
3537         $mailbox = $this->mod_mailbox($mailbox);
3538
3539         if ($this->get_capability('ACL'))
3540             return $this->conn->myRights($mailbox);
3541
3542         return NULL;
3543     }
3544
3545
3546     /**
3547      * Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
3548      *
3549      * @param string $mailbox Mailbox name (empty for server metadata)
3550      * @param array  $entries Entry-value array (use NULL value as NIL)
3551      *
3552      * @return boolean True on success, False on failure
3553      * @access public
3554      * @since 0.5-beta
3555      */
3556     function set_metadata($mailbox, $entries)
3557     {
3558         if ($mailbox)
3559             $mailbox = $this->mod_mailbox($mailbox);
3560
3561         if ($this->get_capability('METADATA') ||
3562             (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
3563         ) {
3564             return $this->conn->setMetadata($mailbox, $entries);
3565         }
3566         else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
3567             foreach ($entries as $entry => $value) {
3568                 list($ent, $attr) = $this->md2annotate($entry);
3569                 $entries[$entry] = array($ent, $attr, $value);
3570             }
3571             return $this->conn->setAnnotation($mailbox, $entries);
3572         }
3573
3574         return false;
3575     }
3576
3577
3578     /**
3579      * Unsets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
3580      *
3581      * @param string $mailbox Mailbox name (empty for server metadata)
3582      * @param array  $entries Entry names array
3583      *
3584      * @return boolean True on success, False on failure
3585      *
3586      * @access public
3587      * @since 0.5-beta
3588      */
3589     function delete_metadata($mailbox, $entries)
3590     {
3591         if ($mailbox)
3592             $mailbox = $this->mod_mailbox($mailbox);
3593
3594         if ($this->get_capability('METADATA') || 
3595             (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
3596         ) {
3597             return $this->conn->deleteMetadata($mailbox, $entries);
3598         }
3599         else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
3600             foreach ($entries as $idx => $entry) {
3601                 list($ent, $attr) = $this->md2annotate($entry);
3602                 $entries[$idx] = array($ent, $attr, NULL);
3603             }
3604             return $this->conn->setAnnotation($mailbox, $entries);
3605         }
3606
3607         return false;
3608     }
3609
3610
3611     /**
3612      * Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION)
3613      *
3614      * @param string $mailbox Mailbox name (empty for server metadata)
3615      * @param array  $entries Entries
3616      * @param array  $options Command options (with MAXSIZE and DEPTH keys)
3617      *
3618      * @return array Metadata entry-value hash array on success, NULL on error
3619      *
3620      * @access public
3621      * @since 0.5-beta
3622      */
3623     function get_metadata($mailbox, $entries, $options=array())
3624     {
3625         if ($mailbox)
3626             $mailbox = $this->mod_mailbox($mailbox);
3627
3628         if ($this->get_capability('METADATA') || 
3629             !strlen(($mailbox) && $this->get_capability('METADATA-SERVER'))
3630         ) {
3631             return $this->conn->getMetadata($mailbox, $entries, $options);
3632         }
3633         else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
3634             $queries = array();
3635             $res     = array();
3636
3637             // Convert entry names
3638             foreach ($entries as $entry) {
3639                 list($ent, $attr) = $this->md2annotate($entry);
3640                 $queries[$attr][] = $ent;
3641             }
3642
3643             // @TODO: Honor MAXSIZE and DEPTH options
3644             foreach ($queries as $attrib => $entry)
3645                 if ($result = $this->conn->getAnnotation($mailbox, $entry, $attrib))
3646                     $res = array_merge($res, $result);
3647
3648             return $res;
3649         }
3650
3651         return NULL;
3652     }
3653
3654
3655     /**
3656      * Converts the METADATA extension entry name into the correct
3657      * entry-attrib names for older ANNOTATEMORE version.
3658      *
3659      * @param string Entry name
3660      *
3661      * @return array Entry-attribute list, NULL if not supported (?)
3662      */
3663     private function md2annotate($name)
3664     {
3665         if (substr($entry, 0, 7) == '/shared') {
3666             return array(substr($entry, 7), 'value.shared');
3667         }
3668         else if (substr($entry, 0, 8) == '/private') {
3669             return array(substr($entry, 8), 'value.priv');
3670         }
3671
3672         // @TODO: log error
3673         return NULL;
3674     }
3675
3676
3677     /* --------------------------------
3678      *   internal caching methods
3679      * --------------------------------*/
3680
3681     /**
3682      * Enable or disable caching
3683      *
3684      * @param boolean $set Flag
3685      * @access public
3686      */
3687     function set_caching($set)
3688     {
3689         if ($set && is_object($this->db))
3690             $this->caching_enabled = true;
3691         else
3692             $this->caching_enabled = false;
3693     }
3694
3695
3696     /**
3697      * Returns cached value
3698      *
3699      * @param string $key Cache key
3700      * @return mixed
3701      * @access public
3702      */
3703     function get_cache($key)
3704     {
3705         // read cache (if it was not read before)
3706         if (!count($this->cache) && $this->caching_enabled) {
3707             return $this->_read_cache_record($key);
3708         }
3709
3710         return $this->cache[$key];
3711     }
3712
3713
3714     /**
3715      * Update cache
3716      *
3717      * @param string $key  Cache key
3718      * @param mixed  $data Data
3719      * @access private
3720      */
3721     private function update_cache($key, $data)
3722     {
3723         $this->cache[$key] = $data;
3724         $this->cache_changed = true;
3725         $this->cache_changes[$key] = true;
3726     }
3727
3728
3729     /**
3730      * Writes the cache
3731      *
3732      * @access private
3733      */
3734     private function write_cache()
3735     {
3736         if ($this->caching_enabled && $this->cache_changed) {
3737             foreach ($this->cache as $key => $data) {
3738                 if ($this->cache_changes[$key])
3739                     $this->_write_cache_record($key, serialize($data));
3740             }
3741         }
3742     }
3743
3744
3745     /**
3746      * Clears the cache.
3747      *
3748      * @param string $key Cache key
3749      * @access public
3750      */
3751     function clear_cache($key=NULL)
3752     {
3753         if (!$this->caching_enabled)
3754             return;
3755
3756         if ($key===NULL) {
3757             foreach ($this->cache as $key => $data)
3758                 $this->_clear_cache_record($key);
3759
3760             $this->cache = array();
3761             $this->cache_changed = false;
3762             $this->cache_changes = array();
3763         }
3764         else {
3765             $this->_clear_cache_record($key);
3766             $this->cache_changes[$key] = false;
3767             unset($this->cache[$key]);
3768         }
3769     }
3770
3771
3772     /**
3773      * Returns cached entry
3774      *
3775      * @param string $key Cache key
3776      * @return mixed Cached value
3777      * @access private
3778      */
3779     private function _read_cache_record($key)
3780     {
3781         if ($this->db) {
3782             // get cached data from DB
3783             $sql_result = $this->db->query(
3784                 "SELECT cache_id, data, cache_key ".
3785                 "FROM ".get_table_name('cache').
3786                 " WHERE user_id=? ".
3787                     "AND cache_key LIKE 'IMAP.%'",
3788                 $_SESSION['user_id']);
3789
3790             while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
3791                     $sql_key = preg_replace('/^IMAP\./', '', $sql_arr['cache_key']);
3792                 $this->cache_keys[$sql_key] = $sql_arr['cache_id'];
3793                     if (!isset($this->cache[$sql_key]))
3794                         $this->cache[$sql_key] = $sql_arr['data'] ? unserialize($sql_arr['data']) : false;
3795             }
3796         }
3797
3798         return $this->cache[$key];
3799     }
3800
3801
3802     /**
3803      * Writes single cache record
3804      *
3805      * @param string $key  Cache key
3806      * @param mxied  $data Cache value
3807      * @access private
3808      */
3809     private function _write_cache_record($key, $data)
3810     {
3811         if (!$this->db)
3812             return false;
3813
3814         // update existing cache record
3815         if ($this->cache_keys[$key]) {
3816             $this->db->query(
3817                 "UPDATE ".get_table_name('cache').
3818                 " SET created=". $this->db->now().", data=? ".
3819                 "WHERE user_id=? ".
3820                 "AND cache_key=?",
3821                 $data,
3822                 $_SESSION['user_id'],
3823                 'IMAP.'.$key);
3824         }
3825         // add new cache record
3826         else {
3827             $this->db->query(
3828                 "INSERT INTO ".get_table_name('cache').
3829                 " (created, user_id, cache_key, data) ".
3830                 "VALUES (".$this->db->now().", ?, ?, ?)",
3831                 $_SESSION['user_id'],
3832                 'IMAP.'.$key,
3833                 $data);
3834
3835             // get cache entry ID for this key
3836             $sql_result = $this->db->query(
3837                 "SELECT cache_id ".
3838                 "FROM ".get_table_name('cache').
3839                 " WHERE user_id=? ".
3840                 "AND cache_key=?",
3841                 $_SESSION['user_id'],
3842                 'IMAP.'.$key);
3843
3844             if ($sql_arr = $this->db->fetch_assoc($sql_result))
3845                 $this->cache_keys[$key] = $sql_arr['cache_id'];
3846         }
3847     }
3848
3849
3850     /**
3851      * Clears cache for single record
3852      *
3853      * @param string $ket Cache key
3854      * @access private
3855      */
3856     private function _clear_cache_record($key)
3857     {
3858         $this->db->query(
3859             "DELETE FROM ".get_table_name('cache').
3860             " WHERE user_id=? ".
3861             "AND cache_key=?",
3862             $_SESSION['user_id'],
3863             'IMAP.'.$key);
3864
3865         unset($this->cache_keys[$key]);
3866     }
3867
3868
3869
3870     /* --------------------------------
3871      *   message caching methods
3872      * --------------------------------*/
3873
3874     /**
3875      * Checks if the cache is up-to-date
3876      *
3877      * @param string $mailbox   Mailbox name
3878      * @param string $cache_key Internal cache key
3879      * @return int   Cache status: -3 = off, -2 = incomplete, -1 = dirty, 1 = OK
3880      */
3881     private function check_cache_status($mailbox, $cache_key)
3882     {
3883         if (!$this->caching_enabled)
3884             return -3;
3885
3886         $cache_index = $this->get_message_cache_index($cache_key);
3887         $msg_count = $this->_messagecount($mailbox);
3888         $cache_count = count($cache_index);
3889
3890         // empty mailbox
3891         if (!$msg_count) {
3892             return $cache_count ? -2 : 1;
3893         }
3894
3895         if ($cache_count == $msg_count) {
3896             if ($this->skip_deleted) {
3897                 if (!empty($this->icache['all_undeleted_idx'])) {
3898                     $uids = rcube_imap_generic::uncompressMessageSet($this->icache['all_undeleted_idx']);
3899                     $uids = array_flip($uids);
3900                     foreach ($cache_index as $uid) {
3901                         unset($uids[$uid]);
3902                     }
3903                 }
3904                 else {
3905                     // get all undeleted messages excluding cached UIDs
3906                     $uids = $this->search_once($mailbox, 'ALL UNDELETED NOT UID '.
3907                         rcube_imap_generic::compressMessageSet($cache_index));
3908                 }
3909                 if (empty($uids)) {
3910                     return 1;
3911                 }
3912             } else {
3913                 // get UID of the message with highest index
3914                 $uid = $this->_id2uid($msg_count, $mailbox);
3915                 $cache_uid = array_pop($cache_index);
3916
3917                 // uids of highest message matches -> cache seems OK
3918                 if ($cache_uid == $uid) {
3919                     return 1;
3920                 }
3921             }
3922             // cache is dirty
3923             return -1;
3924         }
3925
3926         // if cache count differs less than 10% report as dirty
3927         return (abs($msg_count - $cache_count) < $msg_count/10) ? -1 : -2;
3928     }
3929
3930
3931     /**
3932      * @param string $key Cache key
3933      * @param string $from
3934      * @param string $to
3935      * @param string $sort_field
3936      * @param string $sort_order
3937      * @access private
3938      */
3939     private function get_message_cache($key, $from, $to, $sort_field, $sort_order)
3940     {
3941         if (!$this->caching_enabled)
3942             return NULL;
3943
3944         // use idx sort as default sorting
3945         if (!$sort_field || !in_array($sort_field, $this->db_header_fields)) {
3946             $sort_field = 'idx';
3947         }
3948
3949         $result = array();
3950
3951         $sql_result = $this->db->limitquery(
3952                 "SELECT idx, uid, headers".
3953                 " FROM ".get_table_name('messages').
3954                 " WHERE user_id=?".
3955                 " AND cache_key=?".
3956                 " ORDER BY ".$this->db->quoteIdentifier($sort_field)." ".strtoupper($sort_order),
3957                 $from,
3958                 $to - $from,
3959                 $_SESSION['user_id'],
3960                 $key);
3961
3962         while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
3963             $uid = intval($sql_arr['uid']);
3964             $result[$uid] = $this->db->decode(unserialize($sql_arr['headers']));
3965
3966             // featch headers if unserialize failed
3967             if (empty($result[$uid]))
3968                 $result[$uid] = $this->conn->fetchHeader(
3969                     preg_replace('/.msg$/', '', $key), $uid, true, false, $this->get_fetch_headers());
3970         }
3971
3972         return $result;
3973     }
3974
3975
3976     /**
3977      * @param string $key Cache key
3978      * @param int    $uid Message UID
3979      * @return mixed
3980      * @access private
3981      */
3982     private function &get_cached_message($key, $uid)
3983     {
3984         $internal_key = 'message';
3985
3986         if ($this->caching_enabled && !isset($this->icache[$internal_key][$uid])) {
3987             $sql_result = $this->db->query(
3988                 "SELECT idx, headers, structure, message_id".
3989                 " FROM ".get_table_name('messages').
3990                 " WHERE user_id=?".
3991                 " AND cache_key=?".
3992                 " AND uid=?",
3993                 $_SESSION['user_id'],
3994                 $key,
3995                 $uid);
3996
3997             if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
3998                 $this->icache['message.id'][$uid] = intval($sql_arr['message_id']);
3999                     $this->uid_id_map[preg_replace('/\.msg$/', '', $key)][$uid] = intval($sql_arr['idx']);
4000                 $this->icache[$internal_key][$uid] = $this->db->decode(unserialize($sql_arr['headers']));
4001
4002                 if (is_object($this->icache[$internal_key][$uid]) && !empty($sql_arr['structure']))
4003                     $this->icache[$internal_key][$uid]->structure = $this->db->decode(unserialize($sql_arr['structure']));
4004             }
4005         }
4006
4007         return $this->icache[$internal_key][$uid];
4008     }
4009
4010
4011     /**
4012      * @param string  $key        Cache key
4013      * @param string  $sort_field Sorting column
4014      * @param string  $sort_order Sorting order
4015      * @return array Messages index
4016      * @access private
4017      */
4018     private function get_message_cache_index($key, $sort_field='idx', $sort_order='ASC')
4019     {
4020         if (!$this->caching_enabled || empty($key))
4021             return NULL;
4022
4023         // use idx sort as default
4024         if (!$sort_field || !in_array($sort_field, $this->db_header_fields))
4025             $sort_field = 'idx';
4026
4027         if (array_key_exists('index', $this->icache)
4028             && $this->icache['index']['key'] == $key
4029             && $this->icache['index']['sort_field'] == $sort_field
4030         ) {
4031             if ($this->icache['index']['sort_order'] == $sort_order)
4032                 return $this->icache['index']['result'];
4033             else
4034                 return array_reverse($this->icache['index']['result'], true);
4035         }
4036
4037         $this->icache['index'] = array(
4038             'result'     => array(),
4039             'key'        => $key,
4040             'sort_field' => $sort_field,
4041             'sort_order' => $sort_order,
4042         );
4043
4044         $sql_result = $this->db->query(
4045             "SELECT idx, uid".
4046             " FROM ".get_table_name('messages').
4047             " WHERE user_id=?".
4048             " AND cache_key=?".
4049             " ORDER BY ".$this->db->quote_identifier($sort_field)." ".$sort_order,
4050             $_SESSION['user_id'],
4051             $key);
4052
4053         while ($sql_arr = $this->db->fetch_assoc($sql_result))
4054             $this->icache['index']['result'][$sql_arr['idx']] = intval($sql_arr['uid']);
4055
4056         return $this->icache['index']['result'];
4057     }
4058
4059
4060     /**
4061      * @access private
4062      */
4063     private function add_message_cache($key, $index, $headers, $struct=null, $force=false, $internal_cache=false)
4064     {
4065         if (empty($key) || !is_object($headers) || empty($headers->uid))
4066             return;
4067
4068         // add to internal (fast) cache
4069         if ($internal_cache) {
4070             $this->icache['message'][$headers->uid] = clone $headers;
4071             $this->icache['message'][$headers->uid]->structure = $struct;
4072         }
4073
4074         // no further caching
4075         if (!$this->caching_enabled)
4076             return;
4077
4078         // known message id
4079         if (is_int($force) && $force > 0) {
4080             $message_id = $force;
4081         }
4082         // check for an existing record (probably headers are cached but structure not)
4083         else if (!$force) {
4084             $sql_result = $this->db->query(
4085                 "SELECT message_id".
4086                 " FROM ".get_table_name('messages').
4087                 " WHERE user_id=?".
4088                 " AND cache_key=?".
4089                 " AND uid=?",
4090                 $_SESSION['user_id'],
4091                 $key,
4092                 $headers->uid);
4093
4094             if ($sql_arr = $this->db->fetch_assoc($sql_result))
4095                 $message_id = $sql_arr['message_id'];
4096         }
4097
4098         // update cache record
4099         if ($message_id) {
4100             $this->db->query(
4101                 "UPDATE ".get_table_name('messages').
4102                 " SET idx=?, headers=?, structure=?".
4103                 " WHERE message_id=?",
4104                 $index,
4105                 serialize($this->db->encode(clone $headers)),
4106                 is_object($struct) ? serialize($this->db->encode(clone $struct)) : NULL,
4107                 $message_id
4108             );
4109         }
4110         else { // insert new record
4111             $this->db->query(
4112                 "INSERT INTO ".get_table_name('messages').
4113                 " (user_id, del, cache_key, created, idx, uid, subject, ".
4114                 $this->db->quoteIdentifier('from').", ".
4115                 $this->db->quoteIdentifier('to').", ".
4116                 "cc, date, size, headers, structure)".
4117                 " VALUES (?, 0, ?, ".$this->db->now().", ?, ?, ?, ?, ?, ?, ".
4118                 $this->db->fromunixtime($headers->timestamp).", ?, ?, ?)",
4119                 $_SESSION['user_id'],
4120                 $key,
4121                 $index,
4122                 $headers->uid,
4123                 (string)mb_substr($this->db->encode($this->decode_header($headers->subject, true)), 0, 128),
4124                 (string)mb_substr($this->db->encode($this->decode_header($headers->from, true)), 0, 128),
4125                 (string)mb_substr($this->db->encode($this->decode_header($headers->to, true)), 0, 128),
4126                 (string)mb_substr($this->db->encode($this->decode_header($headers->cc, true)), 0, 128),
4127                 (int)$headers->size,
4128                 serialize($this->db->encode(clone $headers)),
4129                 is_object($struct) ? serialize($this->db->encode(clone $struct)) : NULL
4130             );
4131         }
4132
4133         unset($this->icache['index']);
4134     }
4135
4136
4137     /**
4138      * @access private
4139      */
4140     private function remove_message_cache($key, $ids, $idx=false)
4141     {
4142         if (!$this->caching_enabled)
4143             return;
4144
4145         $this->db->query(
4146             "DELETE FROM ".get_table_name('messages').
4147             " WHERE user_id=?".
4148             " AND cache_key=?".
4149             " AND ".($idx ? "idx" : "uid")." IN (".$this->db->array2list($ids, 'integer').")",
4150             $_SESSION['user_id'],
4151             $key);
4152
4153         unset($this->icache['index']);
4154     }
4155
4156
4157     /**
4158      * @param string $key         Cache key
4159      * @param int    $start_index Start index
4160      * @access private
4161      */
4162     private function clear_message_cache($key, $start_index=1)
4163     {
4164         if (!$this->caching_enabled)
4165             return;
4166
4167         $this->db->query(
4168             "DELETE FROM ".get_table_name('messages').
4169             " WHERE user_id=?".
4170             " AND cache_key=?".
4171             " AND idx>=?",
4172             $_SESSION['user_id'], $key, $start_index);
4173
4174         unset($this->icache['index']);
4175     }
4176
4177
4178     /**
4179      * @access private
4180      */
4181     private function get_message_cache_index_min($key, $uids=NULL)
4182     {
4183         if (!$this->caching_enabled)
4184             return;
4185
4186         if (!empty($uids) && !is_array($uids)) {
4187             if ($uids == '*' || $uids == '1:*')
4188                 $uids = NULL;
4189             else
4190                 $uids = explode(',', $uids);
4191         }
4192
4193         $sql_result = $this->db->query(
4194             "SELECT MIN(idx) AS minidx".
4195             " FROM ".get_table_name('messages').
4196             " WHERE  user_id=?".
4197             " AND    cache_key=?"
4198             .(!empty($uids) ? " AND uid IN (".$this->db->array2list($uids, 'integer').")" : ''),
4199             $_SESSION['user_id'],
4200             $key);
4201
4202         if ($sql_arr = $this->db->fetch_assoc($sql_result))
4203             return $sql_arr['minidx'];
4204         else
4205             return 0;
4206     }
4207
4208
4209     /**
4210      * @param string $key Cache key
4211      * @param int    $id  Message (sequence) ID
4212      * @return int Message UID
4213      * @access private
4214      */
4215     private function get_cache_id2uid($key, $id)
4216     {
4217         if (!$this->caching_enabled)
4218             return null;
4219
4220         if (array_key_exists('index', $this->icache)
4221             && $this->icache['index']['key'] == $key
4222         ) {
4223             return $this->icache['index']['result'][$id];
4224         }
4225
4226         $sql_result = $this->db->query(
4227             "SELECT uid".
4228             " FROM ".get_table_name('messages').
4229             " WHERE user_id=?".
4230             " AND cache_key=?".
4231             " AND idx=?",
4232             $_SESSION['user_id'], $key, $id);
4233
4234         if ($sql_arr = $this->db->fetch_assoc($sql_result))
4235             return intval($sql_arr['uid']);
4236
4237         return null;
4238     }
4239
4240
4241     /**
4242      * @param string $key Cache key
4243      * @param int    $uid Message UID
4244      * @return int Message (sequence) ID
4245      * @access private
4246      */
4247     private function get_cache_uid2id($key, $uid)
4248     {
4249         if (!$this->caching_enabled)
4250             return null;
4251
4252         if (array_key_exists('index', $this->icache)
4253             && $this->icache['index']['key'] == $key
4254         ) {
4255             return array_search($uid, $this->icache['index']['result']);
4256         }
4257
4258         $sql_result = $this->db->query(
4259             "SELECT idx".
4260             " FROM ".get_table_name('messages').
4261             " WHERE user_id=?".
4262             " AND cache_key=?".
4263             " AND uid=?",
4264             $_SESSION['user_id'], $key, $uid);
4265
4266         if ($sql_arr = $this->db->fetch_assoc($sql_result))
4267             return intval($sql_arr['idx']);
4268
4269         return null;
4270     }
4271
4272
4273     /* --------------------------------
4274      *   encoding/decoding methods
4275      * --------------------------------*/
4276
4277     /**
4278      * Split an address list into a structured array list
4279      *
4280      * @param string  $input  Input string
4281      * @param int     $max    List only this number of addresses
4282      * @param boolean $decode Decode address strings
4283      * @return array  Indexed list of addresses
4284      */
4285     function decode_address_list($input, $max=null, $decode=true)
4286     {
4287         $a = $this->_parse_address_list($input, $decode);
4288         $out = array();
4289         // Special chars as defined by RFC 822 need to in quoted string (or escaped).
4290         $special_chars = '[\(\)\<\>\\\.\[\]@,;:"]';
4291
4292         if (!is_array($a))
4293             return $out;
4294
4295         $c = count($a);
4296         $j = 0;
4297
4298         foreach ($a as $val) {
4299             $j++;
4300             $address = trim($val['address']);
4301             $name    = trim($val['name']);
4302
4303             if ($name && $address && $name != $address)
4304                 $string = sprintf('%s <%s>', preg_match("/$special_chars/", $name) ? '"'.addcslashes($name, '"').'"' : $name, $address);
4305             else if ($address)
4306                 $string = $address;
4307             else if ($name)
4308                 $string = $name;
4309
4310             $out[$j] = array(
4311                 'name'   => $name,
4312                 'mailto' => $address,
4313                 'string' => $string
4314             );
4315
4316             if ($max && $j==$max)
4317                 break;
4318         }
4319
4320         return $out;
4321     }
4322
4323
4324     /**
4325      * Decode a message header value
4326      *
4327      * @param string  $input         Header value
4328      * @param boolean $remove_quotas Remove quotes if necessary
4329      * @return string Decoded string
4330      */
4331     function decode_header($input, $remove_quotes=false)
4332     {
4333         $str = rcube_imap::decode_mime_string((string)$input, $this->default_charset);
4334         if ($str[0] == '"' && $remove_quotes)
4335             $str = str_replace('"', '', $str);
4336
4337         return $str;
4338     }
4339
4340
4341     /**
4342      * Decode a mime-encoded string to internal charset
4343      *
4344      * @param string $input    Header value
4345      * @param string $fallback Fallback charset if none specified
4346      *
4347      * @return string Decoded string
4348      * @static
4349      */
4350     public static function decode_mime_string($input, $fallback=null)
4351     {
4352         if (!empty($fallback)) {
4353             $default_charset = $fallback;
4354         }
4355         else {
4356             $default_charset = rcmail::get_instance()->config->get('default_charset', 'ISO-8859-1');
4357         }
4358
4359         // rfc: all line breaks or other characters not found
4360         // in the Base64 Alphabet must be ignored by decoding software
4361         // delete all blanks between MIME-lines, differently we can
4362         // receive unnecessary blanks and broken utf-8 symbols
4363         $input = preg_replace("/\?=\s+=\?/", '?==?', $input);
4364
4365         // encoded-word regexp
4366         $re = '/=\?([^?]+)\?([BbQq])\?([^?\n]*)\?=/';
4367
4368         // Find all RFC2047's encoded words
4369         if (preg_match_all($re, $input, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {
4370             // Initialize variables
4371             $tmp   = array();
4372             $out   = '';
4373             $start = 0;
4374
4375             foreach ($matches as $idx => $m) {
4376                 $pos      = $m[0][1];
4377                 $charset  = $m[1][0];
4378                 $encoding = $m[2][0];
4379                 $text     = $m[3][0];
4380                 $length   = strlen($m[0][0]);
4381
4382                 // Append everything that is before the text to be decoded
4383                 if ($start != $pos) {
4384                     $substr = substr($input, $start, $pos-$start);
4385                     $out   .= rcube_charset_convert($substr, $default_charset);
4386                     $start  = $pos;
4387                 }
4388                 $start += $length;
4389
4390                 // Per RFC2047, each string part "MUST represent an integral number
4391                 // of characters . A multi-octet character may not be split across
4392                 // adjacent encoded-words." However, some mailers break this, so we
4393                 // try to handle characters spanned across parts anyway by iterating
4394                 // through and aggregating sequential encoded parts with the same
4395                 // character set and encoding, then perform the decoding on the
4396                 // aggregation as a whole.
4397
4398                 $tmp[] = $text;
4399                 if ($next_match = $matches[$idx+1]) {
4400                     if ($next_match[0][1] == $start
4401                         && $next_match[1][0] == $charset
4402                         && $next_match[2][0] == $encoding
4403                     ) {
4404                         continue;
4405                     }
4406                 }
4407
4408                 $count = count($tmp);
4409                 $text  = '';
4410
4411                 // Decode and join encoded-word's chunks
4412                 if ($encoding == 'B' || $encoding == 'b') {
4413                     // base64 must be decoded a segment at a time
4414                     for ($i=0; $i<$count; $i++)
4415                         $text .= base64_decode($tmp[$i]);
4416                 }
4417                 else { //if ($encoding == 'Q' || $encoding == 'q') {
4418                     // quoted printable can be combined and processed at once
4419                     for ($i=0; $i<$count; $i++)
4420                         $text .= $tmp[$i];
4421
4422                     $text = str_replace('_', ' ', $text);
4423                     $text = quoted_printable_decode($text);
4424                 }
4425
4426                 $out .= rcube_charset_convert($text, $charset);
4427                 $tmp = array();
4428             }
4429
4430             // add the last part of the input string
4431             if ($start != strlen($input)) {
4432                 $out .= rcube_charset_convert(substr($input, $start), $default_charset);
4433             }
4434
4435             // return the results
4436             return $out;
4437         }
4438
4439         // no encoding information, use fallback
4440         return rcube_charset_convert($input, $default_charset);
4441     }
4442
4443
4444     /**
4445      * Decode a mime part
4446      *
4447      * @param string $input    Input string
4448      * @param string $encoding Part encoding
4449      * @return string Decoded string
4450      */
4451     function mime_decode($input, $encoding='7bit')
4452     {
4453         switch (strtolower($encoding)) {
4454         case 'quoted-printable':
4455             return quoted_printable_decode($input);
4456         case 'base64':
4457             return base64_decode($input);
4458         case 'x-uuencode':
4459         case 'x-uue':
4460         case 'uue':
4461         case 'uuencode':
4462             return convert_uudecode($input);
4463         case '7bit':
4464         default:
4465             return $input;
4466         }
4467     }
4468
4469
4470     /**
4471      * Convert body charset to RCMAIL_CHARSET according to the ctype_parameters
4472      *
4473      * @param string $body        Part body to decode
4474      * @param string $ctype_param Charset to convert from
4475      * @return string Content converted to internal charset
4476      */
4477     function charset_decode($body, $ctype_param)
4478     {
4479         if (is_array($ctype_param) && !empty($ctype_param['charset']))
4480             return rcube_charset_convert($body, $ctype_param['charset']);
4481
4482         // defaults to what is specified in the class header
4483         return rcube_charset_convert($body,  $this->default_charset);
4484     }
4485
4486
4487     /* --------------------------------
4488      *         private methods
4489      * --------------------------------*/
4490
4491     /**
4492      * Validate the given input and save to local properties
4493      *
4494      * @param string $sort_field Sort column
4495      * @param string $sort_order Sort order
4496      * @access private
4497      */
4498     private function _set_sort_order($sort_field, $sort_order)
4499     {
4500         if ($sort_field != null)
4501             $this->sort_field = asciiwords($sort_field);
4502         if ($sort_order != null)
4503             $this->sort_order = strtoupper($sort_order) == 'DESC' ? 'DESC' : 'ASC';
4504     }
4505
4506
4507     /**
4508      * Sort mailboxes first by default folders and then in alphabethical order
4509      *
4510      * @param array $a_folders Mailboxes list
4511      * @access private
4512      */
4513     private function _sort_mailbox_list($a_folders)
4514     {
4515         $a_out = $a_defaults = $folders = array();
4516
4517         $delimiter = $this->get_hierarchy_delimiter();
4518
4519         // find default folders and skip folders starting with '.'
4520         foreach ($a_folders as $i => $folder) {
4521             if ($folder[0] == '.')
4522                 continue;
4523
4524             if (($p = array_search($folder, $this->default_folders)) !== false && !$a_defaults[$p])
4525                 $a_defaults[$p] = $folder;
4526             else
4527                 $folders[$folder] = rcube_charset_convert($folder, 'UTF7-IMAP');
4528         }
4529
4530         // sort folders and place defaults on the top
4531         asort($folders, SORT_LOCALE_STRING);
4532         ksort($a_defaults);
4533         $folders = array_merge($a_defaults, array_keys($folders));
4534
4535         // finally we must rebuild the list to move
4536         // subfolders of default folders to their place...
4537         // ...also do this for the rest of folders because
4538         // asort() is not properly sorting case sensitive names
4539         while (list($key, $folder) = each($folders)) {
4540             // set the type of folder name variable (#1485527)
4541             $a_out[] = (string) $folder;
4542             unset($folders[$key]);
4543             $this->_rsort($folder, $delimiter, $folders, $a_out);
4544         }
4545
4546         return $a_out;
4547     }
4548
4549
4550     /**
4551      * @access private
4552      */
4553     private function _rsort($folder, $delimiter, &$list, &$out)
4554     {
4555         while (list($key, $name) = each($list)) {
4556                 if (strpos($name, $folder.$delimiter) === 0) {
4557                     // set the type of folder name variable (#1485527)
4558                 $out[] = (string) $name;
4559                     unset($list[$key]);
4560                     $this->_rsort($name, $delimiter, $list, $out);
4561                 }
4562         }
4563         reset($list);
4564     }
4565
4566
4567     /**
4568      * @param int    $uid       Message UID
4569      * @param string $mbox_name Mailbox name
4570      * @return int Message (sequence) ID
4571      * @access private
4572      */
4573     private function _uid2id($uid, $mbox_name=NULL)
4574     {
4575         if (!strlen($mbox_name))
4576             $mbox_name = $this->mailbox;
4577
4578         if (!isset($this->uid_id_map[$mbox_name][$uid])) {
4579             if (!($id = $this->get_cache_uid2id($mbox_name.'.msg', $uid)))
4580                 $id = $this->conn->UID2ID($mbox_name, $uid);
4581
4582             $this->uid_id_map[$mbox_name][$uid] = $id;
4583         }
4584
4585         return $this->uid_id_map[$mbox_name][$uid];
4586     }
4587
4588
4589     /**
4590      * @param int    $id        Message (sequence) ID
4591      * @param string $mbox_name Mailbox name
4592      * @return int Message UID
4593      * @access private
4594      */
4595     private function _id2uid($id, $mbox_name=NULL)
4596     {
4597         if (!strlen($mbox_name))
4598             $mbox_name = $this->mailbox;
4599
4600         if ($uid = array_search($id, (array)$this->uid_id_map[$mbox_name]))
4601             return $uid;
4602
4603         if (!($uid = $this->get_cache_id2uid($mbox_name.'.msg', $id)))
4604             $uid = $this->conn->ID2UID($mbox_name, $id);
4605
4606         $this->uid_id_map[$mbox_name][$uid] = $id;
4607
4608         return $uid;
4609     }
4610
4611
4612     /**
4613      * Subscribe/unsubscribe a list of mailboxes and update local cache
4614      * @access private
4615      */
4616     private function _change_subscription($a_mboxes, $mode)
4617     {
4618         $updated = false;
4619
4620         if (is_array($a_mboxes))
4621             foreach ($a_mboxes as $i => $mbox_name) {
4622                 $mailbox = $this->mod_mailbox($mbox_name);
4623                 $a_mboxes[$i] = $mailbox;
4624
4625                 if ($mode=='subscribe')
4626                     $updated = $this->conn->subscribe($mailbox);
4627                 else if ($mode=='unsubscribe')
4628                     $updated = $this->conn->unsubscribe($mailbox);
4629             }
4630
4631         // get cached mailbox list
4632         if ($updated) {
4633             $a_mailbox_cache = $this->get_cache('mailboxes');
4634             if (!is_array($a_mailbox_cache))
4635                 return $updated;
4636
4637             // modify cached list
4638             if ($mode=='subscribe')
4639                 $a_mailbox_cache = array_merge($a_mailbox_cache, $a_mboxes);
4640             else if ($mode=='unsubscribe')
4641                 $a_mailbox_cache = array_diff($a_mailbox_cache, $a_mboxes);
4642
4643             // write mailboxlist to cache
4644             $this->update_cache('mailboxes', $this->_sort_mailbox_list($a_mailbox_cache));
4645         }
4646
4647         return $updated;
4648     }
4649
4650
4651     /**
4652      * Increde/decrese messagecount for a specific mailbox
4653      * @access private
4654      */
4655     private function _set_messagecount($mbox_name, $mode, $increment)
4656     {
4657         $a_mailbox_cache = false;
4658         $mailbox = strlen($mbox_name) ? $mbox_name : $this->mailbox;
4659         $mode = strtoupper($mode);
4660
4661         $a_mailbox_cache = $this->get_cache('messagecount');
4662
4663         if (!is_array($a_mailbox_cache[$mailbox]) || !isset($a_mailbox_cache[$mailbox][$mode]) || !is_numeric($increment))
4664             return false;
4665
4666         // add incremental value to messagecount
4667         $a_mailbox_cache[$mailbox][$mode] += $increment;
4668
4669         // there's something wrong, delete from cache
4670         if ($a_mailbox_cache[$mailbox][$mode] < 0)
4671             unset($a_mailbox_cache[$mailbox][$mode]);
4672
4673         // write back to cache
4674         $this->update_cache('messagecount', $a_mailbox_cache);
4675
4676         return true;
4677     }
4678
4679
4680     /**
4681      * Remove messagecount of a specific mailbox from cache
4682      * @access private
4683      */
4684     private function _clear_messagecount($mbox_name='', $mode=null)
4685     {
4686         $mailbox = strlen($mbox_name) ? $mbox_name : $this->mailbox;
4687
4688         $a_mailbox_cache = $this->get_cache('messagecount');
4689
4690         if (is_array($a_mailbox_cache[$mailbox])) {
4691             if ($mode) {
4692                 unset($a_mailbox_cache[$mailbox][$mode]);
4693             }
4694             else {
4695                 unset($a_mailbox_cache[$mailbox]);
4696             }
4697             $this->update_cache('messagecount', $a_mailbox_cache);
4698         }
4699     }
4700
4701
4702     /**
4703      * Split RFC822 header string into an associative array
4704      * @access private
4705      */
4706     private function _parse_headers($headers)
4707     {
4708         $a_headers = array();
4709         $headers = preg_replace('/\r?\n(\t| )+/', ' ', $headers);
4710         $lines = explode("\n", $headers);
4711         $c = count($lines);
4712
4713         for ($i=0; $i<$c; $i++) {
4714             if ($p = strpos($lines[$i], ': ')) {
4715                 $field = strtolower(substr($lines[$i], 0, $p));
4716                 $value = trim(substr($lines[$i], $p+1));
4717                 if (!empty($value))
4718                     $a_headers[$field] = $value;
4719             }
4720         }
4721
4722         return $a_headers;
4723     }
4724
4725
4726     /**
4727      * @access private
4728      */
4729     private function _parse_address_list($str, $decode=true)
4730     {
4731         // remove any newlines and carriage returns before
4732         $str = preg_replace('/\r?\n(\s|\t)?/', ' ', $str);
4733
4734         // extract list items, remove comments
4735         $str = self::explode_header_string(',;', $str, true);
4736         $result = array();
4737
4738         foreach ($str as $key => $val) {
4739             $name    = '';
4740             $address = '';
4741             $val     = trim($val);
4742
4743             if (preg_match('/(.*)<(\S+@\S+)>$/', $val, $m)) {
4744                 $address = $m[2];
4745                 $name    = trim($m[1]);
4746             }
4747             else if (preg_match('/^(\S+@\S+)$/', $val, $m)) {
4748                 $address = $m[1];
4749                 $name    = '';
4750             }
4751             else {
4752                 $name = $val;
4753             }
4754
4755             // dequote and/or decode name
4756             if ($name) {
4757                 if ($name[0] == '"') {
4758                     $name = substr($name, 1, -1);
4759                     $name = stripslashes($name);
4760                 }
4761                 if ($decode) {
4762                     $name = $this->decode_header($name);
4763                 }
4764             }
4765
4766             if (!$address && $name) {
4767                 $address = $name;
4768             }
4769
4770             if ($address) {
4771                 $result[$key] = array('name' => $name, 'address' => $address);
4772             }
4773         }
4774
4775         return $result;
4776     }
4777
4778
4779     /**
4780      * Explodes header (e.g. address-list) string into array of strings
4781      * using specified separator characters with proper handling
4782      * of quoted-strings and comments (RFC2822)
4783      *
4784      * @param string $separator       String containing separator characters
4785      * @param string $str             Header string
4786      * @param bool   $remove_comments Enable to remove comments
4787      *
4788      * @return array Header items
4789      */
4790     static function explode_header_string($separator, $str, $remove_comments=false)
4791     {
4792         $length  = strlen($str);
4793         $result  = array();
4794         $quoted  = false;
4795         $comment = 0;
4796         $out     = '';
4797
4798         for ($i=0; $i<$length; $i++) {
4799             // we're inside a quoted string
4800             if ($quoted) {
4801                 if ($str[$i] == '"') {
4802                     $quoted = false;
4803                 }
4804                 else if ($str[$i] == '\\') {
4805                     if ($comment <= 0) {
4806                         $out .= '\\';
4807                     }
4808                     $i++;
4809                 }
4810             }
4811             // we're inside a comment string
4812             else if ($comment > 0) {
4813                     if ($str[$i] == ')') {
4814                         $comment--;
4815                     }
4816                     else if ($str[$i] == '(') {
4817                         $comment++;
4818                     }
4819                     else if ($str[$i] == '\\') {
4820                         $i++;
4821                     }
4822                     continue;
4823             }
4824             // separator, add to result array
4825             else if (strpos($separator, $str[$i]) !== false) {
4826                     if ($out) {
4827                         $result[] = $out;
4828                     }
4829                     $out = '';
4830                     continue;
4831             }
4832             // start of quoted string
4833             else if ($str[$i] == '"') {
4834                     $quoted = true;
4835             }
4836             // start of comment
4837             else if ($remove_comments && $str[$i] == '(') {
4838                     $comment++;
4839             }
4840
4841             if ($comment <= 0) {
4842                 $out .= $str[$i];
4843             }
4844         }
4845
4846         if ($out && $comment <= 0) {
4847             $result[] = $out;
4848         }
4849
4850         return $result;
4851     }
4852
4853
4854     /**
4855      * This is our own debug handler for the IMAP connection
4856      * @access public
4857      */
4858     public function debug_handler(&$imap, $message)
4859     {
4860         write_log('imap', $message);
4861     }
4862
4863 }  // end class rcube_imap
4864
4865
4866 /**
4867  * Class representing a message part
4868  *
4869  * @package Mail
4870  */
4871 class rcube_message_part
4872 {
4873     var $mime_id = '';
4874     var $ctype_primary = 'text';
4875     var $ctype_secondary = 'plain';
4876     var $mimetype = 'text/plain';
4877     var $disposition = '';
4878     var $filename = '';
4879     var $encoding = '8bit';
4880     var $charset = '';
4881     var $size = 0;
4882     var $headers = array();
4883     var $d_parameters = array();
4884     var $ctype_parameters = array();
4885
4886     function __clone()
4887     {
4888         if (isset($this->parts))
4889             foreach ($this->parts as $idx => $part)
4890                 if (is_object($part))
4891                         $this->parts[$idx] = clone $part;
4892     }
4893 }
4894
4895
4896 /**
4897  * Class for sorting an array of rcube_mail_header objects in a predetermined order.
4898  *
4899  * @package Mail
4900  * @author Eric Stadtherr
4901  */
4902 class rcube_header_sorter
4903 {
4904     var $sequence_numbers = array();
4905
4906     /**
4907      * Set the predetermined sort order.
4908      *
4909      * @param array $seqnums Numerically indexed array of IMAP message sequence numbers
4910      */
4911     function set_sequence_numbers($seqnums)
4912     {
4913         $this->sequence_numbers = array_flip($seqnums);
4914     }
4915
4916     /**
4917      * Sort the array of header objects
4918      *
4919      * @param array $headers Array of rcube_mail_header objects indexed by UID
4920      */
4921     function sort_headers(&$headers)
4922     {
4923         /*
4924         * uksort would work if the keys were the sequence number, but unfortunately
4925         * the keys are the UIDs.  We'll use uasort instead and dereference the value
4926         * to get the sequence number (in the "id" field).
4927         *
4928         * uksort($headers, array($this, "compare_seqnums"));
4929         */
4930         uasort($headers, array($this, "compare_seqnums"));
4931     }
4932
4933     /**
4934      * Sort method called by uasort()
4935      *
4936      * @param rcube_mail_header $a
4937      * @param rcube_mail_header $b
4938      */
4939     function compare_seqnums($a, $b)
4940     {
4941         // First get the sequence number from the header object (the 'id' field).
4942         $seqa = $a->id;
4943         $seqb = $b->id;
4944
4945         // then find each sequence number in my ordered list
4946         $posa = isset($this->sequence_numbers[$seqa]) ? intval($this->sequence_numbers[$seqa]) : -1;
4947         $posb = isset($this->sequence_numbers[$seqb]) ? intval($this->sequence_numbers[$seqb]) : -1;
4948
4949         // return the relative position as the comparison value
4950         return $posa - $posb;
4951     }
4952 }