source: subversion/trunk/roundcubemail/program/include/rcube_imap.php @ 4220

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