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