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