source: github/program/include/rcube_imap.php @ 1d5b73f

HEADdev-browser-capabilitiespdo
Last change on this file since 1d5b73f was 1d5b73f, checked in by Thomas Bruederli <thomas@…>, 12 months ago

Add lost method for backwards compatibility

  • Property mode set to 100644
File size: 121.0 KB
Line 
1<?php
2
3/*
4 +-----------------------------------------------------------------------+
5 | program/include/rcube_imap.php                                        |
6 |                                                                       |
7 | This file is part of the Roundcube Webmail client                     |
8 | Copyright (C) 2005-2012, The Roundcube Dev Team                       |
9 | Copyright (C) 2011-2012, Kolab Systems AG                             |
10 |                                                                       |
11 | Licensed under the GNU General Public License version 3 or            |
12 | any later version with exceptions for skins & plugins.                |
13 | See the README file for a full license statement.                     |
14 |                                                                       |
15 | PURPOSE:                                                              |
16 |   IMAP Storage Engine                                                 |
17 |                                                                       |
18 +-----------------------------------------------------------------------+
19 | Author: Thomas Bruederli <roundcube@gmail.com>                        |
20 | Author: Aleksander Machniak <alec@alec.pl>                            |
21 +-----------------------------------------------------------------------+
22
23 $Id$
24
25*/
26
27
28/**
29 * Interface class for accessing an IMAP server
30 *
31 * @package    Mail
32 * @author     Thomas Bruederli <roundcube@gmail.com>
33 * @author     Aleksander Machniak <alec@alec.pl>
34 * @version    2.0
35 */
36class rcube_imap extends rcube_storage
37{
38    /**
39     * Instance of rcube_imap_generic
40     *
41     * @var rcube_imap_generic
42     */
43    public $conn;
44
45    /**
46     * Instance of rcube_imap_cache
47     *
48     * @var rcube_imap_cache
49     */
50    protected $mcache;
51
52    /**
53     * Instance of rcube_cache
54     *
55     * @var rcube_cache
56     */
57    protected $cache;
58
59    /**
60     * Internal (in-memory) cache
61     *
62     * @var array
63     */
64    protected $icache = array();
65
66    protected $list_page = 1;
67    protected $delimiter;
68    protected $namespace;
69    protected $sort_field = '';
70    protected $sort_order = 'DESC';
71    protected $struct_charset;
72    protected $uid_id_map = array();
73    protected $msg_headers = array();
74    protected $search_set;
75    protected $search_string = '';
76    protected $search_charset = '';
77    protected $search_sort_field = '';
78    protected $search_threads = false;
79    protected $search_sorted = false;
80    protected $options = array('auth_method' => 'check');
81    protected $caching = false;
82    protected $messages_caching = false;
83    protected $threading = false;
84
85
86    /**
87     * Object constructor.
88     */
89    public function __construct()
90    {
91        $this->conn = new rcube_imap_generic();
92
93        // Set namespace and delimiter from session,
94        // so some methods would work before connection
95        if (isset($_SESSION['imap_namespace'])) {
96            $this->namespace = $_SESSION['imap_namespace'];
97        }
98        if (isset($_SESSION['imap_delimiter'])) {
99            $this->delimiter = $_SESSION['imap_delimiter'];
100        }
101    }
102
103
104    /**
105     * Magic getter for backward compat.
106     *
107     * @deprecated.
108     */
109    public function __get($name)
110    {
111        if (isset($this->{$name})) {
112            return $this->{$name};
113        }
114    }
115
116
117    /**
118     * Connect to an IMAP server
119     *
120     * @param  string   $host    Host to connect
121     * @param  string   $user    Username for IMAP account
122     * @param  string   $pass    Password for IMAP account
123     * @param  integer  $port    Port to connect to
124     * @param  string   $use_ssl SSL schema (either ssl or tls) or null if plain connection
125     *
126     * @return boolean  TRUE on success, FALSE on failure
127     */
128    public function connect($host, $user, $pass, $port=143, $use_ssl=null)
129    {
130        // check for OpenSSL support in PHP build
131        if ($use_ssl && extension_loaded('openssl')) {
132            $this->options['ssl_mode'] = $use_ssl == 'imaps' ? 'ssl' : $use_ssl;
133        }
134        else if ($use_ssl) {
135            rcube::raise_error(array('code' => 403, 'type' => 'imap',
136                'file' => __FILE__, 'line' => __LINE__,
137                'message' => "OpenSSL not available"), true, false);
138            $port = 143;
139        }
140
141        $this->options['port'] = $port;
142
143        if ($this->options['debug']) {
144            $this->set_debug(true);
145
146            $this->options['ident'] = array(
147                'name' => 'Roundcube Webmail',
148                'version' => RCMAIL_VERSION,
149                'php' => PHP_VERSION,
150                'os' => PHP_OS,
151                'command' => $_SERVER['REQUEST_URI'],
152            );
153        }
154
155        $attempt = 0;
156        do {
157            $data = rcube::get_instance()->plugins->exec_hook('imap_connect',
158                array_merge($this->options, array('host' => $host, 'user' => $user,
159                    'attempt' => ++$attempt)));
160
161            if (!empty($data['pass'])) {
162                $pass = $data['pass'];
163            }
164
165            $this->conn->connect($data['host'], $data['user'], $pass, $data);
166        } while(!$this->conn->connected() && $data['retry']);
167
168        $config = array(
169            'host'     => $data['host'],
170            'user'     => $data['user'],
171            'password' => $pass,
172            'port'     => $port,
173            'ssl'      => $use_ssl,
174        );
175
176        $this->options      = array_merge($this->options, $config);
177        $this->connect_done = true;
178
179        if ($this->conn->connected()) {
180            // get namespace and delimiter
181            $this->set_env();
182            return true;
183        }
184        // write error log
185        else if ($this->conn->error) {
186            if ($pass && $user) {
187                $message = sprintf("Login failed for %s from %s. %s",
188                    $user, rcube_utils::remote_ip(), $this->conn->error);
189
190                rcube::raise_error(array('code' => 403, 'type' => 'imap',
191                    'file' => __FILE__, 'line' => __LINE__,
192                    'message' => $message), true, false);
193            }
194        }
195
196        return false;
197    }
198
199
200    /**
201     * Close IMAP connection.
202     * Usually done on script shutdown
203     */
204    public function close()
205    {
206        $this->conn->closeConnection();
207        if ($this->mcache) {
208            $this->mcache->close();
209        }
210    }
211
212
213    /**
214     * Check connection state, connect if not connected.
215     *
216     * @return bool Connection state.
217     */
218    public function check_connection()
219    {
220        // Establish connection if it wasn't done yet
221        if (!$this->connect_done && !empty($this->options['user'])) {
222            return $this->connect(
223                $this->options['host'],
224                $this->options['user'],
225                $this->options['password'],
226                $this->options['port'],
227                $this->options['ssl']
228            );
229        }
230
231        return $this->is_connected();
232    }
233
234
235    /**
236     * Checks IMAP connection.
237     *
238     * @return boolean  TRUE on success, FALSE on failure
239     */
240    public function is_connected()
241    {
242        return $this->conn->connected();
243    }
244
245
246    /**
247     * Returns code of last error
248     *
249     * @return int Error code
250     */
251    public function get_error_code()
252    {
253        return $this->conn->errornum;
254    }
255
256
257    /**
258     * Returns text of last error
259     *
260     * @return string Error string
261     */
262    public function get_error_str()
263    {
264        return $this->conn->error;
265    }
266
267
268    /**
269     * Returns code of last command response
270     *
271     * @return int Response code
272     */
273    public function get_response_code()
274    {
275        switch ($this->conn->resultcode) {
276            case 'NOPERM':
277                return self::NOPERM;
278            case 'READ-ONLY':
279                return self::READONLY;
280            case 'TRYCREATE':
281                return self::TRYCREATE;
282            case 'INUSE':
283                return self::INUSE;
284            case 'OVERQUOTA':
285                return self::OVERQUOTA;
286            case 'ALREADYEXISTS':
287                return self::ALREADYEXISTS;
288            case 'NONEXISTENT':
289                return self::NONEXISTENT;
290            case 'CONTACTADMIN':
291                return self::CONTACTADMIN;
292            default:
293                return self::UNKNOWN;
294        }
295    }
296
297
298    /**
299     * Activate/deactivate debug mode
300     *
301     * @param boolean $dbg True if IMAP conversation should be logged
302     */
303    public function set_debug($dbg = true)
304    {
305        $this->options['debug'] = $dbg;
306        $this->conn->setDebug($dbg, array($this, 'debug_handler'));
307    }
308
309
310    /**
311     * Set internal folder reference.
312     * All operations will be perfomed on this folder.
313     *
314     * @param  string $folder Folder name
315     */
316    public function set_folder($folder)
317    {
318        if ($this->folder == $folder) {
319            return;
320        }
321
322        $this->folder = $folder;
323
324        // clear messagecount cache for this folder
325        $this->clear_messagecount($folder);
326    }
327
328
329    /**
330     * Save a search result for future message listing methods
331     *
332     * @param  array  $set  Search set, result from rcube_imap::get_search_set():
333     *                      0 - searching criteria, string
334     *                      1 - search result, rcube_result_index|rcube_result_thread
335     *                      2 - searching character set, string
336     *                      3 - sorting field, string
337     *                      4 - true if sorted, bool
338     */
339    public function set_search_set($set)
340    {
341        $set = (array)$set;
342
343        $this->search_string     = $set[0];
344        $this->search_set        = $set[1];
345        $this->search_charset    = $set[2];
346        $this->search_sort_field = $set[3];
347        $this->search_sorted     = $set[4];
348        $this->search_threads    = is_a($this->search_set, 'rcube_result_thread');
349    }
350
351
352    /**
353     * Return the saved search set as hash array
354     *
355     * @return array Search set
356     */
357    public function get_search_set()
358    {
359        if (empty($this->search_set)) {
360            return null;
361        }
362
363        return array(
364            $this->search_string,
365                $this->search_set,
366                $this->search_charset,
367                $this->search_sort_field,
368                $this->search_sorted,
369            );
370    }
371
372
373    /**
374     * Returns the IMAP server's capability.
375     *
376     * @param   string  $cap Capability name
377     *
378     * @return  mixed   Capability value or TRUE if supported, FALSE if not
379     */
380    public function get_capability($cap)
381    {
382        $cap      = strtoupper($cap);
383        $sess_key = "STORAGE_$cap";
384
385        if (!isset($_SESSION[$sess_key])) {
386            if (!$this->check_connection()) {
387                return false;
388            }
389
390            $_SESSION[$sess_key] = $this->conn->getCapability($cap);
391        }
392
393        return $_SESSION[$sess_key];
394    }
395
396
397    /**
398     * Checks the PERMANENTFLAGS capability of the current folder
399     * and returns true if the given flag is supported by the IMAP server
400     *
401     * @param   string  $flag Permanentflag name
402     *
403     * @return  boolean True if this flag is supported
404     */
405    public function check_permflag($flag)
406    {
407        $flag = strtoupper($flag);
408        $imap_flag = $this->conn->flags[$flag];
409
410        if ($this->folder !== null) {
411            $this->check_connection();
412        }
413        // @TODO: cache permanent flags (?)
414
415        return (in_array_nocase($imap_flag, $this->conn->data['PERMANENTFLAGS']));
416    }
417
418
419    /**
420     * Returns the delimiter that is used by the IMAP server for folder separation
421     *
422     * @return  string  Delimiter string
423     * @access  public
424     */
425    public function get_hierarchy_delimiter()
426    {
427        return $this->delimiter;
428    }
429
430
431    /**
432     * Get namespace
433     *
434     * @param string $name Namespace array index: personal, other, shared, prefix
435     *
436     * @return  array  Namespace data
437     */
438    public function get_namespace($name = null)
439    {
440        $ns = $this->namespace;
441
442        if ($name) {
443            return isset($ns[$name]) ? $ns[$name] : null;
444        }
445
446        unset($ns['prefix']);
447        return $ns;
448    }
449
450
451    /**
452     * Sets delimiter and namespaces
453     */
454    protected function set_env()
455    {
456        if ($this->delimiter !== null && $this->namespace !== null) {
457            return;
458        }
459
460        $config = rcube::get_instance()->config;
461        $imap_personal  = $config->get('imap_ns_personal');
462        $imap_other     = $config->get('imap_ns_other');
463        $imap_shared    = $config->get('imap_ns_shared');
464        $imap_delimiter = $config->get('imap_delimiter');
465
466        if (!$this->check_connection()) {
467            return;
468        }
469
470        $ns = $this->conn->getNamespace();
471
472        // Set namespaces (NAMESPACE supported)
473        if (is_array($ns)) {
474            $this->namespace = $ns;
475        }
476        else {
477            $this->namespace = array(
478                'personal' => NULL,
479                'other'    => NULL,
480                'shared'   => NULL,
481            );
482        }
483
484        if ($imap_delimiter) {
485            $this->delimiter = $imap_delimiter;
486        }
487        if (empty($this->delimiter)) {
488            $this->delimiter = $this->namespace['personal'][0][1];
489        }
490        if (empty($this->delimiter)) {
491            $this->delimiter = $this->conn->getHierarchyDelimiter();
492        }
493        if (empty($this->delimiter)) {
494            $this->delimiter = '/';
495        }
496
497        // Overwrite namespaces
498        if ($imap_personal !== null) {
499            $this->namespace['personal'] = NULL;
500            foreach ((array)$imap_personal as $dir) {
501                $this->namespace['personal'][] = array($dir, $this->delimiter);
502            }
503        }
504        if ($imap_other !== null) {
505            $this->namespace['other'] = NULL;
506            foreach ((array)$imap_other as $dir) {
507                if ($dir) {
508                    $this->namespace['other'][] = array($dir, $this->delimiter);
509                }
510            }
511        }
512        if ($imap_shared !== null) {
513            $this->namespace['shared'] = NULL;
514            foreach ((array)$imap_shared as $dir) {
515                if ($dir) {
516                    $this->namespace['shared'][] = array($dir, $this->delimiter);
517                }
518            }
519        }
520
521        // Find personal namespace prefix for mod_folder()
522        // Prefix can be removed when there is only one personal namespace
523        if (is_array($this->namespace['personal']) && count($this->namespace['personal']) == 1) {
524            $this->namespace['prefix'] = $this->namespace['personal'][0][0];
525        }
526
527        $_SESSION['imap_namespace'] = $this->namespace;
528        $_SESSION['imap_delimiter'] = $this->delimiter;
529    }
530
531
532    /**
533     * Get message count for a specific folder
534     *
535     * @param  string  $folder  Folder name
536     * @param  string  $mode    Mode for count [ALL|THREADS|UNSEEN|RECENT]
537     * @param  boolean $force   Force reading from server and update cache
538     * @param  boolean $status  Enables storing folder status info (max UID/count),
539     *                          required for folder_status()
540     *
541     * @return int     Number of messages
542     */
543    public function count($folder='', $mode='ALL', $force=false, $status=true)
544    {
545        if (!strlen($folder)) {
546            $folder = $this->folder;
547        }
548
549        return $this->countmessages($folder, $mode, $force, $status);
550    }
551
552
553    /**
554     * protected method for getting nr of messages
555     *
556     * @param string  $folder  Folder name
557     * @param string  $mode    Mode for count [ALL|THREADS|UNSEEN|RECENT]
558     * @param boolean $force   Force reading from server and update cache
559     * @param boolean $status  Enables storing folder status info (max UID/count),
560     *                         required for folder_status()
561     *
562     * @return int Number of messages
563     * @see rcube_imap::count()
564     */
565    protected function countmessages($folder, $mode='ALL', $force=false, $status=true)
566    {
567        $mode = strtoupper($mode);
568
569        // count search set, assume search set is always up-to-date (don't check $force flag)
570        if ($this->search_string && $folder == $this->folder && ($mode == 'ALL' || $mode == 'THREADS')) {
571            if ($mode == 'ALL') {
572                return $this->search_set->count_messages();
573            }
574            else {
575                return $this->search_set->count();
576            }
577        }
578
579        $a_folder_cache = $this->get_cache('messagecount');
580
581        // return cached value
582        if (!$force && is_array($a_folder_cache[$folder]) && isset($a_folder_cache[$folder][$mode])) {
583            return $a_folder_cache[$folder][$mode];
584        }
585
586        if (!is_array($a_folder_cache[$folder])) {
587            $a_folder_cache[$folder] = array();
588        }
589
590        if ($mode == 'THREADS') {
591            $res   = $this->fetch_threads($folder, $force);
592            $count = $res->count();
593
594            if ($status) {
595                $msg_count = $res->count_messages();
596                $this->set_folder_stats($folder, 'cnt', $msg_count);
597                $this->set_folder_stats($folder, 'maxuid', $msg_count ? $this->id2uid($msg_count, $folder) : 0);
598            }
599        }
600        // Need connection here
601        else if (!$this->check_connection()) {
602            return 0;
603        }
604        // RECENT count is fetched a bit different
605        else if ($mode == 'RECENT') {
606            $count = $this->conn->countRecent($folder);
607        }
608        // use SEARCH for message counting
609        else if (!empty($this->options['skip_deleted'])) {
610            $search_str = "ALL UNDELETED";
611            $keys       = array('COUNT');
612
613            if ($mode == 'UNSEEN') {
614                $search_str .= " UNSEEN";
615            }
616            else {
617                if ($this->messages_caching) {
618                    $keys[] = 'ALL';
619                }
620                if ($status) {
621                    $keys[]   = 'MAX';
622                }
623            }
624
625            // @TODO: if $force==false && $mode == 'ALL' we could try to use cache index here
626
627            // get message count using (E)SEARCH
628            // not very performant but more precise (using UNDELETED)
629            $index = $this->conn->search($folder, $search_str, true, $keys);
630            $count = $index->count();
631
632            if ($mode == 'ALL') {
633                // Cache index data, will be used in index_direct()
634                $this->icache['undeleted_idx'] = $index;
635
636                if ($status) {
637                    $this->set_folder_stats($folder, 'cnt', $count);
638                    $this->set_folder_stats($folder, 'maxuid', $index->max());
639                }
640            }
641        }
642        else {
643            if ($mode == 'UNSEEN') {
644                $count = $this->conn->countUnseen($folder);
645            }
646            else {
647                $count = $this->conn->countMessages($folder);
648                if ($status) {
649                    $this->set_folder_stats($folder,'cnt', $count);
650                    $this->set_folder_stats($folder, 'maxuid', $count ? $this->id2uid($count, $folder) : 0);
651                }
652            }
653        }
654
655        $a_folder_cache[$folder][$mode] = (int)$count;
656
657        // write back to cache
658        $this->update_cache('messagecount', $a_folder_cache);
659
660        return (int)$count;
661    }
662
663
664    /**
665     * Public method for listing headers
666     *
667     * @param   string   $folder     Folder name
668     * @param   int      $page       Current page to list
669     * @param   string   $sort_field Header field to sort by
670     * @param   string   $sort_order Sort order [ASC|DESC]
671     * @param   int      $slice      Number of slice items to extract from result array
672     *
673     * @return  array    Indexed array with message header objects
674     */
675    public function list_messages($folder='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
676    {
677        if (!strlen($folder)) {
678            $folder = $this->folder;
679        }
680
681        return $this->_list_messages($folder, $page, $sort_field, $sort_order, $slice);
682    }
683
684
685    /**
686     * protected method for listing message headers
687     *
688     * @param   string   $folder     Folder name
689     * @param   int      $page       Current page to list
690     * @param   string   $sort_field Header field to sort by
691     * @param   string   $sort_order Sort order [ASC|DESC]
692     * @param   int      $slice      Number of slice items to extract from result array
693     *
694     * @return  array    Indexed array with message header objects
695     * @see     rcube_imap::list_messages
696     */
697    protected function _list_messages($folder='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
698    {
699        if (!strlen($folder)) {
700            return array();
701        }
702
703        $this->set_sort_order($sort_field, $sort_order);
704        $page = $page ? $page : $this->list_page;
705
706        // use saved message set
707        if ($this->search_string && $folder == $this->folder) {
708            return $this->list_search_messages($folder, $page, $slice);
709        }
710
711        if ($this->threading) {
712            return $this->list_thread_messages($folder, $page, $slice);
713        }
714
715        // get UIDs of all messages in the folder, sorted
716        $index = $this->index($folder, $this->sort_field, $this->sort_order);
717
718        if ($index->is_empty()) {
719            return array();
720        }
721
722        $from = ($page-1) * $this->page_size;
723        $to   = $from + $this->page_size;
724
725        $index->slice($from, $to - $from);
726
727        if ($slice) {
728            $index->slice(-$slice, $slice);
729        }
730
731        // fetch reqested messages headers
732        $a_index = $index->get();
733        $a_msg_headers = $this->fetch_headers($folder, $a_index);
734
735        return array_values($a_msg_headers);
736    }
737
738
739    /**
740     * protected method for listing message headers using threads
741     *
742     * @param   string   $folder     Folder name
743     * @param   int      $page       Current page to list
744     * @param   int      $slice      Number of slice items to extract from result array
745     *
746     * @return  array    Indexed array with message header objects
747     * @see     rcube_imap::list_messages
748     */
749    protected function list_thread_messages($folder, $page, $slice=0)
750    {
751        // get all threads (not sorted)
752        if ($mcache = $this->get_mcache_engine()) {
753            $threads = $mcache->get_thread($folder);
754        }
755        else {
756            $threads = $this->fetch_threads($folder);
757        }
758
759        return $this->fetch_thread_headers($folder, $threads, $page, $slice);
760    }
761
762    /**
763     * Method for fetching threads data
764     *
765     * @param  string $folder  Folder name
766     * @param  bool   $force   Use IMAP server, no cache
767     *
768     * @return rcube_imap_thread Thread data object
769     */
770    function fetch_threads($folder, $force = false)
771    {
772        if (!$force && ($mcache = $this->get_mcache_engine())) {
773            // don't store in self's internal cache, cache has it's own internal cache
774            return $mcache->get_thread($folder);
775        }
776
777        if (empty($this->icache['threads'])) {
778            if (!$this->check_connection()) {
779                return new rcube_result_thread();
780            }
781
782            // get all threads
783            $result = $this->conn->thread($folder, $this->threading,
784                $this->options['skip_deleted'] ? 'UNDELETED' : '', true);
785
786            // add to internal (fast) cache
787            $this->icache['threads'] = $result;
788        }
789
790        return $this->icache['threads'];
791    }
792
793
794    /**
795     * protected method for fetching threaded messages headers
796     *
797     * @param string              $folder     Folder name
798     * @param rcube_result_thread $threads    Threads data object
799     * @param int                 $page       List page number
800     * @param int                 $slice      Number of threads to slice
801     *
802     * @return array  Messages headers
803     */
804    protected function fetch_thread_headers($folder, $threads, $page, $slice=0)
805    {
806        // Sort thread structure
807        $this->sort_threads($threads);
808
809        $from = ($page-1) * $this->page_size;
810        $to   = $from + $this->page_size;
811
812        $threads->slice($from, $to - $from);
813
814        if ($slice) {
815            $threads->slice(-$slice, $slice);
816        }
817
818        // Get UIDs of all messages in all threads
819        $a_index = $threads->get();
820
821        // fetch reqested headers from server
822        $a_msg_headers = $this->fetch_headers($folder, $a_index);
823
824        unset($a_index);
825
826        // Set depth, has_children and unread_children fields in headers
827        $this->set_thread_flags($a_msg_headers, $threads);
828
829        return array_values($a_msg_headers);
830    }
831
832
833    /**
834     * protected method for setting threaded messages flags:
835     * depth, has_children and unread_children
836     *
837     * @param  array               $headers  Reference to headers array indexed by message UID
838     * @param  rcube_result_thread $threads  Threads data object
839     *
840     * @return array Message headers array indexed by message UID
841     */
842    protected function set_thread_flags(&$headers, $threads)
843    {
844        $parents = array();
845
846        list ($msg_depth, $msg_children) = $threads->get_thread_data();
847
848        foreach ($headers as $uid => $header) {
849            $depth = $msg_depth[$uid];
850            $parents = array_slice($parents, 0, $depth);
851
852            if (!empty($parents)) {
853                $headers[$uid]->parent_uid = end($parents);
854                if (empty($header->flags['SEEN']))
855                    $headers[$parents[0]]->unread_children++;
856            }
857            array_push($parents, $uid);
858
859            $headers[$uid]->depth = $depth;
860            $headers[$uid]->has_children = $msg_children[$uid];
861        }
862    }
863
864
865    /**
866     * protected method for listing a set of message headers (search results)
867     *
868     * @param   string   $folder   Folder name
869     * @param   int      $page     Current page to list
870     * @param   int      $slice    Number of slice items to extract from result array
871     *
872     * @return  array    Indexed array with message header objects
873     */
874    protected function list_search_messages($folder, $page, $slice=0)
875    {
876        if (!strlen($folder) || empty($this->search_set) || $this->search_set->is_empty()) {
877            return array();
878        }
879
880        // use saved messages from searching
881        if ($this->threading) {
882            return $this->list_search_thread_messages($folder, $page, $slice);
883        }
884
885        // search set is threaded, we need a new one
886        if ($this->search_threads) {
887            $this->search('', $this->search_string, $this->search_charset, $this->sort_field);
888        }
889
890        $index = clone $this->search_set;
891        $from  = ($page-1) * $this->page_size;
892        $to    = $from + $this->page_size;
893
894        // return empty array if no messages found
895        if ($index->is_empty()) {
896            return array();
897        }
898
899        // quickest method (default sorting)
900        if (!$this->search_sort_field && !$this->sort_field) {
901            $got_index = true;
902        }
903        // sorted messages, so we can first slice array and then fetch only wanted headers
904        else if ($this->search_sorted) { // SORT searching result
905            $got_index = true;
906            // reset search set if sorting field has been changed
907            if ($this->sort_field && $this->search_sort_field != $this->sort_field) {
908                $this->search('', $this->search_string, $this->search_charset, $this->sort_field);
909
910                $index = clone $this->search_set;
911
912                // return empty array if no messages found
913                if ($index->is_empty()) {
914                    return array();
915                }
916            }
917        }
918
919        if ($got_index) {
920            if ($this->sort_order != $index->get_parameters('ORDER')) {
921                $index->revert();
922            }
923
924            // get messages uids for one page
925            $index->slice($from, $to-$from);
926
927            if ($slice) {
928                $index->slice(-$slice, $slice);
929            }
930
931            // fetch headers
932            $a_index       = $index->get();
933            $a_msg_headers = $this->fetch_headers($folder, $a_index);
934
935            return array_values($a_msg_headers);
936        }
937
938        // SEARCH result, need sorting
939        $cnt = $index->count();
940
941        // 300: experimantal value for best result
942        if (($cnt > 300 && $cnt > $this->page_size) || !$this->sort_field) {
943            // use memory less expensive (and quick) method for big result set
944            $index = clone $this->index('', $this->sort_field, $this->sort_order);
945            // get messages uids for one page...
946            $index->slice($start_msg, min($cnt-$from, $this->page_size));
947
948            if ($slice) {
949                $index->slice(-$slice, $slice);
950            }
951
952            // ...and fetch headers
953            $a_index       = $index->get();
954            $a_msg_headers = $this->fetch_headers($folder, $a_index);
955
956            return array_values($a_msg_headers);
957        }
958        else {
959            // for small result set we can fetch all messages headers
960            $a_index       = $index->get();
961            $a_msg_headers = $this->fetch_headers($folder, $a_index, false);
962
963            // return empty array if no messages found
964            if (!is_array($a_msg_headers) || empty($a_msg_headers)) {
965                return array();
966            }
967
968            if (!$this->check_connection()) {
969                return array();
970            }
971
972            // if not already sorted
973            $a_msg_headers = $this->conn->sortHeaders(
974                $a_msg_headers, $this->sort_field, $this->sort_order);
975
976            // only return the requested part of the set
977            $a_msg_headers = array_slice(array_values($a_msg_headers),
978                $from, min($cnt-$to, $this->page_size));
979
980            if ($slice) {
981                $a_msg_headers = array_slice($a_msg_headers, -$slice, $slice);
982            }
983
984            return $a_msg_headers;
985        }
986    }
987
988
989    /**
990     * protected method for listing a set of threaded message headers (search results)
991     *
992     * @param   string   $folder     Folder name
993     * @param   int      $page       Current page to list
994     * @param   int      $slice      Number of slice items to extract from result array
995     *
996     * @return  array    Indexed array with message header objects
997     * @see rcube_imap::list_search_messages()
998     */
999    protected function list_search_thread_messages($folder, $page, $slice=0)
1000    {
1001        // update search_set if previous data was fetched with disabled threading
1002        if (!$this->search_threads) {
1003            if ($this->search_set->is_empty()) {
1004                return array();
1005            }
1006            $this->search('', $this->search_string, $this->search_charset, $this->sort_field);
1007        }
1008
1009        return $this->fetch_thread_headers($folder, clone $this->search_set, $page, $slice);
1010    }
1011
1012
1013    /**
1014     * Fetches messages headers (by UID)
1015     *
1016     * @param  string  $folder   Folder name
1017     * @param  array   $msgs     Message UIDs
1018     * @param  bool    $sort     Enables result sorting by $msgs
1019     * @param  bool    $force    Disables cache use
1020     *
1021     * @return array Messages headers indexed by UID
1022     */
1023    function fetch_headers($folder, $msgs, $sort = true, $force = false)
1024    {
1025        if (empty($msgs)) {
1026            return array();
1027        }
1028
1029        if (!$force && ($mcache = $this->get_mcache_engine())) {
1030            $headers = $mcache->get_messages($folder, $msgs);
1031        }
1032        else if (!$this->check_connection()) {
1033            return array();
1034        }
1035        else {
1036            // fetch reqested headers from server
1037            $headers = $this->conn->fetchHeaders(
1038                $folder, $msgs, true, false, $this->get_fetch_headers());
1039        }
1040
1041        if (empty($headers)) {
1042            return array();
1043        }
1044
1045        foreach ($headers as $h) {
1046            $a_msg_headers[$h->uid] = $h;
1047        }
1048
1049        if ($sort) {
1050            // use this class for message sorting
1051            $sorter = new rcube_message_header_sorter();
1052            $sorter->set_index($msgs);
1053            $sorter->sort_headers($a_msg_headers);
1054        }
1055
1056        return $a_msg_headers;
1057    }
1058
1059
1060    /**
1061     * Returns current status of folder
1062     *
1063     * We compare the maximum UID to determine the number of
1064     * new messages because the RECENT flag is not reliable.
1065     *
1066     * @param string $folder Folder name
1067     *
1068     * @return int   Folder status
1069     */
1070    public function folder_status($folder = null)
1071    {
1072        if (!strlen($folder)) {
1073            $folder = $this->folder;
1074        }
1075        $old = $this->get_folder_stats($folder);
1076
1077        // refresh message count -> will update
1078        $this->countmessages($folder, 'ALL', true);
1079
1080        $result = 0;
1081
1082        if (empty($old)) {
1083            return $result;
1084        }
1085
1086        $new = $this->get_folder_stats($folder);
1087
1088        // got new messages
1089        if ($new['maxuid'] > $old['maxuid']) {
1090            $result += 1;
1091        }
1092        // some messages has been deleted
1093        if ($new['cnt'] < $old['cnt']) {
1094            $result += 2;
1095        }
1096
1097        // @TODO: optional checking for messages flags changes (?)
1098        // @TODO: UIDVALIDITY checking
1099
1100        return $result;
1101    }
1102
1103
1104    /**
1105     * Stores folder statistic data in session
1106     * @TODO: move to separate DB table (cache?)
1107     *
1108     * @param string $folder  Folder name
1109     * @param string $name    Data name
1110     * @param mixed  $data    Data value
1111     */
1112    protected function set_folder_stats($folder, $name, $data)
1113    {
1114        $_SESSION['folders'][$folder][$name] = $data;
1115    }
1116
1117
1118    /**
1119     * Gets folder statistic data
1120     *
1121     * @param string $folder Folder name
1122     *
1123     * @return array Stats data
1124     */
1125    protected function get_folder_stats($folder)
1126    {
1127        if ($_SESSION['folders'][$folder]) {
1128            return (array) $_SESSION['folders'][$folder];
1129        }
1130
1131        return array();
1132    }
1133
1134
1135    /**
1136     * Return sorted list of message UIDs
1137     *
1138     * @param string $folder     Folder to get index from
1139     * @param string $sort_field Sort column
1140     * @param string $sort_order Sort order [ASC, DESC]
1141     *
1142     * @return rcube_result_index|rcube_result_thread List of messages (UIDs)
1143     */
1144    public function index($folder = '', $sort_field = NULL, $sort_order = NULL)
1145    {
1146        if ($this->threading) {
1147            return $this->thread_index($folder, $sort_field, $sort_order);
1148        }
1149
1150        $this->set_sort_order($sort_field, $sort_order);
1151
1152        if (!strlen($folder)) {
1153            $folder = $this->folder;
1154        }
1155
1156        // we have a saved search result, get index from there
1157        if ($this->search_string) {
1158            if ($this->search_threads) {
1159                $this->search($folder, $this->search_string, $this->search_charset, $this->sort_field);
1160            }
1161
1162            // use message index sort as default sorting
1163            if (!$this->sort_field || $this->search_sorted) {
1164                if ($this->sort_field && $this->search_sort_field != $this->sort_field) {
1165                    $this->search($folder, $this->search_string, $this->search_charset, $this->sort_field);
1166                }
1167                $index = $this->search_set;
1168            }
1169            else if (!$this->check_connection()) {
1170                return new rcube_result_index();
1171            }
1172            else {
1173                $index = $this->conn->index($folder, $this->search_set->get(),
1174                    $this->sort_field, $this->options['skip_deleted'], true, true);
1175            }
1176
1177            if ($this->sort_order != $index->get_parameters('ORDER')) {
1178                $index->revert();
1179            }
1180
1181            return $index;
1182        }
1183
1184        // check local cache
1185        if ($mcache = $this->get_mcache_engine()) {
1186            $index = $mcache->get_index($folder, $this->sort_field, $this->sort_order);
1187        }
1188        // fetch from IMAP server
1189        else {
1190            $index = $this->index_direct(
1191                $folder, $this->sort_field, $this->sort_order);
1192        }
1193
1194        return $index;
1195    }
1196
1197
1198    /**
1199     * Return sorted list of message UIDs ignoring current search settings.
1200     * Doesn't uses cache by default.
1201     *
1202     * @param string $folder     Folder to get index from
1203     * @param string $sort_field Sort column
1204     * @param string $sort_order Sort order [ASC, DESC]
1205     * @param bool   $skip_cache Disables cache usage
1206     *
1207     * @return rcube_result_index Sorted list of message UIDs
1208     */
1209    public function index_direct($folder, $sort_field = null, $sort_order = null, $skip_cache = true)
1210    {
1211        if (!$skip_cache && ($mcache = $this->get_mcache_engine())) {
1212            $index = $mcache->get_index($folder, $sort_field, $sort_order);
1213        }
1214        // use message index sort as default sorting
1215        else if (!$sort_field) {
1216            // use search result from count() if possible
1217            if ($this->options['skip_deleted'] && !empty($this->icache['undeleted_idx'])
1218                && $this->icache['undeleted_idx']->get_parameters('ALL') !== null
1219                && $this->icache['undeleted_idx']->get_parameters('MAILBOX') == $folder
1220            ) {
1221                $index = $this->icache['undeleted_idx'];
1222            }
1223            else if (!$this->check_connection()) {
1224                return new rcube_result_index();
1225            }
1226            else {
1227                $index = $this->conn->search($folder,
1228                    'ALL' .($this->options['skip_deleted'] ? ' UNDELETED' : ''), true);
1229            }
1230        }
1231        else if (!$this->check_connection()) {
1232            return new rcube_result_index();
1233        }
1234        // fetch complete message index
1235        else {
1236            if ($this->get_capability('SORT')) {
1237                $index = $this->conn->sort($folder, $sort_field,
1238                    $this->options['skip_deleted'] ? 'UNDELETED' : '', true);
1239            }
1240
1241            if (empty($index) || $index->is_error()) {
1242                $index = $this->conn->index($folder, "1:*", $sort_field,
1243                    $this->options['skip_deleted'], false, true);
1244            }
1245        }
1246
1247        if ($sort_order != $index->get_parameters('ORDER')) {
1248            $index->revert();
1249        }
1250
1251        return $index;
1252    }
1253
1254
1255    /**
1256     * Return index of threaded message UIDs
1257     *
1258     * @param string $folder     Folder to get index from
1259     * @param string $sort_field Sort column
1260     * @param string $sort_order Sort order [ASC, DESC]
1261     *
1262     * @return rcube_result_thread Message UIDs
1263     */
1264    public function thread_index($folder='', $sort_field=NULL, $sort_order=NULL)
1265    {
1266        if (!strlen($folder)) {
1267            $folder = $this->folder;
1268        }
1269
1270        // we have a saved search result, get index from there
1271        if ($this->search_string && $this->search_threads && $folder == $this->folder) {
1272            $threads = $this->search_set;
1273        }
1274        else {
1275            // get all threads (default sort order)
1276            $threads = $this->fetch_threads($folder);
1277        }
1278
1279        $this->set_sort_order($sort_field, $sort_order);
1280        $this->sort_threads($threads);
1281
1282        return $threads;
1283    }
1284
1285
1286    /**
1287     * Sort threaded result, using THREAD=REFS method
1288     *
1289     * @param rcube_result_thread $threads  Threads result set
1290     */
1291    protected function sort_threads($threads)
1292    {
1293        if ($threads->is_empty()) {
1294            return;
1295        }
1296
1297        // THREAD=ORDEREDSUBJECT: sorting by sent date of root message
1298        // THREAD=REFERENCES:     sorting by sent date of root message
1299        // THREAD=REFS:           sorting by the most recent date in each thread
1300
1301        if ($this->sort_field && ($this->sort_field != 'date' || $this->get_capability('THREAD') != 'REFS')) {
1302            $index = $this->index_direct($this->folder, $this->sort_field, $this->sort_order, false);
1303
1304            if (!$index->is_empty()) {
1305                $threads->sort($index);
1306            }
1307        }
1308        else {
1309            if ($this->sort_order != $threads->get_parameters('ORDER')) {
1310                $threads->revert();
1311            }
1312        }
1313    }
1314
1315
1316    /**
1317     * Invoke search request to IMAP server
1318     *
1319     * @param  string  $folder     Folder name to search in
1320     * @param  string  $str        Search criteria
1321     * @param  string  $charset    Search charset
1322     * @param  string  $sort_field Header field to sort by
1323     *
1324     * @todo: Search criteria should be provided in non-IMAP format, eg. array
1325     */
1326    public function search($folder='', $str='ALL', $charset=NULL, $sort_field=NULL)
1327    {
1328        if (!$str) {
1329            $str = 'ALL';
1330        }
1331
1332        if (!strlen($folder)) {
1333            $folder = $this->folder;
1334        }
1335
1336        $results = $this->search_index($folder, $str, $charset, $sort_field);
1337
1338        $this->set_search_set(array($str, $results, $charset, $sort_field,
1339            $this->threading || $this->search_sorted ? true : false));
1340    }
1341
1342
1343    /**
1344     * Direct (real and simple) SEARCH request (without result sorting and caching).
1345     *
1346     * @param  string  $mailbox Mailbox name to search in
1347     * @param  string  $str     Search string
1348     *
1349     * @return rcube_result_index  Search result (UIDs)
1350     */
1351    public function search_once($folder = null, $str = 'ALL')
1352    {
1353        if (!$str) {
1354            return 'ALL';
1355        }
1356
1357        if (!strlen($folder)) {
1358            $folder = $this->folder;
1359        }
1360
1361        if (!$this->check_connection()) {
1362            return new rcube_result_index();
1363        }
1364
1365        $index = $this->conn->search($folder, $str, true);
1366
1367        return $index;
1368    }
1369
1370
1371    /**
1372     * protected search method
1373     *
1374     * @param string $folder     Folder name
1375     * @param string $criteria   Search criteria
1376     * @param string $charset    Charset
1377     * @param string $sort_field Sorting field
1378     *
1379     * @return rcube_result_index|rcube_result_thread  Search results (UIDs)
1380     * @see rcube_imap::search()
1381     */
1382    protected function search_index($folder, $criteria='ALL', $charset=NULL, $sort_field=NULL)
1383    {
1384        $orig_criteria = $criteria;
1385
1386        if (!$this->check_connection()) {
1387            if ($this->threading) {
1388                return new rcube_result_thread();
1389            }
1390            else {
1391                return new rcube_result_index();
1392            }
1393        }
1394
1395        if ($this->options['skip_deleted'] && !preg_match('/UNDELETED/', $criteria)) {
1396            $criteria = 'UNDELETED '.$criteria;
1397        }
1398
1399        if ($this->threading) {
1400            $threads = $this->conn->thread($folder, $this->threading, $criteria, true, $charset);
1401
1402            // Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
1403            // but I've seen that Courier doesn't support UTF-8)
1404            if ($threads->is_error() && $charset && $charset != 'US-ASCII') {
1405                $threads = $this->conn->thread($folder, $this->threading,
1406                    $this->convert_criteria($criteria, $charset), true, 'US-ASCII');
1407            }
1408
1409            return $threads;
1410        }
1411
1412        if ($sort_field && $this->get_capability('SORT')) {
1413            $charset  = $charset ? $charset : $this->default_charset;
1414            $messages = $this->conn->sort($folder, $sort_field, $criteria, true, $charset);
1415
1416            // Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
1417            // but I've seen Courier with disabled UTF-8 support)
1418            if ($messages->is_error() && $charset && $charset != 'US-ASCII') {
1419                $messages = $this->conn->sort($folder, $sort_field,
1420                    $this->convert_criteria($criteria, $charset), true, 'US-ASCII');
1421            }
1422
1423            if (!$messages->is_error()) {
1424                $this->search_sorted = true;
1425                return $messages;
1426            }
1427        }
1428
1429        $messages = $this->conn->search($folder,
1430            ($charset ? "CHARSET $charset " : '') . $criteria, true);
1431
1432        // Error, try with US-ASCII (some servers may support only US-ASCII)
1433        if ($messages->is_error() && $charset && $charset != 'US-ASCII') {
1434            $messages = $this->conn->search($folder,
1435                $this->convert_criteria($criteria, $charset), true);
1436        }
1437
1438        $this->search_sorted = false;
1439
1440        return $messages;
1441    }
1442
1443
1444    /**
1445     * Converts charset of search criteria string
1446     *
1447     * @param  string  $str          Search string
1448     * @param  string  $charset      Original charset
1449     * @param  string  $dest_charset Destination charset (default US-ASCII)
1450     *
1451     * @return string  Search string
1452     */
1453    protected function convert_criteria($str, $charset, $dest_charset='US-ASCII')
1454    {
1455        // convert strings to US_ASCII
1456        if (preg_match_all('/\{([0-9]+)\}\r\n/', $str, $matches, PREG_OFFSET_CAPTURE)) {
1457            $last = 0; $res = '';
1458            foreach ($matches[1] as $m) {
1459                $string_offset = $m[1] + strlen($m[0]) + 4; // {}\r\n
1460                $string = substr($str, $string_offset - 1, $m[0]);
1461                $string = rcube_charset::convert($string, $charset, $dest_charset);
1462                if ($string === false) {
1463                    continue;
1464                }
1465                $res .= substr($str, $last, $m[1] - $last - 1) . rcube_imap_generic::escape($string);
1466                $last = $m[0] + $string_offset - 1;
1467            }
1468            if ($last < strlen($str)) {
1469                $res .= substr($str, $last, strlen($str)-$last);
1470            }
1471        }
1472        // strings for conversion not found
1473        else {
1474            $res = $str;
1475        }
1476
1477        return $res;
1478    }
1479
1480
1481    /**
1482     * Refresh saved search set
1483     *
1484     * @return array Current search set
1485     */
1486    public function refresh_search()
1487    {
1488        if (!empty($this->search_string)) {
1489            $this->search('', $this->search_string, $this->search_charset, $this->search_sort_field);
1490        }
1491
1492        return $this->get_search_set();
1493    }
1494
1495
1496    /**
1497     * Return message headers object of a specific message
1498     *
1499     * @param int     $id       Message UID
1500     * @param string  $folder   Folder to read from
1501     * @param bool    $force    True to skip cache
1502     *
1503     * @return rcube_message_header Message headers
1504     */
1505    public function get_message_headers($uid, $folder = null, $force = false)
1506    {
1507        if (!strlen($folder)) {
1508            $folder = $this->folder;
1509        }
1510
1511        // get cached headers
1512        if (!$force && $uid && ($mcache = $this->get_mcache_engine())) {
1513            $headers = $mcache->get_message($folder, $uid);
1514        }
1515        else if (!$this->check_connection()) {
1516            $headers = false;
1517        }
1518        else {
1519            $headers = $this->conn->fetchHeader(
1520                $folder, $uid, true, true, $this->get_fetch_headers());
1521        }
1522
1523        return $headers;
1524    }
1525
1526
1527    /**
1528     * Fetch message headers and body structure from the IMAP server and build
1529     * an object structure similar to the one generated by PEAR::Mail_mimeDecode
1530     *
1531     * @param int     $uid      Message UID to fetch
1532     * @param string  $folder   Folder to read from
1533     *
1534     * @return object rcube_message_header Message data
1535     */
1536    public function get_message($uid, $folder = null)
1537    {
1538        if (!strlen($folder)) {
1539            $folder = $this->folder;
1540        }
1541
1542        // Check internal cache
1543        if (!empty($this->icache['message'])) {
1544            if (($headers = $this->icache['message']) && $headers->uid == $uid) {
1545                return $headers;
1546            }
1547        }
1548
1549        $headers = $this->get_message_headers($uid, $folder);
1550
1551        // message doesn't exist?
1552        if (empty($headers)) {
1553            return null;
1554        }
1555
1556        // structure might be cached
1557        if (!empty($headers->structure)) {
1558            return $headers;
1559        }
1560
1561        $this->msg_uid = $uid;
1562
1563        if (!$this->check_connection()) {
1564            return $headers;
1565        }
1566
1567        if (empty($headers->bodystructure)) {
1568            $headers->bodystructure = $this->conn->getStructure($folder, $uid, true);
1569        }
1570
1571        $structure = $headers->bodystructure;
1572
1573        if (empty($structure)) {
1574            return $headers;
1575        }
1576
1577        // set message charset from message headers
1578        if ($headers->charset) {
1579            $this->struct_charset = $headers->charset;
1580        }
1581        else {
1582            $this->struct_charset = $this->structure_charset($structure);
1583        }
1584
1585        $headers->ctype = strtolower($headers->ctype);
1586
1587        // Here we can recognize malformed BODYSTRUCTURE and
1588        // 1. [@TODO] parse the message in other way to create our own message structure
1589        // 2. or just show the raw message body.
1590        // Example of structure for malformed MIME message:
1591        // ("text" "plain" NIL NIL NIL "7bit" 2154 70 NIL NIL NIL)
1592        if ($headers->ctype && !is_array($structure[0]) && $headers->ctype != 'text/plain'
1593            && strtolower($structure[0].'/'.$structure[1]) == 'text/plain') {
1594            // we can handle single-part messages, by simple fix in structure (#1486898)
1595            if (preg_match('/^(text|application)\/(.*)/', $headers->ctype, $m)) {
1596                $structure[0] = $m[1];
1597                $structure[1] = $m[2];
1598            }
1599            else {
1600                return $headers;
1601            }
1602        }
1603
1604        $struct = $this->structure_part($structure, 0, '', $headers);
1605
1606        // don't trust given content-type
1607        if (empty($struct->parts) && !empty($headers->ctype)) {
1608            $struct->mime_id = '1';
1609            $struct->mimetype = strtolower($headers->ctype);
1610            list($struct->ctype_primary, $struct->ctype_secondary) = explode('/', $struct->mimetype);
1611        }
1612
1613        $headers->structure = $struct;
1614
1615        return $this->icache['message'] = $headers;
1616    }
1617
1618
1619    /**
1620     * Build message part object
1621     *
1622     * @param array  $part
1623     * @param int    $count
1624     * @param string $parent
1625     */
1626    protected function structure_part($part, $count=0, $parent='', $mime_headers=null)
1627    {
1628        $struct = new rcube_message_part;
1629        $struct->mime_id = empty($parent) ? (string)$count : "$parent.$count";
1630
1631        // multipart
1632        if (is_array($part[0])) {
1633            $struct->ctype_primary = 'multipart';
1634
1635        /* RFC3501: BODYSTRUCTURE fields of multipart part
1636            part1 array
1637            part2 array
1638            part3 array
1639            ....
1640            1. subtype
1641            2. parameters (optional)
1642            3. description (optional)
1643            4. language (optional)
1644            5. location (optional)
1645        */
1646
1647            // find first non-array entry
1648            for ($i=1; $i<count($part); $i++) {
1649                if (!is_array($part[$i])) {
1650                    $struct->ctype_secondary = strtolower($part[$i]);
1651                    break;
1652                }
1653            }
1654
1655            $struct->mimetype = 'multipart/'.$struct->ctype_secondary;
1656
1657            // build parts list for headers pre-fetching
1658            for ($i=0; $i<count($part); $i++) {
1659                if (!is_array($part[$i])) {
1660                    break;
1661                }
1662                // fetch message headers if message/rfc822
1663                // or named part (could contain Content-Location header)
1664                if (!is_array($part[$i][0])) {
1665                    $tmp_part_id = $struct->mime_id ? $struct->mime_id.'.'.($i+1) : $i+1;
1666                    if (strtolower($part[$i][0]) == 'message' && strtolower($part[$i][1]) == 'rfc822') {
1667                        $mime_part_headers[] = $tmp_part_id;
1668                    }
1669                    else if (in_array('name', (array)$part[$i][2]) && empty($part[$i][3])) {
1670                        $mime_part_headers[] = $tmp_part_id;
1671                    }
1672                }
1673            }
1674
1675            // pre-fetch headers of all parts (in one command for better performance)
1676            // @TODO: we could do this before _structure_part() call, to fetch
1677            // headers for parts on all levels
1678            if ($mime_part_headers) {
1679                $mime_part_headers = $this->conn->fetchMIMEHeaders($this->folder,
1680                    $this->msg_uid, $mime_part_headers);
1681            }
1682
1683            $struct->parts = array();
1684            for ($i=0, $count=0; $i<count($part); $i++) {
1685                if (!is_array($part[$i])) {
1686                    break;
1687                }
1688                $tmp_part_id = $struct->mime_id ? $struct->mime_id.'.'.($i+1) : $i+1;
1689                $struct->parts[] = $this->structure_part($part[$i], ++$count, $struct->mime_id,
1690                    $mime_part_headers[$tmp_part_id]);
1691            }
1692
1693            return $struct;
1694        }
1695
1696        /* RFC3501: BODYSTRUCTURE fields of non-multipart part
1697            0. type
1698            1. subtype
1699            2. parameters
1700            3. id
1701            4. description
1702            5. encoding
1703            6. size
1704          -- text
1705            7. lines
1706          -- message/rfc822
1707            7. envelope structure
1708            8. body structure
1709            9. lines
1710          --
1711            x. md5 (optional)
1712            x. disposition (optional)
1713            x. language (optional)
1714            x. location (optional)
1715        */
1716
1717        // regular part
1718        $struct->ctype_primary = strtolower($part[0]);
1719        $struct->ctype_secondary = strtolower($part[1]);
1720        $struct->mimetype = $struct->ctype_primary.'/'.$struct->ctype_secondary;
1721
1722        // read content type parameters
1723        if (is_array($part[2])) {
1724            $struct->ctype_parameters = array();
1725            for ($i=0; $i<count($part[2]); $i+=2) {
1726                $struct->ctype_parameters[strtolower($part[2][$i])] = $part[2][$i+1];
1727            }
1728
1729            if (isset($struct->ctype_parameters['charset'])) {
1730                $struct->charset = $struct->ctype_parameters['charset'];
1731            }
1732        }
1733
1734        // #1487700: workaround for lack of charset in malformed structure
1735        if (empty($struct->charset) && !empty($mime_headers) && $mime_headers->charset) {
1736            $struct->charset = $mime_headers->charset;
1737        }
1738
1739        // read content encoding
1740        if (!empty($part[5])) {
1741            $struct->encoding = strtolower($part[5]);
1742            $struct->headers['content-transfer-encoding'] = $struct->encoding;
1743        }
1744
1745        // get part size
1746        if (!empty($part[6])) {
1747            $struct->size = intval($part[6]);
1748        }
1749
1750        // read part disposition
1751        $di = 8;
1752        if ($struct->ctype_primary == 'text') {
1753            $di += 1;
1754        }
1755        else if ($struct->mimetype == 'message/rfc822') {
1756            $di += 3;
1757        }
1758
1759        if (is_array($part[$di]) && count($part[$di]) == 2) {
1760            $struct->disposition = strtolower($part[$di][0]);
1761
1762            if (is_array($part[$di][1])) {
1763                for ($n=0; $n<count($part[$di][1]); $n+=2) {
1764                    $struct->d_parameters[strtolower($part[$di][1][$n])] = $part[$di][1][$n+1];
1765                }
1766            }
1767        }
1768
1769        // get message/rfc822's child-parts
1770        if (is_array($part[8]) && $di != 8) {
1771            $struct->parts = array();
1772            for ($i=0, $count=0; $i<count($part[8]); $i++) {
1773                if (!is_array($part[8][$i])) {
1774                    break;
1775                }
1776                $struct->parts[] = $this->structure_part($part[8][$i], ++$count, $struct->mime_id);
1777            }
1778        }
1779
1780        // get part ID
1781        if (!empty($part[3])) {
1782            $struct->content_id = $part[3];
1783            $struct->headers['content-id'] = $part[3];
1784
1785            if (empty($struct->disposition)) {
1786                $struct->disposition = 'inline';
1787            }
1788        }
1789
1790        // fetch message headers if message/rfc822 or named part (could contain Content-Location header)
1791        if ($struct->ctype_primary == 'message' || ($struct->ctype_parameters['name'] && !$struct->content_id)) {
1792            if (empty($mime_headers)) {
1793                $mime_headers = $this->conn->fetchPartHeader(
1794                    $this->folder, $this->msg_uid, true, $struct->mime_id);
1795            }
1796
1797            if (is_string($mime_headers)) {
1798                $struct->headers = rcube_mime::parse_headers($mime_headers) + $struct->headers;
1799            }
1800            else if (is_object($mime_headers)) {
1801                $struct->headers = get_object_vars($mime_headers) + $struct->headers;
1802            }
1803
1804            // get real content-type of message/rfc822
1805            if ($struct->mimetype == 'message/rfc822') {
1806                // single-part
1807                if (!is_array($part[8][0])) {
1808                    $struct->real_mimetype = strtolower($part[8][0] . '/' . $part[8][1]);
1809                }
1810                // multi-part
1811                else {
1812                    for ($n=0; $n<count($part[8]); $n++) {
1813                        if (!is_array($part[8][$n])) {
1814                            break;
1815                        }
1816                    }
1817                    $struct->real_mimetype = 'multipart/' . strtolower($part[8][$n]);
1818                }
1819            }
1820
1821            if ($struct->ctype_primary == 'message' && empty($struct->parts)) {
1822                if (is_array($part[8]) && $di != 8) {
1823                    $struct->parts[] = $this->structure_part($part[8], ++$count, $struct->mime_id);
1824                }
1825            }
1826        }
1827
1828        // normalize filename property
1829        $this->set_part_filename($struct, $mime_headers);
1830
1831        return $struct;
1832    }
1833
1834
1835    /**
1836     * Set attachment filename from message part structure
1837     *
1838     * @param  rcube_message_part $part    Part object
1839     * @param  string             $headers Part's raw headers
1840     */
1841    protected function set_part_filename(&$part, $headers=null)
1842    {
1843        if (!empty($part->d_parameters['filename'])) {
1844            $filename_mime = $part->d_parameters['filename'];
1845        }
1846        else if (!empty($part->d_parameters['filename*'])) {
1847            $filename_encoded = $part->d_parameters['filename*'];
1848        }
1849        else if (!empty($part->ctype_parameters['name*'])) {
1850            $filename_encoded = $part->ctype_parameters['name*'];
1851        }
1852        // RFC2231 value continuations
1853        // TODO: this should be rewrited to support RFC2231 4.1 combinations
1854        else if (!empty($part->d_parameters['filename*0'])) {
1855            $i = 0;
1856            while (isset($part->d_parameters['filename*'.$i])) {
1857                $filename_mime .= $part->d_parameters['filename*'.$i];
1858                $i++;
1859            }
1860            // some servers (eg. dovecot-1.x) have no support for parameter value continuations
1861            // we must fetch and parse headers "manually"
1862            if ($i<2) {
1863                if (!$headers) {
1864                    $headers = $this->conn->fetchPartHeader(
1865                        $this->folder, $this->msg_uid, true, $part->mime_id);
1866                }
1867                $filename_mime = '';
1868                $i = 0;
1869                while (preg_match('/filename\*'.$i.'\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
1870                    $filename_mime .= $matches[1];
1871                    $i++;
1872                }
1873            }
1874        }
1875        else if (!empty($part->d_parameters['filename*0*'])) {
1876            $i = 0;
1877            while (isset($part->d_parameters['filename*'.$i.'*'])) {
1878                $filename_encoded .= $part->d_parameters['filename*'.$i.'*'];
1879                $i++;
1880            }
1881            if ($i<2) {
1882                if (!$headers) {
1883                    $headers = $this->conn->fetchPartHeader(
1884                            $this->folder, $this->msg_uid, true, $part->mime_id);
1885                }
1886                $filename_encoded = '';
1887                $i = 0; $matches = array();
1888                while (preg_match('/filename\*'.$i.'\*\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
1889                    $filename_encoded .= $matches[1];
1890                    $i++;
1891                }
1892            }
1893        }
1894        else if (!empty($part->ctype_parameters['name*0'])) {
1895            $i = 0;
1896            while (isset($part->ctype_parameters['name*'.$i])) {
1897                $filename_mime .= $part->ctype_parameters['name*'.$i];
1898                $i++;
1899            }
1900            if ($i<2) {
1901                if (!$headers) {
1902                    $headers = $this->conn->fetchPartHeader(
1903                        $this->folder, $this->msg_uid, true, $part->mime_id);
1904                }
1905                $filename_mime = '';
1906                $i = 0; $matches = array();
1907                while (preg_match('/\s+name\*'.$i.'\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
1908                    $filename_mime .= $matches[1];
1909                    $i++;
1910                }
1911            }
1912        }
1913        else if (!empty($part->ctype_parameters['name*0*'])) {
1914            $i = 0;
1915            while (isset($part->ctype_parameters['name*'.$i.'*'])) {
1916                $filename_encoded .= $part->ctype_parameters['name*'.$i.'*'];
1917                $i++;
1918            }
1919            if ($i<2) {
1920                if (!$headers) {
1921                    $headers = $this->conn->fetchPartHeader(
1922                        $this->folder, $this->msg_uid, true, $part->mime_id);
1923                }
1924                $filename_encoded = '';
1925                $i = 0; $matches = array();
1926                while (preg_match('/\s+name\*'.$i.'\*\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
1927                    $filename_encoded .= $matches[1];
1928                    $i++;
1929                }
1930            }
1931        }
1932        // read 'name' after rfc2231 parameters as it may contains truncated filename (from Thunderbird)
1933        else if (!empty($part->ctype_parameters['name'])) {
1934            $filename_mime = $part->ctype_parameters['name'];
1935        }
1936        // Content-Disposition
1937        else if (!empty($part->headers['content-description'])) {
1938            $filename_mime = $part->headers['content-description'];
1939        }
1940        else {
1941            return;
1942        }
1943
1944        // decode filename
1945        if (!empty($filename_mime)) {
1946            if (!empty($part->charset)) {
1947                $charset = $part->charset;
1948            }
1949            else if (!empty($this->struct_charset)) {
1950                $charset = $this->struct_charset;
1951            }
1952            else {
1953                $charset = rcube_charset::detect($filename_mime, $this->default_charset);
1954            }
1955
1956            $part->filename = rcube_mime::decode_mime_string($filename_mime, $charset);
1957        }
1958        else if (!empty($filename_encoded)) {
1959            // decode filename according to RFC 2231, Section 4
1960            if (preg_match("/^([^']*)'[^']*'(.*)$/", $filename_encoded, $fmatches)) {
1961                $filename_charset = $fmatches[1];
1962                $filename_encoded = $fmatches[2];
1963            }
1964
1965            $part->filename = rcube_charset::convert(urldecode($filename_encoded), $filename_charset);
1966        }
1967    }
1968
1969
1970    /**
1971     * Get charset name from message structure (first part)
1972     *
1973     * @param  array $structure Message structure
1974     *
1975     * @return string Charset name
1976     */
1977    protected function structure_charset($structure)
1978    {
1979        while (is_array($structure)) {
1980            if (is_array($structure[2]) && $structure[2][0] == 'charset') {
1981                return $structure[2][1];
1982            }
1983            $structure = $structure[0];
1984        }
1985    }
1986
1987
1988    /**
1989     * Fetch message body of a specific message from the server
1990     *
1991     * @param  int                $uid    Message UID
1992     * @param  string             $part   Part number
1993     * @param  rcube_message_part $o_part Part object created by get_structure()
1994     * @param  mixed              $print  True to print part, ressource to write part contents in
1995     * @param  resource           $fp     File pointer to save the message part
1996     * @param  boolean            $skip_charset_conv Disables charset conversion
1997     *
1998     * @return string Message/part body if not printed
1999     */
2000    public function get_message_part($uid, $part=1, $o_part=NULL, $print=NULL, $fp=NULL, $skip_charset_conv=false)
2001    {
2002        if (!$this->check_connection()) {
2003            return null;
2004        }
2005
2006        // get part data if not provided
2007        if (!is_object($o_part)) {
2008            $structure = $this->conn->getStructure($this->folder, $uid, true);
2009            $part_data = rcube_imap_generic::getStructurePartData($structure, $part);
2010
2011            $o_part = new rcube_message_part;
2012            $o_part->ctype_primary = $part_data['type'];
2013            $o_part->encoding      = $part_data['encoding'];
2014            $o_part->charset       = $part_data['charset'];
2015            $o_part->size          = $part_data['size'];
2016        }
2017
2018        if ($o_part && $o_part->size) {
2019            $body = $this->conn->handlePartBody($this->folder, $uid, true,
2020                $part ? $part : 'TEXT', $o_part->encoding, $print, $fp);
2021        }
2022
2023        if ($fp || $print) {
2024            return true;
2025        }
2026
2027        // convert charset (if text or message part)
2028        if ($body && preg_match('/^(text|message)$/', $o_part->ctype_primary)) {
2029            // Remove NULL characters if any (#1486189)
2030            if (strpos($body, "\x00") !== false) {
2031                $body = str_replace("\x00", '', $body);
2032            }
2033
2034            if (!$skip_charset_conv) {
2035                if (!$o_part->charset || strtoupper($o_part->charset) == 'US-ASCII') {
2036                    // try to extract charset information from HTML meta tag (#1488125)
2037                    if ($o_part->ctype_secondary == 'html' && preg_match('/<meta[^>]+charset=([a-z0-9-_]+)/i', $body, $m)) {
2038                        $o_part->charset = strtoupper($m[1]);
2039                    }
2040                    else {
2041                        $o_part->charset = $this->default_charset;
2042                    }
2043                }
2044                $body = rcube_charset::convert($body, $o_part->charset);
2045            }
2046        }
2047
2048        return $body;
2049    }
2050
2051
2052    /**
2053     * Returns the whole message source as string (or saves to a file)
2054     *
2055     * @param int      $uid Message UID
2056     * @param resource $fp  File pointer to save the message
2057     *
2058     * @return string Message source string
2059     */
2060    public function get_raw_body($uid, $fp=null)
2061    {
2062        if (!$this->check_connection()) {
2063            return null;
2064        }
2065
2066        return $this->conn->handlePartBody($this->folder, $uid,
2067            true, null, null, false, $fp);
2068    }
2069
2070
2071    /**
2072     * Returns the message headers as string
2073     *
2074     * @param int $uid  Message UID
2075     *
2076     * @return string Message headers string
2077     */
2078    public function get_raw_headers($uid)
2079    {
2080        if (!$this->check_connection()) {
2081            return null;
2082        }
2083
2084        return $this->conn->fetchPartHeader($this->folder, $uid, true);
2085    }
2086
2087
2088    /**
2089     * Sends the whole message source to stdout
2090     */
2091    public function print_raw_body($uid)
2092    {
2093        if (!$this->check_connection()) {
2094            return;
2095        }
2096
2097        $this->conn->handlePartBody($this->folder, $uid, true, NULL, NULL, true);
2098    }
2099
2100
2101    /**
2102     * Set message flag to one or several messages
2103     *
2104     * @param mixed   $uids       Message UIDs as array or comma-separated string, or '*'
2105     * @param string  $flag       Flag to set: SEEN, UNDELETED, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
2106     * @param string  $folder    Folder name
2107     * @param boolean $skip_cache True to skip message cache clean up
2108     *
2109     * @return boolean  Operation status
2110     */
2111    public function set_flag($uids, $flag, $folder=null, $skip_cache=false)
2112    {
2113        if (!strlen($folder)) {
2114            $folder = $this->folder;
2115        }
2116
2117        if (!$this->check_connection()) {
2118            return false;
2119        }
2120
2121        $flag = strtoupper($flag);
2122        list($uids, $all_mode) = $this->parse_uids($uids);
2123
2124        if (strpos($flag, 'UN') === 0) {
2125            $result = $this->conn->unflag($folder, $uids, substr($flag, 2));
2126        }
2127        else {
2128            $result = $this->conn->flag($folder, $uids, $flag);
2129        }
2130
2131        if ($result) {
2132            // reload message headers if cached
2133            // @TODO: update flags instead removing from cache
2134            if (!$skip_cache && ($mcache = $this->get_mcache_engine())) {
2135                $status = strpos($flag, 'UN') !== 0;
2136                $mflag  = preg_replace('/^UN/', '', $flag);
2137                $mcache->change_flag($folder, $all_mode ? null : explode(',', $uids),
2138                    $mflag, $status);
2139            }
2140
2141            // clear cached counters
2142            if ($flag == 'SEEN' || $flag == 'UNSEEN') {
2143                $this->clear_messagecount($folder, 'SEEN');
2144                $this->clear_messagecount($folder, 'UNSEEN');
2145            }
2146            else if ($flag == 'DELETED') {
2147                $this->clear_messagecount($folder, 'DELETED');
2148            }
2149        }
2150
2151        return $result;
2152    }
2153
2154
2155    /**
2156     * Append a mail message (source) to a specific folder
2157     *
2158     * @param string  $folder  Target folder
2159     * @param string  $message The message source string or filename
2160     * @param string  $headers Headers string if $message contains only the body
2161     * @param boolean $is_file True if $message is a filename
2162     *
2163     * @return int|bool Appended message UID or True on success, False on error
2164     */
2165    public function save_message($folder, &$message, $headers='', $is_file=false)
2166    {
2167        if (!strlen($folder)) {
2168            $folder = $this->folder;
2169        }
2170
2171        // make sure folder exists
2172        if ($this->folder_exists($folder)) {
2173            if ($is_file) {
2174                $saved = $this->conn->appendFromFile($folder, $message, $headers);
2175            }
2176            else {
2177                $saved = $this->conn->append($folder, $message);
2178            }
2179        }
2180
2181        if ($saved) {
2182            // increase messagecount of the target folder
2183            $this->set_messagecount($folder, 'ALL', 1);
2184        }
2185
2186        return $saved;
2187    }
2188
2189
2190    /**
2191     * Move a message from one folder to another
2192     *
2193     * @param mixed  $uids      Message UIDs as array or comma-separated string, or '*'
2194     * @param string $to_mbox   Target folder
2195     * @param string $from_mbox Source folder
2196     *
2197     * @return boolean True on success, False on error
2198     */
2199    public function move_message($uids, $to_mbox, $from_mbox='')
2200    {
2201        if (!strlen($from_mbox)) {
2202            $from_mbox = $this->folder;
2203        }
2204
2205        if ($to_mbox === $from_mbox) {
2206            return false;
2207        }
2208
2209        list($uids, $all_mode) = $this->parse_uids($uids);
2210
2211        // exit if no message uids are specified
2212        if (empty($uids)) {
2213            return false;
2214        }
2215
2216        if (!$this->check_connection()) {
2217            return false;
2218        }
2219
2220        // make sure folder exists
2221        if ($to_mbox != 'INBOX' && !$this->folder_exists($to_mbox)) {
2222            if (in_array($to_mbox, $this->default_folders)) {
2223                if (!$this->create_folder($to_mbox, true)) {
2224                    return false;
2225                }
2226            }
2227            else {
2228                return false;
2229            }
2230        }
2231
2232        $config = rcube::get_instance()->config;
2233        $to_trash = $to_mbox == $config->get('trash_mbox');
2234
2235        // flag messages as read before moving them
2236        if ($to_trash && $config->get('read_when_deleted')) {
2237            // don't flush cache (4th argument)
2238            $this->set_flag($uids, 'SEEN', $from_mbox, true);
2239        }
2240
2241        // move messages
2242        $moved = $this->conn->move($uids, $from_mbox, $to_mbox);
2243
2244        // send expunge command in order to have the moved message
2245        // really deleted from the source folder
2246        if ($moved) {
2247            $this->expunge_message($uids, $from_mbox, false);
2248            $this->clear_messagecount($from_mbox);
2249            $this->clear_messagecount($to_mbox);
2250        }
2251        // moving failed
2252        else if ($to_trash && $config->get('delete_always', false)) {
2253            $moved = $this->delete_message($uids, $from_mbox);
2254        }
2255
2256        if ($moved) {
2257            // unset threads internal cache
2258            unset($this->icache['threads']);
2259
2260            // remove message ids from search set
2261            if ($this->search_set && $from_mbox == $this->folder) {
2262                // threads are too complicated to just remove messages from set
2263                if ($this->search_threads || $all_mode) {
2264                    $this->refresh_search();
2265                }
2266                else {
2267                    $this->search_set->filter(explode(',', $uids));
2268                }
2269            }
2270
2271            // remove cached messages
2272            // @TODO: do cache update instead of clearing it
2273            $this->clear_message_cache($from_mbox, $all_mode ? null : explode(',', $uids));
2274        }
2275
2276        return $moved;
2277    }
2278
2279
2280    /**
2281     * Copy a message from one folder to another
2282     *
2283     * @param mixed  $uids      Message UIDs as array or comma-separated string, or '*'
2284     * @param string $to_mbox   Target folder
2285     * @param string $from_mbox Source folder
2286     *
2287     * @return boolean True on success, False on error
2288     */
2289    public function copy_message($uids, $to_mbox, $from_mbox='')
2290    {
2291        if (!strlen($from_mbox)) {
2292            $from_mbox = $this->folder;
2293        }
2294
2295        list($uids, $all_mode) = $this->parse_uids($uids);
2296
2297        // exit if no message uids are specified
2298        if (empty($uids)) {
2299            return false;
2300        }
2301
2302        if (!$this->check_connection()) {
2303            return false;
2304        }
2305
2306        // make sure folder exists
2307        if ($to_mbox != 'INBOX' && !$this->folder_exists($to_mbox)) {
2308            if (in_array($to_mbox, $this->default_folders)) {
2309                if (!$this->create_folder($to_mbox, true)) {
2310                    return false;
2311                }
2312            }
2313            else {
2314                return false;
2315            }
2316        }
2317
2318        // copy messages
2319        $copied = $this->conn->copy($uids, $from_mbox, $to_mbox);
2320
2321        if ($copied) {
2322            $this->clear_messagecount($to_mbox);
2323        }
2324
2325        return $copied;
2326    }
2327
2328
2329    /**
2330     * Mark messages as deleted and expunge them
2331     *
2332     * @param mixed  $uids    Message UIDs as array or comma-separated string, or '*'
2333     * @param string $folder  Source folder
2334     *
2335     * @return boolean True on success, False on error
2336     */
2337    public function delete_message($uids, $folder='')
2338    {
2339        if (!strlen($folder)) {
2340            $folder = $this->folder;
2341        }
2342
2343        list($uids, $all_mode) = $this->parse_uids($uids);
2344
2345        // exit if no message uids are specified
2346        if (empty($uids)) {
2347            return false;
2348        }
2349
2350        if (!$this->check_connection()) {
2351            return false;
2352        }
2353
2354        $deleted = $this->conn->flag($folder, $uids, 'DELETED');
2355
2356        if ($deleted) {
2357            // send expunge command in order to have the deleted message
2358            // really deleted from the folder
2359            $this->expunge_message($uids, $folder, false);
2360            $this->clear_messagecount($folder);
2361            unset($this->uid_id_map[$folder]);
2362
2363            // unset threads internal cache
2364            unset($this->icache['threads']);
2365
2366            // remove message ids from search set
2367            if ($this->search_set && $folder == $this->folder) {
2368                // threads are too complicated to just remove messages from set
2369                if ($this->search_threads || $all_mode) {
2370                    $this->refresh_search();
2371                }
2372                else {
2373                    $this->search_set->filter(explode(',', $uids));
2374                }
2375            }
2376
2377            // remove cached messages
2378            $this->clear_message_cache($folder, $all_mode ? null : explode(',', $uids));
2379        }
2380
2381        return $deleted;
2382    }
2383
2384
2385    /**
2386     * Send IMAP expunge command and clear cache
2387     *
2388     * @param mixed   $uids        Message UIDs as array or comma-separated string, or '*'
2389     * @param string  $folder      Folder name
2390     * @param boolean $clear_cache False if cache should not be cleared
2391     *
2392     * @return boolean True on success, False on failure
2393     */
2394    public function expunge_message($uids, $folder = null, $clear_cache = true)
2395    {
2396        if ($uids && $this->get_capability('UIDPLUS')) {
2397            list($uids, $all_mode) = $this->parse_uids($uids);
2398        }
2399        else {
2400            $uids = null;
2401        }
2402
2403        if (!strlen($folder)) {
2404            $folder = $this->folder;
2405        }
2406
2407        if (!$this->check_connection()) {
2408            return false;
2409        }
2410
2411        // force folder selection and check if folder is writeable
2412        // to prevent a situation when CLOSE is executed on closed
2413        // or EXPUNGE on read-only folder
2414        $result = $this->conn->select($folder);
2415        if (!$result) {
2416            return false;
2417        }
2418
2419        if (!$this->conn->data['READ-WRITE']) {
2420            $this->conn->setError(rcube_imap_generic::ERROR_READONLY, "Folder is read-only");
2421            return false;
2422        }
2423
2424        // CLOSE(+SELECT) should be faster than EXPUNGE
2425        if (empty($uids) || $all_mode) {
2426            $result = $this->conn->close();
2427        }
2428        else {
2429            $result = $this->conn->expunge($folder, $uids);
2430        }
2431
2432        if ($result && $clear_cache) {
2433            $this->clear_message_cache($folder, $all_mode ? null : explode(',', $uids));
2434            $this->clear_messagecount($folder);
2435        }
2436
2437        return $result;
2438    }
2439
2440
2441    /* --------------------------------
2442     *        folder managment
2443     * --------------------------------*/
2444
2445    /**
2446     * Public method for listing subscribed folders.
2447     *
2448     * @param   string  $root      Optional root folder
2449     * @param   string  $name      Optional name pattern
2450     * @param   string  $filter    Optional filter
2451     * @param   string  $rights    Optional ACL requirements
2452     * @param   bool    $skip_sort Enable to return unsorted list (for better performance)
2453     *
2454     * @return  array   List of folders
2455     */
2456    public function list_folders_subscribed($root='', $name='*', $filter=null, $rights=null, $skip_sort=false)
2457    {
2458        $cache_key = $root.':'.$name;
2459        if (!empty($filter)) {
2460            $cache_key .= ':'.(is_string($filter) ? $filter : serialize($filter));
2461        }
2462        $cache_key .= ':'.$rights;
2463        $cache_key = 'mailboxes.'.md5($cache_key);
2464
2465        // get cached folder list
2466        $a_mboxes = $this->get_cache($cache_key);
2467        if (is_array($a_mboxes)) {
2468            return $a_mboxes;
2469        }
2470
2471        // Give plugins a chance to provide a list of folders
2472        $data = rcube::get_instance()->plugins->exec_hook('storage_folders',
2473            array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LSUB'));
2474
2475        if (isset($data['folders'])) {
2476            $a_mboxes = $data['folders'];
2477        }
2478        else {
2479            $a_mboxes = $this->list_folders_subscribed_direct($root, $name);
2480        }
2481
2482        if (!is_array($a_mboxes)) {
2483            return array();
2484        }
2485
2486        // filter folders list according to rights requirements
2487        if ($rights && $this->get_capability('ACL')) {
2488            $a_mboxes = $this->filter_rights($a_mboxes, $rights);
2489        }
2490
2491        // INBOX should always be available
2492        if ((!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)) {
2493            array_unshift($a_mboxes, 'INBOX');
2494        }
2495
2496        // sort folders (always sort for cache)
2497        if (!$skip_sort || $this->cache) {
2498            $a_mboxes = $this->sort_folder_list($a_mboxes);
2499        }
2500
2501        // write folders list to cache
2502        $this->update_cache($cache_key, $a_mboxes);
2503
2504        return $a_mboxes;
2505    }
2506
2507
2508    /**
2509     * Method for direct folders listing (LSUB)
2510     *
2511     * @param   string  $root   Optional root folder
2512     * @param   string  $name   Optional name pattern
2513     *
2514     * @return  array   List of subscribed folders
2515     * @see     rcube_imap::list_folders_subscribed()
2516     */
2517    public function list_folders_subscribed_direct($root='', $name='*')
2518    {
2519        if (!$this->check_connection()) {
2520           return null;
2521        }
2522
2523        $config = rcube::get_instance()->config;
2524
2525        // Server supports LIST-EXTENDED, we can use selection options
2526        // #1486225: Some dovecot versions returns wrong result using LIST-EXTENDED
2527        if (!$config->get('imap_force_lsub') && $this->get_capability('LIST-EXTENDED')) {
2528            // This will also set folder options, LSUB doesn't do that
2529            $a_folders = $this->conn->listMailboxes($root, $name,
2530                NULL, array('SUBSCRIBED'));
2531
2532            // unsubscribe non-existent folders, remove from the list
2533            // we can do this only when LIST response is available
2534            if (is_array($a_folders) && $name == '*' && !empty($this->conn->data['LIST'])) {
2535                foreach ($a_folders as $idx => $folder) {
2536                    if (($opts = $this->conn->data['LIST'][$folder])
2537                        && in_array('\\NonExistent', $opts)
2538                    ) {
2539                        $this->conn->unsubscribe($folder);
2540                        unset($a_folders[$idx]);
2541                    }
2542                }
2543            }
2544        }
2545        // retrieve list of folders from IMAP server using LSUB
2546        else {
2547            $a_folders = $this->conn->listSubscribed($root, $name);
2548
2549            // unsubscribe non-existent folders, remove them from the list,
2550            // we can do this only when LIST response is available
2551            if (is_array($a_folders) && $name == '*' && !empty($this->conn->data['LIST'])) {
2552                foreach ($a_folders as $idx => $folder) {
2553                    if (!isset($this->conn->data['LIST'][$folder])
2554                        || in_array('\\Noselect', $this->conn->data['LIST'][$folder])
2555                    ) {
2556                        // Some servers returns \Noselect for existing folders
2557                        if (!$this->folder_exists($folder)) {
2558                            $this->conn->unsubscribe($folder);
2559                            unset($a_folders[$idx]);
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_direct($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     * Method for direct 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    public function list_folders_direct($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.', true);
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.', true);
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        $entries = (array)$entries;
3465
3466        // create cache key
3467        // @TODO: this is the simplest solution, but we do the same with folders list
3468        //        maybe we should store data per-entry and merge on request
3469        sort($options);
3470        sort($entries);
3471        $cache_key = 'mailboxes.metadata.' . $folder;
3472        $cache_key .= '.' . md5(serialize($options).serialize($entries));
3473
3474        // get cached data
3475        $cached_data = $this->get_cache($cache_key);
3476
3477        if (is_array($cached_data)) {
3478            return $cached_data;
3479        }
3480
3481        if (!$this->check_connection()) {
3482            return null;
3483        }
3484
3485        if ($this->get_capability('METADATA') ||
3486            (!strlen($folder) && $this->get_capability('METADATA-SERVER'))
3487        ) {
3488            $res = $this->conn->getMetadata($folder, $entries, $options);
3489        }
3490        else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
3491            $queries = array();
3492            $res     = array();
3493
3494            // Convert entry names
3495            foreach ($entries as $entry) {
3496                list($ent, $attr) = $this->md2annotate($entry);
3497                $queries[$attr][] = $ent;
3498            }
3499
3500            // @TODO: Honor MAXSIZE and DEPTH options
3501            foreach ($queries as $attrib => $entry) {
3502                if ($result = $this->conn->getAnnotation($folder, $entry, $attrib)) {
3503                    $res = array_merge_recursive($res, $result);
3504                }
3505            }
3506        }
3507
3508        if (isset($res)) {
3509            $this->update_cache($cache_key, $res);
3510            return $res;
3511        }
3512
3513        return null;
3514    }
3515
3516
3517    /**
3518     * Converts the METADATA extension entry name into the correct
3519     * entry-attrib names for older ANNOTATEMORE version.
3520     *
3521     * @param string $entry Entry name
3522     *
3523     * @return array Entry-attribute list, NULL if not supported (?)
3524     */
3525    protected function md2annotate($entry)
3526    {
3527        if (substr($entry, 0, 7) == '/shared') {
3528            return array(substr($entry, 7), 'value.shared');
3529        }
3530        else if (substr($entry, 0, 8) == '/private') {
3531            return array(substr($entry, 8), 'value.priv');
3532        }
3533
3534        // @TODO: log error
3535        return null;
3536    }
3537
3538
3539    /* --------------------------------
3540     *   internal caching methods
3541     * --------------------------------*/
3542
3543    /**
3544     * Enable or disable indexes caching
3545     *
3546     * @param string $type Cache type (@see rcube::get_cache)
3547     */
3548    public function set_caching($type)
3549    {
3550        if ($type) {
3551            $this->caching = $type;
3552        }
3553        else {
3554            if ($this->cache) {
3555                $this->cache->close();
3556            }
3557            $this->cache   = null;
3558            $this->caching = false;
3559        }
3560    }
3561
3562    /**
3563     * Getter for IMAP cache object
3564     */
3565    protected function get_cache_engine()
3566    {
3567        if ($this->caching && !$this->cache) {
3568            $rcube = rcube::get_instance();
3569            $ttl = $rcube->config->get('message_cache_lifetime', '10d');
3570            $this->cache = $rcube->get_cache('IMAP', $this->caching, $ttl);
3571        }
3572
3573        return $this->cache;
3574    }
3575
3576    /**
3577     * Returns cached value
3578     *
3579     * @param string $key Cache key
3580     *
3581     * @return mixed
3582     */
3583    public function get_cache($key)
3584    {
3585        if ($cache = $this->get_cache_engine()) {
3586            return $cache->get($key);
3587        }
3588    }
3589
3590    /**
3591     * Update cache
3592     *
3593     * @param string $key  Cache key
3594     * @param mixed  $data Data
3595     */
3596    public function update_cache($key, $data)
3597    {
3598        if ($cache = $this->get_cache_engine()) {
3599            $cache->set($key, $data);
3600        }
3601    }
3602
3603    /**
3604     * Clears the cache.
3605     *
3606     * @param string  $key         Cache key name or pattern
3607     * @param boolean $prefix_mode Enable it to clear all keys starting
3608     *                             with prefix specified in $key
3609     */
3610    public function clear_cache($key = null, $prefix_mode = false)
3611    {
3612        if ($cache = $this->get_cache_engine()) {
3613            $cache->remove($key, $prefix_mode);
3614        }
3615    }
3616
3617    /**
3618     * Delete outdated cache entries
3619     */
3620    public function expunge_cache()
3621    {
3622        if ($this->mcache) {
3623            $ttl = rcube::get_instance()->config->get('message_cache_lifetime', '10d');
3624            $this->mcache->expunge($ttl);
3625        }
3626
3627        if ($this->cache) {
3628            $this->cache->expunge();
3629        }
3630    }
3631
3632
3633    /* --------------------------------
3634     *   message caching methods
3635     * --------------------------------*/
3636
3637    /**
3638     * Enable or disable messages caching
3639     *
3640     * @param boolean $set Flag
3641     */
3642    public function set_messages_caching($set)
3643    {
3644        if ($set) {
3645            $this->messages_caching = true;
3646        }
3647        else {
3648            if ($this->mcache) {
3649                $this->mcache->close();
3650            }
3651            $this->mcache = null;
3652            $this->messages_caching = false;
3653        }
3654    }
3655
3656
3657    /**
3658     * Getter for messages cache object
3659     */
3660    protected function get_mcache_engine()
3661    {
3662        if ($this->messages_caching && !$this->mcache) {
3663            $rcube = rcube::get_instance();
3664            if ($dbh = $rcube->get_dbh()) {
3665                $this->mcache = new rcube_imap_cache(
3666                    $dbh, $this, $rcube->get_user_id(), $this->options['skip_deleted']);
3667            }
3668        }
3669
3670        return $this->mcache;
3671    }
3672
3673
3674    /**
3675     * Clears the messages cache.
3676     *
3677     * @param string $folder Folder name
3678     * @param array  $uids    Optional message UIDs to remove from cache
3679     */
3680    protected function clear_message_cache($folder = null, $uids = null)
3681    {
3682        if ($mcache = $this->get_mcache_engine()) {
3683            $mcache->clear($folder, $uids);
3684        }
3685    }
3686
3687
3688    /* --------------------------------
3689     *         protected methods
3690     * --------------------------------*/
3691
3692    /**
3693     * Validate the given input and save to local properties
3694     *
3695     * @param string $sort_field Sort column
3696     * @param string $sort_order Sort order
3697     */
3698    protected function set_sort_order($sort_field, $sort_order)
3699    {
3700        if ($sort_field != null) {
3701            $this->sort_field = asciiwords($sort_field);
3702        }
3703        if ($sort_order != null) {
3704            $this->sort_order = strtoupper($sort_order) == 'DESC' ? 'DESC' : 'ASC';
3705        }
3706    }
3707
3708
3709    /**
3710     * Sort folders first by default folders and then in alphabethical order
3711     *
3712     * @param array $a_folders Folders list
3713     */
3714    protected function sort_folder_list($a_folders)
3715    {
3716        $a_out = $a_defaults = $folders = array();
3717
3718        $delimiter = $this->get_hierarchy_delimiter();
3719
3720        // find default folders and skip folders starting with '.'
3721        foreach ($a_folders as $i => $folder) {
3722            if ($folder[0] == '.') {
3723                continue;
3724            }
3725
3726            if (($p = array_search($folder, $this->default_folders)) !== false && !$a_defaults[$p]) {
3727                $a_defaults[$p] = $folder;
3728            }
3729            else {
3730                $folders[$folder] = rcube_charset::convert($folder, 'UTF7-IMAP');
3731            }
3732        }
3733
3734        // sort folders and place defaults on the top
3735        asort($folders, SORT_LOCALE_STRING);
3736        ksort($a_defaults);
3737        $folders = array_merge($a_defaults, array_keys($folders));
3738
3739        // finally we must rebuild the list to move
3740        // subfolders of default folders to their place...
3741        // ...also do this for the rest of folders because
3742        // asort() is not properly sorting case sensitive names
3743        while (list($key, $folder) = each($folders)) {
3744            // set the type of folder name variable (#1485527)
3745            $a_out[] = (string) $folder;
3746            unset($folders[$key]);
3747            $this->rsort($folder, $delimiter, $folders, $a_out);
3748        }
3749
3750        return $a_out;
3751    }
3752
3753
3754    /**
3755     * Recursive method for sorting folders
3756     */
3757    protected function rsort($folder, $delimiter, &$list, &$out)
3758    {
3759        while (list($key, $name) = each($list)) {
3760                if (strpos($name, $folder.$delimiter) === 0) {
3761                    // set the type of folder name variable (#1485527)
3762                $out[] = (string) $name;
3763                    unset($list[$key]);
3764                    $this->rsort($name, $delimiter, $list, $out);
3765                }
3766        }
3767        reset($list);
3768    }
3769
3770
3771    /**
3772     * Find UID of the specified message sequence ID
3773     *
3774     * @param int    $id       Message (sequence) ID
3775     * @param string $folder   Folder name
3776     *
3777     * @return int Message UID
3778     */
3779    public function id2uid($id, $folder = null)
3780    {
3781        if (!strlen($folder)) {
3782            $folder = $this->folder;
3783        }
3784
3785        if ($uid = array_search($id, (array)$this->uid_id_map[$folder])) {
3786            return $uid;
3787        }
3788
3789        if (!$this->check_connection()) {
3790            return null;
3791        }
3792
3793        $uid = $this->conn->ID2UID($folder, $id);
3794
3795        $this->uid_id_map[$folder][$uid] = $id;
3796
3797        return $uid;
3798    }
3799
3800
3801    /**
3802     * Subscribe/unsubscribe a list of folders and update local cache
3803     */
3804    protected function change_subscription($folders, $mode)
3805    {
3806        $updated = false;
3807
3808        if (!empty($folders)) {
3809            if (!$this->check_connection()) {
3810                return false;
3811            }
3812
3813            foreach ((array)$folders as $i => $folder) {
3814                $folders[$i] = $folder;
3815
3816                if ($mode == 'subscribe') {
3817                    $updated = $this->conn->subscribe($folder);
3818                }
3819                else if ($mode == 'unsubscribe') {
3820                    $updated = $this->conn->unsubscribe($folder);
3821                }
3822            }
3823        }
3824
3825        // clear cached folders list(s)
3826        if ($updated) {
3827            $this->clear_cache('mailboxes', true);
3828        }
3829
3830        return $updated;
3831    }
3832
3833
3834    /**
3835     * Increde/decrese messagecount for a specific folder
3836     */
3837    protected function set_messagecount($folder, $mode, $increment)
3838    {
3839        if (!is_numeric($increment)) {
3840            return false;
3841        }
3842
3843        $mode = strtoupper($mode);
3844        $a_folder_cache = $this->get_cache('messagecount');
3845
3846        if (!is_array($a_folder_cache[$folder]) || !isset($a_folder_cache[$folder][$mode])) {
3847            return false;
3848        }
3849
3850        // add incremental value to messagecount
3851        $a_folder_cache[$folder][$mode] += $increment;
3852
3853        // there's something wrong, delete from cache
3854        if ($a_folder_cache[$folder][$mode] < 0) {
3855            unset($a_folder_cache[$folder][$mode]);
3856        }
3857
3858        // write back to cache
3859        $this->update_cache('messagecount', $a_folder_cache);
3860
3861        return true;
3862    }
3863
3864
3865    /**
3866     * Remove messagecount of a specific folder from cache
3867     */
3868    protected function clear_messagecount($folder, $mode=null)
3869    {
3870        $a_folder_cache = $this->get_cache('messagecount');
3871
3872        if (is_array($a_folder_cache[$folder])) {
3873            if ($mode) {
3874                unset($a_folder_cache[$folder][$mode]);
3875            }
3876            else {
3877                unset($a_folder_cache[$folder]);
3878            }
3879            $this->update_cache('messagecount', $a_folder_cache);
3880        }
3881    }
3882
3883
3884    /**
3885     * This is our own debug handler for the IMAP connection
3886     * @access public
3887     */
3888    public function debug_handler(&$imap, $message)
3889    {
3890        rcube::write_log('imap', $message);
3891    }
3892
3893
3894    /**
3895     * Deprecated methods (to be removed)
3896     */
3897
3898    public function decode_address_list($input, $max = null, $decode = true, $fallback = null)
3899    {
3900        return rcube_mime::decode_address_list($input, $max, $decode, $fallback);
3901    }
3902
3903    public function decode_header($input, $fallback = null)
3904    {
3905        return rcube_mime::decode_mime_string((string)$input, $fallback);
3906    }
3907
3908    public static function decode_mime_string($input, $fallback = null)
3909    {
3910        return rcube_mime::decode_mime_string($input, $fallback);
3911    }
3912
3913    public function mime_decode($input, $encoding = '7bit')
3914    {
3915        return rcube_mime::decode($input, $encoding);
3916    }
3917
3918    public static function explode_header_string($separator, $str, $remove_comments = false)
3919    {
3920        return rcube_mime::explode_header_string($separator, $str, $remove_comments);
3921    }
3922
3923    public function select_mailbox($mailbox)
3924    {
3925        // do nothing
3926    }
3927
3928    public function set_mailbox($folder)
3929    {
3930        $this->set_folder($folder);
3931    }
3932
3933    public function get_mailbox_name()
3934    {
3935        return $this->get_folder();
3936    }
3937
3938    public function list_headers($folder='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
3939    {
3940        return $this->list_messages($folder, $page, $sort_field, $sort_order, $slice);
3941    }
3942
3943    public function get_headers($uid, $folder = null, $force = false)
3944    {
3945        return $this->get_message_headers($uid, $folder, $force);
3946    }
3947
3948    public function mailbox_status($folder = null)
3949    {
3950        return $this->folder_status($folder);
3951    }
3952
3953    public function message_index($folder = '', $sort_field = NULL, $sort_order = NULL)
3954    {
3955        return $this->index($folder, $sort_field, $sort_order);
3956    }
3957
3958    public function message_index_direct($folder, $sort_field = null, $sort_order = null, $skip_cache = true)
3959    {
3960        return $this->index_direct($folder, $sort_field, $sort_order, $skip_cache);
3961    }
3962
3963    public function list_mailboxes($root='', $name='*', $filter=null, $rights=null, $skip_sort=false)
3964    {
3965        return $this->list_folders_subscribed($root, $name, $filter, $rights, $skip_sort);
3966    }
3967
3968    public function list_unsubscribed($root='', $name='*', $filter=null, $rights=null, $skip_sort=false)
3969    {
3970        return $this->list_folders($root, $name, $filter, $rights, $skip_sort);
3971    }
3972
3973    public function get_mailbox_size($folder)
3974    {
3975        return $this->folder_size($folder);
3976    }
3977
3978    public function create_mailbox($folder, $subscribe=false)
3979    {
3980        return $this->create_folder($folder, $subscribe);
3981    }
3982
3983    public function rename_mailbox($folder, $new_name)
3984    {
3985        return $this->rename_folder($folder, $new_name);
3986    }
3987
3988    function delete_mailbox($folder)
3989    {
3990        return $this->delete_folder($folder);
3991    }
3992
3993    public function mailbox_exists($folder, $subscription=false)
3994    {
3995        return $this->folder_exists($folder, $subscription);
3996    }
3997
3998    public function mailbox_namespace($folder)
3999    {
4000        return $this->folder_namespace($folder);
4001    }
4002
4003    public function mod_mailbox($folder, $mode = 'out')
4004    {
4005        return $this->mod_folder($folder, $mode);
4006    }
4007
4008    public function mailbox_attributes($folder, $force=false)
4009    {
4010        return $this->folder_attributes($folder, $force);
4011    }
4012
4013    public function mailbox_data($folder)
4014    {
4015        return $this->folder_data($folder);
4016    }
4017
4018    public function mailbox_info($folder)
4019    {
4020        return $this->folder_info($folder);
4021    }
4022
4023    public function mailbox_sync($folder)
4024    {
4025        return $this->folder_sync($folder);
4026    }
4027
4028    public function expunge($folder='', $clear_cache=true)
4029    {
4030        return $this->expunge_folder($folder, $clear_cache);
4031    }
4032
4033}
Note: See TracBrowser for help on using the repository browser.