source: github/program/include/rcube_imap.php @ 29983c1

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