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