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