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