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

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