source: subversion/branches/devel-framework/roundcubemail/program/include/rcube_imap.php @ 5765

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