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

Last change on this file since 4743 was 4743, checked in by alec, 2 years ago
  • Fix possible ambiguity in folders caching (when list_mailboxes() was called the same result was returned, no matter what was the 1st and 2nd argument)
  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
File size: 159.7 KB
Line 
1<?php
2
3/*
4 +-----------------------------------------------------------------------+
5 | program/include/rcube_imap.php                                        |
6 |                                                                       |
7 | This file is part of the Roundcube Webmail client                     |
8 | Copyright (C) 2005-2010, The Roundcube Dev Team                       |
9 | Licensed under the GNU GPL                                            |
10 |                                                                       |
11 | PURPOSE:                                                              |
12 |   IMAP Engine                                                         |
13 |                                                                       |
14 +-----------------------------------------------------------------------+
15 | Author: Thomas Bruederli <roundcube@gmail.com>                        |
16 | Author: Aleksander Machniak <alec@alec.pl>                            |
17 +-----------------------------------------------------------------------+
18
19 $Id$
20
21*/
22
23
24/**
25 * Interface class for accessing an IMAP server
26 *
27 * @package    Mail
28 * @author     Thomas Bruederli <roundcube@gmail.com>
29 * @author     Aleksander Machniak <alec@alec.pl>
30 * @version    2.0
31 */
32class rcube_imap
33{
34    public $debug_level = 1;
35    public $skip_deleted = false;
36    public $page_size = 10;
37    public $list_page = 1;
38    public $threading = false;
39    public $fetch_add_headers = '';
40    public $get_all_headers = false;
41
42    /**
43     * Instance of rcube_imap_generic
44     *
45     * @var rcube_imap_generic
46     */
47    public $conn;
48
49    /**
50     * Instance of rcube_mdb2
51     *
52     * @var rcube_mdb2
53     */
54    private $db;
55    private $mailbox = 'INBOX';
56    private $delimiter = NULL;
57    private $namespace = NULL;
58    private $sort_field = '';
59    private $sort_order = 'DESC';
60    private $caching_enabled = false;
61    private $default_charset = 'ISO-8859-1';
62    private $struct_charset = NULL;
63    private $default_folders = array('INBOX');
64    private $icache = array();
65    private $cache = array();
66    private $cache_keys = array();
67    private $cache_changes = array();
68    private $uid_id_map = array();
69    private $msg_headers = array();
70    public  $search_set = NULL;
71    public  $search_string = '';
72    private $search_charset = '';
73    private $search_sort_field = '';
74    private $search_threads = false;
75    private $search_sorted = false;
76    private $db_header_fields = array('idx', 'uid', 'subject', 'from', 'to', 'cc', 'date', 'size');
77    private $options = array('auth_method' => 'check');
78    private $host, $user, $pass, $port, $ssl;
79
80    /**
81     * All (additional) headers used (in any way) by Roundcube
82     * Not listed here: DATE, FROM, TO, SUBJECT, CONTENT-TYPE, LIST-POST
83     * (used for messages listing) are hardcoded in rcube_imap_generic::fetchHeaders()
84     *
85     * @var array
86     * @see rcube_imap::fetch_add_headers
87     */
88    private $all_headers = array(
89        'REPLY-TO',
90        'IN-REPLY-TO',
91        'CC',
92        'BCC',
93        'MESSAGE-ID',
94        'CONTENT-TRANSFER-ENCODING',
95        'REFERENCES',
96        'X-PRIORITY',
97        'X-DRAFT-INFO',
98        'MAIL-FOLLOWUP-TO',
99        'MAIL-REPLY-TO',
100        'RETURN-PATH',
101    );
102
103    const UNKNOWN       = 0;
104    const NOPERM        = 1;
105    const READONLY      = 2;
106    const TRYCREATE     = 3;
107    const INUSE         = 4;
108    const OVERQUOTA     = 5;
109    const ALREADYEXISTS = 6;
110    const NONEXISTENT   = 7;
111    const CONTACTADMIN  = 8;
112
113
114    /**
115     * Object constructor
116     *
117     * @param object DB Database connection
118     */
119    function __construct($db_conn)
120    {
121        $this->db = $db_conn;
122        $this->conn = new rcube_imap_generic();
123    }
124
125
126    /**
127     * Connect to an IMAP server
128     *
129     * @param  string   $host    Host to connect
130     * @param  string   $user    Username for IMAP account
131     * @param  string   $pass    Password for IMAP account
132     * @param  integer  $port    Port to connect to
133     * @param  string   $use_ssl SSL schema (either ssl or tls) or null if plain connection
134     * @return boolean  TRUE on success, FALSE on failure
135     * @access public
136     */
137    function connect($host, $user, $pass, $port=143, $use_ssl=null)
138    {
139        // check for OpenSSL support in PHP build
140        if ($use_ssl && extension_loaded('openssl'))
141            $this->options['ssl_mode'] = $use_ssl == 'imaps' ? 'ssl' : $use_ssl;
142        else if ($use_ssl) {
143            raise_error(array('code' => 403, 'type' => 'imap',
144                'file' => __FILE__, 'line' => __LINE__,
145                'message' => "OpenSSL not available"), true, false);
146            $port = 143;
147        }
148
149        $this->options['port'] = $port;
150
151        if ($this->options['debug']) {
152            $this->conn->setDebug(true, array($this, 'debug_handler'));
153
154            $this->options['ident'] = array(
155                'name' => 'Roundcube Webmail',
156                'version' => RCMAIL_VERSION,
157                'php' => PHP_VERSION,
158                'os' => PHP_OS,
159                'command' => $_SERVER['REQUEST_URI'],
160            );
161        }
162
163        $attempt = 0;
164        do {
165            $data = rcmail::get_instance()->plugins->exec_hook('imap_connect',
166                array('host' => $host, 'user' => $user, 'attempt' => ++$attempt));
167
168            if (!empty($data['pass']))
169                $pass = $data['pass'];
170
171            $this->conn->connect($data['host'], $data['user'], $pass, $this->options);
172        } while(!$this->conn->connected() && $data['retry']);
173
174        $this->host = $data['host'];
175        $this->user = $data['user'];
176        $this->pass = $pass;
177        $this->port = $port;
178        $this->ssl  = $use_ssl;
179
180        if ($this->conn->connected()) {
181            // get namespace and delimiter
182            $this->set_env();
183            return true;
184        }
185        // write error log
186        else if ($this->conn->error) {
187            if ($pass && $user) {
188                $message = sprintf("Login failed for %s from %s. %s",
189                    $user, rcmail_remote_ip(), $this->conn->error);
190
191                raise_error(array('code' => 403, 'type' => 'imap',
192                    'file' => __FILE__, 'line' => __LINE__,
193                    'message' => $message), true, false);
194            }
195        }
196
197        return false;
198    }
199
200
201    /**
202     * Close IMAP connection
203     * Usually done on script shutdown
204     *
205     * @access public
206     */
207    function close()
208    {
209        $this->conn->closeConnection();
210        $this->write_cache();
211    }
212
213
214    /**
215     * Close IMAP connection and re-connect
216     * This is used to avoid some strange socket errors when talking to Courier IMAP
217     *
218     * @access public
219     */
220    function reconnect()
221    {
222        $this->conn->closeConnection();
223        $connected = $this->connect($this->host, $this->user, $this->pass, $this->port, $this->ssl);
224
225        // issue SELECT command to restore connection status
226        if ($connected && strlen($this->mailbox))
227            $this->conn->select($this->mailbox);
228    }
229
230
231    /**
232     * Returns code of last error
233     *
234     * @return int Error code
235     */
236    function get_error_code()
237    {
238        return $this->conn->errornum;
239    }
240
241
242    /**
243     * Returns message of last error
244     *
245     * @return string Error message
246     */
247    function get_error_str()
248    {
249        return $this->conn->error;
250    }
251
252
253    /**
254     * Returns code of last command response
255     *
256     * @return int Response code
257     */
258    function get_response_code()
259    {
260        switch ($this->conn->resultcode) {
261            case 'NOPERM':
262                return self::NOPERM;
263            case 'READ-ONLY':
264                return self::READONLY;
265            case 'TRYCREATE':
266                return self::TRYCREATE;
267            case 'INUSE':
268                return self::INUSE;
269            case 'OVERQUOTA':
270                return self::OVERQUOTA;
271            case 'ALREADYEXISTS':
272                return self::ALREADYEXISTS;
273            case 'NONEXISTENT':
274                return self::NONEXISTENT;
275            case 'CONTACTADMIN':
276                return self::CONTACTADMIN;
277            default:
278                return self::UNKNOWN;
279        }
280    }
281
282
283    /**
284     * Returns last command response
285     *
286     * @return string Response
287     */
288    function get_response_str()
289    {
290        return $this->conn->result;
291    }
292
293
294    /**
295     * Set options to be used in rcube_imap_generic::connect()
296     *
297     * @param array $opt Options array
298     */
299    function set_options($opt)
300    {
301        $this->options = array_merge($this->options, (array)$opt);
302    }
303
304
305    /**
306     * Set default message charset
307     *
308     * This will be used for message decoding if a charset specification is not available
309     *
310     * @param  string $cs Charset string
311     * @access public
312     */
313    function set_charset($cs)
314    {
315        $this->default_charset = $cs;
316    }
317
318
319    /**
320     * This list of folders will be listed above all other folders
321     *
322     * @param  array $arr Indexed list of folder names
323     * @access public
324     */
325    function set_default_mailboxes($arr)
326    {
327        if (is_array($arr)) {
328            $this->default_folders = $arr;
329
330            // add inbox if not included
331            if (!in_array('INBOX', $this->default_folders))
332                array_unshift($this->default_folders, 'INBOX');
333        }
334    }
335
336
337    /**
338     * Set internal mailbox reference.
339     *
340     * All operations will be perfomed on this mailbox/folder
341     *
342     * @param  string $mailbox Mailbox/Folder name
343     * @access public
344     */
345    function set_mailbox($mailbox)
346    {
347        if ($this->mailbox == $mailbox)
348            return;
349
350        $this->mailbox = $mailbox;
351
352        // clear messagecount cache for this mailbox
353        $this->_clear_messagecount($mailbox);
354    }
355
356
357    /**
358     * Forces selection of a mailbox
359     *
360     * @param  string $mailbox Mailbox/Folder name
361     * @access public
362     */
363    function select_mailbox($mailbox=null)
364    {
365        if (!strlen($mailbox)) {
366            $mailbox = $this->mailbox;
367        }
368
369        $selected = $this->conn->select($mailbox);
370
371        if ($selected && $this->mailbox != $mailbox) {
372            // clear messagecount cache for this mailbox
373            $this->_clear_messagecount($mailbox);
374            $this->mailbox = $mailbox;
375        }
376    }
377
378
379    /**
380     * Set internal list page
381     *
382     * @param  number $page Page number to list
383     * @access public
384     */
385    function set_page($page)
386    {
387        $this->list_page = (int)$page;
388    }
389
390
391    /**
392     * Set internal page size
393     *
394     * @param  number $size Number of messages to display on one page
395     * @access public
396     */
397    function set_pagesize($size)
398    {
399        $this->page_size = (int)$size;
400    }
401
402
403    /**
404     * Save a set of message ids for future message listing methods
405     *
406     * @param  string  IMAP Search query
407     * @param  array   List of message ids or NULL if empty
408     * @param  string  Charset of search string
409     * @param  string  Sorting field
410     * @param  string  True if set is sorted (SORT was used for searching)
411     */
412    function set_search_set($str=null, $msgs=null, $charset=null, $sort_field=null, $threads=false, $sorted=false)
413    {
414        if (is_array($str) && $msgs == null)
415            list($str, $msgs, $charset, $sort_field, $threads) = $str;
416        if ($msgs === false)
417            $msgs = array();
418        else if ($msgs != null && !is_array($msgs))
419            $msgs = explode(',', $msgs);
420
421        $this->search_string     = $str;
422        $this->search_set        = $msgs;
423        $this->search_charset    = $charset;
424        $this->search_sort_field = $sort_field;
425        $this->search_threads    = $threads;
426        $this->search_sorted     = $sorted;
427    }
428
429
430    /**
431     * Return the saved search set as hash array
432     * @return array Search set
433     */
434    function get_search_set()
435    {
436        return array($this->search_string,
437                $this->search_set,
438                $this->search_charset,
439                $this->search_sort_field,
440                $this->search_threads,
441                $this->search_sorted,
442            );
443    }
444
445
446    /**
447     * Returns the currently used mailbox name
448     *
449     * @return  string Name of the mailbox/folder
450     * @access  public
451     */
452    function get_mailbox_name()
453    {
454        return $this->conn->connected() ? $this->mailbox : '';
455    }
456
457
458    /**
459     * Returns the IMAP server's capability
460     *
461     * @param   string  $cap Capability name
462     * @return  mixed   Capability value or TRUE if supported, FALSE if not
463     * @access  public
464     */
465    function get_capability($cap)
466    {
467        return $this->conn->getCapability(strtoupper($cap));
468    }
469
470
471    /**
472     * Sets threading flag to the best supported THREAD algorithm
473     *
474     * @param  boolean  $enable TRUE to enable and FALSE
475     * @return string   Algorithm or false if THREAD is not supported
476     * @access public
477     */
478    function set_threading($enable=false)
479    {
480        $this->threading = false;
481
482        if ($enable && ($caps = $this->get_capability('THREAD'))) {
483            if (in_array('REFS', $caps))
484                $this->threading = 'REFS';
485            else if (in_array('REFERENCES', $caps))
486                $this->threading = 'REFERENCES';
487            else if (in_array('ORDEREDSUBJECT', $caps))
488                $this->threading = 'ORDEREDSUBJECT';
489        }
490
491        return $this->threading;
492    }
493
494
495    /**
496     * Checks the PERMANENTFLAGS capability of the current mailbox
497     * and returns true if the given flag is supported by the IMAP server
498     *
499     * @param   string  $flag Permanentflag name
500     * @return  boolean True if this flag is supported
501     * @access  public
502     */
503    function check_permflag($flag)
504    {
505        $flag = strtoupper($flag);
506        $imap_flag = $this->conn->flags[$flag];
507        return (in_array_nocase($imap_flag, $this->conn->data['PERMANENTFLAGS']));
508    }
509
510
511    /**
512     * Returns the delimiter that is used by the IMAP server for folder separation
513     *
514     * @return  string  Delimiter string
515     * @access  public
516     */
517    function get_hierarchy_delimiter()
518    {
519        return $this->delimiter;
520    }
521
522
523    /**
524     * Get namespace
525     *
526     * @param string $name Namespace array index: personal, other, shared, prefix
527     *
528     * @return  array  Namespace data
529     * @access  public
530     */
531    function get_namespace($name=null)
532    {
533        $ns = $this->namespace;
534
535        if ($name) {
536            return isset($ns[$name]) ? $ns[$name] : null;
537        }
538
539        unset($ns['prefix']);
540        return $ns;
541    }
542
543
544    /**
545     * Sets delimiter and namespaces
546     *
547     * @access private
548     */
549    private function set_env()
550    {
551        if ($this->delimiter !== null && $this->namespace !== null) {
552            return;
553        }
554
555        if (isset($_SESSION['imap_namespace']) && isset($_SESSION['imap_delimiter'])) {
556            $this->namespace = $_SESSION['imap_namespace'];
557            $this->delimiter = $_SESSION['imap_delimiter'];
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->caching_enabled) {
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->caching_enabled) {
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->caching_enabled) {
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->caching_enabled && $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->caching_enabled)
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]) || $part[$i][3]=='NIL')) {
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]) && $part[5]!='NIL') {
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]) && $part[6]!='NIL')
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]) && $part[3]!='NIL') {
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
2525     *
2526     * @param int $uid Message UID
2527     * @return string Message source string
2528     */
2529    function &get_raw_body($uid)
2530    {
2531        return $this->conn->handlePartBody($this->mailbox, $uid, true);
2532    }
2533
2534
2535    /**
2536     * Returns the message headers as string
2537     *
2538     * @param int $uid  Message UID
2539     * @return string Message headers string
2540     */
2541    function &get_raw_headers($uid)
2542    {
2543        return $this->conn->fetchPartHeader($this->mailbox, $uid, true);
2544    }
2545
2546
2547    /**
2548     * Sends the whole message source to stdout
2549     *
2550     * @param int $uid Message UID
2551     */
2552    function print_raw_body($uid)
2553    {
2554        $this->conn->handlePartBody($this->mailbox, $uid, true, NULL, NULL, true);
2555    }
2556
2557
2558    /**
2559     * Set message flag to one or several messages
2560     *
2561     * @param mixed   $uids       Message UIDs as array or comma-separated string, or '*'
2562     * @param string  $flag       Flag to set: SEEN, UNDELETED, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
2563     * @param string  $mailbox    Folder name
2564     * @param boolean $skip_cache True to skip message cache clean up
2565     *
2566     * @return boolean  Operation status
2567     */
2568    function set_flag($uids, $flag, $mailbox=null, $skip_cache=false)
2569    {
2570        if (!strlen($mailbox)) {
2571            $mailbox = $this->mailbox;
2572        }
2573
2574        $flag = strtoupper($flag);
2575        list($uids, $all_mode) = $this->_parse_uids($uids, $mailbox);
2576
2577        if (strpos($flag, 'UN') === 0)
2578            $result = $this->conn->unflag($mailbox, $uids, substr($flag, 2));
2579        else
2580            $result = $this->conn->flag($mailbox, $uids, $flag);
2581
2582        if ($result) {
2583            // reload message headers if cached
2584            if ($this->caching_enabled && !$skip_cache) {
2585                $cache_key = $mailbox.'.msg';
2586                if ($all_mode)
2587                    $this->clear_message_cache($cache_key);
2588                else
2589                    $this->remove_message_cache($cache_key, explode(',', $uids));
2590            }
2591
2592            // clear cached counters
2593            if ($flag == 'SEEN' || $flag == 'UNSEEN') {
2594                $this->_clear_messagecount($mailbox, 'SEEN');
2595                $this->_clear_messagecount($mailbox, 'UNSEEN');
2596            }
2597            else if ($flag == 'DELETED') {
2598                $this->_clear_messagecount($mailbox, 'DELETED');
2599            }
2600        }
2601
2602        return $result;
2603    }
2604
2605
2606    /**
2607     * Remove message flag for one or several messages
2608     *
2609     * @param mixed  $uids    Message UIDs as array or comma-separated string, or '*'
2610     * @param string $flag    Flag to unset: SEEN, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
2611     * @param string $mailbox Folder name
2612     *
2613     * @return int   Number of flagged messages, -1 on failure
2614     * @see set_flag
2615     */
2616    function unset_flag($uids, $flag, $mailbox=null)
2617    {
2618        return $this->set_flag($uids, 'UN'.$flag, $mailbox);
2619    }
2620
2621
2622    /**
2623     * Append a mail message (source) to a specific mailbox
2624     *
2625     * @param string  $mailbox Target mailbox
2626     * @param string  $message The message source string or filename
2627     * @param string  $headers Headers string if $message contains only the body
2628     * @param boolean $is_file True if $message is a filename
2629     *
2630     * @return boolean True on success, False on error
2631     */
2632    function save_message($mailbox, &$message, $headers='', $is_file=false)
2633    {
2634        if (!strlen($mailbox)) {
2635            $mailbox = $this->mailbox;
2636        }
2637
2638        // make sure mailbox exists
2639        if ($this->mailbox_exists($mailbox)) {
2640            if ($is_file)
2641                $saved = $this->conn->appendFromFile($mailbox, $message, $headers);
2642            else
2643                $saved = $this->conn->append($mailbox, $message);
2644        }
2645
2646        if ($saved) {
2647            // increase messagecount of the target mailbox
2648            $this->_set_messagecount($mailbox, 'ALL', 1);
2649        }
2650
2651        return $saved;
2652    }
2653
2654
2655    /**
2656     * Move a message from one mailbox to another
2657     *
2658     * @param mixed  $uids      Message UIDs as array or comma-separated string, or '*'
2659     * @param string $to_mbox   Target mailbox
2660     * @param string $from_mbox Source mailbox
2661     * @return boolean True on success, False on error
2662     */
2663    function move_message($uids, $to_mbox, $from_mbox='')
2664    {
2665        if (!strlen($from_mbox)) {
2666            $from_mbox = $this->mailbox;
2667        }
2668
2669        if ($to_mbox === $from_mbox) {
2670            return false;
2671        }
2672
2673        list($uids, $all_mode) = $this->_parse_uids($uids, $from_mbox);
2674
2675        // exit if no message uids are specified
2676        if (empty($uids))
2677            return false;
2678
2679        // make sure mailbox exists
2680        if ($to_mbox != 'INBOX' && !$this->mailbox_exists($to_mbox)) {
2681            if (in_array($to_mbox, $this->default_folders))
2682                $this->create_mailbox($to_mbox, true);
2683            else
2684                return false;
2685        }
2686
2687        $config = rcmail::get_instance()->config;
2688        $to_trash = $to_mbox == $config->get('trash_mbox');
2689
2690        // flag messages as read before moving them
2691        if ($to_trash && $config->get('read_when_deleted')) {
2692            // don't flush cache (4th argument)
2693            $this->set_flag($uids, 'SEEN', $from_mbox, true);
2694        }
2695
2696        // move messages
2697        $moved = $this->conn->move($uids, $from_mbox, $to_mbox);
2698
2699        // send expunge command in order to have the moved message
2700        // really deleted from the source mailbox
2701        if ($moved) {
2702            $this->_expunge($from_mbox, false, $uids);
2703            $this->_clear_messagecount($from_mbox);
2704            $this->_clear_messagecount($to_mbox);
2705        }
2706        // moving failed
2707        else if ($to_trash && $config->get('delete_always', false)) {
2708            $moved = $this->delete_message($uids, $from_mbox);
2709        }
2710
2711        if ($moved) {
2712            // unset threads internal cache
2713            unset($this->icache['threads']);
2714
2715            // remove message ids from search set
2716            if ($this->search_set && $from_mbox == $this->mailbox) {
2717                // threads are too complicated to just remove messages from set
2718                if ($this->search_threads || $all_mode)
2719                    $this->refresh_search();
2720                else {
2721                    $uids = explode(',', $uids);
2722                    foreach ($uids as $uid)
2723                        $a_mids[] = $this->_uid2id($uid, $from_mbox);
2724                    $this->search_set = array_diff($this->search_set, $a_mids);
2725                }
2726            }
2727
2728            // update cached message headers
2729            $cache_key = $from_mbox.'.msg';
2730            if ($all_mode || ($start_index = $this->get_message_cache_index_min($cache_key, $uids))) {
2731                // clear cache from the lowest index on
2732                $this->clear_message_cache($cache_key, $all_mode ? 1 : $start_index);
2733            }
2734        }
2735
2736        return $moved;
2737    }
2738
2739
2740    /**
2741     * Copy a message from one mailbox to another
2742     *
2743     * @param mixed  $uids      Message UIDs as array or comma-separated string, or '*'
2744     * @param string $to_mbox   Target mailbox
2745     * @param string $from_mbox Source mailbox
2746     * @return boolean True on success, False on error
2747     */
2748    function copy_message($uids, $to_mbox, $from_mbox='')
2749    {
2750        if (!strlen($from_mbox)) {
2751            $from_mbox = $this->mailbox;
2752        }
2753
2754        list($uids, $all_mode) = $this->_parse_uids($uids, $from_mbox);
2755
2756        // exit if no message uids are specified
2757        if (empty($uids)) {
2758            return false;
2759        }
2760
2761        // make sure mailbox exists
2762        if ($to_mbox != 'INBOX' && !$this->mailbox_exists($to_mbox)) {
2763            if (in_array($to_mbox, $this->default_folders))
2764                $this->create_mailbox($to_mbox, true);
2765            else
2766                return false;
2767        }
2768
2769        // copy messages
2770        $copied = $this->conn->copy($uids, $from_mbox, $to_mbox);
2771
2772        if ($copied) {
2773            $this->_clear_messagecount($to_mbox);
2774        }
2775
2776        return $copied;
2777    }
2778
2779
2780    /**
2781     * Mark messages as deleted and expunge mailbox
2782     *
2783     * @param mixed  $uids    Message UIDs as array or comma-separated string, or '*'
2784     * @param string $mailbox Source mailbox
2785     *
2786     * @return boolean True on success, False on error
2787     */
2788    function delete_message($uids, $mailbox='')
2789    {
2790        if (!strlen($mailbox)) {
2791            $mailbox = $this->mailbox;
2792        }
2793
2794        list($uids, $all_mode) = $this->_parse_uids($uids, $mailbox);
2795
2796        // exit if no message uids are specified
2797        if (empty($uids))
2798            return false;
2799
2800        $deleted = $this->conn->delete($mailbox, $uids);
2801
2802        if ($deleted) {
2803            // send expunge command in order to have the deleted message
2804            // really deleted from the mailbox
2805            $this->_expunge($mailbox, false, $uids);
2806            $this->_clear_messagecount($mailbox);
2807            unset($this->uid_id_map[$mailbox]);
2808
2809            // unset threads internal cache
2810            unset($this->icache['threads']);
2811
2812            // remove message ids from search set
2813            if ($this->search_set && $mailbox == $this->mailbox) {
2814                // threads are too complicated to just remove messages from set
2815                if ($this->search_threads || $all_mode)
2816                    $this->refresh_search();
2817                else {
2818                    $uids = explode(',', $uids);
2819                    foreach ($uids as $uid)
2820                        $a_mids[] = $this->_uid2id($uid, $mailbox);
2821                    $this->search_set = array_diff($this->search_set, $a_mids);
2822                }
2823            }
2824
2825            // remove deleted messages from cache
2826            $cache_key = $mailbox.'.msg';
2827            if ($all_mode || ($start_index = $this->get_message_cache_index_min($cache_key, $uids))) {
2828                // clear cache from the lowest index on
2829                $this->clear_message_cache($cache_key, $all_mode ? 1 : $start_index);
2830            }
2831        }
2832
2833        return $deleted;
2834    }
2835
2836
2837    /**
2838     * Clear all messages in a specific mailbox
2839     *
2840     * @param string $mailbox Mailbox name
2841     *
2842     * @return int Above 0 on success
2843     */
2844    function clear_mailbox($mailbox=null)
2845    {
2846        if (!strlen($mailbox)) {
2847            $mailbox = $this->mailbox;
2848        }
2849
2850        // SELECT will set messages count for clearFolder()
2851        if ($this->conn->select($mailbox)) {
2852            $cleared = $this->conn->clearFolder($mailbox);
2853        }
2854
2855        // make sure the message count cache is cleared as well
2856        if ($cleared) {
2857            $this->clear_message_cache($mailbox.'.msg');
2858            $a_mailbox_cache = $this->get_cache('messagecount');
2859            unset($a_mailbox_cache[$mailbox]);
2860            $this->update_cache('messagecount', $a_mailbox_cache);
2861        }
2862
2863        return $cleared;
2864    }
2865
2866
2867    /**
2868     * Send IMAP expunge command and clear cache
2869     *
2870     * @param string  $mailbox     Mailbox name
2871     * @param boolean $clear_cache False if cache should not be cleared
2872     *
2873     * @return boolean True on success
2874     */
2875    function expunge($mailbox='', $clear_cache=true)
2876    {
2877        if (!strlen($mailbox)) {
2878            $mailbox = $this->mailbox;
2879        }
2880
2881        return $this->_expunge($mailbox, $clear_cache);
2882    }
2883
2884
2885    /**
2886     * Send IMAP expunge command and clear cache
2887     *
2888     * @param string  $mailbox     Mailbox name
2889     * @param boolean $clear_cache False if cache should not be cleared
2890     * @param mixed   $uids        Message UIDs as array or comma-separated string, or '*'
2891     * @return boolean True on success
2892     * @access private
2893     * @see rcube_imap::expunge()
2894     */
2895    private function _expunge($mailbox, $clear_cache=true, $uids=NULL)
2896    {
2897        if ($uids && $this->get_capability('UIDPLUS'))
2898            $a_uids = is_array($uids) ? join(',', $uids) : $uids;
2899        else
2900            $a_uids = NULL;
2901
2902        // force mailbox selection and check if mailbox is writeable
2903        // to prevent a situation when CLOSE is executed on closed
2904        // or EXPUNGE on read-only mailbox
2905        $result = $this->conn->select($mailbox);
2906        if (!$result) {
2907            return false;
2908        }
2909        if (!$this->conn->data['READ-WRITE']) {
2910            $this->conn->setError(rcube_imap_generic::ERROR_READONLY, "Mailbox is read-only");
2911            return false;
2912        }
2913
2914        // CLOSE(+SELECT) should be faster than EXPUNGE
2915        if (empty($a_uids) || $a_uids == '1:*')
2916            $result = $this->conn->close();
2917        else
2918            $result = $this->conn->expunge($mailbox, $a_uids);
2919
2920        if ($result && $clear_cache) {
2921            $this->clear_message_cache($mailbox.'.msg');
2922            $this->_clear_messagecount($mailbox);
2923        }
2924
2925        return $result;
2926    }
2927
2928
2929    /**
2930     * Parse message UIDs input
2931     *
2932     * @param mixed  $uids    UIDs array or comma-separated list or '*' or '1:*'
2933     * @param string $mailbox Mailbox name
2934     * @return array Two elements array with UIDs converted to list and ALL flag
2935     * @access private
2936     */
2937    private function _parse_uids($uids, $mailbox)
2938    {
2939        if ($uids === '*' || $uids === '1:*') {
2940            if (empty($this->search_set)) {
2941                $uids = '1:*';
2942                $all = true;
2943            }
2944            // get UIDs from current search set
2945            // @TODO: skip fetchUIDs() and work with IDs instead of UIDs (?)
2946            else {
2947                if ($this->search_threads)
2948                    $uids = $this->conn->fetchUIDs($mailbox, array_keys($this->search_set['depth']));
2949                else
2950                    $uids = $this->conn->fetchUIDs($mailbox, $this->search_set);
2951
2952                // save ID-to-UID mapping in local cache
2953                if (is_array($uids))
2954                    foreach ($uids as $id => $uid)
2955                        $this->uid_id_map[$mailbox][$uid] = $id;
2956
2957                $uids = join(',', $uids);
2958            }
2959        }
2960        else {
2961            if (is_array($uids))
2962                $uids = join(',', $uids);
2963
2964            if (preg_match('/[^0-9,]/', $uids))
2965                $uids = '';
2966        }
2967
2968        return array($uids, (bool) $all);
2969    }
2970
2971
2972    /**
2973     * Translate UID to message ID
2974     *
2975     * @param int    $uid     Message UID
2976     * @param string $mailbox Mailbox name
2977     *
2978     * @return int   Message ID
2979     */
2980    function get_id($uid, $mailbox=null)
2981    {
2982        if (!strlen($mailbox)) {
2983            $mailbox = $this->mailbox;
2984        }
2985
2986        return $this->_uid2id($uid, $mailbox);
2987    }
2988
2989
2990    /**
2991     * Translate message number to UID
2992     *
2993     * @param int    $id      Message ID
2994     * @param string $mailbox Mailbox name
2995     *
2996     * @return int   Message UID
2997     */
2998    function get_uid($id, $mailbox=null)
2999    {
3000        if (!strlen($mailbox)) {
3001            $mailbox = $this->mailbox;
3002        }
3003
3004        return $this->_id2uid($id, $mailbox);
3005    }
3006
3007
3008
3009    /* --------------------------------
3010     *        folder managment
3011     * --------------------------------*/
3012
3013    /**
3014     * Public method for listing subscribed folders
3015     *
3016     * @param   string  $root   Optional root folder
3017     * @param   string  $name   Optional name pattern
3018     * @param   string  $filter Optional filter
3019     *
3020     * @return  array   List of mailboxes/folders
3021     * @access  public
3022     */
3023    function list_mailboxes($root='', $name='*', $filter=null)
3024    {
3025        $a_mboxes = $this->_list_mailboxes($root, $name, $filter);
3026
3027        // INBOX should always be available
3028        if ((!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)) {
3029            array_unshift($a_mboxes, 'INBOX');
3030        }
3031
3032        // sort mailboxes
3033        $a_mboxes = $this->_sort_mailbox_list($a_mboxes);
3034
3035        return $a_mboxes;
3036    }
3037
3038
3039    /**
3040     * Private method for mailbox listing
3041     *
3042     * @param   string  $root   Optional root folder
3043     * @param   string  $name   Optional name pattern
3044     * @param   mixed   $filter Optional filter
3045     *
3046     * @return  array   List of mailboxes/folders
3047     * @see     rcube_imap::list_mailboxes()
3048     * @access  private
3049     */
3050    private function _list_mailboxes($root='', $name='*', $filter=null)
3051    {
3052        $cache_key = $root.':'.$name;
3053        if (!empty($filter)) {
3054            $cache_key .= ':'.(is_string($filter) ? $filter : serialize($filter));
3055        }
3056
3057        $cache_key = 'mailboxes.'.md5($cache_key);
3058
3059        // get cached folder list
3060        $a_mboxes = $this->get_cache($cache_key);
3061        if (is_array($a_mboxes)) {
3062            return $a_mboxes;
3063        }
3064
3065        $a_defaults = $a_out = array();
3066
3067        // Give plugins a chance to provide a list of mailboxes
3068        $data = rcmail::get_instance()->plugins->exec_hook('mailboxes_list',
3069            array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LSUB'));
3070
3071        if (isset($data['folders'])) {
3072            $a_folders = $data['folders'];
3073        }
3074        else {
3075            // Server supports LIST-EXTENDED, we can use selection options
3076            $config = rcmail::get_instance()->config;
3077            // #1486225: Some dovecot versions returns wrong result using LIST-EXTENDED
3078            if (!$config->get('imap_force_lsub') && $this->get_capability('LIST-EXTENDED')) {
3079                // This will also set mailbox options, LSUB doesn't do that
3080                $a_folders = $this->conn->listMailboxes($root, $name,
3081                    NULL, array('SUBSCRIBED'));
3082
3083                // remove non-existent folders
3084                if (is_array($a_folders)) {
3085                    foreach ($a_folders as $idx => $folder) {
3086                        if ($this->conn->data['LIST'] && ($opts = $this->conn->data['LIST'][$folder])
3087                            && in_array('\\NonExistent', $opts)
3088                        ) {
3089                            unset($a_folders[$idx]);
3090                        } 
3091                    }
3092                }
3093            }
3094            // retrieve list of folders from IMAP server using LSUB
3095            else {
3096                $a_folders = $this->conn->listSubscribed($root, $name);
3097            }
3098        }
3099
3100        if (!is_array($a_folders) || !sizeof($a_folders)) {
3101            $a_folders = array();
3102        }
3103
3104        // write mailboxlist to cache
3105        $this->update_cache($cache_key, $a_folders);
3106
3107        return $a_folders;
3108    }
3109
3110
3111    /**
3112     * Get a list of all folders available on the IMAP server
3113     *
3114     * @param string $root   IMAP root dir
3115     * @param string  $name   Optional name pattern
3116     * @param mixed   $filter Optional filter
3117     *
3118     * @return array Indexed array with folder names
3119     */
3120    function list_unsubscribed($root='', $name='*', $filter=null)
3121    {
3122        // @TODO: caching
3123        // Give plugins a chance to provide a list of mailboxes
3124        $data = rcmail::get_instance()->plugins->exec_hook('mailboxes_list',
3125            array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LIST'));
3126
3127        if (isset($data['folders'])) {
3128            $a_mboxes = $data['folders'];
3129        }
3130        else {
3131            // retrieve list of folders from IMAP server
3132            $a_mboxes = $this->conn->listMailboxes($root, $name);
3133        }
3134
3135        if (!is_array($a_mboxes)) {
3136            $a_mboxes = array();
3137        }
3138
3139        // INBOX should always be available
3140        if ((!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)) {
3141            array_unshift($a_mboxes, 'INBOX');
3142        }
3143
3144        // filter folders and sort them
3145        $a_mboxes = $this->_sort_mailbox_list($a_mboxes);
3146
3147        return $a_mboxes;
3148    }
3149
3150
3151    /**
3152     * Get mailbox quota information
3153     * added by Nuny
3154     *
3155     * @return mixed Quota info or False if not supported
3156     */
3157    function get_quota()
3158    {
3159        if ($this->get_capability('QUOTA'))
3160            return $this->conn->getQuota();
3161
3162        return false;
3163    }
3164
3165
3166    /**
3167     * Get mailbox size (size of all messages in a mailbox)
3168     *
3169     * @param string $mailbox Mailbox name
3170     *
3171     * @return int Mailbox size in bytes, False on error
3172     */
3173    function get_mailbox_size($mailbox)
3174    {
3175        // @TODO: could we try to use QUOTA here?
3176        $result = $this->conn->fetchHeaderIndex($mailbox, '1:*', 'SIZE', false);
3177
3178        if (is_array($result))
3179            $result = array_sum($result);
3180
3181        return $result;
3182    }
3183
3184
3185    /**
3186     * Subscribe to a specific mailbox(es)
3187     *
3188     * @param array $a_mboxes Mailbox name(s)
3189     * @return boolean True on success
3190     */
3191    function subscribe($a_mboxes)
3192    {
3193        if (!is_array($a_mboxes))
3194            $a_mboxes = array($a_mboxes);
3195
3196        // let this common function do the main work
3197        return $this->_change_subscription($a_mboxes, 'subscribe');
3198    }
3199
3200
3201    /**
3202     * Unsubscribe mailboxes
3203     *
3204     * @param array $a_mboxes Mailbox name(s)
3205     * @return boolean True on success
3206     */
3207    function unsubscribe($a_mboxes)
3208    {
3209        if (!is_array($a_mboxes))
3210            $a_mboxes = array($a_mboxes);
3211
3212        // let this common function do the main work
3213        return $this->_change_subscription($a_mboxes, 'unsubscribe');
3214    }
3215
3216
3217    /**
3218     * Create a new mailbox on the server and register it in local cache
3219     *
3220     * @param string  $mailbox   New mailbox name
3221     * @param boolean $subscribe True if the new mailbox should be subscribed
3222     *
3223     * @return boolean True on success
3224     */
3225    function create_mailbox($mailbox, $subscribe=false)
3226    {
3227        $result = $this->conn->createFolder($mailbox);
3228
3229        // try to subscribe it
3230        if ($result && $subscribe)
3231            $this->subscribe($mailbox);
3232
3233        return $result;
3234    }
3235
3236
3237    /**
3238     * Set a new name to an existing mailbox
3239     *
3240     * @param string $mailbox  Mailbox to rename
3241     * @param string $new_name New mailbox name
3242     *
3243     * @return boolean True on success
3244     */
3245    function rename_mailbox($mailbox, $new_name)
3246    {
3247        if (!strlen($new_name)) {
3248            return false;
3249        }
3250
3251        $delm = $this->get_hierarchy_delimiter();
3252
3253        // get list of subscribed folders
3254        if ((strpos($mailbox, '%') === false) && (strpos($mailbox, '*') === false)) {
3255            $a_subscribed = $this->_list_mailboxes('', $mailbox . $delm . '*');
3256            $subscribed   = $this->mailbox_exists($mailbox, true);
3257        }
3258        else {
3259            $a_subscribed = $this->_list_mailboxes();
3260            $subscribed   = in_array($mailbox, $a_subscribed);
3261        }
3262
3263        $result = $this->conn->renameFolder($mailbox, $new_name);
3264
3265        if ($result) {
3266            // unsubscribe the old folder, subscribe the new one
3267            if ($subscribed) {
3268                $this->conn->unsubscribe($mailbox);
3269                $this->conn->subscribe($new_name);
3270            }
3271
3272            // check if mailbox children are subscribed
3273            foreach ($a_subscribed as $c_subscribed) {
3274                if (preg_match('/^'.preg_quote($mailbox.$delm, '/').'/', $c_subscribed)) {
3275                    $this->conn->unsubscribe($c_subscribed);
3276                    $this->conn->subscribe(preg_replace('/^'.preg_quote($mailbox, '/').'/',
3277                        $new_name, $c_subscribed));
3278                }
3279            }
3280
3281            // clear cache
3282            $this->clear_message_cache($mailbox.'.msg');
3283            $this->clear_cache('/^mailboxes.*/', true);
3284        }
3285
3286        return $result;
3287    }
3288
3289
3290    /**
3291     * Remove mailbox from server
3292     *
3293     * @param string $mailbox Mailbox name
3294     *
3295     * @return boolean True on success
3296     */
3297    function delete_mailbox($mailbox)
3298    {
3299        $delm = $this->get_hierarchy_delimiter();
3300
3301        // get list of folders
3302        if ((strpos($mailbox, '%') === false) && (strpos($mailbox, '*') === false))
3303            $sub_mboxes = $this->list_unsubscribed('', $mailbox . $delm . '*');
3304        else
3305            $sub_mboxes = $this->list_unsubscribed();
3306
3307        // send delete command to server
3308        $result = $this->conn->deleteFolder($mailbox);
3309
3310        if ($result) {
3311            // unsubscribe mailbox
3312            $this->conn->unsubscribe($mailbox);
3313
3314            foreach ($sub_mboxes as $c_mbox) {
3315                if (preg_match('/^'.preg_quote($mailbox.$delm, '/').'/', $c_mbox)) {
3316                    $this->conn->unsubscribe($c_mbox);
3317                    if ($this->conn->deleteFolder($c_mbox)) {
3318                            $this->clear_message_cache($c_mbox.'.msg');
3319                    }
3320                }
3321            }
3322
3323            // clear mailbox-related cache
3324            $this->clear_message_cache($mailbox.'.msg');
3325            $this->clear_cache('/^mailboxes.*/', true);
3326        }
3327
3328        return $result;
3329    }
3330
3331
3332    /**
3333     * Create all folders specified as default
3334     */
3335    function create_default_folders()
3336    {
3337        // create default folders if they do not exist
3338        foreach ($this->default_folders as $folder) {
3339            if (!$this->mailbox_exists($folder))
3340                $this->create_mailbox($folder, true);
3341            else if (!$this->mailbox_exists($folder, true))
3342                $this->subscribe($folder);
3343        }
3344    }
3345
3346
3347    /**
3348     * Checks if folder exists and is subscribed
3349     *
3350     * @param string   $mailbox      Folder name
3351     * @param boolean  $subscription Enable subscription checking
3352     *
3353     * @return boolean TRUE or FALSE
3354     */
3355    function mailbox_exists($mailbox, $subscription=false)
3356    {
3357        if ($mailbox == 'INBOX') {
3358            return true;
3359        }
3360
3361        $key  = $subscription ? 'subscribed' : 'existing';
3362
3363        if (is_array($this->icache[$key]) && in_array($mailbox, $this->icache[$key]))
3364            return true;
3365
3366        if ($subscription) {
3367            $a_folders = $this->conn->listSubscribed('', $mailbox);
3368        }
3369        else {
3370            $a_folders = $this->conn->listMailboxes('', $mailbox);
3371        }
3372
3373        if (is_array($a_folders) && in_array($mailbox, $a_folders)) {
3374            $this->icache[$key][] = $mailbox;
3375            return true;
3376        }
3377
3378        return false;
3379    }
3380
3381
3382    /**
3383     * Returns the namespace where the folder is in
3384     *
3385     * @param string $mailbox Folder name
3386     *
3387     * @return string One of 'personal', 'other' or 'shared'
3388     * @access public
3389     */
3390    function mailbox_namespace($mailbox)
3391    {
3392        if ($mailbox == 'INBOX') {
3393            return 'personal';
3394        }
3395
3396        foreach ($this->namespace as $type => $namespace) {
3397            if (is_array($namespace)) {
3398                foreach ($namespace as $ns) {
3399                    if (strlen($ns[0])) {
3400                        if ((strlen($ns[0])>1 && $mailbox == substr($ns[0], 0, -1))
3401                            || strpos($mailbox, $ns[0]) === 0
3402                        ) {
3403                            return $type;
3404                        }
3405                    }
3406                }
3407            }
3408        }
3409
3410        return 'personal';
3411    }
3412
3413
3414    /**
3415     * Modify folder name according to namespace.
3416     * For output it removes prefix of the personal namespace if it's possible.
3417     * For input it adds the prefix. Use it before creating a folder in root
3418     * of the folders tree.
3419     *
3420     * @param string $mailbox Folder name
3421     * @param string $mode    Mode name (out/in)
3422     *
3423     * @return string Folder name
3424     */
3425    function mod_mailbox($mailbox, $mode = 'out')
3426    {
3427        if (!strlen($mailbox)) {
3428            return $mailbox;
3429        }
3430
3431        $prefix     = $this->namespace['prefix']; // see set_env()
3432        $prefix_len = strlen($prefix);
3433
3434        if (!$prefix_len) {
3435            return $mailbox;
3436        }
3437
3438        // remove prefix for output
3439        if ($mode == 'out') {
3440            if (substr($mailbox, 0, $prefix_len) === $prefix) {
3441                return substr($mailbox, $prefix_len);
3442            }
3443        }
3444        // add prefix for input (e.g. folder creation)
3445        else {
3446            return $prefix . $mailbox;
3447        }
3448
3449        return $mailbox;
3450    }
3451
3452
3453    /**
3454     * Gets folder options from LIST response, e.g. \Noselect, \Noinferiors
3455     *
3456     * @param string $mailbox Folder name
3457     * @param bool   $force   Set to True if options should be refreshed
3458     *                        Options are available after LIST command only
3459     *
3460     * @return array Options list
3461     */
3462    function mailbox_options($mailbox, $force=false)
3463    {
3464        if ($mailbox == 'INBOX') {
3465            return array();
3466        }
3467
3468        if (!is_array($this->conn->data['LIST']) || !is_array($this->conn->data['LIST'][$mailbox])) {
3469            if ($force) {
3470                $this->conn->listMailboxes('', $mailbox);
3471            }
3472            else {
3473                return array();
3474            }
3475        }
3476
3477        $opts = $this->conn->data['LIST'][$mailbox];
3478
3479        return is_array($opts) ? $opts : array();
3480    }
3481
3482
3483    /**
3484     * Get message header names for rcube_imap_generic::fetchHeader(s)
3485     *
3486     * @return string Space-separated list of header names
3487     */
3488    private function get_fetch_headers()
3489    {
3490        $headers = explode(' ', $this->fetch_add_headers);
3491        $headers = array_map('strtoupper', $headers);
3492
3493        if ($this->caching_enabled || $this->get_all_headers)
3494            $headers = array_merge($headers, $this->all_headers);
3495
3496        return implode(' ', array_unique($headers));
3497    }
3498
3499
3500    /* -----------------------------------------
3501     *   ACL and METADATA/ANNOTATEMORE methods
3502     * ----------------------------------------*/
3503
3504    /**
3505     * Changes the ACL on the specified mailbox (SETACL)
3506     *
3507     * @param string $mailbox Mailbox name
3508     * @param string $user    User name
3509     * @param string $acl     ACL string
3510     *
3511     * @return boolean True on success, False on failure
3512     *
3513     * @access public
3514     * @since 0.5-beta
3515     */
3516    function set_acl($mailbox, $user, $acl)
3517    {
3518        if ($this->get_capability('ACL'))
3519            return $this->conn->setACL($mailbox, $user, $acl);
3520
3521        return false;
3522    }
3523
3524
3525    /**
3526     * Removes any <identifier,rights> pair for the
3527     * specified user from the ACL for the specified
3528     * mailbox (DELETEACL)
3529     *
3530     * @param string $mailbox Mailbox name
3531     * @param string $user    User name
3532     *
3533     * @return boolean True on success, False on failure
3534     *
3535     * @access public
3536     * @since 0.5-beta
3537     */
3538    function delete_acl($mailbox, $user)
3539    {
3540        if ($this->get_capability('ACL'))
3541            return $this->conn->deleteACL($mailbox, $user);
3542
3543        return false;
3544    }
3545
3546
3547    /**
3548     * Returns the access control list for mailbox (GETACL)
3549     *
3550     * @param string $mailbox Mailbox name
3551     *
3552     * @return array User-rights array on success, NULL on error
3553     * @access public
3554     * @since 0.5-beta
3555     */
3556    function get_acl($mailbox)
3557    {
3558        if ($this->get_capability('ACL'))
3559            return $this->conn->getACL($mailbox);
3560
3561        return NULL;
3562    }
3563
3564
3565    /**
3566     * Returns information about what rights can be granted to the
3567     * user (identifier) in the ACL for the mailbox (LISTRIGHTS)
3568     *
3569     * @param string $mailbox Mailbox name
3570     * @param string $user    User name
3571     *
3572     * @return array List of user rights
3573     * @access public
3574     * @since 0.5-beta
3575     */
3576    function list_rights($mailbox, $user)
3577    {
3578        if ($this->get_capability('ACL'))
3579            return $this->conn->listRights($mailbox, $user);
3580
3581        return NULL;
3582    }
3583
3584
3585    /**
3586     * Returns the set of rights that the current user has to
3587     * mailbox (MYRIGHTS)
3588     *
3589     * @param string $mailbox Mailbox name
3590     *
3591     * @return array MYRIGHTS response on success, NULL on error
3592     * @access public
3593     * @since 0.5-beta
3594     */
3595    function my_rights($mailbox)
3596    {
3597        if ($this->get_capability('ACL'))
3598            return $this->conn->myRights($mailbox);
3599
3600        return NULL;
3601    }
3602
3603
3604    /**
3605     * Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
3606     *
3607     * @param string $mailbox Mailbox name (empty for server metadata)
3608     * @param array  $entries Entry-value array (use NULL value as NIL)
3609     *
3610     * @return boolean True on success, False on failure
3611     * @access public
3612     * @since 0.5-beta
3613     */
3614    function set_metadata($mailbox, $entries)
3615    {
3616        if ($this->get_capability('METADATA') ||
3617            (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
3618        ) {
3619            return $this->conn->setMetadata($mailbox, $entries);
3620        }
3621        else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
3622            foreach ((array)$entries as $entry => $value) {
3623                list($ent, $attr) = $this->md2annotate($entry);
3624                $entries[$entry] = array($ent, $attr, $value);
3625            }
3626            return $this->conn->setAnnotation($mailbox, $entries);
3627        }
3628
3629        return false;
3630    }
3631
3632
3633    /**
3634     * Unsets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
3635     *
3636     * @param string $mailbox Mailbox name (empty for server metadata)
3637     * @param array  $entries Entry names array
3638     *
3639     * @return boolean True on success, False on failure
3640     *
3641     * @access public
3642     * @since 0.5-beta
3643     */
3644    function delete_metadata($mailbox, $entries)
3645    {
3646        if ($this->get_capability('METADATA') || 
3647            (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
3648        ) {
3649            return $this->conn->deleteMetadata($mailbox, $entries);
3650        }
3651        else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
3652            foreach ((array)$entries as $idx => $entry) {
3653                list($ent, $attr) = $this->md2annotate($entry);
3654                $entries[$idx] = array($ent, $attr, NULL);
3655            }
3656            return $this->conn->setAnnotation($mailbox, $entries);
3657        }
3658
3659        return false;
3660    }
3661
3662
3663    /**
3664     * Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION)
3665     *
3666     * @param string $mailbox Mailbox name (empty for server metadata)
3667     * @param array  $entries Entries
3668     * @param array  $options Command options (with MAXSIZE and DEPTH keys)
3669     *
3670     * @return array Metadata entry-value hash array on success, NULL on error
3671     *
3672     * @access public
3673     * @since 0.5-beta
3674     */
3675    function get_metadata($mailbox, $entries, $options=array())
3676    {
3677        if ($this->get_capability('METADATA') || 
3678            (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
3679        ) {
3680            return $this->conn->getMetadata($mailbox, $entries, $options);
3681        }
3682        else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
3683            $queries = array();
3684            $res     = array();
3685
3686            // Convert entry names
3687            foreach ((array)$entries as $entry) {
3688                list($ent, $attr) = $this->md2annotate($entry);
3689                $queries[$attr][] = $ent;
3690            }
3691
3692            // @TODO: Honor MAXSIZE and DEPTH options
3693            foreach ($queries as $attrib => $entry)
3694                if ($result = $this->conn->getAnnotation($mailbox, $entry, $attrib))
3695                    $res = array_merge($res, $result);
3696
3697            return $res;
3698        }
3699
3700        return NULL;
3701    }
3702
3703
3704    /**
3705     * Converts the METADATA extension entry name into the correct
3706     * entry-attrib names for older ANNOTATEMORE version.
3707     *
3708     * @param string $entry Entry name
3709     *
3710     * @return array Entry-attribute list, NULL if not supported (?)
3711     */
3712    private function md2annotate($entry)
3713    {
3714        if (substr($entry, 0, 7) == '/shared') {
3715            return array(substr($entry, 7), 'value.shared');
3716        }
3717        else if (substr($entry, 0, 8) == '/private') {
3718            return array(substr($entry, 8), 'value.priv');
3719        }
3720
3721        // @TODO: log error
3722        return NULL;
3723    }
3724
3725
3726    /* --------------------------------
3727     *   internal caching methods
3728     * --------------------------------*/
3729
3730    /**
3731     * Enable or disable caching
3732     *
3733     * @param boolean $set Flag
3734     * @access public
3735     */
3736    function set_caching($set)
3737    {
3738        if ($set && is_object($this->db))
3739            $this->caching_enabled = true;
3740        else
3741            $this->caching_enabled = false;
3742    }
3743
3744
3745    /**
3746     * Returns cached value
3747     *
3748     * @param string $key Cache key
3749     * @return mixed
3750     * @access public
3751     */
3752    function get_cache($key)
3753    {
3754        // read cache (if it was not read before)
3755        if (!count($this->cache) && $this->caching_enabled) {
3756            return $this->_read_cache_record($key);
3757        }
3758
3759        return $this->cache[$key];
3760    }
3761
3762
3763    /**
3764     * Update cache
3765     *
3766     * @param string $key  Cache key
3767     * @param mixed  $data Data
3768     * @access private
3769     */
3770    private function update_cache($key, $data)
3771    {
3772        $this->cache[$key] = $data;
3773        $this->cache_changed = true;
3774        $this->cache_changes[$key] = true;
3775    }
3776
3777
3778    /**
3779     * Writes the cache
3780     *
3781     * @access private
3782     */
3783    private function write_cache()
3784    {
3785        if ($this->caching_enabled && $this->cache_changed) {
3786            foreach ($this->cache as $key => $data) {
3787                if ($this->cache_changes[$key])
3788                    $this->_write_cache_record($key, serialize($data));
3789            }
3790        }
3791    }
3792
3793
3794    /**
3795     * Clears the cache.
3796     *
3797     * @param string  $key          Cache key name or pattern
3798     * @param boolean $pattern_mode Enable it to clear all keys with name
3799     *                              matching PREG pattern in $key
3800     * @access public
3801     */
3802    function clear_cache($key=null, $pattern_mode=false)
3803    {
3804        if (!$this->caching_enabled)
3805            return;
3806
3807        if ($key === null) {
3808            foreach (array_keys($this->cache) as $key)
3809                $this->_clear_cache_record($key);
3810
3811            $this->cache = array();
3812            $this->cache_changed = false;
3813            $this->cache_changes = array();
3814        }
3815        else if ($pattern_mode) {
3816            foreach (array_keys($this->cache) as $k) {
3817                if (preg_match($key, $k)) {
3818                    $this->_clear_cache_record($k);
3819                    $this->cache_changes[$k] = false;
3820                    unset($this->cache[$key]);
3821                }
3822            }
3823            if (!count($this->cache)) {
3824                $this->cache_changed = false;
3825            }
3826        }
3827        else {
3828            $this->_clear_cache_record($key);
3829            $this->cache_changes[$key] = false;
3830            unset($this->cache[$key]);
3831        }
3832    }
3833
3834
3835    /**
3836     * Returns cached entry
3837     *
3838     * @param string $key Cache key
3839     * @return mixed Cached value
3840     * @access private
3841     */
3842    private function _read_cache_record($key)
3843    {
3844        if ($this->db) {
3845            // get cached data from DB
3846            $sql_result = $this->db->query(
3847                "SELECT cache_id, data, cache_key ".
3848                "FROM ".get_table_name('cache').
3849                " WHERE user_id=? ".
3850                    "AND cache_key LIKE 'IMAP.%'",
3851                $_SESSION['user_id']);
3852
3853            while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
3854                    $sql_key = preg_replace('/^IMAP\./', '', $sql_arr['cache_key']);
3855                $this->cache_keys[$sql_key] = $sql_arr['cache_id'];
3856                    if (!isset($this->cache[$sql_key]))
3857                        $this->cache[$sql_key] = $sql_arr['data'] ? unserialize($sql_arr['data']) : false;
3858            }
3859        }
3860
3861        return $this->cache[$key];
3862    }
3863
3864
3865    /**
3866     * Writes single cache record
3867     *
3868     * @param string $key  Cache key
3869     * @param mxied  $data Cache value
3870     * @access private
3871     */
3872    private function _write_cache_record($key, $data)
3873    {
3874        if (!$this->db)
3875            return false;
3876
3877        // update existing cache record
3878        if ($this->cache_keys[$key]) {
3879            $this->db->query(
3880                "UPDATE ".get_table_name('cache').
3881                " SET created=". $this->db->now().", data=? ".
3882                "WHERE user_id=? ".
3883                "AND cache_key=?",
3884                $data,
3885                $_SESSION['user_id'],
3886                'IMAP.'.$key);
3887        }
3888        // add new cache record
3889        else {
3890            $this->db->query(
3891                "INSERT INTO ".get_table_name('cache').
3892                " (created, user_id, cache_key, data) ".
3893                "VALUES (".$this->db->now().", ?, ?, ?)",
3894                $_SESSION['user_id'],
3895                'IMAP.'.$key,
3896                $data);
3897
3898            // get cache entry ID for this key
3899            $sql_result = $this->db->query(
3900                "SELECT cache_id ".
3901                "FROM ".get_table_name('cache').
3902                " WHERE user_id=? ".
3903                "AND cache_key=?",
3904                $_SESSION['user_id'],
3905                'IMAP.'.$key);
3906
3907            if ($sql_arr = $this->db->fetch_assoc($sql_result))
3908                $this->cache_keys[$key] = $sql_arr['cache_id'];
3909        }
3910    }
3911
3912
3913    /**
3914     * Clears cache for single record
3915     *
3916     * @param string $ket Cache key
3917     * @access private
3918     */
3919    private function _clear_cache_record($key)
3920    {
3921        $this->db->query(
3922            "DELETE FROM ".get_table_name('cache').
3923            " WHERE user_id=? ".
3924            "AND cache_key=?",
3925            $_SESSION['user_id'],
3926            'IMAP.'.$key);
3927
3928        unset($this->cache_keys[$key]);
3929    }
3930
3931
3932
3933    /* --------------------------------
3934     *   message caching methods
3935     * --------------------------------*/
3936
3937    /**
3938     * Checks if the cache is up-to-date
3939     *
3940     * @param string $mailbox   Mailbox name
3941     * @param string $cache_key Internal cache key
3942     * @return int   Cache status: -3 = off, -2 = incomplete, -1 = dirty, 1 = OK
3943     */
3944    private function check_cache_status($mailbox, $cache_key)
3945    {
3946        if (!$this->caching_enabled)
3947            return -3;
3948
3949        $cache_index = $this->get_message_cache_index($cache_key);
3950        $msg_count = $this->_messagecount($mailbox);
3951        $cache_count = count($cache_index);
3952
3953        // empty mailbox
3954        if (!$msg_count) {
3955            return $cache_count ? -2 : 1;
3956        }
3957
3958        if ($cache_count == $msg_count) {
3959            if ($this->skip_deleted) {
3960                if (!empty($this->icache['all_undeleted_idx'])) {
3961                    $uids = rcube_imap_generic::uncompressMessageSet($this->icache['all_undeleted_idx']);
3962                    $uids = array_flip($uids);
3963                    foreach ($cache_index as $uid) {
3964                        unset($uids[$uid]);
3965                    }
3966                }
3967                else {
3968                    // get all undeleted messages excluding cached UIDs
3969                    $uids = $this->search_once($mailbox, 'ALL UNDELETED NOT UID '.
3970                        rcube_imap_generic::compressMessageSet($cache_index));
3971                }
3972                if (empty($uids)) {
3973                    return 1;
3974                }
3975            } else {
3976                // get UID of the message with highest index
3977                $uid = $this->_id2uid($msg_count, $mailbox);
3978                $cache_uid = array_pop($cache_index);
3979
3980                // uids of highest message matches -> cache seems OK
3981                if ($cache_uid == $uid) {
3982                    return 1;
3983                }
3984            }
3985            // cache is dirty
3986            return -1;
3987        }
3988
3989        // if cache count differs less than 10% report as dirty
3990        return (abs($msg_count - $cache_count) < $msg_count/10) ? -1 : -2;
3991    }
3992
3993
3994    /**
3995     * @param string $key Cache key
3996     * @param string $from
3997     * @param string $to
3998     * @param string $sort_field
3999     * @param string $sort_order
4000     * @access private
4001     */
4002    private function get_message_cache($key, $from, $to, $sort_field, $sort_order)
4003    {
4004        if (!$this->caching_enabled)
4005            return NULL;
4006
4007        // use idx sort as default sorting
4008        if (!$sort_field || !in_array($sort_field, $this->db_header_fields)) {
4009            $sort_field = 'idx';
4010        }
4011
4012        $result = array();
4013
4014        $sql_result = $this->db->limitquery(
4015                "SELECT idx, uid, headers".
4016                " FROM ".get_table_name('messages').
4017                " WHERE user_id=?".
4018                " AND cache_key=?".
4019                " ORDER BY ".$this->db->quoteIdentifier($sort_field)." ".strtoupper($sort_order),
4020                $from,
4021                $to - $from,
4022                $_SESSION['user_id'],
4023                $key);
4024
4025        while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
4026            $uid = intval($sql_arr['uid']);
4027            $result[$uid] = $this->db->decode(unserialize($sql_arr['headers']));
4028
4029            // featch headers if unserialize failed
4030            if (empty($result[$uid]))
4031                $result[$uid] = $this->conn->fetchHeader(
4032                    preg_replace('/.msg$/', '', $key), $uid, true, false, $this->get_fetch_headers());
4033        }
4034
4035        return $result;
4036    }
4037
4038
4039    /**
4040     * @param string $key Cache key
4041     * @param int    $uid Message UID
4042     * @return mixed
4043     * @access private
4044     */
4045    private function &get_cached_message($key, $uid)
4046    {
4047        $internal_key = 'message';
4048
4049        if ($this->caching_enabled && !isset($this->icache[$internal_key][$uid])) {
4050            $sql_result = $this->db->query(
4051                "SELECT idx, headers, structure, message_id".
4052                " FROM ".get_table_name('messages').
4053                " WHERE user_id=?".
4054                " AND cache_key=?".
4055                " AND uid=?",
4056                $_SESSION['user_id'],
4057                $key,
4058                $uid);
4059
4060            if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
4061                $this->icache['message.id'][$uid] = intval($sql_arr['message_id']);
4062                    $this->uid_id_map[preg_replace('/\.msg$/', '', $key)][$uid] = intval($sql_arr['idx']);
4063                $this->icache[$internal_key][$uid] = $this->db->decode(unserialize($sql_arr['headers']));
4064
4065                if (is_object($this->icache[$internal_key][$uid]) && !empty($sql_arr['structure']))
4066                    $this->icache[$internal_key][$uid]->structure = $this->db->decode(unserialize($sql_arr['structure']));
4067            }
4068        }
4069
4070        return $this->icache[$internal_key][$uid];
4071    }
4072
4073
4074    /**
4075     * @param string  $key        Cache key
4076     * @param string  $sort_field Sorting column
4077     * @param string  $sort_order Sorting order
4078     * @return array Messages index
4079     * @access private
4080     */
4081    private function get_message_cache_index($key, $sort_field='idx', $sort_order='ASC')
4082    {
4083        if (!$this->caching_enabled || empty($key))
4084            return NULL;
4085
4086        // use idx sort as default
4087        if (!$sort_field || !in_array($sort_field, $this->db_header_fields))
4088            $sort_field = 'idx';
4089
4090        if (array_key_exists('index', $this->icache)
4091            && $this->icache['index']['key'] == $key
4092            && $this->icache['index']['sort_field'] == $sort_field
4093        ) {
4094            if ($this->icache['index']['sort_order'] == $sort_order)
4095                return $this->icache['index']['result'];
4096            else
4097                return array_reverse($this->icache['index']['result'], true);
4098        }
4099
4100        $this->icache['index'] = array(
4101            'result'     => array(),
4102            'key'        => $key,
4103            'sort_field' => $sort_field,
4104            'sort_order' => $sort_order,
4105        );
4106
4107        $sql_result = $this->db->query(
4108            "SELECT idx, uid".
4109            " FROM ".get_table_name('messages').
4110            " WHERE user_id=?".
4111            " AND cache_key=?".
4112            " ORDER BY ".$this->db->quote_identifier($sort_field)." ".$sort_order,
4113            $_SESSION['user_id'],
4114            $key);
4115
4116        while ($sql_arr = $this->db->fetch_assoc($sql_result))
4117            $this->icache['index']['result'][$sql_arr['idx']] = intval($sql_arr['uid']);
4118
4119        return $this->icache['index']['result'];
4120    }
4121
4122
4123    /**
4124     * @access private
4125     */
4126    private function add_message_cache($key, $index, $headers, $struct=null, $force=false, $internal_cache=false)
4127    {
4128        if (empty($key) || !is_object($headers) || empty($headers->uid))
4129            return;
4130
4131        // add to internal (fast) cache
4132        if ($internal_cache) {
4133            $this->icache['message'][$headers->uid] = clone $headers;
4134            $this->icache['message'][$headers->uid]->structure = $struct;
4135        }
4136
4137        // no further caching
4138        if (!$this->caching_enabled)
4139            return;
4140
4141        // known message id
4142        if (is_int($force) && $force > 0) {
4143            $message_id = $force;
4144        }
4145        // check for an existing record (probably headers are cached but structure not)
4146        else if (!$force) {
4147            $sql_result = $this->db->query(
4148                "SELECT message_id".
4149                " FROM ".get_table_name('messages').
4150                " WHERE user_id=?".
4151                " AND cache_key=?".
4152                " AND uid=?",
4153                $_SESSION['user_id'],
4154                $key,
4155                $headers->uid);
4156
4157            if ($sql_arr = $this->db->fetch_assoc($sql_result))
4158                $message_id = $sql_arr['message_id'];
4159        }
4160
4161        // update cache record
4162        if ($message_id) {
4163            $this->db->query(
4164                "UPDATE ".get_table_name('messages').
4165                " SET idx=?, headers=?, structure=?".
4166                " WHERE message_id=?",
4167                $index,
4168                serialize($this->db->encode(clone $headers)),
4169                is_object($struct) ? serialize($this->db->encode(clone $struct)) : NULL,
4170                $message_id
4171            );
4172        }
4173        else { // insert new record
4174            $this->db->query(
4175                "INSERT INTO ".get_table_name('messages').
4176                " (user_id, del, cache_key, created, idx, uid, subject, ".
4177                $this->db->quoteIdentifier('from').", ".
4178                $this->db->quoteIdentifier('to').", ".
4179                "cc, date, size, headers, structure)".
4180                " VALUES (?, 0, ?, ".$this->db->now().", ?, ?, ?, ?, ?, ?, ".
4181                $this->db->fromunixtime($headers->timestamp).", ?, ?, ?)",
4182                $_SESSION['user_id'],
4183                $key,
4184                $index,
4185                $headers->uid,
4186                (string)mb_substr($this->db->encode($this->decode_header($headers->subject, true)), 0, 128),
4187                (string)mb_substr($this->db->encode($this->decode_header($headers->from, true)), 0, 128),
4188                (string)mb_substr($this->db->encode($this->decode_header($headers->to, true)), 0, 128),
4189                (string)mb_substr($this->db->encode($this->decode_header($headers->cc, true)), 0, 128),
4190                (int)$headers->size,
4191                serialize($this->db->encode(clone $headers)),
4192                is_object($struct) ? serialize($this->db->encode(clone $struct)) : NULL
4193            );
4194        }
4195
4196        unset($this->icache['index']);
4197    }
4198
4199
4200    /**
4201     * @access private
4202     */
4203    private function remove_message_cache($key, $ids, $idx=false)
4204    {
4205        if (!$this->caching_enabled)
4206            return;
4207
4208        $this->db->query(
4209            "DELETE FROM ".get_table_name('messages').
4210            " WHERE user_id=?".
4211            " AND cache_key=?".
4212            " AND ".($idx ? "idx" : "uid")." IN (".$this->db->array2list($ids, 'integer').")",
4213            $_SESSION['user_id'],
4214            $key);
4215
4216        unset($this->icache['index']);
4217    }
4218
4219
4220    /**
4221     * @param string $key         Cache key
4222     * @param int    $start_index Start index
4223     * @access private
4224     */
4225    private function clear_message_cache($key, $start_index=1)
4226    {
4227        if (!$this->caching_enabled)
4228            return;
4229
4230        $this->db->query(
4231            "DELETE FROM ".get_table_name('messages').
4232            " WHERE user_id=?".
4233            " AND cache_key=?".
4234            " AND idx>=?",
4235            $_SESSION['user_id'], $key, $start_index);
4236
4237        unset($this->icache['index']);
4238    }
4239
4240
4241    /**
4242     * @access private
4243     */
4244    private function get_message_cache_index_min($key, $uids=NULL)
4245    {
4246        if (!$this->caching_enabled)
4247            return;
4248
4249        if (!empty($uids) && !is_array($uids)) {
4250            if ($uids == '*' || $uids == '1:*')
4251                $uids = NULL;
4252            else
4253                $uids = explode(',', $uids);
4254        }
4255
4256        $sql_result = $this->db->query(
4257            "SELECT MIN(idx) AS minidx".
4258            " FROM ".get_table_name('messages').
4259            " WHERE  user_id=?".
4260            " AND    cache_key=?"
4261            .(!empty($uids) ? " AND uid IN (".$this->db->array2list($uids, 'integer').")" : ''),
4262            $_SESSION['user_id'],
4263            $key);
4264
4265        if ($sql_arr = $this->db->fetch_assoc($sql_result))
4266            return $sql_arr['minidx'];
4267        else
4268            return 0;
4269    }
4270
4271
4272    /**
4273     * @param string $key Cache key
4274     * @param int    $id  Message (sequence) ID
4275     * @return int Message UID
4276     * @access private
4277     */
4278    private function get_cache_id2uid($key, $id)
4279    {
4280        if (!$this->caching_enabled)
4281            return null;
4282
4283        if (array_key_exists('index', $this->icache)
4284            && $this->icache['index']['key'] == $key
4285        ) {
4286            return $this->icache['index']['result'][$id];
4287        }
4288
4289        $sql_result = $this->db->query(
4290            "SELECT uid".
4291            " FROM ".get_table_name('messages').
4292            " WHERE user_id=?".
4293            " AND cache_key=?".
4294            " AND idx=?",
4295            $_SESSION['user_id'], $key, $id);
4296
4297        if ($sql_arr = $this->db->fetch_assoc($sql_result))
4298            return intval($sql_arr['uid']);
4299
4300        return null;
4301    }
4302
4303
4304    /**
4305     * @param string $key Cache key
4306     * @param int    $uid Message UID
4307     * @return int Message (sequence) ID
4308     * @access private
4309     */
4310    private function get_cache_uid2id($key, $uid)
4311    {
4312        if (!$this->caching_enabled)
4313            return null;
4314
4315        if (array_key_exists('index', $this->icache)
4316            && $this->icache['index']['key'] == $key
4317        ) {
4318            return array_search($uid, $this->icache['index']['result']);
4319        }
4320
4321        $sql_result = $this->db->query(
4322            "SELECT idx".
4323            " FROM ".get_table_name('messages').
4324            " WHERE user_id=?".
4325            " AND cache_key=?".
4326            " AND uid=?",
4327            $_SESSION['user_id'], $key, $uid);
4328
4329        if ($sql_arr = $this->db->fetch_assoc($sql_result))
4330            return intval($sql_arr['idx']);
4331
4332        return null;
4333    }
4334
4335
4336    /* --------------------------------
4337     *   encoding/decoding methods
4338     * --------------------------------*/
4339
4340    /**
4341     * Split an address list into a structured array list
4342     *
4343     * @param string  $input  Input string
4344     * @param int     $max    List only this number of addresses
4345     * @param boolean $decode Decode address strings
4346     * @return array  Indexed list of addresses
4347     */
4348    function decode_address_list($input, $max=null, $decode=true)
4349    {
4350        $a = $this->_parse_address_list($input, $decode);
4351        $out = array();
4352        // Special chars as defined by RFC 822 need to in quoted string (or escaped).
4353        $special_chars = '[\(\)\<\>\\\.\[\]@,;:"]';
4354
4355        if (!is_array($a))
4356            return $out;
4357
4358        $c = count($a);
4359        $j = 0;
4360
4361        foreach ($a as $val) {
4362            $j++;
4363            $address = trim($val['address']);
4364            $name    = trim($val['name']);
4365
4366            if ($name && $address && $name != $address)
4367                $string = sprintf('%s <%s>', preg_match("/$special_chars/", $name) ? '"'.addcslashes($name, '"').'"' : $name, $address);
4368            else if ($address)
4369                $string = $address;
4370            else if ($name)
4371                $string = $name;
4372
4373            $out[$j] = array(
4374                'name'   => $name,
4375                'mailto' => $address,
4376                'string' => $string
4377            );
4378
4379            if ($max && $j==$max)
4380                break;
4381        }
4382
4383        return $out;
4384    }
4385
4386
4387    /**
4388     * Decode a message header value
4389     *
4390     * @param string  $input         Header value
4391     * @param boolean $remove_quotas Remove quotes if necessary
4392     * @return string Decoded string
4393     */
4394    function decode_header($input, $remove_quotes=false)
4395    {
4396        $str = rcube_imap::decode_mime_string((string)$input, $this->default_charset);
4397        if ($str[0] == '"' && $remove_quotes)
4398            $str = str_replace('"', '', $str);
4399
4400        return $str;
4401    }
4402
4403
4404    /**
4405     * Decode a mime-encoded string to internal charset
4406     *
4407     * @param string $input    Header value
4408     * @param string $fallback Fallback charset if none specified
4409     *
4410     * @return string Decoded string
4411     * @static
4412     */
4413    public static function decode_mime_string($input, $fallback=null)
4414    {
4415        if (!empty($fallback)) {
4416            $default_charset = $fallback;
4417        }
4418        else {
4419            $default_charset = rcmail::get_instance()->config->get('default_charset', 'ISO-8859-1');
4420        }
4421
4422        // rfc: all line breaks or other characters not found
4423        // in the Base64 Alphabet must be ignored by decoding software
4424        // delete all blanks between MIME-lines, differently we can
4425        // receive unnecessary blanks and broken utf-8 symbols
4426        $input = preg_replace("/\?=\s+=\?/", '?==?', $input);
4427
4428        // encoded-word regexp
4429        $re = '/=\?([^?]+)\?([BbQq])\?([^?\n]*)\?=/';
4430
4431        // Find all RFC2047's encoded words
4432        if (preg_match_all($re, $input, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {
4433            // Initialize variables
4434            $tmp   = array();
4435            $out   = '';
4436            $start = 0;
4437
4438            foreach ($matches as $idx => $m) {
4439                $pos      = $m[0][1];
4440                $charset  = $m[1][0];
4441                $encoding = $m[2][0];
4442                $text     = $m[3][0];
4443                $length   = strlen($m[0][0]);
4444
4445                // Append everything that is before the text to be decoded
4446                if ($start != $pos) {
4447                    $substr = substr($input, $start, $pos-$start);
4448                    $out   .= rcube_charset_convert($substr, $default_charset);
4449                    $start  = $pos;
4450                }
4451                $start += $length;
4452
4453                // Per RFC2047, each string part "MUST represent an integral number
4454                // of characters . A multi-octet character may not be split across
4455                // adjacent encoded-words." However, some mailers break this, so we
4456                // try to handle characters spanned across parts anyway by iterating
4457                // through and aggregating sequential encoded parts with the same
4458                // character set and encoding, then perform the decoding on the
4459                // aggregation as a whole.
4460
4461                $tmp[] = $text;
4462                if ($next_match = $matches[$idx+1]) {
4463                    if ($next_match[0][1] == $start
4464                        && $next_match[1][0] == $charset
4465                        && $next_match[2][0] == $encoding
4466                    ) {
4467                        continue;
4468                    }
4469                }
4470
4471                $count = count($tmp);
4472                $text  = '';
4473
4474                // Decode and join encoded-word's chunks
4475                if ($encoding == 'B' || $encoding == 'b') {
4476                    // base64 must be decoded a segment at a time
4477                    for ($i=0; $i<$count; $i++)
4478                        $text .= base64_decode($tmp[$i]);
4479                }
4480                else { //if ($encoding == 'Q' || $encoding == 'q') {
4481                    // quoted printable can be combined and processed at once
4482                    for ($i=0; $i<$count; $i++)
4483                        $text .= $tmp[$i];
4484
4485                    $text = str_replace('_', ' ', $text);
4486                    $text = quoted_printable_decode($text);
4487                }
4488
4489                $out .= rcube_charset_convert($text, $charset);
4490                $tmp = array();
4491            }
4492
4493            // add the last part of the input string
4494            if ($start != strlen($input)) {
4495                $out .= rcube_charset_convert(substr($input, $start), $default_charset);
4496            }
4497
4498            // return the results
4499            return $out;
4500        }
4501
4502        // no encoding information, use fallback
4503        return rcube_charset_convert($input, $default_charset);
4504    }
4505
4506
4507    /**
4508     * Decode a mime part
4509     *
4510     * @param string $input    Input string
4511     * @param string $encoding Part encoding
4512     * @return string Decoded string
4513     */
4514    function mime_decode($input, $encoding='7bit')
4515    {
4516        switch (strtolower($encoding)) {
4517        case 'quoted-printable':
4518            return quoted_printable_decode($input);
4519        case 'base64':
4520            return base64_decode($input);
4521        case 'x-uuencode':
4522        case 'x-uue':
4523        case 'uue':
4524        case 'uuencode':
4525            return convert_uudecode($input);
4526        case '7bit':
4527        default:
4528            return $input;
4529        }
4530    }
4531
4532
4533    /**
4534     * Convert body charset to RCMAIL_CHARSET according to the ctype_parameters
4535     *
4536     * @param string $body        Part body to decode
4537     * @param string $ctype_param Charset to convert from
4538     * @return string Content converted to internal charset
4539     */
4540    function charset_decode($body, $ctype_param)
4541    {
4542        if (is_array($ctype_param) && !empty($ctype_param['charset']))
4543            return rcube_charset_convert($body, $ctype_param['charset']);
4544
4545        // defaults to what is specified in the class header
4546        return rcube_charset_convert($body,  $this->default_charset);
4547    }
4548
4549
4550    /* --------------------------------
4551     *         private methods
4552     * --------------------------------*/
4553
4554    /**
4555     * Validate the given input and save to local properties
4556     *
4557     * @param string $sort_field Sort column
4558     * @param string $sort_order Sort order
4559     * @access private
4560     */
4561    private function _set_sort_order($sort_field, $sort_order)
4562    {
4563        if ($sort_field != null)
4564            $this->sort_field = asciiwords($sort_field);
4565        if ($sort_order != null)
4566            $this->sort_order = strtoupper($sort_order) == 'DESC' ? 'DESC' : 'ASC';
4567    }
4568
4569
4570    /**
4571     * Sort mailboxes first by default folders and then in alphabethical order
4572     *
4573     * @param array $a_folders Mailboxes list
4574     * @access private
4575     */
4576    private function _sort_mailbox_list($a_folders)
4577    {
4578        $a_out = $a_defaults = $folders = array();
4579
4580        $delimiter = $this->get_hierarchy_delimiter();
4581
4582        // find default folders and skip folders starting with '.'
4583        foreach ($a_folders as $i => $folder) {
4584            if ($folder[0] == '.')
4585                continue;
4586
4587            if (($p = array_search($folder, $this->default_folders)) !== false && !$a_defaults[$p])
4588                $a_defaults[$p] = $folder;
4589            else
4590                $folders[$folder] = rcube_charset_convert($folder, 'UTF7-IMAP');
4591        }
4592
4593        // sort folders and place defaults on the top
4594        asort($folders, SORT_LOCALE_STRING);
4595        ksort($a_defaults);
4596        $folders = array_merge($a_defaults, array_keys($folders));
4597
4598        // finally we must rebuild the list to move
4599        // subfolders of default folders to their place...
4600        // ...also do this for the rest of folders because
4601        // asort() is not properly sorting case sensitive names
4602        while (list($key, $folder) = each($folders)) {
4603            // set the type of folder name variable (#1485527)
4604            $a_out[] = (string) $folder;
4605            unset($folders[$key]);
4606            $this->_rsort($folder, $delimiter, $folders, $a_out);
4607        }
4608
4609        return $a_out;
4610    }
4611
4612
4613    /**
4614     * @access private
4615     */
4616    private function _rsort($folder, $delimiter, &$list, &$out)
4617    {
4618        while (list($key, $name) = each($list)) {
4619                if (strpos($name, $folder.$delimiter) === 0) {
4620                    // set the type of folder name variable (#1485527)
4621                $out[] = (string) $name;
4622                    unset($list[$key]);
4623                    $this->_rsort($name, $delimiter, $list, $out);
4624                }
4625        }
4626        reset($list);
4627    }
4628
4629
4630    /**
4631     * @param int    $uid     Message UID
4632     * @param string $mailbox Mailbox name
4633     * @return int Message (sequence) ID
4634     * @access private
4635     */
4636    private function _uid2id($uid, $mailbox=NULL)
4637    {
4638        if (!strlen($mailbox)) {
4639            $mailbox = $this->mailbox;
4640        }
4641
4642        if (!isset($this->uid_id_map[$mailbox][$uid])) {
4643            if (!($id = $this->get_cache_uid2id($mailbox.'.msg', $uid)))
4644                $id = $this->conn->UID2ID($mailbox, $uid);
4645
4646            $this->uid_id_map[$mailbox][$uid] = $id;
4647        }
4648
4649        return $this->uid_id_map[$mailbox][$uid];
4650    }
4651
4652
4653    /**
4654     * @param int    $id      Message (sequence) ID
4655     * @param string $mailbox Mailbox name
4656     *
4657     * @return int Message UID
4658     * @access private
4659     */
4660    private function _id2uid($id, $mailbox=null)
4661    {
4662        if (!strlen($mailbox)) {
4663            $mailbox = $this->mailbox;
4664        }
4665
4666        if ($uid = array_search($id, (array)$this->uid_id_map[$mailbox])) {
4667            return $uid;
4668        }
4669
4670        if (!($uid = $this->get_cache_id2uid($mailbox.'.msg', $id))) {
4671            $uid = $this->conn->ID2UID($mailbox, $id);
4672        }
4673
4674        $this->uid_id_map[$mailbox][$uid] = $id;
4675
4676        return $uid;
4677    }
4678
4679
4680    /**
4681     * Subscribe/unsubscribe a list of mailboxes and update local cache
4682     * @access private
4683     */
4684    private function _change_subscription($a_mboxes, $mode)
4685    {
4686        $updated = false;
4687
4688        if (is_array($a_mboxes))
4689            foreach ($a_mboxes as $i => $mailbox) {
4690                $a_mboxes[$i] = $mailbox;
4691
4692                if ($mode == 'subscribe')
4693                    $updated = $this->conn->subscribe($mailbox);
4694                else if ($mode == 'unsubscribe')
4695                    $updated = $this->conn->unsubscribe($mailbox);
4696            }
4697
4698        // get cached mailbox list
4699        if ($updated) {
4700            $a_mailbox_cache = $this->get_cache('mailboxes');
4701            if (!is_array($a_mailbox_cache))
4702                return $updated;
4703
4704            // modify cached list
4705            if ($mode == 'subscribe')
4706                $a_mailbox_cache = array_merge($a_mailbox_cache, $a_mboxes);
4707            else if ($mode == 'unsubscribe')
4708                $a_mailbox_cache = array_diff($a_mailbox_cache, $a_mboxes);
4709
4710            // write mailboxlist to cache
4711            $this->update_cache('mailboxes', $this->_sort_mailbox_list($a_mailbox_cache));
4712        }
4713
4714        return $updated;
4715    }
4716
4717
4718    /**
4719     * Increde/decrese messagecount for a specific mailbox
4720     * @access private
4721     */
4722    private function _set_messagecount($mailbox, $mode, $increment)
4723    {
4724        $mode = strtoupper($mode);
4725        $a_mailbox_cache = $this->get_cache('messagecount');
4726
4727        if (!is_array($a_mailbox_cache[$mailbox]) || !isset($a_mailbox_cache[$mailbox][$mode]) || !is_numeric($increment))
4728            return false;
4729
4730        // add incremental value to messagecount
4731        $a_mailbox_cache[$mailbox][$mode] += $increment;
4732
4733        // there's something wrong, delete from cache
4734        if ($a_mailbox_cache[$mailbox][$mode] < 0)
4735            unset($a_mailbox_cache[$mailbox][$mode]);
4736
4737        // write back to cache
4738        $this->update_cache('messagecount', $a_mailbox_cache);
4739
4740        return true;
4741    }
4742
4743
4744    /**
4745     * Remove messagecount of a specific mailbox from cache
4746     * @access private
4747     */
4748    private function _clear_messagecount($mailbox, $mode=null)
4749    {
4750        $a_mailbox_cache = $this->get_cache('messagecount');
4751
4752        if (is_array($a_mailbox_cache[$mailbox])) {
4753            if ($mode) {
4754                unset($a_mailbox_cache[$mailbox][$mode]);
4755            }
4756            else {
4757                unset($a_mailbox_cache[$mailbox]);
4758            }
4759            $this->update_cache('messagecount', $a_mailbox_cache);
4760        }
4761    }
4762
4763
4764    /**
4765     * Split RFC822 header string into an associative array
4766     * @access private
4767     */
4768    private function _parse_headers($headers)
4769    {
4770        $a_headers = array();
4771        $headers = preg_replace('/\r?\n(\t| )+/', ' ', $headers);
4772        $lines = explode("\n", $headers);
4773        $c = count($lines);
4774
4775        for ($i=0; $i<$c; $i++) {
4776            if ($p = strpos($lines[$i], ': ')) {
4777                $field = strtolower(substr($lines[$i], 0, $p));
4778                $value = trim(substr($lines[$i], $p+1));
4779                if (!empty($value))
4780                    $a_headers[$field] = $value;
4781            }
4782        }
4783
4784        return $a_headers;
4785    }
4786
4787
4788    /**
4789     * @access private
4790     */
4791    private function _parse_address_list($str, $decode=true)
4792    {
4793        // remove any newlines and carriage returns before
4794        $str = preg_replace('/\r?\n(\s|\t)?/', ' ', $str);
4795
4796        // extract list items, remove comments
4797        $str = self::explode_header_string(',;', $str, true);
4798        $result = array();
4799
4800        foreach ($str as $key => $val) {
4801            $name    = '';
4802            $address = '';
4803            $val     = trim($val);
4804
4805            if (preg_match('/(.*)<(\S+@\S+)>$/', $val, $m)) {
4806                $address = $m[2];
4807                $name    = trim($m[1]);
4808            }
4809            else if (preg_match('/^(\S+@\S+)$/', $val, $m)) {
4810                $address = $m[1];
4811                $name    = '';
4812            }
4813            else {
4814                $name = $val;
4815            }
4816
4817            // dequote and/or decode name
4818            if ($name) {
4819                if ($name[0] == '"') {
4820                    $name = substr($name, 1, -1);
4821                    $name = stripslashes($name);
4822                }
4823                if ($decode) {
4824                    $name = $this->decode_header($name);
4825                }
4826            }
4827
4828            if (!$address && $name) {
4829                $address = $name;
4830            }
4831
4832            if ($address) {
4833                $result[$key] = array('name' => $name, 'address' => $address);
4834            }
4835        }
4836
4837        return $result;
4838    }
4839
4840
4841    /**
4842     * Explodes header (e.g. address-list) string into array of strings
4843     * using specified separator characters with proper handling
4844     * of quoted-strings and comments (RFC2822)
4845     *
4846     * @param string $separator       String containing separator characters
4847     * @param string $str             Header string
4848     * @param bool   $remove_comments Enable to remove comments
4849     *
4850     * @return array Header items
4851     */
4852    static function explode_header_string($separator, $str, $remove_comments=false)
4853    {
4854        $length  = strlen($str);
4855        $result  = array();
4856        $quoted  = false;
4857        $comment = 0;
4858        $out     = '';
4859
4860        for ($i=0; $i<$length; $i++) {
4861            // we're inside a quoted string
4862            if ($quoted) {
4863                if ($str[$i] == '"') {
4864                    $quoted = false;
4865                }
4866                else if ($str[$i] == '\\') {
4867                    if ($comment <= 0) {
4868                        $out .= '\\';
4869                    }
4870                    $i++;
4871                }
4872            }
4873            // we're inside a comment string
4874            else if ($comment > 0) {
4875                    if ($str[$i] == ')') {
4876                        $comment--;
4877                    }
4878                    else if ($str[$i] == '(') {
4879                        $comment++;
4880                    }
4881                    else if ($str[$i] == '\\') {
4882                        $i++;
4883                    }
4884                    continue;
4885            }
4886            // separator, add to result array
4887            else if (strpos($separator, $str[$i]) !== false) {
4888                    if ($out) {
4889                        $result[] = $out;
4890                    }
4891                    $out = '';
4892                    continue;
4893            }
4894            // start of quoted string
4895            else if ($str[$i] == '"') {
4896                    $quoted = true;
4897            }
4898            // start of comment
4899            else if ($remove_comments && $str[$i] == '(') {
4900                    $comment++;
4901            }
4902
4903            if ($comment <= 0) {
4904                $out .= $str[$i];
4905            }
4906        }
4907
4908        if ($out && $comment <= 0) {
4909            $result[] = $out;
4910        }
4911
4912        return $result;
4913    }
4914
4915
4916    /**
4917     * This is our own debug handler for the IMAP connection
4918     * @access public
4919     */
4920    public function debug_handler(&$imap, $message)
4921    {
4922        write_log('imap', $message);
4923    }
4924
4925}  // end class rcube_imap
4926
4927
4928/**
4929 * Class representing a message part
4930 *
4931 * @package Mail
4932 */
4933class rcube_message_part
4934{
4935    var $mime_id = '';
4936    var $ctype_primary = 'text';
4937    var $ctype_secondary = 'plain';
4938    var $mimetype = 'text/plain';
4939    var $disposition = '';
4940    var $filename = '';
4941    var $encoding = '8bit';
4942    var $charset = '';
4943    var $size = 0;
4944    var $headers = array();
4945    var $d_parameters = array();
4946    var $ctype_parameters = array();
4947
4948    function __clone()
4949    {
4950        if (isset($this->parts))
4951            foreach ($this->parts as $idx => $part)
4952                if (is_object($part))
4953                        $this->parts[$idx] = clone $part;
4954    }
4955}
4956
4957
4958/**
4959 * Class for sorting an array of rcube_mail_header objects in a predetermined order.
4960 *
4961 * @package Mail
4962 * @author Eric Stadtherr
4963 */
4964class rcube_header_sorter
4965{
4966    var $sequence_numbers = array();
4967
4968    /**
4969     * Set the predetermined sort order.
4970     *
4971     * @param array $seqnums Numerically indexed array of IMAP message sequence numbers
4972     */
4973    function set_sequence_numbers($seqnums)
4974    {
4975        $this->sequence_numbers = array_flip($seqnums);
4976    }
4977
4978    /**
4979     * Sort the array of header objects
4980     *
4981     * @param array $headers Array of rcube_mail_header objects indexed by UID
4982     */
4983    function sort_headers(&$headers)
4984    {
4985        /*
4986        * uksort would work if the keys were the sequence number, but unfortunately
4987        * the keys are the UIDs.  We'll use uasort instead and dereference the value
4988        * to get the sequence number (in the "id" field).
4989        *
4990        * uksort($headers, array($this, "compare_seqnums"));
4991        */
4992        uasort($headers, array($this, "compare_seqnums"));
4993    }
4994
4995    /**
4996     * Sort method called by uasort()
4997     *
4998     * @param rcube_mail_header $a
4999     * @param rcube_mail_header $b
5000     */
5001    function compare_seqnums($a, $b)
5002    {
5003        // First get the sequence number from the header object (the 'id' field).
5004        $seqa = $a->id;
5005        $seqb = $b->id;
5006
5007        // then find each sequence number in my ordered list
5008        $posa = isset($this->sequence_numbers[$seqa]) ? intval($this->sequence_numbers[$seqa]) : -1;
5009        $posb = isset($this->sequence_numbers[$seqb]) ? intval($this->sequence_numbers[$seqb]) : -1;
5010
5011        // return the relative position as the comparison value
5012        return $posa - $posb;
5013    }
5014}
Note: See TracBrowser for help on using the repository browser.