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

Last change on this file since 5557 was 5557, checked in by alec, 18 months ago
  • Fixed issues with big memory allocation of IMAP results, improved a lot of rcube_imap class
  • 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     */
1404    function search($mailbox='', $str='ALL', $charset=NULL, $sort_field=NULL)
1405    {
1406        if (!$str)
1407            $str = 'ALL';
1408
1409        if (!strlen($mailbox)) {
1410            $mailbox = $this->mailbox;
1411        }
1412
1413        $results = $this->_search_index($mailbox, $str, $charset, $sort_field);
1414
1415        $this->set_search_set($str, $results, $charset, $sort_field,
1416            $this->threading || $this->search_sorted ? true : false);
1417    }
1418
1419
1420    /**
1421     * Private search method
1422     *
1423     * @param string $mailbox    Mailbox name
1424     * @param string $criteria   Search criteria
1425     * @param string $charset    Charset
1426     * @param string $sort_field Sorting field
1427     *
1428     * @return rcube_result_index|rcube_result_thread  Search results (UIDs)
1429     * @see rcube_imap::search()
1430     */
1431    private function _search_index($mailbox, $criteria='ALL', $charset=NULL, $sort_field=NULL)
1432    {
1433        $orig_criteria = $criteria;
1434
1435        if ($this->skip_deleted && !preg_match('/UNDELETED/', $criteria))
1436            $criteria = 'UNDELETED '.$criteria;
1437
1438        if ($this->threading) {
1439            $threads = $this->conn->thread($mailbox, $this->threading, $criteria, true, $charset);
1440
1441            // Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
1442            // but I've seen that Courier doesn't support UTF-8)
1443            if ($threads->isError() && $charset && $charset != 'US-ASCII')
1444                $threads = $this->conn->thread($mailbox, $this->threading,
1445                    $this->convert_criteria($criteria, $charset), true, 'US-ASCII');
1446
1447            return $threads;
1448        }
1449
1450        if ($sort_field && $this->get_capability('SORT')) {
1451            $charset  = $charset ? $charset : $this->default_charset;
1452            $messages = $this->conn->sort($mailbox, $sort_field, $criteria, true, $charset);
1453
1454            // Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
1455            // but I've seen Courier with disabled UTF-8 support)
1456            if ($messages->isError() && $charset && $charset != 'US-ASCII')
1457                $messages = $this->conn->sort($mailbox, $sort_field,
1458                    $this->convert_criteria($criteria, $charset), true, 'US-ASCII');
1459
1460            if (!$messages->isError()) {
1461                $this->search_sorted = true;
1462                return $messages;
1463            }
1464        }
1465
1466        $messages = $this->conn->search($mailbox,
1467            ($charset ? "CHARSET $charset " : '') . $criteria, true);
1468
1469        // Error, try with US-ASCII (some servers may support only US-ASCII)
1470        if ($messages->isError() && $charset && $charset != 'US-ASCII')
1471            $messages = $this->conn->search($mailbox,
1472                'CHARSET US-ASCII ' . $this->convert_criteria($criteria, $charset), true);
1473
1474        $this->search_sorted = false;
1475
1476        return $messages;
1477    }
1478
1479
1480    /**
1481     * Direct (real and simple) SEARCH request to IMAP server,
1482     * without result sorting and caching
1483     *
1484     * @param  string  $mailbox Mailbox name to search in
1485     * @param  string  $str     Search string
1486     * @param  boolean $ret_uid True if UIDs should be returned
1487     *
1488     * @return rcube_result_index  Search result (UIDs)
1489     */
1490    function search_once($mailbox='', $str='ALL')
1491    {
1492        if (!$str)
1493            return 'ALL';
1494
1495        if (!strlen($mailbox)) {
1496            $mailbox = $this->mailbox;
1497        }
1498
1499        $index = $this->conn->search($mailbox, $str, true);
1500
1501        return $index;
1502    }
1503
1504
1505    /**
1506     * Converts charset of search criteria string
1507     *
1508     * @param  string  $str          Search string
1509     * @param  string  $charset      Original charset
1510     * @param  string  $dest_charset Destination charset (default US-ASCII)
1511     * @return string  Search string
1512     * @access private
1513     */
1514    private function convert_criteria($str, $charset, $dest_charset='US-ASCII')
1515    {
1516        // convert strings to US_ASCII
1517        if (preg_match_all('/\{([0-9]+)\}\r\n/', $str, $matches, PREG_OFFSET_CAPTURE)) {
1518            $last = 0; $res = '';
1519            foreach ($matches[1] as $m) {
1520                $string_offset = $m[1] + strlen($m[0]) + 4; // {}\r\n
1521                $string = substr($str, $string_offset - 1, $m[0]);
1522                $string = rcube_charset_convert($string, $charset, $dest_charset);
1523                if (!$string)
1524                    continue;
1525                $res .= sprintf("%s{%d}\r\n%s", substr($str, $last, $m[1] - $last - 1), strlen($string), $string);
1526                $last = $m[0] + $string_offset - 1;
1527            }
1528            if ($last < strlen($str))
1529                $res .= substr($str, $last, strlen($str)-$last);
1530        }
1531        else // strings for conversion not found
1532            $res = $str;
1533
1534        return $res;
1535    }
1536
1537
1538    /**
1539     * Refresh saved search set
1540     *
1541     * @return array Current search set
1542     */
1543    function refresh_search()
1544    {
1545        if (!empty($this->search_string)) {
1546            $this->search('', $this->search_string, $this->search_charset, $this->search_sort_field);
1547        }
1548
1549        return $this->get_search_set();
1550    }
1551
1552
1553    /**
1554     * Check if the given message UID is part of the current search set
1555     *
1556     * @param string $msgid Message UID
1557     *
1558     * @return boolean True on match or if no search request is stored
1559     */
1560    function in_searchset($uid)
1561    {
1562        if (!empty($this->search_string)) {
1563            return $this->search_set->exists($uid);
1564        }
1565        return true;
1566    }
1567
1568
1569    /**
1570     * Return message headers object of a specific message
1571     *
1572     * @param int     $id       Message sequence ID or UID
1573     * @param string  $mailbox  Mailbox to read from
1574     * @param bool    $force    True to skip cache
1575     *
1576     * @return rcube_mail_header Message headers
1577     */
1578    function get_headers($uid, $mailbox = null, $force = false)
1579    {
1580        if (!strlen($mailbox)) {
1581            $mailbox = $this->mailbox;
1582        }
1583
1584        // get cached headers
1585        if (!$force && $uid && ($mcache = $this->get_mcache_engine())) {
1586            $headers = $mcache->get_message($mailbox, $uid);
1587        }
1588        else {
1589            $headers = $this->conn->fetchHeader(
1590                $mailbox, $uid, true, true, $this->get_fetch_headers());
1591        }
1592
1593        return $headers;
1594    }
1595
1596
1597    /**
1598     * Fetch message headers and body structure from the IMAP server and build
1599     * an object structure similar to the one generated by PEAR::Mail_mimeDecode
1600     *
1601     * @param int     $uid      Message UID to fetch
1602     * @param string  $mailbox  Mailbox to read from
1603     *
1604     * @return object rcube_mail_header Message data
1605     */
1606    function get_message($uid, $mailbox = null)
1607    {
1608        if (!strlen($mailbox)) {
1609            $mailbox = $this->mailbox;
1610        }
1611
1612        // Check internal cache
1613        if (!empty($this->icache['message'])) {
1614            if (($headers = $this->icache['message']) && $headers->uid == $uid) {
1615                return $headers;
1616            }
1617        }
1618
1619        $headers = $this->get_headers($uid, $mailbox);
1620
1621        // message doesn't exist?
1622        if (empty($headers))
1623            return null;
1624
1625        // structure might be cached
1626        if (!empty($headers->structure))
1627            return $headers;
1628
1629        $this->_msg_uid = $uid;
1630
1631        if (empty($headers->bodystructure)) {
1632            $headers->bodystructure = $this->conn->getStructure($mailbox, $uid, true);
1633        }
1634
1635        $structure = $headers->bodystructure;
1636
1637        if (empty($structure))
1638            return $headers;
1639
1640        // set message charset from message headers
1641        if ($headers->charset)
1642            $this->struct_charset = $headers->charset;
1643        else
1644            $this->struct_charset = $this->_structure_charset($structure);
1645
1646        $headers->ctype = strtolower($headers->ctype);
1647
1648        // Here we can recognize malformed BODYSTRUCTURE and
1649        // 1. [@TODO] parse the message in other way to create our own message structure
1650        // 2. or just show the raw message body.
1651        // Example of structure for malformed MIME message:
1652        // ("text" "plain" NIL NIL NIL "7bit" 2154 70 NIL NIL NIL)
1653        if ($headers->ctype && !is_array($structure[0]) && $headers->ctype != 'text/plain'
1654            && strtolower($structure[0].'/'.$structure[1]) == 'text/plain') {
1655            // we can handle single-part messages, by simple fix in structure (#1486898)
1656            if (preg_match('/^(text|application)\/(.*)/', $headers->ctype, $m)) {
1657                $structure[0] = $m[1];
1658                $structure[1] = $m[2];
1659            }
1660            else
1661                return $headers;
1662        }
1663
1664        $struct = &$this->_structure_part($structure, 0, '', $headers);
1665
1666        // don't trust given content-type
1667        if (empty($struct->parts) && !empty($headers->ctype)) {
1668            $struct->mime_id = '1';
1669            $struct->mimetype = strtolower($headers->ctype);
1670            list($struct->ctype_primary, $struct->ctype_secondary) = explode('/', $struct->mimetype);
1671        }
1672
1673        $headers->structure = $struct;
1674
1675        return $this->icache['message'] = $headers;
1676    }
1677
1678
1679    /**
1680     * Build message part object
1681     *
1682     * @param array  $part
1683     * @param int    $count
1684     * @param string $parent
1685     * @access private
1686     */
1687    function &_structure_part($part, $count=0, $parent='', $mime_headers=null)
1688    {
1689        $struct = new rcube_message_part;
1690        $struct->mime_id = empty($parent) ? (string)$count : "$parent.$count";
1691
1692        // multipart
1693        if (is_array($part[0])) {
1694            $struct->ctype_primary = 'multipart';
1695
1696        /* RFC3501: BODYSTRUCTURE fields of multipart part
1697            part1 array
1698            part2 array
1699            part3 array
1700            ....
1701            1. subtype
1702            2. parameters (optional)
1703            3. description (optional)
1704            4. language (optional)
1705            5. location (optional)
1706        */
1707
1708            // find first non-array entry
1709            for ($i=1; $i<count($part); $i++) {
1710                if (!is_array($part[$i])) {
1711                    $struct->ctype_secondary = strtolower($part[$i]);
1712                    break;
1713                }
1714            }
1715
1716            $struct->mimetype = 'multipart/'.$struct->ctype_secondary;
1717
1718            // build parts list for headers pre-fetching
1719            for ($i=0; $i<count($part); $i++) {
1720                if (!is_array($part[$i]))
1721                    break;
1722                // fetch message headers if message/rfc822
1723                // or named part (could contain Content-Location header)
1724                if (!is_array($part[$i][0])) {
1725                    $tmp_part_id = $struct->mime_id ? $struct->mime_id.'.'.($i+1) : $i+1;
1726                    if (strtolower($part[$i][0]) == 'message' && strtolower($part[$i][1]) == 'rfc822') {
1727                        $mime_part_headers[] = $tmp_part_id;
1728                    }
1729                    else if (in_array('name', (array)$part[$i][2]) && empty($part[$i][3])) {
1730                        $mime_part_headers[] = $tmp_part_id;
1731                    }
1732                }
1733            }
1734
1735            // pre-fetch headers of all parts (in one command for better performance)
1736            // @TODO: we could do this before _structure_part() call, to fetch
1737            // headers for parts on all levels
1738            if ($mime_part_headers) {
1739                $mime_part_headers = $this->conn->fetchMIMEHeaders($this->mailbox,
1740                    $this->_msg_uid, $mime_part_headers);
1741            }
1742
1743            $struct->parts = array();
1744            for ($i=0, $count=0; $i<count($part); $i++) {
1745                if (!is_array($part[$i]))
1746                    break;
1747                $tmp_part_id = $struct->mime_id ? $struct->mime_id.'.'.($i+1) : $i+1;
1748                $struct->parts[] = $this->_structure_part($part[$i], ++$count, $struct->mime_id,
1749                    $mime_part_headers[$tmp_part_id]);
1750            }
1751
1752            return $struct;
1753        }
1754
1755        /* RFC3501: BODYSTRUCTURE fields of non-multipart part
1756            0. type
1757            1. subtype
1758            2. parameters
1759            3. id
1760            4. description
1761            5. encoding
1762            6. size
1763          -- text
1764            7. lines
1765          -- message/rfc822
1766            7. envelope structure
1767            8. body structure
1768            9. lines
1769          --
1770            x. md5 (optional)
1771            x. disposition (optional)
1772            x. language (optional)
1773            x. location (optional)
1774        */
1775
1776        // regular part
1777        $struct->ctype_primary = strtolower($part[0]);
1778        $struct->ctype_secondary = strtolower($part[1]);
1779        $struct->mimetype = $struct->ctype_primary.'/'.$struct->ctype_secondary;
1780
1781        // read content type parameters
1782        if (is_array($part[2])) {
1783            $struct->ctype_parameters = array();
1784            for ($i=0; $i<count($part[2]); $i+=2)
1785                $struct->ctype_parameters[strtolower($part[2][$i])] = $part[2][$i+1];
1786
1787            if (isset($struct->ctype_parameters['charset']))
1788                $struct->charset = $struct->ctype_parameters['charset'];
1789        }
1790
1791        // #1487700: workaround for lack of charset in malformed structure
1792        if (empty($struct->charset) && !empty($mime_headers) && $mime_headers->charset) {
1793            $struct->charset = $mime_headers->charset;
1794        }
1795
1796        // read content encoding
1797        if (!empty($part[5])) {
1798            $struct->encoding = strtolower($part[5]);
1799            $struct->headers['content-transfer-encoding'] = $struct->encoding;
1800        }
1801
1802        // get part size
1803        if (!empty($part[6]))
1804            $struct->size = intval($part[6]);
1805
1806        // read part disposition
1807        $di = 8;
1808        if ($struct->ctype_primary == 'text') $di += 1;
1809        else if ($struct->mimetype == 'message/rfc822') $di += 3;
1810
1811        if (is_array($part[$di]) && count($part[$di]) == 2) {
1812            $struct->disposition = strtolower($part[$di][0]);
1813
1814            if (is_array($part[$di][1]))
1815                for ($n=0; $n<count($part[$di][1]); $n+=2)
1816                    $struct->d_parameters[strtolower($part[$di][1][$n])] = $part[$di][1][$n+1];
1817        }
1818
1819        // get message/rfc822's child-parts
1820        if (is_array($part[8]) && $di != 8) {
1821            $struct->parts = array();
1822            for ($i=0, $count=0; $i<count($part[8]); $i++) {
1823                if (!is_array($part[8][$i]))
1824                    break;
1825                $struct->parts[] = $this->_structure_part($part[8][$i], ++$count, $struct->mime_id);
1826            }
1827        }
1828
1829        // get part ID
1830        if (!empty($part[3])) {
1831            $struct->content_id = $part[3];
1832            $struct->headers['content-id'] = $part[3];
1833
1834            if (empty($struct->disposition))
1835                $struct->disposition = 'inline';
1836        }
1837
1838        // fetch message headers if message/rfc822 or named part (could contain Content-Location header)
1839        if ($struct->ctype_primary == 'message' || ($struct->ctype_parameters['name'] && !$struct->content_id)) {
1840            if (empty($mime_headers)) {
1841                $mime_headers = $this->conn->fetchPartHeader(
1842                    $this->mailbox, $this->_msg_uid, true, $struct->mime_id);
1843            }
1844
1845            if (is_string($mime_headers))
1846                $struct->headers = $this->_parse_headers($mime_headers) + $struct->headers;
1847            else if (is_object($mime_headers))
1848                $struct->headers = get_object_vars($mime_headers) + $struct->headers;
1849
1850            // get real content-type of message/rfc822
1851            if ($struct->mimetype == 'message/rfc822') {
1852                // single-part
1853                if (!is_array($part[8][0]))
1854                    $struct->real_mimetype = strtolower($part[8][0] . '/' . $part[8][1]);
1855                // multi-part
1856                else {
1857                    for ($n=0; $n<count($part[8]); $n++)
1858                        if (!is_array($part[8][$n]))
1859                            break;
1860                    $struct->real_mimetype = 'multipart/' . strtolower($part[8][$n]);
1861                }
1862            }
1863
1864            if ($struct->ctype_primary == 'message' && empty($struct->parts)) {
1865                if (is_array($part[8]) && $di != 8)
1866                    $struct->parts[] = $this->_structure_part($part[8], ++$count, $struct->mime_id);
1867            }
1868        }
1869
1870        // normalize filename property
1871        $this->_set_part_filename($struct, $mime_headers);
1872
1873        return $struct;
1874    }
1875
1876
1877    /**
1878     * Set attachment filename from message part structure
1879     *
1880     * @param  rcube_message_part $part    Part object
1881     * @param  string             $headers Part's raw headers
1882     * @access private
1883     */
1884    private function _set_part_filename(&$part, $headers=null)
1885    {
1886        if (!empty($part->d_parameters['filename']))
1887            $filename_mime = $part->d_parameters['filename'];
1888        else if (!empty($part->d_parameters['filename*']))
1889            $filename_encoded = $part->d_parameters['filename*'];
1890        else if (!empty($part->ctype_parameters['name*']))
1891            $filename_encoded = $part->ctype_parameters['name*'];
1892        // RFC2231 value continuations
1893        // TODO: this should be rewrited to support RFC2231 4.1 combinations
1894        else if (!empty($part->d_parameters['filename*0'])) {
1895            $i = 0;
1896            while (isset($part->d_parameters['filename*'.$i])) {
1897                $filename_mime .= $part->d_parameters['filename*'.$i];
1898                $i++;
1899            }
1900            // some servers (eg. dovecot-1.x) have no support for parameter value continuations
1901            // we must fetch and parse headers "manually"
1902            if ($i<2) {
1903                if (!$headers) {
1904                    $headers = $this->conn->fetchPartHeader(
1905                        $this->mailbox, $this->_msg_uid, true, $part->mime_id);
1906                }
1907                $filename_mime = '';
1908                $i = 0;
1909                while (preg_match('/filename\*'.$i.'\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
1910                    $filename_mime .= $matches[1];
1911                    $i++;
1912                }
1913            }
1914        }
1915        else if (!empty($part->d_parameters['filename*0*'])) {
1916            $i = 0;
1917            while (isset($part->d_parameters['filename*'.$i.'*'])) {
1918                $filename_encoded .= $part->d_parameters['filename*'.$i.'*'];
1919                $i++;
1920            }
1921            if ($i<2) {
1922                if (!$headers) {
1923                    $headers = $this->conn->fetchPartHeader(
1924                            $this->mailbox, $this->_msg_uid, true, $part->mime_id);
1925                }
1926                $filename_encoded = '';
1927                $i = 0; $matches = array();
1928                while (preg_match('/filename\*'.$i.'\*\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
1929                    $filename_encoded .= $matches[1];
1930                    $i++;
1931                }
1932            }
1933        }
1934        else if (!empty($part->ctype_parameters['name*0'])) {
1935            $i = 0;
1936            while (isset($part->ctype_parameters['name*'.$i])) {
1937                $filename_mime .= $part->ctype_parameters['name*'.$i];
1938                $i++;
1939            }
1940            if ($i<2) {
1941                if (!$headers) {
1942                    $headers = $this->conn->fetchPartHeader(
1943                        $this->mailbox, $this->_msg_uid, true, $part->mime_id);
1944                }
1945                $filename_mime = '';
1946                $i = 0; $matches = array();
1947                while (preg_match('/\s+name\*'.$i.'\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
1948                    $filename_mime .= $matches[1];
1949                    $i++;
1950                }
1951            }
1952        }
1953        else if (!empty($part->ctype_parameters['name*0*'])) {
1954            $i = 0;
1955            while (isset($part->ctype_parameters['name*'.$i.'*'])) {
1956                $filename_encoded .= $part->ctype_parameters['name*'.$i.'*'];
1957                $i++;
1958            }
1959            if ($i<2) {
1960                if (!$headers) {
1961                    $headers = $this->conn->fetchPartHeader(
1962                        $this->mailbox, $this->_msg_uid, true, $part->mime_id);
1963                }
1964                $filename_encoded = '';
1965                $i = 0; $matches = array();
1966                while (preg_match('/\s+name\*'.$i.'\*\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
1967                    $filename_encoded .= $matches[1];
1968                    $i++;
1969                }
1970            }
1971        }
1972        // read 'name' after rfc2231 parameters as it may contains truncated filename (from Thunderbird)
1973        else if (!empty($part->ctype_parameters['name']))
1974            $filename_mime = $part->ctype_parameters['name'];
1975        // Content-Disposition
1976        else if (!empty($part->headers['content-description']))
1977            $filename_mime = $part->headers['content-description'];
1978        else
1979            return;
1980
1981        // decode filename
1982        if (!empty($filename_mime)) {
1983            if (!empty($part->charset))
1984                $charset = $part->charset;
1985            else if (!empty($this->struct_charset))
1986                $charset = $this->struct_charset;
1987            else
1988                $charset = rc_detect_encoding($filename_mime, $this->default_charset);
1989
1990            $part->filename = rcube_imap::decode_mime_string($filename_mime, $charset);
1991        }
1992        else if (!empty($filename_encoded)) {
1993            // decode filename according to RFC 2231, Section 4
1994            if (preg_match("/^([^']*)'[^']*'(.*)$/", $filename_encoded, $fmatches)) {
1995                $filename_charset = $fmatches[1];
1996                $filename_encoded = $fmatches[2];
1997            }
1998
1999            $part->filename = rcube_charset_convert(urldecode($filename_encoded), $filename_charset);
2000        }
2001    }
2002
2003
2004    /**
2005     * Get charset name from message structure (first part)
2006     *
2007     * @param  array $structure Message structure
2008     * @return string Charset name
2009     * @access private
2010     */
2011    private function _structure_charset($structure)
2012    {
2013        while (is_array($structure)) {
2014            if (is_array($structure[2]) && $structure[2][0] == 'charset')
2015                return $structure[2][1];
2016            $structure = $structure[0];
2017        }
2018    }
2019
2020
2021    /**
2022     * Fetch message body of a specific message from the server
2023     *
2024     * @param  int                $uid    Message UID
2025     * @param  string             $part   Part number
2026     * @param  rcube_message_part $o_part Part object created by get_structure()
2027     * @param  mixed              $print  True to print part, ressource to write part contents in
2028     * @param  resource           $fp     File pointer to save the message part
2029     * @param  boolean            $skip_charset_conv Disables charset conversion
2030     *
2031     * @return string Message/part body if not printed
2032     */
2033    function &get_message_part($uid, $part=1, $o_part=NULL, $print=NULL, $fp=NULL, $skip_charset_conv=false)
2034    {
2035        // get part data if not provided
2036        if (!is_object($o_part)) {
2037            $structure = $this->conn->getStructure($this->mailbox, $uid, true);
2038            $part_data = rcube_imap_generic::getStructurePartData($structure, $part);
2039
2040            $o_part = new rcube_message_part;
2041            $o_part->ctype_primary = $part_data['type'];
2042            $o_part->encoding      = $part_data['encoding'];
2043            $o_part->charset       = $part_data['charset'];
2044            $o_part->size          = $part_data['size'];
2045        }
2046
2047        if ($o_part && $o_part->size) {
2048            $body = $this->conn->handlePartBody($this->mailbox, $uid, true,
2049                $part ? $part : 'TEXT', $o_part->encoding, $print, $fp);
2050        }
2051
2052        if ($fp || $print) {
2053            return true;
2054        }
2055
2056        // convert charset (if text or message part)
2057        if ($body && preg_match('/^(text|message)$/', $o_part->ctype_primary)) {
2058            // Remove NULL characters if any (#1486189)
2059            if (strpos($body, "\x00") !== false) {
2060                $body = str_replace("\x00", '', $body);
2061            }
2062
2063            if (!$skip_charset_conv) {
2064                if (!$o_part->charset || strtoupper($o_part->charset) == 'US-ASCII') {
2065                    // try to extract charset information from HTML meta tag (#1488125)
2066                    if ($o_part->ctype_secondary == 'html' && preg_match('/<meta[^>]+charset=([a-z0-9-_]+)/i', $body, $m))
2067                        $o_part->charset = strtoupper($m[1]);
2068                    else
2069                        $o_part->charset = $this->default_charset;
2070                }
2071                $body = rcube_charset_convert($body, $o_part->charset);
2072            }
2073        }
2074
2075        return $body;
2076    }
2077
2078
2079    /**
2080     * Fetch message body of a specific message from the server
2081     *
2082     * @param  int    $uid  Message UID
2083     * @return string $part Message/part body
2084     * @see    rcube_imap::get_message_part()
2085     */
2086    function &get_body($uid, $part=1)
2087    {
2088        $headers = $this->get_headers($uid);
2089        return rcube_charset_convert($this->get_message_part($uid, $part, NULL),
2090            $headers->charset ? $headers->charset : $this->default_charset);
2091    }
2092
2093
2094    /**
2095     * Returns the whole message source as string (or saves to a file)
2096     *
2097     * @param int      $uid Message UID
2098     * @param resource $fp  File pointer to save the message
2099     *
2100     * @return string Message source string
2101     */
2102    function &get_raw_body($uid, $fp=null)
2103    {
2104        return $this->conn->handlePartBody($this->mailbox, $uid,
2105            true, null, null, false, $fp);
2106    }
2107
2108
2109    /**
2110     * Returns the message headers as string
2111     *
2112     * @param int $uid  Message UID
2113     * @return string Message headers string
2114     */
2115    function &get_raw_headers($uid)
2116    {
2117        return $this->conn->fetchPartHeader($this->mailbox, $uid, true);
2118    }
2119
2120
2121    /**
2122     * Sends the whole message source to stdout
2123     *
2124     * @param int $uid Message UID
2125     */
2126    function print_raw_body($uid)
2127    {
2128        $this->conn->handlePartBody($this->mailbox, $uid, true, NULL, NULL, true);
2129    }
2130
2131
2132    /**
2133     * Set message flag to one or several messages
2134     *
2135     * @param mixed   $uids       Message UIDs as array or comma-separated string, or '*'
2136     * @param string  $flag       Flag to set: SEEN, UNDELETED, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
2137     * @param string  $mailbox    Folder name
2138     * @param boolean $skip_cache True to skip message cache clean up
2139     *
2140     * @return boolean  Operation status
2141     */
2142    function set_flag($uids, $flag, $mailbox=null, $skip_cache=false)
2143    {
2144        if (!strlen($mailbox)) {
2145            $mailbox = $this->mailbox;
2146        }
2147
2148        $flag = strtoupper($flag);
2149        list($uids, $all_mode) = $this->_parse_uids($uids, $mailbox);
2150
2151        if (strpos($flag, 'UN') === 0)
2152            $result = $this->conn->unflag($mailbox, $uids, substr($flag, 2));
2153        else
2154            $result = $this->conn->flag($mailbox, $uids, $flag);
2155
2156        if ($result) {
2157            // reload message headers if cached
2158            // @TODO: update flags instead removing from cache
2159            if (!$skip_cache && ($mcache = $this->get_mcache_engine())) {
2160                $status = strpos($flag, 'UN') !== 0;
2161                $mflag  = preg_replace('/^UN/', '', $flag);
2162                $mcache->change_flag($mailbox, $all_mode ? null : explode(',', $uids),
2163                    $mflag, $status);
2164            }
2165
2166            // clear cached counters
2167            if ($flag == 'SEEN' || $flag == 'UNSEEN') {
2168                $this->_clear_messagecount($mailbox, 'SEEN');
2169                $this->_clear_messagecount($mailbox, 'UNSEEN');
2170            }
2171            else if ($flag == 'DELETED') {
2172                $this->_clear_messagecount($mailbox, 'DELETED');
2173            }
2174        }
2175
2176        return $result;
2177    }
2178
2179
2180    /**
2181     * Remove message flag for one or several messages
2182     *
2183     * @param mixed  $uids    Message UIDs as array or comma-separated string, or '*'
2184     * @param string $flag    Flag to unset: SEEN, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
2185     * @param string $mailbox Folder name
2186     *
2187     * @return int   Number of flagged messages, -1 on failure
2188     * @see set_flag
2189     */
2190    function unset_flag($uids, $flag, $mailbox=null)
2191    {
2192        return $this->set_flag($uids, 'UN'.$flag, $mailbox);
2193    }
2194
2195
2196    /**
2197     * Append a mail message (source) to a specific mailbox
2198     *
2199     * @param string  $mailbox Target mailbox
2200     * @param string  $message The message source string or filename
2201     * @param string  $headers Headers string if $message contains only the body
2202     * @param boolean $is_file True if $message is a filename
2203     *
2204     * @return int|bool Appended message UID or True on success, False on error
2205     */
2206    function save_message($mailbox, &$message, $headers='', $is_file=false)
2207    {
2208        if (!strlen($mailbox)) {
2209            $mailbox = $this->mailbox;
2210        }
2211
2212        // make sure mailbox exists
2213        if ($this->mailbox_exists($mailbox)) {
2214            if ($is_file)
2215                $saved = $this->conn->appendFromFile($mailbox, $message, $headers);
2216            else
2217                $saved = $this->conn->append($mailbox, $message);
2218        }
2219
2220        if ($saved) {
2221            // increase messagecount of the target mailbox
2222            $this->_set_messagecount($mailbox, 'ALL', 1);
2223        }
2224
2225        return $saved;
2226    }
2227
2228
2229    /**
2230     * Move a message from one mailbox to another
2231     *
2232     * @param mixed  $uids      Message UIDs as array or comma-separated string, or '*'
2233     * @param string $to_mbox   Target mailbox
2234     * @param string $from_mbox Source mailbox
2235     * @return boolean True on success, False on error
2236     */
2237    function move_message($uids, $to_mbox, $from_mbox='')
2238    {
2239        if (!strlen($from_mbox)) {
2240            $from_mbox = $this->mailbox;
2241        }
2242
2243        if ($to_mbox === $from_mbox) {
2244            return false;
2245        }
2246
2247        list($uids, $all_mode) = $this->_parse_uids($uids, $from_mbox);
2248
2249        // exit if no message uids are specified
2250        if (empty($uids))
2251            return false;
2252
2253        // make sure mailbox exists
2254        if ($to_mbox != 'INBOX' && !$this->mailbox_exists($to_mbox)) {
2255            if (in_array($to_mbox, $this->default_folders)) {
2256                if (!$this->create_mailbox($to_mbox, true)) {
2257                    return false;
2258                }
2259            }
2260            else {
2261                return false;
2262            }
2263        }
2264
2265        $config = rcmail::get_instance()->config;
2266        $to_trash = $to_mbox == $config->get('trash_mbox');
2267
2268        // flag messages as read before moving them
2269        if ($to_trash && $config->get('read_when_deleted')) {
2270            // don't flush cache (4th argument)
2271            $this->set_flag($uids, 'SEEN', $from_mbox, true);
2272        }
2273
2274        // move messages
2275        $moved = $this->conn->move($uids, $from_mbox, $to_mbox);
2276
2277        // send expunge command in order to have the moved message
2278        // really deleted from the source mailbox
2279        if ($moved) {
2280            $this->_expunge($from_mbox, false, $uids);
2281            $this->_clear_messagecount($from_mbox);
2282            $this->_clear_messagecount($to_mbox);
2283        }
2284        // moving failed
2285        else if ($to_trash && $config->get('delete_always', false)) {
2286            $moved = $this->delete_message($uids, $from_mbox);
2287        }
2288
2289        if ($moved) {
2290            // unset threads internal cache
2291            unset($this->icache['threads']);
2292
2293            // remove message ids from search set
2294            if ($this->search_set && $from_mbox == $this->mailbox) {
2295                // threads are too complicated to just remove messages from set
2296                if ($this->search_threads || $all_mode)
2297                    $this->refresh_search();
2298                else
2299                    $this->search_set->filter(explode(',', $uids));
2300            }
2301
2302            // remove cached messages
2303            // @TODO: do cache update instead of clearing it
2304            $this->clear_message_cache($from_mbox, $all_mode ? null : explode(',', $uids));
2305        }
2306
2307        return $moved;
2308    }
2309
2310
2311    /**
2312     * Copy a message from one mailbox to another
2313     *
2314     * @param mixed  $uids      Message UIDs as array or comma-separated string, or '*'
2315     * @param string $to_mbox   Target mailbox
2316     * @param string $from_mbox Source mailbox
2317     * @return boolean True on success, False on error
2318     */
2319    function copy_message($uids, $to_mbox, $from_mbox='')
2320    {
2321        if (!strlen($from_mbox)) {
2322            $from_mbox = $this->mailbox;
2323        }
2324
2325        list($uids, $all_mode) = $this->_parse_uids($uids, $from_mbox);
2326
2327        // exit if no message uids are specified
2328        if (empty($uids)) {
2329            return false;
2330        }
2331
2332        // make sure mailbox exists
2333        if ($to_mbox != 'INBOX' && !$this->mailbox_exists($to_mbox)) {
2334            if (in_array($to_mbox, $this->default_folders)) {
2335                if (!$this->create_mailbox($to_mbox, true)) {
2336                    return false;
2337                }
2338            }
2339            else {
2340                return false;
2341            }
2342        }
2343
2344        // copy messages
2345        $copied = $this->conn->copy($uids, $from_mbox, $to_mbox);
2346
2347        if ($copied) {
2348            $this->_clear_messagecount($to_mbox);
2349        }
2350
2351        return $copied;
2352    }
2353
2354
2355    /**
2356     * Mark messages as deleted and expunge mailbox
2357     *
2358     * @param mixed  $uids    Message UIDs as array or comma-separated string, or '*'
2359     * @param string $mailbox Source mailbox
2360     *
2361     * @return boolean True on success, False on error
2362     */
2363    function delete_message($uids, $mailbox='')
2364    {
2365        if (!strlen($mailbox)) {
2366            $mailbox = $this->mailbox;
2367        }
2368
2369        list($uids, $all_mode) = $this->_parse_uids($uids, $mailbox);
2370
2371        // exit if no message uids are specified
2372        if (empty($uids))
2373            return false;
2374
2375        $deleted = $this->conn->delete($mailbox, $uids);
2376
2377        if ($deleted) {
2378            // send expunge command in order to have the deleted message
2379            // really deleted from the mailbox
2380            $this->_expunge($mailbox, false, $uids);
2381            $this->_clear_messagecount($mailbox);
2382            unset($this->uid_id_map[$mailbox]);
2383
2384            // unset threads internal cache
2385            unset($this->icache['threads']);
2386
2387            // remove message ids from search set
2388            if ($this->search_set && $mailbox == $this->mailbox) {
2389                // threads are too complicated to just remove messages from set
2390                if ($this->search_threads || $all_mode)
2391                    $this->refresh_search();
2392                else
2393                    $this->search_set->filter(explode(',', $uids));
2394            }
2395
2396            // remove cached messages
2397            $this->clear_message_cache($mailbox, $all_mode ? null : explode(',', $uids));
2398        }
2399
2400        return $deleted;
2401    }
2402
2403
2404    /**
2405     * Clear all messages in a specific mailbox
2406     *
2407     * @param string $mailbox Mailbox name
2408     *
2409     * @return int Above 0 on success
2410     */
2411    function clear_mailbox($mailbox=null)
2412    {
2413        if (!strlen($mailbox)) {
2414            $mailbox = $this->mailbox;
2415        }
2416
2417        // SELECT will set messages count for clearFolder()
2418        if ($this->conn->select($mailbox)) {
2419            $cleared = $this->conn->clearFolder($mailbox);
2420        }
2421
2422        // make sure the cache is cleared as well
2423        if ($cleared) {
2424            $this->clear_message_cache($mailbox);
2425            $a_mailbox_cache = $this->get_cache('messagecount');
2426            unset($a_mailbox_cache[$mailbox]);
2427            $this->update_cache('messagecount', $a_mailbox_cache);
2428        }
2429
2430        return $cleared;
2431    }
2432
2433
2434    /**
2435     * Send IMAP expunge command and clear cache
2436     *
2437     * @param string  $mailbox     Mailbox name
2438     * @param boolean $clear_cache False if cache should not be cleared
2439     *
2440     * @return boolean True on success
2441     */
2442    function expunge($mailbox='', $clear_cache=true)
2443    {
2444        if (!strlen($mailbox)) {
2445            $mailbox = $this->mailbox;
2446        }
2447
2448        return $this->_expunge($mailbox, $clear_cache);
2449    }
2450
2451
2452    /**
2453     * Send IMAP expunge command and clear cache
2454     *
2455     * @param string  $mailbox     Mailbox name
2456     * @param boolean $clear_cache False if cache should not be cleared
2457     * @param mixed   $uids        Message UIDs as array or comma-separated string, or '*'
2458     * @return boolean True on success
2459     * @access private
2460     * @see rcube_imap::expunge()
2461     */
2462    private function _expunge($mailbox, $clear_cache=true, $uids=NULL)
2463    {
2464        if ($uids && $this->get_capability('UIDPLUS'))
2465            list($uids, $all_mode) = $this->_parse_uids($uids, $mailbox);
2466        else
2467            $uids = null;
2468
2469        // force mailbox selection and check if mailbox is writeable
2470        // to prevent a situation when CLOSE is executed on closed
2471        // or EXPUNGE on read-only mailbox
2472        $result = $this->conn->select($mailbox);
2473        if (!$result) {
2474            return false;
2475        }
2476        if (!$this->conn->data['READ-WRITE']) {
2477            $this->conn->setError(rcube_imap_generic::ERROR_READONLY, "Mailbox is read-only");
2478            return false;
2479        }
2480
2481        // CLOSE(+SELECT) should be faster than EXPUNGE
2482        if (empty($uids) || $all_mode)
2483            $result = $this->conn->close();
2484        else
2485            $result = $this->conn->expunge($mailbox, $uids);
2486
2487        if ($result && $clear_cache) {
2488            $this->clear_message_cache($mailbox, $all_mode ? null : explode(',', $uids));
2489            $this->_clear_messagecount($mailbox);
2490        }
2491
2492        return $result;
2493    }
2494
2495
2496    /**
2497     * Parse message UIDs input
2498     *
2499     * @param mixed  $uids    UIDs array or comma-separated list or '*' or '1:*'
2500     * @param string $mailbox Mailbox name
2501     * @return array Two elements array with UIDs converted to list and ALL flag
2502     * @access private
2503     */
2504    private function _parse_uids($uids, $mailbox)
2505    {
2506        if ($uids === '*' || $uids === '1:*') {
2507            if (empty($this->search_set)) {
2508                $uids = '1:*';
2509                $all = true;
2510            }
2511            // get UIDs from current search set
2512            else {
2513                $uids = join(',', $this->search_set->get());
2514            }
2515        }
2516        else {
2517            if (is_array($uids))
2518                $uids = join(',', $uids);
2519
2520            if (preg_match('/[^0-9,]/', $uids))
2521                $uids = '';
2522        }
2523
2524        return array($uids, (bool) $all);
2525    }
2526
2527
2528    /* --------------------------------
2529     *        folder managment
2530     * --------------------------------*/
2531
2532    /**
2533     * Public method for listing subscribed folders
2534     *
2535     * @param   string  $root      Optional root folder
2536     * @param   string  $name      Optional name pattern
2537     * @param   string  $filter    Optional filter
2538     * @param   string  $rights    Optional ACL requirements
2539     * @param   bool    $skip_sort Enable to return unsorted list (for better performance)
2540     *
2541     * @return  array   List of folders
2542     * @access  public
2543     */
2544    function list_mailboxes($root='', $name='*', $filter=null, $rights=null, $skip_sort=false)
2545    {
2546        $cache_key = $root.':'.$name;
2547        if (!empty($filter)) {
2548            $cache_key .= ':'.(is_string($filter) ? $filter : serialize($filter));
2549        }
2550        $cache_key .= ':'.$rights;
2551        $cache_key = 'mailboxes.'.md5($cache_key);
2552
2553        // get cached folder list
2554        $a_mboxes = $this->get_cache($cache_key);
2555        if (is_array($a_mboxes)) {
2556            return $a_mboxes;
2557        }
2558
2559        $a_mboxes = $this->_list_mailboxes($root, $name, $filter, $rights);
2560
2561        if (!is_array($a_mboxes)) {
2562            return array();
2563        }
2564
2565        // filter folders list according to rights requirements
2566        if ($rights && $this->get_capability('ACL')) {
2567            $a_mboxes = $this->filter_rights($a_mboxes, $rights);
2568        }
2569
2570        // INBOX should always be available
2571        if ((!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)) {
2572            array_unshift($a_mboxes, 'INBOX');
2573        }
2574
2575        // sort mailboxes (always sort for cache)
2576        if (!$skip_sort || $this->cache) {
2577            $a_mboxes = $this->_sort_mailbox_list($a_mboxes);
2578        }
2579
2580        // write mailboxlist to cache
2581        $this->update_cache($cache_key, $a_mboxes);
2582
2583        return $a_mboxes;
2584    }
2585
2586
2587    /**
2588     * Private method for mailbox listing (LSUB)
2589     *
2590     * @param   string  $root   Optional root folder
2591     * @param   string  $name   Optional name pattern
2592     * @param   mixed   $filter Optional filter
2593     * @param   string  $rights Optional ACL requirements
2594     *
2595     * @return  array   List of subscribed folders
2596     * @see     rcube_imap::list_mailboxes()
2597     * @access  private
2598     */
2599    private function _list_mailboxes($root='', $name='*', $filter=null, $rights=null)
2600    {
2601        $a_defaults = $a_out = array();
2602
2603        // Give plugins a chance to provide a list of mailboxes
2604        $data = rcmail::get_instance()->plugins->exec_hook('mailboxes_list',
2605            array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LSUB'));
2606
2607        if (isset($data['folders'])) {
2608            $a_folders = $data['folders'];
2609        }
2610        else if (!$this->conn->connected()) {
2611           return null;
2612        }
2613        else {
2614            // Server supports LIST-EXTENDED, we can use selection options
2615            $config = rcmail::get_instance()->config;
2616            // #1486225: Some dovecot versions returns wrong result using LIST-EXTENDED
2617            if (!$config->get('imap_force_lsub') && $this->get_capability('LIST-EXTENDED')) {
2618                // This will also set mailbox options, LSUB doesn't do that
2619                $a_folders = $this->conn->listMailboxes($root, $name,
2620                    NULL, array('SUBSCRIBED'));
2621
2622                // unsubscribe non-existent folders, remove from the list
2623                if (is_array($a_folders) && $name == '*') {
2624                    foreach ($a_folders as $idx => $folder) {
2625                        if ($this->conn->data['LIST'] && ($opts = $this->conn->data['LIST'][$folder])
2626                            && in_array('\\NonExistent', $opts)
2627                        ) {
2628                            $this->conn->unsubscribe($folder);
2629                            unset($a_folders[$idx]);
2630                        }
2631                    }
2632                }
2633            }
2634            // retrieve list of folders from IMAP server using LSUB
2635            else {
2636                $a_folders = $this->conn->listSubscribed($root, $name);
2637
2638                // unsubscribe non-existent folders, remove from the list
2639                if (is_array($a_folders) && $name == '*') {
2640                    foreach ($a_folders as $idx => $folder) {
2641                        if ($this->conn->data['LIST'] && ($opts = $this->conn->data['LIST'][$folder])
2642                            && in_array('\\Noselect', $opts)
2643                        ) {
2644                            // Some servers returns \Noselect for existing folders
2645                            if (!$this->mailbox_exists($folder)) {
2646                                $this->conn->unsubscribe($folder);
2647                                unset($a_folders[$idx]);
2648                            }
2649                        }
2650                    }
2651                }
2652            }
2653        }
2654
2655        if (!is_array($a_folders) || !sizeof($a_folders)) {
2656            $a_folders = array();
2657        }
2658
2659        return $a_folders;
2660    }
2661
2662
2663    /**
2664     * Get a list of all folders available on the IMAP server
2665     *
2666     * @param string  $root      IMAP root dir
2667     * @param string  $name      Optional name pattern
2668     * @param mixed   $filter    Optional filter
2669     * @param string  $rights    Optional ACL requirements
2670     * @param bool    $skip_sort Enable to return unsorted list (for better performance)
2671     *
2672     * @return array Indexed array with folder names
2673     */
2674    function list_unsubscribed($root='', $name='*', $filter=null, $rights=null, $skip_sort=false)
2675    {
2676        $cache_key = $root.':'.$name;
2677        if (!empty($filter)) {
2678            $cache_key .= ':'.(is_string($filter) ? $filter : serialize($filter));
2679        }
2680        $cache_key .= ':'.$rights;
2681        $cache_key = 'mailboxes.list.'.md5($cache_key);
2682
2683        // get cached folder list
2684        $a_mboxes = $this->get_cache($cache_key);
2685        if (is_array($a_mboxes)) {
2686            return $a_mboxes;
2687        }
2688
2689        // Give plugins a chance to provide a list of mailboxes
2690        $data = rcmail::get_instance()->plugins->exec_hook('mailboxes_list',
2691            array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LIST'));
2692
2693        if (isset($data['folders'])) {
2694            $a_mboxes = $data['folders'];
2695        }
2696        else {
2697            // retrieve list of folders from IMAP server
2698            $a_mboxes = $this->_list_unsubscribed($root, $name);
2699        }
2700
2701        if (!is_array($a_mboxes)) {
2702            $a_mboxes = array();
2703        }
2704
2705        // INBOX should always be available
2706        if ((!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)) {
2707            array_unshift($a_mboxes, 'INBOX');
2708        }
2709
2710        // cache folder attributes
2711        if ($root == '' && $name == '*' && empty($filter)) {
2712            $this->update_cache('mailboxes.attributes', $this->conn->data['LIST']);
2713        }
2714
2715        // filter folders list according to rights requirements
2716        if ($rights && $this->get_capability('ACL')) {
2717            $a_folders = $this->filter_rights($a_folders, $rights);
2718        }
2719
2720        // filter folders and sort them
2721        if (!$skip_sort) {
2722            $a_mboxes = $this->_sort_mailbox_list($a_mboxes);
2723        }
2724
2725        // write mailboxlist to cache
2726        $this->update_cache($cache_key, $a_mboxes);
2727
2728        return $a_mboxes;
2729    }
2730
2731
2732    /**
2733     * Private method for mailbox listing (LIST)
2734     *
2735     * @param   string  $root   Optional root folder
2736     * @param   string  $name   Optional name pattern
2737     *
2738     * @return  array   List of folders
2739     * @see     rcube_imap::list_unsubscribed()
2740     */
2741    private function _list_unsubscribed($root='', $name='*')
2742    {
2743        $result = $this->conn->listMailboxes($root, $name);
2744
2745        if (!is_array($result)) {
2746            return array();
2747        }
2748
2749        // #1486796: some server configurations doesn't
2750        // return folders in all namespaces, we'll try to detect that situation
2751        // and ask for these namespaces separately
2752        if ($root == '' && $name == '*') {
2753            $delim     = $this->get_hierarchy_delimiter();
2754            $namespace = $this->get_namespace();
2755            $search    = array();
2756
2757            // build list of namespace prefixes
2758            foreach ((array)$namespace as $ns) {
2759                if (is_array($ns)) {
2760                    foreach ($ns as $ns_data) {
2761                        if (strlen($ns_data[0])) {
2762                            $search[] = $ns_data[0];
2763                        }
2764                    }
2765                }
2766            }
2767
2768            if (!empty($search)) {
2769                // go through all folders detecting namespace usage
2770                foreach ($result as $folder) {
2771                    foreach ($search as $idx => $prefix) {
2772                        if (strpos($folder, $prefix) === 0) {
2773                            unset($search[$idx]);
2774                        }
2775                    }
2776                    if (empty($search)) {
2777                        break;
2778                    }
2779                }
2780
2781                // get folders in hidden namespaces and add to the result
2782                foreach ($search as $prefix) {
2783                    $list = $this->conn->listMailboxes($prefix, $name);
2784
2785                    if (!empty($list)) {
2786                        $result = array_merge($result, $list);
2787                    }
2788                }
2789            }
2790        }
2791
2792        return $result;
2793    }
2794
2795
2796    /**
2797     * Filter the given list of folders according to access rights
2798     */
2799    private function filter_rights($a_folders, $rights)
2800    {
2801        $regex = '/('.$rights.')/';
2802        foreach ($a_folders as $idx => $folder) {
2803            $myrights = join('', (array)$this->my_rights($folder));
2804            if ($myrights !== null && !preg_match($regex, $myrights))
2805                unset($a_folders[$idx]);
2806        }
2807
2808        return $a_folders;
2809    }
2810
2811
2812    /**
2813     * Get mailbox quota information
2814     * added by Nuny
2815     *
2816     * @return mixed Quota info or False if not supported
2817     */
2818    function get_quota()
2819    {
2820        if ($this->get_capability('QUOTA'))
2821            return $this->conn->getQuota();
2822
2823        return false;
2824    }
2825
2826
2827    /**
2828     * Get mailbox size (size of all messages in a mailbox)
2829     *
2830     * @param string $mailbox Mailbox name
2831     *
2832     * @return int Mailbox size in bytes, False on error
2833     */
2834    function get_mailbox_size($mailbox)
2835    {
2836        // @TODO: could we try to use QUOTA here?
2837        $result = $this->conn->fetchHeaderIndex($mailbox, '1:*', 'SIZE', false);
2838
2839        if (is_array($result))
2840            $result = array_sum($result);
2841
2842        return $result;
2843    }
2844
2845
2846    /**
2847     * Subscribe to a specific mailbox(es)
2848     *
2849     * @param array $a_mboxes Mailbox name(s)
2850     * @return boolean True on success
2851     */
2852    function subscribe($a_mboxes)
2853    {
2854        if (!is_array($a_mboxes))
2855            $a_mboxes = array($a_mboxes);
2856
2857        // let this common function do the main work
2858        return $this->_change_subscription($a_mboxes, 'subscribe');
2859    }
2860
2861
2862    /**
2863     * Unsubscribe mailboxes
2864     *
2865     * @param array $a_mboxes Mailbox name(s)
2866     * @return boolean True on success
2867     */
2868    function unsubscribe($a_mboxes)
2869    {
2870        if (!is_array($a_mboxes))
2871            $a_mboxes = array($a_mboxes);
2872
2873        // let this common function do the main work
2874        return $this->_change_subscription($a_mboxes, 'unsubscribe');
2875    }
2876
2877
2878    /**
2879     * Create a new mailbox on the server and register it in local cache
2880     *
2881     * @param string  $mailbox   New mailbox name
2882     * @param boolean $subscribe True if the new mailbox should be subscribed
2883     *
2884     * @return boolean True on success
2885     */
2886    function create_mailbox($mailbox, $subscribe=false)
2887    {
2888        $result = $this->conn->createFolder($mailbox);
2889
2890        // try to subscribe it
2891        if ($result) {
2892            // clear cache
2893            $this->clear_cache('mailboxes', true);
2894
2895            if ($subscribe)
2896                $this->subscribe($mailbox);
2897        }
2898
2899        return $result;
2900    }
2901
2902
2903    /**
2904     * Set a new name to an existing mailbox
2905     *
2906     * @param string $mailbox  Mailbox to rename
2907     * @param string $new_name New mailbox name
2908     *
2909     * @return boolean True on success
2910     */
2911    function rename_mailbox($mailbox, $new_name)
2912    {
2913        if (!strlen($new_name)) {
2914            return false;
2915        }
2916
2917        $delm = $this->get_hierarchy_delimiter();
2918
2919        // get list of subscribed folders
2920        if ((strpos($mailbox, '%') === false) && (strpos($mailbox, '*') === false)) {
2921            $a_subscribed = $this->_list_mailboxes('', $mailbox . $delm . '*');
2922            $subscribed   = $this->mailbox_exists($mailbox, true);
2923        }
2924        else {
2925            $a_subscribed = $this->_list_mailboxes();
2926            $subscribed   = in_array($mailbox, $a_subscribed);
2927        }
2928
2929        $result = $this->conn->renameFolder($mailbox, $new_name);
2930
2931        if ($result) {
2932            // unsubscribe the old folder, subscribe the new one
2933            if ($subscribed) {
2934                $this->conn->unsubscribe($mailbox);
2935                $this->conn->subscribe($new_name);
2936            }
2937
2938            // check if mailbox children are subscribed
2939            foreach ($a_subscribed as $c_subscribed) {
2940                if (preg_match('/^'.preg_quote($mailbox.$delm, '/').'/', $c_subscribed)) {
2941                    $this->conn->unsubscribe($c_subscribed);
2942                    $this->conn->subscribe(preg_replace('/^'.preg_quote($mailbox, '/').'/',
2943                        $new_name, $c_subscribed));
2944
2945                    // clear cache
2946                    $this->clear_message_cache($c_subscribed);
2947                }
2948            }
2949
2950            // clear cache
2951            $this->clear_message_cache($mailbox);
2952            $this->clear_cache('mailboxes', true);
2953        }
2954
2955        return $result;
2956    }
2957
2958
2959    /**
2960     * Remove mailbox from server
2961     *
2962     * @param string $mailbox Mailbox name
2963     *
2964     * @return boolean True on success
2965     */
2966    function delete_mailbox($mailbox)
2967    {
2968        $delm = $this->get_hierarchy_delimiter();
2969
2970        // get list of folders
2971        if ((strpos($mailbox, '%') === false) && (strpos($mailbox, '*') === false))
2972            $sub_mboxes = $this->list_unsubscribed('', $mailbox . $delm . '*');
2973        else
2974            $sub_mboxes = $this->list_unsubscribed();
2975
2976        // send delete command to server
2977        $result = $this->conn->deleteFolder($mailbox);
2978
2979        if ($result) {
2980            // unsubscribe mailbox
2981            $this->conn->unsubscribe($mailbox);
2982
2983            foreach ($sub_mboxes as $c_mbox) {
2984                if (preg_match('/^'.preg_quote($mailbox.$delm, '/').'/', $c_mbox)) {
2985                    $this->conn->unsubscribe($c_mbox);
2986                    if ($this->conn->deleteFolder($c_mbox)) {
2987                            $this->clear_message_cache($c_mbox);
2988                    }
2989                }
2990            }
2991
2992            // clear mailbox-related cache
2993            $this->clear_message_cache($mailbox);
2994            $this->clear_cache('mailboxes', true);
2995        }
2996
2997        return $result;
2998    }
2999
3000
3001    /**
3002     * Create all folders specified as default
3003     */
3004    function create_default_folders()
3005    {
3006        // create default folders if they do not exist
3007        foreach ($this->default_folders as $folder) {
3008            if (!$this->mailbox_exists($folder))
3009                $this->create_mailbox($folder, true);
3010            else if (!$this->mailbox_exists($folder, true))
3011                $this->subscribe($folder);
3012        }
3013    }
3014
3015
3016    /**
3017     * Checks if folder exists and is subscribed
3018     *
3019     * @param string   $mailbox      Folder name
3020     * @param boolean  $subscription Enable subscription checking
3021     *
3022     * @return boolean TRUE or FALSE
3023     */
3024    function mailbox_exists($mailbox, $subscription=false)
3025    {
3026        if ($mailbox == 'INBOX') {
3027            return true;
3028        }
3029
3030        $key  = $subscription ? 'subscribed' : 'existing';
3031
3032        if (is_array($this->icache[$key]) && in_array($mailbox, $this->icache[$key]))
3033            return true;
3034
3035        if ($subscription) {
3036            $a_folders = $this->conn->listSubscribed('', $mailbox);
3037        }
3038        else {
3039            $a_folders = $this->conn->listMailboxes('', $mailbox);
3040        }
3041
3042        if (is_array($a_folders) && in_array($mailbox, $a_folders)) {
3043            $this->icache[$key][] = $mailbox;
3044            return true;
3045        }
3046
3047        return false;
3048    }
3049
3050
3051    /**
3052     * Returns the namespace where the folder is in
3053     *
3054     * @param string $mailbox Folder name
3055     *
3056     * @return string One of 'personal', 'other' or 'shared'
3057     * @access public
3058     */
3059    function mailbox_namespace($mailbox)
3060    {
3061        if ($mailbox == 'INBOX') {
3062            return 'personal';
3063        }
3064
3065        foreach ($this->namespace as $type => $namespace) {
3066            if (is_array($namespace)) {
3067                foreach ($namespace as $ns) {
3068                    if ($len = strlen($ns[0])) {
3069                        if (($len > 1 && $mailbox == substr($ns[0], 0, -1))
3070                            || strpos($mailbox, $ns[0]) === 0
3071                        ) {
3072                            return $type;
3073                        }
3074                    }
3075                }
3076            }
3077        }
3078
3079        return 'personal';
3080    }
3081
3082
3083    /**
3084     * Modify folder name according to namespace.
3085     * For output it removes prefix of the personal namespace if it's possible.
3086     * For input it adds the prefix. Use it before creating a folder in root
3087     * of the folders tree.
3088     *
3089     * @param string $mailbox Folder name
3090     * @param string $mode    Mode name (out/in)
3091     *
3092     * @return string Folder name
3093     */
3094    function mod_mailbox($mailbox, $mode = 'out')
3095    {
3096        if (!strlen($mailbox)) {
3097            return $mailbox;
3098        }
3099
3100        $prefix     = $this->namespace['prefix']; // see set_env()
3101        $prefix_len = strlen($prefix);
3102
3103        if (!$prefix_len) {
3104            return $mailbox;
3105        }
3106
3107        // remove prefix for output
3108        if ($mode == 'out') {
3109            if (substr($mailbox, 0, $prefix_len) === $prefix) {
3110                return substr($mailbox, $prefix_len);
3111            }
3112        }
3113        // add prefix for input (e.g. folder creation)
3114        else {
3115            return $prefix . $mailbox;
3116        }
3117
3118        return $mailbox;
3119    }
3120
3121
3122    /**
3123     * Gets folder attributes from LIST response, e.g. \Noselect, \Noinferiors
3124     *
3125     * @param string $mailbox Folder name
3126     * @param bool   $force   Set to True if attributes should be refreshed
3127     *
3128     * @return array Options list
3129     */
3130    function mailbox_attributes($mailbox, $force=false)
3131    {
3132        // get attributes directly from LIST command
3133        if (!empty($this->conn->data['LIST']) && is_array($this->conn->data['LIST'][$mailbox])) {
3134            $opts = $this->conn->data['LIST'][$mailbox];
3135        }
3136        // get cached folder attributes
3137        else if (!$force) {
3138            $opts = $this->get_cache('mailboxes.attributes');
3139            $opts = $opts[$mailbox];
3140        }
3141
3142        if (!is_array($opts)) {
3143            $this->conn->listMailboxes('', $mailbox);
3144            $opts = $this->conn->data['LIST'][$mailbox];
3145        }
3146
3147        return is_array($opts) ? $opts : array();
3148    }
3149
3150
3151    /**
3152     * Gets connection (and current mailbox) data: UIDVALIDITY, EXISTS, RECENT,
3153     * PERMANENTFLAGS, UIDNEXT, UNSEEN
3154     *
3155     * @param string $mailbox Folder name
3156     *
3157     * @return array Data
3158     */
3159    function mailbox_data($mailbox)
3160    {
3161        if (!strlen($mailbox))
3162            $mailbox = $this->mailbox !== null ? $this->mailbox : 'INBOX';
3163
3164        if ($this->conn->selected != $mailbox) {
3165            if ($this->conn->select($mailbox))
3166                $this->mailbox = $mailbox;
3167            else
3168                return null;
3169        }
3170
3171        $data = $this->conn->data;
3172
3173        // add (E)SEARCH result for ALL UNDELETED query
3174        if (!empty($this->icache['undeleted_idx'])
3175            && $this->icache['undeleted_idx']->getParameters('MAILBOX') == $mailbox
3176        ) {
3177            $data['UNDELETED'] = $this->icache['undeleted_idx'];
3178        }
3179
3180        return $data;
3181    }
3182
3183
3184    /**
3185     * Returns extended information about the folder
3186     *
3187     * @param string $mailbox Folder name
3188     *
3189     * @return array Data
3190     */
3191    function mailbox_info($mailbox)
3192    {
3193        if ($this->icache['options'] && $this->icache['options']['name'] == $mailbox) {
3194            return $this->icache['options'];
3195        }
3196
3197        $acl       = $this->get_capability('ACL');
3198        $namespace = $this->get_namespace();
3199        $options   = array();
3200
3201        // check if the folder is a namespace prefix
3202        if (!empty($namespace)) {
3203            $mbox = $mailbox . $this->delimiter;
3204            foreach ($namespace as $ns) {
3205                if (!empty($ns)) {
3206                    foreach ($ns as $item) {
3207                        if ($item[0] === $mbox) {
3208                            $options['is_root'] = true;
3209                            break 2;
3210                        }
3211                    }
3212                }
3213            }
3214        }
3215        // check if the folder is other user virtual-root
3216        if (!$options['is_root'] && !empty($namespace) && !empty($namespace['other'])) {
3217            $parts = explode($this->delimiter, $mailbox);
3218            if (count($parts) == 2) {
3219                $mbox = $parts[0] . $this->delimiter;
3220                foreach ($namespace['other'] as $item) {
3221                    if ($item[0] === $mbox) {
3222                        $options['is_root'] = true;
3223                        break;
3224                    }
3225                }
3226            }
3227        }
3228
3229        $options['name']       = $mailbox;
3230        $options['attributes'] = $this->mailbox_attributes($mailbox, true);
3231        $options['namespace']  = $this->mailbox_namespace($mailbox);
3232        $options['rights']     = $acl && !$options['is_root'] ? (array)$this->my_rights($mailbox) : array();
3233        $options['special']    = in_array($mailbox, $this->default_folders);
3234
3235        // Set 'noselect' and 'norename' flags
3236        if (is_array($options['attributes'])) {
3237            foreach ($options['attributes'] as $attrib) {
3238                $attrib = strtolower($attrib);
3239                if ($attrib == '\noselect' || $attrib == '\nonexistent') {
3240                    $options['noselect'] = true;
3241                }
3242            }
3243        }
3244        else {
3245            $options['noselect'] = true;
3246        }
3247
3248        if (!empty($options['rights'])) {
3249            $options['norename'] = !in_array('x', $options['rights']) && !in_array('d', $options['rights']);
3250
3251            if (!$options['noselect']) {
3252                $options['noselect'] = !in_array('r', $options['rights']);
3253            }
3254        }
3255        else {
3256            $options['norename'] = $options['is_root'] || $options['namespace'] != 'personal';
3257        }
3258
3259        $this->icache['options'] = $options;
3260
3261        return $options;
3262    }
3263
3264
3265    /**
3266     * Synchronizes messages cache.
3267     *
3268     * @param string $mailbox Folder name
3269     */
3270    public function mailbox_sync($mailbox)
3271    {
3272        if ($mcache = $this->get_mcache_engine()) {
3273            $mcache->synchronize($mailbox);
3274        }
3275    }
3276
3277
3278    /**
3279     * Get message header names for rcube_imap_generic::fetchHeader(s)
3280     *
3281     * @return string Space-separated list of header names
3282     */
3283    private function get_fetch_headers()
3284    {
3285        $headers = explode(' ', $this->fetch_add_headers);
3286        $headers = array_map('strtoupper', $headers);
3287
3288        if ($this->messages_caching || $this->get_all_headers)
3289            $headers = array_merge($headers, $this->all_headers);
3290
3291        return implode(' ', array_unique($headers));
3292    }
3293
3294
3295    /* -----------------------------------------
3296     *   ACL and METADATA/ANNOTATEMORE methods
3297     * ----------------------------------------*/
3298
3299    /**
3300     * Changes the ACL on the specified mailbox (SETACL)
3301     *
3302     * @param string $mailbox Mailbox name
3303     * @param string $user    User name
3304     * @param string $acl     ACL string
3305     *
3306     * @return boolean True on success, False on failure
3307     *
3308     * @access public
3309     * @since 0.5-beta
3310     */
3311    function set_acl($mailbox, $user, $acl)
3312    {
3313        if ($this->get_capability('ACL'))
3314            return $this->conn->setACL($mailbox, $user, $acl);
3315
3316        return false;
3317    }
3318
3319
3320    /**
3321     * Removes any <identifier,rights> pair for the
3322     * specified user from the ACL for the specified
3323     * mailbox (DELETEACL)
3324     *
3325     * @param string $mailbox Mailbox name
3326     * @param string $user    User name
3327     *
3328     * @return boolean True on success, False on failure
3329     *
3330     * @access public
3331     * @since 0.5-beta
3332     */
3333    function delete_acl($mailbox, $user)
3334    {
3335        if ($this->get_capability('ACL'))
3336            return $this->conn->deleteACL($mailbox, $user);
3337
3338        return false;
3339    }
3340
3341
3342    /**
3343     * Returns the access control list for mailbox (GETACL)
3344     *
3345     * @param string $mailbox Mailbox name
3346     *
3347     * @return array User-rights array on success, NULL on error
3348     * @access public
3349     * @since 0.5-beta
3350     */
3351    function get_acl($mailbox)
3352    {
3353        if ($this->get_capability('ACL'))
3354            return $this->conn->getACL($mailbox);
3355
3356        return NULL;
3357    }
3358
3359
3360    /**
3361     * Returns information about what rights can be granted to the
3362     * user (identifier) in the ACL for the mailbox (LISTRIGHTS)
3363     *
3364     * @param string $mailbox Mailbox name
3365     * @param string $user    User name
3366     *
3367     * @return array List of user rights
3368     * @access public
3369     * @since 0.5-beta
3370     */
3371    function list_rights($mailbox, $user)
3372    {
3373        if ($this->get_capability('ACL'))
3374            return $this->conn->listRights($mailbox, $user);
3375
3376        return NULL;
3377    }
3378
3379
3380    /**
3381     * Returns the set of rights that the current user has to
3382     * mailbox (MYRIGHTS)
3383     *
3384     * @param string $mailbox Mailbox name
3385     *
3386     * @return array MYRIGHTS response on success, NULL on error
3387     * @access public
3388     * @since 0.5-beta
3389     */
3390    function my_rights($mailbox)
3391    {
3392        if ($this->get_capability('ACL'))
3393            return $this->conn->myRights($mailbox);
3394
3395        return NULL;
3396    }
3397
3398
3399    /**
3400     * Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
3401     *
3402     * @param string $mailbox Mailbox name (empty for server metadata)
3403     * @param array  $entries Entry-value array (use NULL value as NIL)
3404     *
3405     * @return boolean True on success, False on failure
3406     * @access public
3407     * @since 0.5-beta
3408     */
3409    function set_metadata($mailbox, $entries)
3410    {
3411        if ($this->get_capability('METADATA') ||
3412            (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
3413        ) {
3414            return $this->conn->setMetadata($mailbox, $entries);
3415        }
3416        else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
3417            foreach ((array)$entries as $entry => $value) {
3418                list($ent, $attr) = $this->md2annotate($entry);
3419                $entries[$entry] = array($ent, $attr, $value);
3420            }
3421            return $this->conn->setAnnotation($mailbox, $entries);
3422        }
3423
3424        return false;
3425    }
3426
3427
3428    /**
3429     * Unsets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
3430     *
3431     * @param string $mailbox Mailbox name (empty for server metadata)
3432     * @param array  $entries Entry names array
3433     *
3434     * @return boolean True on success, False on failure
3435     *
3436     * @access public
3437     * @since 0.5-beta
3438     */
3439    function delete_metadata($mailbox, $entries)
3440    {
3441        if ($this->get_capability('METADATA') || 
3442            (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
3443        ) {
3444            return $this->conn->deleteMetadata($mailbox, $entries);
3445        }
3446        else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
3447            foreach ((array)$entries as $idx => $entry) {
3448                list($ent, $attr) = $this->md2annotate($entry);
3449                $entries[$idx] = array($ent, $attr, NULL);
3450            }
3451            return $this->conn->setAnnotation($mailbox, $entries);
3452        }
3453
3454        return false;
3455    }
3456
3457
3458    /**
3459     * Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION)
3460     *
3461     * @param string $mailbox Mailbox name (empty for server metadata)
3462     * @param array  $entries Entries
3463     * @param array  $options Command options (with MAXSIZE and DEPTH keys)
3464     *
3465     * @return array Metadata entry-value hash array on success, NULL on error
3466     *
3467     * @access public
3468     * @since 0.5-beta
3469     */
3470    function get_metadata($mailbox, $entries, $options=array())
3471    {
3472        if ($this->get_capability('METADATA') || 
3473            (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
3474        ) {
3475            return $this->conn->getMetadata($mailbox, $entries, $options);
3476        }
3477        else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
3478            $queries = array();
3479            $res     = array();
3480
3481            // Convert entry names
3482            foreach ((array)$entries as $entry) {
3483                list($ent, $attr) = $this->md2annotate($entry);
3484                $queries[$attr][] = $ent;
3485            }
3486
3487            // @TODO: Honor MAXSIZE and DEPTH options
3488            foreach ($queries as $attrib => $entry)
3489                if ($result = $this->conn->getAnnotation($mailbox, $entry, $attrib))
3490                    $res = array_merge_recursive($res, $result);
3491
3492            return $res;
3493        }
3494
3495        return NULL;
3496    }
3497
3498
3499    /**
3500     * Converts the METADATA extension entry name into the correct
3501     * entry-attrib names for older ANNOTATEMORE version.
3502     *
3503     * @param string $entry Entry name
3504     *
3505     * @return array Entry-attribute list, NULL if not supported (?)
3506     */
3507    private function md2annotate($entry)
3508    {
3509        if (substr($entry, 0, 7) == '/shared') {
3510            return array(substr($entry, 7), 'value.shared');
3511        }
3512        else if (substr($entry, 0, 8) == '/private') {
3513            return array(substr($entry, 8), 'value.priv');
3514        }
3515
3516        // @TODO: log error
3517        return NULL;
3518    }
3519
3520
3521    /* --------------------------------
3522     *   internal caching methods
3523     * --------------------------------*/
3524
3525    /**
3526     * Enable or disable indexes caching
3527     *
3528     * @param string $type Cache type (@see rcmail::get_cache)
3529     * @access public
3530     */
3531    function set_caching($type)
3532    {
3533        if ($type) {
3534            $this->caching = $type;
3535        }
3536        else {
3537            if ($this->cache)
3538                $this->cache->close();
3539            $this->cache   = null;
3540            $this->caching = false;
3541        }
3542    }
3543
3544    /**
3545     * Getter for IMAP cache object
3546     */
3547    private function get_cache_engine()
3548    {
3549        if ($this->caching && !$this->cache) {
3550            $rcmail = rcmail::get_instance();
3551            $this->cache = $rcmail->get_cache('IMAP', $this->caching);
3552        }
3553
3554        return $this->cache;
3555    }
3556
3557    /**
3558     * Returns cached value
3559     *
3560     * @param string $key Cache key
3561     * @return mixed
3562     * @access public
3563     */
3564    function get_cache($key)
3565    {
3566        if ($cache = $this->get_cache_engine()) {
3567            return $cache->get($key);
3568        }
3569    }
3570
3571    /**
3572     * Update cache
3573     *
3574     * @param string $key  Cache key
3575     * @param mixed  $data Data
3576     * @access public
3577     */
3578    function update_cache($key, $data)
3579    {
3580        if ($cache = $this->get_cache_engine()) {
3581            $cache->set($key, $data);
3582        }
3583    }
3584
3585    /**
3586     * Clears the cache.
3587     *
3588     * @param string  $key         Cache key name or pattern
3589     * @param boolean $prefix_mode Enable it to clear all keys starting
3590     *                             with prefix specified in $key
3591     * @access public
3592     */
3593    function clear_cache($key=null, $prefix_mode=false)
3594    {
3595        if ($cache = $this->get_cache_engine()) {
3596            $cache->remove($key, $prefix_mode);
3597        }
3598    }
3599
3600
3601    /* --------------------------------
3602     *   message caching methods
3603     * --------------------------------*/
3604
3605    /**
3606     * Enable or disable messages caching
3607     *
3608     * @param boolean $set Flag
3609     */
3610    function set_messages_caching($set)
3611    {
3612        if ($set) {
3613            $this->messages_caching = true;
3614        }
3615        else {
3616            if ($this->mcache)
3617                $this->mcache->close();
3618            $this->mcache = null;
3619            $this->messages_caching = false;
3620        }
3621    }
3622
3623    /**
3624     * Getter for messages cache object
3625     */
3626    private function get_mcache_engine()
3627    {
3628        if ($this->messages_caching && !$this->mcache) {
3629            $rcmail = rcmail::get_instance();
3630            if ($dbh = $rcmail->get_dbh()) {
3631                $this->mcache = new rcube_imap_cache(
3632                    $dbh, $this, $rcmail->user->ID, $this->skip_deleted);
3633            }
3634        }
3635
3636        return $this->mcache;
3637    }
3638
3639    /**
3640     * Clears the messages cache.
3641     *
3642     * @param string $mailbox Folder name
3643     * @param array  $uids    Optional message UIDs to remove from cache
3644     */
3645    function clear_message_cache($mailbox = null, $uids = null)
3646    {
3647        if ($mcache = $this->get_mcache_engine()) {
3648            $mcache->clear($mailbox, $uids);
3649        }
3650    }
3651
3652
3653
3654    /* --------------------------------
3655     *   encoding/decoding methods
3656     * --------------------------------*/
3657
3658    /**
3659     * Split an address list into a structured array list
3660     *
3661     * @param string  $input  Input string
3662     * @param int     $max    List only this number of addresses
3663     * @param boolean $decode Decode address strings
3664     * @return array  Indexed list of addresses
3665     */
3666    function decode_address_list($input, $max=null, $decode=true)
3667    {
3668        $a = $this->_parse_address_list($input, $decode);
3669        $out = array();
3670        // Special chars as defined by RFC 822 need to in quoted string (or escaped).
3671        $special_chars = '[\(\)\<\>\\\.\[\]@,;:"]';
3672
3673        if (!is_array($a))
3674            return $out;
3675
3676        $c = count($a);
3677        $j = 0;
3678
3679        foreach ($a as $val) {
3680            $j++;
3681            $address = trim($val['address']);
3682            $name    = trim($val['name']);
3683
3684            if ($name && $address && $name != $address)
3685                $string = sprintf('%s <%s>', preg_match("/$special_chars/", $name) ? '"'.addcslashes($name, '"').'"' : $name, $address);
3686            else if ($address)
3687                $string = $address;
3688            else if ($name)
3689                $string = $name;
3690
3691            $out[$j] = array(
3692                'name'   => $name,
3693                'mailto' => $address,
3694                'string' => $string
3695            );
3696
3697            if ($max && $j==$max)
3698                break;
3699        }
3700
3701        return $out;
3702    }
3703
3704
3705    /**
3706     * Decode a message header value
3707     *
3708     * @param string  $input         Header value
3709     * @param boolean $remove_quotas Remove quotes if necessary
3710     * @return string Decoded string
3711     */
3712    function decode_header($input, $remove_quotes=false)
3713    {
3714        $str = rcube_imap::decode_mime_string((string)$input, $this->default_charset);
3715        if ($str[0] == '"' && $remove_quotes)
3716            $str = str_replace('"', '', $str);
3717
3718        return $str;
3719    }
3720
3721
3722    /**
3723     * Decode a mime-encoded string to internal charset
3724     *
3725     * @param string $input    Header value
3726     * @param string $fallback Fallback charset if none specified
3727     *
3728     * @return string Decoded string
3729     * @static
3730     */
3731    public static function decode_mime_string($input, $fallback=null)
3732    {
3733        if (!empty($fallback)) {
3734            $default_charset = $fallback;
3735        }
3736        else {
3737            $default_charset = rcmail::get_instance()->config->get('default_charset', 'ISO-8859-1');
3738        }
3739
3740        // rfc: all line breaks or other characters not found
3741        // in the Base64 Alphabet must be ignored by decoding software
3742        // delete all blanks between MIME-lines, differently we can
3743        // receive unnecessary blanks and broken utf-8 symbols
3744        $input = preg_replace("/\?=\s+=\?/", '?==?', $input);
3745
3746        // encoded-word regexp
3747        $re = '/=\?([^?]+)\?([BbQq])\?([^\n]*?)\?=/';
3748
3749        // Find all RFC2047's encoded words
3750        if (preg_match_all($re, $input, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {
3751            // Initialize variables
3752            $tmp   = array();
3753            $out   = '';
3754            $start = 0;
3755
3756            foreach ($matches as $idx => $m) {
3757                $pos      = $m[0][1];
3758                $charset  = $m[1][0];
3759                $encoding = $m[2][0];
3760                $text     = $m[3][0];
3761                $length   = strlen($m[0][0]);
3762
3763                // Append everything that is before the text to be decoded
3764                if ($start != $pos) {
3765                    $substr = substr($input, $start, $pos-$start);
3766                    $out   .= rcube_charset_convert($substr, $default_charset);
3767                    $start  = $pos;
3768                }
3769                $start += $length;
3770
3771                // Per RFC2047, each string part "MUST represent an integral number
3772                // of characters . A multi-octet character may not be split across
3773                // adjacent encoded-words." However, some mailers break this, so we
3774                // try to handle characters spanned across parts anyway by iterating
3775                // through and aggregating sequential encoded parts with the same
3776                // character set and encoding, then perform the decoding on the
3777                // aggregation as a whole.
3778
3779                $tmp[] = $text;
3780                if ($next_match = $matches[$idx+1]) {
3781                    if ($next_match[0][1] == $start
3782                        && $next_match[1][0] == $charset
3783                        && $next_match[2][0] == $encoding
3784                    ) {
3785                        continue;
3786                    }
3787                }
3788
3789                $count = count($tmp);
3790                $text  = '';
3791
3792                // Decode and join encoded-word's chunks
3793                if ($encoding == 'B' || $encoding == 'b') {
3794                    // base64 must be decoded a segment at a time
3795                    for ($i=0; $i<$count; $i++)
3796                        $text .= base64_decode($tmp[$i]);
3797                }
3798                else { //if ($encoding == 'Q' || $encoding == 'q') {
3799                    // quoted printable can be combined and processed at once
3800                    for ($i=0; $i<$count; $i++)
3801                        $text .= $tmp[$i];
3802
3803                    $text = str_replace('_', ' ', $text);
3804                    $text = quoted_printable_decode($text);
3805                }
3806
3807                $out .= rcube_charset_convert($text, $charset);
3808                $tmp = array();
3809            }
3810
3811            // add the last part of the input string
3812            if ($start != strlen($input)) {
3813                $out .= rcube_charset_convert(substr($input, $start), $default_charset);
3814            }
3815
3816            // return the results
3817            return $out;
3818        }
3819
3820        // no encoding information, use fallback
3821        return rcube_charset_convert($input, $default_charset);
3822    }
3823
3824
3825    /**
3826     * Decode a mime part
3827     *
3828     * @param string $input    Input string
3829     * @param string $encoding Part encoding
3830     * @return string Decoded string
3831     */
3832    function mime_decode($input, $encoding='7bit')
3833    {
3834        switch (strtolower($encoding)) {
3835        case 'quoted-printable':
3836            return quoted_printable_decode($input);
3837        case 'base64':
3838            return base64_decode($input);
3839        case 'x-uuencode':
3840        case 'x-uue':
3841        case 'uue':
3842        case 'uuencode':
3843            return convert_uudecode($input);
3844        case '7bit':
3845        default:
3846            return $input;
3847        }
3848    }
3849
3850
3851    /* --------------------------------
3852     *         private methods
3853     * --------------------------------*/
3854
3855    /**
3856     * Validate the given input and save to local properties
3857     *
3858     * @param string $sort_field Sort column
3859     * @param string $sort_order Sort order
3860     * @access private
3861     */
3862    private function set_sort_order($sort_field, $sort_order)
3863    {
3864        if ($sort_field != null)
3865            $this->sort_field = asciiwords($sort_field);
3866        if ($sort_order != null)
3867            $this->sort_order = strtoupper($sort_order) == 'DESC' ? 'DESC' : 'ASC';
3868    }
3869
3870
3871    /**
3872     * Sort mailboxes first by default folders and then in alphabethical order
3873     *
3874     * @param array $a_folders Mailboxes list
3875     * @access private
3876     */
3877    private function _sort_mailbox_list($a_folders)
3878    {
3879        $a_out = $a_defaults = $folders = array();
3880
3881        $delimiter = $this->get_hierarchy_delimiter();
3882
3883        // find default folders and skip folders starting with '.'
3884        foreach ($a_folders as $i => $folder) {
3885            if ($folder[0] == '.')
3886                continue;
3887
3888            if (($p = array_search($folder, $this->default_folders)) !== false && !$a_defaults[$p])
3889                $a_defaults[$p] = $folder;
3890            else
3891                $folders[$folder] = rcube_charset_convert($folder, 'UTF7-IMAP');
3892        }
3893
3894        // sort folders and place defaults on the top
3895        asort($folders, SORT_LOCALE_STRING);
3896        ksort($a_defaults);
3897        $folders = array_merge($a_defaults, array_keys($folders));
3898
3899        // finally we must rebuild the list to move
3900        // subfolders of default folders to their place...
3901        // ...also do this for the rest of folders because
3902        // asort() is not properly sorting case sensitive names
3903        while (list($key, $folder) = each($folders)) {
3904            // set the type of folder name variable (#1485527)
3905            $a_out[] = (string) $folder;
3906            unset($folders[$key]);
3907            $this->_rsort($folder, $delimiter, $folders, $a_out);
3908        }
3909
3910        return $a_out;
3911    }
3912
3913
3914    /**
3915     * @access private
3916     */
3917    private function _rsort($folder, $delimiter, &$list, &$out)
3918    {
3919        while (list($key, $name) = each($list)) {
3920                if (strpos($name, $folder.$delimiter) === 0) {
3921                    // set the type of folder name variable (#1485527)
3922                $out[] = (string) $name;
3923                    unset($list[$key]);
3924                    $this->_rsort($name, $delimiter, $list, $out);
3925                }
3926        }
3927        reset($list);
3928    }
3929
3930
3931    /**
3932     * Find UID of the specified message sequence ID
3933     *
3934     * @param int    $id       Message (sequence) ID
3935     * @param string $mailbox  Mailbox name
3936     *
3937     * @return int Message UID
3938     */
3939    function id2uid($id, $mailbox = null)
3940    {
3941        if (!strlen($mailbox)) {
3942            $mailbox = $this->mailbox;
3943        }
3944
3945        if ($uid = array_search($id, (array)$this->uid_id_map[$mailbox])) {
3946            return $uid;
3947        }
3948
3949        $uid = $this->conn->ID2UID($mailbox, $id);
3950
3951        $this->uid_id_map[$mailbox][$uid] = $id;
3952
3953        return $uid;
3954    }
3955
3956
3957    /**
3958     * Subscribe/unsubscribe a list of mailboxes and update local cache
3959     * @access private
3960     */
3961    private function _change_subscription($a_mboxes, $mode)
3962    {
3963        $updated = false;
3964
3965        if (is_array($a_mboxes))
3966            foreach ($a_mboxes as $i => $mailbox) {
3967                $a_mboxes[$i] = $mailbox;
3968
3969                if ($mode == 'subscribe')
3970                    $updated = $this->conn->subscribe($mailbox);
3971                else if ($mode == 'unsubscribe')
3972                    $updated = $this->conn->unsubscribe($mailbox);
3973            }
3974
3975        // clear cached mailbox list(s)
3976        if ($updated) {
3977            $this->clear_cache('mailboxes', true);
3978        }
3979
3980        return $updated;
3981    }
3982
3983
3984    /**
3985     * Increde/decrese messagecount for a specific mailbox
3986     * @access private
3987     */
3988    private function _set_messagecount($mailbox, $mode, $increment)
3989    {
3990        $mode = strtoupper($mode);
3991        $a_mailbox_cache = $this->get_cache('messagecount');
3992
3993        if (!is_array($a_mailbox_cache[$mailbox]) || !isset($a_mailbox_cache[$mailbox][$mode]) || !is_numeric($increment))
3994            return false;
3995
3996        // add incremental value to messagecount
3997        $a_mailbox_cache[$mailbox][$mode] += $increment;
3998
3999        // there's something wrong, delete from cache
4000        if ($a_mailbox_cache[$mailbox][$mode] < 0)
4001            unset($a_mailbox_cache[$mailbox][$mode]);
4002
4003        // write back to cache
4004        $this->update_cache('messagecount', $a_mailbox_cache);
4005
4006        return true;
4007    }
4008
4009
4010    /**
4011     * Remove messagecount of a specific mailbox from cache
4012     * @access private
4013     */
4014    private function _clear_messagecount($mailbox, $mode=null)
4015    {
4016        $a_mailbox_cache = $this->get_cache('messagecount');
4017
4018        if (is_array($a_mailbox_cache[$mailbox])) {
4019            if ($mode) {
4020                unset($a_mailbox_cache[$mailbox][$mode]);
4021            }
4022            else {
4023                unset($a_mailbox_cache[$mailbox]);
4024            }
4025            $this->update_cache('messagecount', $a_mailbox_cache);
4026        }
4027    }
4028
4029
4030    /**
4031     * Split RFC822 header string into an associative array
4032     * @access private
4033     */
4034    private function _parse_headers($headers)
4035    {
4036        $a_headers = array();
4037        $headers = preg_replace('/\r?\n(\t| )+/', ' ', $headers);
4038        $lines = explode("\n", $headers);
4039        $c = count($lines);
4040
4041        for ($i=0; $i<$c; $i++) {
4042            if ($p = strpos($lines[$i], ': ')) {
4043                $field = strtolower(substr($lines[$i], 0, $p));
4044                $value = trim(substr($lines[$i], $p+1));
4045                if (!empty($value))
4046                    $a_headers[$field] = $value;
4047            }
4048        }
4049
4050        return $a_headers;
4051    }
4052
4053
4054    /**
4055     * @access private
4056     */
4057    private function _parse_address_list($str, $decode=true)
4058    {
4059        // remove any newlines and carriage returns before
4060        $str = preg_replace('/\r?\n(\s|\t)?/', ' ', $str);
4061
4062        // extract list items, remove comments
4063        $str = self::explode_header_string(',;', $str, true);
4064        $result = array();
4065
4066        // simplified regexp, supporting quoted local part
4067        $email_rx = '(\S+|("\s*(?:[^"\f\n\r\t\v\b\s]+\s*)+"))@\S+';
4068
4069        foreach ($str as $key => $val) {
4070            $name    = '';
4071            $address = '';
4072            $val     = trim($val);
4073
4074            if (preg_match('/(.*)<('.$email_rx.')>$/', $val, $m)) {
4075                $address = $m[2];
4076                $name    = trim($m[1]);
4077            }
4078            else if (preg_match('/^('.$email_rx.')$/', $val, $m)) {
4079                $address = $m[1];
4080                $name    = '';
4081            }
4082            else {
4083                $name = $val;
4084            }
4085
4086            // dequote and/or decode name
4087            if ($name) {
4088                if ($name[0] == '"' && $name[strlen($name)-1] == '"') {
4089                    $name = substr($name, 1, -1);
4090                    $name = stripslashes($name);
4091                }
4092                if ($decode) {
4093                    $name = $this->decode_header($name);
4094                }
4095            }
4096
4097            if (!$address && $name) {
4098                $address = $name;
4099            }
4100
4101            if ($address) {
4102                $result[$key] = array('name' => $name, 'address' => $address);
4103            }
4104        }
4105
4106        return $result;
4107    }
4108
4109
4110    /**
4111     * Explodes header (e.g. address-list) string into array of strings
4112     * using specified separator characters with proper handling
4113     * of quoted-strings and comments (RFC2822)
4114     *
4115     * @param string $separator       String containing separator characters
4116     * @param string $str             Header string
4117     * @param bool   $remove_comments Enable to remove comments
4118     *
4119     * @return array Header items
4120     */
4121    static function explode_header_string($separator, $str, $remove_comments=false)
4122    {
4123        $length  = strlen($str);
4124        $result  = array();
4125        $quoted  = false;
4126        $comment = 0;
4127        $out     = '';
4128
4129        for ($i=0; $i<$length; $i++) {
4130            // we're inside a quoted string
4131            if ($quoted) {
4132                if ($str[$i] == '"') {
4133                    $quoted = false;
4134                }
4135                else if ($str[$i] == '\\') {
4136                    if ($comment <= 0) {
4137                        $out .= '\\';
4138                    }
4139                    $i++;
4140                }
4141            }
4142            // we're inside a comment string
4143            else if ($comment > 0) {
4144                    if ($str[$i] == ')') {
4145                        $comment--;
4146                    }
4147                    else if ($str[$i] == '(') {
4148                        $comment++;
4149                    }
4150                    else if ($str[$i] == '\\') {
4151                        $i++;
4152                    }
4153                    continue;
4154            }
4155            // separator, add to result array
4156            else if (strpos($separator, $str[$i]) !== false) {
4157                    if ($out) {
4158                        $result[] = $out;
4159                    }
4160                    $out = '';
4161                    continue;
4162            }
4163            // start of quoted string
4164            else if ($str[$i] == '"') {
4165                    $quoted = true;
4166            }
4167            // start of comment
4168            else if ($remove_comments && $str[$i] == '(') {
4169                    $comment++;
4170            }
4171
4172            if ($comment <= 0) {
4173                $out .= $str[$i];
4174            }
4175        }
4176
4177        if ($out && $comment <= 0) {
4178            $result[] = $out;
4179        }
4180
4181        return $result;
4182    }
4183
4184
4185    /**
4186     * This is our own debug handler for the IMAP connection
4187     * @access public
4188     */
4189    public function debug_handler(&$imap, $message)
4190    {
4191        write_log('imap', $message);
4192    }
4193
4194}  // end class rcube_imap
4195
4196
4197/**
4198 * Class representing a message part
4199 *
4200 * @package Mail
4201 */
4202class rcube_message_part
4203{
4204    var $mime_id = '';
4205    var $ctype_primary = 'text';
4206    var $ctype_secondary = 'plain';
4207    var $mimetype = 'text/plain';
4208    var $disposition = '';
4209    var $filename = '';
4210    var $encoding = '8bit';
4211    var $charset = '';
4212    var $size = 0;
4213    var $headers = array();
4214    var $d_parameters = array();
4215    var $ctype_parameters = array();
4216
4217    function __clone()
4218    {
4219        if (isset($this->parts))
4220            foreach ($this->parts as $idx => $part)
4221                if (is_object($part))
4222                        $this->parts[$idx] = clone $part;
4223    }
4224}
4225
4226
4227/**
4228 * Class for sorting an array of rcube_mail_header objects in a predetermined order.
4229 *
4230 * @package Mail
4231 * @author Eric Stadtherr
4232 */
4233class rcube_header_sorter
4234{
4235    private $uids = array();
4236
4237
4238    /**
4239     * Set the predetermined sort order.
4240     *
4241     * @param array $index  Numerically indexed array of IMAP UIDs
4242     */
4243    function set_index($index)
4244    {
4245        $index = array_flip($index);
4246
4247        $this->uids = $index;
4248    }
4249
4250    /**
4251     * Sort the array of header objects
4252     *
4253     * @param array $headers Array of rcube_mail_header objects indexed by UID
4254     */
4255    function sort_headers(&$headers)
4256    {
4257        uksort($headers, array($this, "compare_uids"));
4258    }
4259
4260    /**
4261     * Sort method called by uksort()
4262     *
4263     * @param int $a Array key (UID)
4264     * @param int $b Array key (UID)
4265     */
4266    function compare_uids($a, $b)
4267    {
4268        // then find each sequence number in my ordered list
4269        $posa = isset($this->uids[$a]) ? intval($this->uids[$a]) : -1;
4270        $posb = isset($this->uids[$b]) ? intval($this->uids[$b]) : -1;
4271
4272        // return the relative position as the comparison value
4273        return $posa - $posb;
4274    }
4275}
Note: See TracBrowser for help on using the repository browser.