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