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