source: github/program/include/rcube_imap.php @ a2e8cb36

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