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

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