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

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