source: github/program/include/rcube_imap.php @ 9220169

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