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

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