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