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