source: github/program/include/rcube_imap.php @ 103ddcde

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