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

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