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