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

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