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