source: github/program/include/rcube_imap.php @ 3fec695

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