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

Last change on this file since 6108 was 6108, checked in by thomasb, 13 months ago

Revert r6094; Add caching for ACL and Metadata

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