source: github/program/include/rcube_imap.php @ 68070e44

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