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