source: github/program/include/rcube_imap.php @ 0af82c8

HEADcourier-fixdev-browser-capabilitiespdo
Last change on this file since 0af82c8 was 0af82c8, checked in by Aleksander Machniak <alec@…>, 12 months ago

Fix listing folders on Courier IMAP (#1488466)

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