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