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