source: github/program/include/rcube_imap.php @ 3bb9b52

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