Aleksander Machniak
2015-12-22 9fbabc466870bdb70d8f806e366d68f0d37b7a01
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();
AM 3298
3299         if (isset($this->icache['special-use'])) {
3300             return array_merge($result, $this->icache['special-use']);
3301         }
3302
3303         if (!$forced || !$this->get_capability('SPECIAL-USE')) {
3304             return $result;
3305         }
3306
3307         if (!$this->check_connection()) {
3308             return $result;
3309         }
3310
3311         $types   = array_map(function($value) { return "\\" . ucfirst($value); }, rcube_storage::$folder_types);
3312         $special = array();
3313
3314         // request \Subscribed flag in LIST response as performance improvement for folder_exists()
3315         $folders = $this->conn->listMailboxes('', '*', array('SUBSCRIBED'), array('SPECIAL-USE'));
3316
e0492d 3317         if (!empty($folders)) {
AM 3318             foreach ($folders as $folder) {
3319                 if ($flags = $this->conn->data['LIST'][$folder]) {
3320                     foreach ($types as $type) {
3321                         if (in_array($type, $flags)) {
3322                             $type           = strtolower(substr($type, 1));
3323                             $special[$type] = $folder;
3324                         }
dc0b50 3325                     }
AM 3326                 }
c321a9 3327             }
59c216 3328         }
dc0b50 3329
AM 3330         $this->icache['special-use'] = $special;
3331         unset($this->icache['special-folders']);
3332
3333         return array_merge($result, $special);
3334     }
3335
3336
3337     /**
3338      * Set special folder associations stored in storage backend
3339      */
3340     public function set_special_folders($specials)
3341     {
3342         if (!$this->get_capability('SPECIAL-USE') || !$this->get_capability('METADATA')) {
3343             return false;
3344         }
3345
3346         if (!$this->check_connection()) {
3347             return false;
3348         }
3349
3350         $folders = $this->get_special_folders(true);
3351         $old     = (array) $this->icache['special-use'];
3352
3353         foreach ($specials as $type => $folder) {
3354             if (in_array($type, rcube_storage::$folder_types)) {
3355                 $old_folder = $old[$type];
3356                 if ($old_folder !== $folder) {
3357                     // unset old-folder metadata
3358                     if ($old_folder !== null) {
3359                         $this->delete_metadata($old_folder, array('/private/specialuse'));
3360                     }
3361                     // set new folder metadata
3362                     if ($folder) {
3363                         $this->set_metadata($folder, array('/private/specialuse' => "\\" . ucfirst($type)));
3364                     }
3365                 }
3366             }
3367         }
3368
3369         $this->icache['special-use'] = $specials;
3370         unset($this->icache['special-folders']);
3371
3372         return true;
59c216 3373     }
A 3374
3375
3376     /**
3377      * Checks if folder exists and is subscribed
3378      *
c321a9 3379      * @param string   $folder       Folder name
5c461b 3380      * @param boolean  $subscription Enable subscription checking
d08333 3381      *
59c216 3382      * @return boolean TRUE or FALSE
A 3383      */
dc0b50 3384     public function folder_exists($folder, $subscription = false)
59c216 3385     {
c321a9 3386         if ($folder == 'INBOX') {
448409 3387             return true;
d08333 3388         }
59c216 3389
dc0b50 3390         $key = $subscription ? 'subscribed' : 'existing';
ad5881 3391
c321a9 3392         if (is_array($this->icache[$key]) && in_array($folder, $this->icache[$key])) {
448409 3393             return true;
c321a9 3394         }
T 3395
3396         if (!$this->check_connection()) {
3397             return false;
3398         }
16378f 3399
448409 3400         if ($subscription) {
dc0b50 3401             // It's possible we already called LIST command, check LIST data
AM 3402             if (!empty($this->conn->data['LIST']) && !empty($this->conn->data['LIST'][$folder])
19a618 3403                 && in_array_nocase('\\Subscribed', $this->conn->data['LIST'][$folder])
dc0b50 3404             ) {
AM 3405                 $a_folders = array($folder);
3406             }
3407             else {
3408                 $a_folders = $this->conn->listSubscribed('', $folder);
3409             }
448409 3410         }
A 3411         else {
dc0b50 3412             // It's possible we already called LIST command, check LIST data
AM 3413             if (!empty($this->conn->data['LIST']) && isset($this->conn->data['LIST'][$folder])) {
3414                 $a_folders = array($folder);
3415             }
3416             else {
3417                 $a_folders = $this->conn->listMailboxes('', $folder);
3418             }
af3c04 3419         }
c43517 3420
c321a9 3421         if (is_array($a_folders) && in_array($folder, $a_folders)) {
T 3422             $this->icache[$key][] = $folder;
448409 3423             return true;
59c216 3424         }
A 3425
3426         return false;
3427     }
3428
3429
3430     /**
bbce3e 3431      * Returns the namespace where the folder is in
A 3432      *
c321a9 3433      * @param string $folder Folder name
bbce3e 3434      *
A 3435      * @return string One of 'personal', 'other' or 'shared'
3436      */
c321a9 3437     public function folder_namespace($folder)
bbce3e 3438     {
c321a9 3439         if ($folder == 'INBOX') {
bbce3e 3440             return 'personal';
A 3441         }
3442
3443         foreach ($this->namespace as $type => $namespace) {
3444             if (is_array($namespace)) {
3445                 foreach ($namespace as $ns) {
305b36 3446                     if ($len = strlen($ns[0])) {
c321a9 3447                         if (($len > 1 && $folder == substr($ns[0], 0, -1))
T 3448                             || strpos($folder, $ns[0]) === 0
bbce3e 3449                         ) {
A 3450                             return $type;
3451                         }
3452                     }
3453                 }
3454             }
3455         }
3456
3457         return 'personal';
3458     }
3459
3460
3461     /**
d08333 3462      * Modify folder name according to namespace.
A 3463      * For output it removes prefix of the personal namespace if it's possible.
3464      * For input it adds the prefix. Use it before creating a folder in root
3465      * of the folders tree.
59c216 3466      *
c321a9 3467      * @param string $folder Folder name
d08333 3468      * @param string $mode    Mode name (out/in)
A 3469      *
59c216 3470      * @return string Folder name
A 3471      */
c321a9 3472     public function mod_folder($folder, $mode = 'out')
59c216 3473     {
c321a9 3474         if (!strlen($folder)) {
T 3475             return $folder;
d08333 3476         }
59c216 3477
d08333 3478         $prefix     = $this->namespace['prefix']; // see set_env()
A 3479         $prefix_len = strlen($prefix);
3480
3481         if (!$prefix_len) {
c321a9 3482             return $folder;
d08333 3483         }
A 3484
3485         // remove prefix for output
3486         if ($mode == 'out') {
c321a9 3487             if (substr($folder, 0, $prefix_len) === $prefix) {
T 3488                 return substr($folder, $prefix_len);
00290a 3489             }
A 3490         }
d08333 3491         // add prefix for input (e.g. folder creation)
00290a 3492         else {
c321a9 3493             return $prefix . $folder;
59c216 3494         }
c43517 3495
c321a9 3496         return $folder;
59c216 3497     }
A 3498
3499
103ddc 3500     /**
aa07b2 3501      * Gets folder attributes from LIST response, e.g. \Noselect, \Noinferiors
a5a4bf 3502      *
c321a9 3503      * @param string $folder Folder name
aa07b2 3504      * @param bool   $force   Set to True if attributes should be refreshed
a5a4bf 3505      *
A 3506      * @return array Options list
3507      */
c321a9 3508     public function folder_attributes($folder, $force=false)
a5a4bf 3509     {
aa07b2 3510         // get attributes directly from LIST command
c321a9 3511         if (!empty($this->conn->data['LIST']) && is_array($this->conn->data['LIST'][$folder])) {
T 3512             $opts = $this->conn->data['LIST'][$folder];
aa07b2 3513         }
A 3514         // get cached folder attributes
3515         else if (!$force) {
3516             $opts = $this->get_cache('mailboxes.attributes');
c321a9 3517             $opts = $opts[$folder];
a5a4bf 3518         }
A 3519
aa07b2 3520         if (!is_array($opts)) {
c321a9 3521             if (!$this->check_connection()) {
T 3522                 return array();
3523             }
3524
3525             $this->conn->listMailboxes('', $folder);
3526             $opts = $this->conn->data['LIST'][$folder];
a5a4bf 3527         }
A 3528
3529         return is_array($opts) ? $opts : array();
80152b 3530     }
A 3531
3532
3533     /**
c321a9 3534      * Gets connection (and current folder) data: UIDVALIDITY, EXISTS, RECENT,
80152b 3535      * PERMANENTFLAGS, UIDNEXT, UNSEEN
A 3536      *
c321a9 3537      * @param string $folder Folder name
80152b 3538      *
A 3539      * @return array Data
3540      */
c321a9 3541     public function folder_data($folder)
80152b 3542     {
c321a9 3543         if (!strlen($folder)) {
T 3544             $folder = $this->folder !== null ? $this->folder : 'INBOX';
3545         }
80152b 3546
c321a9 3547         if ($this->conn->selected != $folder) {
T 3548             if (!$this->check_connection()) {
3549                 return array();
3550             }
3551
3552             if ($this->conn->select($folder)) {
3553                 $this->folder = $folder;
3554             }
3555             else {
609d39 3556                 return null;
c321a9 3557             }
80152b 3558         }
A 3559
3560         $data = $this->conn->data;
3561
3562         // add (E)SEARCH result for ALL UNDELETED query
40c45e 3563         if (!empty($this->icache['undeleted_idx'])
c321a9 3564             && $this->icache['undeleted_idx']->get_parameters('MAILBOX') == $folder
40c45e 3565         ) {
A 3566             $data['UNDELETED'] = $this->icache['undeleted_idx'];
80152b 3567         }
A 3568
3569         return $data;
a5a4bf 3570     }
A 3571
3572
3573     /**
25e6a0 3574      * Returns extended information about the folder
A 3575      *
c321a9 3576      * @param string $folder Folder name
25e6a0 3577      *
A 3578      * @return array Data
3579      */
c321a9 3580     public function folder_info($folder)
25e6a0 3581     {
c321a9 3582         if ($this->icache['options'] && $this->icache['options']['name'] == $folder) {
2ce8e5 3583             return $this->icache['options'];
A 3584         }
3585
862de1 3586         // get cached metadata
T 3587         $cache_key = 'mailboxes.folder-info.' . $folder;
3588         $cached = $this->get_cache($cache_key);
3589
b866a2 3590         if (is_array($cached)) {
862de1 3591             return $cached;
b866a2 3592         }
862de1 3593
25e6a0 3594         $acl       = $this->get_capability('ACL');
A 3595         $namespace = $this->get_namespace();
3596         $options   = array();
3597
3598         // check if the folder is a namespace prefix
3599         if (!empty($namespace)) {
c321a9 3600             $mbox = $folder . $this->delimiter;
25e6a0 3601             foreach ($namespace as $ns) {
68070e 3602                 if (!empty($ns)) {
A 3603                     foreach ($ns as $item) {
3604                         if ($item[0] === $mbox) {
3605                             $options['is_root'] = true;
922016 3606                             break 2;
68070e 3607                         }
25e6a0 3608                     }
A 3609                 }
3610             }
3611         }
922016 3612         // check if the folder is other user virtual-root
A 3613         if (!$options['is_root'] && !empty($namespace) && !empty($namespace['other'])) {
c321a9 3614             $parts = explode($this->delimiter, $folder);
922016 3615             if (count($parts) == 2) {
A 3616                 $mbox = $parts[0] . $this->delimiter;
3617                 foreach ($namespace['other'] as $item) {
3618                     if ($item[0] === $mbox) {
3619                         $options['is_root'] = true;
3620                         break;
3621                     }
3622                 }
3623             }
3624         }
25e6a0 3625
c321a9 3626         $options['name']       = $folder;
T 3627         $options['attributes'] = $this->folder_attributes($folder, true);
3628         $options['namespace']  = $this->folder_namespace($folder);
dc0b50 3629         $options['special']    = $this->is_special_folder($folder);
25e6a0 3630
b866a2 3631         // Set 'noselect' flag
aa07b2 3632         if (is_array($options['attributes'])) {
A 3633             foreach ($options['attributes'] as $attrib) {
3634                 $attrib = strtolower($attrib);
3635                 if ($attrib == '\noselect' || $attrib == '\nonexistent') {
25e6a0 3636                     $options['noselect'] = true;
A 3637                 }
3638             }
3639         }
3640         else {
3641             $options['noselect'] = true;
3642         }
3643
b866a2 3644         // Get folder rights (MYRIGHTS)
4697c2 3645         if ($acl && ($rights = $this->my_rights($folder))) {
AM 3646             $options['rights'] = $rights;
b866a2 3647         }
AM 3648
3649         // Set 'norename' flag
25e6a0 3650         if (!empty($options['rights'])) {
30f505 3651             $options['norename'] = !in_array('x', $options['rights']) && !in_array('d', $options['rights']);
A 3652
25e6a0 3653             if (!$options['noselect']) {
A 3654                 $options['noselect'] = !in_array('r', $options['rights']);
3655             }
3656         }
1cd362 3657         else {
A 3658             $options['norename'] = $options['is_root'] || $options['namespace'] != 'personal';
3659         }
25e6a0 3660
862de1 3661         // update caches
2ce8e5 3662         $this->icache['options'] = $options;
862de1 3663         $this->update_cache($cache_key, $options);
2ce8e5 3664
25e6a0 3665         return $options;
A 3666     }
3667
3668
3669     /**
609d39 3670      * Synchronizes messages cache.
A 3671      *
c321a9 3672      * @param string $folder Folder name
609d39 3673      */
c321a9 3674     public function folder_sync($folder)
609d39 3675     {
A 3676         if ($mcache = $this->get_mcache_engine()) {
c321a9 3677             $mcache->synchronize($folder);
609d39 3678         }
A 3679     }
3680
3681
3682     /**
103ddc 3683      * Get message header names for rcube_imap_generic::fetchHeader(s)
A 3684      *
3685      * @return string Space-separated list of header names
3686      */
c321a9 3687     protected function get_fetch_headers()
103ddc 3688     {
c321a9 3689         if (!empty($this->options['fetch_headers'])) {
T 3690             $headers = explode(' ', $this->options['fetch_headers']);
3691         }
3692         else {
3693             $headers = array();
3694         }
103ddc 3695
c321a9 3696         if ($this->messages_caching || $this->options['all_headers']) {
103ddc 3697             $headers = array_merge($headers, $this->all_headers);
c321a9 3698         }
103ddc 3699
99edf8 3700         return $headers;
103ddc 3701     }
A 3702
3703
8b6eff 3704     /* -----------------------------------------
A 3705      *   ACL and METADATA/ANNOTATEMORE methods
3706      * ----------------------------------------*/
3707
3708     /**
c321a9 3709      * Changes the ACL on the specified folder (SETACL)
8b6eff 3710      *
c321a9 3711      * @param string $folder  Folder name
8b6eff 3712      * @param string $user    User name
A 3713      * @param string $acl     ACL string
3714      *
3715      * @return boolean True on success, False on failure
3716      * @since 0.5-beta
3717      */
c321a9 3718     public function set_acl($folder, $user, $acl)
8b6eff 3719     {
c321a9 3720         if (!$this->get_capability('ACL')) {
T 3721             return false;
3722         }
8b6eff 3723
c321a9 3724         if (!$this->check_connection()) {
T 3725             return false;
3726         }
862de1 3727
T 3728         $this->clear_cache('mailboxes.folder-info.' . $folder);
c321a9 3729
T 3730         return $this->conn->setACL($folder, $user, $acl);
8b6eff 3731     }
A 3732
3733
3734     /**
3735      * Removes any <identifier,rights> pair for the
3736      * specified user from the ACL for the specified
c321a9 3737      * folder (DELETEACL)
8b6eff 3738      *
c321a9 3739      * @param string $folder  Folder name
8b6eff 3740      * @param string $user    User name
A 3741      *
3742      * @return boolean True on success, False on failure
3743      * @since 0.5-beta
3744      */
c321a9 3745     public function delete_acl($folder, $user)
8b6eff 3746     {
c321a9 3747         if (!$this->get_capability('ACL')) {
T 3748             return false;
3749         }
8b6eff 3750
c321a9 3751         if (!$this->check_connection()) {
T 3752             return false;
3753         }
3754
3755         return $this->conn->deleteACL($folder, $user);
8b6eff 3756     }
A 3757
3758
3759     /**
c321a9 3760      * Returns the access control list for folder (GETACL)
8b6eff 3761      *
c321a9 3762      * @param string $folder Folder name
8b6eff 3763      *
A 3764      * @return array User-rights array on success, NULL on error
3765      * @since 0.5-beta
3766      */
c321a9 3767     public function get_acl($folder)
8b6eff 3768     {
c321a9 3769         if (!$this->get_capability('ACL')) {
T 3770             return null;
3771         }
8b6eff 3772
c321a9 3773         if (!$this->check_connection()) {
T 3774             return null;
3775         }
3776
3777         return $this->conn->getACL($folder);
8b6eff 3778     }
A 3779
3780
3781     /**
3782      * Returns information about what rights can be granted to the
c321a9 3783      * user (identifier) in the ACL for the folder (LISTRIGHTS)
8b6eff 3784      *
c321a9 3785      * @param string $folder  Folder name
8b6eff 3786      * @param string $user    User name
A 3787      *
3788      * @return array List of user rights
3789      * @since 0.5-beta
3790      */
c321a9 3791     public function list_rights($folder, $user)
8b6eff 3792     {
c321a9 3793         if (!$this->get_capability('ACL')) {
T 3794             return null;
3795         }
8b6eff 3796
c321a9 3797         if (!$this->check_connection()) {
T 3798             return null;
3799         }
3800
3801         return $this->conn->listRights($folder, $user);
8b6eff 3802     }
A 3803
3804
3805     /**
3806      * Returns the set of rights that the current user has to
c321a9 3807      * folder (MYRIGHTS)
8b6eff 3808      *
c321a9 3809      * @param string $folder Folder name
8b6eff 3810      *
A 3811      * @return array MYRIGHTS response on success, NULL on error
3812      * @since 0.5-beta
3813      */
c321a9 3814     public function my_rights($folder)
8b6eff 3815     {
c321a9 3816         if (!$this->get_capability('ACL')) {
T 3817             return null;
3818         }
8b6eff 3819
c321a9 3820         if (!$this->check_connection()) {
T 3821             return null;
3822         }
3823
3824         return $this->conn->myRights($folder);
8b6eff 3825     }
A 3826
3827
3828     /**
3829      * Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
3830      *
c321a9 3831      * @param string $folder  Folder name (empty for server metadata)
8b6eff 3832      * @param array  $entries Entry-value array (use NULL value as NIL)
A 3833      *
3834      * @return boolean True on success, False on failure
3835      * @since 0.5-beta
3836      */
c321a9 3837     public function set_metadata($folder, $entries)
8b6eff 3838     {
c321a9 3839         if (!$this->check_connection()) {
T 3840             return false;
3841         }
3842
938925 3843         $this->clear_cache('mailboxes.metadata.', true);
862de1 3844
448409 3845         if ($this->get_capability('METADATA') ||
c321a9 3846             (!strlen($folder) && $this->get_capability('METADATA-SERVER'))
8b6eff 3847         ) {
c321a9 3848             return $this->conn->setMetadata($folder, $entries);
8b6eff 3849         }
A 3850         else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
e22740 3851             foreach ((array)$entries as $entry => $value) {
8b6eff 3852                 list($ent, $attr) = $this->md2annotate($entry);
A 3853                 $entries[$entry] = array($ent, $attr, $value);
3854             }
c321a9 3855             return $this->conn->setAnnotation($folder, $entries);
8b6eff 3856         }
A 3857
3858         return false;
3859     }
3860
3861
3862     /**
3863      * Unsets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
3864      *
c321a9 3865      * @param string $folder  Folder name (empty for server metadata)
8b6eff 3866      * @param array  $entries Entry names array
A 3867      *
3868      * @return boolean True on success, False on failure
3869      * @since 0.5-beta
3870      */
c321a9 3871     public function delete_metadata($folder, $entries)
8b6eff 3872     {
c321a9 3873         if (!$this->check_connection()) {
T 3874             return false;
3875         }
862de1 3876
938925 3877         $this->clear_cache('mailboxes.metadata.', true);
c321a9 3878
938925 3879         if ($this->get_capability('METADATA') ||
c321a9 3880             (!strlen($folder) && $this->get_capability('METADATA-SERVER'))
8b6eff 3881         ) {
c321a9 3882             return $this->conn->deleteMetadata($folder, $entries);
8b6eff 3883         }
A 3884         else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
e22740 3885             foreach ((array)$entries as $idx => $entry) {
8b6eff 3886                 list($ent, $attr) = $this->md2annotate($entry);
A 3887                 $entries[$idx] = array($ent, $attr, NULL);
3888             }
c321a9 3889             return $this->conn->setAnnotation($folder, $entries);
8b6eff 3890         }
A 3891
3892         return false;
3893     }
3894
3895
3896     /**
3897      * Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION)
3898      *
c321a9 3899      * @param string $folder  Folder name (empty for server metadata)
8b6eff 3900      * @param array  $entries Entries
A 3901      * @param array  $options Command options (with MAXSIZE and DEPTH keys)
3902      *
3903      * @return array Metadata entry-value hash array on success, NULL on error
3904      * @since 0.5-beta
3905      */
c321a9 3906     public function get_metadata($folder, $entries, $options=array())
8b6eff 3907     {
4f7ab0 3908         $entries = (array)$entries;
TB 3909
938925 3910         // create cache key
AM 3911         // @TODO: this is the simplest solution, but we do the same with folders list
3912         //        maybe we should store data per-entry and merge on request
3913         sort($options);
3914         sort($entries);
3915         $cache_key = 'mailboxes.metadata.' . $folder;
3916         $cache_key .= '.' . md5(serialize($options).serialize($entries));
3917
3918         // get cached data
3919         $cached_data = $this->get_cache($cache_key);
3920
3921         if (is_array($cached_data)) {
3922             return $cached_data;
4f7ab0 3923         }
TB 3924
938925 3925         if (!$this->check_connection()) {
AM 3926             return null;
4f7ab0 3927         }
862de1 3928
c321a9 3929         if ($this->get_capability('METADATA') ||
T 3930             (!strlen($folder) && $this->get_capability('METADATA-SERVER'))
8b6eff 3931         ) {
862de1 3932             $res = $this->conn->getMetadata($folder, $entries, $options);
8b6eff 3933         }
A 3934         else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
3935             $queries = array();
3936             $res     = array();
3937
3938             // Convert entry names
4f7ab0 3939             foreach ($entries as $entry) {
8b6eff 3940                 list($ent, $attr) = $this->md2annotate($entry);
A 3941                 $queries[$attr][] = $ent;
3942             }
3943
3944             // @TODO: Honor MAXSIZE and DEPTH options
c321a9 3945             foreach ($queries as $attrib => $entry) {
e8fc8d 3946                 $result = $this->conn->getAnnotation($folder, $entry, $attrib);
AM 3947
3948                 // an error, invalidate any previous getAnnotation() results
3949                 if (!is_array($result)) {
3950                     return null;
3951                 }
3952                 else {
fb3ccf 3953                     foreach ($result as $fldr => $data) {
TB 3954                         $res[$fldr] = array_merge((array) $res[$fldr], $data);
334bc9 3955                     }
c321a9 3956                 }
T 3957             }
938925 3958         }
8b6eff 3959
938925 3960         if (isset($res)) {
AM 3961             $this->update_cache($cache_key, $res);
8b6eff 3962             return $res;
A 3963         }
3964
c321a9 3965         return null;
4f7ab0 3966     }
TB 3967
3968
3969     /**
8b6eff 3970      * Converts the METADATA extension entry name into the correct
A 3971      * entry-attrib names for older ANNOTATEMORE version.
3972      *
e22740 3973      * @param string $entry Entry name
8b6eff 3974      *
A 3975      * @return array Entry-attribute list, NULL if not supported (?)
3976      */
c321a9 3977     protected function md2annotate($entry)
8b6eff 3978     {
A 3979         if (substr($entry, 0, 7) == '/shared') {
3980             return array(substr($entry, 7), 'value.shared');
3981         }
f295d2 3982         else if (substr($entry, 0, 8) == '/private') {
8b6eff 3983             return array(substr($entry, 8), 'value.priv');
A 3984         }
3985
3986         // @TODO: log error
c321a9 3987         return null;
8b6eff 3988     }
A 3989
3990
59c216 3991     /* --------------------------------
A 3992      *   internal caching methods
3993      * --------------------------------*/
3994
3995     /**
5cf5ee 3996      * Enable or disable indexes caching
5c461b 3997      *
be98df 3998      * @param string $type Cache type (@see rcube::get_cache)
59c216 3999      */
c321a9 4000     public function set_caching($type)
59c216 4001     {
5cf5ee 4002         if ($type) {
682819 4003             $this->caching = $type;
5cf5ee 4004         }
A 4005         else {
c321a9 4006             if ($this->cache) {
5cf5ee 4007                 $this->cache->close();
c321a9 4008             }
80152b 4009             $this->cache   = null;
341d96 4010             $this->caching = false;
5cf5ee 4011         }
59c216 4012     }
A 4013
341d96 4014     /**
A 4015      * Getter for IMAP cache object
4016      */
c321a9 4017     protected function get_cache_engine()
341d96 4018     {
A 4019         if ($this->caching && !$this->cache) {
be98df 4020             $rcube = rcube::get_instance();
60b6d7 4021             $ttl   = $rcube->config->get('imap_cache_ttl', '10d');
be98df 4022             $this->cache = $rcube->get_cache('IMAP', $this->caching, $ttl);
341d96 4023         }
A 4024
4025         return $this->cache;
4026     }
29983c 4027
59c216 4028     /**
5c461b 4029      * Returns cached value
A 4030      *
4031      * @param string $key Cache key
c321a9 4032      *
5c461b 4033      * @return mixed
59c216 4034      */
c321a9 4035     public function get_cache($key)
59c216 4036     {
341d96 4037         if ($cache = $this->get_cache_engine()) {
A 4038             return $cache->get($key);
59c216 4039         }
A 4040     }
29983c 4041
59c216 4042     /**
5c461b 4043      * Update cache
A 4044      *
4045      * @param string $key  Cache key
4046      * @param mixed  $data Data
59c216 4047      */
37cec4 4048     public function update_cache($key, $data)
59c216 4049     {
341d96 4050         if ($cache = $this->get_cache_engine()) {
A 4051             $cache->set($key, $data);
59c216 4052         }
A 4053     }
4054
4055     /**
5c461b 4056      * Clears the cache.
A 4057      *
ccc059 4058      * @param string  $key         Cache key name or pattern
A 4059      * @param boolean $prefix_mode Enable it to clear all keys starting
4060      *                             with prefix specified in $key
59c216 4061      */
c321a9 4062     public function clear_cache($key = null, $prefix_mode = false)
59c216 4063     {
341d96 4064         if ($cache = $this->get_cache_engine()) {
A 4065             $cache->remove($key, $prefix_mode);
59c216 4066         }
A 4067     }
4068
4069
4070     /* --------------------------------
4071      *   message caching methods
4072      * --------------------------------*/
5cf5ee 4073
A 4074     /**
4075      * Enable or disable messages caching
4076      *
aceb01 4077      * @param boolean $set  Flag
AM 4078      * @param int     $mode Cache mode
5cf5ee 4079      */
aceb01 4080     public function set_messages_caching($set, $mode = null)
5cf5ee 4081     {
80152b 4082         if ($set) {
5cf5ee 4083             $this->messages_caching = true;
aceb01 4084
AM 4085             if ($mode && ($cache = $this->get_mcache_engine())) {
4086                 $cache->set_mode($mode);
4087             }
5cf5ee 4088         }
A 4089         else {
c321a9 4090             if ($this->mcache) {
80152b 4091                 $this->mcache->close();
c321a9 4092             }
80152b 4093             $this->mcache = null;
5cf5ee 4094             $this->messages_caching = false;
A 4095         }
4096     }
c43517 4097
1c4f23 4098
59c216 4099     /**
80152b 4100      * Getter for messages cache object
A 4101      */
c321a9 4102     protected function get_mcache_engine()
80152b 4103     {
A 4104         if ($this->messages_caching && !$this->mcache) {
be98df 4105             $rcube = rcube::get_instance();
6bb44a 4106             if (($dbh = $rcube->get_dbh()) && ($userid = $rcube->get_user_id())) {
21601b 4107                 $ttl       = $rcube->config->get('messages_cache_ttl', '10d');
AM 4108                 $threshold = $rcube->config->get('messages_cache_threshold', 50);
80152b 4109                 $this->mcache = new rcube_imap_cache(
21601b 4110                     $dbh, $this, $userid, $this->options['skip_deleted'], $ttl, $threshold);
80152b 4111             }
A 4112         }
4113
4114         return $this->mcache;
4115     }
4116
1c4f23 4117
80152b 4118     /**
A 4119      * Clears the messages cache.
59c216 4120      *
c321a9 4121      * @param string $folder Folder name
aceb01 4122      * @param array  $uids   Optional message UIDs to remove from cache
59c216 4123      */
c321a9 4124     protected function clear_message_cache($folder = null, $uids = null)
59c216 4125     {
80152b 4126         if ($mcache = $this->get_mcache_engine()) {
c321a9 4127             $mcache->clear($folder, $uids);
59c216 4128         }
A 4129     }
4130
4131
60b6d7 4132     /**
AM 4133      * Delete outdated cache entries
4134      */
4135     function cache_gc()
4136     {
4137         rcube_imap_cache::gc();
4138     }
4139
4140
59c216 4141     /* --------------------------------
c321a9 4142      *         protected methods
59c216 4143      * --------------------------------*/
A 4144
4145     /**
4146      * Validate the given input and save to local properties
5c461b 4147      *
A 4148      * @param string $sort_field Sort column
4149      * @param string $sort_order Sort order
59c216 4150      */
c321a9 4151     protected function set_sort_order($sort_field, $sort_order)
59c216 4152     {
c321a9 4153         if ($sort_field != null) {
59c216 4154             $this->sort_field = asciiwords($sort_field);
c321a9 4155         }
T 4156         if ($sort_order != null) {
59c216 4157             $this->sort_order = strtoupper($sort_order) == 'DESC' ? 'DESC' : 'ASC';
c321a9 4158         }
59c216 4159     }
A 4160
29983c 4161
59c216 4162     /**
c321a9 4163      * Sort folders first by default folders and then in alphabethical order
5c461b 4164      *
978ff8 4165      * @param array $a_folders    Folders list
AM 4166      * @param bool  $skip_default Skip default folders handling
4167      *
4168      * @return array Sorted list
59c216 4169      */
978ff8 4170     public function sort_folder_list($a_folders, $skip_default = false)
59c216 4171     {
dc0b50 4172         $specials  = array_merge(array('INBOX'), array_values($this->get_special_folders()));
a62cc3 4173         $folders   = array();
59c216 4174
693612 4175         // convert names to UTF-8 and skip folders starting with '.'
3725cf 4176         foreach ($a_folders as $folder) {
693612 4177             if ($folder[0] != '.') {
AM 4178                 // for better performance skip encoding conversion
4179                 // if the string does not look like UTF7-IMAP
20ef29 4180                 $folders[$folder] = strpos($folder, '&') === false ? $folder : rcube_charset::convert($folder, 'UTF7-IMAP');
c321a9 4181             }
59c216 4182         }
A 4183
693612 4184         // sort folders
AM 4185         // asort($folders, SORT_LOCALE_STRING) is not properly sorting case sensitive names
4186         uasort($folders, array($this, 'sort_folder_comparator'));
59c216 4187
693612 4188         $folders = array_keys($folders);
AM 4189
4190         if ($skip_default) {
4191             return $folders;
59c216 4192         }
A 4193
081f3b 4194         // force the type of folder name variable (#1485527)
AM 4195         $folders  = array_map('strval', $folders);
354c7d 4196         $out      = array();
AM 4197
4198         // finally we must put special folders on top and rebuild the list
4199         // to move their subfolders where they belong...
693612 4200         $specials = array_unique(array_intersect($specials, $folders));
354c7d 4201         $folders  = array_merge($specials, array_diff($folders, $specials));
693612 4202
354c7d 4203         $this->sort_folder_specials(null, $folders, $specials, $out);
693612 4204
354c7d 4205         return $out;
AM 4206     }
4207
4208     /**
4209      * Recursive function to put subfolders of special folders in place
4210      */
4211     protected function sort_folder_specials($folder, &$list, &$specials, &$out)
4212     {
4213         while (list($key, $name) = each($list)) {
4214             if ($folder === null || strpos($name, $folder.$this->delimiter) === 0) {
4215                 $out[] = $name;
4216                 unset($list[$key]);
4217
4218                 if (!empty($specials) && ($found = array_search($name, $specials)) !== false) {
4219                     unset($specials[$found]);
4220                     $this->sort_folder_specials($name, $list, $specials, $out);
693612 4221                 }
AM 4222             }
4223         }
4224
354c7d 4225         reset($list);
59c216 4226     }
A 4227
4228     /**
693612 4229      * Callback for uasort() that implements correct
AM 4230      * locale-aware case-sensitive sorting
59c216 4231      */
693612 4232     protected function sort_folder_comparator($str1, $str2)
59c216 4233     {
a62cc3 4234         $path1 = explode($this->delimiter, $str1);
AM 4235         $path2 = explode($this->delimiter, $str2);
693612 4236
AM 4237         foreach ($path1 as $idx => $folder1) {
4238             $folder2 = $path2[$idx];
4239
4240             if ($folder1 === $folder2) {
4241                 continue;
413df0 4242             }
693612 4243
AM 4244             return strcoll($folder1, $folder2);
59c216 4245         }
A 4246     }
4247
4248
4249     /**
80152b 4250      * Find UID of the specified message sequence ID
A 4251      *
4252      * @param int    $id       Message (sequence) ID
c321a9 4253      * @param string $folder   Folder name
d08333 4254      *
29983c 4255      * @return int Message UID
59c216 4256      */
c321a9 4257     public function id2uid($id, $folder = null)
59c216 4258     {
c321a9 4259         if (!strlen($folder)) {
T 4260             $folder = $this->folder;
d08333 4261         }
59c216 4262
c321a9 4263         if ($uid = array_search($id, (array)$this->uid_id_map[$folder])) {
59c216 4264             return $uid;
d08333 4265         }
59c216 4266
c321a9 4267         if (!$this->check_connection()) {
T 4268             return null;
4269         }
29983c 4270
c321a9 4271         $uid = $this->conn->ID2UID($folder, $id);
T 4272
4273         $this->uid_id_map[$folder][$uid] = $id;
c43517 4274
59c216 4275         return $uid;
A 4276     }
4277
4278
4279     /**
c321a9 4280      * Subscribe/unsubscribe a list of folders and update local cache
59c216 4281      */
c321a9 4282     protected function change_subscription($folders, $mode)
59c216 4283     {
A 4284         $updated = false;
4285
c321a9 4286         if (!empty($folders)) {
T 4287             if (!$this->check_connection()) {
4288                 return false;
59c216 4289             }
A 4290
c321a9 4291             foreach ((array)$folders as $i => $folder) {
T 4292                 $folders[$i] = $folder;
4293
4294                 if ($mode == 'subscribe') {
4295                     $updated = $this->conn->subscribe($folder);
4296                 }
4297                 else if ($mode == 'unsubscribe') {
4298                     $updated = $this->conn->unsubscribe($folder);
4299                 }
4300             }
4301         }
4302
4303         // clear cached folders list(s)
59c216 4304         if ($updated) {
ccc059 4305             $this->clear_cache('mailboxes', true);
59c216 4306         }
A 4307
4308         return $updated;
4309     }
4310
4311
4312     /**
c321a9 4313      * Increde/decrese messagecount for a specific folder
59c216 4314      */
c321a9 4315     protected function set_messagecount($folder, $mode, $increment)
59c216 4316     {
c321a9 4317         if (!is_numeric($increment)) {
59c216 4318             return false;
c321a9 4319         }
T 4320
4321         $mode = strtoupper($mode);
4322         $a_folder_cache = $this->get_cache('messagecount');
4323
4324         if (!is_array($a_folder_cache[$folder]) || !isset($a_folder_cache[$folder][$mode])) {
4325             return false;
4326         }
c43517 4327
59c216 4328         // add incremental value to messagecount
c321a9 4329         $a_folder_cache[$folder][$mode] += $increment;
c43517 4330
59c216 4331         // there's something wrong, delete from cache
c321a9 4332         if ($a_folder_cache[$folder][$mode] < 0) {
T 4333             unset($a_folder_cache[$folder][$mode]);
4334         }
59c216 4335
A 4336         // write back to cache
c321a9 4337         $this->update_cache('messagecount', $a_folder_cache);
c43517 4338
59c216 4339         return true;
A 4340     }
4341
4342
4343     /**
c321a9 4344      * Remove messagecount of a specific folder from cache
59c216 4345      */
c321a9 4346     protected function clear_messagecount($folder, $mode=null)
59c216 4347     {
c321a9 4348         $a_folder_cache = $this->get_cache('messagecount');
59c216 4349
c321a9 4350         if (is_array($a_folder_cache[$folder])) {
c309cd 4351             if ($mode) {
c321a9 4352                 unset($a_folder_cache[$folder][$mode]);
c309cd 4353             }
A 4354             else {
c321a9 4355                 unset($a_folder_cache[$folder]);
c309cd 4356             }
c321a9 4357             $this->update_cache('messagecount', $a_folder_cache);
59c216 4358         }
6c68cb 4359     }
A 4360
4361
4362     /**
7ac533 4363      * Converts date string/object into IMAP date/time format
AM 4364      */
4365     protected function date_format($date)
4366     {
4367         if (empty($date)) {
4368             return null;
4369         }
4370
4371         if (!is_object($date) || !is_a($date, 'DateTime')) {
4372             try {
4373                 $timestamp = rcube_utils::strtotime($date);
4374                 $date      = new DateTime("@".$timestamp);
4375             }
4376             catch (Exception $e) {
4377                 return null;
4378             }
4379         }
4380
4381         return $date->format('d-M-Y H:i:s O');
4382     }
4383
4384
4385     /**
7f1da4 4386      * This is our own debug handler for the IMAP connection
A 4387      * @access public
4388      */
4389     public function debug_handler(&$imap, $message)
4390     {
be98df 4391         rcube::write_log('imap', $message);
7f1da4 4392     }
A 4393
b91f04 4394
T 4395     /**
4396      * Deprecated methods (to be removed)
4397      */
4398
4399     public function decode_address_list($input, $max = null, $decode = true, $fallback = null)
4400     {
4401         return rcube_mime::decode_address_list($input, $max, $decode, $fallback);
4402     }
4403
4404     public function decode_header($input, $fallback = null)
4405     {
4406         return rcube_mime::decode_mime_string((string)$input, $fallback);
4407     }
4408
4409     public static function decode_mime_string($input, $fallback = null)
4410     {
4411         return rcube_mime::decode_mime_string($input, $fallback);
4412     }
4413
4414     public function mime_decode($input, $encoding = '7bit')
4415     {
4416         return rcube_mime::decode($input, $encoding);
4417     }
4418
4419     public static function explode_header_string($separator, $str, $remove_comments = false)
4420     {
4421         return rcube_mime::explode_header_string($separator, $str, $remove_comments);
4422     }
4423
4424     public function select_mailbox($mailbox)
4425     {
4426         // do nothing
4427     }
4428
4429     public function set_mailbox($folder)
4430     {
4431         $this->set_folder($folder);
4432     }
4433
4434     public function get_mailbox_name()
4435     {
4436         return $this->get_folder();
4437     }
4438
4439     public function list_headers($folder='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
4440     {
4441         return $this->list_messages($folder, $page, $sort_field, $sort_order, $slice);
4442     }
4443
1d5b73 4444     public function get_headers($uid, $folder = null, $force = false)
TB 4445     {
4446         return $this->get_message_headers($uid, $folder, $force);
4447     }
4448
b91f04 4449     public function mailbox_status($folder = null)
T 4450     {
4451         return $this->folder_status($folder);
4452     }
4453
4454     public function message_index($folder = '', $sort_field = NULL, $sort_order = NULL)
4455     {
4456         return $this->index($folder, $sort_field, $sort_order);
4457     }
4458
603e04 4459     public function message_index_direct($folder, $sort_field = null, $sort_order = null)
b91f04 4460     {
603e04 4461         return $this->index_direct($folder, $sort_field, $sort_order);
b91f04 4462     }
T 4463
4464     public function list_mailboxes($root='', $name='*', $filter=null, $rights=null, $skip_sort=false)
4465     {
4466         return $this->list_folders_subscribed($root, $name, $filter, $rights, $skip_sort);
4467     }
4468
4469     public function list_unsubscribed($root='', $name='*', $filter=null, $rights=null, $skip_sort=false)
4470     {
4471         return $this->list_folders($root, $name, $filter, $rights, $skip_sort);
4472     }
4473
4474     public function get_mailbox_size($folder)
4475     {
4476         return $this->folder_size($folder);
4477     }
4478
4479     public function create_mailbox($folder, $subscribe=false)
4480     {
4481         return $this->create_folder($folder, $subscribe);
4482     }
4483
4484     public function rename_mailbox($folder, $new_name)
4485     {
4486         return $this->rename_folder($folder, $new_name);
4487     }
4488
4489     function delete_mailbox($folder)
4490     {
4491         return $this->delete_folder($folder);
4492     }
4493
575d34 4494     function clear_mailbox($folder = null)
AM 4495     {
4496         return $this->clear_folder($folder);
4497     }
4498
b91f04 4499     public function mailbox_exists($folder, $subscription=false)
T 4500     {
4501         return $this->folder_exists($folder, $subscription);
4502     }
4503
4504     public function mailbox_namespace($folder)
4505     {
4506         return $this->folder_namespace($folder);
4507     }
4508
4509     public function mod_mailbox($folder, $mode = 'out')
4510     {
4511         return $this->mod_folder($folder, $mode);
4512     }
4513
4514     public function mailbox_attributes($folder, $force=false)
4515     {
4516         return $this->folder_attributes($folder, $force);
4517     }
4518
4519     public function mailbox_data($folder)
4520     {
4521         return $this->folder_data($folder);
4522     }
4523
4524     public function mailbox_info($folder)
4525     {
4526         return $this->folder_info($folder);
4527     }
4528
4529     public function mailbox_sync($folder)
4530     {
4531         return $this->folder_sync($folder);
4532     }
4533
4534     public function expunge($folder='', $clear_cache=true)
4535     {
4536         return $this->expunge_folder($folder, $clear_cache);
4537     }
4538
4539 }