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