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