Aleksander Machniak
2012-12-07 996af3bfd9bfcac84396790a9a215d177b17c79e
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  *
9ab346 28  * @package    Framework
AM 29  * @subpackage Storage
15a9d1 30  * @author     Thomas Bruederli <roundcube@gmail.com>
c43517 31  * @author     Aleksander Machniak <alec@alec.pl>
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(
3ff8cc 144                 'name'    => 'Roundcube',
AM 145                 'version' => RCUBE_VERSION,
146                 'php'     => PHP_VERSION,
147                 'os'      => PHP_OS,
890eae 148                 'command' => $_SERVER['REQUEST_URI'],
A 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
dff2c7 2054      * @param  int                $max_bytes  Only read this number of bytes
8abc17 2055      *
59c216 2056      * @return string Message/part body if not printed
A 2057      */
dff2c7 2058     public function get_message_part($uid, $part=1, $o_part=NULL, $print=NULL, $fp=NULL, $skip_charset_conv=false, $max_bytes=0)
8d4bcd 2059     {
c321a9 2060         if (!$this->check_connection()) {
T 2061             return null;
2062         }
2063
8a6503 2064         // get part data if not provided
59c216 2065         if (!is_object($o_part)) {
c321a9 2066             $structure = $this->conn->getStructure($this->folder, $uid, true);
8a6503 2067             $part_data = rcube_imap_generic::getStructurePartData($structure, $part);
5f571e 2068
59c216 2069             $o_part = new rcube_message_part;
8a6503 2070             $o_part->ctype_primary = $part_data['type'];
A 2071             $o_part->encoding      = $part_data['encoding'];
2072             $o_part->charset       = $part_data['charset'];
2073             $o_part->size          = $part_data['size'];
59c216 2074         }
c43517 2075
1ae119 2076         if ($o_part && $o_part->size) {
c321a9 2077             $body = $this->conn->handlePartBody($this->folder, $uid, true,
dff2c7 2078                 $part ? $part : 'TEXT', $o_part->encoding, $print, $fp, $o_part->ctype_primary == 'text', $max_bytes);
9840ab 2079         }
5f571e 2080
9840ab 2081         if ($fp || $print) {
59c216 2082             return true;
9840ab 2083         }
8d4bcd 2084
8abc17 2085         // convert charset (if text or message part)
eeae0d 2086         if ($body && preg_match('/^(text|message)$/', $o_part->ctype_primary)) {
e4a4ca 2087             // Remove NULL characters if any (#1486189)
A 2088             if (strpos($body, "\x00") !== false) {
2089                 $body = str_replace("\x00", '', $body);
2090             }
eeae0d 2091
e4a4ca 2092             if (!$skip_charset_conv) {
eeae0d 2093                 if (!$o_part->charset || strtoupper($o_part->charset) == 'US-ASCII') {
dfc79b 2094                     // try to extract charset information from HTML meta tag (#1488125)
c321a9 2095                     if ($o_part->ctype_secondary == 'html' && preg_match('/<meta[^>]+charset=([a-z0-9-_]+)/i', $body, $m)) {
dfc79b 2096                         $o_part->charset = strtoupper($m[1]);
c321a9 2097                     }
T 2098                     else {
dfc79b 2099                         $o_part->charset = $this->default_charset;
c321a9 2100                     }
eeae0d 2101                 }
0c2596 2102                 $body = rcube_charset::convert($body, $o_part->charset);
8abc17 2103             }
59c216 2104         }
c43517 2105
59c216 2106         return $body;
8d4bcd 2107     }
T 2108
2109
59c216 2110     /**
a208a4 2111      * Returns the whole message source as string (or saves to a file)
59c216 2112      *
a208a4 2113      * @param int      $uid Message UID
A 2114      * @param resource $fp  File pointer to save the message
2115      *
59c216 2116      * @return string Message source string
A 2117      */
c321a9 2118     public function get_raw_body($uid, $fp=null)
4e17e6 2119     {
c321a9 2120         if (!$this->check_connection()) {
T 2121             return null;
2122         }
2123
2124         return $this->conn->handlePartBody($this->folder, $uid,
a208a4 2125             true, null, null, false, $fp);
4e17e6 2126     }
e5686f 2127
A 2128
59c216 2129     /**
A 2130      * Returns the message headers as string
2131      *
5c461b 2132      * @param int $uid  Message UID
c321a9 2133      *
59c216 2134      * @return string Message headers string
A 2135      */
c321a9 2136     public function get_raw_headers($uid)
e5686f 2137     {
c321a9 2138         if (!$this->check_connection()) {
T 2139             return null;
2140         }
2141
2142         return $this->conn->fetchPartHeader($this->folder, $uid, true);
e5686f 2143     }
c43517 2144
8d4bcd 2145
59c216 2146     /**
A 2147      * Sends the whole message source to stdout
fb2f82 2148      *
AM 2149      * @param int  $uid       Message UID
2150      * @param bool $formatted Enables line-ending formatting
c43517 2151      */
fb2f82 2152     public function print_raw_body($uid, $formatted = true)
8d4bcd 2153     {
c321a9 2154         if (!$this->check_connection()) {
T 2155             return;
2156         }
2157
fb2f82 2158         $this->conn->handlePartBody($this->folder, $uid, true, null, null, true, null, $formatted);
8d4bcd 2159     }
4e17e6 2160
T 2161
59c216 2162     /**
A 2163      * Set message flag to one or several messages
2164      *
5c461b 2165      * @param mixed   $uids       Message UIDs as array or comma-separated string, or '*'
A 2166      * @param string  $flag       Flag to set: SEEN, UNDELETED, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
c321a9 2167      * @param string  $folder    Folder name
5c461b 2168      * @param boolean $skip_cache True to skip message cache clean up
d08333 2169      *
c309cd 2170      * @return boolean  Operation status
59c216 2171      */
c321a9 2172     public function set_flag($uids, $flag, $folder=null, $skip_cache=false)
4e17e6 2173     {
c321a9 2174         if (!strlen($folder)) {
T 2175             $folder = $this->folder;
2176         }
2177
2178         if (!$this->check_connection()) {
2179             return false;
d08333 2180         }
48958e 2181
59c216 2182         $flag = strtoupper($flag);
c321a9 2183         list($uids, $all_mode) = $this->parse_uids($uids);
cff886 2184
c321a9 2185         if (strpos($flag, 'UN') === 0) {
T 2186             $result = $this->conn->unflag($folder, $uids, substr($flag, 2));
2187         }
2188         else {
2189             $result = $this->conn->flag($folder, $uids, $flag);
2190         }
fa4cd2 2191
d0edbf 2192         if ($result && !$skip_cache) {
59c216 2193             // reload message headers if cached
d0edbf 2194             // update flags instead removing from cache
AM 2195             if ($mcache = $this->get_mcache_engine()) {
80152b 2196                 $status = strpos($flag, 'UN') !== 0;
A 2197                 $mflag  = preg_replace('/^UN/', '', $flag);
c321a9 2198                 $mcache->change_flag($folder, $all_mode ? null : explode(',', $uids),
80152b 2199                     $mflag, $status);
15e00b 2200             }
c309cd 2201
A 2202             // clear cached counters
2203             if ($flag == 'SEEN' || $flag == 'UNSEEN') {
c321a9 2204                 $this->clear_messagecount($folder, 'SEEN');
T 2205                 $this->clear_messagecount($folder, 'UNSEEN');
c309cd 2206             }
d0edbf 2207             else if ($flag == 'DELETED' || $flag == 'UNDELETED') {
c321a9 2208                 $this->clear_messagecount($folder, 'DELETED');
d0edbf 2209                 // remove cached messages
AM 2210                 if ($this->options['skip_deleted']) {
2211                     $this->clear_message_cache($folder, $all_mode ? null : explode(',', $uids));
2212                 }
c309cd 2213             }
4e17e6 2214         }
T 2215
59c216 2216         return $result;
4e17e6 2217     }
T 2218
fa4cd2 2219
59c216 2220     /**
c321a9 2221      * Append a mail message (source) to a specific folder
59c216 2222      *
c321a9 2223      * @param string  $folder  Target folder
d08333 2224      * @param string  $message The message source string or filename
A 2225      * @param string  $headers Headers string if $message contains only the body
2226      * @param boolean $is_file True if $message is a filename
7ac533 2227      * @param array   $flags   Message flags
AM 2228      * @param mixed   $date    Message internal date
59c216 2229      *
765fde 2230      * @return int|bool Appended message UID or True on success, False on error
59c216 2231      */
7ac533 2232     public function save_message($folder, &$message, $headers='', $is_file=false, $flags = array(), $date = null)
15e00b 2233     {
c321a9 2234         if (!strlen($folder)) {
T 2235             $folder = $this->folder;
d08333 2236         }
4e17e6 2237
b56526 2238         if (!$this->check_connection()) {
AM 2239             return false;
2240         }
2241
c321a9 2242         // make sure folder exists
7ac533 2243         if (!$this->folder_exists($folder)) {
AM 2244             return false;
2245         }
2246
2247         $date = $this->date_format($date);
2248
2249         if ($is_file) {
2250             $saved = $this->conn->appendFromFile($folder, $message, $headers, $flags, $date);
2251         }
2252         else {
2253             $saved = $this->conn->append($folder, $message, $flags, $date);
4e17e6 2254         }
f8a846 2255
59c216 2256         if ($saved) {
c321a9 2257             // increase messagecount of the target folder
T 2258             $this->set_messagecount($folder, 'ALL', 1);
8d4bcd 2259         }
59c216 2260
A 2261         return $saved;
8d4bcd 2262     }
T 2263
2264
59c216 2265     /**
c321a9 2266      * Move a message from one folder to another
59c216 2267      *
5c461b 2268      * @param mixed  $uids      Message UIDs as array or comma-separated string, or '*'
c321a9 2269      * @param string $to_mbox   Target folder
T 2270      * @param string $from_mbox Source folder
2271      *
59c216 2272      * @return boolean True on success, False on error
A 2273      */
c321a9 2274     public function move_message($uids, $to_mbox, $from_mbox='')
4e17e6 2275     {
d08333 2276         if (!strlen($from_mbox)) {
c321a9 2277             $from_mbox = $this->folder;
d08333 2278         }
c58c0a 2279
d08333 2280         if ($to_mbox === $from_mbox) {
af3c04 2281             return false;
d08333 2282         }
af3c04 2283
c321a9 2284         list($uids, $all_mode) = $this->parse_uids($uids);
41fa0b 2285
59c216 2286         // exit if no message uids are specified
c321a9 2287         if (empty($uids)) {
59c216 2288             return false;
c321a9 2289         }
59c216 2290
c321a9 2291         if (!$this->check_connection()) {
T 2292             return false;
2293         }
2294
2295         // make sure folder exists
2296         if ($to_mbox != 'INBOX' && !$this->folder_exists($to_mbox)) {
a561cd 2297             if (in_array($to_mbox, $this->default_folders)) {
c321a9 2298                 if (!$this->create_folder($to_mbox, true)) {
a561cd 2299                     return false;
A 2300                 }
2301             }
2302             else {
59c216 2303                 return false;
a561cd 2304             }
59c216 2305         }
A 2306
0c2596 2307         $config = rcube::get_instance()->config;
d08333 2308         $to_trash = $to_mbox == $config->get('trash_mbox');
A 2309
2310         // flag messages as read before moving them
2311         if ($to_trash && $config->get('read_when_deleted')) {
59c216 2312             // don't flush cache (4th argument)
d08333 2313             $this->set_flag($uids, 'SEEN', $from_mbox, true);
59c216 2314         }
A 2315
2316         // move messages
93272e 2317         $moved = $this->conn->move($uids, $from_mbox, $to_mbox);
59c216 2318
A 2319         // send expunge command in order to have the moved message
c321a9 2320         // really deleted from the source folder
59c216 2321         if ($moved) {
c321a9 2322             $this->expunge_message($uids, $from_mbox, false);
T 2323             $this->clear_messagecount($from_mbox);
2324             $this->clear_messagecount($to_mbox);
59c216 2325         }
A 2326         // moving failed
d08333 2327         else if ($to_trash && $config->get('delete_always', false)) {
A 2328             $moved = $this->delete_message($uids, $from_mbox);
59c216 2329         }
9e81b5 2330
59c216 2331         if ($moved) {
A 2332             // unset threads internal cache
2333             unset($this->icache['threads']);
2334
2335             // remove message ids from search set
c321a9 2336             if ($this->search_set && $from_mbox == $this->folder) {
59c216 2337                 // threads are too complicated to just remove messages from set
c321a9 2338                 if ($this->search_threads || $all_mode) {
59c216 2339                     $this->refresh_search();
c321a9 2340                 }
T 2341                 else {
40c45e 2342                     $this->search_set->filter(explode(',', $uids));
c321a9 2343                 }
59c216 2344             }
A 2345
80152b 2346             // remove cached messages
A 2347             // @TODO: do cache update instead of clearing it
2348             $this->clear_message_cache($from_mbox, $all_mode ? null : explode(',', $uids));
59c216 2349         }
A 2350
2351         return $moved;
2352     }
2353
2354
2355     /**
c321a9 2356      * Copy a message from one folder to another
59c216 2357      *
5c461b 2358      * @param mixed  $uids      Message UIDs as array or comma-separated string, or '*'
c321a9 2359      * @param string $to_mbox   Target folder
T 2360      * @param string $from_mbox Source folder
2361      *
59c216 2362      * @return boolean True on success, False on error
A 2363      */
c321a9 2364     public function copy_message($uids, $to_mbox, $from_mbox='')
59c216 2365     {
d08333 2366         if (!strlen($from_mbox)) {
c321a9 2367             $from_mbox = $this->folder;
d08333 2368         }
59c216 2369
c321a9 2370         list($uids, $all_mode) = $this->parse_uids($uids);
59c216 2371
A 2372         // exit if no message uids are specified
93272e 2373         if (empty($uids)) {
59c216 2374             return false;
93272e 2375         }
59c216 2376
c321a9 2377         if (!$this->check_connection()) {
T 2378             return false;
2379         }
2380
2381         // make sure folder exists
2382         if ($to_mbox != 'INBOX' && !$this->folder_exists($to_mbox)) {
a561cd 2383             if (in_array($to_mbox, $this->default_folders)) {
c321a9 2384                 if (!$this->create_folder($to_mbox, true)) {
a561cd 2385                     return false;
A 2386                 }
2387             }
2388             else {
59c216 2389                 return false;
a561cd 2390             }
59c216 2391         }
A 2392
2393         // copy messages
93272e 2394         $copied = $this->conn->copy($uids, $from_mbox, $to_mbox);
59c216 2395
A 2396         if ($copied) {
c321a9 2397             $this->clear_messagecount($to_mbox);
59c216 2398         }
A 2399
2400         return $copied;
2401     }
2402
2403
2404     /**
c321a9 2405      * Mark messages as deleted and expunge them
59c216 2406      *
d08333 2407      * @param mixed  $uids    Message UIDs as array or comma-separated string, or '*'
c321a9 2408      * @param string $folder  Source folder
d08333 2409      *
59c216 2410      * @return boolean True on success, False on error
A 2411      */
c321a9 2412     public function delete_message($uids, $folder='')
59c216 2413     {
c321a9 2414         if (!strlen($folder)) {
T 2415             $folder = $this->folder;
d08333 2416         }
59c216 2417
c321a9 2418         list($uids, $all_mode) = $this->parse_uids($uids);
59c216 2419
A 2420         // exit if no message uids are specified
c321a9 2421         if (empty($uids)) {
59c216 2422             return false;
c321a9 2423         }
59c216 2424
c321a9 2425         if (!$this->check_connection()) {
T 2426             return false;
2427         }
2428
2429         $deleted = $this->conn->flag($folder, $uids, 'DELETED');
59c216 2430
A 2431         if ($deleted) {
2432             // send expunge command in order to have the deleted message
c321a9 2433             // really deleted from the folder
T 2434             $this->expunge_message($uids, $folder, false);
2435             $this->clear_messagecount($folder);
2436             unset($this->uid_id_map[$folder]);
59c216 2437
A 2438             // unset threads internal cache
2439             unset($this->icache['threads']);
c43517 2440
59c216 2441             // remove message ids from search set
c321a9 2442             if ($this->search_set && $folder == $this->folder) {
59c216 2443                 // threads are too complicated to just remove messages from set
c321a9 2444                 if ($this->search_threads || $all_mode) {
59c216 2445                     $this->refresh_search();
c321a9 2446                 }
T 2447                 else {
40c45e 2448                     $this->search_set->filter(explode(',', $uids));
c321a9 2449                 }
59c216 2450             }
A 2451
80152b 2452             // remove cached messages
c321a9 2453             $this->clear_message_cache($folder, $all_mode ? null : explode(',', $uids));
59c216 2454         }
A 2455
2456         return $deleted;
2457     }
2458
2459
2460     /**
2461      * Send IMAP expunge command and clear cache
2462      *
5c461b 2463      * @param mixed   $uids        Message UIDs as array or comma-separated string, or '*'
c321a9 2464      * @param string  $folder      Folder name
T 2465      * @param boolean $clear_cache False if cache should not be cleared
2466      *
2467      * @return boolean True on success, False on failure
59c216 2468      */
c321a9 2469     public function expunge_message($uids, $folder = null, $clear_cache = true)
59c216 2470     {
c321a9 2471         if ($uids && $this->get_capability('UIDPLUS')) {
T 2472             list($uids, $all_mode) = $this->parse_uids($uids);
2473         }
2474         else {
80152b 2475             $uids = null;
c321a9 2476         }
59c216 2477
c321a9 2478         if (!strlen($folder)) {
T 2479             $folder = $this->folder;
2480         }
2481
2482         if (!$this->check_connection()) {
2483             return false;
2484         }
2485
2486         // force folder selection and check if folder is writeable
90f81a 2487         // to prevent a situation when CLOSE is executed on closed
c321a9 2488         // or EXPUNGE on read-only folder
T 2489         $result = $this->conn->select($folder);
90f81a 2490         if (!$result) {
A 2491             return false;
2492         }
c321a9 2493
90f81a 2494         if (!$this->conn->data['READ-WRITE']) {
c321a9 2495             $this->conn->setError(rcube_imap_generic::ERROR_READONLY, "Folder is read-only");
90f81a 2496             return false;
A 2497         }
2498
e232ac 2499         // CLOSE(+SELECT) should be faster than EXPUNGE
c321a9 2500         if (empty($uids) || $all_mode) {
e232ac 2501             $result = $this->conn->close();
c321a9 2502         }
T 2503         else {
2504             $result = $this->conn->expunge($folder, $uids);
2505         }
59c216 2506
854cf2 2507         if ($result && $clear_cache) {
c321a9 2508             $this->clear_message_cache($folder, $all_mode ? null : explode(',', $uids));
T 2509             $this->clear_messagecount($folder);
59c216 2510         }
c43517 2511
59c216 2512         return $result;
A 2513     }
2514
2515
2516     /* --------------------------------
2517      *        folder managment
2518      * --------------------------------*/
2519
2520     /**
c321a9 2521      * Public method for listing subscribed folders.
59c216 2522      *
888176 2523      * @param   string  $root      Optional root folder
A 2524      * @param   string  $name      Optional name pattern
2525      * @param   string  $filter    Optional filter
2526      * @param   string  $rights    Optional ACL requirements
2527      * @param   bool    $skip_sort Enable to return unsorted list (for better performance)
d08333 2528      *
d342f8 2529      * @return  array   List of folders
59c216 2530      */
c321a9 2531     public function list_folders_subscribed($root='', $name='*', $filter=null, $rights=null, $skip_sort=false)
59c216 2532     {
d342f8 2533         $cache_key = $root.':'.$name;
A 2534         if (!empty($filter)) {
2535             $cache_key .= ':'.(is_string($filter) ? $filter : serialize($filter));
2536         }
2537         $cache_key .= ':'.$rights;
2538         $cache_key = 'mailboxes.'.md5($cache_key);
2539
2540         // get cached folder list
2541         $a_mboxes = $this->get_cache($cache_key);
2542         if (is_array($a_mboxes)) {
2543             return $a_mboxes;
2544         }
2545
435d55 2546         // Give plugins a chance to provide a list of folders
AM 2547         $data = rcube::get_instance()->plugins->exec_hook('storage_folders',
2548             array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LSUB'));
2549
2550         if (isset($data['folders'])) {
2551             $a_mboxes = $data['folders'];
2552         }
2553         else {
2554             $a_mboxes = $this->list_folders_subscribed_direct($root, $name);
2555         }
d342f8 2556
A 2557         if (!is_array($a_mboxes)) {
2558             return array();
2559         }
2560
2561         // filter folders list according to rights requirements
2562         if ($rights && $this->get_capability('ACL')) {
2563             $a_mboxes = $this->filter_rights($a_mboxes, $rights);
2564         }
59c216 2565
A 2566         // INBOX should always be available
94bdcc 2567         if ((!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)) {
d08333 2568             array_unshift($a_mboxes, 'INBOX');
94bdcc 2569         }
59c216 2570
c321a9 2571         // sort folders (always sort for cache)
d342f8 2572         if (!$skip_sort || $this->cache) {
c321a9 2573             $a_mboxes = $this->sort_folder_list($a_mboxes);
888176 2574         }
d342f8 2575
c321a9 2576         // write folders list to cache
d342f8 2577         $this->update_cache($cache_key, $a_mboxes);
59c216 2578
d08333 2579         return $a_mboxes;
59c216 2580     }
A 2581
2582
2583     /**
435d55 2584      * Method for direct folders listing (LSUB)
59c216 2585      *
5c461b 2586      * @param   string  $root   Optional root folder
94bdcc 2587      * @param   string  $name   Optional name pattern
A 2588      *
89dcf5 2589      * @return  array   List of subscribed folders
c321a9 2590      * @see     rcube_imap::list_folders_subscribed()
59c216 2591      */
435d55 2592     public function list_folders_subscribed_direct($root='', $name='*')
59c216 2593     {
435d55 2594         if (!$this->check_connection()) {
d342f8 2595            return null;
20ed37 2596         }
3870be 2597
435d55 2598         $config = rcube::get_instance()->config;
AM 2599
2600         // Server supports LIST-EXTENDED, we can use selection options
2601         // #1486225: Some dovecot versions returns wrong result using LIST-EXTENDED
0af82c 2602         $list_extended = !$config->get('imap_force_lsub') && $this->get_capability('LIST-EXTENDED');
AM 2603         if ($list_extended) {
435d55 2604             // This will also set folder options, LSUB doesn't do that
AM 2605             $a_folders = $this->conn->listMailboxes($root, $name,
2606                 NULL, array('SUBSCRIBED'));
0af82c 2607         }
AM 2608         else {
2609             // retrieve list of folders from IMAP server using LSUB
2610             $a_folders = $this->conn->listSubscribed($root, $name);
2611         }
435d55 2612
0af82c 2613         if (!is_array($a_folders)) {
AM 2614             return array();
2615         }
2616
3c5489 2617         // #1486796: some server configurations doesn't return folders in all namespaces
AM 2618         if ($root == '' && $name == '*' && $config->get('imap_force_ns')) {
0af82c 2619             $this->list_folders_update($a_folders, ($list_extended ? 'ext-' : '') . 'subscribed');
AM 2620         }
2621
2622         if ($list_extended) {
435d55 2623             // unsubscribe non-existent folders, remove from the list
AM 2624             // we can do this only when LIST response is available
2625             if (is_array($a_folders) && $name == '*' && !empty($this->conn->data['LIST'])) {
2626                 foreach ($a_folders as $idx => $folder) {
2627                     if (($opts = $this->conn->data['LIST'][$folder])
2628                         && in_array('\\NonExistent', $opts)
2629                     ) {
2630                         $this->conn->unsubscribe($folder);
2631                         unset($a_folders[$idx]);
3870be 2632                     }
A 2633                 }
2634             }
435d55 2635         }
AM 2636         else {
2637             // unsubscribe non-existent folders, remove them from the list,
2638             // we can do this only when LIST response is available
2639             if (is_array($a_folders) && $name == '*' && !empty($this->conn->data['LIST'])) {
2640                 foreach ($a_folders as $idx => $folder) {
2641                     if (!isset($this->conn->data['LIST'][$folder])
2642                         || in_array('\\Noselect', $this->conn->data['LIST'][$folder])
2643                     ) {
2644                         // Some servers returns \Noselect for existing folders
2645                         if (!$this->folder_exists($folder)) {
2646                             $this->conn->unsubscribe($folder);
2647                             unset($a_folders[$idx]);
189a0a 2648                         }
A 2649                     }
2650                 }
3870be 2651             }
94bdcc 2652         }
c43517 2653
59c216 2654         return $a_folders;
A 2655     }
2656
2657
2658     /**
c321a9 2659      * Get a list of all folders available on the server
c43517 2660      *
888176 2661      * @param string  $root      IMAP root dir
A 2662      * @param string  $name      Optional name pattern
2663      * @param mixed   $filter    Optional filter
2664      * @param string  $rights    Optional ACL requirements
2665      * @param bool    $skip_sort Enable to return unsorted list (for better performance)
94bdcc 2666      *
59c216 2667      * @return array Indexed array with folder names
A 2668      */
c321a9 2669     public function list_folders($root='', $name='*', $filter=null, $rights=null, $skip_sort=false)
59c216 2670     {
aa07b2 2671         $cache_key = $root.':'.$name;
A 2672         if (!empty($filter)) {
2673             $cache_key .= ':'.(is_string($filter) ? $filter : serialize($filter));
2674         }
2675         $cache_key .= ':'.$rights;
2676         $cache_key = 'mailboxes.list.'.md5($cache_key);
2677
2678         // get cached folder list
2679         $a_mboxes = $this->get_cache($cache_key);
2680         if (is_array($a_mboxes)) {
2681             return $a_mboxes;
2682         }
2683
c321a9 2684         // Give plugins a chance to provide a list of folders
be98df 2685         $data = rcube::get_instance()->plugins->exec_hook('storage_folders',
94bdcc 2686             array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LIST'));
c43517 2687
6f4e7d 2688         if (isset($data['folders'])) {
A 2689             $a_mboxes = $data['folders'];
2690         }
2691         else {
2692             // retrieve list of folders from IMAP server
435d55 2693             $a_mboxes = $this->list_folders_direct($root, $name);
6f4e7d 2694         }
64e3e8 2695
d08333 2696         if (!is_array($a_mboxes)) {
6f4e7d 2697             $a_mboxes = array();
59c216 2698         }
f0485a 2699
A 2700         // INBOX should always be available
94bdcc 2701         if ((!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)) {
d08333 2702             array_unshift($a_mboxes, 'INBOX');
94bdcc 2703         }
59c216 2704
aa07b2 2705         // cache folder attributes
c321a9 2706         if ($root == '' && $name == '*' && empty($filter) && !empty($this->conn->data)) {
aa07b2 2707             $this->update_cache('mailboxes.attributes', $this->conn->data['LIST']);
A 2708         }
2709
e750d1 2710         // filter folders list according to rights requirements
T 2711         if ($rights && $this->get_capability('ACL')) {
2712             $a_folders = $this->filter_rights($a_folders, $rights);
2713         }
2714
59c216 2715         // filter folders and sort them
888176 2716         if (!$skip_sort) {
c321a9 2717             $a_mboxes = $this->sort_folder_list($a_mboxes);
888176 2718         }
aa07b2 2719
c321a9 2720         // write folders list to cache
aa07b2 2721         $this->update_cache($cache_key, $a_mboxes);
d08333 2722
A 2723         return $a_mboxes;
59c216 2724     }
A 2725
2726
2727     /**
435d55 2728      * Method for direct folders listing (LIST)
89dcf5 2729      *
A 2730      * @param   string  $root   Optional root folder
2731      * @param   string  $name   Optional name pattern
2732      *
2733      * @return  array   List of folders
c321a9 2734      * @see     rcube_imap::list_folders()
89dcf5 2735      */
435d55 2736     public function list_folders_direct($root='', $name='*')
89dcf5 2737     {
c321a9 2738         if (!$this->check_connection()) {
T 2739             return null;
2740         }
2741
89dcf5 2742         $result = $this->conn->listMailboxes($root, $name);
A 2743
2744         if (!is_array($result)) {
2745             return array();
2746         }
2747
38184e 2748         $config = rcube::get_instance()->config;
AM 2749
3c5489 2750         // #1486796: some server configurations doesn't return folders in all namespaces
AM 2751         if ($root == '' && $name == '*' && $config->get('imap_force_ns')) {
0af82c 2752             $this->list_folders_update($result);
AM 2753         }
89dcf5 2754
0af82c 2755         return $result;
AM 2756     }
89dcf5 2757
A 2758
0af82c 2759     /**
AM 2760      * Fix folders list by adding folders from other namespaces.
2761      * Needed on some servers eg. Courier IMAP
2762      *
2763      * @param array  $result  Reference to folders list
2764      * @param string $type    Listing type (ext-subscribed, subscribed or all)
2765      */
2766     private function list_folders_update(&$result, $type = null)
2767     {
2768         $delim     = $this->get_hierarchy_delimiter();
2769         $namespace = $this->get_namespace();
2770         $search    = array();
89dcf5 2771
0af82c 2772         // build list of namespace prefixes
AM 2773         foreach ((array)$namespace as $ns) {
2774             if (is_array($ns)) {
2775                 foreach ($ns as $ns_data) {
2776                     if (strlen($ns_data[0])) {
2777                         $search[] = $ns_data[0];
89dcf5 2778                     }
A 2779                 }
2780             }
2781         }
2782
0af82c 2783         if (!empty($search)) {
AM 2784             // go through all folders detecting namespace usage
2785             foreach ($result as $folder) {
2786                 foreach ($search as $idx => $prefix) {
2787                     if (strpos($folder, $prefix) === 0) {
2788                         unset($search[$idx]);
2789                     }
2790                 }
2791                 if (empty($search)) {
2792                     break;
2793                 }
2794             }
2795
2796             // get folders in hidden namespaces and add to the result
2797             foreach ($search as $prefix) {
2798                 if ($type == 'ext-subscribed') {
2799                     $list = $this->conn->listMailboxes('', $prefix . '*', null, array('SUBSCRIBED'));
2800                 }
2801                 else if ($type == 'subscribed') {
2802                     $list = $this->conn->listSubscribed('', $prefix . '*');
2803                 }
2804                 else {
2805                     $list = $this->conn->listMailboxes('', $prefix . '*');
2806                 }
2807
2808                 if (!empty($list)) {
2809                     $result = array_merge($result, $list);
2810                 }
2811             }
2812         }
89dcf5 2813     }
A 2814
2815
2816     /**
e750d1 2817      * Filter the given list of folders according to access rights
T 2818      */
c321a9 2819     protected function filter_rights($a_folders, $rights)
e750d1 2820     {
T 2821         $regex = '/('.$rights.')/';
2822         foreach ($a_folders as $idx => $folder) {
2823             $myrights = join('', (array)$this->my_rights($folder));
c321a9 2824             if ($myrights !== null && !preg_match($regex, $myrights)) {
e750d1 2825                 unset($a_folders[$idx]);
c321a9 2826             }
e750d1 2827         }
T 2828
2829         return $a_folders;
2830     }
2831
2832
2833     /**
59c216 2834      * Get mailbox quota information
A 2835      * added by Nuny
c43517 2836      *
59c216 2837      * @return mixed Quota info or False if not supported
A 2838      */
c321a9 2839     public function get_quota()
59c216 2840     {
ef1e87 2841         if ($this->get_capability('QUOTA') && $this->check_connection()) {
59c216 2842             return $this->conn->getQuota();
c321a9 2843         }
c43517 2844
59c216 2845         return false;
A 2846     }
2847
2848
2849     /**
c321a9 2850      * Get folder size (size of all messages in a folder)
af3c04 2851      *
c321a9 2852      * @param string $folder Folder name
d08333 2853      *
c321a9 2854      * @return int Folder size in bytes, False on error
af3c04 2855      */
c321a9 2856     public function folder_size($folder)
af3c04 2857     {
c321a9 2858         if (!$this->check_connection()) {
T 2859             return 0;
2860         }
af3c04 2861
c321a9 2862         // @TODO: could we try to use QUOTA here?
T 2863         $result = $this->conn->fetchHeaderIndex($folder, '1:*', 'SIZE', false);
2864
2865         if (is_array($result)) {
af3c04 2866             $result = array_sum($result);
c321a9 2867         }
af3c04 2868
A 2869         return $result;
2870     }
2871
2872
2873     /**
c321a9 2874      * Subscribe to a specific folder(s)
59c216 2875      *
c321a9 2876      * @param array $folders Folder name(s)
T 2877      *
59c216 2878      * @return boolean True on success
c43517 2879      */
c321a9 2880     public function subscribe($folders)
59c216 2881     {
A 2882         // let this common function do the main work
c321a9 2883         return $this->change_subscription($folders, 'subscribe');
59c216 2884     }
A 2885
2886
2887     /**
c321a9 2888      * Unsubscribe folder(s)
59c216 2889      *
c321a9 2890      * @param array $a_mboxes Folder name(s)
T 2891      *
59c216 2892      * @return boolean True on success
A 2893      */
c321a9 2894     public function unsubscribe($folders)
59c216 2895     {
A 2896         // let this common function do the main work
c321a9 2897         return $this->change_subscription($folders, 'unsubscribe');
59c216 2898     }
A 2899
2900
2901     /**
c321a9 2902      * Create a new folder on the server and register it in local cache
59c216 2903      *
c321a9 2904      * @param string  $folder    New folder name
T 2905      * @param boolean $subscribe True if the new folder should be subscribed
d08333 2906      *
A 2907      * @return boolean True on success
59c216 2908      */
c321a9 2909     public function create_folder($folder, $subscribe=false)
59c216 2910     {
c321a9 2911         if (!$this->check_connection()) {
T 2912             return false;
2913         }
2914
2915         $result = $this->conn->createFolder($folder);
59c216 2916
A 2917         // try to subscribe it
392589 2918         if ($result) {
A 2919             // clear cache
ccc059 2920             $this->clear_cache('mailboxes', true);
392589 2921
c321a9 2922             if ($subscribe) {
T 2923                 $this->subscribe($folder);
2924             }
392589 2925         }
59c216 2926
af3c04 2927         return $result;
59c216 2928     }
A 2929
2930
2931     /**
c321a9 2932      * Set a new name to an existing folder
59c216 2933      *
c321a9 2934      * @param string $folder   Folder to rename
T 2935      * @param string $new_name New folder name
0e1194 2936      *
af3c04 2937      * @return boolean True on success
59c216 2938      */
c321a9 2939     public function rename_folder($folder, $new_name)
59c216 2940     {
d08333 2941         if (!strlen($new_name)) {
c321a9 2942             return false;
T 2943         }
2944
2945         if (!$this->check_connection()) {
d08333 2946             return false;
A 2947         }
59c216 2948
d08333 2949         $delm = $this->get_hierarchy_delimiter();
c43517 2950
0e1194 2951         // get list of subscribed folders
c321a9 2952         if ((strpos($folder, '%') === false) && (strpos($folder, '*') === false)) {
fa5f3f 2953             $a_subscribed = $this->list_folders_subscribed('', $folder . $delm . '*');
c321a9 2954             $subscribed   = $this->folder_exists($folder, true);
0e1194 2955         }
A 2956         else {
fa5f3f 2957             $a_subscribed = $this->list_folders_subscribed();
c321a9 2958             $subscribed   = in_array($folder, $a_subscribed);
0e1194 2959         }
59c216 2960
c321a9 2961         $result = $this->conn->renameFolder($folder, $new_name);
59c216 2962
A 2963         if ($result) {
0e1194 2964             // unsubscribe the old folder, subscribe the new one
A 2965             if ($subscribed) {
c321a9 2966                 $this->conn->unsubscribe($folder);
d08333 2967                 $this->conn->subscribe($new_name);
0e1194 2968             }
677e1f 2969
c321a9 2970             // check if folder children are subscribed
0e1194 2971             foreach ($a_subscribed as $c_subscribed) {
c321a9 2972                 if (strpos($c_subscribed, $folder.$delm) === 0) {
59c216 2973                     $this->conn->unsubscribe($c_subscribed);
c321a9 2974                     $this->conn->subscribe(preg_replace('/^'.preg_quote($folder, '/').'/',
d08333 2975                         $new_name, $c_subscribed));
80152b 2976
A 2977                     // clear cache
2978                     $this->clear_message_cache($c_subscribed);
59c216 2979                 }
0e1194 2980             }
59c216 2981
A 2982             // clear cache
c321a9 2983             $this->clear_message_cache($folder);
ccc059 2984             $this->clear_cache('mailboxes', true);
59c216 2985         }
A 2986
af3c04 2987         return $result;
59c216 2988     }
A 2989
2990
2991     /**
c321a9 2992      * Remove folder from server
59c216 2993      *
c321a9 2994      * @param string $folder Folder name
0e1194 2995      *
59c216 2996      * @return boolean True on success
A 2997      */
c321a9 2998     function delete_folder($folder)
59c216 2999     {
d08333 3000         $delm = $this->get_hierarchy_delimiter();
59c216 3001
c321a9 3002         if (!$this->check_connection()) {
T 3003             return false;
3004         }
3005
0e1194 3006         // get list of folders
c321a9 3007         if ((strpos($folder, '%') === false) && (strpos($folder, '*') === false)) {
0457c5 3008             $sub_mboxes = $this->list_folders('', $folder . $delm . '*');
c321a9 3009         }
T 3010         else {
0457c5 3011             $sub_mboxes = $this->list_folders();
c321a9 3012         }
59c216 3013
0e1194 3014         // send delete command to server
c321a9 3015         $result = $this->conn->deleteFolder($folder);
59c216 3016
0e1194 3017         if ($result) {
c321a9 3018             // unsubscribe folder
T 3019             $this->conn->unsubscribe($folder);
59c216 3020
0e1194 3021             foreach ($sub_mboxes as $c_mbox) {
c321a9 3022                 if (strpos($c_mbox, $folder.$delm) === 0) {
0e1194 3023                     $this->conn->unsubscribe($c_mbox);
A 3024                     if ($this->conn->deleteFolder($c_mbox)) {
435d55 3025                         $this->clear_message_cache($c_mbox);
59c216 3026                     }
A 3027                 }
3028             }
0e1194 3029
c321a9 3030             // clear folder-related cache
T 3031             $this->clear_message_cache($folder);
ccc059 3032             $this->clear_cache('mailboxes', true);
59c216 3033         }
A 3034
0e1194 3035         return $result;
59c216 3036     }
A 3037
3038
3039     /**
3040      * Create all folders specified as default
3041      */
c321a9 3042     public function create_default_folders()
59c216 3043     {
A 3044         // create default folders if they do not exist
3045         foreach ($this->default_folders as $folder) {
c321a9 3046             if (!$this->folder_exists($folder)) {
T 3047                 $this->create_folder($folder, true);
3048             }
3049             else if (!$this->folder_exists($folder, true)) {
59c216 3050                 $this->subscribe($folder);
c321a9 3051             }
59c216 3052         }
A 3053     }
3054
3055
3056     /**
3057      * Checks if folder exists and is subscribed
3058      *
c321a9 3059      * @param string   $folder       Folder name
5c461b 3060      * @param boolean  $subscription Enable subscription checking
d08333 3061      *
59c216 3062      * @return boolean TRUE or FALSE
A 3063      */
c321a9 3064     public function folder_exists($folder, $subscription=false)
59c216 3065     {
c321a9 3066         if ($folder == 'INBOX') {
448409 3067             return true;
d08333 3068         }
59c216 3069
448409 3070         $key  = $subscription ? 'subscribed' : 'existing';
ad5881 3071
c321a9 3072         if (is_array($this->icache[$key]) && in_array($folder, $this->icache[$key])) {
448409 3073             return true;
c321a9 3074         }
T 3075
3076         if (!$this->check_connection()) {
3077             return false;
3078         }
16378f 3079
448409 3080         if ($subscription) {
c321a9 3081             $a_folders = $this->conn->listSubscribed('', $folder);
448409 3082         }
A 3083         else {
c321a9 3084             $a_folders = $this->conn->listMailboxes('', $folder);
af3c04 3085         }
c43517 3086
c321a9 3087         if (is_array($a_folders) && in_array($folder, $a_folders)) {
T 3088             $this->icache[$key][] = $folder;
448409 3089             return true;
59c216 3090         }
A 3091
3092         return false;
3093     }
3094
3095
3096     /**
bbce3e 3097      * Returns the namespace where the folder is in
A 3098      *
c321a9 3099      * @param string $folder Folder name
bbce3e 3100      *
A 3101      * @return string One of 'personal', 'other' or 'shared'
3102      */
c321a9 3103     public function folder_namespace($folder)
bbce3e 3104     {
c321a9 3105         if ($folder == 'INBOX') {
bbce3e 3106             return 'personal';
A 3107         }
3108
3109         foreach ($this->namespace as $type => $namespace) {
3110             if (is_array($namespace)) {
3111                 foreach ($namespace as $ns) {
305b36 3112                     if ($len = strlen($ns[0])) {
c321a9 3113                         if (($len > 1 && $folder == substr($ns[0], 0, -1))
T 3114                             || strpos($folder, $ns[0]) === 0
bbce3e 3115                         ) {
A 3116                             return $type;
3117                         }
3118                     }
3119                 }
3120             }
3121         }
3122
3123         return 'personal';
3124     }
3125
3126
3127     /**
d08333 3128      * Modify folder name according to namespace.
A 3129      * For output it removes prefix of the personal namespace if it's possible.
3130      * For input it adds the prefix. Use it before creating a folder in root
3131      * of the folders tree.
59c216 3132      *
c321a9 3133      * @param string $folder Folder name
d08333 3134      * @param string $mode    Mode name (out/in)
A 3135      *
59c216 3136      * @return string Folder name
A 3137      */
c321a9 3138     public function mod_folder($folder, $mode = 'out')
59c216 3139     {
c321a9 3140         if (!strlen($folder)) {
T 3141             return $folder;
d08333 3142         }
59c216 3143
d08333 3144         $prefix     = $this->namespace['prefix']; // see set_env()
A 3145         $prefix_len = strlen($prefix);
3146
3147         if (!$prefix_len) {
c321a9 3148             return $folder;
d08333 3149         }
A 3150
3151         // remove prefix for output
3152         if ($mode == 'out') {
c321a9 3153             if (substr($folder, 0, $prefix_len) === $prefix) {
T 3154                 return substr($folder, $prefix_len);
00290a 3155             }
A 3156         }
d08333 3157         // add prefix for input (e.g. folder creation)
00290a 3158         else {
c321a9 3159             return $prefix . $folder;
59c216 3160         }
c43517 3161
c321a9 3162         return $folder;
59c216 3163     }
A 3164
3165
103ddc 3166     /**
aa07b2 3167      * Gets folder attributes from LIST response, e.g. \Noselect, \Noinferiors
a5a4bf 3168      *
c321a9 3169      * @param string $folder Folder name
aa07b2 3170      * @param bool   $force   Set to True if attributes should be refreshed
a5a4bf 3171      *
A 3172      * @return array Options list
3173      */
c321a9 3174     public function folder_attributes($folder, $force=false)
a5a4bf 3175     {
aa07b2 3176         // get attributes directly from LIST command
c321a9 3177         if (!empty($this->conn->data['LIST']) && is_array($this->conn->data['LIST'][$folder])) {
T 3178             $opts = $this->conn->data['LIST'][$folder];
aa07b2 3179         }
A 3180         // get cached folder attributes
3181         else if (!$force) {
3182             $opts = $this->get_cache('mailboxes.attributes');
c321a9 3183             $opts = $opts[$folder];
a5a4bf 3184         }
A 3185
aa07b2 3186         if (!is_array($opts)) {
c321a9 3187             if (!$this->check_connection()) {
T 3188                 return array();
3189             }
3190
3191             $this->conn->listMailboxes('', $folder);
3192             $opts = $this->conn->data['LIST'][$folder];
a5a4bf 3193         }
A 3194
3195         return is_array($opts) ? $opts : array();
80152b 3196     }
A 3197
3198
3199     /**
c321a9 3200      * Gets connection (and current folder) data: UIDVALIDITY, EXISTS, RECENT,
80152b 3201      * PERMANENTFLAGS, UIDNEXT, UNSEEN
A 3202      *
c321a9 3203      * @param string $folder Folder name
80152b 3204      *
A 3205      * @return array Data
3206      */
c321a9 3207     public function folder_data($folder)
80152b 3208     {
c321a9 3209         if (!strlen($folder)) {
T 3210             $folder = $this->folder !== null ? $this->folder : 'INBOX';
3211         }
80152b 3212
c321a9 3213         if ($this->conn->selected != $folder) {
T 3214             if (!$this->check_connection()) {
3215                 return array();
3216             }
3217
3218             if ($this->conn->select($folder)) {
3219                 $this->folder = $folder;
3220             }
3221             else {
609d39 3222                 return null;
c321a9 3223             }
80152b 3224         }
A 3225
3226         $data = $this->conn->data;
3227
3228         // add (E)SEARCH result for ALL UNDELETED query
40c45e 3229         if (!empty($this->icache['undeleted_idx'])
c321a9 3230             && $this->icache['undeleted_idx']->get_parameters('MAILBOX') == $folder
40c45e 3231         ) {
A 3232             $data['UNDELETED'] = $this->icache['undeleted_idx'];
80152b 3233         }
A 3234
3235         return $data;
a5a4bf 3236     }
A 3237
3238
3239     /**
25e6a0 3240      * Returns extended information about the folder
A 3241      *
c321a9 3242      * @param string $folder Folder name
25e6a0 3243      *
A 3244      * @return array Data
3245      */
c321a9 3246     public function folder_info($folder)
25e6a0 3247     {
c321a9 3248         if ($this->icache['options'] && $this->icache['options']['name'] == $folder) {
2ce8e5 3249             return $this->icache['options'];
A 3250         }
3251
862de1 3252         // get cached metadata
T 3253         $cache_key = 'mailboxes.folder-info.' . $folder;
3254         $cached = $this->get_cache($cache_key);
3255
b866a2 3256         if (is_array($cached)) {
862de1 3257             return $cached;
b866a2 3258         }
862de1 3259
25e6a0 3260         $acl       = $this->get_capability('ACL');
A 3261         $namespace = $this->get_namespace();
3262         $options   = array();
3263
3264         // check if the folder is a namespace prefix
3265         if (!empty($namespace)) {
c321a9 3266             $mbox = $folder . $this->delimiter;
25e6a0 3267             foreach ($namespace as $ns) {
68070e 3268                 if (!empty($ns)) {
A 3269                     foreach ($ns as $item) {
3270                         if ($item[0] === $mbox) {
3271                             $options['is_root'] = true;
922016 3272                             break 2;
68070e 3273                         }
25e6a0 3274                     }
A 3275                 }
3276             }
3277         }
922016 3278         // check if the folder is other user virtual-root
A 3279         if (!$options['is_root'] && !empty($namespace) && !empty($namespace['other'])) {
c321a9 3280             $parts = explode($this->delimiter, $folder);
922016 3281             if (count($parts) == 2) {
A 3282                 $mbox = $parts[0] . $this->delimiter;
3283                 foreach ($namespace['other'] as $item) {
3284                     if ($item[0] === $mbox) {
3285                         $options['is_root'] = true;
3286                         break;
3287                     }
3288                 }
3289             }
3290         }
25e6a0 3291
c321a9 3292         $options['name']       = $folder;
T 3293         $options['attributes'] = $this->folder_attributes($folder, true);
3294         $options['namespace']  = $this->folder_namespace($folder);
3295         $options['special']    = in_array($folder, $this->default_folders);
25e6a0 3296
b866a2 3297         // Set 'noselect' flag
aa07b2 3298         if (is_array($options['attributes'])) {
A 3299             foreach ($options['attributes'] as $attrib) {
3300                 $attrib = strtolower($attrib);
3301                 if ($attrib == '\noselect' || $attrib == '\nonexistent') {
25e6a0 3302                     $options['noselect'] = true;
A 3303                 }
3304             }
3305         }
3306         else {
3307             $options['noselect'] = true;
3308         }
3309
b866a2 3310         // Get folder rights (MYRIGHTS)
4697c2 3311         if ($acl && ($rights = $this->my_rights($folder))) {
AM 3312             $options['rights'] = $rights;
b866a2 3313         }
AM 3314
3315         // Set 'norename' flag
25e6a0 3316         if (!empty($options['rights'])) {
30f505 3317             $options['norename'] = !in_array('x', $options['rights']) && !in_array('d', $options['rights']);
A 3318
25e6a0 3319             if (!$options['noselect']) {
A 3320                 $options['noselect'] = !in_array('r', $options['rights']);
3321             }
3322         }
1cd362 3323         else {
A 3324             $options['norename'] = $options['is_root'] || $options['namespace'] != 'personal';
3325         }
25e6a0 3326
862de1 3327         // update caches
2ce8e5 3328         $this->icache['options'] = $options;
862de1 3329         $this->update_cache($cache_key, $options);
2ce8e5 3330
25e6a0 3331         return $options;
A 3332     }
3333
3334
3335     /**
609d39 3336      * Synchronizes messages cache.
A 3337      *
c321a9 3338      * @param string $folder Folder name
609d39 3339      */
c321a9 3340     public function folder_sync($folder)
609d39 3341     {
A 3342         if ($mcache = $this->get_mcache_engine()) {
c321a9 3343             $mcache->synchronize($folder);
609d39 3344         }
A 3345     }
3346
3347
3348     /**
103ddc 3349      * Get message header names for rcube_imap_generic::fetchHeader(s)
A 3350      *
3351      * @return string Space-separated list of header names
3352      */
c321a9 3353     protected function get_fetch_headers()
103ddc 3354     {
c321a9 3355         if (!empty($this->options['fetch_headers'])) {
T 3356             $headers = explode(' ', $this->options['fetch_headers']);
3357             $headers = array_map('strtoupper', $headers);
3358         }
3359         else {
3360             $headers = array();
3361         }
103ddc 3362
c321a9 3363         if ($this->messages_caching || $this->options['all_headers']) {
103ddc 3364             $headers = array_merge($headers, $this->all_headers);
c321a9 3365         }
103ddc 3366
A 3367         return implode(' ', array_unique($headers));
3368     }
3369
3370
8b6eff 3371     /* -----------------------------------------
A 3372      *   ACL and METADATA/ANNOTATEMORE methods
3373      * ----------------------------------------*/
3374
3375     /**
c321a9 3376      * Changes the ACL on the specified folder (SETACL)
8b6eff 3377      *
c321a9 3378      * @param string $folder  Folder name
8b6eff 3379      * @param string $user    User name
A 3380      * @param string $acl     ACL string
3381      *
3382      * @return boolean True on success, False on failure
3383      * @since 0.5-beta
3384      */
c321a9 3385     public function set_acl($folder, $user, $acl)
8b6eff 3386     {
c321a9 3387         if (!$this->get_capability('ACL')) {
T 3388             return false;
3389         }
8b6eff 3390
c321a9 3391         if (!$this->check_connection()) {
T 3392             return false;
3393         }
862de1 3394
T 3395         $this->clear_cache('mailboxes.folder-info.' . $folder);
c321a9 3396
T 3397         return $this->conn->setACL($folder, $user, $acl);
8b6eff 3398     }
A 3399
3400
3401     /**
3402      * Removes any <identifier,rights> pair for the
3403      * specified user from the ACL for the specified
c321a9 3404      * folder (DELETEACL)
8b6eff 3405      *
c321a9 3406      * @param string $folder  Folder name
8b6eff 3407      * @param string $user    User name
A 3408      *
3409      * @return boolean True on success, False on failure
3410      * @since 0.5-beta
3411      */
c321a9 3412     public function delete_acl($folder, $user)
8b6eff 3413     {
c321a9 3414         if (!$this->get_capability('ACL')) {
T 3415             return false;
3416         }
8b6eff 3417
c321a9 3418         if (!$this->check_connection()) {
T 3419             return false;
3420         }
3421
3422         return $this->conn->deleteACL($folder, $user);
8b6eff 3423     }
A 3424
3425
3426     /**
c321a9 3427      * Returns the access control list for folder (GETACL)
8b6eff 3428      *
c321a9 3429      * @param string $folder Folder name
8b6eff 3430      *
A 3431      * @return array User-rights array on success, NULL on error
3432      * @since 0.5-beta
3433      */
c321a9 3434     public function get_acl($folder)
8b6eff 3435     {
c321a9 3436         if (!$this->get_capability('ACL')) {
T 3437             return null;
3438         }
8b6eff 3439
c321a9 3440         if (!$this->check_connection()) {
T 3441             return null;
3442         }
3443
3444         return $this->conn->getACL($folder);
8b6eff 3445     }
A 3446
3447
3448     /**
3449      * Returns information about what rights can be granted to the
c321a9 3450      * user (identifier) in the ACL for the folder (LISTRIGHTS)
8b6eff 3451      *
c321a9 3452      * @param string $folder  Folder name
8b6eff 3453      * @param string $user    User name
A 3454      *
3455      * @return array List of user rights
3456      * @since 0.5-beta
3457      */
c321a9 3458     public function list_rights($folder, $user)
8b6eff 3459     {
c321a9 3460         if (!$this->get_capability('ACL')) {
T 3461             return null;
3462         }
8b6eff 3463
c321a9 3464         if (!$this->check_connection()) {
T 3465             return null;
3466         }
3467
3468         return $this->conn->listRights($folder, $user);
8b6eff 3469     }
A 3470
3471
3472     /**
3473      * Returns the set of rights that the current user has to
c321a9 3474      * folder (MYRIGHTS)
8b6eff 3475      *
c321a9 3476      * @param string $folder Folder name
8b6eff 3477      *
A 3478      * @return array MYRIGHTS response on success, NULL on error
3479      * @since 0.5-beta
3480      */
c321a9 3481     public function my_rights($folder)
8b6eff 3482     {
c321a9 3483         if (!$this->get_capability('ACL')) {
T 3484             return null;
3485         }
8b6eff 3486
c321a9 3487         if (!$this->check_connection()) {
T 3488             return null;
3489         }
3490
3491         return $this->conn->myRights($folder);
8b6eff 3492     }
A 3493
3494
3495     /**
3496      * Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
3497      *
c321a9 3498      * @param string $folder  Folder name (empty for server metadata)
8b6eff 3499      * @param array  $entries Entry-value array (use NULL value as NIL)
A 3500      *
3501      * @return boolean True on success, False on failure
3502      * @since 0.5-beta
3503      */
c321a9 3504     public function set_metadata($folder, $entries)
8b6eff 3505     {
c321a9 3506         if (!$this->check_connection()) {
T 3507             return false;
3508         }
3509
938925 3510         $this->clear_cache('mailboxes.metadata.', true);
862de1 3511
448409 3512         if ($this->get_capability('METADATA') ||
c321a9 3513             (!strlen($folder) && $this->get_capability('METADATA-SERVER'))
8b6eff 3514         ) {
c321a9 3515             return $this->conn->setMetadata($folder, $entries);
8b6eff 3516         }
A 3517         else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
e22740 3518             foreach ((array)$entries as $entry => $value) {
8b6eff 3519                 list($ent, $attr) = $this->md2annotate($entry);
A 3520                 $entries[$entry] = array($ent, $attr, $value);
3521             }
c321a9 3522             return $this->conn->setAnnotation($folder, $entries);
8b6eff 3523         }
A 3524
3525         return false;
3526     }
3527
3528
3529     /**
3530      * Unsets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
3531      *
c321a9 3532      * @param string $folder  Folder name (empty for server metadata)
8b6eff 3533      * @param array  $entries Entry names array
A 3534      *
3535      * @return boolean True on success, False on failure
3536      * @since 0.5-beta
3537      */
c321a9 3538     public function delete_metadata($folder, $entries)
8b6eff 3539     {
c321a9 3540         if (!$this->check_connection()) {
T 3541             return false;
3542         }
862de1 3543
938925 3544         $this->clear_cache('mailboxes.metadata.', true);
c321a9 3545
938925 3546         if ($this->get_capability('METADATA') ||
c321a9 3547             (!strlen($folder) && $this->get_capability('METADATA-SERVER'))
8b6eff 3548         ) {
c321a9 3549             return $this->conn->deleteMetadata($folder, $entries);
8b6eff 3550         }
A 3551         else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
e22740 3552             foreach ((array)$entries as $idx => $entry) {
8b6eff 3553                 list($ent, $attr) = $this->md2annotate($entry);
A 3554                 $entries[$idx] = array($ent, $attr, NULL);
3555             }
c321a9 3556             return $this->conn->setAnnotation($folder, $entries);
8b6eff 3557         }
A 3558
3559         return false;
3560     }
3561
3562
3563     /**
3564      * Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION)
3565      *
c321a9 3566      * @param string $folder  Folder name (empty for server metadata)
8b6eff 3567      * @param array  $entries Entries
A 3568      * @param array  $options Command options (with MAXSIZE and DEPTH keys)
3569      *
3570      * @return array Metadata entry-value hash array on success, NULL on error
3571      * @since 0.5-beta
3572      */
c321a9 3573     public function get_metadata($folder, $entries, $options=array())
8b6eff 3574     {
4f7ab0 3575         $entries = (array)$entries;
TB 3576
938925 3577         // create cache key
AM 3578         // @TODO: this is the simplest solution, but we do the same with folders list
3579         //        maybe we should store data per-entry and merge on request
3580         sort($options);
3581         sort($entries);
3582         $cache_key = 'mailboxes.metadata.' . $folder;
3583         $cache_key .= '.' . md5(serialize($options).serialize($entries));
3584
3585         // get cached data
3586         $cached_data = $this->get_cache($cache_key);
3587
3588         if (is_array($cached_data)) {
3589             return $cached_data;
4f7ab0 3590         }
TB 3591
938925 3592         if (!$this->check_connection()) {
AM 3593             return null;
4f7ab0 3594         }
862de1 3595
c321a9 3596         if ($this->get_capability('METADATA') ||
T 3597             (!strlen($folder) && $this->get_capability('METADATA-SERVER'))
8b6eff 3598         ) {
862de1 3599             $res = $this->conn->getMetadata($folder, $entries, $options);
8b6eff 3600         }
A 3601         else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
3602             $queries = array();
3603             $res     = array();
3604
3605             // Convert entry names
4f7ab0 3606             foreach ($entries as $entry) {
8b6eff 3607                 list($ent, $attr) = $this->md2annotate($entry);
A 3608                 $queries[$attr][] = $ent;
3609             }
3610
3611             // @TODO: Honor MAXSIZE and DEPTH options
c321a9 3612             foreach ($queries as $attrib => $entry) {
T 3613                 if ($result = $this->conn->getAnnotation($folder, $entry, $attrib)) {
00d424 3614                     $res = array_merge_recursive($res, $result);
c321a9 3615                 }
T 3616             }
938925 3617         }
8b6eff 3618
938925 3619         if (isset($res)) {
AM 3620             $this->update_cache($cache_key, $res);
8b6eff 3621             return $res;
A 3622         }
3623
c321a9 3624         return null;
4f7ab0 3625     }
TB 3626
3627
3628     /**
8b6eff 3629      * Converts the METADATA extension entry name into the correct
A 3630      * entry-attrib names for older ANNOTATEMORE version.
3631      *
e22740 3632      * @param string $entry Entry name
8b6eff 3633      *
A 3634      * @return array Entry-attribute list, NULL if not supported (?)
3635      */
c321a9 3636     protected function md2annotate($entry)
8b6eff 3637     {
A 3638         if (substr($entry, 0, 7) == '/shared') {
3639             return array(substr($entry, 7), 'value.shared');
3640         }
f295d2 3641         else if (substr($entry, 0, 8) == '/private') {
8b6eff 3642             return array(substr($entry, 8), 'value.priv');
A 3643         }
3644
3645         // @TODO: log error
c321a9 3646         return null;
8b6eff 3647     }
A 3648
3649
59c216 3650     /* --------------------------------
A 3651      *   internal caching methods
3652      * --------------------------------*/
3653
3654     /**
5cf5ee 3655      * Enable or disable indexes caching
5c461b 3656      *
be98df 3657      * @param string $type Cache type (@see rcube::get_cache)
59c216 3658      */
c321a9 3659     public function set_caching($type)
59c216 3660     {
5cf5ee 3661         if ($type) {
682819 3662             $this->caching = $type;
5cf5ee 3663         }
A 3664         else {
c321a9 3665             if ($this->cache) {
5cf5ee 3666                 $this->cache->close();
c321a9 3667             }
80152b 3668             $this->cache   = null;
341d96 3669             $this->caching = false;
5cf5ee 3670         }
59c216 3671     }
A 3672
341d96 3673     /**
A 3674      * Getter for IMAP cache object
3675      */
c321a9 3676     protected function get_cache_engine()
341d96 3677     {
A 3678         if ($this->caching && !$this->cache) {
be98df 3679             $rcube = rcube::get_instance();
6a8b4c 3680             $ttl = $rcube->config->get('message_cache_lifetime', '10d');
be98df 3681             $this->cache = $rcube->get_cache('IMAP', $this->caching, $ttl);
341d96 3682         }
A 3683
3684         return $this->cache;
3685     }
29983c 3686
59c216 3687     /**
5c461b 3688      * Returns cached value
A 3689      *
3690      * @param string $key Cache key
c321a9 3691      *
5c461b 3692      * @return mixed
59c216 3693      */
c321a9 3694     public function get_cache($key)
59c216 3695     {
341d96 3696         if ($cache = $this->get_cache_engine()) {
A 3697             return $cache->get($key);
59c216 3698         }
A 3699     }
29983c 3700
59c216 3701     /**
5c461b 3702      * Update cache
A 3703      *
3704      * @param string $key  Cache key
3705      * @param mixed  $data Data
59c216 3706      */
37cec4 3707     public function update_cache($key, $data)
59c216 3708     {
341d96 3709         if ($cache = $this->get_cache_engine()) {
A 3710             $cache->set($key, $data);
59c216 3711         }
A 3712     }
3713
3714     /**
5c461b 3715      * Clears the cache.
A 3716      *
ccc059 3717      * @param string  $key         Cache key name or pattern
A 3718      * @param boolean $prefix_mode Enable it to clear all keys starting
3719      *                             with prefix specified in $key
59c216 3720      */
c321a9 3721     public function clear_cache($key = null, $prefix_mode = false)
59c216 3722     {
341d96 3723         if ($cache = $this->get_cache_engine()) {
A 3724             $cache->remove($key, $prefix_mode);
59c216 3725         }
A 3726     }
3727
fec2d8 3728     /**
T 3729      * Delete outdated cache entries
3730      */
3731     public function expunge_cache()
3732     {
3733         if ($this->mcache) {
be98df 3734             $ttl = rcube::get_instance()->config->get('message_cache_lifetime', '10d');
fec2d8 3735             $this->mcache->expunge($ttl);
T 3736         }
3737
0c2596 3738         if ($this->cache) {
fec2d8 3739             $this->cache->expunge();
0c2596 3740         }
fec2d8 3741     }
T 3742
59c216 3743
A 3744     /* --------------------------------
3745      *   message caching methods
3746      * --------------------------------*/
5cf5ee 3747
A 3748     /**
3749      * Enable or disable messages caching
3750      *
3751      * @param boolean $set Flag
3752      */
c321a9 3753     public function set_messages_caching($set)
5cf5ee 3754     {
80152b 3755         if ($set) {
5cf5ee 3756             $this->messages_caching = true;
A 3757         }
3758         else {
c321a9 3759             if ($this->mcache) {
80152b 3760                 $this->mcache->close();
c321a9 3761             }
80152b 3762             $this->mcache = null;
5cf5ee 3763             $this->messages_caching = false;
A 3764         }
3765     }
c43517 3766
1c4f23 3767
59c216 3768     /**
80152b 3769      * Getter for messages cache object
A 3770      */
c321a9 3771     protected function get_mcache_engine()
80152b 3772     {
A 3773         if ($this->messages_caching && !$this->mcache) {
be98df 3774             $rcube = rcube::get_instance();
6bb44a 3775             if (($dbh = $rcube->get_dbh()) && ($userid = $rcube->get_user_id())) {
80152b 3776                 $this->mcache = new rcube_imap_cache(
6bb44a 3777                     $dbh, $this, $userid, $this->options['skip_deleted']);
80152b 3778             }
A 3779         }
3780
3781         return $this->mcache;
3782     }
3783
1c4f23 3784
80152b 3785     /**
A 3786      * Clears the messages cache.
59c216 3787      *
c321a9 3788      * @param string $folder Folder name
80152b 3789      * @param array  $uids    Optional message UIDs to remove from cache
59c216 3790      */
c321a9 3791     protected function clear_message_cache($folder = null, $uids = null)
59c216 3792     {
80152b 3793         if ($mcache = $this->get_mcache_engine()) {
c321a9 3794             $mcache->clear($folder, $uids);
59c216 3795         }
A 3796     }
3797
3798
3799     /* --------------------------------
c321a9 3800      *         protected methods
59c216 3801      * --------------------------------*/
A 3802
3803     /**
3804      * Validate the given input and save to local properties
5c461b 3805      *
A 3806      * @param string $sort_field Sort column
3807      * @param string $sort_order Sort order
59c216 3808      */
c321a9 3809     protected function set_sort_order($sort_field, $sort_order)
59c216 3810     {
c321a9 3811         if ($sort_field != null) {
59c216 3812             $this->sort_field = asciiwords($sort_field);
c321a9 3813         }
T 3814         if ($sort_order != null) {
59c216 3815             $this->sort_order = strtoupper($sort_order) == 'DESC' ? 'DESC' : 'ASC';
c321a9 3816         }
59c216 3817     }
A 3818
29983c 3819
59c216 3820     /**
c321a9 3821      * Sort folders first by default folders and then in alphabethical order
5c461b 3822      *
c321a9 3823      * @param array $a_folders Folders list
59c216 3824      */
c321a9 3825     protected function sort_folder_list($a_folders)
59c216 3826     {
A 3827         $a_out = $a_defaults = $folders = array();
3828
3829         $delimiter = $this->get_hierarchy_delimiter();
3830
3831         // find default folders and skip folders starting with '.'
3832         foreach ($a_folders as $i => $folder) {
c321a9 3833             if ($folder[0] == '.') {
59c216 3834                 continue;
c321a9 3835             }
59c216 3836
c321a9 3837             if (($p = array_search($folder, $this->default_folders)) !== false && !$a_defaults[$p]) {
59c216 3838                 $a_defaults[$p] = $folder;
c321a9 3839             }
T 3840             else {
0c2596 3841                 $folders[$folder] = rcube_charset::convert($folder, 'UTF7-IMAP');
c321a9 3842             }
59c216 3843         }
A 3844
3845         // sort folders and place defaults on the top
3846         asort($folders, SORT_LOCALE_STRING);
3847         ksort($a_defaults);
3848         $folders = array_merge($a_defaults, array_keys($folders));
3849
c43517 3850         // finally we must rebuild the list to move
59c216 3851         // subfolders of default folders to their place...
A 3852         // ...also do this for the rest of folders because
3853         // asort() is not properly sorting case sensitive names
3854         while (list($key, $folder) = each($folders)) {
c43517 3855             // set the type of folder name variable (#1485527)
59c216 3856             $a_out[] = (string) $folder;
A 3857             unset($folders[$key]);
c321a9 3858             $this->rsort($folder, $delimiter, $folders, $a_out);
59c216 3859         }
A 3860
3861         return $a_out;
3862     }
3863
3864
3865     /**
c321a9 3866      * Recursive method for sorting folders
59c216 3867      */
c321a9 3868     protected function rsort($folder, $delimiter, &$list, &$out)
59c216 3869     {
A 3870         while (list($key, $name) = each($list)) {
413df0 3871             if (strpos($name, $folder.$delimiter) === 0) {
AM 3872                 // set the type of folder name variable (#1485527)
3873                 $out[] = (string) $name;
3874                 unset($list[$key]);
3875                 $this->rsort($name, $delimiter, $list, $out);
3876             }
59c216 3877         }
c43517 3878         reset($list);
59c216 3879     }
A 3880
3881
3882     /**
80152b 3883      * Find UID of the specified message sequence ID
A 3884      *
3885      * @param int    $id       Message (sequence) ID
c321a9 3886      * @param string $folder   Folder name
d08333 3887      *
29983c 3888      * @return int Message UID
59c216 3889      */
c321a9 3890     public function id2uid($id, $folder = null)
59c216 3891     {
c321a9 3892         if (!strlen($folder)) {
T 3893             $folder = $this->folder;
d08333 3894         }
59c216 3895
c321a9 3896         if ($uid = array_search($id, (array)$this->uid_id_map[$folder])) {
59c216 3897             return $uid;
d08333 3898         }
59c216 3899
c321a9 3900         if (!$this->check_connection()) {
T 3901             return null;
3902         }
29983c 3903
c321a9 3904         $uid = $this->conn->ID2UID($folder, $id);
T 3905
3906         $this->uid_id_map[$folder][$uid] = $id;
c43517 3907
59c216 3908         return $uid;
A 3909     }
3910
3911
3912     /**
c321a9 3913      * Subscribe/unsubscribe a list of folders and update local cache
59c216 3914      */
c321a9 3915     protected function change_subscription($folders, $mode)
59c216 3916     {
A 3917         $updated = false;
3918
c321a9 3919         if (!empty($folders)) {
T 3920             if (!$this->check_connection()) {
3921                 return false;
59c216 3922             }
A 3923
c321a9 3924             foreach ((array)$folders as $i => $folder) {
T 3925                 $folders[$i] = $folder;
3926
3927                 if ($mode == 'subscribe') {
3928                     $updated = $this->conn->subscribe($folder);
3929                 }
3930                 else if ($mode == 'unsubscribe') {
3931                     $updated = $this->conn->unsubscribe($folder);
3932                 }
3933             }
3934         }
3935
3936         // clear cached folders list(s)
59c216 3937         if ($updated) {
ccc059 3938             $this->clear_cache('mailboxes', true);
59c216 3939         }
A 3940
3941         return $updated;
3942     }
3943
3944
3945     /**
c321a9 3946      * Increde/decrese messagecount for a specific folder
59c216 3947      */
c321a9 3948     protected function set_messagecount($folder, $mode, $increment)
59c216 3949     {
c321a9 3950         if (!is_numeric($increment)) {
59c216 3951             return false;
c321a9 3952         }
T 3953
3954         $mode = strtoupper($mode);
3955         $a_folder_cache = $this->get_cache('messagecount');
3956
3957         if (!is_array($a_folder_cache[$folder]) || !isset($a_folder_cache[$folder][$mode])) {
3958             return false;
3959         }
c43517 3960
59c216 3961         // add incremental value to messagecount
c321a9 3962         $a_folder_cache[$folder][$mode] += $increment;
c43517 3963
59c216 3964         // there's something wrong, delete from cache
c321a9 3965         if ($a_folder_cache[$folder][$mode] < 0) {
T 3966             unset($a_folder_cache[$folder][$mode]);
3967         }
59c216 3968
A 3969         // write back to cache
c321a9 3970         $this->update_cache('messagecount', $a_folder_cache);
c43517 3971
59c216 3972         return true;
A 3973     }
3974
3975
3976     /**
c321a9 3977      * Remove messagecount of a specific folder from cache
59c216 3978      */
c321a9 3979     protected function clear_messagecount($folder, $mode=null)
59c216 3980     {
c321a9 3981         $a_folder_cache = $this->get_cache('messagecount');
59c216 3982
c321a9 3983         if (is_array($a_folder_cache[$folder])) {
c309cd 3984             if ($mode) {
c321a9 3985                 unset($a_folder_cache[$folder][$mode]);
c309cd 3986             }
A 3987             else {
c321a9 3988                 unset($a_folder_cache[$folder]);
c309cd 3989             }
c321a9 3990             $this->update_cache('messagecount', $a_folder_cache);
59c216 3991         }
6c68cb 3992     }
A 3993
3994
3995     /**
7ac533 3996      * Converts date string/object into IMAP date/time format
AM 3997      */
3998     protected function date_format($date)
3999     {
4000         if (empty($date)) {
4001             return null;
4002         }
4003
4004         if (!is_object($date) || !is_a($date, 'DateTime')) {
4005             try {
4006                 $timestamp = rcube_utils::strtotime($date);
4007                 $date      = new DateTime("@".$timestamp);
4008             }
4009             catch (Exception $e) {
4010                 return null;
4011             }
4012         }
4013
4014         return $date->format('d-M-Y H:i:s O');
4015     }
4016
4017
4018     /**
7f1da4 4019      * This is our own debug handler for the IMAP connection
A 4020      * @access public
4021      */
4022     public function debug_handler(&$imap, $message)
4023     {
be98df 4024         rcube::write_log('imap', $message);
7f1da4 4025     }
A 4026
b91f04 4027
T 4028     /**
4029      * Deprecated methods (to be removed)
4030      */
4031
4032     public function decode_address_list($input, $max = null, $decode = true, $fallback = null)
4033     {
4034         return rcube_mime::decode_address_list($input, $max, $decode, $fallback);
4035     }
4036
4037     public function decode_header($input, $fallback = null)
4038     {
4039         return rcube_mime::decode_mime_string((string)$input, $fallback);
4040     }
4041
4042     public static function decode_mime_string($input, $fallback = null)
4043     {
4044         return rcube_mime::decode_mime_string($input, $fallback);
4045     }
4046
4047     public function mime_decode($input, $encoding = '7bit')
4048     {
4049         return rcube_mime::decode($input, $encoding);
4050     }
4051
4052     public static function explode_header_string($separator, $str, $remove_comments = false)
4053     {
4054         return rcube_mime::explode_header_string($separator, $str, $remove_comments);
4055     }
4056
4057     public function select_mailbox($mailbox)
4058     {
4059         // do nothing
4060     }
4061
4062     public function set_mailbox($folder)
4063     {
4064         $this->set_folder($folder);
4065     }
4066
4067     public function get_mailbox_name()
4068     {
4069         return $this->get_folder();
4070     }
4071
4072     public function list_headers($folder='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
4073     {
4074         return $this->list_messages($folder, $page, $sort_field, $sort_order, $slice);
4075     }
4076
1d5b73 4077     public function get_headers($uid, $folder = null, $force = false)
TB 4078     {
4079         return $this->get_message_headers($uid, $folder, $force);
4080     }
4081
b91f04 4082     public function mailbox_status($folder = null)
T 4083     {
4084         return $this->folder_status($folder);
4085     }
4086
4087     public function message_index($folder = '', $sort_field = NULL, $sort_order = NULL)
4088     {
4089         return $this->index($folder, $sort_field, $sort_order);
4090     }
4091
4092     public function message_index_direct($folder, $sort_field = null, $sort_order = null, $skip_cache = true)
4093     {
4094         return $this->index_direct($folder, $sort_field, $sort_order, $skip_cache);
4095     }
4096
4097     public function list_mailboxes($root='', $name='*', $filter=null, $rights=null, $skip_sort=false)
4098     {
4099         return $this->list_folders_subscribed($root, $name, $filter, $rights, $skip_sort);
4100     }
4101
4102     public function list_unsubscribed($root='', $name='*', $filter=null, $rights=null, $skip_sort=false)
4103     {
4104         return $this->list_folders($root, $name, $filter, $rights, $skip_sort);
4105     }
4106
4107     public function get_mailbox_size($folder)
4108     {
4109         return $this->folder_size($folder);
4110     }
4111
4112     public function create_mailbox($folder, $subscribe=false)
4113     {
4114         return $this->create_folder($folder, $subscribe);
4115     }
4116
4117     public function rename_mailbox($folder, $new_name)
4118     {
4119         return $this->rename_folder($folder, $new_name);
4120     }
4121
4122     function delete_mailbox($folder)
4123     {
4124         return $this->delete_folder($folder);
4125     }
4126
575d34 4127     function clear_mailbox($folder = null)
AM 4128     {
4129         return $this->clear_folder($folder);
4130     }
4131
b91f04 4132     public function mailbox_exists($folder, $subscription=false)
T 4133     {
4134         return $this->folder_exists($folder, $subscription);
4135     }
4136
4137     public function mailbox_namespace($folder)
4138     {
4139         return $this->folder_namespace($folder);
4140     }
4141
4142     public function mod_mailbox($folder, $mode = 'out')
4143     {
4144         return $this->mod_folder($folder, $mode);
4145     }
4146
4147     public function mailbox_attributes($folder, $force=false)
4148     {
4149         return $this->folder_attributes($folder, $force);
4150     }
4151
4152     public function mailbox_data($folder)
4153     {
4154         return $this->folder_data($folder);
4155     }
4156
4157     public function mailbox_info($folder)
4158     {
4159         return $this->folder_info($folder);
4160     }
4161
4162     public function mailbox_sync($folder)
4163     {
4164         return $this->folder_sync($folder);
4165     }
4166
4167     public function expunge($folder='', $clear_cache=true)
4168     {
4169         return $this->expunge_folder($folder, $clear_cache);
4170     }
4171
4172 }