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