source: subversion/branches/release-0.7/program/include/rcube_imap.php @ 5638

Last change on this file since 5638 was 5638, checked in by alec, 17 months ago
  • Backport r5637 fix for searching without CHARSET
  • 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     * @todo: Search criteria should be provided in non-IMAP format, eg. array
1640     */
1641    function search($mailbox='', $str=NULL, $charset=NULL, $sort_field=NULL)
1642    {
1643        if (!$str)
1644            return false;
1645
1646        if (!strlen($mailbox)) {
1647            $mailbox = $this->mailbox;
1648        }
1649
1650        $results = $this->_search_index($mailbox, $str, $charset, $sort_field);
1651
1652        $this->set_search_set($str, $results, $charset, $sort_field, (bool)$this->threading,
1653            $this->threading || $this->search_sorted ? true : false);
1654
1655        return $results;
1656    }
1657
1658
1659    /**
1660     * Private search method
1661     *
1662     * @param string $mailbox    Mailbox name
1663     * @param string $criteria   Search criteria
1664     * @param string $charset    Charset
1665     * @param string $sort_field Sorting field
1666     *
1667     * @return array   search results as list of message ids
1668     * @see rcube_imap::search()
1669     */
1670    private function _search_index($mailbox, $criteria='ALL', $charset=NULL, $sort_field=NULL)
1671    {
1672        $orig_criteria = $criteria;
1673
1674        if ($this->skip_deleted && !preg_match('/UNDELETED/', $criteria))
1675            $criteria = 'UNDELETED '.$criteria;
1676
1677        if ($this->threading) {
1678            $a_messages = $this->conn->thread($mailbox, $this->threading, $criteria, $charset);
1679
1680            // Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
1681            // but I've seen that Courier doesn't support UTF-8)
1682            if ($a_messages === false && $charset && $charset != 'US-ASCII')
1683                $a_messages = $this->conn->thread($mailbox, $this->threading,
1684                    $this->convert_criteria($criteria, $charset), 'US-ASCII');
1685
1686            if ($a_messages !== false) {
1687                list ($thread_tree, $msg_depth, $has_children) = $a_messages;
1688                $a_messages = array(
1689                    'tree' => $thread_tree,
1690                    'depth'=> $msg_depth,
1691                    'children' => $has_children
1692                );
1693            }
1694
1695            return $a_messages;
1696        }
1697
1698        if ($sort_field && $this->get_capability('SORT')) {
1699            $charset = $charset ? $charset : $this->default_charset;
1700            $a_messages = $this->conn->sort($mailbox, $sort_field, $criteria, false, $charset);
1701
1702            // Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
1703            // but I've seen Courier with disabled UTF-8 support)
1704            if ($a_messages === false && $charset && $charset != 'US-ASCII')
1705                $a_messages = $this->conn->sort($mailbox, $sort_field,
1706                    $this->convert_criteria($criteria, $charset), false, 'US-ASCII');
1707
1708            if ($a_messages !== false) {
1709                $this->search_sorted = true;
1710                return $a_messages;
1711            }
1712        }
1713
1714        if ($orig_criteria == 'ALL') {
1715            $max = $this->_messagecount($mailbox, 'ALL', true, false);
1716            $a_messages = $max ? range(1, $max) : array();
1717        }
1718        else {
1719            $a_messages = $this->conn->search($mailbox,
1720                ($charset ? "CHARSET $charset " : '') . $criteria);
1721
1722            // Error, try with US-ASCII (some servers may support only US-ASCII)
1723            if ($a_messages === false && $charset && $charset != 'US-ASCII')
1724                $a_messages = $this->conn->search($mailbox,
1725                    $this->convert_criteria($criteria, $charset));
1726
1727            // I didn't found that SEARCH should return sorted IDs
1728            if (is_array($a_messages) && !$this->sort_field)
1729                sort($a_messages);
1730        }
1731
1732        $this->search_sorted = false;
1733
1734        return $a_messages;
1735    }
1736
1737
1738    /**
1739     * Direct (real and simple) SEARCH request to IMAP server,
1740     * without result sorting and caching
1741     *
1742     * @param  string  $mailbox Mailbox name to search in
1743     * @param  string  $str     Search string
1744     * @param  boolean $ret_uid True if UIDs should be returned
1745     *
1746     * @return array   Search results as list of message IDs or UIDs
1747     */
1748    function search_once($mailbox='', $str=NULL, $ret_uid=false)
1749    {
1750        if (!$str)
1751            return false;
1752
1753        if (!strlen($mailbox)) {
1754            $mailbox = $this->mailbox;
1755        }
1756
1757        return $this->conn->search($mailbox, $str, $ret_uid);
1758    }
1759
1760
1761    /**
1762     * Converts charset of search criteria string
1763     *
1764     * @param  string  $str          Search string
1765     * @param  string  $charset      Original charset
1766     * @param  string  $dest_charset Destination charset (default US-ASCII)
1767     * @return string  Search string
1768     * @access private
1769     */
1770    private function convert_criteria($str, $charset, $dest_charset='US-ASCII')
1771    {
1772        // convert strings to US_ASCII
1773        if (preg_match_all('/\{([0-9]+)\}\r\n/', $str, $matches, PREG_OFFSET_CAPTURE)) {
1774            $last = 0; $res = '';
1775            foreach ($matches[1] as $m) {
1776                $string_offset = $m[1] + strlen($m[0]) + 4; // {}\r\n
1777                $string = substr($str, $string_offset - 1, $m[0]);
1778                $string = rcube_charset_convert($string, $charset, $dest_charset);
1779                if ($string === false)
1780                    continue;
1781                $res .= substr($str, $last, $m[1] - $last - 1) . rcube_imap_generic::escape($string);
1782                $last = $m[0] + $string_offset - 1;
1783            }
1784            if ($last < strlen($str))
1785                $res .= substr($str, $last, strlen($str)-$last);
1786        }
1787        else // strings for conversion not found
1788            $res = $str;
1789
1790        return $res;
1791    }
1792
1793
1794    /**
1795     * Sort thread
1796     *
1797     * @param string $mailbox     Mailbox name
1798     * @param  array $thread_tree Unsorted thread tree (rcube_imap_generic::thread() result)
1799     * @param  array $ids         Message IDs if we know what we need (e.g. search result)
1800     *
1801     * @return array Sorted roots IDs
1802     */
1803    function sort_threads($mailbox, $thread_tree, $ids = null)
1804    {
1805        // THREAD=ORDEREDSUBJECT: sorting by sent date of root message
1806        // THREAD=REFERENCES:     sorting by sent date of root message
1807        // THREAD=REFS:           sorting by the most recent date in each thread
1808
1809        // default sorting
1810        if (!$this->sort_field || ($this->sort_field == 'date' && $this->threading == 'REFS')) {
1811            return array_keys((array)$thread_tree);
1812        }
1813        // here we'll implement REFS sorting
1814        else {
1815            if ($mcache = $this->get_mcache_engine()) {
1816                $a_index = $mcache->get_index($mailbox, $this->sort_field, 'ASC');
1817                if (is_array($a_index)) {
1818                    $a_index = array_keys($a_index);
1819                    // now we must remove IDs that doesn't exist in $ids
1820                    if (!empty($ids))
1821                        $a_index = array_intersect($a_index, $ids);
1822                }
1823            }
1824            // use SORT command
1825            else if ($this->get_capability('SORT') &&
1826                ($a_index = $this->conn->sort($mailbox, $this->sort_field,
1827                    !empty($ids) ? $ids : ($this->skip_deleted ? 'UNDELETED' : ''))) !== false
1828            ) {
1829                // do nothing
1830            }
1831            else {
1832                // fetch specified headers for all messages and sort them
1833                $a_index = $this->conn->fetchHeaderIndex($mailbox, !empty($ids) ? $ids : "1:*",
1834                    $this->sort_field, $this->skip_deleted);
1835
1836                // return unsorted tree if we've got no index data
1837                if (!empty($a_index)) {
1838                    asort($a_index); // ASC
1839                    $a_index = array_values($a_index);
1840                }
1841            }
1842
1843            if (empty($a_index))
1844                return array_keys((array)$thread_tree);
1845
1846            return $this->_sort_thread_refs($thread_tree, $a_index);
1847        }
1848    }
1849
1850
1851    /**
1852     * THREAD=REFS sorting implementation
1853     *
1854     * @param  array $tree   Thread tree array (message identifiers as keys)
1855     * @param  array $index  Array of sorted message identifiers
1856     *
1857     * @return array   Array of sorted roots messages
1858     */
1859    private function _sort_thread_refs($tree, $index)
1860    {
1861        if (empty($tree))
1862            return array();
1863
1864        $index = array_combine(array_values($index), $index);
1865
1866        // assign roots
1867        foreach ($tree as $idx => $val) {
1868            $index[$idx] = $idx;
1869            if (!empty($val)) {
1870                $idx_arr = array_keys_recursive($tree[$idx]);
1871                foreach ($idx_arr as $subidx)
1872                    $index[$subidx] = $idx;
1873            }
1874        }
1875
1876        $index = array_values($index);
1877
1878        // create sorted array of roots
1879        $msg_index = array();
1880        if ($this->sort_order != 'DESC') {
1881            foreach ($index as $idx)
1882                if (!isset($msg_index[$idx]))
1883                    $msg_index[$idx] = $idx;
1884            $msg_index = array_values($msg_index);
1885        }
1886        else {
1887            for ($x=count($index)-1; $x>=0; $x--)
1888                if (!isset($msg_index[$index[$x]]))
1889                    $msg_index[$index[$x]] = $index[$x];
1890            $msg_index = array_reverse($msg_index);
1891        }
1892
1893        return $msg_index;
1894    }
1895
1896
1897    /**
1898     * Refresh saved search set
1899     *
1900     * @return array Current search set
1901     */
1902    function refresh_search()
1903    {
1904        if (!empty($this->search_string))
1905            $this->search_set = $this->search('', $this->search_string, $this->search_charset,
1906                $this->search_sort_field, $this->search_threads, $this->search_sorted);
1907
1908        return $this->get_search_set();
1909    }
1910
1911
1912    /**
1913     * Check if the given message ID is part of the current search set
1914     *
1915     * @param string $msgid Message id
1916     * @return boolean True on match or if no search request is stored
1917     */
1918    function in_searchset($msgid)
1919    {
1920        if (!empty($this->search_string)) {
1921            if ($this->search_threads)
1922                return isset($this->search_set['depth']["$msgid"]);
1923            else
1924                return in_array("$msgid", (array)$this->search_set, true);
1925        }
1926        else
1927            return true;
1928    }
1929
1930
1931    /**
1932     * Return message headers object of a specific message
1933     *
1934     * @param int     $id       Message sequence ID or UID
1935     * @param string  $mailbox  Mailbox to read from
1936     * @param bool    $force    True to skip cache
1937     *
1938     * @return rcube_mail_header Message headers
1939     */
1940    function get_headers($uid, $mailbox = null, $force = false)
1941    {
1942        if (!strlen($mailbox)) {
1943            $mailbox = $this->mailbox;
1944        }
1945
1946        // get cached headers
1947        if (!$force && $uid && ($mcache = $this->get_mcache_engine())) {
1948            $headers = $mcache->get_message($mailbox, $uid);
1949        }
1950        else {
1951            $headers = $this->conn->fetchHeader(
1952                $mailbox, $uid, true, true, $this->get_fetch_headers());
1953        }
1954
1955        return $headers;
1956    }
1957
1958
1959    /**
1960     * Fetch message headers and body structure from the IMAP server and build
1961     * an object structure similar to the one generated by PEAR::Mail_mimeDecode
1962     *
1963     * @param int     $uid      Message UID to fetch
1964     * @param string  $mailbox  Mailbox to read from
1965     *
1966     * @return object rcube_mail_header Message data
1967     */
1968    function get_message($uid, $mailbox = null)
1969    {
1970        if (!strlen($mailbox)) {
1971            $mailbox = $this->mailbox;
1972        }
1973
1974        // Check internal cache
1975        if (!empty($this->icache['message'])) {
1976            if (($headers = $this->icache['message']) && $headers->uid == $uid) {
1977                return $headers;
1978            }
1979        }
1980
1981        $headers = $this->get_headers($uid, $mailbox);
1982
1983        // message doesn't exist?
1984        if (empty($headers))
1985            return null; 
1986
1987        // structure might be cached
1988        if (!empty($headers->structure))
1989            return $headers;
1990
1991        $this->_msg_uid = $uid;
1992
1993        if (empty($headers->bodystructure)) {
1994            $headers->bodystructure = $this->conn->getStructure($mailbox, $uid, true);
1995        }
1996
1997        $structure = $headers->bodystructure;
1998
1999        if (empty($structure))
2000            return $headers;
2001
2002        // set message charset from message headers
2003        if ($headers->charset)
2004            $this->struct_charset = $headers->charset;
2005        else
2006            $this->struct_charset = $this->_structure_charset($structure);
2007
2008        $headers->ctype = strtolower($headers->ctype);
2009
2010        // Here we can recognize malformed BODYSTRUCTURE and
2011        // 1. [@TODO] parse the message in other way to create our own message structure
2012        // 2. or just show the raw message body.
2013        // Example of structure for malformed MIME message:
2014        // ("text" "plain" NIL NIL NIL "7bit" 2154 70 NIL NIL NIL)
2015        if ($headers->ctype && !is_array($structure[0]) && $headers->ctype != 'text/plain'
2016            && strtolower($structure[0].'/'.$structure[1]) == 'text/plain') {
2017            // we can handle single-part messages, by simple fix in structure (#1486898)
2018            if (preg_match('/^(text|application)\/(.*)/', $headers->ctype, $m)) {
2019                $structure[0] = $m[1];
2020                $structure[1] = $m[2];
2021            }
2022            else
2023                return $headers;
2024        }
2025
2026        $struct = &$this->_structure_part($structure, 0, '', $headers);
2027
2028        // don't trust given content-type
2029        if (empty($struct->parts) && !empty($headers->ctype)) {
2030            $struct->mime_id = '1';
2031            $struct->mimetype = strtolower($headers->ctype);
2032            list($struct->ctype_primary, $struct->ctype_secondary) = explode('/', $struct->mimetype);
2033        }
2034
2035        $headers->structure = $struct;
2036
2037        return $this->icache['message'] = $headers;
2038    }
2039
2040
2041    /**
2042     * Build message part object
2043     *
2044     * @param array  $part
2045     * @param int    $count
2046     * @param string $parent
2047     * @access private
2048     */
2049    function &_structure_part($part, $count=0, $parent='', $mime_headers=null)
2050    {
2051        $struct = new rcube_message_part;
2052        $struct->mime_id = empty($parent) ? (string)$count : "$parent.$count";
2053
2054        // multipart
2055        if (is_array($part[0])) {
2056            $struct->ctype_primary = 'multipart';
2057
2058        /* RFC3501: BODYSTRUCTURE fields of multipart part
2059            part1 array
2060            part2 array
2061            part3 array
2062            ....
2063            1. subtype
2064            2. parameters (optional)
2065            3. description (optional)
2066            4. language (optional)
2067            5. location (optional)
2068        */
2069
2070            // find first non-array entry
2071            for ($i=1; $i<count($part); $i++) {
2072                if (!is_array($part[$i])) {
2073                    $struct->ctype_secondary = strtolower($part[$i]);
2074                    break;
2075                }
2076            }
2077
2078            $struct->mimetype = 'multipart/'.$struct->ctype_secondary;
2079
2080            // build parts list for headers pre-fetching
2081            for ($i=0; $i<count($part); $i++) {
2082                if (!is_array($part[$i]))
2083                    break;
2084                // fetch message headers if message/rfc822
2085                // or named part (could contain Content-Location header)
2086                if (!is_array($part[$i][0])) {
2087                    $tmp_part_id = $struct->mime_id ? $struct->mime_id.'.'.($i+1) : $i+1;
2088                    if (strtolower($part[$i][0]) == 'message' && strtolower($part[$i][1]) == 'rfc822') {
2089                        $mime_part_headers[] = $tmp_part_id;
2090                    }
2091                    else if (in_array('name', (array)$part[$i][2]) && empty($part[$i][3])) {
2092                        $mime_part_headers[] = $tmp_part_id;
2093                    }
2094                }
2095            }
2096
2097            // pre-fetch headers of all parts (in one command for better performance)
2098            // @TODO: we could do this before _structure_part() call, to fetch
2099            // headers for parts on all levels
2100            if ($mime_part_headers) {
2101                $mime_part_headers = $this->conn->fetchMIMEHeaders($this->mailbox,
2102                    $this->_msg_uid, $mime_part_headers);
2103            }
2104
2105            $struct->parts = array();
2106            for ($i=0, $count=0; $i<count($part); $i++) {
2107                if (!is_array($part[$i]))
2108                    break;
2109                $tmp_part_id = $struct->mime_id ? $struct->mime_id.'.'.($i+1) : $i+1;
2110                $struct->parts[] = $this->_structure_part($part[$i], ++$count, $struct->mime_id,
2111                    $mime_part_headers[$tmp_part_id]);
2112            }
2113
2114            return $struct;
2115        }
2116
2117        /* RFC3501: BODYSTRUCTURE fields of non-multipart part
2118            0. type
2119            1. subtype
2120            2. parameters
2121            3. id
2122            4. description
2123            5. encoding
2124            6. size
2125          -- text
2126            7. lines
2127          -- message/rfc822
2128            7. envelope structure
2129            8. body structure
2130            9. lines
2131          --
2132            x. md5 (optional)
2133            x. disposition (optional)
2134            x. language (optional)
2135            x. location (optional)
2136        */
2137
2138        // regular part
2139        $struct->ctype_primary = strtolower($part[0]);
2140        $struct->ctype_secondary = strtolower($part[1]);
2141        $struct->mimetype = $struct->ctype_primary.'/'.$struct->ctype_secondary;
2142
2143        // read content type parameters
2144        if (is_array($part[2])) {
2145            $struct->ctype_parameters = array();
2146            for ($i=0; $i<count($part[2]); $i+=2)
2147                $struct->ctype_parameters[strtolower($part[2][$i])] = $part[2][$i+1];
2148
2149            if (isset($struct->ctype_parameters['charset']))
2150                $struct->charset = $struct->ctype_parameters['charset'];
2151        }
2152
2153        // #1487700: workaround for lack of charset in malformed structure
2154        if (empty($struct->charset) && !empty($mime_headers) && $mime_headers->charset) {
2155            $struct->charset = $mime_headers->charset;
2156        }
2157
2158        // read content encoding
2159        if (!empty($part[5])) {
2160            $struct->encoding = strtolower($part[5]);
2161            $struct->headers['content-transfer-encoding'] = $struct->encoding;
2162        }
2163
2164        // get part size
2165        if (!empty($part[6]))
2166            $struct->size = intval($part[6]);
2167
2168        // read part disposition
2169        $di = 8;
2170        if ($struct->ctype_primary == 'text') $di += 1;
2171        else if ($struct->mimetype == 'message/rfc822') $di += 3;
2172
2173        if (is_array($part[$di]) && count($part[$di]) == 2) {
2174            $struct->disposition = strtolower($part[$di][0]);
2175
2176            if (is_array($part[$di][1]))
2177                for ($n=0; $n<count($part[$di][1]); $n+=2)
2178                    $struct->d_parameters[strtolower($part[$di][1][$n])] = $part[$di][1][$n+1];
2179        }
2180
2181        // get message/rfc822's child-parts
2182        if (is_array($part[8]) && $di != 8) {
2183            $struct->parts = array();
2184            for ($i=0, $count=0; $i<count($part[8]); $i++) {
2185                if (!is_array($part[8][$i]))
2186                    break;
2187                $struct->parts[] = $this->_structure_part($part[8][$i], ++$count, $struct->mime_id);
2188            }
2189        }
2190
2191        // get part ID
2192        if (!empty($part[3])) {
2193            $struct->content_id = $part[3];
2194            $struct->headers['content-id'] = $part[3];
2195
2196            if (empty($struct->disposition))
2197                $struct->disposition = 'inline';
2198        }
2199
2200        // fetch message headers if message/rfc822 or named part (could contain Content-Location header)
2201        if ($struct->ctype_primary == 'message' || ($struct->ctype_parameters['name'] && !$struct->content_id)) {
2202            if (empty($mime_headers)) {
2203                $mime_headers = $this->conn->fetchPartHeader(
2204                    $this->mailbox, $this->_msg_uid, true, $struct->mime_id);
2205            }
2206
2207            if (is_string($mime_headers))
2208                $struct->headers = $this->_parse_headers($mime_headers) + $struct->headers;
2209            else if (is_object($mime_headers))
2210                $struct->headers = get_object_vars($mime_headers) + $struct->headers;
2211
2212            // get real content-type of message/rfc822
2213            if ($struct->mimetype == 'message/rfc822') {
2214                // single-part
2215                if (!is_array($part[8][0]))
2216                    $struct->real_mimetype = strtolower($part[8][0] . '/' . $part[8][1]);
2217                // multi-part
2218                else {
2219                    for ($n=0; $n<count($part[8]); $n++)
2220                        if (!is_array($part[8][$n]))
2221                            break;
2222                    $struct->real_mimetype = 'multipart/' . strtolower($part[8][$n]);
2223                }
2224            }
2225
2226            if ($struct->ctype_primary == 'message' && empty($struct->parts)) {
2227                if (is_array($part[8]) && $di != 8)
2228                    $struct->parts[] = $this->_structure_part($part[8], ++$count, $struct->mime_id);
2229            }
2230        }
2231
2232        // normalize filename property
2233        $this->_set_part_filename($struct, $mime_headers);
2234
2235        return $struct;
2236    }
2237
2238
2239    /**
2240     * Set attachment filename from message part structure
2241     *
2242     * @param  rcube_message_part $part    Part object
2243     * @param  string             $headers Part's raw headers
2244     * @access private
2245     */
2246    private function _set_part_filename(&$part, $headers=null)
2247    {
2248        if (!empty($part->d_parameters['filename']))
2249            $filename_mime = $part->d_parameters['filename'];
2250        else if (!empty($part->d_parameters['filename*']))
2251            $filename_encoded = $part->d_parameters['filename*'];
2252        else if (!empty($part->ctype_parameters['name*']))
2253            $filename_encoded = $part->ctype_parameters['name*'];
2254        // RFC2231 value continuations
2255        // TODO: this should be rewrited to support RFC2231 4.1 combinations
2256        else if (!empty($part->d_parameters['filename*0'])) {
2257            $i = 0;
2258            while (isset($part->d_parameters['filename*'.$i])) {
2259                $filename_mime .= $part->d_parameters['filename*'.$i];
2260                $i++;
2261            }
2262            // some servers (eg. dovecot-1.x) have no support for parameter value continuations
2263            // we must fetch and parse headers "manually"
2264            if ($i<2) {
2265                if (!$headers) {
2266                    $headers = $this->conn->fetchPartHeader(
2267                        $this->mailbox, $this->_msg_uid, true, $part->mime_id);
2268                }
2269                $filename_mime = '';
2270                $i = 0;
2271                while (preg_match('/filename\*'.$i.'\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
2272                    $filename_mime .= $matches[1];
2273                    $i++;
2274                }
2275            }
2276        }
2277        else if (!empty($part->d_parameters['filename*0*'])) {
2278            $i = 0;
2279            while (isset($part->d_parameters['filename*'.$i.'*'])) {
2280                $filename_encoded .= $part->d_parameters['filename*'.$i.'*'];
2281                $i++;
2282            }
2283            if ($i<2) {
2284                if (!$headers) {
2285                    $headers = $this->conn->fetchPartHeader(
2286                            $this->mailbox, $this->_msg_uid, true, $part->mime_id);
2287                }
2288                $filename_encoded = '';
2289                $i = 0; $matches = array();
2290                while (preg_match('/filename\*'.$i.'\*\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
2291                    $filename_encoded .= $matches[1];
2292                    $i++;
2293                }
2294            }
2295        }
2296        else if (!empty($part->ctype_parameters['name*0'])) {
2297            $i = 0;
2298            while (isset($part->ctype_parameters['name*'.$i])) {
2299                $filename_mime .= $part->ctype_parameters['name*'.$i];
2300                $i++;
2301            }
2302            if ($i<2) {
2303                if (!$headers) {
2304                    $headers = $this->conn->fetchPartHeader(
2305                        $this->mailbox, $this->_msg_uid, true, $part->mime_id);
2306                }
2307                $filename_mime = '';
2308                $i = 0; $matches = array();
2309                while (preg_match('/\s+name\*'.$i.'\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
2310                    $filename_mime .= $matches[1];
2311                    $i++;
2312                }
2313            }
2314        }
2315        else if (!empty($part->ctype_parameters['name*0*'])) {
2316            $i = 0;
2317            while (isset($part->ctype_parameters['name*'.$i.'*'])) {
2318                $filename_encoded .= $part->ctype_parameters['name*'.$i.'*'];
2319                $i++;
2320            }
2321            if ($i<2) {
2322                if (!$headers) {
2323                    $headers = $this->conn->fetchPartHeader(
2324                        $this->mailbox, $this->_msg_uid, true, $part->mime_id);
2325                }
2326                $filename_encoded = '';
2327                $i = 0; $matches = array();
2328                while (preg_match('/\s+name\*'.$i.'\*\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
2329                    $filename_encoded .= $matches[1];
2330                    $i++;
2331                }
2332            }
2333        }
2334        // read 'name' after rfc2231 parameters as it may contains truncated filename (from Thunderbird)
2335        else if (!empty($part->ctype_parameters['name']))
2336            $filename_mime = $part->ctype_parameters['name'];
2337        // Content-Disposition
2338        else if (!empty($part->headers['content-description']))
2339            $filename_mime = $part->headers['content-description'];
2340        else
2341            return;
2342
2343        // decode filename
2344        if (!empty($filename_mime)) {
2345            if (!empty($part->charset))
2346                $charset = $part->charset;
2347            else if (!empty($this->struct_charset))
2348                $charset = $this->struct_charset;
2349            else
2350                $charset = rc_detect_encoding($filename_mime, $this->default_charset);
2351
2352            $part->filename = rcube_imap::decode_mime_string($filename_mime, $charset);
2353        }
2354        else if (!empty($filename_encoded)) {
2355            // decode filename according to RFC 2231, Section 4
2356            if (preg_match("/^([^']*)'[^']*'(.*)$/", $filename_encoded, $fmatches)) {
2357                $filename_charset = $fmatches[1];
2358                $filename_encoded = $fmatches[2];
2359            }
2360
2361            $part->filename = rcube_charset_convert(urldecode($filename_encoded), $filename_charset);
2362        }
2363    }
2364
2365
2366    /**
2367     * Get charset name from message structure (first part)
2368     *
2369     * @param  array $structure Message structure
2370     * @return string Charset name
2371     * @access private
2372     */
2373    private function _structure_charset($structure)
2374    {
2375        while (is_array($structure)) {
2376            if (is_array($structure[2]) && $structure[2][0] == 'charset')
2377                return $structure[2][1];
2378            $structure = $structure[0];
2379        }
2380    }
2381
2382
2383    /**
2384     * Fetch message body of a specific message from the server
2385     *
2386     * @param  int                $uid    Message UID
2387     * @param  string             $part   Part number
2388     * @param  rcube_message_part $o_part Part object created by get_structure()
2389     * @param  mixed              $print  True to print part, ressource to write part contents in
2390     * @param  resource           $fp     File pointer to save the message part
2391     * @param  boolean            $skip_charset_conv Disables charset conversion
2392     *
2393     * @return string Message/part body if not printed
2394     */
2395    function &get_message_part($uid, $part=1, $o_part=NULL, $print=NULL, $fp=NULL, $skip_charset_conv=false)
2396    {
2397        // get part data if not provided
2398        if (!is_object($o_part)) {
2399            $structure = $this->conn->getStructure($this->mailbox, $uid, true);
2400            $part_data = rcube_imap_generic::getStructurePartData($structure, $part);
2401
2402            $o_part = new rcube_message_part;
2403            $o_part->ctype_primary = $part_data['type'];
2404            $o_part->encoding      = $part_data['encoding'];
2405            $o_part->charset       = $part_data['charset'];
2406            $o_part->size          = $part_data['size'];
2407        }
2408
2409        if ($o_part && $o_part->size) {
2410            $body = $this->conn->handlePartBody($this->mailbox, $uid, true,
2411                $part ? $part : 'TEXT', $o_part->encoding, $print, $fp);
2412        }
2413
2414        if ($fp || $print) {
2415            return true;
2416        }
2417
2418        // convert charset (if text or message part)
2419        if ($body && preg_match('/^(text|message)$/', $o_part->ctype_primary)) {
2420            // Remove NULL characters (#1486189)
2421            $body = str_replace("\x00", '', $body);
2422
2423           if (!$skip_charset_conv) {
2424                if (!$o_part->charset || strtoupper($o_part->charset) == 'US-ASCII') {
2425                    // try to extract charset information from HTML meta tag (#1488125)
2426                    if ($o_part->ctype_secondary == 'html' && preg_match('/<meta[^>]+charset=([a-z0-9-_]+)/i', $body, $m))
2427                        $o_part->charset = strtoupper($m[1]);
2428                    else
2429                        $o_part->charset = $this->default_charset;
2430                }
2431                $body = rcube_charset_convert($body, $o_part->charset);
2432            }
2433        }
2434
2435        return $body;
2436    }
2437
2438
2439    /**
2440     * Fetch message body of a specific message from the server
2441     *
2442     * @param  int    $uid  Message UID
2443     * @return string $part Message/part body
2444     * @see    rcube_imap::get_message_part()
2445     */
2446    function &get_body($uid, $part=1)
2447    {
2448        $headers = $this->get_headers($uid);
2449        return rcube_charset_convert($this->get_message_part($uid, $part, NULL),
2450            $headers->charset ? $headers->charset : $this->default_charset);
2451    }
2452
2453
2454    /**
2455     * Returns the whole message source as string (or saves to a file)
2456     *
2457     * @param int      $uid Message UID
2458     * @param resource $fp  File pointer to save the message
2459     *
2460     * @return string Message source string
2461     */
2462    function &get_raw_body($uid, $fp=null)
2463    {
2464        return $this->conn->handlePartBody($this->mailbox, $uid,
2465            true, null, null, false, $fp);
2466    }
2467
2468
2469    /**
2470     * Returns the message headers as string
2471     *
2472     * @param int $uid  Message UID
2473     * @return string Message headers string
2474     */
2475    function &get_raw_headers($uid)
2476    {
2477        return $this->conn->fetchPartHeader($this->mailbox, $uid, true);
2478    }
2479
2480
2481    /**
2482     * Sends the whole message source to stdout
2483     *
2484     * @param int $uid Message UID
2485     */
2486    function print_raw_body($uid)
2487    {
2488        $this->conn->handlePartBody($this->mailbox, $uid, true, NULL, NULL, true);
2489    }
2490
2491
2492    /**
2493     * Set message flag to one or several messages
2494     *
2495     * @param mixed   $uids       Message UIDs as array or comma-separated string, or '*'
2496     * @param string  $flag       Flag to set: SEEN, UNDELETED, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
2497     * @param string  $mailbox    Folder name
2498     * @param boolean $skip_cache True to skip message cache clean up
2499     *
2500     * @return boolean  Operation status
2501     */
2502    function set_flag($uids, $flag, $mailbox=null, $skip_cache=false)
2503    {
2504        if (!strlen($mailbox)) {
2505            $mailbox = $this->mailbox;
2506        }
2507
2508        $flag = strtoupper($flag);
2509        list($uids, $all_mode) = $this->_parse_uids($uids, $mailbox);
2510
2511        if (strpos($flag, 'UN') === 0)
2512            $result = $this->conn->unflag($mailbox, $uids, substr($flag, 2));
2513        else
2514            $result = $this->conn->flag($mailbox, $uids, $flag);
2515
2516        if ($result) {
2517            // reload message headers if cached
2518            // @TODO: update flags instead removing from cache
2519            if (!$skip_cache && ($mcache = $this->get_mcache_engine())) {
2520                $status = strpos($flag, 'UN') !== 0;
2521                $mflag  = preg_replace('/^UN/', '', $flag);
2522                $mcache->change_flag($mailbox, $all_mode ? null : explode(',', $uids),
2523                    $mflag, $status);
2524            }
2525
2526            // clear cached counters
2527            if ($flag == 'SEEN' || $flag == 'UNSEEN') {
2528                $this->_clear_messagecount($mailbox, 'SEEN');
2529                $this->_clear_messagecount($mailbox, 'UNSEEN');
2530            }
2531            else if ($flag == 'DELETED') {
2532                $this->_clear_messagecount($mailbox, 'DELETED');
2533            }
2534        }
2535
2536        return $result;
2537    }
2538
2539
2540    /**
2541     * Remove message flag for one or several messages
2542     *
2543     * @param mixed  $uids    Message UIDs as array or comma-separated string, or '*'
2544     * @param string $flag    Flag to unset: SEEN, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
2545     * @param string $mailbox Folder name
2546     *
2547     * @return int   Number of flagged messages, -1 on failure
2548     * @see set_flag
2549     */
2550    function unset_flag($uids, $flag, $mailbox=null)
2551    {
2552        return $this->set_flag($uids, 'UN'.$flag, $mailbox);
2553    }
2554
2555
2556    /**
2557     * Append a mail message (source) to a specific mailbox
2558     *
2559     * @param string  $mailbox Target mailbox
2560     * @param string  $message The message source string or filename
2561     * @param string  $headers Headers string if $message contains only the body
2562     * @param boolean $is_file True if $message is a filename
2563     *
2564     * @return int|bool Appended message UID or True on success, False on error
2565     */
2566    function save_message($mailbox, &$message, $headers='', $is_file=false)
2567    {
2568        if (!strlen($mailbox)) {
2569            $mailbox = $this->mailbox;
2570        }
2571
2572        // make sure mailbox exists
2573        if ($this->mailbox_exists($mailbox)) {
2574            if ($is_file)
2575                $saved = $this->conn->appendFromFile($mailbox, $message, $headers);
2576            else
2577                $saved = $this->conn->append($mailbox, $message);
2578        }
2579
2580        if ($saved) {
2581            // increase messagecount of the target mailbox
2582            $this->_set_messagecount($mailbox, 'ALL', 1);
2583        }
2584
2585        return $saved;
2586    }
2587
2588
2589    /**
2590     * Move a message from one mailbox to another
2591     *
2592     * @param mixed  $uids      Message UIDs as array or comma-separated string, or '*'
2593     * @param string $to_mbox   Target mailbox
2594     * @param string $from_mbox Source mailbox
2595     * @return boolean True on success, False on error
2596     */
2597    function move_message($uids, $to_mbox, $from_mbox='')
2598    {
2599        if (!strlen($from_mbox)) {
2600            $from_mbox = $this->mailbox;
2601        }
2602
2603        if ($to_mbox === $from_mbox) {
2604            return false;
2605        }
2606
2607        list($uids, $all_mode) = $this->_parse_uids($uids, $from_mbox);
2608
2609        // exit if no message uids are specified
2610        if (empty($uids))
2611            return false;
2612
2613        // make sure mailbox exists
2614        if ($to_mbox != 'INBOX' && !$this->mailbox_exists($to_mbox)) {
2615            if (in_array($to_mbox, $this->default_folders)) {
2616                if (!$this->create_mailbox($to_mbox, true)) {
2617                    return false;
2618                }
2619            }
2620            else {
2621                return false;
2622            }
2623        }
2624
2625        $config = rcmail::get_instance()->config;
2626        $to_trash = $to_mbox == $config->get('trash_mbox');
2627
2628        // flag messages as read before moving them
2629        if ($to_trash && $config->get('read_when_deleted')) {
2630            // don't flush cache (4th argument)
2631            $this->set_flag($uids, 'SEEN', $from_mbox, true);
2632        }
2633
2634        // move messages
2635        $moved = $this->conn->move($uids, $from_mbox, $to_mbox);
2636
2637        // send expunge command in order to have the moved message
2638        // really deleted from the source mailbox
2639        if ($moved) {
2640            $this->_expunge($from_mbox, false, $uids);
2641            $this->_clear_messagecount($from_mbox);
2642            $this->_clear_messagecount($to_mbox);
2643        }
2644        // moving failed
2645        else if ($to_trash && $config->get('delete_always', false)) {
2646            $moved = $this->delete_message($uids, $from_mbox);
2647        }
2648
2649        if ($moved) {
2650            // unset threads internal cache
2651            unset($this->icache['threads']);
2652
2653            // remove message ids from search set
2654            if ($this->search_set && $from_mbox == $this->mailbox) {
2655                // threads are too complicated to just remove messages from set
2656                if ($this->search_threads || $all_mode)
2657                    $this->refresh_search();
2658                else {
2659                    $a_uids = explode(',', $uids);
2660                    foreach ($a_uids as $uid)
2661                        $a_mids[] = $this->uid2id($uid, $from_mbox);
2662                    $this->search_set = array_diff($this->search_set, $a_mids);
2663                }
2664                unset($a_mids);
2665                unset($a_uids);
2666            }
2667
2668            // remove cached messages
2669            // @TODO: do cache update instead of clearing it
2670            $this->clear_message_cache($from_mbox, $all_mode ? null : explode(',', $uids));
2671        }
2672
2673        return $moved;
2674    }
2675
2676
2677    /**
2678     * Copy a message from one mailbox to another
2679     *
2680     * @param mixed  $uids      Message UIDs as array or comma-separated string, or '*'
2681     * @param string $to_mbox   Target mailbox
2682     * @param string $from_mbox Source mailbox
2683     * @return boolean True on success, False on error
2684     */
2685    function copy_message($uids, $to_mbox, $from_mbox='')
2686    {
2687        if (!strlen($from_mbox)) {
2688            $from_mbox = $this->mailbox;
2689        }
2690
2691        list($uids, $all_mode) = $this->_parse_uids($uids, $from_mbox);
2692
2693        // exit if no message uids are specified
2694        if (empty($uids)) {
2695            return false;
2696        }
2697
2698        // make sure mailbox exists
2699        if ($to_mbox != 'INBOX' && !$this->mailbox_exists($to_mbox)) {
2700            if (in_array($to_mbox, $this->default_folders)) {
2701                if (!$this->create_mailbox($to_mbox, true)) {
2702                    return false;
2703                }
2704            }
2705            else {
2706                return false;
2707            }
2708        }
2709
2710        // copy messages
2711        $copied = $this->conn->copy($uids, $from_mbox, $to_mbox);
2712
2713        if ($copied) {
2714            $this->_clear_messagecount($to_mbox);
2715        }
2716
2717        return $copied;
2718    }
2719
2720
2721    /**
2722     * Mark messages as deleted and expunge mailbox
2723     *
2724     * @param mixed  $uids    Message UIDs as array or comma-separated string, or '*'
2725     * @param string $mailbox Source mailbox
2726     *
2727     * @return boolean True on success, False on error
2728     */
2729    function delete_message($uids, $mailbox='')
2730    {
2731        if (!strlen($mailbox)) {
2732            $mailbox = $this->mailbox;
2733        }
2734
2735        list($uids, $all_mode) = $this->_parse_uids($uids, $mailbox);
2736
2737        // exit if no message uids are specified
2738        if (empty($uids))
2739            return false;
2740
2741        $deleted = $this->conn->delete($mailbox, $uids);
2742
2743        if ($deleted) {
2744            // send expunge command in order to have the deleted message
2745            // really deleted from the mailbox
2746            $this->_expunge($mailbox, false, $uids);
2747            $this->_clear_messagecount($mailbox);
2748            unset($this->uid_id_map[$mailbox]);
2749
2750            // unset threads internal cache
2751            unset($this->icache['threads']);
2752
2753            // remove message ids from search set
2754            if ($this->search_set && $mailbox == $this->mailbox) {
2755                // threads are too complicated to just remove messages from set
2756                if ($this->search_threads || $all_mode)
2757                    $this->refresh_search();
2758                else {
2759                    $a_uids = explode(',', $uids);
2760                    foreach ($a_uids as $uid)
2761                        $a_mids[] = $this->uid2id($uid, $mailbox);
2762                    $this->search_set = array_diff($this->search_set, $a_mids);
2763                    unset($a_uids);
2764                    unset($a_mids);
2765                }
2766            }
2767
2768            // remove cached messages
2769            $this->clear_message_cache($mailbox, $all_mode ? null : explode(',', $uids));
2770        }
2771
2772        return $deleted;
2773    }
2774
2775
2776    /**
2777     * Clear all messages in a specific mailbox
2778     *
2779     * @param string $mailbox Mailbox name
2780     *
2781     * @return int Above 0 on success
2782     */
2783    function clear_mailbox($mailbox=null)
2784    {
2785        if (!strlen($mailbox)) {
2786            $mailbox = $this->mailbox;
2787        }
2788
2789        // SELECT will set messages count for clearFolder()
2790        if ($this->conn->select($mailbox)) {
2791            $cleared = $this->conn->clearFolder($mailbox);
2792        }
2793
2794        // make sure the cache is cleared as well
2795        if ($cleared) {
2796            $this->clear_message_cache($mailbox);
2797            $a_mailbox_cache = $this->get_cache('messagecount');
2798            unset($a_mailbox_cache[$mailbox]);
2799            $this->update_cache('messagecount', $a_mailbox_cache);
2800        }
2801
2802        return $cleared;
2803    }
2804
2805
2806    /**
2807     * Send IMAP expunge command and clear cache
2808     *
2809     * @param string  $mailbox     Mailbox name
2810     * @param boolean $clear_cache False if cache should not be cleared
2811     *
2812     * @return boolean True on success
2813     */
2814    function expunge($mailbox='', $clear_cache=true)
2815    {
2816        if (!strlen($mailbox)) {
2817            $mailbox = $this->mailbox;
2818        }
2819
2820        return $this->_expunge($mailbox, $clear_cache);
2821    }
2822
2823
2824    /**
2825     * Send IMAP expunge command and clear cache
2826     *
2827     * @param string  $mailbox     Mailbox name
2828     * @param boolean $clear_cache False if cache should not be cleared
2829     * @param mixed   $uids        Message UIDs as array or comma-separated string, or '*'
2830     * @return boolean True on success
2831     * @access private
2832     * @see rcube_imap::expunge()
2833     */
2834    private function _expunge($mailbox, $clear_cache=true, $uids=NULL)
2835    {
2836        if ($uids && $this->get_capability('UIDPLUS'))
2837            list($uids, $all_mode) = $this->_parse_uids($uids, $mailbox);
2838        else
2839            $uids = null;
2840
2841        // force mailbox selection and check if mailbox is writeable
2842        // to prevent a situation when CLOSE is executed on closed
2843        // or EXPUNGE on read-only mailbox
2844        $result = $this->conn->select($mailbox);
2845        if (!$result) {
2846            return false;
2847        }
2848        if (!$this->conn->data['READ-WRITE']) {
2849            $this->conn->setError(rcube_imap_generic::ERROR_READONLY, "Mailbox is read-only");
2850            return false;
2851        }
2852
2853        // CLOSE(+SELECT) should be faster than EXPUNGE
2854        if (empty($uids) || $all_mode)
2855            $result = $this->conn->close();
2856        else
2857            $result = $this->conn->expunge($mailbox, $uids);
2858
2859        if ($result && $clear_cache) {
2860            $this->clear_message_cache($mailbox, $all_mode ? null : explode(',', $uids));
2861            $this->_clear_messagecount($mailbox);
2862        }
2863
2864        return $result;
2865    }
2866
2867
2868    /**
2869     * Parse message UIDs input
2870     *
2871     * @param mixed  $uids    UIDs array or comma-separated list or '*' or '1:*'
2872     * @param string $mailbox Mailbox name
2873     * @return array Two elements array with UIDs converted to list and ALL flag
2874     * @access private
2875     */
2876    private function _parse_uids($uids, $mailbox)
2877    {
2878        if ($uids === '*' || $uids === '1:*') {
2879            if (empty($this->search_set)) {
2880                $uids = '1:*';
2881                $all = true;
2882            }
2883            // get UIDs from current search set
2884            // @TODO: skip fetchUIDs() and work with IDs instead of UIDs (?)
2885            else {
2886                if ($this->search_threads)
2887                    $uids = $this->conn->fetchUIDs($mailbox, array_keys($this->search_set['depth']));
2888                else
2889                    $uids = $this->conn->fetchUIDs($mailbox, $this->search_set);
2890
2891                // save ID-to-UID mapping in local cache
2892                if (is_array($uids))
2893                    foreach ($uids as $id => $uid)
2894                        $this->uid_id_map[$mailbox][$uid] = $id;
2895
2896                $uids = join(',', $uids);
2897            }
2898        }
2899        else {
2900            if (is_array($uids))
2901                $uids = join(',', $uids);
2902
2903            if (preg_match('/[^0-9,]/', $uids))
2904                $uids = '';
2905        }
2906
2907        return array($uids, (bool) $all);
2908    }
2909
2910
2911    /**
2912     * Translate UID to message ID
2913     *
2914     * @param int    $uid     Message UID
2915     * @param string $mailbox Mailbox name
2916     *
2917     * @return int   Message ID
2918     */
2919    function get_id($uid, $mailbox=null)
2920    {
2921        if (!strlen($mailbox)) {
2922            $mailbox = $this->mailbox;
2923        }
2924
2925        return $this->uid2id($uid, $mailbox);
2926    }
2927
2928
2929    /**
2930     * Translate message number to UID
2931     *
2932     * @param int    $id      Message ID
2933     * @param string $mailbox Mailbox name
2934     *
2935     * @return int   Message UID
2936     */
2937    function get_uid($id, $mailbox=null)
2938    {
2939        if (!strlen($mailbox)) {
2940            $mailbox = $this->mailbox;
2941        }
2942
2943        return $this->id2uid($id, $mailbox);
2944    }
2945
2946
2947
2948    /* --------------------------------
2949     *        folder managment
2950     * --------------------------------*/
2951
2952    /**
2953     * Public method for listing subscribed folders
2954     *
2955     * @param   string  $root      Optional root folder
2956     * @param   string  $name      Optional name pattern
2957     * @param   string  $filter    Optional filter
2958     * @param   string  $rights    Optional ACL requirements
2959     * @param   bool    $skip_sort Enable to return unsorted list (for better performance)
2960     *
2961     * @return  array   List of folders
2962     * @access  public
2963     */
2964    function list_mailboxes($root='', $name='*', $filter=null, $rights=null, $skip_sort=false)
2965    {
2966        $cache_key = $root.':'.$name;
2967        if (!empty($filter)) {
2968            $cache_key .= ':'.(is_string($filter) ? $filter : serialize($filter));
2969        }
2970        $cache_key .= ':'.$rights;
2971        $cache_key = 'mailboxes.'.md5($cache_key);
2972
2973        // get cached folder list
2974        $a_mboxes = $this->get_cache($cache_key);
2975        if (is_array($a_mboxes)) {
2976            return $a_mboxes;
2977        }
2978
2979        $a_mboxes = $this->_list_mailboxes($root, $name, $filter, $rights);
2980
2981        if (!is_array($a_mboxes)) {
2982            return array();
2983        }
2984
2985        // filter folders list according to rights requirements
2986        if ($rights && $this->get_capability('ACL')) {
2987            $a_mboxes = $this->filter_rights($a_mboxes, $rights);
2988        }
2989
2990        // INBOX should always be available
2991        if ((!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)) {
2992            array_unshift($a_mboxes, 'INBOX');
2993        }
2994
2995        // sort mailboxes (always sort for cache)
2996        if (!$skip_sort || $this->cache) {
2997            $a_mboxes = $this->_sort_mailbox_list($a_mboxes);
2998        }
2999
3000        // write mailboxlist to cache
3001        $this->update_cache($cache_key, $a_mboxes);
3002
3003        return $a_mboxes;
3004    }
3005
3006
3007    /**
3008     * Private method for mailbox listing (LSUB)
3009     *
3010     * @param   string  $root   Optional root folder
3011     * @param   string  $name   Optional name pattern
3012     * @param   mixed   $filter Optional filter
3013     * @param   string  $rights Optional ACL requirements
3014     *
3015     * @return  array   List of subscribed folders
3016     * @see     rcube_imap::list_mailboxes()
3017     * @access  private
3018     */
3019    private function _list_mailboxes($root='', $name='*', $filter=null, $rights=null)
3020    {
3021        $a_defaults = $a_out = array();
3022
3023        // Give plugins a chance to provide a list of mailboxes
3024        $data = rcmail::get_instance()->plugins->exec_hook('mailboxes_list',
3025            array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LSUB'));
3026
3027        if (isset($data['folders'])) {
3028            $a_folders = $data['folders'];
3029        }
3030        else if (!$this->conn->connected()) {
3031           return null;
3032        }
3033        else {
3034            // Server supports LIST-EXTENDED, we can use selection options
3035            $config = rcmail::get_instance()->config;
3036            // #1486225: Some dovecot versions returns wrong result using LIST-EXTENDED
3037            if (!$config->get('imap_force_lsub') && $this->get_capability('LIST-EXTENDED')) {
3038                // This will also set mailbox options, LSUB doesn't do that
3039                $a_folders = $this->conn->listMailboxes($root, $name,
3040                    NULL, array('SUBSCRIBED'));
3041
3042                // unsubscribe non-existent folders, remove from the list
3043                if (is_array($a_folders) && $name == '*') {
3044                    foreach ($a_folders as $idx => $folder) {
3045                        if ($this->conn->data['LIST'] && ($opts = $this->conn->data['LIST'][$folder])
3046                            && in_array('\\NonExistent', $opts)
3047                        ) {
3048                            $this->conn->unsubscribe($folder);
3049                            unset($a_folders[$idx]);
3050                        }
3051                    }
3052                }
3053            }
3054            // retrieve list of folders from IMAP server using LSUB
3055            else {
3056                $a_folders = $this->conn->listSubscribed($root, $name);
3057
3058                // unsubscribe non-existent folders, remove from the list
3059                if (is_array($a_folders) && $name == '*') {
3060                    foreach ($a_folders as $idx => $folder) {
3061                        if ($this->conn->data['LIST'] && ($opts = $this->conn->data['LIST'][$folder])
3062                            && in_array('\\Noselect', $opts)
3063                        ) {
3064                            // Some servers returns \Noselect for existing folders
3065                            if (!$this->mailbox_exists($folder)) {
3066                                $this->conn->unsubscribe($folder);
3067                                unset($a_folders[$idx]);
3068                            }
3069                        }
3070                    }
3071                }
3072            }
3073        }
3074
3075        if (!is_array($a_folders) || !sizeof($a_folders)) {
3076            $a_folders = array();
3077        }
3078
3079        return $a_folders;
3080    }
3081
3082
3083    /**
3084     * Get a list of all folders available on the IMAP server
3085     *
3086     * @param string  $root      IMAP root dir
3087     * @param string  $name      Optional name pattern
3088     * @param mixed   $filter    Optional filter
3089     * @param string  $rights    Optional ACL requirements
3090     * @param bool    $skip_sort Enable to return unsorted list (for better performance)
3091     *
3092     * @return array Indexed array with folder names
3093     */
3094    function list_unsubscribed($root='', $name='*', $filter=null, $rights=null, $skip_sort=false)
3095    {
3096        $cache_key = $root.':'.$name;
3097        if (!empty($filter)) {
3098            $cache_key .= ':'.(is_string($filter) ? $filter : serialize($filter));
3099        }
3100        $cache_key .= ':'.$rights;
3101        $cache_key = 'mailboxes.list.'.md5($cache_key);
3102
3103        // get cached folder list
3104        $a_mboxes = $this->get_cache($cache_key);
3105        if (is_array($a_mboxes)) {
3106            return $a_mboxes;
3107        }
3108
3109        // Give plugins a chance to provide a list of mailboxes
3110        $data = rcmail::get_instance()->plugins->exec_hook('mailboxes_list',
3111            array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LIST'));
3112
3113        if (isset($data['folders'])) {
3114            $a_mboxes = $data['folders'];
3115        }
3116        else {
3117            // retrieve list of folders from IMAP server
3118            $a_mboxes = $this->_list_unsubscribed($root, $name);
3119        }
3120
3121        if (!is_array($a_mboxes)) {
3122            $a_mboxes = array();
3123        }
3124
3125        // INBOX should always be available
3126        if ((!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)) {
3127            array_unshift($a_mboxes, 'INBOX');
3128        }
3129
3130        // cache folder attributes
3131        if ($root == '' && $name == '*' && empty($filter)) {
3132            $this->update_cache('mailboxes.attributes', $this->conn->data['LIST']);
3133        }
3134
3135        // filter folders list according to rights requirements
3136        if ($rights && $this->get_capability('ACL')) {
3137            $a_folders = $this->filter_rights($a_folders, $rights);
3138        }
3139
3140        // filter folders and sort them
3141        if (!$skip_sort) {
3142            $a_mboxes = $this->_sort_mailbox_list($a_mboxes);
3143        }
3144
3145        // write mailboxlist to cache
3146        $this->update_cache($cache_key, $a_mboxes);
3147
3148        return $a_mboxes;
3149    }
3150
3151
3152    /**
3153     * Private method for mailbox listing (LIST)
3154     *
3155     * @param   string  $root   Optional root folder
3156     * @param   string  $name   Optional name pattern
3157     *
3158     * @return  array   List of folders
3159     * @see     rcube_imap::list_unsubscribed()
3160     */
3161    private function _list_unsubscribed($root='', $name='*')
3162    {
3163        $result = $this->conn->listMailboxes($root, $name);
3164
3165        if (!is_array($result)) {
3166            return array();
3167        }
3168
3169        // #1486796: some server configurations doesn't
3170        // return folders in all namespaces, we'll try to detect that situation
3171        // and ask for these namespaces separately
3172        if ($root == '' && $name == '*') {
3173            $delim     = $this->get_hierarchy_delimiter();
3174            $namespace = $this->get_namespace();
3175            $search    = array();
3176
3177            // build list of namespace prefixes
3178            foreach ((array)$namespace as $ns) {
3179                if (is_array($ns)) {
3180                    foreach ($ns as $ns_data) {
3181                        if (strlen($ns_data[0])) {
3182                            $search[] = $ns_data[0];
3183                        }
3184                    }
3185                }
3186            }
3187
3188            if (!empty($search)) {
3189                // go through all folders detecting namespace usage
3190                foreach ($result as $folder) {
3191                    foreach ($search as $idx => $prefix) {
3192                        if (strpos($folder, $prefix) === 0) {
3193                            unset($search[$idx]);
3194                        }
3195                    }
3196                    if (empty($search)) {
3197                        break;
3198                    }
3199                }
3200
3201                // get folders in hidden namespaces and add to the result
3202                foreach ($search as $prefix) {
3203                    $list = $this->conn->listMailboxes($prefix, $name);
3204
3205                    if (!empty($list)) {
3206                        $result = array_merge($result, $list);
3207                    }
3208                }
3209            }
3210        }
3211
3212        return $result;
3213    }
3214
3215
3216    /**
3217     * Filter the given list of folders according to access rights
3218     */
3219    private function filter_rights($a_folders, $rights)
3220    {
3221        $regex = '/('.$rights.')/';
3222        foreach ($a_folders as $idx => $folder) {
3223            $myrights = join('', (array)$this->my_rights($folder));
3224            if ($myrights !== null && !preg_match($regex, $myrights))
3225                unset($a_folders[$idx]);
3226        }
3227
3228        return $a_folders;
3229    }
3230
3231
3232    /**
3233     * Get mailbox quota information
3234     * added by Nuny
3235     *
3236     * @return mixed Quota info or False if not supported
3237     */
3238    function get_quota()
3239    {
3240        if ($this->get_capability('QUOTA'))
3241            return $this->conn->getQuota();
3242
3243        return false;
3244    }
3245
3246
3247    /**
3248     * Get mailbox size (size of all messages in a mailbox)
3249     *
3250     * @param string $mailbox Mailbox name
3251     *
3252     * @return int Mailbox size in bytes, False on error
3253     */
3254    function get_mailbox_size($mailbox)
3255    {
3256        // @TODO: could we try to use QUOTA here?
3257        $result = $this->conn->fetchHeaderIndex($mailbox, '1:*', 'SIZE', false);
3258
3259        if (is_array($result))
3260            $result = array_sum($result);
3261
3262        return $result;
3263    }
3264
3265
3266    /**
3267     * Subscribe to a specific mailbox(es)
3268     *
3269     * @param array $a_mboxes Mailbox name(s)
3270     * @return boolean True on success
3271     */
3272    function subscribe($a_mboxes)
3273    {
3274        if (!is_array($a_mboxes))
3275            $a_mboxes = array($a_mboxes);
3276
3277        // let this common function do the main work
3278        return $this->_change_subscription($a_mboxes, 'subscribe');
3279    }
3280
3281
3282    /**
3283     * Unsubscribe mailboxes
3284     *
3285     * @param array $a_mboxes Mailbox name(s)
3286     * @return boolean True on success
3287     */
3288    function unsubscribe($a_mboxes)
3289    {
3290        if (!is_array($a_mboxes))
3291            $a_mboxes = array($a_mboxes);
3292
3293        // let this common function do the main work
3294        return $this->_change_subscription($a_mboxes, 'unsubscribe');
3295    }
3296
3297
3298    /**
3299     * Create a new mailbox on the server and register it in local cache
3300     *
3301     * @param string  $mailbox   New mailbox name
3302     * @param boolean $subscribe True if the new mailbox should be subscribed
3303     *
3304     * @return boolean True on success
3305     */
3306    function create_mailbox($mailbox, $subscribe=false)
3307    {
3308        $result = $this->conn->createFolder($mailbox);
3309
3310        // try to subscribe it
3311        if ($result) {
3312            // clear cache
3313            $this->clear_cache('mailboxes', true);
3314
3315            if ($subscribe)
3316                $this->subscribe($mailbox);
3317        }
3318
3319        return $result;
3320    }
3321
3322
3323    /**
3324     * Set a new name to an existing mailbox
3325     *
3326     * @param string $mailbox  Mailbox to rename
3327     * @param string $new_name New mailbox name
3328     *
3329     * @return boolean True on success
3330     */
3331    function rename_mailbox($mailbox, $new_name)
3332    {
3333        if (!strlen($new_name)) {
3334            return false;
3335        }
3336
3337        $delm = $this->get_hierarchy_delimiter();
3338
3339        // get list of subscribed folders
3340        if ((strpos($mailbox, '%') === false) && (strpos($mailbox, '*') === false)) {
3341            $a_subscribed = $this->_list_mailboxes('', $mailbox . $delm . '*');
3342            $subscribed   = $this->mailbox_exists($mailbox, true);
3343        }
3344        else {
3345            $a_subscribed = $this->_list_mailboxes();
3346            $subscribed   = in_array($mailbox, $a_subscribed);
3347        }
3348
3349        $result = $this->conn->renameFolder($mailbox, $new_name);
3350
3351        if ($result) {
3352            // unsubscribe the old folder, subscribe the new one
3353            if ($subscribed) {
3354                $this->conn->unsubscribe($mailbox);
3355                $this->conn->subscribe($new_name);
3356            }
3357
3358            // check if mailbox children are subscribed
3359            foreach ($a_subscribed as $c_subscribed) {
3360                if (preg_match('/^'.preg_quote($mailbox.$delm, '/').'/', $c_subscribed)) {
3361                    $this->conn->unsubscribe($c_subscribed);
3362                    $this->conn->subscribe(preg_replace('/^'.preg_quote($mailbox, '/').'/',
3363                        $new_name, $c_subscribed));
3364
3365                    // clear cache
3366                    $this->clear_message_cache($c_subscribed);
3367                }
3368            }
3369
3370            // clear cache
3371            $this->clear_message_cache($mailbox);
3372            $this->clear_cache('mailboxes', true);
3373        }
3374
3375        return $result;
3376    }
3377
3378
3379    /**
3380     * Remove mailbox from server
3381     *
3382     * @param string $mailbox Mailbox name
3383     *
3384     * @return boolean True on success
3385     */
3386    function delete_mailbox($mailbox)
3387    {
3388        $delm = $this->get_hierarchy_delimiter();
3389
3390        // get list of folders
3391        if ((strpos($mailbox, '%') === false) && (strpos($mailbox, '*') === false))
3392            $sub_mboxes = $this->list_unsubscribed('', $mailbox . $delm . '*');
3393        else
3394            $sub_mboxes = $this->list_unsubscribed();
3395
3396        // send delete command to server
3397        $result = $this->conn->deleteFolder($mailbox);
3398
3399        if ($result) {
3400            // unsubscribe mailbox
3401            $this->conn->unsubscribe($mailbox);
3402
3403            foreach ($sub_mboxes as $c_mbox) {
3404                if (preg_match('/^'.preg_quote($mailbox.$delm, '/').'/', $c_mbox)) {
3405                    $this->conn->unsubscribe($c_mbox);
3406                    if ($this->conn->deleteFolder($c_mbox)) {
3407                            $this->clear_message_cache($c_mbox);
3408                    }
3409                }
3410            }
3411
3412            // clear mailbox-related cache
3413            $this->clear_message_cache($mailbox);
3414            $this->clear_cache('mailboxes', true);
3415        }
3416
3417        return $result;
3418    }
3419
3420
3421    /**
3422     * Create all folders specified as default
3423     */
3424    function create_default_folders()
3425    {
3426        // create default folders if they do not exist
3427        foreach ($this->default_folders as $folder) {
3428            if (!$this->mailbox_exists($folder))
3429                $this->create_mailbox($folder, true);
3430            else if (!$this->mailbox_exists($folder, true))
3431                $this->subscribe($folder);
3432        }
3433    }
3434
3435
3436    /**
3437     * Checks if folder exists and is subscribed
3438     *
3439     * @param string   $mailbox      Folder name
3440     * @param boolean  $subscription Enable subscription checking
3441     *
3442     * @return boolean TRUE or FALSE
3443     */
3444    function mailbox_exists($mailbox, $subscription=false)
3445    {
3446        if ($mailbox == 'INBOX') {
3447            return true;
3448        }
3449
3450        $key  = $subscription ? 'subscribed' : 'existing';
3451
3452        if (is_array($this->icache[$key]) && in_array($mailbox, $this->icache[$key]))
3453            return true;
3454
3455        if ($subscription) {
3456            $a_folders = $this->conn->listSubscribed('', $mailbox);
3457        }
3458        else {
3459            $a_folders = $this->conn->listMailboxes('', $mailbox);
3460        }
3461
3462        if (is_array($a_folders) && in_array($mailbox, $a_folders)) {
3463            $this->icache[$key][] = $mailbox;
3464            return true;
3465        }
3466
3467        return false;
3468    }
3469
3470
3471    /**
3472     * Returns the namespace where the folder is in
3473     *
3474     * @param string $mailbox Folder name
3475     *
3476     * @return string One of 'personal', 'other' or 'shared'
3477     * @access public
3478     */
3479    function mailbox_namespace($mailbox)
3480    {
3481        if ($mailbox == 'INBOX') {
3482            return 'personal';
3483        }
3484
3485        foreach ($this->namespace as $type => $namespace) {
3486            if (is_array($namespace)) {
3487                foreach ($namespace as $ns) {
3488                    if ($len = strlen($ns[0])) {
3489                        if (($len > 1 && $mailbox == substr($ns[0], 0, -1))
3490                            || strpos($mailbox, $ns[0]) === 0
3491                        ) {
3492                            return $type;
3493                        }
3494                    }
3495                }
3496            }
3497        }
3498
3499        return 'personal';
3500    }
3501
3502
3503    /**
3504     * Modify folder name according to namespace.
3505     * For output it removes prefix of the personal namespace if it's possible.
3506     * For input it adds the prefix. Use it before creating a folder in root
3507     * of the folders tree.
3508     *
3509     * @param string $mailbox Folder name
3510     * @param string $mode    Mode name (out/in)
3511     *
3512     * @return string Folder name
3513     */
3514    function mod_mailbox($mailbox, $mode = 'out')
3515    {
3516        if (!strlen($mailbox)) {
3517            return $mailbox;
3518        }
3519
3520        $prefix     = $this->namespace['prefix']; // see set_env()
3521        $prefix_len = strlen($prefix);
3522
3523        if (!$prefix_len) {
3524            return $mailbox;
3525        }
3526
3527        // remove prefix for output
3528        if ($mode == 'out') {
3529            if (substr($mailbox, 0, $prefix_len) === $prefix) {
3530                return substr($mailbox, $prefix_len);
3531            }
3532        }
3533        // add prefix for input (e.g. folder creation)
3534        else {
3535            return $prefix . $mailbox;
3536        }
3537
3538        return $mailbox;
3539    }
3540
3541
3542    /**
3543     * Gets folder attributes from LIST response, e.g. \Noselect, \Noinferiors
3544     *
3545     * @param string $mailbox Folder name
3546     * @param bool   $force   Set to True if attributes should be refreshed
3547     *
3548     * @return array Options list
3549     */
3550    function mailbox_attributes($mailbox, $force=false)
3551    {
3552        // get attributes directly from LIST command
3553        if (!empty($this->conn->data['LIST']) && is_array($this->conn->data['LIST'][$mailbox])) {
3554            $opts = $this->conn->data['LIST'][$mailbox];
3555        }
3556        // get cached folder attributes
3557        else if (!$force) {
3558            $opts = $this->get_cache('mailboxes.attributes');
3559            $opts = $opts[$mailbox];
3560        }
3561
3562        if (!is_array($opts)) {
3563            $this->conn->listMailboxes('', $mailbox);
3564            $opts = $this->conn->data['LIST'][$mailbox];
3565        }
3566
3567        return is_array($opts) ? $opts : array();
3568    }
3569
3570
3571    /**
3572     * Gets connection (and current mailbox) data: UIDVALIDITY, EXISTS, RECENT,
3573     * PERMANENTFLAGS, UIDNEXT, UNSEEN
3574     *
3575     * @param string $mailbox Folder name
3576     *
3577     * @return array Data
3578     */
3579    function mailbox_data($mailbox)
3580    {
3581        if (!strlen($mailbox))
3582            $mailbox = $this->mailbox !== null ? $this->mailbox : 'INBOX';
3583
3584        if ($this->conn->selected != $mailbox) {
3585            if ($this->conn->select($mailbox))
3586                $this->mailbox = $mailbox;
3587            else
3588                return null;
3589        }
3590
3591        $data = $this->conn->data;
3592
3593        // add (E)SEARCH result for ALL UNDELETED query
3594        if (!empty($this->icache['undeleted_idx']) && $this->icache['undeleted_idx'][0] == $mailbox) {
3595            $data['ALL_UNDELETED']   = $this->icache['undeleted_idx'][1];
3596            $data['COUNT_UNDELETED'] = $this->icache['undeleted_idx'][2];
3597        }
3598
3599        return $data;
3600    }
3601
3602
3603    /**
3604     * Returns extended information about the folder
3605     *
3606     * @param string $mailbox Folder name
3607     *
3608     * @return array Data
3609     */
3610    function mailbox_info($mailbox)
3611    {
3612        if ($this->icache['options'] && $this->icache['options']['name'] == $mailbox) {
3613            return $this->icache['options'];
3614        }
3615
3616        $acl       = $this->get_capability('ACL');
3617        $namespace = $this->get_namespace();
3618        $options   = array();
3619
3620        // check if the folder is a namespace prefix
3621        if (!empty($namespace)) {
3622            $mbox = $mailbox . $this->delimiter;
3623            foreach ($namespace as $ns) {
3624                if (!empty($ns)) {
3625                    foreach ($ns as $item) {
3626                        if ($item[0] === $mbox) {
3627                            $options['is_root'] = true;
3628                            break 2;
3629                        }
3630                    }
3631                }
3632            }
3633        }
3634        // check if the folder is other user virtual-root
3635        if (!$options['is_root'] && !empty($namespace) && !empty($namespace['other'])) {
3636            $parts = explode($this->delimiter, $mailbox);
3637            if (count($parts) == 2) {
3638                $mbox = $parts[0] . $this->delimiter;
3639                foreach ($namespace['other'] as $item) {
3640                    if ($item[0] === $mbox) {
3641                        $options['is_root'] = true;
3642                        break;
3643                    }
3644                }
3645            }
3646        }
3647
3648        $options['name']       = $mailbox;
3649        $options['attributes'] = $this->mailbox_attributes($mailbox, true);
3650        $options['namespace']  = $this->mailbox_namespace($mailbox);
3651        $options['rights']     = $acl && !$options['is_root'] ? (array)$this->my_rights($mailbox) : array();
3652        $options['special']    = in_array($mailbox, $this->default_folders);
3653
3654        // Set 'noselect' and 'norename' flags
3655        if (is_array($options['attributes'])) {
3656            foreach ($options['attributes'] as $attrib) {
3657                $attrib = strtolower($attrib);
3658                if ($attrib == '\noselect' || $attrib == '\nonexistent') {
3659                    $options['noselect'] = true;
3660                }
3661            }
3662        }
3663        else {
3664            $options['noselect'] = true;
3665        }
3666
3667        if (!empty($options['rights'])) {
3668            $options['norename'] = !in_array('x', $options['rights']) && !in_array('d', $options['rights']);
3669
3670            if (!$options['noselect']) {
3671                $options['noselect'] = !in_array('r', $options['rights']);
3672            }
3673        }
3674        else {
3675            $options['norename'] = $options['is_root'] || $options['namespace'] != 'personal';
3676        }
3677
3678        $this->icache['options'] = $options;
3679
3680        return $options;
3681    }
3682
3683
3684    /**
3685     * Synchronizes messages cache.
3686     *
3687     * @param string $mailbox Folder name
3688     */
3689    public function mailbox_sync($mailbox)
3690    {
3691        if ($mcache = $this->get_mcache_engine()) {
3692            $mcache->synchronize($mailbox);
3693        }
3694    }
3695
3696
3697    /**
3698     * Get message header names for rcube_imap_generic::fetchHeader(s)
3699     *
3700     * @return string Space-separated list of header names
3701     */
3702    private function get_fetch_headers()
3703    {
3704        $headers = explode(' ', $this->fetch_add_headers);
3705        $headers = array_map('strtoupper', $headers);
3706
3707        if ($this->messages_caching || $this->get_all_headers)
3708            $headers = array_merge($headers, $this->all_headers);
3709
3710        return implode(' ', array_unique($headers));
3711    }
3712
3713
3714    /* -----------------------------------------
3715     *   ACL and METADATA/ANNOTATEMORE methods
3716     * ----------------------------------------*/
3717
3718    /**
3719     * Changes the ACL on the specified mailbox (SETACL)
3720     *
3721     * @param string $mailbox Mailbox name
3722     * @param string $user    User name
3723     * @param string $acl     ACL string
3724     *
3725     * @return boolean True on success, False on failure
3726     *
3727     * @access public
3728     * @since 0.5-beta
3729     */
3730    function set_acl($mailbox, $user, $acl)
3731    {
3732        if ($this->get_capability('ACL'))
3733            return $this->conn->setACL($mailbox, $user, $acl);
3734
3735        return false;
3736    }
3737
3738
3739    /**
3740     * Removes any <identifier,rights> pair for the
3741     * specified user from the ACL for the specified
3742     * mailbox (DELETEACL)
3743     *
3744     * @param string $mailbox Mailbox name
3745     * @param string $user    User name
3746     *
3747     * @return boolean True on success, False on failure
3748     *
3749     * @access public
3750     * @since 0.5-beta
3751     */
3752    function delete_acl($mailbox, $user)
3753    {
3754        if ($this->get_capability('ACL'))
3755            return $this->conn->deleteACL($mailbox, $user);
3756
3757        return false;
3758    }
3759
3760
3761    /**
3762     * Returns the access control list for mailbox (GETACL)
3763     *
3764     * @param string $mailbox Mailbox name
3765     *
3766     * @return array User-rights array on success, NULL on error
3767     * @access public
3768     * @since 0.5-beta
3769     */
3770    function get_acl($mailbox)
3771    {
3772        if ($this->get_capability('ACL'))
3773            return $this->conn->getACL($mailbox);
3774
3775        return NULL;
3776    }
3777
3778
3779    /**
3780     * Returns information about what rights can be granted to the
3781     * user (identifier) in the ACL for the mailbox (LISTRIGHTS)
3782     *
3783     * @param string $mailbox Mailbox name
3784     * @param string $user    User name
3785     *
3786     * @return array List of user rights
3787     * @access public
3788     * @since 0.5-beta
3789     */
3790    function list_rights($mailbox, $user)
3791    {
3792        if ($this->get_capability('ACL'))
3793            return $this->conn->listRights($mailbox, $user);
3794
3795        return NULL;
3796    }
3797
3798
3799    /**
3800     * Returns the set of rights that the current user has to
3801     * mailbox (MYRIGHTS)
3802     *
3803     * @param string $mailbox Mailbox name
3804     *
3805     * @return array MYRIGHTS response on success, NULL on error
3806     * @access public
3807     * @since 0.5-beta
3808     */
3809    function my_rights($mailbox)
3810    {
3811        if ($this->get_capability('ACL'))
3812            return $this->conn->myRights($mailbox);
3813
3814        return NULL;
3815    }
3816
3817
3818    /**
3819     * Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
3820     *
3821     * @param string $mailbox Mailbox name (empty for server metadata)
3822     * @param array  $entries Entry-value array (use NULL value as NIL)
3823     *
3824     * @return boolean True on success, False on failure
3825     * @access public
3826     * @since 0.5-beta
3827     */
3828    function set_metadata($mailbox, $entries)
3829    {
3830        if ($this->get_capability('METADATA') ||
3831            (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
3832        ) {
3833            return $this->conn->setMetadata($mailbox, $entries);
3834        }
3835        else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
3836            foreach ((array)$entries as $entry => $value) {
3837                list($ent, $attr) = $this->md2annotate($entry);
3838                $entries[$entry] = array($ent, $attr, $value);
3839            }
3840            return $this->conn->setAnnotation($mailbox, $entries);
3841        }
3842
3843        return false;
3844    }
3845
3846
3847    /**
3848     * Unsets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
3849     *
3850     * @param string $mailbox Mailbox name (empty for server metadata)
3851     * @param array  $entries Entry names array
3852     *
3853     * @return boolean True on success, False on failure
3854     *
3855     * @access public
3856     * @since 0.5-beta
3857     */
3858    function delete_metadata($mailbox, $entries)
3859    {
3860        if ($this->get_capability('METADATA') || 
3861            (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
3862        ) {
3863            return $this->conn->deleteMetadata($mailbox, $entries);
3864        }
3865        else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
3866            foreach ((array)$entries as $idx => $entry) {
3867                list($ent, $attr) = $this->md2annotate($entry);
3868                $entries[$idx] = array($ent, $attr, NULL);
3869            }
3870            return $this->conn->setAnnotation($mailbox, $entries);
3871        }
3872
3873        return false;
3874    }
3875
3876
3877    /**
3878     * Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION)
3879     *
3880     * @param string $mailbox Mailbox name (empty for server metadata)
3881     * @param array  $entries Entries
3882     * @param array  $options Command options (with MAXSIZE and DEPTH keys)
3883     *
3884     * @return array Metadata entry-value hash array on success, NULL on error
3885     *
3886     * @access public
3887     * @since 0.5-beta
3888     */
3889    function get_metadata($mailbox, $entries, $options=array())
3890    {
3891        if ($this->get_capability('METADATA') || 
3892            (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
3893        ) {
3894            return $this->conn->getMetadata($mailbox, $entries, $options);
3895        }
3896        else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
3897            $queries = array();
3898            $res     = array();
3899
3900            // Convert entry names
3901            foreach ((array)$entries as $entry) {
3902                list($ent, $attr) = $this->md2annotate($entry);
3903                $queries[$attr][] = $ent;
3904            }
3905
3906            // @TODO: Honor MAXSIZE and DEPTH options
3907            foreach ($queries as $attrib => $entry)
3908                if ($result = $this->conn->getAnnotation($mailbox, $entry, $attrib))
3909                    $res = array_merge_recursive($res, $result);
3910
3911            return $res;
3912        }
3913
3914        return NULL;
3915    }
3916
3917
3918    /**
3919     * Converts the METADATA extension entry name into the correct
3920     * entry-attrib names for older ANNOTATEMORE version.
3921     *
3922     * @param string $entry Entry name
3923     *
3924     * @return array Entry-attribute list, NULL if not supported (?)
3925     */
3926    private function md2annotate($entry)
3927    {
3928        if (substr($entry, 0, 7) == '/shared') {
3929            return array(substr($entry, 7), 'value.shared');
3930        }
3931        else if (substr($entry, 0, 8) == '/private') {
3932            return array(substr($entry, 8), 'value.priv');
3933        }
3934
3935        // @TODO: log error
3936        return NULL;
3937    }
3938
3939
3940    /* --------------------------------
3941     *   internal caching methods
3942     * --------------------------------*/
3943
3944    /**
3945     * Enable or disable indexes caching
3946     *
3947     * @param string $type Cache type (@see rcmail::get_cache)
3948     * @access public
3949     */
3950    function set_caching($type)
3951    {
3952        if ($type) {
3953            $this->caching = $type;
3954        }
3955        else {
3956            if ($this->cache)
3957                $this->cache->close();
3958            $this->cache   = null;
3959            $this->caching = false;
3960        }
3961    }
3962
3963    /**
3964     * Getter for IMAP cache object
3965     */
3966    private function get_cache_engine()
3967    {
3968        if ($this->caching && !$this->cache) {
3969            $rcmail = rcmail::get_instance();
3970            $this->cache = $rcmail->get_cache('IMAP', $this->caching);
3971        }
3972
3973        return $this->cache;
3974    }
3975
3976    /**
3977     * Returns cached value
3978     *
3979     * @param string $key Cache key
3980     * @return mixed
3981     * @access public
3982     */
3983    function get_cache($key)
3984    {
3985        if ($cache = $this->get_cache_engine()) {
3986            return $cache->get($key);
3987        }
3988    }
3989
3990    /**
3991     * Update cache
3992     *
3993     * @param string $key  Cache key
3994     * @param mixed  $data Data
3995     * @access public
3996     */
3997    function update_cache($key, $data)
3998    {
3999        if ($cache = $this->get_cache_engine()) {
4000            $cache->set($key, $data);
4001        }
4002    }
4003
4004    /**
4005     * Clears the cache.
4006     *
4007     * @param string  $key         Cache key name or pattern
4008     * @param boolean $prefix_mode Enable it to clear all keys starting
4009     *                             with prefix specified in $key
4010     * @access public
4011     */
4012    function clear_cache($key=null, $prefix_mode=false)
4013    {
4014        if ($cache = $this->get_cache_engine()) {
4015            $cache->remove($key, $prefix_mode);
4016        }
4017    }
4018
4019
4020    /* --------------------------------
4021     *   message caching methods
4022     * --------------------------------*/
4023
4024    /**
4025     * Enable or disable messages caching
4026     *
4027     * @param boolean $set Flag
4028     */
4029    function set_messages_caching($set)
4030    {
4031        if ($set) {
4032            $this->messages_caching = true;
4033        }
4034        else {
4035            if ($this->mcache)
4036                $this->mcache->close();
4037            $this->mcache = null;
4038            $this->messages_caching = false;
4039        }
4040    }
4041
4042    /**
4043     * Getter for messages cache object
4044     */
4045    private function get_mcache_engine()
4046    {
4047        if ($this->messages_caching && !$this->mcache) {
4048            $rcmail = rcmail::get_instance();
4049            if ($dbh = $rcmail->get_dbh()) {
4050                $this->mcache = new rcube_imap_cache(
4051                    $dbh, $this, $rcmail->user->ID, $this->skip_deleted);
4052            }
4053        }
4054
4055        return $this->mcache;
4056    }
4057
4058    /**
4059     * Clears the messages cache.
4060     *
4061     * @param string $mailbox Folder name
4062     * @param array  $uids    Optional message UIDs to remove from cache
4063     */
4064    function clear_message_cache($mailbox = null, $uids = null)
4065    {
4066        if ($mcache = $this->get_mcache_engine()) {
4067            $mcache->clear($mailbox, $uids);
4068        }
4069    }
4070
4071
4072
4073    /* --------------------------------
4074     *   encoding/decoding methods
4075     * --------------------------------*/
4076
4077    /**
4078     * Split an address list into a structured array list
4079     *
4080     * @param string  $input  Input string
4081     * @param int     $max    List only this number of addresses
4082     * @param boolean $decode Decode address strings
4083     * @return array  Indexed list of addresses
4084     */
4085    function decode_address_list($input, $max=null, $decode=true)
4086    {
4087        $a = $this->_parse_address_list($input, $decode);
4088        $out = array();
4089        // Special chars as defined by RFC 822 need to in quoted string (or escaped).
4090        $special_chars = '[\(\)\<\>\\\.\[\]@,;:"]';
4091
4092        if (!is_array($a))
4093            return $out;
4094
4095        $c = count($a);
4096        $j = 0;
4097
4098        foreach ($a as $val) {
4099            $j++;
4100            $address = trim($val['address']);
4101            $name    = trim($val['name']);
4102
4103            if ($name && $address && $name != $address)
4104                $string = sprintf('%s <%s>', preg_match("/$special_chars/", $name) ? '"'.addcslashes($name, '"').'"' : $name, $address);
4105            else if ($address)
4106                $string = $address;
4107            else if ($name)
4108                $string = $name;
4109
4110            $out[$j] = array(
4111                'name'   => $name,
4112                'mailto' => $address,
4113                'string' => $string
4114            );
4115
4116            if ($max && $j==$max)
4117                break;
4118        }
4119
4120        return $out;
4121    }
4122
4123
4124    /**
4125     * Decode a message header value
4126     *
4127     * @param string  $input         Header value
4128     * @param boolean $remove_quotas Remove quotes if necessary
4129     * @return string Decoded string
4130     */
4131    function decode_header($input, $remove_quotes=false)
4132    {
4133        $str = rcube_imap::decode_mime_string((string)$input, $this->default_charset);
4134        if ($str[0] == '"' && $remove_quotes)
4135            $str = str_replace('"', '', $str);
4136
4137        return $str;
4138    }
4139
4140
4141    /**
4142     * Decode a mime-encoded string to internal charset
4143     *
4144     * @param string $input    Header value
4145     * @param string $fallback Fallback charset if none specified
4146     *
4147     * @return string Decoded string
4148     * @static
4149     */
4150    public static function decode_mime_string($input, $fallback=null)
4151    {
4152        if (!empty($fallback)) {
4153            $default_charset = $fallback;
4154        }
4155        else {
4156            $default_charset = rcmail::get_instance()->config->get('default_charset', 'ISO-8859-1');
4157        }
4158
4159        // rfc: all line breaks or other characters not found
4160        // in the Base64 Alphabet must be ignored by decoding software
4161        // delete all blanks between MIME-lines, differently we can
4162        // receive unnecessary blanks and broken utf-8 symbols
4163        $input = preg_replace("/\?=\s+=\?/", '?==?', $input);
4164
4165        // encoded-word regexp
4166        $re = '/=\?([^?]+)\?([BbQq])\?([^\n]*?)\?=/';
4167
4168        // Find all RFC2047's encoded words
4169        if (preg_match_all($re, $input, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {
4170            // Initialize variables
4171            $tmp   = array();
4172            $out   = '';
4173            $start = 0;
4174
4175            foreach ($matches as $idx => $m) {
4176                $pos      = $m[0][1];
4177                $charset  = $m[1][0];
4178                $encoding = $m[2][0];
4179                $text     = $m[3][0];
4180                $length   = strlen($m[0][0]);
4181
4182                // Append everything that is before the text to be decoded
4183                if ($start != $pos) {
4184                    $substr = substr($input, $start, $pos-$start);
4185                    $out   .= rcube_charset_convert($substr, $default_charset);
4186                    $start  = $pos;
4187                }
4188                $start += $length;
4189
4190                // Per RFC2047, each string part "MUST represent an integral number
4191                // of characters . A multi-octet character may not be split across
4192                // adjacent encoded-words." However, some mailers break this, so we
4193                // try to handle characters spanned across parts anyway by iterating
4194                // through and aggregating sequential encoded parts with the same
4195                // character set and encoding, then perform the decoding on the
4196                // aggregation as a whole.
4197
4198                $tmp[] = $text;
4199                if ($next_match = $matches[$idx+1]) {
4200                    if ($next_match[0][1] == $start
4201                        && $next_match[1][0] == $charset
4202                        && $next_match[2][0] == $encoding
4203                    ) {
4204                        continue;
4205                    }
4206                }
4207
4208                $count = count($tmp);
4209                $text  = '';
4210
4211                // Decode and join encoded-word's chunks
4212                if ($encoding == 'B' || $encoding == 'b') {
4213                    // base64 must be decoded a segment at a time
4214                    for ($i=0; $i<$count; $i++)
4215                        $text .= base64_decode($tmp[$i]);
4216                }
4217                else { //if ($encoding == 'Q' || $encoding == 'q') {
4218                    // quoted printable can be combined and processed at once
4219                    for ($i=0; $i<$count; $i++)
4220                        $text .= $tmp[$i];
4221
4222                    $text = str_replace('_', ' ', $text);
4223                    $text = quoted_printable_decode($text);
4224                }
4225
4226                $out .= rcube_charset_convert($text, $charset);
4227                $tmp = array();
4228            }
4229
4230            // add the last part of the input string
4231            if ($start != strlen($input)) {
4232                $out .= rcube_charset_convert(substr($input, $start), $default_charset);
4233            }
4234
4235            // return the results
4236            return $out;
4237        }
4238
4239        // no encoding information, use fallback
4240        return rcube_charset_convert($input, $default_charset);
4241    }
4242
4243
4244    /**
4245     * Decode a mime part
4246     *
4247     * @param string $input    Input string
4248     * @param string $encoding Part encoding
4249     * @return string Decoded string
4250     */
4251    function mime_decode($input, $encoding='7bit')
4252    {
4253        switch (strtolower($encoding)) {
4254        case 'quoted-printable':
4255            return quoted_printable_decode($input);
4256        case 'base64':
4257            return base64_decode($input);
4258        case 'x-uuencode':
4259        case 'x-uue':
4260        case 'uue':
4261        case 'uuencode':
4262            return convert_uudecode($input);
4263        case '7bit':
4264        default:
4265            return $input;
4266        }
4267    }
4268
4269
4270    /**
4271     * Convert body charset to RCMAIL_CHARSET according to the ctype_parameters
4272     *
4273     * @param string $body        Part body to decode
4274     * @param string $ctype_param Charset to convert from
4275     * @return string Content converted to internal charset
4276     */
4277    function charset_decode($body, $ctype_param)
4278    {
4279        if (is_array($ctype_param) && !empty($ctype_param['charset']))
4280            return rcube_charset_convert($body, $ctype_param['charset']);
4281
4282        // defaults to what is specified in the class header
4283        return rcube_charset_convert($body,  $this->default_charset);
4284    }
4285
4286
4287    /* --------------------------------
4288     *         private methods
4289     * --------------------------------*/
4290
4291    /**
4292     * Validate the given input and save to local properties
4293     *
4294     * @param string $sort_field Sort column
4295     * @param string $sort_order Sort order
4296     * @access private
4297     */
4298    private function _set_sort_order($sort_field, $sort_order)
4299    {
4300        if ($sort_field != null)
4301            $this->sort_field = asciiwords($sort_field);
4302        if ($sort_order != null)
4303            $this->sort_order = strtoupper($sort_order) == 'DESC' ? 'DESC' : 'ASC';
4304    }
4305
4306
4307    /**
4308     * Sort mailboxes first by default folders and then in alphabethical order
4309     *
4310     * @param array $a_folders Mailboxes list
4311     * @access private
4312     */
4313    private function _sort_mailbox_list($a_folders)
4314    {
4315        $a_out = $a_defaults = $folders = array();
4316
4317        $delimiter = $this->get_hierarchy_delimiter();
4318
4319        // find default folders and skip folders starting with '.'
4320        foreach ($a_folders as $i => $folder) {
4321            if ($folder[0] == '.')
4322                continue;
4323
4324            if (($p = array_search($folder, $this->default_folders)) !== false && !$a_defaults[$p])
4325                $a_defaults[$p] = $folder;
4326            else
4327                $folders[$folder] = rcube_charset_convert($folder, 'UTF7-IMAP');
4328        }
4329
4330        // sort folders and place defaults on the top
4331        asort($folders, SORT_LOCALE_STRING);
4332        ksort($a_defaults);
4333        $folders = array_merge($a_defaults, array_keys($folders));
4334
4335        // finally we must rebuild the list to move
4336        // subfolders of default folders to their place...
4337        // ...also do this for the rest of folders because
4338        // asort() is not properly sorting case sensitive names
4339        while (list($key, $folder) = each($folders)) {
4340            // set the type of folder name variable (#1485527)
4341            $a_out[] = (string) $folder;
4342            unset($folders[$key]);
4343            $this->_rsort($folder, $delimiter, $folders, $a_out);
4344        }
4345
4346        return $a_out;
4347    }
4348
4349
4350    /**
4351     * @access private
4352     */
4353    private function _rsort($folder, $delimiter, &$list, &$out)
4354    {
4355        while (list($key, $name) = each($list)) {
4356                if (strpos($name, $folder.$delimiter) === 0) {
4357                    // set the type of folder name variable (#1485527)
4358                $out[] = (string) $name;
4359                    unset($list[$key]);
4360                    $this->_rsort($name, $delimiter, $list, $out);
4361                }
4362        }
4363        reset($list);
4364    }
4365
4366
4367    /**
4368     * Finds message sequence ID for specified UID
4369     *
4370     * @param int    $uid      Message UID
4371     * @param string $mailbox  Mailbox name
4372     * @param bool   $force    True to skip cache
4373     *
4374     * @return int Message (sequence) ID
4375     */
4376    function uid2id($uid, $mailbox = null, $force = false)
4377    {
4378        if (!strlen($mailbox)) {
4379            $mailbox = $this->mailbox;
4380        }
4381
4382        if (!empty($this->uid_id_map[$mailbox][$uid])) {
4383            return $this->uid_id_map[$mailbox][$uid];
4384        }
4385
4386        if (!$force && ($mcache = $this->get_mcache_engine()))
4387            $id = $mcache->uid2id($mailbox, $uid);
4388
4389        if (empty($id))
4390            $id = $this->conn->UID2ID($mailbox, $uid);
4391
4392        $this->uid_id_map[$mailbox][$uid] = $id;
4393
4394        return $id;
4395    }
4396
4397
4398    /**
4399     * Find UID of the specified message sequence ID
4400     *
4401     * @param int    $id       Message (sequence) ID
4402     * @param string $mailbox  Mailbox name
4403     * @param bool   $force    True to skip cache
4404     *
4405     * @return int Message UID
4406     */
4407    function id2uid($id, $mailbox = null, $force = false)
4408    {
4409        if (!strlen($mailbox)) {
4410            $mailbox = $this->mailbox;
4411        }
4412
4413        if ($uid = array_search($id, (array)$this->uid_id_map[$mailbox])) {
4414            return $uid;
4415        }
4416
4417        if (!$force && ($mcache = $this->get_mcache_engine()))
4418            $uid = $mcache->id2uid($mailbox, $id);
4419
4420        if (empty($uid))
4421            $uid = $this->conn->ID2UID($mailbox, $id);
4422
4423        $this->uid_id_map[$mailbox][$uid] = $id;
4424
4425        return $uid;
4426    }
4427
4428
4429    /**
4430     * Subscribe/unsubscribe a list of mailboxes and update local cache
4431     * @access private
4432     */
4433    private function _change_subscription($a_mboxes, $mode)
4434    {
4435        $updated = false;
4436
4437        if (is_array($a_mboxes))
4438            foreach ($a_mboxes as $i => $mailbox) {
4439                $a_mboxes[$i] = $mailbox;
4440
4441                if ($mode == 'subscribe')
4442                    $updated = $this->conn->subscribe($mailbox);
4443                else if ($mode == 'unsubscribe')
4444                    $updated = $this->conn->unsubscribe($mailbox);
4445            }
4446
4447        // clear cached mailbox list(s)
4448        if ($updated) {
4449            $this->clear_cache('mailboxes', true);
4450        }
4451
4452        return $updated;
4453    }
4454
4455
4456    /**
4457     * Increde/decrese messagecount for a specific mailbox
4458     * @access private
4459     */
4460    private function _set_messagecount($mailbox, $mode, $increment)
4461    {
4462        $mode = strtoupper($mode);
4463        $a_mailbox_cache = $this->get_cache('messagecount');
4464
4465        if (!is_array($a_mailbox_cache[$mailbox]) || !isset($a_mailbox_cache[$mailbox][$mode]) || !is_numeric($increment))
4466            return false;
4467
4468        // add incremental value to messagecount
4469        $a_mailbox_cache[$mailbox][$mode] += $increment;
4470
4471        // there's something wrong, delete from cache
4472        if ($a_mailbox_cache[$mailbox][$mode] < 0)
4473            unset($a_mailbox_cache[$mailbox][$mode]);
4474
4475        // write back to cache
4476        $this->update_cache('messagecount', $a_mailbox_cache);
4477
4478        return true;
4479    }
4480
4481
4482    /**
4483     * Remove messagecount of a specific mailbox from cache
4484     * @access private
4485     */
4486    private function _clear_messagecount($mailbox, $mode=null)
4487    {
4488        $a_mailbox_cache = $this->get_cache('messagecount');
4489
4490        if (is_array($a_mailbox_cache[$mailbox])) {
4491            if ($mode) {
4492                unset($a_mailbox_cache[$mailbox][$mode]);
4493            }
4494            else {
4495                unset($a_mailbox_cache[$mailbox]);
4496            }
4497            $this->update_cache('messagecount', $a_mailbox_cache);
4498        }
4499    }
4500
4501
4502    /**
4503     * Split RFC822 header string into an associative array
4504     * @access private
4505     */
4506    private function _parse_headers($headers)
4507    {
4508        $a_headers = array();
4509        $headers = preg_replace('/\r?\n(\t| )+/', ' ', $headers);
4510        $lines = explode("\n", $headers);
4511        $c = count($lines);
4512
4513        for ($i=0; $i<$c; $i++) {
4514            if ($p = strpos($lines[$i], ': ')) {
4515                $field = strtolower(substr($lines[$i], 0, $p));
4516                $value = trim(substr($lines[$i], $p+1));
4517                if (!empty($value))
4518                    $a_headers[$field] = $value;
4519            }
4520        }
4521
4522        return $a_headers;
4523    }
4524
4525
4526    /**
4527     * @access private
4528     */
4529    private function _parse_address_list($str, $decode=true)
4530    {
4531        // remove any newlines and carriage returns before
4532        $str = preg_replace('/\r?\n(\s|\t)?/', ' ', $str);
4533
4534        // extract list items, remove comments
4535        $str = self::explode_header_string(',;', $str, true);
4536        $result = array();
4537
4538        // simplified regexp, supporting quoted local part
4539        $email_rx = '(\S+|("\s*(?:[^"\f\n\r\t\v\b\s]+\s*)+"))@\S+';
4540
4541        foreach ($str as $key => $val) {
4542            $name    = '';
4543            $address = '';
4544            $val     = trim($val);
4545
4546            if (preg_match('/(.*)<('.$email_rx.')>$/', $val, $m)) {
4547                $address = $m[2];
4548                $name    = trim($m[1]);
4549            }
4550            else if (preg_match('/^('.$email_rx.')$/', $val, $m)) {
4551                $address = $m[1];
4552                $name    = '';
4553            }
4554            else {
4555                $name = $val;
4556            }
4557
4558            // dequote and/or decode name
4559            if ($name) {
4560                if ($name[0] == '"' && $name[strlen($name)-1] == '"') {
4561                    $name = substr($name, 1, -1);
4562                    $name = stripslashes($name);
4563                }
4564                if ($decode) {
4565                    $name = $this->decode_header($name);
4566                }
4567            }
4568
4569            if (!$address && $name) {
4570                $address = $name;
4571            }
4572
4573            if ($address) {
4574                $result[$key] = array('name' => $name, 'address' => $address);
4575            }
4576        }
4577
4578        return $result;
4579    }
4580
4581
4582    /**
4583     * Explodes header (e.g. address-list) string into array of strings
4584     * using specified separator characters with proper handling
4585     * of quoted-strings and comments (RFC2822)
4586     *
4587     * @param string $separator       String containing separator characters
4588     * @param string $str             Header string
4589     * @param bool   $remove_comments Enable to remove comments
4590     *
4591     * @return array Header items
4592     */
4593    static function explode_header_string($separator, $str, $remove_comments=false)
4594    {
4595        $length  = strlen($str);
4596        $result  = array();
4597        $quoted  = false;
4598        $comment = 0;
4599        $out     = '';
4600
4601        for ($i=0; $i<$length; $i++) {
4602            // we're inside a quoted string
4603            if ($quoted) {
4604                if ($str[$i] == '"') {
4605                    $quoted = false;
4606                }
4607                else if ($str[$i] == '\\') {
4608                    if ($comment <= 0) {
4609                        $out .= '\\';
4610                    }
4611                    $i++;
4612                }
4613            }
4614            // we're inside a comment string
4615            else if ($comment > 0) {
4616                    if ($str[$i] == ')') {
4617                        $comment--;
4618                    }
4619                    else if ($str[$i] == '(') {
4620                        $comment++;
4621                    }
4622                    else if ($str[$i] == '\\') {
4623                        $i++;
4624                    }
4625                    continue;
4626            }
4627            // separator, add to result array
4628            else if (strpos($separator, $str[$i]) !== false) {
4629                    if ($out) {
4630                        $result[] = $out;
4631                    }
4632                    $out = '';
4633                    continue;
4634            }
4635            // start of quoted string
4636            else if ($str[$i] == '"') {
4637                    $quoted = true;
4638            }
4639            // start of comment
4640            else if ($remove_comments && $str[$i] == '(') {
4641                    $comment++;
4642            }
4643
4644            if ($comment <= 0) {
4645                $out .= $str[$i];
4646            }
4647        }
4648
4649        if ($out && $comment <= 0) {
4650            $result[] = $out;
4651        }
4652
4653        return $result;
4654    }
4655
4656
4657    /**
4658     * This is our own debug handler for the IMAP connection
4659     * @access public
4660     */
4661    public function debug_handler(&$imap, $message)
4662    {
4663        write_log('imap', $message);
4664    }
4665
4666}  // end class rcube_imap
4667
4668
4669/**
4670 * Class representing a message part
4671 *
4672 * @package Mail
4673 */
4674class rcube_message_part
4675{
4676    var $mime_id = '';
4677    var $ctype_primary = 'text';
4678    var $ctype_secondary = 'plain';
4679    var $mimetype = 'text/plain';
4680    var $disposition = '';
4681    var $filename = '';
4682    var $encoding = '8bit';
4683    var $charset = '';
4684    var $size = 0;
4685    var $headers = array();
4686    var $d_parameters = array();
4687    var $ctype_parameters = array();
4688
4689    function __clone()
4690    {
4691        if (isset($this->parts))
4692            foreach ($this->parts as $idx => $part)
4693                if (is_object($part))
4694                        $this->parts[$idx] = clone $part;
4695    }
4696}
4697
4698
4699/**
4700 * Class for sorting an array of rcube_mail_header objects in a predetermined order.
4701 *
4702 * @package Mail
4703 * @author Eric Stadtherr
4704 */
4705class rcube_header_sorter
4706{
4707    private $seqs = array();
4708    private $uids = array();
4709
4710
4711    /**
4712     * Set the predetermined sort order.
4713     *
4714     * @param array $index  Numerically indexed array of IMAP ID or UIDs
4715     * @param bool  $is_uid Set to true if $index contains UIDs
4716     */
4717    function set_index($index, $is_uid = false)
4718    {
4719        $index = array_flip($index);
4720
4721        if ($is_uid)
4722            $this->uids = $index;
4723        else
4724            $this->seqs = $index;
4725    }
4726
4727    /**
4728     * Sort the array of header objects
4729     *
4730     * @param array $headers Array of rcube_mail_header objects indexed by UID
4731     */
4732    function sort_headers(&$headers)
4733    {
4734        if (!empty($this->uids))
4735            uksort($headers, array($this, "compare_uids"));
4736        else
4737            uasort($headers, array($this, "compare_seqnums"));
4738    }
4739
4740    /**
4741     * Sort method called by uasort()
4742     *
4743     * @param rcube_mail_header $a
4744     * @param rcube_mail_header $b
4745     */
4746    function compare_seqnums($a, $b)
4747    {
4748        // First get the sequence number from the header object (the 'id' field).
4749        $seqa = $a->id;
4750        $seqb = $b->id;
4751
4752        // then find each sequence number in my ordered list
4753        $posa = isset($this->seqs[$seqa]) ? intval($this->seqs[$seqa]) : -1;
4754        $posb = isset($this->seqs[$seqb]) ? intval($this->seqs[$seqb]) : -1;
4755
4756        // return the relative position as the comparison value
4757        return $posa - $posb;
4758    }
4759
4760    /**
4761     * Sort method called by uksort()
4762     *
4763     * @param int $a Array key (UID)
4764     * @param int $b Array key (UID)
4765     */
4766    function compare_uids($a, $b)
4767    {
4768        // then find each sequence number in my ordered list
4769        $posa = isset($this->uids[$a]) ? intval($this->uids[$a]) : -1;
4770        $posb = isset($this->uids[$b]) ? intval($this->uids[$b]) : -1;
4771
4772        // return the relative position as the comparison value
4773        return $posa - $posb;
4774    }
4775}
Note: See TracBrowser for help on using the repository browser.