thomascube
2011-08-18 fbe54043cf598b19a753dc2b21a7ed558d23fd15
commit | author | age
4e17e6 1 <?php
T 2
3 /*
4  +-----------------------------------------------------------------------+
47124c 5  | program/include/rcube_imap.php                                        |
4e17e6 6  |                                                                       |
e019f2 7  | This file is part of the Roundcube Webmail client                     |
f5e7b3 8  | Copyright (C) 2005-2010, The Roundcube Dev Team                       |
30233b 9  | Licensed under the GNU GPL                                            |
4e17e6 10  |                                                                       |
T 11  | PURPOSE:                                                              |
59c216 12  |   IMAP Engine                                                         |
4e17e6 13  |                                                                       |
T 14  +-----------------------------------------------------------------------+
15  | Author: Thomas Bruederli <roundcube@gmail.com>                        |
59c216 16  | Author: Aleksander Machniak <alec@alec.pl>                            |
4e17e6 17  +-----------------------------------------------------------------------+
T 18
19  $Id$
20
21 */
22
23
15a9d1 24 /**
T 25  * Interface class for accessing an IMAP server
26  *
6d969b 27  * @package    Mail
15a9d1 28  * @author     Thomas Bruederli <roundcube@gmail.com>
c43517 29  * @author     Aleksander Machniak <alec@alec.pl>
d062db 30  * @version    2.0
15a9d1 31  */
4e17e6 32 class rcube_imap
6d969b 33 {
59c216 34     public $debug_level = 1;
A 35     public $skip_deleted = false;
36     public $page_size = 10;
37     public $list_page = 1;
38     public $threading = false;
39     public $fetch_add_headers = '';
103ddc 40     public $get_all_headers = false;
f52c93 41
5c461b 42     /**
A 43      * Instance of rcube_imap_generic
44      *
45      * @var rcube_imap_generic
46      */
47     public $conn;
48
49     /**
50      * Instance of rcube_mdb2
51      *
52      * @var rcube_mdb2
53      */
59c216 54     private $db;
5cf5ee 55
A 56     /**
57      * Instance of rcube_cache
58      *
59      * @var rcube_cache
60      */
61     private $cache;
59c216 62     private $mailbox = 'INBOX';
00290a 63     private $delimiter = NULL;
A 64     private $namespace = NULL;
59c216 65     private $sort_field = '';
A 66     private $sort_order = 'DESC';
67     private $default_charset = 'ISO-8859-1';
68     private $struct_charset = NULL;
69     private $default_folders = array('INBOX');
5cf5ee 70     private $messages_caching = false;
59c216 71     private $icache = array();
A 72     private $uid_id_map = array();
73     private $msg_headers = array();
74     public  $search_set = NULL;
75     public  $search_string = '';
76     private $search_charset = '';
77     private $search_sort_field = '';
78     private $search_threads = false;
c60978 79     private $search_sorted = false;
59c216 80     private $db_header_fields = array('idx', 'uid', 'subject', 'from', 'to', 'cc', 'date', 'size');
A 81     private $options = array('auth_method' => 'check');
82     private $host, $user, $pass, $port, $ssl;
341d96 83     private $caching = false;
103ddc 84
A 85     /**
86      * All (additional) headers used (in any way) by Roundcube
8bce65 87      * Not listed here: DATE, FROM, TO, CC, REPLY-TO, SUBJECT, CONTENT-TYPE, LIST-POST
103ddc 88      * (used for messages listing) are hardcoded in rcube_imap_generic::fetchHeaders()
A 89      *
90      * @var array
91      * @see rcube_imap::fetch_add_headers
92      */
93     private $all_headers = array(
94         'IN-REPLY-TO',
95         'BCC',
96         'MESSAGE-ID',
97         'CONTENT-TRANSFER-ENCODING',
98         'REFERENCES',
99         'X-PRIORITY',
100         'X-DRAFT-INFO',
101         'MAIL-FOLLOWUP-TO',
102         'MAIL-REPLY-TO',
103         'RETURN-PATH',
104     );
4e17e6 105
90f81a 106     const UNKNOWN       = 0;
A 107     const NOPERM        = 1;
108     const READONLY      = 2;
109     const TRYCREATE     = 3;
110     const INUSE         = 4;
111     const OVERQUOTA     = 5;
112     const ALREADYEXISTS = 6;
113     const NONEXISTENT   = 7;
114     const CONTACTADMIN  = 8;
115
4e17e6 116
59c216 117     /**
5cf5ee 118      * Object constructor.
59c216 119      */
5cf5ee 120     function __construct()
4e17e6 121     {
59c216 122         $this->conn = new rcube_imap_generic();
68070e 123
A 124         // Set namespace and delimiter from session,
125         // so some methods would work before connection
126         if (isset($_SESSION['imap_namespace']))
127             $this->namespace = $_SESSION['imap_namespace'];
128         if (isset($_SESSION['imap_delimiter']))
129             $this->delimiter = $_SESSION['imap_delimiter'];
4e17e6 130     }
T 131
30b302 132
59c216 133     /**
A 134      * Connect to an IMAP server
135      *
5c461b 136      * @param  string   $host    Host to connect
A 137      * @param  string   $user    Username for IMAP account
138      * @param  string   $pass    Password for IMAP account
139      * @param  integer  $port    Port to connect to
140      * @param  string   $use_ssl SSL schema (either ssl or tls) or null if plain connection
59c216 141      * @return boolean  TRUE on success, FALSE on failure
A 142      * @access public
143      */
144     function connect($host, $user, $pass, $port=143, $use_ssl=null)
4e17e6 145     {
175d8e 146         // check for OpenSSL support in PHP build
59c216 147         if ($use_ssl && extension_loaded('openssl'))
A 148             $this->options['ssl_mode'] = $use_ssl == 'imaps' ? 'ssl' : $use_ssl;
149         else if ($use_ssl) {
150             raise_error(array('code' => 403, 'type' => 'imap',
151                 'file' => __FILE__, 'line' => __LINE__,
152                 'message' => "OpenSSL not available"), true, false);
153             $port = 143;
154         }
4e17e6 155
59c216 156         $this->options['port'] = $port;
f28124 157
890eae 158         if ($this->options['debug']) {
7f1da4 159             $this->conn->setDebug(true, array($this, 'debug_handler'));
A 160
890eae 161             $this->options['ident'] = array(
A 162                 'name' => 'Roundcube Webmail',
163                 'version' => RCMAIL_VERSION,
164                 'php' => PHP_VERSION,
165                 'os' => PHP_OS,
166                 'command' => $_SERVER['REQUEST_URI'],
167             );
168         }
169
59c216 170         $attempt = 0;
A 171         do {
172             $data = rcmail::get_instance()->plugins->exec_hook('imap_connect',
173                 array('host' => $host, 'user' => $user, 'attempt' => ++$attempt));
c43517 174
59c216 175             if (!empty($data['pass']))
A 176                 $pass = $data['pass'];
b026c3 177
59c216 178             $this->conn->connect($data['host'], $data['user'], $pass, $this->options);
A 179         } while(!$this->conn->connected() && $data['retry']);
80fbda 180
59c216 181         $this->host = $data['host'];
A 182         $this->user = $data['user'];
183         $this->pass = $pass;
184         $this->port = $port;
185         $this->ssl  = $use_ssl;
520c36 186
59c216 187         if ($this->conn->connected()) {
00290a 188             // get namespace and delimiter
A 189             $this->set_env();
94a6c6 190             return true;
59c216 191         }
A 192         // write error log
193         else if ($this->conn->error) {
ad399a 194             if ($pass && $user) {
A 195                 $message = sprintf("Login failed for %s from %s. %s",
196                     $user, rcmail_remote_ip(), $this->conn->error);
197
b36491 198                 raise_error(array('code' => 403, 'type' => 'imap',
A 199                     'file' => __FILE__, 'line' => __LINE__,
ad399a 200                     'message' => $message), true, false);
A 201             }
59c216 202         }
A 203
94a6c6 204         return false;
4e17e6 205     }
T 206
30b302 207
59c216 208     /**
A 209      * Close IMAP connection
210      * Usually done on script shutdown
211      *
212      * @access public
213      */
214     function close()
c43517 215     {
e232ac 216         $this->conn->closeConnection();
4e17e6 217     }
T 218
30b302 219
59c216 220     /**
A 221      * Close IMAP connection and re-connect
222      * This is used to avoid some strange socket errors when talking to Courier IMAP
223      *
224      * @access public
225      */
226     function reconnect()
520c36 227     {
dd8354 228         $this->conn->closeConnection();
A 229         $connected = $this->connect($this->host, $this->user, $this->pass, $this->port, $this->ssl);
c43517 230
59c216 231         // issue SELECT command to restore connection status
dd8354 232         if ($connected && strlen($this->mailbox))
59c216 233             $this->conn->select($this->mailbox);
520c36 234     }
T 235
8fcc3e 236
A 237     /**
238      * Returns code of last error
239      *
240      * @return int Error code
241      */
242     function get_error_code()
243     {
7f1da4 244         return $this->conn->errornum;
8fcc3e 245     }
A 246
247
248     /**
249      * Returns message of last error
250      *
251      * @return string Error message
252      */
253     function get_error_str()
254     {
7f1da4 255         return $this->conn->error;
90f81a 256     }
A 257
258
259     /**
260      * Returns code of last command response
261      *
262      * @return int Response code
263      */
264     function get_response_code()
265     {
266         switch ($this->conn->resultcode) {
267             case 'NOPERM':
268                 return self::NOPERM;
269             case 'READ-ONLY':
270                 return self::READONLY;
271             case 'TRYCREATE':
272                 return self::TRYCREATE;
273             case 'INUSE':
274                 return self::INUSE;
275             case 'OVERQUOTA':
276                 return self::OVERQUOTA;
277             case 'ALREADYEXISTS':
278                 return self::ALREADYEXISTS;
279             case 'NONEXISTENT':
280                 return self::NONEXISTENT;
281             case 'CONTACTADMIN':
282                 return self::CONTACTADMIN;
283             default:
284                 return self::UNKNOWN;
285         }
286     }
287
288
289     /**
290      * Returns last command response
291      *
292      * @return string Response
293      */
294     function get_response_str()
295     {
7f1da4 296         return $this->conn->result;
8fcc3e 297     }
30b302 298
8fcc3e 299
59c216 300     /**
A 301      * Set options to be used in rcube_imap_generic::connect()
5c461b 302      *
A 303      * @param array $opt Options array
59c216 304      */
A 305     function set_options($opt)
4e17e6 306     {
59c216 307         $this->options = array_merge($this->options, (array)$opt);
17b5fb 308     }
T 309
30b302 310
59c216 311     /**
A 312      * Set default message charset
313      *
314      * This will be used for message decoding if a charset specification is not available
315      *
5c461b 316      * @param  string $cs Charset string
59c216 317      * @access public
A 318      */
319     function set_charset($cs)
17b5fb 320     {
59c216 321         $this->default_charset = $cs;
4e17e6 322     }
T 323
30b302 324
59c216 325     /**
A 326      * This list of folders will be listed above all other folders
327      *
5c461b 328      * @param  array $arr Indexed list of folder names
59c216 329      * @access public
A 330      */
331     function set_default_mailboxes($arr)
4e17e6 332     {
59c216 333         if (is_array($arr)) {
A 334             $this->default_folders = $arr;
fa4cd2 335
59c216 336             // add inbox if not included
A 337             if (!in_array('INBOX', $this->default_folders))
338                 array_unshift($this->default_folders, 'INBOX');
339         }
4e17e6 340     }
T 341
30b302 342
59c216 343     /**
A 344      * Set internal mailbox reference.
345      *
346      * All operations will be perfomed on this mailbox/folder
347      *
d08333 348      * @param  string $mailbox Mailbox/Folder name
59c216 349      * @access public
A 350      */
d08333 351     function set_mailbox($mailbox)
4e17e6 352     {
59c216 353         if ($this->mailbox == $mailbox)
A 354             return;
4e17e6 355
59c216 356         $this->mailbox = $mailbox;
4e17e6 357
59c216 358         // clear messagecount cache for this mailbox
A 359         $this->_clear_messagecount($mailbox);
4e17e6 360     }
T 361
30b302 362
A 363     /**
364      * Forces selection of a mailbox
365      *
366      * @param  string $mailbox Mailbox/Folder name
367      * @access public
368      */
90f81a 369     function select_mailbox($mailbox=null)
30b302 370     {
d08333 371         if (!strlen($mailbox)) {
A 372             $mailbox = $this->mailbox;
373         }
30b302 374
A 375         $selected = $this->conn->select($mailbox);
376
377         if ($selected && $this->mailbox != $mailbox) {
378             // clear messagecount cache for this mailbox
379             $this->_clear_messagecount($mailbox);
380             $this->mailbox = $mailbox;
381         }
382     }
383
384
59c216 385     /**
A 386      * Set internal list page
387      *
5c461b 388      * @param  number $page Page number to list
59c216 389      * @access public
A 390      */
391     function set_page($page)
4e17e6 392     {
59c216 393         $this->list_page = (int)$page;
4e17e6 394     }
T 395
30b302 396
59c216 397     /**
A 398      * Set internal page size
399      *
5c461b 400      * @param  number $size Number of messages to display on one page
59c216 401      * @access public
A 402      */
403     function set_pagesize($size)
4e17e6 404     {
59c216 405         $this->page_size = (int)$size;
4e17e6 406     }
c43517 407
30b302 408
59c216 409     /**
A 410      * Save a set of message ids for future message listing methods
411      *
412      * @param  string  IMAP Search query
413      * @param  array   List of message ids or NULL if empty
414      * @param  string  Charset of search string
415      * @param  string  Sorting field
c60978 416      * @param  string  True if set is sorted (SORT was used for searching)
59c216 417      */
c60978 418     function set_search_set($str=null, $msgs=null, $charset=null, $sort_field=null, $threads=false, $sorted=false)
04c618 419     {
59c216 420         if (is_array($str) && $msgs == null)
A 421             list($str, $msgs, $charset, $sort_field, $threads) = $str;
ffd3e2 422         if ($msgs === false)
A 423             $msgs = array();
424         else if ($msgs != null && !is_array($msgs))
59c216 425             $msgs = explode(',', $msgs);
f52c93 426
59c216 427         $this->search_string     = $str;
A 428         $this->search_set        = $msgs;
429         $this->search_charset    = $charset;
430         $this->search_sort_field = $sort_field;
431         $this->search_threads    = $threads;
c60978 432         $this->search_sorted     = $sorted;
04c618 433     }
T 434
30b302 435
59c216 436     /**
A 437      * Return the saved search set as hash array
438      * @return array Search set
439      */
440     function get_search_set()
04c618 441     {
59c216 442         return array($this->search_string,
A 443             $this->search_set,
444             $this->search_charset,
445             $this->search_sort_field,
446             $this->search_threads,
c60978 447             $this->search_sorted,
59c216 448         );
04c618 449     }
4e17e6 450
30b302 451
59c216 452     /**
A 453      * Returns the currently used mailbox name
454      *
455      * @return  string Name of the mailbox/folder
456      * @access  public
457      */
458     function get_mailbox_name()
4e17e6 459     {
d08333 460         return $this->conn->connected() ? $this->mailbox : '';
1cded8 461     }
T 462
30b302 463
59c216 464     /**
A 465      * Returns the IMAP server's capability
466      *
5c461b 467      * @param   string  $cap Capability name
59c216 468      * @return  mixed   Capability value or TRUE if supported, FALSE if not
A 469      * @access  public
470      */
471     function get_capability($cap)
1cded8 472     {
59c216 473         return $this->conn->getCapability(strtoupper($cap));
f52c93 474     }
T 475
30b302 476
59c216 477     /**
A 478      * Sets threading flag to the best supported THREAD algorithm
479      *
5c461b 480      * @param  boolean  $enable TRUE to enable and FALSE
59c216 481      * @return string   Algorithm or false if THREAD is not supported
A 482      * @access public
483      */
484     function set_threading($enable=false)
f52c93 485     {
59c216 486         $this->threading = false;
c43517 487
600bb1 488         if ($enable && ($caps = $this->get_capability('THREAD'))) {
A 489             if (in_array('REFS', $caps))
59c216 490                 $this->threading = 'REFS';
600bb1 491             else if (in_array('REFERENCES', $caps))
59c216 492                 $this->threading = 'REFERENCES';
600bb1 493             else if (in_array('ORDEREDSUBJECT', $caps))
59c216 494                 $this->threading = 'ORDEREDSUBJECT';
A 495         }
496
497         return $this->threading;
498     }
499
30b302 500
59c216 501     /**
A 502      * Checks the PERMANENTFLAGS capability of the current mailbox
503      * and returns true if the given flag is supported by the IMAP server
504      *
5c461b 505      * @param   string  $flag Permanentflag name
29983c 506      * @return  boolean True if this flag is supported
59c216 507      * @access  public
A 508      */
509     function check_permflag($flag)
510     {
511         $flag = strtoupper($flag);
512         $imap_flag = $this->conn->flags[$flag];
a2e8cb 513         return (in_array_nocase($imap_flag, $this->conn->data['PERMANENTFLAGS']));
59c216 514     }
A 515
30b302 516
59c216 517     /**
A 518      * Returns the delimiter that is used by the IMAP server for folder separation
519      *
520      * @return  string  Delimiter string
521      * @access  public
522      */
523     function get_hierarchy_delimiter()
524     {
525         return $this->delimiter;
00290a 526     }
A 527
528
529     /**
530      * Get namespace
531      *
d08333 532      * @param string $name Namespace array index: personal, other, shared, prefix
A 533      *
00290a 534      * @return  array  Namespace data
A 535      * @access  public
536      */
d08333 537     function get_namespace($name=null)
00290a 538     {
d08333 539         $ns = $this->namespace;
A 540
541         if ($name) {
542             return isset($ns[$name]) ? $ns[$name] : null;
543         }
544
545         unset($ns['prefix']);
546         return $ns;
00290a 547     }
A 548
549
550     /**
551      * Sets delimiter and namespaces
552      *
553      * @access private
554      */
555     private function set_env()
556     {
557         if ($this->delimiter !== null && $this->namespace !== null) {
558             return;
559         }
560
561         $config = rcmail::get_instance()->config;
562         $imap_personal  = $config->get('imap_ns_personal');
563         $imap_other     = $config->get('imap_ns_other');
564         $imap_shared    = $config->get('imap_ns_shared');
565         $imap_delimiter = $config->get('imap_delimiter');
566
7f1da4 567         if (!$this->conn->connected())
00290a 568             return;
A 569
570         $ns = $this->conn->getNamespace();
571
02491a 572         // Set namespaces (NAMESPACE supported)
00290a 573         if (is_array($ns)) {
A 574             $this->namespace = $ns;
575         }
02491a 576         else {
00290a 577             $this->namespace = array(
A 578                 'personal' => NULL,
579                 'other'    => NULL,
580                 'shared'   => NULL,
581             );
02491a 582         }
00290a 583
02491a 584         if ($imap_delimiter) {
A 585             $this->delimiter = $imap_delimiter;
586         }
587         if (empty($this->delimiter)) {
588             $this->delimiter = $this->namespace['personal'][0][1];
589         }
590         if (empty($this->delimiter)) {
591             $this->delimiter = $this->conn->getHierarchyDelimiter();
592         }
593         if (empty($this->delimiter)) {
594             $this->delimiter = '/';
595         }
596
597         // Overwrite namespaces
598         if ($imap_personal !== null) {
599             $this->namespace['personal'] = NULL;
600             foreach ((array)$imap_personal as $dir) {
601                 $this->namespace['personal'][] = array($dir, $this->delimiter);
602             }
603         }
604         if ($imap_other !== null) {
605             $this->namespace['other'] = NULL;
606             foreach ((array)$imap_other as $dir) {
607                 if ($dir) {
608                     $this->namespace['other'][] = array($dir, $this->delimiter);
00290a 609                 }
A 610             }
02491a 611         }
A 612         if ($imap_shared !== null) {
613             $this->namespace['shared'] = NULL;
614             foreach ((array)$imap_shared as $dir) {
615                 if ($dir) {
616                     $this->namespace['shared'][] = array($dir, $this->delimiter);
00290a 617                 }
A 618             }
619         }
620
d08333 621         // Find personal namespace prefix for mod_mailbox()
A 622         // Prefix can be removed when there is only one personal namespace
623         if (is_array($this->namespace['personal']) && count($this->namespace['personal']) == 1) {
624             $this->namespace['prefix'] = $this->namespace['personal'][0][0];
625         }
626
00290a 627         $_SESSION['imap_namespace'] = $this->namespace;
A 628         $_SESSION['imap_delimiter'] = $this->delimiter;
59c216 629     }
A 630
30b302 631
59c216 632     /**
A 633      * Get message count for a specific mailbox
634      *
d08333 635      * @param  string  $mailbox Mailbox/folder name
A 636      * @param  string  $mode    Mode for count [ALL|THREADS|UNSEEN|RECENT]
637      * @param  boolean $force   Force reading from server and update cache
638      * @param  boolean $status  Enables storing folder status info (max UID/count),
639      *                          required for mailbox_status()
a03c98 640      * @return int     Number of messages
A 641      * @access public
59c216 642      */
d08333 643     function messagecount($mailbox='', $mode='ALL', $force=false, $status=true)
59c216 644     {
d08333 645         if (!strlen($mailbox)) {
A 646             $mailbox = $this->mailbox;
647         }
648
488074 649         return $this->_messagecount($mailbox, $mode, $force, $status);
59c216 650     }
A 651
30b302 652
59c216 653     /**
A 654      * Private method for getting nr of messages
655      *
5c461b 656      * @param string  $mailbox Mailbox name
A 657      * @param string  $mode    Mode for count [ALL|THREADS|UNSEEN|RECENT]
658      * @param boolean $force   Force reading from server and update cache
659      * @param boolean $status  Enables storing folder status info (max UID/count),
660      *                         required for mailbox_status()
661      * @return int Number of messages
59c216 662      * @access  private
A 663      * @see     rcube_imap::messagecount()
664      */
d08333 665     private function _messagecount($mailbox, $mode='ALL', $force=false, $status=true)
59c216 666     {
A 667         $mode = strtoupper($mode);
668
669         // count search set
670         if ($this->search_string && $mailbox == $this->mailbox && ($mode == 'ALL' || $mode == 'THREADS') && !$force) {
671             if ($this->search_threads)
672                 return $mode == 'ALL' ? count((array)$this->search_set['depth']) : count((array)$this->search_set['tree']);
673             else
674                 return count((array)$this->search_set);
675         }
c43517 676
59c216 677         $a_mailbox_cache = $this->get_cache('messagecount');
c43517 678
59c216 679         // return cached value
A 680         if (!$force && is_array($a_mailbox_cache[$mailbox]) && isset($a_mailbox_cache[$mailbox][$mode]))
681             return $a_mailbox_cache[$mailbox][$mode];
682
683         if (!is_array($a_mailbox_cache[$mailbox]))
684             $a_mailbox_cache[$mailbox] = array();
685
686         if ($mode == 'THREADS') {
c26b39 687             $res   = $this->_threadcount($mailbox, $msg_count);
A 688             $count = $res['count'];
689
488074 690             if ($status) {
c26b39 691                 $this->set_folder_stats($mailbox, 'cnt', $res['msgcount']);
A 692                 $this->set_folder_stats($mailbox, 'maxuid', $res['maxuid'] ? $this->_id2uid($res['maxuid'], $mailbox) : 0);
488074 693             }
59c216 694         }
A 695         // RECENT count is fetched a bit different
696         else if ($mode == 'RECENT') {
e232ac 697             $count = $this->conn->countRecent($mailbox);
59c216 698         }
A 699         // use SEARCH for message counting
700         else if ($this->skip_deleted) {
701             $search_str = "ALL UNDELETED";
659cf1 702             $keys       = array('COUNT');
A 703             $need_uid   = false;
59c216 704
659cf1 705             if ($mode == 'UNSEEN') {
59c216 706                 $search_str .= " UNSEEN";
659cf1 707             }
9ae29c 708             else {
5cf5ee 709                 if ($this->messages_caching) {
9ae29c 710                     $keys[] = 'ALL';
A 711                 }
712                 if ($status) {
713                     $keys[]   = 'MAX';
714                     $need_uid = true;
715                 }
659cf1 716             }
c43517 717
659cf1 718             // get message count using (E)SEARCH
A 719             // not very performant but more precise (using UNDELETED)
720             $index = $this->conn->search($mailbox, $search_str, $need_uid, $keys);
721
722             $count = is_array($index) ? $index['COUNT'] : 0;
59c216 723
9ae29c 724             if ($mode == 'ALL') {
5cf5ee 725                 if ($need_uid && $this->messages_caching) {
9ae29c 726                     // Save messages index for check_cache_status()
A 727                     $this->icache['all_undeleted_idx'] = $index['ALL'];
728                 }
729                 if ($status) {
730                     $this->set_folder_stats($mailbox, 'cnt', $count);
731                     $this->set_folder_stats($mailbox, 'maxuid', is_array($index) ? $index['MAX'] : 0);
732                 }
488074 733             }
59c216 734         }
A 735         else {
736             if ($mode == 'UNSEEN')
737                 $count = $this->conn->countUnseen($mailbox);
738             else {
739                 $count = $this->conn->countMessages($mailbox);
488074 740                 if ($status) {
A 741                     $this->set_folder_stats($mailbox,'cnt', $count);
742                     $this->set_folder_stats($mailbox, 'maxuid', $count ? $this->_id2uid($count, $mailbox) : 0);
743                 }
59c216 744             }
A 745         }
746
747         $a_mailbox_cache[$mailbox][$mode] = (int)$count;
748
749         // write back to cache
750         $this->update_cache('messagecount', $a_mailbox_cache);
751
752         return (int)$count;
4e17e6 753     }
T 754
755
59c216 756     /**
A 757      * Private method for getting nr of threads
758      *
29983c 759      * @param string $mailbox   Folder name
c26b39 760      *
A 761      * @returns array Array containing items: 'count' - threads count,
762      *                'msgcount' = messages count, 'maxuid' = max. UID in the set
59c216 763      * @access  private
A 764      */
c26b39 765     private function _threadcount($mailbox)
5b3dd4 766     {
c26b39 767         $result = array();
A 768
f13baa 769         if (!empty($this->icache['threads'])) {
d1a988 770             $dcount = count($this->icache['threads']['depth']);
c26b39 771             $result = array(
A 772                 'count'    => count($this->icache['threads']['tree']),
d1a988 773                 'msgcount' => $dcount,
A 774                 'maxuid'   => $dcount ? max(array_keys($this->icache['threads']['depth'])) : 0,
c26b39 775             );
f13baa 776         }
c26b39 777         else if (is_array($result = $this->_fetch_threads($mailbox))) {
d1a988 778             $dcount = count($result[1]);
c26b39 779             $result = array(
A 780                 'count'    => count($result[0]),
d1a988 781                 'msgcount' => $dcount,
A 782                 'maxuid'   => $dcount ? max(array_keys($result[1])) : 0,
c26b39 783             );
A 784         }
785
786         return $result;
f52c93 787     }
T 788
f13baa 789
59c216 790     /**
A 791      * Public method for listing headers
792      * convert mailbox name with root dir first
793      *
d08333 794      * @param   string   $mailbox    Mailbox/folder name
5c461b 795      * @param   int      $page       Current page to list
A 796      * @param   string   $sort_field Header field to sort by
797      * @param   string   $sort_order Sort order [ASC|DESC]
798      * @param   int      $slice      Number of slice items to extract from result array
59c216 799      * @return  array    Indexed array with message header objects
c43517 800      * @access  public
59c216 801      */
d08333 802     function list_headers($mailbox='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
4e17e6 803     {
d08333 804         if (!strlen($mailbox)) {
A 805             $mailbox = $this->mailbox;
806         }
807
59c216 808         return $this->_list_headers($mailbox, $page, $sort_field, $sort_order, false, $slice);
4e17e6 809     }
T 810
f13baa 811
59c216 812     /**
A 813      * Private method for listing message headers
814      *
5c461b 815      * @param   string   $mailbox    Mailbox name
A 816      * @param   int      $page       Current page to list
817      * @param   string   $sort_field Header field to sort by
818      * @param   string   $sort_order Sort order [ASC|DESC]
819      * @param   int      $slice      Number of slice items to extract from result array
820      * @return  array    Indexed array with message header objects
59c216 821      * @access  private
A 822      * @see     rcube_imap::list_headers
823      */
824     private function _list_headers($mailbox='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $recursive=false, $slice=0)
4e17e6 825     {
59c216 826         if (!strlen($mailbox))
A 827             return array();
04c618 828
59c216 829         // use saved message set
A 830         if ($this->search_string && $mailbox == $this->mailbox)
831             return $this->_list_header_set($mailbox, $page, $sort_field, $sort_order, $slice);
04c618 832
59c216 833         if ($this->threading)
A 834             return $this->_list_thread_headers($mailbox, $page, $sort_field, $sort_order, $recursive, $slice);
f52c93 835
59c216 836         $this->_set_sort_order($sort_field, $sort_order);
4e17e6 837
59c216 838         $page         = $page ? $page : $this->list_page;
A 839         $cache_key    = $mailbox.'.msg';
1cded8 840
5cf5ee 841         if ($this->messages_caching) {
eacce9 842             // cache is OK, we can get messages from local cache
A 843             // (assume cache is in sync when in recursive mode)
844             if ($recursive || $this->check_cache_status($mailbox, $cache_key)>0) {
845                 $start_msg = ($page-1) * $this->page_size;
846                 $a_msg_headers = $this->get_message_cache($cache_key, $start_msg,
847                     $start_msg+$this->page_size, $this->sort_field, $this->sort_order);
848                 $result = array_values($a_msg_headers);
849                 if ($slice)
850                     $result = array_slice($result, -$slice, $slice);
851                 return $result;
852             }
853             // cache is incomplete, sync it (all messages in the folder)
854             else if (!$recursive) {
855                 $this->sync_header_index($mailbox);
856                 return $this->_list_headers($mailbox, $page, $this->sort_field, $this->sort_order, true, $slice);
857             }
59c216 858         }
2dd7ee 859
59c216 860         // retrieve headers from IMAP
A 861         $a_msg_headers = array();
2dd7ee 862
59c216 863         // use message index sort as default sorting (for better performance)
A 864         if (!$this->sort_field) {
865             if ($this->skip_deleted) {
866                 // @TODO: this could be cached
867                 if ($msg_index = $this->_search_index($mailbox, 'ALL UNDELETED')) {
868                     $max = max($msg_index);
869                     list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
870                     $msg_index = array_slice($msg_index, $begin, $end-$begin);
871                 }
872             }
873             else if ($max = $this->conn->countMessages($mailbox)) {
874                 list($begin, $end) = $this->_get_message_range($max, $page);
875                 $msg_index = range($begin+1, $end);
876             }
877             else
878                 $msg_index = array();
879
ffd3e2 880             if ($slice && $msg_index)
59c216 881                 $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
A 882
883             // fetch reqested headers from server
884             if ($msg_index)
885                 $this->_fetch_headers($mailbox, join(",", $msg_index), $a_msg_headers, $cache_key);
886         }
887         // use SORT command
c60978 888         else if ($this->get_capability('SORT') &&
A 889             // Courier-IMAP provides SORT capability but allows to disable it by admin (#1486959)
890             ($msg_index = $this->conn->sort($mailbox, $this->sort_field, $this->skip_deleted ? 'UNDELETED' : '')) !== false
891         ) {
892             if (!empty($msg_index)) {
59c216 893                 list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
A 894                 $max = max($msg_index);
895                 $msg_index = array_slice($msg_index, $begin, $end-$begin);
896
897                 if ($slice)
898                     $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
899
900                 // fetch reqested headers from server
901                 $this->_fetch_headers($mailbox, join(',', $msg_index), $a_msg_headers, $cache_key);
902             }
903         }
904         // fetch specified header for all messages and sort
905         else if ($a_index = $this->conn->fetchHeaderIndex($mailbox, "1:*", $this->sort_field, $this->skip_deleted)) {
906             asort($a_index); // ASC
907             $msg_index = array_keys($a_index);
efe93a 908             $max = max($msg_index);
A 909             list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
910             $msg_index = array_slice($msg_index, $begin, $end-$begin);
59c216 911
A 912             if ($slice)
913                 $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
914
915             // fetch reqested headers from server
916             $this->_fetch_headers($mailbox, join(",", $msg_index), $a_msg_headers, $cache_key);
a96183 917         }
1cead0 918
59c216 919         // delete cached messages with a higher index than $max+1
A 920         // Changed $max to $max+1 to fix this bug : #1484295
921         $this->clear_message_cache($cache_key, $max + 1);
1cead0 922
59c216 923         // kick child process to sync cache
A 924         // ...
2dd7ee 925
59c216 926         // return empty array if no messages found
A 927         if (!is_array($a_msg_headers) || empty($a_msg_headers))
928             return array();
c43517 929
59c216 930         // use this class for message sorting
A 931         $sorter = new rcube_header_sorter();
932         $sorter->set_sequence_numbers($msg_index);
933         $sorter->sort_headers($a_msg_headers);
1cded8 934
59c216 935         if ($this->sort_order == 'DESC')
c43517 936             $a_msg_headers = array_reverse($a_msg_headers);
1cded8 937
59c216 938         return array_values($a_msg_headers);
4e17e6 939     }
7e93ff 940
f13baa 941
59c216 942     /**
A 943      * Private method for listing message headers using threads
944      *
5c461b 945      * @param   string   $mailbox    Mailbox/folder name
A 946      * @param   int      $page       Current page to list
947      * @param   string   $sort_field Header field to sort by
948      * @param   string   $sort_order Sort order [ASC|DESC]
29983c 949      * @param   boolean  $recursive  True if called recursively
5c461b 950      * @param   int      $slice      Number of slice items to extract from result array
A 951      * @return  array    Indexed array with message header objects
59c216 952      * @access  private
A 953      * @see     rcube_imap::list_headers
954      */
955     private function _list_thread_headers($mailbox, $page=NULL, $sort_field=NULL, $sort_order=NULL, $recursive=false, $slice=0)
f52c93 956     {
59c216 957         $this->_set_sort_order($sort_field, $sort_order);
f52c93 958
59c216 959         $page = $page ? $page : $this->list_page;
f52c93 960 //    $cache_key = $mailbox.'.msg';
T 961 //    $cache_status = $this->check_cache_status($mailbox, $cache_key);
962
59c216 963         // get all threads (default sort order)
A 964         list ($thread_tree, $msg_depth, $has_children) = $this->_fetch_threads($mailbox);
f52c93 965
59c216 966         if (empty($thread_tree))
A 967             return array();
f52c93 968
59c216 969         $msg_index = $this->_sort_threads($mailbox, $thread_tree);
f52c93 970
59c216 971         return $this->_fetch_thread_headers($mailbox,
A 972             $thread_tree, $msg_depth, $has_children, $msg_index, $page, $slice);
f52c93 973     }
T 974
975
59c216 976     /**
A 977      * Private method for fetching threads data
978      *
5c461b 979      * @param   string   $mailbox Mailbox/folder name
59c216 980      * @return  array    Array with thread data
A 981      * @access  private
982      */
983     private function _fetch_threads($mailbox)
f52c93 984     {
59c216 985         if (empty($this->icache['threads'])) {
A 986             // get all threads
e9a974 987             $result = $this->conn->thread($mailbox, $this->threading,
A 988                 $this->skip_deleted ? 'UNDELETED' : '');
c43517 989
59c216 990             // add to internal (fast) cache
A 991             $this->icache['threads'] = array();
e9a974 992             $this->icache['threads']['tree'] = is_array($result) ? $result[0] : array();
A 993             $this->icache['threads']['depth'] = is_array($result) ? $result[1] : array();
994             $this->icache['threads']['has_children'] = is_array($result) ? $result[2] : array();
f52c93 995         }
T 996
59c216 997         return array(
A 998             $this->icache['threads']['tree'],
999             $this->icache['threads']['depth'],
1000             $this->icache['threads']['has_children'],
1001         );
f52c93 1002     }
T 1003
1004
59c216 1005     /**
A 1006      * Private method for fetching threaded messages headers
1007      *
29983c 1008      * @param string  $mailbox      Mailbox name
A 1009      * @param array   $thread_tree  Thread tree data
1010      * @param array   $msg_depth    Thread depth data
1011      * @param array   $has_children Thread children data
1012      * @param array   $msg_index    Messages index
1013      * @param int     $page         List page number
1014      * @param int     $slice        Number of threads to slice
1015      * @return array  Messages headers
59c216 1016      * @access  private
A 1017      */
1018     private function _fetch_thread_headers($mailbox, $thread_tree, $msg_depth, $has_children, $msg_index, $page, $slice=0)
4647e1 1019     {
59c216 1020         $cache_key = $mailbox.'.msg';
A 1021         // now get IDs for current page
1022         list($begin, $end) = $this->_get_message_range(count($msg_index), $page);
1023         $msg_index = array_slice($msg_index, $begin, $end-$begin);
4647e1 1024
0b6e97 1025         if ($slice)
59c216 1026             $msg_index = array_slice($msg_index, ($this->sort_order == 'DESC' ? 0 : -$slice), $slice);
A 1027
1028         if ($this->sort_order == 'DESC')
1029             $msg_index = array_reverse($msg_index);
1030
1031         // flatten threads array
1032         // @TODO: fetch children only in expanded mode (?)
1033         $all_ids = array();
00290a 1034         foreach ($msg_index as $root) {
59c216 1035             $all_ids[] = $root;
A 1036             if (!empty($thread_tree[$root]))
1037                 $all_ids = array_merge($all_ids, array_keys_recursive($thread_tree[$root]));
1038         }
1039
1040         // fetch reqested headers from server
1041         $this->_fetch_headers($mailbox, $all_ids, $a_msg_headers, $cache_key);
0803fb 1042
84b884 1043         // return empty array if no messages found
A 1044         if (!is_array($a_msg_headers) || empty($a_msg_headers))
59c216 1045             return array();
c43517 1046
59c216 1047         // use this class for message sorting
84b884 1048         $sorter = new rcube_header_sorter();
59c216 1049         $sorter->set_sequence_numbers($all_ids);
84b884 1050         $sorter->sort_headers($a_msg_headers);
A 1051
59c216 1052         // Set depth, has_children and unread_children fields in headers
A 1053         $this->_set_thread_flags($a_msg_headers, $msg_depth, $has_children);
1054
84b884 1055         return array_values($a_msg_headers);
59c216 1056     }
84b884 1057
d15d59 1058
59c216 1059     /**
A 1060      * Private method for setting threaded messages flags:
1061      * depth, has_children and unread_children
1062      *
5c461b 1063      * @param  array  $headers      Reference to headers array indexed by message ID
A 1064      * @param  array  $msg_depth    Array of messages depth indexed by message ID
1065      * @param  array  $msg_children Array of messages children flags indexed by message ID
59c216 1066      * @return array   Message headers array indexed by message ID
A 1067      * @access private
1068      */
1069     private function _set_thread_flags(&$headers, $msg_depth, $msg_children)
1070     {
1071         $parents = array();
0b6e97 1072
59c216 1073         foreach ($headers as $idx => $header) {
A 1074             $id = $header->id;
1075             $depth = $msg_depth[$id];
1076             $parents = array_slice($parents, 0, $depth);
1077
1078             if (!empty($parents)) {
1079                 $headers[$idx]->parent_uid = end($parents);
1080                 if (!$header->seen)
1081                     $headers[$parents[0]]->unread_children++;
1082             }
1083             array_push($parents, $header->uid);
1084
1085             $headers[$idx]->depth = $depth;
1086             $headers[$idx]->has_children = $msg_children[$id];
84b884 1087         }
f52c93 1088     }
T 1089
1090
59c216 1091     /**
A 1092      * Private method for listing a set of message headers (search results)
1093      *
5c461b 1094      * @param   string   $mailbox    Mailbox/folder name
A 1095      * @param   int      $page       Current page to list
1096      * @param   string   $sort_field Header field to sort by
1097      * @param   string   $sort_order Sort order [ASC|DESC]
1098      * @param   int  $slice      Number of slice items to extract from result array
59c216 1099      * @return  array    Indexed array with message header objects
A 1100      * @access  private
1101      * @see     rcube_imap::list_header_set()
1102      */
1103     private function _list_header_set($mailbox, $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
f52c93 1104     {
59c216 1105         if (!strlen($mailbox) || empty($this->search_set))
A 1106             return array();
f52c93 1107
59c216 1108         // use saved messages from searching
A 1109         if ($this->threading)
1110             return $this->_list_thread_header_set($mailbox, $page, $sort_field, $sort_order, $slice);
f52c93 1111
59c216 1112         // search set is threaded, we need a new one
f22b54 1113         if ($this->search_threads) {
A 1114             if (empty($this->search_set['tree']))
1115                 return array();
59c216 1116             $this->search('', $this->search_string, $this->search_charset, $sort_field);
f22b54 1117         }
c43517 1118
a96183 1119         $msgs = $this->search_set;
59c216 1120         $a_msg_headers = array();
A 1121         $page = $page ? $page : $this->list_page;
1122         $start_msg = ($page-1) * $this->page_size;
a96183 1123
59c216 1124         $this->_set_sort_order($sort_field, $sort_order);
a96183 1125
59c216 1126         // quickest method (default sorting)
A 1127         if (!$this->search_sort_field && !$this->sort_field) {
1128             if ($sort_order == 'DESC')
1129                 $msgs = array_reverse($msgs);
df0da2 1130
59c216 1131             // get messages uids for one page
A 1132             $msgs = array_slice(array_values($msgs), $start_msg, min(count($msgs)-$start_msg, $this->page_size));
e538b3 1133
59c216 1134             if ($slice)
A 1135                 $msgs = array_slice($msgs, -$slice, $slice);
84b884 1136
59c216 1137             // fetch headers
A 1138             $this->_fetch_headers($mailbox, join(',',$msgs), $a_msg_headers, NULL);
1139
1140             // I didn't found in RFC that FETCH always returns messages sorted by index
1141             $sorter = new rcube_header_sorter();
1142             $sorter->set_sequence_numbers($msgs);
1143             $sorter->sort_headers($a_msg_headers);
1144
1145             return array_values($a_msg_headers);
1146         }
1147
1148         // sorted messages, so we can first slice array and then fetch only wanted headers
c60978 1149         if ($this->search_sorted) { // SORT searching result
59c216 1150             // reset search set if sorting field has been changed
A 1151             if ($this->sort_field && $this->search_sort_field != $this->sort_field)
1152                 $msgs = $this->search('', $this->search_string, $this->search_charset, $this->sort_field);
1153
1154             // return empty array if no messages found
1155             if (empty($msgs))
1156                 return array();
1157
1158             if ($sort_order == 'DESC')
1159                 $msgs = array_reverse($msgs);
1160
1161             // get messages uids for one page
1162             $msgs = array_slice(array_values($msgs), $start_msg, min(count($msgs)-$start_msg, $this->page_size));
1163
1164             if ($slice)
1165                 $msgs = array_slice($msgs, -$slice, $slice);
1166
1167             // fetch headers
1168             $this->_fetch_headers($mailbox, join(',',$msgs), $a_msg_headers, NULL);
1169
1170             $sorter = new rcube_header_sorter();
1171             $sorter->set_sequence_numbers($msgs);
1172             $sorter->sort_headers($a_msg_headers);
1173
1174             return array_values($a_msg_headers);
1175         }
1176         else { // SEARCH result, need sorting
1177             $cnt = count($msgs);
1178             // 300: experimantal value for best result
1179             if (($cnt > 300 && $cnt > $this->page_size) || !$this->sort_field) {
1180                 // use memory less expensive (and quick) method for big result set
1181                 $a_index = $this->message_index('', $this->sort_field, $this->sort_order);
1182                 // get messages uids for one page...
1183                 $msgs = array_slice($a_index, $start_msg, min($cnt-$start_msg, $this->page_size));
1184                 if ($slice)
1185                     $msgs = array_slice($msgs, -$slice, $slice);
1186                 // ...and fetch headers
1187                 $this->_fetch_headers($mailbox, join(',', $msgs), $a_msg_headers, NULL);
1188
1189                 // return empty array if no messages found
1190                 if (!is_array($a_msg_headers) || empty($a_msg_headers))
1191                     return array();
1192
1193                 $sorter = new rcube_header_sorter();
1194                 $sorter->set_sequence_numbers($msgs);
1195                 $sorter->sort_headers($a_msg_headers);
1196
1197                 return array_values($a_msg_headers);
1198             }
1199             else {
1200                 // for small result set we can fetch all messages headers
1201                 $this->_fetch_headers($mailbox, join(',', $msgs), $a_msg_headers, NULL);
c43517 1202
59c216 1203                 // return empty array if no messages found
A 1204                 if (!is_array($a_msg_headers) || empty($a_msg_headers))
1205                     return array();
1206
1207                 // if not already sorted
1208                 $a_msg_headers = $this->conn->sortHeaders(
1209                     $a_msg_headers, $this->sort_field, $this->sort_order);
1210
1211                 // only return the requested part of the set
1212                 $a_msg_headers = array_slice(array_values($a_msg_headers),
1213                     $start_msg, min($cnt-$start_msg, $this->page_size));
1214
1215                 if ($slice)
1216                     $a_msg_headers = array_slice($a_msg_headers, -$slice, $slice);
1217
1218                 return $a_msg_headers;
1219             }
1220         }
df0da2 1221     }
4e17e6 1222
T 1223
59c216 1224     /**
A 1225      * Private method for listing a set of threaded message headers (search results)
1226      *
5c461b 1227      * @param   string   $mailbox    Mailbox/folder name
A 1228      * @param   int      $page       Current page to list
1229      * @param   string   $sort_field Header field to sort by
1230      * @param   string   $sort_order Sort order [ASC|DESC]
1231      * @param   int      $slice      Number of slice items to extract from result array
59c216 1232      * @return  array    Indexed array with message header objects
A 1233      * @access  private
1234      * @see     rcube_imap::list_header_set()
1235      */
1236     private function _list_thread_header_set($mailbox, $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
1237     {
1238         // update search_set if previous data was fetched with disabled threading
f22b54 1239         if (!$this->search_threads) {
A 1240             if (empty($this->search_set))
1241                 return array();
59c216 1242             $this->search('', $this->search_string, $this->search_charset, $sort_field);
f22b54 1243         }
A 1244
1245         // empty result
1246         if (empty($this->search_set['tree']))
1247             return array();
4e17e6 1248
59c216 1249         $thread_tree = $this->search_set['tree'];
A 1250         $msg_depth = $this->search_set['depth'];
1251         $has_children = $this->search_set['children'];
1252         $a_msg_headers = array();
31b2ce 1253
59c216 1254         $page = $page ? $page : $this->list_page;
A 1255         $start_msg = ($page-1) * $this->page_size;
1cead0 1256
59c216 1257         $this->_set_sort_order($sort_field, $sort_order);
1cead0 1258
59c216 1259         $msg_index = $this->_sort_threads($mailbox, $thread_tree, array_keys($msg_depth));
A 1260
1261         return $this->_fetch_thread_headers($mailbox,
1262             $thread_tree, $msg_depth, $has_children, $msg_index, $page, $slice=0);
1263     }
1264
1265
1266     /**
1267      * Helper function to get first and last index of the requested set
1268      *
29983c 1269      * @param  int     $max  Messages count
A 1270      * @param  mixed   $page Page number to show, or string 'all'
1271      * @return array   Array with two values: first index, last index
59c216 1272      * @access private
A 1273      */
1274     private function _get_message_range($max, $page)
1275     {
1276         $start_msg = ($page-1) * $this->page_size;
c43517 1277
59c216 1278         if ($page=='all') {
A 1279             $begin  = 0;
1280             $end    = $max;
1281         }
1282         else if ($this->sort_order=='DESC') {
1283             $begin  = $max - $this->page_size - $start_msg;
1284             $end    = $max - $start_msg;
1285         }
1286         else {
1287             $begin  = $start_msg;
1288             $end    = $start_msg + $this->page_size;
1289         }
1290
1291         if ($begin < 0) $begin = 0;
1292         if ($end < 0) $end = $max;
1293         if ($end > $max) $end = $max;
c43517 1294
59c216 1295         return array($begin, $end);
A 1296     }
c43517 1297
59c216 1298
A 1299     /**
29983c 1300      * Fetches message headers (used for loop)
59c216 1301      *
29983c 1302      * @param  string  $mailbox       Mailbox name
A 1303      * @param  string  $msgs          Message index to fetch
1304      * @param  array   $a_msg_headers Reference to message headers array
1305      * @param  string  $cache_key     Cache index key
59c216 1306      * @return int     Messages count
A 1307      * @access private
1308      */
1309     private function _fetch_headers($mailbox, $msgs, &$a_msg_headers, $cache_key)
1310     {
1311         // fetch reqested headers from server
1312         $a_header_index = $this->conn->fetchHeaders(
103ddc 1313             $mailbox, $msgs, false, false, $this->get_fetch_headers());
59c216 1314
A 1315         if (empty($a_header_index))
1316             return 0;
1317
1318         foreach ($a_header_index as $i => $headers) {
eacce9 1319             $a_msg_headers[$headers->uid] = $headers;
A 1320         }
1321
1322         // Update cache
5cf5ee 1323         if ($this->messages_caching && $cache_key) {
eacce9 1324             // cache is incomplete?
A 1325             $cache_index = $this->get_message_cache_index($cache_key);
1326
1327             foreach ($a_header_index as $headers) {
1328                 // message in cache
1329                 if ($cache_index[$headers->id] == $headers->uid) {
1330                     unset($cache_index[$headers->id]);
1331                     continue;
1332                 }
1333                 // wrong UID at this position
59c216 1334                 if ($cache_index[$headers->id]) {
eacce9 1335                     $for_remove[] = $cache_index[$headers->id];
59c216 1336                     unset($cache_index[$headers->id]);
A 1337                 }
eacce9 1338                 // message UID in cache but at wrong position
A 1339                 if (is_int($key = array_search($headers->uid, $cache_index))) {
1340                     $for_remove[] = $cache_index[$key];
1341                     unset($cache_index[$key]);
1342                 }
59c216 1343
eacce9 1344                 $for_create[] = $headers->uid;
A 1345             }
f13baa 1346
eacce9 1347             if ($for_remove)
A 1348                 $this->remove_message_cache($cache_key, $for_remove);
1349
1350             // add messages to cache
1351             foreach ((array)$for_create as $uid) {
1352                 $headers = $a_msg_headers[$uid];
1353                 $this->add_message_cache($cache_key, $headers->id, $headers, NULL, true);
1354             }
59c216 1355         }
A 1356
1357         return count($a_msg_headers);
1358     }
c43517 1359
488074 1360
59c216 1361     /**
488074 1362      * Returns current status of mailbox
59c216 1363      *
A 1364      * We compare the maximum UID to determine the number of
1365      * new messages because the RECENT flag is not reliable.
1366      *
d08333 1367      * @param string $mailbox Mailbox/folder name
488074 1368      * @return int   Folder status
59c216 1369      */
d08333 1370     function mailbox_status($mailbox = null)
59c216 1371     {
d08333 1372         if (!strlen($mailbox)) {
A 1373             $mailbox = $this->mailbox;
1374         }
488074 1375         $old = $this->get_folder_stats($mailbox);
A 1376
c43517 1377         // refresh message count -> will update
59c216 1378         $this->_messagecount($mailbox, 'ALL', true);
488074 1379
A 1380         $result = 0;
1381         $new = $this->get_folder_stats($mailbox);
1382
1383         // got new messages
1384         if ($new['maxuid'] > $old['maxuid'])
1385             $result += 1;
1386         // some messages has been deleted
1387         if ($new['cnt'] < $old['cnt'])
1388             $result += 2;
1389
1390         // @TODO: optional checking for messages flags changes (?)
1391         // @TODO: UIDVALIDITY checking
1392
1393         return $result;
59c216 1394     }
488074 1395
A 1396
1397     /**
1398      * Stores folder statistic data in session
1399      * @TODO: move to separate DB table (cache?)
1400      *
d08333 1401      * @param string $mailbox Mailbox name
A 1402      * @param string $name    Data name
1403      * @param mixed  $data    Data value
488074 1404      */
d08333 1405     private function set_folder_stats($mailbox, $name, $data)
488074 1406     {
d08333 1407         $_SESSION['folders'][$mailbox][$name] = $data;
488074 1408     }
A 1409
1410
1411     /**
1412      * Gets folder statistic data
1413      *
d08333 1414      * @param string $mailbox Mailbox name
A 1415      *
488074 1416      * @return array Stats data
A 1417      */
d08333 1418     private function get_folder_stats($mailbox)
488074 1419     {
d08333 1420         if ($_SESSION['folders'][$mailbox])
A 1421             return (array) $_SESSION['folders'][$mailbox];
488074 1422         else
A 1423             return array();
1424     }
1425
1426
59c216 1427     /**
A 1428      * Return sorted array of message IDs (not UIDs)
1429      *
d08333 1430      * @param string $mailbox    Mailbox to get index from
5c461b 1431      * @param string $sort_field Sort column
A 1432      * @param string $sort_order Sort order [ASC, DESC]
29983c 1433      * @return array Indexed array with message IDs
59c216 1434      */
d08333 1435     function message_index($mailbox='', $sort_field=NULL, $sort_order=NULL)
59c216 1436     {
A 1437         if ($this->threading)
d08333 1438             return $this->thread_index($mailbox, $sort_field, $sort_order);
59c216 1439
A 1440         $this->_set_sort_order($sort_field, $sort_order);
1441
d08333 1442         if (!strlen($mailbox)) {
A 1443             $mailbox = $this->mailbox;
1444         }
59c216 1445         $key = "{$mailbox}:{$this->sort_field}:{$this->sort_order}:{$this->search_string}.msgi";
A 1446
1447         // we have a saved search result, get index from there
fa2173 1448         if (!isset($this->icache[$key]) && $this->search_string
59c216 1449             && !$this->search_threads && $mailbox == $this->mailbox) {
A 1450             // use message index sort as default sorting
1451             if (!$this->sort_field) {
1452                 $msgs = $this->search_set;
1453
1454                 if ($this->search_sort_field != 'date')
1455                     sort($msgs);
1456
1457                 if ($this->sort_order == 'DESC')
fa2173 1458                     $this->icache[$key] = array_reverse($msgs);
59c216 1459                 else
fa2173 1460                     $this->icache[$key] = $msgs;
59c216 1461             }
A 1462             // sort with SORT command
c60978 1463             else if ($this->search_sorted) {
59c216 1464                 if ($this->sort_field && $this->search_sort_field != $this->sort_field)
A 1465                     $this->search('', $this->search_string, $this->search_charset, $this->sort_field);
1466
1467                 if ($this->sort_order == 'DESC')
fa2173 1468                     $this->icache[$key] = array_reverse($this->search_set);
59c216 1469                 else
fa2173 1470                     $this->icache[$key] = $this->search_set;
59c216 1471             }
A 1472             else {
1473                 $a_index = $this->conn->fetchHeaderIndex($mailbox,
1474                     join(',', $this->search_set), $this->sort_field, $this->skip_deleted);
c43517 1475
59c216 1476                 if (is_array($a_index)) {
A 1477                     if ($this->sort_order=="ASC")
1478                         asort($a_index);
1479                     else if ($this->sort_order=="DESC")
1480                         arsort($a_index);
1481
fa2173 1482                     $this->icache[$key] = array_keys($a_index);
59c216 1483                 }
A 1484                 else {
fa2173 1485                     $this->icache[$key] = array();
59c216 1486                 }
A 1487             }
1488         }
1489
1490         // have stored it in RAM
fa2173 1491         if (isset($this->icache[$key]))
A 1492             return $this->icache[$key];
59c216 1493
A 1494         // check local cache
1495         $cache_key = $mailbox.'.msg';
1496         $cache_status = $this->check_cache_status($mailbox, $cache_key);
1497
1498         // cache is OK
1499         if ($cache_status>0) {
1500             $a_index = $this->get_message_cache_index($cache_key,
eacce9 1501                 $this->sort_field, $this->sort_order);
59c216 1502             return array_keys($a_index);
A 1503         }
1504
1505         // use message index sort as default sorting
1506         if (!$this->sort_field) {
1507             if ($this->skip_deleted) {
1508                 $a_index = $this->_search_index($mailbox, 'ALL');
1509             } else if ($max = $this->_messagecount($mailbox)) {
1510                 $a_index = range(1, $max);
1511             }
1512
ffd3e2 1513             if ($a_index !== false && $this->sort_order == 'DESC')
59c216 1514                 $a_index = array_reverse($a_index);
A 1515
fa2173 1516             $this->icache[$key] = $a_index;
59c216 1517         }
A 1518         // fetch complete message index
c60978 1519         else if ($this->get_capability('SORT') &&
A 1520             ($a_index = $this->conn->sort($mailbox,
1521                 $this->sort_field, $this->skip_deleted ? 'UNDELETED' : '')) !== false
1522         ) {
1523             if ($this->sort_order == 'DESC')
ffd3e2 1524                 $a_index = array_reverse($a_index);
A 1525
fa2173 1526             $this->icache[$key] = $a_index;
59c216 1527         }
A 1528         else if ($a_index = $this->conn->fetchHeaderIndex(
1529             $mailbox, "1:*", $this->sort_field, $this->skip_deleted)) {
1530             if ($this->sort_order=="ASC")
1531                 asort($a_index);
1532             else if ($this->sort_order=="DESC")
1533                 arsort($a_index);
c43517 1534
fa2173 1535             $this->icache[$key] = array_keys($a_index);
59c216 1536         }
31b2ce 1537
fa2173 1538         return $this->icache[$key] !== false ? $this->icache[$key] : array();
4e17e6 1539     }
T 1540
1541
59c216 1542     /**
A 1543      * Return sorted array of threaded message IDs (not UIDs)
1544      *
d08333 1545      * @param string $mailbox    Mailbox to get index from
5c461b 1546      * @param string $sort_field Sort column
A 1547      * @param string $sort_order Sort order [ASC, DESC]
59c216 1548      * @return array Indexed array with message IDs
A 1549      */
d08333 1550     function thread_index($mailbox='', $sort_field=NULL, $sort_order=NULL)
f52c93 1551     {
59c216 1552         $this->_set_sort_order($sort_field, $sort_order);
f52c93 1553
d08333 1554         if (!strlen($mailbox)) {
A 1555             $mailbox = $this->mailbox;
1556         }
59c216 1557         $key = "{$mailbox}:{$this->sort_field}:{$this->sort_order}:{$this->search_string}.thi";
f52c93 1558
59c216 1559         // we have a saved search result, get index from there
fa2173 1560         if (!isset($this->icache[$key]) && $this->search_string
59c216 1561             && $this->search_threads && $mailbox == $this->mailbox) {
A 1562             // use message IDs for better performance
1563             $ids = array_keys_recursive($this->search_set['tree']);
fa2173 1564             $this->icache[$key] = $this->_flatten_threads($mailbox, $this->search_set['tree'], $ids);
59c216 1565         }
f52c93 1566
59c216 1567         // have stored it in RAM
fa2173 1568         if (isset($this->icache[$key]))
A 1569             return $this->icache[$key];
f52c93 1570 /*
59c216 1571         // check local cache
A 1572         $cache_key = $mailbox.'.msg';
1573         $cache_status = $this->check_cache_status($mailbox, $cache_key);
f52c93 1574
59c216 1575         // cache is OK
A 1576         if ($cache_status>0) {
eacce9 1577             $a_index = $this->get_message_cache_index($cache_key, $this->sort_field, $this->sort_order);
59c216 1578             return array_keys($a_index);
A 1579         }
f52c93 1580 */
59c216 1581         // get all threads (default sort order)
A 1582         list ($thread_tree) = $this->_fetch_threads($mailbox);
f52c93 1583
fa2173 1584         $this->icache[$key] = $this->_flatten_threads($mailbox, $thread_tree);
c43517 1585
fa2173 1586         return $this->icache[$key];
f52c93 1587     }
T 1588
1589
59c216 1590     /**
A 1591      * Return array of threaded messages (all, not only roots)
1592      *
5c461b 1593      * @param string $mailbox     Mailbox to get index from
A 1594      * @param array  $thread_tree Threaded messages array (see _fetch_threads())
1595      * @param array  $ids         Message IDs if we know what we need (e.g. search result)
1596      *                            for better performance
59c216 1597      * @return array Indexed array with message IDs
A 1598      *
1599      * @access private
1600      */
1601     private function _flatten_threads($mailbox, $thread_tree, $ids=null)
f52c93 1602     {
59c216 1603         if (empty($thread_tree))
A 1604             return array();
f52c93 1605
59c216 1606         $msg_index = $this->_sort_threads($mailbox, $thread_tree, $ids);
f52c93 1607
59c216 1608         if ($this->sort_order == 'DESC')
A 1609             $msg_index = array_reverse($msg_index);
c43517 1610
59c216 1611         // flatten threads array
A 1612         $all_ids = array();
00290a 1613         foreach ($msg_index as $root) {
59c216 1614             $all_ids[] = $root;
c51304 1615             if (!empty($thread_tree[$root])) {
A 1616                 foreach (array_keys_recursive($thread_tree[$root]) as $val)
1617                     $all_ids[] = $val;
1618             }
59c216 1619         }
f52c93 1620
59c216 1621         return $all_ids;
f52c93 1622     }
T 1623
1624
59c216 1625     /**
5c461b 1626      * @param string $mailbox Mailbox name
59c216 1627      * @access private
A 1628      */
1629     private function sync_header_index($mailbox)
4e17e6 1630     {
59c216 1631         $cache_key = $mailbox.'.msg';
A 1632         $cache_index = $this->get_message_cache_index($cache_key);
eacce9 1633         $chunk_size = 1000;
A 1634
1635         // cache is empty, get all messages
1636         if (is_array($cache_index) && empty($cache_index)) {
1637             $max = $this->_messagecount($mailbox);
1638             // syncing a big folder maybe slow
1639             @set_time_limit(0);
1640             $start = 1;
1641             $end   = min($chunk_size, $max);
1642             while (true) {
1643                 // do this in loop to save memory (1000 msgs ~= 10 MB)
1644                 if ($headers = $this->conn->fetchHeaders($mailbox,
103ddc 1645                     "$start:$end", false, false, $this->get_fetch_headers())
eacce9 1646                 ) {
A 1647                     foreach ($headers as $header) {
1648                         $this->add_message_cache($cache_key, $header->id, $header, NULL, true);
1649                     }
1650                 }
1651                 if ($end - $start < $chunk_size - 1)
1652                     break;
1653
1654                 $end   = min($end+$chunk_size, $max);
1655                 $start += $chunk_size;
1656             }
1657             return;
1658         }
1cded8 1659
59c216 1660         // fetch complete message index
eacce9 1661         if (isset($this->icache['folder_index']))
A 1662             $a_message_index = &$this->icache['folder_index'];
1663         else
1664             $a_message_index = $this->conn->fetchHeaderIndex($mailbox, "1:*", 'UID', $this->skip_deleted);
c43517 1665
eacce9 1666         if ($a_message_index === false || $cache_index === null)
A 1667             return;
c43517 1668
eacce9 1669         // compare cache index with real index
59c216 1670         foreach ($a_message_index as $id => $uid) {
A 1671             // message in cache at correct position
1672             if ($cache_index[$id] == $uid) {
1673                 unset($cache_index[$id]);
1674                 continue;
aa16b4 1675             }
c43517 1676
59c216 1677             // other message at this position
A 1678             if (isset($cache_index[$id])) {
1679                 $for_remove[] = $cache_index[$id];
1680                 unset($cache_index[$id]);
1681             }
c43517 1682
eacce9 1683             // message in cache but at wrong position
A 1684             if (is_int($key = array_search($uid, $cache_index))) {
1685                 $for_remove[] = $uid;
1686                 unset($cache_index[$key]);
1687             }
1688
59c216 1689             $for_update[] = $id;
aa16b4 1690         }
8d4bcd 1691
eacce9 1692         // remove messages at wrong positions and those deleted that are still in cache_index
59c216 1693         if (!empty($for_remove))
A 1694             $cache_index = array_merge($cache_index, $for_remove);
c43517 1695
59c216 1696         if (!empty($cache_index))
A 1697             $this->remove_message_cache($cache_key, $cache_index);
5df0ad 1698
59c216 1699         // fetch complete headers and add to cache
A 1700         if (!empty($for_update)) {
eacce9 1701             // syncing a big folder maybe slow
A 1702             @set_time_limit(0);
1703             // To save memory do this in chunks
1704             $for_update = array_chunk($for_update, $chunk_size);
1705             foreach ($for_update as $uids) {
1706                 if ($headers = $this->conn->fetchHeaders($mailbox,
103ddc 1707                     $uids, false, false, $this->get_fetch_headers())
eacce9 1708                 ) {
A 1709                     foreach ($headers as $header) {
1710                         $this->add_message_cache($cache_key, $header->id, $header, NULL, true);
1711                     }
59c216 1712                 }
A 1713             }
1714         }
1715     }
1716
1717
1718     /**
1719      * Invoke search request to IMAP server
1720      *
d08333 1721      * @param  string  $mailbox    Mailbox name to search in
29983c 1722      * @param  string  $str        Search criteria
A 1723      * @param  string  $charset    Search charset
1724      * @param  string  $sort_field Header field to sort by
1725      * @return array   search results as list of message IDs
59c216 1726      * @access public
A 1727      */
d08333 1728     function search($mailbox='', $str=NULL, $charset=NULL, $sort_field=NULL)
5df0ad 1729     {
59c216 1730         if (!$str)
A 1731             return false;
c43517 1732
d08333 1733         if (!strlen($mailbox)) {
A 1734             $mailbox = $this->mailbox;
1735         }
5df0ad 1736
59c216 1737         $results = $this->_search_index($mailbox, $str, $charset, $sort_field);
A 1738
c60978 1739         $this->set_search_set($str, $results, $charset, $sort_field, (bool)$this->threading,
A 1740             $this->threading || $this->search_sorted ? true : false);
59c216 1741
A 1742         return $results;
5df0ad 1743     }
b20bca 1744
A 1745
59c216 1746     /**
A 1747      * Private search method
1748      *
5c461b 1749      * @param string $mailbox    Mailbox name
A 1750      * @param string $criteria   Search criteria
1751      * @param string $charset    Charset
1752      * @param string $sort_field Sorting field
59c216 1753      * @return array   search results as list of message ids
A 1754      * @access private
1755      * @see rcube_imap::search()
1756      */
1757     private function _search_index($mailbox, $criteria='ALL', $charset=NULL, $sort_field=NULL)
b20bca 1758     {
59c216 1759         $orig_criteria = $criteria;
A 1760
1761         if ($this->skip_deleted && !preg_match('/UNDELETED/', $criteria))
1762             $criteria = 'UNDELETED '.$criteria;
1763
1764         if ($this->threading) {
ffd3e2 1765             $a_messages = $this->conn->thread($mailbox, $this->threading, $criteria, $charset);
59c216 1766
ffd3e2 1767             // Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
A 1768             // but I've seen that Courier doesn't support UTF-8)
1769             if ($a_messages === false && $charset && $charset != 'US-ASCII')
1770                 $a_messages = $this->conn->thread($mailbox, $this->threading,
1771                     $this->convert_criteria($criteria, $charset), 'US-ASCII');
1772
1773             if ($a_messages !== false) {
1774                 list ($thread_tree, $msg_depth, $has_children) = $a_messages;
1775                 $a_messages = array(
1776                     'tree'     => $thread_tree,
1777                     'depth'    => $msg_depth,
1778                     'children' => $has_children
1779                 );
1780             }
c60978 1781
A 1782             return $a_messages;
59c216 1783         }
e9a974 1784
c60978 1785         if ($sort_field && $this->get_capability('SORT')) {
59c216 1786             $charset = $charset ? $charset : $this->default_charset;
A 1787             $a_messages = $this->conn->sort($mailbox, $sort_field, $criteria, false, $charset);
1788
ffd3e2 1789             // Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
A 1790             // but I've seen that Courier doesn't support UTF-8)
1791             if ($a_messages === false && $charset && $charset != 'US-ASCII')
1792                 $a_messages = $this->conn->sort($mailbox, $sort_field,
1793                     $this->convert_criteria($criteria, $charset), false, 'US-ASCII');
c60978 1794
A 1795             if ($a_messages !== false) {
1796                 $this->search_sorted = true;
1797                 return $a_messages;
1798             }
1799         }
1800
1801         if ($orig_criteria == 'ALL') {
1802             $max = $this->_messagecount($mailbox);
1803             $a_messages = $max ? range(1, $max) : array();
59c216 1804         }
A 1805         else {
c60978 1806             $a_messages = $this->conn->search($mailbox,
A 1807                 ($charset ? "CHARSET $charset " : '') . $criteria);
1808
1809             // Error, try with US-ASCII (some servers may support only US-ASCII)
1810             if ($a_messages === false && $charset && $charset != 'US-ASCII')
59c216 1811                 $a_messages = $this->conn->search($mailbox,
c60978 1812                     'CHARSET US-ASCII ' . $this->convert_criteria($criteria, $charset));
59c216 1813
c60978 1814             // I didn't found that SEARCH should return sorted IDs
A 1815             if (is_array($a_messages) && !$this->sort_field)
1816                 sort($a_messages);
59c216 1817         }
A 1818
c60978 1819         $this->search_sorted = false;
c43517 1820
59c216 1821         return $a_messages;
A 1822     }
c43517 1823
6f31b3 1824
A 1825     /**
1826      * Direct (real and simple) SEARCH request to IMAP server,
1827      * without result sorting and caching
1828      *
d08333 1829      * @param  string  $mailbox Mailbox name to search in
A 1830      * @param  string  $str     Search string
1831      * @param  boolean $ret_uid True if UIDs should be returned
6f31b3 1832      * @return array   Search results as list of message IDs or UIDs
A 1833      * @access public
1834      */
d08333 1835     function search_once($mailbox='', $str=NULL, $ret_uid=false)
6f31b3 1836     {
A 1837         if (!$str)
1838             return false;
c43517 1839
d08333 1840         if (!strlen($mailbox)) {
A 1841             $mailbox = $this->mailbox;
1842         }
6f31b3 1843
A 1844         return $this->conn->search($mailbox, $str, $ret_uid);
1845     }
1846
c43517 1847
59c216 1848     /**
ffd3e2 1849      * Converts charset of search criteria string
A 1850      *
5c461b 1851      * @param  string  $str          Search string
A 1852      * @param  string  $charset      Original charset
1853      * @param  string  $dest_charset Destination charset (default US-ASCII)
ffd3e2 1854      * @return string  Search string
A 1855      * @access private
1856      */
1857     private function convert_criteria($str, $charset, $dest_charset='US-ASCII')
1858     {
1859         // convert strings to US_ASCII
1860         if (preg_match_all('/\{([0-9]+)\}\r\n/', $str, $matches, PREG_OFFSET_CAPTURE)) {
1861             $last = 0; $res = '';
1862             foreach ($matches[1] as $m) {
1863                 $string_offset = $m[1] + strlen($m[0]) + 4; // {}\r\n
1864                 $string = substr($str, $string_offset - 1, $m[0]);
1865                 $string = rcube_charset_convert($string, $charset, $dest_charset);
1866                 if (!$string)
1867                     continue;
1868                 $res .= sprintf("%s{%d}\r\n%s", substr($str, $last, $m[1] - $last - 1), strlen($string), $string);
1869                 $last = $m[0] + $string_offset - 1;
1870             }
1871             if ($last < strlen($str))
1872                 $res .= substr($str, $last, strlen($str)-$last);
1873         }
1874         else // strings for conversion not found
1875             $res = $str;
1876
1877         return $res;
1878     }
1879
1880
1881     /**
59c216 1882      * Sort thread
A 1883      *
5c461b 1884      * @param string $mailbox     Mailbox name
A 1885      * @param  array $thread_tree Unsorted thread tree (rcube_imap_generic::thread() result)
1886      * @param  array $ids         Message IDs if we know what we need (e.g. search result)
59c216 1887      * @return array Sorted roots IDs
A 1888      * @access private
1889      */
1890     private function _sort_threads($mailbox, $thread_tree, $ids=NULL)
1891     {
1892         // THREAD=ORDEREDSUBJECT:     sorting by sent date of root message
1893         // THREAD=REFERENCES:     sorting by sent date of root message
1894         // THREAD=REFS:         sorting by the most recent date in each thread
1895         // default sorting
1896         if (!$this->sort_field || ($this->sort_field == 'date' && $this->threading == 'REFS')) {
1897             return array_keys((array)$thread_tree);
1898           }
1899         // here we'll implement REFS sorting, for performance reason
1900         else { // ($sort_field == 'date' && $this->threading != 'REFS')
1901             // use SORT command
c60978 1902             if ($this->get_capability('SORT') && 
A 1903                 ($a_index = $this->conn->sort($mailbox, $this->sort_field,
1904                     !empty($ids) ? $ids : ($this->skip_deleted ? 'UNDELETED' : ''))) !== false
1905             ) {
59c216 1906                 // return unsorted tree if we've got no index data
A 1907                 if (!$a_index)
1908                     return array_keys((array)$thread_tree);
1909             }
1910             else {
1911                 // fetch specified headers for all messages and sort them
1912                 $a_index = $this->conn->fetchHeaderIndex($mailbox, !empty($ids) ? $ids : "1:*",
1913                     $this->sort_field, $this->skip_deleted);
1914
1915                 // return unsorted tree if we've got no index data
1916                 if (!$a_index)
1917                     return array_keys((array)$thread_tree);
1918
1919                 asort($a_index); // ASC
1920                 $a_index = array_values($a_index);
1921             }
1922
1923             return $this->_sort_thread_refs($thread_tree, $a_index);
1924         }
1925     }
1926
1927
1928     /**
1929      * THREAD=REFS sorting implementation
1930      *
5c461b 1931      * @param  array $tree  Thread tree array (message identifiers as keys)
A 1932      * @param  array $index Array of sorted message identifiers
59c216 1933      * @return array   Array of sorted roots messages
A 1934      * @access private
1935      */
1936     private function _sort_thread_refs($tree, $index)
1937     {
1938         if (empty($tree))
1939             return array();
c43517 1940
59c216 1941         $index = array_combine(array_values($index), $index);
A 1942
1943         // assign roots
1944         foreach ($tree as $idx => $val) {
1945             $index[$idx] = $idx;
1946             if (!empty($val)) {
1947                 $idx_arr = array_keys_recursive($tree[$idx]);
1948                 foreach ($idx_arr as $subidx)
1949                     $index[$subidx] = $idx;
1950             }
1951         }
1952
c43517 1953         $index = array_values($index);
59c216 1954
A 1955         // create sorted array of roots
1956         $msg_index = array();
1957         if ($this->sort_order != 'DESC') {
1958             foreach ($index as $idx)
1959                 if (!isset($msg_index[$idx]))
1960                     $msg_index[$idx] = $idx;
1961             $msg_index = array_values($msg_index);
1962         }
1963         else {
1964             for ($x=count($index)-1; $x>=0; $x--)
1965                 if (!isset($msg_index[$index[$x]]))
1966                     $msg_index[$index[$x]] = $index[$x];
1967             $msg_index = array_reverse($msg_index);
1968         }
1969
1970         return $msg_index;
1971     }
1972
1973
1974     /**
1975      * Refresh saved search set
1976      *
1977      * @return array Current search set
1978      */
1979     function refresh_search()
1980     {
1981         if (!empty($this->search_string))
1982             $this->search_set = $this->search('', $this->search_string, $this->search_charset,
c60978 1983                 $this->search_sort_field, $this->search_threads, $this->search_sorted);
59c216 1984
A 1985         return $this->get_search_set();
1986     }
c43517 1987
A 1988
59c216 1989     /**
A 1990      * Check if the given message ID is part of the current search set
1991      *
5c461b 1992      * @param string $msgid Message id
59c216 1993      * @return boolean True on match or if no search request is stored
A 1994      */
1995     function in_searchset($msgid)
1996     {
1997         if (!empty($this->search_string)) {
1998             if ($this->search_threads)
1999                 return isset($this->search_set['depth']["$msgid"]);
2000             else
2001                 return in_array("$msgid", (array)$this->search_set, true);
2002         }
2003         else
2004             return true;
2005     }
2006
2007
2008     /**
2009      * Return message headers object of a specific message
2010      *
d08333 2011      * @param int     $id       Message ID
A 2012      * @param string  $mailbox  Mailbox to read from
2013      * @param boolean $is_uid   True if $id is the message UID
2014      * @param boolean $bodystr  True if we need also BODYSTRUCTURE in headers
59c216 2015      * @return object Message headers representation
A 2016      */
d08333 2017     function get_headers($id, $mailbox=null, $is_uid=true, $bodystr=false)
59c216 2018     {
d08333 2019         if (!strlen($mailbox)) {
A 2020             $mailbox = $this->mailbox;
2021         }
a03c98 2022         $uid = $is_uid ? $id : $this->_id2uid($id, $mailbox);
59c216 2023
A 2024         // get cached headers
2025         if ($uid && ($headers = &$this->get_cached_message($mailbox.'.msg', $uid)))
2026             return $headers;
2027
2028         $headers = $this->conn->fetchHeader(
103ddc 2029             $mailbox, $id, $is_uid, $bodystr, $this->get_fetch_headers());
59c216 2030
A 2031         // write headers cache
2032         if ($headers) {
2033             if ($headers->uid && $headers->id)
2034                 $this->uid_id_map[$mailbox][$headers->uid] = $headers->id;
2035
eacce9 2036             $this->add_message_cache($mailbox.'.msg', $headers->id, $headers, NULL, false, true);
59c216 2037         }
A 2038
2039         return $headers;
2040     }
2041
2042
2043     /**
2044      * Fetch body structure from the IMAP server and build
2045      * an object structure similar to the one generated by PEAR::Mail_mimeDecode
2046      *
5c461b 2047      * @param int    $uid           Message UID to fetch
A 2048      * @param string $structure_str Message BODYSTRUCTURE string (optional)
59c216 2049      * @return object rcube_message_part Message part tree or False on failure
A 2050      */
2051     function &get_structure($uid, $structure_str='')
2052     {
2053         $cache_key = $this->mailbox.'.msg';
2054         $headers = &$this->get_cached_message($cache_key, $uid);
2055
2056         // return cached message structure
2057         if (is_object($headers) && is_object($headers->structure)) {
2058             return $headers->structure;
2059         }
2060
70318e 2061         if (!$structure_str) {
59c216 2062             $structure_str = $this->conn->fetchStructureString($this->mailbox, $uid, true);
70318e 2063         }
A 2064         $structure = rcube_mime_struct::parseStructure($structure_str);
59c216 2065         $struct = false;
A 2066
2067         // parse structure and add headers
2068         if (!empty($structure)) {
2069             $headers = $this->get_headers($uid);
2070             $this->_msg_id = $headers->id;
2071
2072         // set message charset from message headers
2073         if ($headers->charset)
2074             $this->struct_charset = $headers->charset;
2075         else
2076             $this->struct_charset = $this->_structure_charset($structure);
2077
3c9d9a 2078         $headers->ctype = strtolower($headers->ctype);
A 2079
c43517 2080         // Here we can recognize malformed BODYSTRUCTURE and
59c216 2081         // 1. [@TODO] parse the message in other way to create our own message structure
A 2082         // 2. or just show the raw message body.
2083         // Example of structure for malformed MIME message:
3c9d9a 2084         // ("text" "plain" NIL NIL NIL "7bit" 2154 70 NIL NIL NIL)
A 2085         if ($headers->ctype && !is_array($structure[0]) && $headers->ctype != 'text/plain'
2086             && strtolower($structure[0].'/'.$structure[1]) == 'text/plain') {
2087             // we can handle single-part messages, by simple fix in structure (#1486898)
ecc28c 2088             if (preg_match('/^(text|application)\/(.*)/', $headers->ctype, $m)) {
3c9d9a 2089                 $structure[0] = $m[1];
A 2090                 $structure[1] = $m[2];
2091             }
2092             else
2093                 return false;
59c216 2094         }
A 2095
824144 2096         $struct = &$this->_structure_part($structure, 0, '', $headers);
59c216 2097         $struct->headers = get_object_vars($headers);
A 2098
2099         // don't trust given content-type
2100         if (empty($struct->parts) && !empty($struct->headers['ctype'])) {
2101             $struct->mime_id = '1';
2102             $struct->mimetype = strtolower($struct->headers['ctype']);
2103             list($struct->ctype_primary, $struct->ctype_secondary) = explode('/', $struct->mimetype);
2104         }
2105
2106         // write structure to cache
5cf5ee 2107         if ($this->messages_caching)
eacce9 2108             $this->add_message_cache($cache_key, $this->_msg_id, $headers, $struct,
A 2109                 $this->icache['message.id'][$uid], true);
59c216 2110         }
A 2111
2112         return $struct;
2113     }
2114
c43517 2115
59c216 2116     /**
A 2117      * Build message part object
2118      *
5c461b 2119      * @param array  $part
A 2120      * @param int    $count
2121      * @param string $parent
59c216 2122      * @access private
A 2123      */
253768 2124     function &_structure_part($part, $count=0, $parent='', $mime_headers=null)
59c216 2125     {
A 2126         $struct = new rcube_message_part;
2127         $struct->mime_id = empty($parent) ? (string)$count : "$parent.$count";
2128
2129         // multipart
2130         if (is_array($part[0])) {
2131             $struct->ctype_primary = 'multipart';
c43517 2132
95fd49 2133         /* RFC3501: BODYSTRUCTURE fields of multipart part
A 2134             part1 array
2135             part2 array
2136             part3 array
2137             ....
2138             1. subtype
2139             2. parameters (optional)
2140             3. description (optional)
2141             4. language (optional)
2142             5. location (optional)
2143         */
2144
59c216 2145             // find first non-array entry
A 2146             for ($i=1; $i<count($part); $i++) {
2147                 if (!is_array($part[$i])) {
2148                     $struct->ctype_secondary = strtolower($part[$i]);
2149                     break;
2150                 }
2151             }
c43517 2152
59c216 2153             $struct->mimetype = 'multipart/'.$struct->ctype_secondary;
A 2154
2155             // build parts list for headers pre-fetching
95fd49 2156             for ($i=0; $i<count($part); $i++) {
A 2157                 if (!is_array($part[$i]))
2158                     break;
2159                 // fetch message headers if message/rfc822
2160                 // or named part (could contain Content-Location header)
2161                 if (!is_array($part[$i][0])) {
2162                     $tmp_part_id = $struct->mime_id ? $struct->mime_id.'.'.($i+1) : $i+1;
2163                     if (strtolower($part[$i][0]) == 'message' && strtolower($part[$i][1]) == 'rfc822') {
2164                         $mime_part_headers[] = $tmp_part_id;
2165                     }
733ed0 2166                     else if (in_array('name', (array)$part[$i][2]) && empty($part[$i][3])) {
95fd49 2167                         $mime_part_headers[] = $tmp_part_id;
59c216 2168                     }
A 2169                 }
2170             }
c43517 2171
59c216 2172             // pre-fetch headers of all parts (in one command for better performance)
A 2173             // @TODO: we could do this before _structure_part() call, to fetch
2174             // headers for parts on all levels
2175             if ($mime_part_headers) {
2176                 $mime_part_headers = $this->conn->fetchMIMEHeaders($this->mailbox,
2177                     $this->_msg_id, $mime_part_headers);
2178             }
95fd49 2179
59c216 2180             $struct->parts = array();
A 2181             for ($i=0, $count=0; $i<count($part); $i++) {
95fd49 2182                 if (!is_array($part[$i]))
A 2183                     break;
2184                 $tmp_part_id = $struct->mime_id ? $struct->mime_id.'.'.($i+1) : $i+1;
2185                 $struct->parts[] = $this->_structure_part($part[$i], ++$count, $struct->mime_id,
253768 2186                     $mime_part_headers[$tmp_part_id]);
59c216 2187             }
A 2188
2189             return $struct;
2190         }
95fd49 2191
A 2192         /* RFC3501: BODYSTRUCTURE fields of non-multipart part
2193             0. type
2194             1. subtype
2195             2. parameters
2196             3. id
2197             4. description
2198             5. encoding
2199             6. size
2200           -- text
2201             7. lines
2202           -- message/rfc822
2203             7. envelope structure
2204             8. body structure
2205             9. lines
2206           --
2207             x. md5 (optional)
2208             x. disposition (optional)
2209             x. language (optional)
2210             x. location (optional)
2211         */
59c216 2212
A 2213         // regular part
2214         $struct->ctype_primary = strtolower($part[0]);
2215         $struct->ctype_secondary = strtolower($part[1]);
2216         $struct->mimetype = $struct->ctype_primary.'/'.$struct->ctype_secondary;
2217
2218         // read content type parameters
2219         if (is_array($part[2])) {
2220             $struct->ctype_parameters = array();
2221             for ($i=0; $i<count($part[2]); $i+=2)
2222                 $struct->ctype_parameters[strtolower($part[2][$i])] = $part[2][$i+1];
c43517 2223
59c216 2224             if (isset($struct->ctype_parameters['charset']))
A 2225                 $struct->charset = $struct->ctype_parameters['charset'];
2226         }
c43517 2227
824144 2228         // #1487700: workaround for lack of charset in malformed structure
A 2229         if (empty($struct->charset) && !empty($mime_headers) && $mime_headers->charset) {
2230             $struct->charset = $mime_headers->charset;
2231         }
2232
59c216 2233         // read content encoding
733ed0 2234         if (!empty($part[5])) {
59c216 2235             $struct->encoding = strtolower($part[5]);
A 2236             $struct->headers['content-transfer-encoding'] = $struct->encoding;
2237         }
c43517 2238
59c216 2239         // get part size
733ed0 2240         if (!empty($part[6]))
59c216 2241             $struct->size = intval($part[6]);
A 2242
2243         // read part disposition
95fd49 2244         $di = 8;
A 2245         if ($struct->ctype_primary == 'text') $di += 1;
2246         else if ($struct->mimetype == 'message/rfc822') $di += 3;
2247
2248         if (is_array($part[$di]) && count($part[$di]) == 2) {
59c216 2249             $struct->disposition = strtolower($part[$di][0]);
A 2250
2251             if (is_array($part[$di][1]))
2252                 for ($n=0; $n<count($part[$di][1]); $n+=2)
2253                     $struct->d_parameters[strtolower($part[$di][1][$n])] = $part[$di][1][$n+1];
2254         }
c43517 2255
95fd49 2256         // get message/rfc822's child-parts
59c216 2257         if (is_array($part[8]) && $di != 8) {
A 2258             $struct->parts = array();
95fd49 2259             for ($i=0, $count=0; $i<count($part[8]); $i++) {
A 2260                 if (!is_array($part[8][$i]))
2261                     break;
2262                 $struct->parts[] = $this->_structure_part($part[8][$i], ++$count, $struct->mime_id);
2263             }
59c216 2264         }
A 2265
2266         // get part ID
733ed0 2267         if (!empty($part[3])) {
59c216 2268             $struct->content_id = $part[3];
A 2269             $struct->headers['content-id'] = $part[3];
c43517 2270
59c216 2271             if (empty($struct->disposition))
A 2272                 $struct->disposition = 'inline';
2273         }
c43517 2274
59c216 2275         // fetch message headers if message/rfc822 or named part (could contain Content-Location header)
A 2276         if ($struct->ctype_primary == 'message' || ($struct->ctype_parameters['name'] && !$struct->content_id)) {
2277             if (empty($mime_headers)) {
2278                 $mime_headers = $this->conn->fetchPartHeader(
2279                     $this->mailbox, $this->_msg_id, false, $struct->mime_id);
2280             }
d755ea 2281
T 2282             if (is_string($mime_headers))
2283                 $struct->headers = $this->_parse_headers($mime_headers) + $struct->headers;
2284             else if (is_object($mime_headers))
2285                 $struct->headers = get_object_vars($mime_headers) + $struct->headers;
59c216 2286
253768 2287             // get real content-type of message/rfc822
59c216 2288             if ($struct->mimetype == 'message/rfc822') {
253768 2289                 // single-part
A 2290                 if (!is_array($part[8][0]))
2291                     $struct->real_mimetype = strtolower($part[8][0] . '/' . $part[8][1]);
2292                 // multi-part
2293                 else {
2294                     for ($n=0; $n<count($part[8]); $n++)
2295                         if (!is_array($part[8][$n]))
2296                             break;
2297                     $struct->real_mimetype = 'multipart/' . strtolower($part[8][$n]);
c43517 2298                 }
59c216 2299             }
A 2300
253768 2301             if ($struct->ctype_primary == 'message' && empty($struct->parts)) {
A 2302                 if (is_array($part[8]) && $di != 8)
2303                     $struct->parts[] = $this->_structure_part($part[8], ++$count, $struct->mime_id);
2304             }
59c216 2305         }
A 2306
2307         // normalize filename property
2308         $this->_set_part_filename($struct, $mime_headers);
2309
2310         return $struct;
2311     }
c43517 2312
59c216 2313
A 2314     /**
c43517 2315      * Set attachment filename from message part structure
59c216 2316      *
5c461b 2317      * @param  rcube_message_part $part    Part object
A 2318      * @param  string             $headers Part's raw headers
29983c 2319      * @access private
59c216 2320      */
A 2321     private function _set_part_filename(&$part, $headers=null)
2322     {
2323         if (!empty($part->d_parameters['filename']))
2324             $filename_mime = $part->d_parameters['filename'];
2325         else if (!empty($part->d_parameters['filename*']))
2326             $filename_encoded = $part->d_parameters['filename*'];
2327         else if (!empty($part->ctype_parameters['name*']))
2328             $filename_encoded = $part->ctype_parameters['name*'];
2329         // RFC2231 value continuations
2330         // TODO: this should be rewrited to support RFC2231 4.1 combinations
2331         else if (!empty($part->d_parameters['filename*0'])) {
2332             $i = 0;
2333             while (isset($part->d_parameters['filename*'.$i])) {
2334                 $filename_mime .= $part->d_parameters['filename*'.$i];
2335                 $i++;
2336             }
2337             // some servers (eg. dovecot-1.x) have no support for parameter value continuations
2338             // we must fetch and parse headers "manually"
2339             if ($i<2) {
2340                 if (!$headers) {
2341                     $headers = $this->conn->fetchPartHeader(
2342                         $this->mailbox, $this->_msg_id, false, $part->mime_id);
2343                 }
2344                 $filename_mime = '';
2345                 $i = 0;
2346                 while (preg_match('/filename\*'.$i.'\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
2347                     $filename_mime .= $matches[1];
2348                     $i++;
2349                 }
2350             }
2351         }
2352         else if (!empty($part->d_parameters['filename*0*'])) {
2353             $i = 0;
2354             while (isset($part->d_parameters['filename*'.$i.'*'])) {
2355                 $filename_encoded .= $part->d_parameters['filename*'.$i.'*'];
2356                 $i++;
2357             }
2358             if ($i<2) {
2359                 if (!$headers) {
2360                     $headers = $this->conn->fetchPartHeader(
2361                             $this->mailbox, $this->_msg_id, false, $part->mime_id);
2362                 }
2363                 $filename_encoded = '';
2364                 $i = 0; $matches = array();
2365                 while (preg_match('/filename\*'.$i.'\*\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
2366                     $filename_encoded .= $matches[1];
2367                     $i++;
2368                 }
2369             }
2370         }
2371         else if (!empty($part->ctype_parameters['name*0'])) {
2372             $i = 0;
2373             while (isset($part->ctype_parameters['name*'.$i])) {
2374                 $filename_mime .= $part->ctype_parameters['name*'.$i];
2375                 $i++;
2376             }
2377             if ($i<2) {
2378                 if (!$headers) {
2379                     $headers = $this->conn->fetchPartHeader(
2380                         $this->mailbox, $this->_msg_id, false, $part->mime_id);
2381                 }
2382                 $filename_mime = '';
2383                 $i = 0; $matches = array();
2384                 while (preg_match('/\s+name\*'.$i.'\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
2385                     $filename_mime .= $matches[1];
2386                     $i++;
2387                 }
2388             }
2389         }
2390         else if (!empty($part->ctype_parameters['name*0*'])) {
2391             $i = 0;
2392             while (isset($part->ctype_parameters['name*'.$i.'*'])) {
2393                 $filename_encoded .= $part->ctype_parameters['name*'.$i.'*'];
2394                 $i++;
2395             }
2396             if ($i<2) {
2397                 if (!$headers) {
2398                     $headers = $this->conn->fetchPartHeader(
2399                         $this->mailbox, $this->_msg_id, false, $part->mime_id);
2400                 }
2401                 $filename_encoded = '';
2402                 $i = 0; $matches = array();
2403                 while (preg_match('/\s+name\*'.$i.'\*\s*=\s*"*([^"\n;]+)[";]*/', $headers, $matches)) {
2404                     $filename_encoded .= $matches[1];
2405                     $i++;
2406                 }
2407             }
2408         }
2409         // read 'name' after rfc2231 parameters as it may contains truncated filename (from Thunderbird)
2410         else if (!empty($part->ctype_parameters['name']))
2411             $filename_mime = $part->ctype_parameters['name'];
2412         // Content-Disposition
2413         else if (!empty($part->headers['content-description']))
2414             $filename_mime = $part->headers['content-description'];
2415         else
2416             return;
2417
2418         // decode filename
2419         if (!empty($filename_mime)) {
c43517 2420             $part->filename = rcube_imap::decode_mime_string($filename_mime,
59c216 2421                 $part->charset ? $part->charset : ($this->struct_charset ? $this->struct_charset :
A 2422                 rc_detect_encoding($filename_mime, $this->default_charset)));
c43517 2423         }
59c216 2424         else if (!empty($filename_encoded)) {
A 2425             // decode filename according to RFC 2231, Section 4
2426             if (preg_match("/^([^']*)'[^']*'(.*)$/", $filename_encoded, $fmatches)) {
2427                 $filename_charset = $fmatches[1];
2428                 $filename_encoded = $fmatches[2];
2429             }
2430             $part->filename = rcube_charset_convert(urldecode($filename_encoded), $filename_charset);
2431         }
2432     }
2433
2434
2435     /**
2436      * Get charset name from message structure (first part)
2437      *
5c461b 2438      * @param  array $structure Message structure
59c216 2439      * @return string Charset name
29983c 2440      * @access private
59c216 2441      */
29983c 2442     private function _structure_charset($structure)
59c216 2443     {
A 2444         while (is_array($structure)) {
2445             if (is_array($structure[2]) && $structure[2][0] == 'charset')
2446                 return $structure[2][1];
2447             $structure = $structure[0];
2448         }
c43517 2449     }
b20bca 2450
A 2451
59c216 2452     /**
A 2453      * Fetch message body of a specific message from the server
2454      *
5c461b 2455      * @param  int                $uid    Message UID
A 2456      * @param  string             $part   Part number
2457      * @param  rcube_message_part $o_part Part object created by get_structure()
2458      * @param  mixed              $print  True to print part, ressource to write part contents in
2459      * @param  resource           $fp     File pointer to save the message part
8abc17 2460      * @param  boolean            $skip_charset_conv Disables charset conversion
A 2461      *
59c216 2462      * @return string Message/part body if not printed
A 2463      */
8abc17 2464     function &get_message_part($uid, $part=1, $o_part=NULL, $print=NULL, $fp=NULL, $skip_charset_conv=false)
8d4bcd 2465     {
59c216 2466         // get part encoding if not provided
A 2467         if (!is_object($o_part)) {
c43517 2468             $structure_str = $this->conn->fetchStructureString($this->mailbox, $uid, true);
70318e 2469             $structure = new rcube_mime_struct();
59c216 2470             // error or message not found
70318e 2471             if (!$structure->loadStructure($structure_str)) {
59c216 2472                 return false;
70318e 2473             }
5f571e 2474
59c216 2475             $o_part = new rcube_message_part;
70318e 2476             $o_part->ctype_primary = strtolower($structure->getPartType($part));
A 2477             $o_part->encoding      = strtolower($structure->getPartEncoding($part));
2478             $o_part->charset       = $structure->getPartCharset($part);
59c216 2479         }
c43517 2480
59c216 2481         // TODO: Add caching for message parts
8d4bcd 2482
9840ab 2483         if (!$part) {
A 2484             $part = 'TEXT';
2485         }
73ba7c 2486
59c216 2487         $body = $this->conn->handlePartBody($this->mailbox, $uid, true, $part,
A 2488             $o_part->encoding, $print, $fp);
5f571e 2489
9840ab 2490         if ($fp || $print) {
59c216 2491             return true;
9840ab 2492         }
8d4bcd 2493
8abc17 2494         // convert charset (if text or message part)
A 2495         if ($body && !$skip_charset_conv &&
2496             preg_match('/^(text|message)$/', $o_part->ctype_primary)
9840ab 2497         ) {
8abc17 2498             if (!$o_part->charset || strtoupper($o_part->charset) == 'US-ASCII') {
A 2499                 $o_part->charset = $this->default_charset;
2500             }
59c216 2501             $body = rcube_charset_convert($body, $o_part->charset);
A 2502         }
c43517 2503
59c216 2504         return $body;
4e17e6 2505     }
T 2506
2507
59c216 2508     /**
A 2509      * Fetch message body of a specific message from the server
2510      *
5c461b 2511      * @param  int    $uid  Message UID
A 2512      * @return string $part Message/part body
59c216 2513      * @see    rcube_imap::get_message_part()
A 2514      */
2515     function &get_body($uid, $part=1)
8d4bcd 2516     {
59c216 2517         $headers = $this->get_headers($uid);
A 2518         return rcube_charset_convert($this->get_message_part($uid, $part, NULL),
2519             $headers->charset ? $headers->charset : $this->default_charset);
8d4bcd 2520     }
T 2521
2522
59c216 2523     /**
a208a4 2524      * Returns the whole message source as string (or saves to a file)
59c216 2525      *
a208a4 2526      * @param int      $uid Message UID
A 2527      * @param resource $fp  File pointer to save the message
2528      *
59c216 2529      * @return string Message source string
A 2530      */
a208a4 2531     function &get_raw_body($uid, $fp=null)
4e17e6 2532     {
a208a4 2533         return $this->conn->handlePartBody($this->mailbox, $uid,
A 2534             true, null, null, false, $fp);
4e17e6 2535     }
e5686f 2536
A 2537
59c216 2538     /**
A 2539      * Returns the message headers as string
2540      *
5c461b 2541      * @param int $uid  Message UID
59c216 2542      * @return string Message headers string
A 2543      */
2544     function &get_raw_headers($uid)
e5686f 2545     {
59c216 2546         return $this->conn->fetchPartHeader($this->mailbox, $uid, true);
e5686f 2547     }
c43517 2548
8d4bcd 2549
59c216 2550     /**
A 2551      * Sends the whole message source to stdout
2552      *
5c461b 2553      * @param int $uid Message UID
c43517 2554      */
59c216 2555     function print_raw_body($uid)
8d4bcd 2556     {
59c216 2557         $this->conn->handlePartBody($this->mailbox, $uid, true, NULL, NULL, true);
8d4bcd 2558     }
4e17e6 2559
T 2560
59c216 2561     /**
A 2562      * Set message flag to one or several messages
2563      *
5c461b 2564      * @param mixed   $uids       Message UIDs as array or comma-separated string, or '*'
A 2565      * @param string  $flag       Flag to set: SEEN, UNDELETED, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
d08333 2566      * @param string  $mailbox    Folder name
5c461b 2567      * @param boolean $skip_cache True to skip message cache clean up
d08333 2568      *
c309cd 2569      * @return boolean  Operation status
59c216 2570      */
d08333 2571     function set_flag($uids, $flag, $mailbox=null, $skip_cache=false)
4e17e6 2572     {
d08333 2573         if (!strlen($mailbox)) {
A 2574             $mailbox = $this->mailbox;
2575         }
48958e 2576
59c216 2577         $flag = strtoupper($flag);
A 2578         list($uids, $all_mode) = $this->_parse_uids($uids, $mailbox);
cff886 2579
59c216 2580         if (strpos($flag, 'UN') === 0)
A 2581             $result = $this->conn->unflag($mailbox, $uids, substr($flag, 2));
cff886 2582         else
59c216 2583             $result = $this->conn->flag($mailbox, $uids, $flag);
fa4cd2 2584
c309cd 2585         if ($result) {
59c216 2586             // reload message headers if cached
5cf5ee 2587             if ($this->messages_caching && !$skip_cache) {
59c216 2588                 $cache_key = $mailbox.'.msg';
A 2589                 if ($all_mode)
2590                     $this->clear_message_cache($cache_key);
2591                 else
2592                     $this->remove_message_cache($cache_key, explode(',', $uids));
15e00b 2593             }
c309cd 2594
A 2595             // clear cached counters
2596             if ($flag == 'SEEN' || $flag == 'UNSEEN') {
2597                 $this->_clear_messagecount($mailbox, 'SEEN');
2598                 $this->_clear_messagecount($mailbox, 'UNSEEN');
2599             }
2600             else if ($flag == 'DELETED') {
2601                 $this->_clear_messagecount($mailbox, 'DELETED');
2602             }
4e17e6 2603         }
T 2604
59c216 2605         return $result;
4e17e6 2606     }
T 2607
fa4cd2 2608
59c216 2609     /**
A 2610      * Remove message flag for one or several messages
2611      *
d08333 2612      * @param mixed  $uids    Message UIDs as array or comma-separated string, or '*'
A 2613      * @param string $flag    Flag to unset: SEEN, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
2614      * @param string $mailbox Folder name
2615      *
59c216 2616      * @return int   Number of flagged messages, -1 on failure
A 2617      * @see set_flag
2618      */
d08333 2619     function unset_flag($uids, $flag, $mailbox=null)
fa4cd2 2620     {
d08333 2621         return $this->set_flag($uids, 'UN'.$flag, $mailbox);
15e00b 2622     }
A 2623
2624
59c216 2625     /**
A 2626      * Append a mail message (source) to a specific mailbox
2627      *
d08333 2628      * @param string  $mailbox Target mailbox
A 2629      * @param string  $message The message source string or filename
2630      * @param string  $headers Headers string if $message contains only the body
2631      * @param boolean $is_file True if $message is a filename
59c216 2632      *
A 2633      * @return boolean True on success, False on error
2634      */
d08333 2635     function save_message($mailbox, &$message, $headers='', $is_file=false)
15e00b 2636     {
d08333 2637         if (!strlen($mailbox)) {
A 2638             $mailbox = $this->mailbox;
2639         }
4e17e6 2640
59c216 2641         // make sure mailbox exists
d08333 2642         if ($this->mailbox_exists($mailbox)) {
272a7e 2643             if ($is_file)
A 2644                 $saved = $this->conn->appendFromFile($mailbox, $message, $headers);
59c216 2645             else
A 2646                 $saved = $this->conn->append($mailbox, $message);
4e17e6 2647         }
f8a846 2648
59c216 2649         if ($saved) {
A 2650             // increase messagecount of the target mailbox
2651             $this->_set_messagecount($mailbox, 'ALL', 1);
8d4bcd 2652         }
59c216 2653
A 2654         return $saved;
8d4bcd 2655     }
T 2656
2657
59c216 2658     /**
A 2659      * Move a message from one mailbox to another
2660      *
5c461b 2661      * @param mixed  $uids      Message UIDs as array or comma-separated string, or '*'
A 2662      * @param string $to_mbox   Target mailbox
2663      * @param string $from_mbox Source mailbox
59c216 2664      * @return boolean True on success, False on error
A 2665      */
2666     function move_message($uids, $to_mbox, $from_mbox='')
4e17e6 2667     {
d08333 2668         if (!strlen($from_mbox)) {
A 2669             $from_mbox = $this->mailbox;
2670         }
c58c0a 2671
d08333 2672         if ($to_mbox === $from_mbox) {
af3c04 2673             return false;
d08333 2674         }
af3c04 2675
59c216 2676         list($uids, $all_mode) = $this->_parse_uids($uids, $from_mbox);
41fa0b 2677
59c216 2678         // exit if no message uids are specified
A 2679         if (empty($uids))
2680             return false;
2681
2682         // make sure mailbox exists
d08333 2683         if ($to_mbox != 'INBOX' && !$this->mailbox_exists($to_mbox)) {
A 2684             if (in_array($to_mbox, $this->default_folders))
2685                 $this->create_mailbox($to_mbox, true);
59c216 2686             else
A 2687                 return false;
2688         }
2689
2690         $config = rcmail::get_instance()->config;
d08333 2691         $to_trash = $to_mbox == $config->get('trash_mbox');
A 2692
2693         // flag messages as read before moving them
2694         if ($to_trash && $config->get('read_when_deleted')) {
59c216 2695             // don't flush cache (4th argument)
d08333 2696             $this->set_flag($uids, 'SEEN', $from_mbox, true);
59c216 2697         }
A 2698
2699         // move messages
93272e 2700         $moved = $this->conn->move($uids, $from_mbox, $to_mbox);
59c216 2701
A 2702         // send expunge command in order to have the moved message
2703         // really deleted from the source mailbox
2704         if ($moved) {
2705             $this->_expunge($from_mbox, false, $uids);
2706             $this->_clear_messagecount($from_mbox);
2707             $this->_clear_messagecount($to_mbox);
2708         }
2709         // moving failed
d08333 2710         else if ($to_trash && $config->get('delete_always', false)) {
A 2711             $moved = $this->delete_message($uids, $from_mbox);
59c216 2712         }
9e81b5 2713
59c216 2714         if ($moved) {
A 2715             // unset threads internal cache
2716             unset($this->icache['threads']);
2717
2718             // remove message ids from search set
2719             if ($this->search_set && $from_mbox == $this->mailbox) {
2720                 // threads are too complicated to just remove messages from set
2721                 if ($this->search_threads || $all_mode)
2722                     $this->refresh_search();
2723                 else {
2724                     $uids = explode(',', $uids);
2725                     foreach ($uids as $uid)
2726                         $a_mids[] = $this->_uid2id($uid, $from_mbox);
2727                     $this->search_set = array_diff($this->search_set, $a_mids);
2728                 }
2729             }
2730
2731             // update cached message headers
2732             $cache_key = $from_mbox.'.msg';
2733             if ($all_mode || ($start_index = $this->get_message_cache_index_min($cache_key, $uids))) {
2734                 // clear cache from the lowest index on
2735                 $this->clear_message_cache($cache_key, $all_mode ? 1 : $start_index);
2736             }
2737         }
2738
2739         return $moved;
2740     }
2741
2742
2743     /**
2744      * Copy a message from one mailbox to another
2745      *
5c461b 2746      * @param mixed  $uids      Message UIDs as array or comma-separated string, or '*'
A 2747      * @param string $to_mbox   Target mailbox
2748      * @param string $from_mbox Source mailbox
59c216 2749      * @return boolean True on success, False on error
A 2750      */
2751     function copy_message($uids, $to_mbox, $from_mbox='')
2752     {
d08333 2753         if (!strlen($from_mbox)) {
A 2754             $from_mbox = $this->mailbox;
2755         }
59c216 2756
A 2757         list($uids, $all_mode) = $this->_parse_uids($uids, $from_mbox);
2758
2759         // exit if no message uids are specified
93272e 2760         if (empty($uids)) {
59c216 2761             return false;
93272e 2762         }
59c216 2763
A 2764         // make sure mailbox exists
d08333 2765         if ($to_mbox != 'INBOX' && !$this->mailbox_exists($to_mbox)) {
A 2766             if (in_array($to_mbox, $this->default_folders))
2767                 $this->create_mailbox($to_mbox, true);
59c216 2768             else
A 2769                 return false;
2770         }
2771
2772         // copy messages
93272e 2773         $copied = $this->conn->copy($uids, $from_mbox, $to_mbox);
59c216 2774
A 2775         if ($copied) {
2776             $this->_clear_messagecount($to_mbox);
2777         }
2778
2779         return $copied;
2780     }
2781
2782
2783     /**
2784      * Mark messages as deleted and expunge mailbox
2785      *
d08333 2786      * @param mixed  $uids    Message UIDs as array or comma-separated string, or '*'
A 2787      * @param string $mailbox Source mailbox
2788      *
59c216 2789      * @return boolean True on success, False on error
A 2790      */
d08333 2791     function delete_message($uids, $mailbox='')
59c216 2792     {
d08333 2793         if (!strlen($mailbox)) {
A 2794             $mailbox = $this->mailbox;
2795         }
59c216 2796
A 2797         list($uids, $all_mode) = $this->_parse_uids($uids, $mailbox);
2798
2799         // exit if no message uids are specified
2800         if (empty($uids))
2801             return false;
2802
2803         $deleted = $this->conn->delete($mailbox, $uids);
2804
2805         if ($deleted) {
2806             // send expunge command in order to have the deleted message
2807             // really deleted from the mailbox
2808             $this->_expunge($mailbox, false, $uids);
2809             $this->_clear_messagecount($mailbox);
2810             unset($this->uid_id_map[$mailbox]);
2811
2812             // unset threads internal cache
2813             unset($this->icache['threads']);
c43517 2814
59c216 2815             // remove message ids from search set
A 2816             if ($this->search_set && $mailbox == $this->mailbox) {
2817                 // threads are too complicated to just remove messages from set
2818                 if ($this->search_threads || $all_mode)
2819                     $this->refresh_search();
2820                 else {
2821                     $uids = explode(',', $uids);
2822                     foreach ($uids as $uid)
2823                         $a_mids[] = $this->_uid2id($uid, $mailbox);
2824                     $this->search_set = array_diff($this->search_set, $a_mids);
2825                 }
2826             }
2827
2828             // remove deleted messages from cache
2829             $cache_key = $mailbox.'.msg';
2830             if ($all_mode || ($start_index = $this->get_message_cache_index_min($cache_key, $uids))) {
2831                 // clear cache from the lowest index on
2832                 $this->clear_message_cache($cache_key, $all_mode ? 1 : $start_index);
2833             }
2834         }
2835
2836         return $deleted;
2837     }
2838
2839
2840     /**
2841      * Clear all messages in a specific mailbox
2842      *
d08333 2843      * @param string $mailbox Mailbox name
A 2844      *
59c216 2845      * @return int Above 0 on success
A 2846      */
d08333 2847     function clear_mailbox($mailbox=null)
59c216 2848     {
d08333 2849         if (!strlen($mailbox)) {
A 2850             $mailbox = $this->mailbox;
2851         }
c43517 2852
01bdfd 2853         // SELECT will set messages count for clearFolder()
A 2854         if ($this->conn->select($mailbox)) {
2855             $cleared = $this->conn->clearFolder($mailbox);
4e17e6 2856         }
c43517 2857
59c216 2858         // make sure the message count cache is cleared as well
A 2859         if ($cleared) {
c43517 2860             $this->clear_message_cache($mailbox.'.msg');
59c216 2861             $a_mailbox_cache = $this->get_cache('messagecount');
A 2862             unset($a_mailbox_cache[$mailbox]);
2863             $this->update_cache('messagecount', $a_mailbox_cache);
2864         }
c43517 2865
59c216 2866         return $cleared;
A 2867     }
2868
2869
2870     /**
2871      * Send IMAP expunge command and clear cache
2872      *
d08333 2873      * @param string  $mailbox     Mailbox name
5c461b 2874      * @param boolean $clear_cache False if cache should not be cleared
d08333 2875      *
59c216 2876      * @return boolean True on success
A 2877      */
d08333 2878     function expunge($mailbox='', $clear_cache=true)
59c216 2879     {
d08333 2880         if (!strlen($mailbox)) {
A 2881             $mailbox = $this->mailbox;
2882         }
2883
59c216 2884         return $this->_expunge($mailbox, $clear_cache);
A 2885     }
2886
2887
2888     /**
2889      * Send IMAP expunge command and clear cache
2890      *
5c461b 2891      * @param string  $mailbox     Mailbox name
A 2892      * @param boolean $clear_cache False if cache should not be cleared
2893      * @param mixed   $uids        Message UIDs as array or comma-separated string, or '*'
59c216 2894      * @return boolean True on success
A 2895      * @access private
2896      * @see rcube_imap::expunge()
2897      */
2898     private function _expunge($mailbox, $clear_cache=true, $uids=NULL)
2899     {
c43517 2900         if ($uids && $this->get_capability('UIDPLUS'))
59c216 2901             $a_uids = is_array($uids) ? join(',', $uids) : $uids;
A 2902         else
2903             $a_uids = NULL;
2904
90f81a 2905         // force mailbox selection and check if mailbox is writeable
A 2906         // to prevent a situation when CLOSE is executed on closed
2907         // or EXPUNGE on read-only mailbox
2908         $result = $this->conn->select($mailbox);
2909         if (!$result) {
2910             return false;
2911         }
2912         if (!$this->conn->data['READ-WRITE']) {
2913             $this->conn->setError(rcube_imap_generic::ERROR_READONLY, "Mailbox is read-only");
2914             return false;
2915         }
2916
e232ac 2917         // CLOSE(+SELECT) should be faster than EXPUNGE
A 2918         if (empty($a_uids) || $a_uids == '1:*')
2919             $result = $this->conn->close();
2920         else
2921             $result = $this->conn->expunge($mailbox, $a_uids);
59c216 2922
854cf2 2923         if ($result && $clear_cache) {
59c216 2924             $this->clear_message_cache($mailbox.'.msg');
A 2925             $this->_clear_messagecount($mailbox);
2926         }
c43517 2927
59c216 2928         return $result;
A 2929     }
2930
2931
2932     /**
2933      * Parse message UIDs input
2934      *
5c461b 2935      * @param mixed  $uids    UIDs array or comma-separated list or '*' or '1:*'
A 2936      * @param string $mailbox Mailbox name
c43517 2937      * @return array Two elements array with UIDs converted to list and ALL flag
59c216 2938      * @access private
A 2939      */
2940     private function _parse_uids($uids, $mailbox)
2941     {
2942         if ($uids === '*' || $uids === '1:*') {
2943             if (empty($this->search_set)) {
2944                 $uids = '1:*';
2945                 $all = true;
2946             }
2947             // get UIDs from current search set
2948             // @TODO: skip fetchUIDs() and work with IDs instead of UIDs (?)
2949             else {
2950                 if ($this->search_threads)
2951                     $uids = $this->conn->fetchUIDs($mailbox, array_keys($this->search_set['depth']));
2952                 else
2953                     $uids = $this->conn->fetchUIDs($mailbox, $this->search_set);
c43517 2954
59c216 2955                 // save ID-to-UID mapping in local cache
A 2956                 if (is_array($uids))
2957                     foreach ($uids as $id => $uid)
2958                         $this->uid_id_map[$mailbox][$uid] = $id;
2959
2960                 $uids = join(',', $uids);
2961             }
2962         }
2963         else {
2964             if (is_array($uids))
2965                 $uids = join(',', $uids);
2966
2967             if (preg_match('/[^0-9,]/', $uids))
2968                 $uids = '';
2969         }
2970
2971         return array($uids, (bool) $all);
2972     }
2973
2974
2975     /**
2976      * Translate UID to message ID
2977      *
d08333 2978      * @param int    $uid     Message UID
A 2979      * @param string $mailbox Mailbox name
2980      *
59c216 2981      * @return int   Message ID
A 2982      */
d08333 2983     function get_id($uid, $mailbox=null)
59c216 2984     {
d08333 2985         if (!strlen($mailbox)) {
A 2986             $mailbox = $this->mailbox;
2987         }
2988
59c216 2989         return $this->_uid2id($uid, $mailbox);
A 2990     }
2991
2992
2993     /**
2994      * Translate message number to UID
2995      *
d08333 2996      * @param int    $id      Message ID
A 2997      * @param string $mailbox Mailbox name
2998      *
59c216 2999      * @return int   Message UID
A 3000      */
d08333 3001     function get_uid($id, $mailbox=null)
59c216 3002     {
d08333 3003         if (!strlen($mailbox)) {
A 3004             $mailbox = $this->mailbox;
3005         }
3006
59c216 3007         return $this->_id2uid($id, $mailbox);
A 3008     }
3009
3010
3011
3012     /* --------------------------------
3013      *        folder managment
3014      * --------------------------------*/
3015
3016     /**
6f4e7d 3017      * Public method for listing subscribed folders
59c216 3018      *
5c461b 3019      * @param   string  $root   Optional root folder
94bdcc 3020      * @param   string  $name   Optional name pattern
A 3021      * @param   string  $filter Optional filter
d08333 3022      *
59c216 3023      * @return  array   List of mailboxes/folders
A 3024      * @access  public
3025      */
94bdcc 3026     function list_mailboxes($root='', $name='*', $filter=null)
59c216 3027     {
94bdcc 3028         $a_mboxes = $this->_list_mailboxes($root, $name, $filter);
59c216 3029
A 3030         // INBOX should always be available
94bdcc 3031         if ((!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)) {
d08333 3032             array_unshift($a_mboxes, 'INBOX');
94bdcc 3033         }
59c216 3034
A 3035         // sort mailboxes
d08333 3036         $a_mboxes = $this->_sort_mailbox_list($a_mboxes);
59c216 3037
d08333 3038         return $a_mboxes;
59c216 3039     }
A 3040
3041
3042     /**
3043      * Private method for mailbox listing
3044      *
5c461b 3045      * @param   string  $root   Optional root folder
94bdcc 3046      * @param   string  $name   Optional name pattern
A 3047      * @param   mixed   $filter Optional filter
3048      *
59c216 3049      * @return  array   List of mailboxes/folders
A 3050      * @see     rcube_imap::list_mailboxes()
3051      * @access  private
3052      */
94bdcc 3053     private function _list_mailboxes($root='', $name='*', $filter=null)
59c216 3054     {
ac3ad6 3055         $cache_key = $root.':'.$name;
94bdcc 3056         if (!empty($filter)) {
ac3ad6 3057             $cache_key .= ':'.(is_string($filter) ? $filter : serialize($filter));
94bdcc 3058         }
A 3059
ac3ad6 3060         $cache_key = 'mailboxes.'.md5($cache_key);
A 3061
c43517 3062         // get cached folder list
94bdcc 3063         $a_mboxes = $this->get_cache($cache_key);
A 3064         if (is_array($a_mboxes)) {
59c216 3065             return $a_mboxes;
94bdcc 3066         }
59c216 3067
6f4e7d 3068         $a_defaults = $a_out = array();
A 3069
59c216 3070         // Give plugins a chance to provide a list of mailboxes
e6ce00 3071         $data = rcmail::get_instance()->plugins->exec_hook('mailboxes_list',
94bdcc 3072             array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LSUB'));
c43517 3073
59c216 3074         if (isset($data['folders'])) {
A 3075             $a_folders = $data['folders'];
3076         }
3077         else {
3870be 3078             // Server supports LIST-EXTENDED, we can use selection options
f75f65 3079             $config = rcmail::get_instance()->config;
A 3080             // #1486225: Some dovecot versions returns wrong result using LIST-EXTENDED
3081             if (!$config->get('imap_force_lsub') && $this->get_capability('LIST-EXTENDED')) {
3870be 3082                 // This will also set mailbox options, LSUB doesn't do that
94bdcc 3083                 $a_folders = $this->conn->listMailboxes($root, $name,
3870be 3084                     NULL, array('SUBSCRIBED'));
A 3085
189a0a 3086                 // unsubscribe non-existent folders, remove from the list
A 3087                 if (is_array($a_folders) && $name == '*') {
3870be 3088                     foreach ($a_folders as $idx => $folder) {
A 3089                         if ($this->conn->data['LIST'] && ($opts = $this->conn->data['LIST'][$folder])
3090                             && in_array('\\NonExistent', $opts)
3091                         ) {
189a0a 3092                             $this->conn->unsubscribe($folder);
3870be 3093                             unset($a_folders[$idx]);
189a0a 3094                         }
3870be 3095                     }
A 3096                 }
3097             }
3098             // retrieve list of folders from IMAP server using LSUB
3099             else {
94bdcc 3100                 $a_folders = $this->conn->listSubscribed($root, $name);
189a0a 3101
A 3102                 // unsubscribe non-existent folders, remove from the list
3103                 if (is_array($a_folders) && $name == '*') {
3104                     foreach ($a_folders as $idx => $folder) {
3105                         if ($this->conn->data['LIST'] && ($opts = $this->conn->data['LIST'][$folder])
3106                             && in_array('\\Noselect', $opts)
3107                         ) {
3108                             // Some servers returns \Noselect for existing folders
3109                             if (!$this->mailbox_exists($folder)) {
3110                                 $this->conn->unsubscribe($folder);
3111                                 unset($a_folders[$idx]);
3112                             }
3113                         }
3114                     }
3115                 }
3870be 3116             }
59c216 3117         }
c43517 3118
94bdcc 3119         if (!is_array($a_folders) || !sizeof($a_folders)) {
59c216 3120             $a_folders = array();
94bdcc 3121         }
59c216 3122
A 3123         // write mailboxlist to cache
94bdcc 3124         $this->update_cache($cache_key, $a_folders);
c43517 3125
59c216 3126         return $a_folders;
A 3127     }
3128
3129
3130     /**
3131      * Get a list of all folders available on the IMAP server
c43517 3132      *
5c461b 3133      * @param string $root   IMAP root dir
94bdcc 3134      * @param string  $name   Optional name pattern
A 3135      * @param mixed   $filter Optional filter
3136      *
59c216 3137      * @return array Indexed array with folder names
A 3138      */
94bdcc 3139     function list_unsubscribed($root='', $name='*', $filter=null)
59c216 3140     {
94bdcc 3141         // @TODO: caching
6f4e7d 3142         // Give plugins a chance to provide a list of mailboxes
e6ce00 3143         $data = rcmail::get_instance()->plugins->exec_hook('mailboxes_list',
94bdcc 3144             array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LIST'));
c43517 3145
6f4e7d 3146         if (isset($data['folders'])) {
A 3147             $a_mboxes = $data['folders'];
3148         }
3149         else {
3150             // retrieve list of folders from IMAP server
94bdcc 3151             $a_mboxes = $this->conn->listMailboxes($root, $name);
6f4e7d 3152         }
64e3e8 3153
d08333 3154         if (!is_array($a_mboxes)) {
6f4e7d 3155             $a_mboxes = array();
59c216 3156         }
f0485a 3157
A 3158         // INBOX should always be available
94bdcc 3159         if ((!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)) {
d08333 3160             array_unshift($a_mboxes, 'INBOX');
94bdcc 3161         }
59c216 3162
A 3163         // filter folders and sort them
d08333 3164         $a_mboxes = $this->_sort_mailbox_list($a_mboxes);
A 3165
3166         return $a_mboxes;
59c216 3167     }
A 3168
3169
3170     /**
3171      * Get mailbox quota information
3172      * added by Nuny
c43517 3173      *
59c216 3174      * @return mixed Quota info or False if not supported
A 3175      */
3176     function get_quota()
3177     {
3178         if ($this->get_capability('QUOTA'))
3179             return $this->conn->getQuota();
c43517 3180
59c216 3181         return false;
A 3182     }
3183
3184
3185     /**
af3c04 3186      * Get mailbox size (size of all messages in a mailbox)
A 3187      *
d08333 3188      * @param string $mailbox Mailbox name
A 3189      *
af3c04 3190      * @return int Mailbox size in bytes, False on error
A 3191      */
d08333 3192     function get_mailbox_size($mailbox)
af3c04 3193     {
A 3194         // @TODO: could we try to use QUOTA here?
d08333 3195         $result = $this->conn->fetchHeaderIndex($mailbox, '1:*', 'SIZE', false);
af3c04 3196
A 3197         if (is_array($result))
3198             $result = array_sum($result);
3199
3200         return $result;
3201     }
3202
3203
3204     /**
59c216 3205      * Subscribe to a specific mailbox(es)
A 3206      *
5c461b 3207      * @param array $a_mboxes Mailbox name(s)
59c216 3208      * @return boolean True on success
c43517 3209      */
59c216 3210     function subscribe($a_mboxes)
A 3211     {
3212         if (!is_array($a_mboxes))
3213             $a_mboxes = array($a_mboxes);
3214
3215         // let this common function do the main work
3216         return $this->_change_subscription($a_mboxes, 'subscribe');
3217     }
3218
3219
3220     /**
3221      * Unsubscribe mailboxes
3222      *
5c461b 3223      * @param array $a_mboxes Mailbox name(s)
59c216 3224      * @return boolean True on success
A 3225      */
3226     function unsubscribe($a_mboxes)
3227     {
3228         if (!is_array($a_mboxes))
3229             $a_mboxes = array($a_mboxes);
3230
3231         // let this common function do the main work
3232         return $this->_change_subscription($a_mboxes, 'unsubscribe');
3233     }
3234
3235
3236     /**
3237      * Create a new mailbox on the server and register it in local cache
3238      *
d08333 3239      * @param string  $mailbox   New mailbox name
5c461b 3240      * @param boolean $subscribe True if the new mailbox should be subscribed
d08333 3241      *
A 3242      * @return boolean True on success
59c216 3243      */
d08333 3244     function create_mailbox($mailbox, $subscribe=false)
59c216 3245     {
d08333 3246         $result = $this->conn->createFolder($mailbox);
59c216 3247
A 3248         // try to subscribe it
392589 3249         if ($result) {
A 3250             // clear cache
ccc059 3251             $this->clear_cache('mailboxes', true);
392589 3252
A 3253             if ($subscribe)
3254                 $this->subscribe($mailbox);
3255         }
59c216 3256
af3c04 3257         return $result;
59c216 3258     }
A 3259
3260
3261     /**
3262      * Set a new name to an existing mailbox
3263      *
d08333 3264      * @param string $mailbox  Mailbox to rename
A 3265      * @param string $new_name New mailbox name
0e1194 3266      *
af3c04 3267      * @return boolean True on success
59c216 3268      */
d08333 3269     function rename_mailbox($mailbox, $new_name)
59c216 3270     {
d08333 3271         if (!strlen($new_name)) {
A 3272             return false;
3273         }
59c216 3274
d08333 3275         $delm = $this->get_hierarchy_delimiter();
c43517 3276
0e1194 3277         // get list of subscribed folders
A 3278         if ((strpos($mailbox, '%') === false) && (strpos($mailbox, '*') === false)) {
d08333 3279             $a_subscribed = $this->_list_mailboxes('', $mailbox . $delm . '*');
A 3280             $subscribed   = $this->mailbox_exists($mailbox, true);
0e1194 3281         }
A 3282         else {
3283             $a_subscribed = $this->_list_mailboxes();
3284             $subscribed   = in_array($mailbox, $a_subscribed);
3285         }
59c216 3286
d08333 3287         $result = $this->conn->renameFolder($mailbox, $new_name);
59c216 3288
A 3289         if ($result) {
0e1194 3290             // unsubscribe the old folder, subscribe the new one
A 3291             if ($subscribed) {
3292                 $this->conn->unsubscribe($mailbox);
d08333 3293                 $this->conn->subscribe($new_name);
0e1194 3294             }
677e1f 3295
59c216 3296             // check if mailbox children are subscribed
0e1194 3297             foreach ($a_subscribed as $c_subscribed) {
59c216 3298                 if (preg_match('/^'.preg_quote($mailbox.$delm, '/').'/', $c_subscribed)) {
A 3299                     $this->conn->unsubscribe($c_subscribed);
3300                     $this->conn->subscribe(preg_replace('/^'.preg_quote($mailbox, '/').'/',
d08333 3301                         $new_name, $c_subscribed));
59c216 3302                 }
0e1194 3303             }
59c216 3304
A 3305             // clear cache
3306             $this->clear_message_cache($mailbox.'.msg');
ccc059 3307             $this->clear_cache('mailboxes', true);
59c216 3308         }
A 3309
af3c04 3310         return $result;
59c216 3311     }
A 3312
3313
3314     /**
0e1194 3315      * Remove mailbox from server
59c216 3316      *
d08333 3317      * @param string $mailbox Mailbox name
0e1194 3318      *
59c216 3319      * @return boolean True on success
A 3320      */
d08333 3321     function delete_mailbox($mailbox)
59c216 3322     {
d08333 3323         $delm = $this->get_hierarchy_delimiter();
59c216 3324
0e1194 3325         // get list of folders
A 3326         if ((strpos($mailbox, '%') === false) && (strpos($mailbox, '*') === false))
3327             $sub_mboxes = $this->list_unsubscribed('', $mailbox . $delm . '*');
3328         else
3329             $sub_mboxes = $this->list_unsubscribed();
59c216 3330
0e1194 3331         // send delete command to server
A 3332         $result = $this->conn->deleteFolder($mailbox);
59c216 3333
0e1194 3334         if ($result) {
A 3335             // unsubscribe mailbox
3336             $this->conn->unsubscribe($mailbox);
59c216 3337
0e1194 3338             foreach ($sub_mboxes as $c_mbox) {
A 3339                 if (preg_match('/^'.preg_quote($mailbox.$delm, '/').'/', $c_mbox)) {
3340                     $this->conn->unsubscribe($c_mbox);
3341                     if ($this->conn->deleteFolder($c_mbox)) {
3342                         $this->clear_message_cache($c_mbox.'.msg');
59c216 3343                     }
A 3344                 }
3345             }
0e1194 3346
A 3347             // clear mailbox-related cache
3348             $this->clear_message_cache($mailbox.'.msg');
ccc059 3349             $this->clear_cache('mailboxes', true);
59c216 3350         }
A 3351
0e1194 3352         return $result;
59c216 3353     }
A 3354
3355
3356     /**
3357      * Create all folders specified as default
3358      */
3359     function create_default_folders()
3360     {
3361         // create default folders if they do not exist
3362         foreach ($this->default_folders as $folder) {
3363             if (!$this->mailbox_exists($folder))
3364                 $this->create_mailbox($folder, true);
3365             else if (!$this->mailbox_exists($folder, true))
3366                 $this->subscribe($folder);
3367         }
3368     }
3369
3370
3371     /**
3372      * Checks if folder exists and is subscribed
3373      *
d08333 3374      * @param string   $mailbox      Folder name
5c461b 3375      * @param boolean  $subscription Enable subscription checking
d08333 3376      *
59c216 3377      * @return boolean TRUE or FALSE
A 3378      */
d08333 3379     function mailbox_exists($mailbox, $subscription=false)
59c216 3380     {
d08333 3381         if ($mailbox == 'INBOX') {
448409 3382             return true;
d08333 3383         }
59c216 3384
448409 3385         $key  = $subscription ? 'subscribed' : 'existing';
ad5881 3386
d08333 3387         if (is_array($this->icache[$key]) && in_array($mailbox, $this->icache[$key]))
448409 3388             return true;
16378f 3389
448409 3390         if ($subscription) {
d08333 3391             $a_folders = $this->conn->listSubscribed('', $mailbox);
448409 3392         }
A 3393         else {
d08333 3394             $a_folders = $this->conn->listMailboxes('', $mailbox);
af3c04 3395         }
c43517 3396
d08333 3397         if (is_array($a_folders) && in_array($mailbox, $a_folders)) {
A 3398             $this->icache[$key][] = $mailbox;
448409 3399             return true;
59c216 3400         }
A 3401
3402         return false;
3403     }
3404
3405
3406     /**
bbce3e 3407      * Returns the namespace where the folder is in
A 3408      *
d08333 3409      * @param string $mailbox Folder name
bbce3e 3410      *
A 3411      * @return string One of 'personal', 'other' or 'shared'
3412      * @access public
3413      */
d08333 3414     function mailbox_namespace($mailbox)
bbce3e 3415     {
d08333 3416         if ($mailbox == 'INBOX') {
bbce3e 3417             return 'personal';
A 3418         }
3419
3420         foreach ($this->namespace as $type => $namespace) {
3421             if (is_array($namespace)) {
3422                 foreach ($namespace as $ns) {
3423                     if (strlen($ns[0])) {
d08333 3424                         if ((strlen($ns[0])>1 && $mailbox == substr($ns[0], 0, -1))
A 3425                             || strpos($mailbox, $ns[0]) === 0
bbce3e 3426                         ) {
A 3427                             return $type;
3428                         }
3429                     }
3430                 }
3431             }
3432         }
3433
3434         return 'personal';
3435     }
3436
3437
3438     /**
d08333 3439      * Modify folder name according to namespace.
A 3440      * For output it removes prefix of the personal namespace if it's possible.
3441      * For input it adds the prefix. Use it before creating a folder in root
3442      * of the folders tree.
59c216 3443      *
d08333 3444      * @param string $mailbox Folder name
A 3445      * @param string $mode    Mode name (out/in)
3446      *
59c216 3447      * @return string Folder name
A 3448      */
d08333 3449     function mod_mailbox($mailbox, $mode = 'out')
59c216 3450     {
d08333 3451         if (!strlen($mailbox)) {
A 3452             return $mailbox;
3453         }
59c216 3454
d08333 3455         $prefix     = $this->namespace['prefix']; // see set_env()
A 3456         $prefix_len = strlen($prefix);
3457
3458         if (!$prefix_len) {
3459             return $mailbox;
3460         }
3461
3462         // remove prefix for output
3463         if ($mode == 'out') {
3464             if (substr($mailbox, 0, $prefix_len) === $prefix) {
3465                 return substr($mailbox, $prefix_len);
00290a 3466             }
A 3467         }
d08333 3468         // add prefix for input (e.g. folder creation)
00290a 3469         else {
d08333 3470             return $prefix . $mailbox;
59c216 3471         }
c43517 3472
d08333 3473         return $mailbox;
59c216 3474     }
A 3475
3476
103ddc 3477     /**
3870be 3478      * Gets folder options from LIST response, e.g. \Noselect, \Noinferiors
a5a4bf 3479      *
d08333 3480      * @param string $mailbox Folder name
A 3481      * @param bool   $force   Set to True if options should be refreshed
3482      *                        Options are available after LIST command only
a5a4bf 3483      *
A 3484      * @return array Options list
3485      */
d08333 3486     function mailbox_options($mailbox, $force=false)
a5a4bf 3487     {
d08333 3488         if ($mailbox == 'INBOX') {
a5a4bf 3489             return array();
A 3490         }
3491
d08333 3492         if (!is_array($this->conn->data['LIST']) || !is_array($this->conn->data['LIST'][$mailbox])) {
3870be 3493             if ($force) {
d08333 3494                 $this->conn->listMailboxes('', $mailbox);
3870be 3495             }
A 3496             else {
3497                 return array();
3498             }
a5a4bf 3499         }
A 3500
d08333 3501         $opts = $this->conn->data['LIST'][$mailbox];
a5a4bf 3502
A 3503         return is_array($opts) ? $opts : array();
3504     }
3505
3506
3507     /**
25e6a0 3508      * Returns extended information about the folder
A 3509      *
3510      * @param string $mailbox Folder name
3511      *
3512      * @return array Data
3513      */
3514     function mailbox_info($mailbox)
3515     {
3516         $acl       = $this->get_capability('ACL');
3517         $namespace = $this->get_namespace();
3518         $options   = array();
3519
3520         // check if the folder is a namespace prefix
3521         if (!empty($namespace)) {
3522             $mbox = $mailbox . $this->delimiter;
3523             foreach ($namespace as $ns) {
68070e 3524                 if (!empty($ns)) {
A 3525                     foreach ($ns as $item) {
3526                         if ($item[0] === $mbox) {
3527                             $options['is_root'] = true;
922016 3528                             break 2;
68070e 3529                         }
25e6a0 3530                     }
A 3531                 }
3532             }
3533         }
922016 3534         // check if the folder is other user virtual-root
A 3535         if (!$options['is_root'] && !empty($namespace) && !empty($namespace['other'])) {
3536             $parts = explode($this->delimiter, $mailbox);
3537             if (count($parts) == 2) {
3538                 $mbox = $parts[0] . $this->delimiter;
3539                 foreach ($namespace['other'] as $item) {
3540                     if ($item[0] === $mbox) {
3541                         $options['is_root'] = true;
3542                         break;
3543                     }
3544                 }
3545             }
3546         }
25e6a0 3547
A 3548         $options['name']      = $mailbox;
3549         $options['options']   = $this->mailbox_options($mailbox, true);
3550         $options['namespace'] = $this->mailbox_namespace($mailbox);
3551         $options['rights']    = $acl && !$options['is_root'] ? (array)$this->my_rights($mailbox) : array();
3552         $options['special']   = in_array($mailbox, $this->default_folders);
3553
1cd362 3554         // Set 'noselect' and 'norename' flags
25e6a0 3555         if (is_array($options['options'])) {
A 3556             foreach ($options['options'] as $opt) {
3557                 $opt = strtolower($opt);
3558                 if ($opt == '\noselect' || $opt == '\nonexistent') {
3559                     $options['noselect'] = true;
3560                 }
3561             }
3562         }
3563         else {
3564             $options['noselect'] = true;
3565         }
3566
3567         if (!empty($options['rights'])) {
1cd362 3568             $options['norename'] = !in_array('x', $options['rights']);
25e6a0 3569             if (!$options['noselect']) {
A 3570                 $options['noselect'] = !in_array('r', $options['rights']);
3571             }
3572         }
1cd362 3573         else {
A 3574             $options['norename'] = $options['is_root'] || $options['namespace'] != 'personal';
3575         }
25e6a0 3576
A 3577         return $options;
3578     }
3579
3580
3581     /**
103ddc 3582      * Get message header names for rcube_imap_generic::fetchHeader(s)
A 3583      *
3584      * @return string Space-separated list of header names
3585      */
3586     private function get_fetch_headers()
3587     {
3588         $headers = explode(' ', $this->fetch_add_headers);
3589         $headers = array_map('strtoupper', $headers);
3590
5cf5ee 3591         if ($this->messages_caching || $this->get_all_headers)
103ddc 3592             $headers = array_merge($headers, $this->all_headers);
A 3593
3594         return implode(' ', array_unique($headers));
3595     }
3596
3597
8b6eff 3598     /* -----------------------------------------
A 3599      *   ACL and METADATA/ANNOTATEMORE methods
3600      * ----------------------------------------*/
3601
3602     /**
3603      * Changes the ACL on the specified mailbox (SETACL)
3604      *
3605      * @param string $mailbox Mailbox name
3606      * @param string $user    User name
3607      * @param string $acl     ACL string
3608      *
3609      * @return boolean True on success, False on failure
3610      *
3611      * @access public
3612      * @since 0.5-beta
3613      */
3614     function set_acl($mailbox, $user, $acl)
3615     {
3616         if ($this->get_capability('ACL'))
3617             return $this->conn->setACL($mailbox, $user, $acl);
3618
3619         return false;
3620     }
3621
3622
3623     /**
3624      * Removes any <identifier,rights> pair for the
3625      * specified user from the ACL for the specified
3626      * mailbox (DELETEACL)
3627      *
3628      * @param string $mailbox Mailbox name
3629      * @param string $user    User name
3630      *
3631      * @return boolean True on success, False on failure
3632      *
3633      * @access public
3634      * @since 0.5-beta
3635      */
3636     function delete_acl($mailbox, $user)
3637     {
3638         if ($this->get_capability('ACL'))
3639             return $this->conn->deleteACL($mailbox, $user);
3640
3641         return false;
3642     }
3643
3644
3645     /**
3646      * Returns the access control list for mailbox (GETACL)
3647      *
3648      * @param string $mailbox Mailbox name
3649      *
3650      * @return array User-rights array on success, NULL on error
3651      * @access public
3652      * @since 0.5-beta
3653      */
3654     function get_acl($mailbox)
3655     {
3656         if ($this->get_capability('ACL'))
3657             return $this->conn->getACL($mailbox);
3658
3659         return NULL;
3660     }
3661
3662
3663     /**
3664      * Returns information about what rights can be granted to the
3665      * user (identifier) in the ACL for the mailbox (LISTRIGHTS)
3666      *
3667      * @param string $mailbox Mailbox name
3668      * @param string $user    User name
3669      *
3670      * @return array List of user rights
3671      * @access public
3672      * @since 0.5-beta
3673      */
3674     function list_rights($mailbox, $user)
3675     {
3676         if ($this->get_capability('ACL'))
3677             return $this->conn->listRights($mailbox, $user);
3678
3679         return NULL;
3680     }
3681
3682
3683     /**
3684      * Returns the set of rights that the current user has to
3685      * mailbox (MYRIGHTS)
3686      *
3687      * @param string $mailbox Mailbox name
3688      *
3689      * @return array MYRIGHTS response on success, NULL on error
3690      * @access public
3691      * @since 0.5-beta
3692      */
3693     function my_rights($mailbox)
3694     {
3695         if ($this->get_capability('ACL'))
3696             return $this->conn->myRights($mailbox);
3697
3698         return NULL;
3699     }
3700
3701
3702     /**
3703      * Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
3704      *
3705      * @param string $mailbox Mailbox name (empty for server metadata)
3706      * @param array  $entries Entry-value array (use NULL value as NIL)
3707      *
3708      * @return boolean True on success, False on failure
3709      * @access public
3710      * @since 0.5-beta
3711      */
3712     function set_metadata($mailbox, $entries)
3713     {
448409 3714         if ($this->get_capability('METADATA') ||
A 3715             (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
8b6eff 3716         ) {
A 3717             return $this->conn->setMetadata($mailbox, $entries);
3718         }
3719         else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
e22740 3720             foreach ((array)$entries as $entry => $value) {
8b6eff 3721                 list($ent, $attr) = $this->md2annotate($entry);
A 3722                 $entries[$entry] = array($ent, $attr, $value);
3723             }
3724             return $this->conn->setAnnotation($mailbox, $entries);
3725         }
3726
3727         return false;
3728     }
3729
3730
3731     /**
3732      * Unsets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
3733      *
3734      * @param string $mailbox Mailbox name (empty for server metadata)
3735      * @param array  $entries Entry names array
3736      *
3737      * @return boolean True on success, False on failure
3738      *
3739      * @access public
3740      * @since 0.5-beta
3741      */
3742     function delete_metadata($mailbox, $entries)
3743     {
3744         if ($this->get_capability('METADATA') || 
448409 3745             (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
8b6eff 3746         ) {
A 3747             return $this->conn->deleteMetadata($mailbox, $entries);
3748         }
3749         else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
e22740 3750             foreach ((array)$entries as $idx => $entry) {
8b6eff 3751                 list($ent, $attr) = $this->md2annotate($entry);
A 3752                 $entries[$idx] = array($ent, $attr, NULL);
3753             }
3754             return $this->conn->setAnnotation($mailbox, $entries);
3755         }
3756
3757         return false;
3758     }
3759
3760
3761     /**
3762      * Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION)
3763      *
3764      * @param string $mailbox Mailbox name (empty for server metadata)
3765      * @param array  $entries Entries
3766      * @param array  $options Command options (with MAXSIZE and DEPTH keys)
3767      *
3768      * @return array Metadata entry-value hash array on success, NULL on error
3769      *
3770      * @access public
3771      * @since 0.5-beta
3772      */
3773     function get_metadata($mailbox, $entries, $options=array())
3774     {
3775         if ($this->get_capability('METADATA') || 
e22740 3776             (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
8b6eff 3777         ) {
A 3778             return $this->conn->getMetadata($mailbox, $entries, $options);
3779         }
3780         else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
3781             $queries = array();
3782             $res     = array();
3783
3784             // Convert entry names
e22740 3785             foreach ((array)$entries as $entry) {
8b6eff 3786                 list($ent, $attr) = $this->md2annotate($entry);
A 3787                 $queries[$attr][] = $ent;
3788             }
3789
3790             // @TODO: Honor MAXSIZE and DEPTH options
3791             foreach ($queries as $attrib => $entry)
3792                 if ($result = $this->conn->getAnnotation($mailbox, $entry, $attrib))
3793                     $res = array_merge($res, $result);
3794
3795             return $res;
3796         }
3797
3798         return NULL;
3799     }
3800
3801
3802     /**
3803      * Converts the METADATA extension entry name into the correct
3804      * entry-attrib names for older ANNOTATEMORE version.
3805      *
e22740 3806      * @param string $entry Entry name
8b6eff 3807      *
A 3808      * @return array Entry-attribute list, NULL if not supported (?)
3809      */
e22740 3810     private function md2annotate($entry)
8b6eff 3811     {
A 3812         if (substr($entry, 0, 7) == '/shared') {
3813             return array(substr($entry, 7), 'value.shared');
3814         }
3815         else if (substr($entry, 0, 8) == '/private') {
3816             return array(substr($entry, 8), 'value.priv');
3817         }
3818
3819         // @TODO: log error
3820         return NULL;
3821     }
3822
3823
59c216 3824     /* --------------------------------
A 3825      *   internal caching methods
3826      * --------------------------------*/
3827
3828     /**
5cf5ee 3829      * Enable or disable indexes caching
5c461b 3830      *
8edb3d 3831      * @param boolean $type Cache type (@see rcmail::get_cache)
59c216 3832      * @access public
A 3833      */
5cf5ee 3834     function set_caching($type)
59c216 3835     {
5cf5ee 3836         if ($type) {
341d96 3837             $this->caching = true;
5cf5ee 3838         }
A 3839         else {
3840             if ($this->cache)
3841                 $this->cache->close();
3842             $this->cache = null;
341d96 3843             $this->caching = false;
5cf5ee 3844         }
59c216 3845     }
A 3846
341d96 3847     /**
A 3848      * Getter for IMAP cache object
3849      */
3850     private function get_cache_engine()
3851     {
3852         if ($this->caching && !$this->cache) {
3853             $rcmail = rcmail::get_instance();
3854             $this->cache = $rcmail->get_cache('IMAP', $type);
3855         }
3856
3857         return $this->cache;
3858     }
29983c 3859
59c216 3860     /**
5c461b 3861      * Returns cached value
A 3862      *
3863      * @param string $key Cache key
3864      * @return mixed
59c216 3865      * @access public
A 3866      */
3867     function get_cache($key)
3868     {
341d96 3869         if ($cache = $this->get_cache_engine()) {
A 3870             return $cache->get($key);
59c216 3871         }
A 3872     }
29983c 3873
59c216 3874     /**
5c461b 3875      * Update cache
A 3876      *
3877      * @param string $key  Cache key
3878      * @param mixed  $data Data
1f385b 3879      * @access public
59c216 3880      */
1f385b 3881     function update_cache($key, $data)
59c216 3882     {
341d96 3883         if ($cache = $this->get_cache_engine()) {
A 3884             $cache->set($key, $data);
59c216 3885         }
A 3886     }
3887
3888     /**
5c461b 3889      * Clears the cache.
A 3890      *
ccc059 3891      * @param string  $key         Cache key name or pattern
A 3892      * @param boolean $prefix_mode Enable it to clear all keys starting
3893      *                             with prefix specified in $key
59c216 3894      * @access public
A 3895      */
ccc059 3896     function clear_cache($key=null, $prefix_mode=false)
59c216 3897     {
341d96 3898         if ($cache = $this->get_cache_engine()) {
A 3899             $cache->remove($key, $prefix_mode);
59c216 3900         }
A 3901     }
3902
3903
3904     /* --------------------------------
3905      *   message caching methods
3906      * --------------------------------*/
5cf5ee 3907
A 3908     /**
3909      * Enable or disable messages caching
3910      *
3911      * @param boolean $set Flag
3912      * @access public
3913      */
3914     function set_messages_caching($set)
3915     {
3916         $rcmail = rcmail::get_instance();
3917
3918         if ($set && ($dbh = $rcmail->get_dbh())) {
3919             $this->db = $dbh;
3920             $this->messages_caching = true;
3921         }
3922         else {
3923             $this->messages_caching = false;
3924         }
3925     }
c43517 3926
59c216 3927     /**
A 3928      * Checks if the cache is up-to-date
3929      *
5c461b 3930      * @param string $mailbox   Mailbox name
A 3931      * @param string $cache_key Internal cache key
59c216 3932      * @return int   Cache status: -3 = off, -2 = incomplete, -1 = dirty, 1 = OK
A 3933      */
3934     private function check_cache_status($mailbox, $cache_key)
3935     {
5cf5ee 3936         if (!$this->messages_caching)
59c216 3937             return -3;
A 3938
3939         $cache_index = $this->get_message_cache_index($cache_key);
3940         $msg_count = $this->_messagecount($mailbox);
3941         $cache_count = count($cache_index);
3942
3943         // empty mailbox
9ae29c 3944         if (!$msg_count) {
59c216 3945             return $cache_count ? -2 : 1;
9ae29c 3946         }
59c216 3947
93272e 3948         if ($cache_count == $msg_count) {
59c216 3949             if ($this->skip_deleted) {
9ae29c 3950                 if (!empty($this->icache['all_undeleted_idx'])) {
A 3951                     $uids = rcube_imap_generic::uncompressMessageSet($this->icache['all_undeleted_idx']);
3952                     $uids = array_flip($uids);
3953                     foreach ($cache_index as $uid) {
3954                         unset($uids[$uid]);
3955                     }
3956                 }
3957                 else {
3958                     // get all undeleted messages excluding cached UIDs
3959                     $uids = $this->search_once($mailbox, 'ALL UNDELETED NOT UID '.
3960                         rcube_imap_generic::compressMessageSet($cache_index));
3961                 }
3962                 if (empty($uids)) {
3963                     return 1;
3964                 }
59c216 3965             } else {
eacce9 3966                 // get UID of the message with highest index
A 3967                 $uid = $this->_id2uid($msg_count, $mailbox);
59c216 3968                 $cache_uid = array_pop($cache_index);
c43517 3969
59c216 3970                 // uids of highest message matches -> cache seems OK
9ae29c 3971                 if ($cache_uid == $uid) {
59c216 3972                     return 1;
9ae29c 3973                 }
59c216 3974             }
A 3975             // cache is dirty
3976             return -1;
3977         }
9ae29c 3978
59c216 3979         // if cache count differs less than 10% report as dirty
9ae29c 3980         return (abs($msg_count - $cache_count) < $msg_count/10) ? -1 : -2;
59c216 3981     }
A 3982
29983c 3983
59c216 3984     /**
5c461b 3985      * @param string $key Cache key
A 3986      * @param string $from
3987      * @param string $to
3988      * @param string $sort_field
3989      * @param string $sort_order
59c216 3990      * @access private
A 3991      */
3992     private function get_message_cache($key, $from, $to, $sort_field, $sort_order)
3993     {
5cf5ee 3994         if (!$this->messages_caching)
eacce9 3995             return NULL;
c43517 3996
59c216 3997         // use idx sort as default sorting
A 3998         if (!$sort_field || !in_array($sort_field, $this->db_header_fields)) {
3999             $sort_field = 'idx';
4000         }
c43517 4001
eacce9 4002         $result = array();
A 4003
4004         $sql_result = $this->db->limitquery(
59c216 4005                 "SELECT idx, uid, headers".
A 4006                 " FROM ".get_table_name('messages').
4007                 " WHERE user_id=?".
4008                 " AND cache_key=?".
4009                 " ORDER BY ".$this->db->quoteIdentifier($sort_field)." ".strtoupper($sort_order),
4010                 $from,
4011                 $to - $from,
4012                 $_SESSION['user_id'],
4013                 $key);
4014
eacce9 4015         while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
A 4016             $uid = intval($sql_arr['uid']);
4017             $result[$uid] = $this->db->decode(unserialize($sql_arr['headers']));
59c216 4018
eacce9 4019             // featch headers if unserialize failed
A 4020             if (empty($result[$uid]))
4021                 $result[$uid] = $this->conn->fetchHeader(
103ddc 4022                     preg_replace('/.msg$/', '', $key), $uid, true, false, $this->get_fetch_headers());
59c216 4023         }
A 4024
eacce9 4025         return $result;
59c216 4026     }
A 4027
29983c 4028
59c216 4029     /**
5c461b 4030      * @param string $key Cache key
29983c 4031      * @param int    $uid Message UID
5c461b 4032      * @return mixed
59c216 4033      * @access private
A 4034      */
4035     private function &get_cached_message($key, $uid)
4036     {
4037         $internal_key = 'message';
c43517 4038
5cf5ee 4039         if ($this->messages_caching && !isset($this->icache[$internal_key][$uid])) {
59c216 4040             $sql_result = $this->db->query(
eacce9 4041                 "SELECT idx, headers, structure, message_id".
59c216 4042                 " FROM ".get_table_name('messages').
A 4043                 " WHERE user_id=?".
4044                 " AND cache_key=?".
4045                 " AND uid=?",
4046                 $_SESSION['user_id'],
4047                 $key,
4048                 $uid);
4049
4050             if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
eacce9 4051                 $this->icache['message.id'][$uid] = intval($sql_arr['message_id']);
A 4052                 $this->uid_id_map[preg_replace('/\.msg$/', '', $key)][$uid] = intval($sql_arr['idx']);
59c216 4053                 $this->icache[$internal_key][$uid] = $this->db->decode(unserialize($sql_arr['headers']));
eacce9 4054
59c216 4055                 if (is_object($this->icache[$internal_key][$uid]) && !empty($sql_arr['structure']))
A 4056                     $this->icache[$internal_key][$uid]->structure = $this->db->decode(unserialize($sql_arr['structure']));
4057             }
4058         }
4059
4060         return $this->icache[$internal_key][$uid];
4061     }
4062
29983c 4063
59c216 4064     /**
29983c 4065      * @param string  $key        Cache key
A 4066      * @param string  $sort_field Sorting column
4067      * @param string  $sort_order Sorting order
4068      * @return array Messages index
59c216 4069      * @access private
c43517 4070      */
eacce9 4071     private function get_message_cache_index($key, $sort_field='idx', $sort_order='ASC')
59c216 4072     {
5cf5ee 4073         if (!$this->messages_caching || empty($key))
eacce9 4074             return NULL;
59c216 4075
A 4076         // use idx sort as default
4077         if (!$sort_field || !in_array($sort_field, $this->db_header_fields))
4078             $sort_field = 'idx';
c43517 4079
eacce9 4080         if (array_key_exists('index', $this->icache)
A 4081             && $this->icache['index']['key'] == $key
29983c 4082             && $this->icache['index']['sort_field'] == $sort_field
eacce9 4083         ) {
29983c 4084             if ($this->icache['index']['sort_order'] == $sort_order)
A 4085                 return $this->icache['index']['result'];
4086             else
4087                 return array_reverse($this->icache['index']['result'], true);
eacce9 4088         }
A 4089
4090         $this->icache['index'] = array(
29983c 4091             'result'     => array(),
A 4092             'key'        => $key,
4093             'sort_field' => $sort_field,
4094             'sort_order' => $sort_order,
eacce9 4095         );
A 4096
59c216 4097         $sql_result = $this->db->query(
A 4098             "SELECT idx, uid".
4099             " FROM ".get_table_name('messages').
4100             " WHERE user_id=?".
4101             " AND cache_key=?".
4102             " ORDER BY ".$this->db->quote_identifier($sort_field)." ".$sort_order,
4103             $_SESSION['user_id'],
4104             $key);
4105
4106         while ($sql_arr = $this->db->fetch_assoc($sql_result))
eacce9 4107             $this->icache['index']['result'][$sql_arr['idx']] = intval($sql_arr['uid']);
c43517 4108
eacce9 4109         return $this->icache['index']['result'];
59c216 4110     }
29983c 4111
59c216 4112
A 4113     /**
4114      * @access private
4115      */
eacce9 4116     private function add_message_cache($key, $index, $headers, $struct=null, $force=false, $internal_cache=false)
59c216 4117     {
A 4118         if (empty($key) || !is_object($headers) || empty($headers->uid))
4119             return;
4120
4121         // add to internal (fast) cache
eacce9 4122         if ($internal_cache) {
A 4123             $this->icache['message'][$headers->uid] = clone $headers;
4124             $this->icache['message'][$headers->uid]->structure = $struct;
4125         }
59c216 4126
A 4127         // no further caching
5cf5ee 4128         if (!$this->messages_caching)
59c216 4129             return;
c43517 4130
eacce9 4131         // known message id
A 4132         if (is_int($force) && $force > 0) {
4133             $message_id = $force;
4134         }
175d8e 4135         // check for an existing record (probably headers are cached but structure not)
eacce9 4136         else if (!$force) {
59c216 4137             $sql_result = $this->db->query(
A 4138                 "SELECT message_id".
4139                 " FROM ".get_table_name('messages').
4140                 " WHERE user_id=?".
4141                 " AND cache_key=?".
4142                 " AND uid=?",
4143                 $_SESSION['user_id'],
4144                 $key,
4145                 $headers->uid);
4146
4147             if ($sql_arr = $this->db->fetch_assoc($sql_result))
4148                 $message_id = $sql_arr['message_id'];
4149         }
4150
4151         // update cache record
4152         if ($message_id) {
4153             $this->db->query(
4154                 "UPDATE ".get_table_name('messages').
4155                 " SET idx=?, headers=?, structure=?".
4156                 " WHERE message_id=?",
4157                 $index,
4158                 serialize($this->db->encode(clone $headers)),
4159                 is_object($struct) ? serialize($this->db->encode(clone $struct)) : NULL,
4160                 $message_id
4161             );
4162         }
4163         else { // insert new record
4164             $this->db->query(
4165                 "INSERT INTO ".get_table_name('messages').
4166                 " (user_id, del, cache_key, created, idx, uid, subject, ".
4167                 $this->db->quoteIdentifier('from').", ".
4168                 $this->db->quoteIdentifier('to').", ".
4169                 "cc, date, size, headers, structure)".
4170                 " VALUES (?, 0, ?, ".$this->db->now().", ?, ?, ?, ?, ?, ?, ".
4171                 $this->db->fromunixtime($headers->timestamp).", ?, ?, ?)",
4172                 $_SESSION['user_id'],
4173                 $key,
4174                 $index,
4175                 $headers->uid,
4176                 (string)mb_substr($this->db->encode($this->decode_header($headers->subject, true)), 0, 128),
4177                 (string)mb_substr($this->db->encode($this->decode_header($headers->from, true)), 0, 128),
4178                 (string)mb_substr($this->db->encode($this->decode_header($headers->to, true)), 0, 128),
4179                 (string)mb_substr($this->db->encode($this->decode_header($headers->cc, true)), 0, 128),
4180                 (int)$headers->size,
4181                 serialize($this->db->encode(clone $headers)),
4182                 is_object($struct) ? serialize($this->db->encode(clone $struct)) : NULL
4183             );
4184         }
eacce9 4185
A 4186         unset($this->icache['index']);
59c216 4187     }
c43517 4188
29983c 4189
59c216 4190     /**
A 4191      * @access private
4192      */
4193     private function remove_message_cache($key, $ids, $idx=false)
4194     {
5cf5ee 4195         if (!$this->messages_caching)
59c216 4196             return;
c43517 4197
59c216 4198         $this->db->query(
A 4199             "DELETE FROM ".get_table_name('messages').
4200             " WHERE user_id=?".
4201             " AND cache_key=?".
4202             " AND ".($idx ? "idx" : "uid")." IN (".$this->db->array2list($ids, 'integer').")",
4203             $_SESSION['user_id'],
4204             $key);
eacce9 4205
A 4206         unset($this->icache['index']);
59c216 4207     }
29983c 4208
59c216 4209
A 4210     /**
5c461b 4211      * @param string $key         Cache key
A 4212      * @param int    $start_index Start index
59c216 4213      * @access private
A 4214      */
4215     private function clear_message_cache($key, $start_index=1)
4216     {
5cf5ee 4217         if (!$this->messages_caching)
59c216 4218             return;
c43517 4219
59c216 4220         $this->db->query(
A 4221             "DELETE FROM ".get_table_name('messages').
4222             " WHERE user_id=?".
4223             " AND cache_key=?".
4224             " AND idx>=?",
4225             $_SESSION['user_id'], $key, $start_index);
eacce9 4226
A 4227         unset($this->icache['index']);
59c216 4228     }
29983c 4229
59c216 4230
A 4231     /**
4232      * @access private
4233      */
4234     private function get_message_cache_index_min($key, $uids=NULL)
4235     {
5cf5ee 4236         if (!$this->messages_caching)
59c216 4237             return;
c43517 4238
59c216 4239         if (!empty($uids) && !is_array($uids)) {
A 4240             if ($uids == '*' || $uids == '1:*')
4241                 $uids = NULL;
4242             else
4243                 $uids = explode(',', $uids);
4244         }
4245
4246         $sql_result = $this->db->query(
4247             "SELECT MIN(idx) AS minidx".
4248             " FROM ".get_table_name('messages').
4249             " WHERE  user_id=?".
4250             " AND    cache_key=?"
4251             .(!empty($uids) ? " AND uid IN (".$this->db->array2list($uids, 'integer').")" : ''),
4252             $_SESSION['user_id'],
4253             $key);
4254
4255         if ($sql_arr = $this->db->fetch_assoc($sql_result))
4256             return $sql_arr['minidx'];
4257         else
c43517 4258             return 0;
29983c 4259     }
A 4260
4261
4262     /**
4263      * @param string $key Cache key
4264      * @param int    $id  Message (sequence) ID
4265      * @return int Message UID
4266      * @access private
4267      */
4268     private function get_cache_id2uid($key, $id)
4269     {
5cf5ee 4270         if (!$this->messages_caching)
29983c 4271             return null;
A 4272
4273         if (array_key_exists('index', $this->icache)
4274             && $this->icache['index']['key'] == $key
4275         ) {
4276             return $this->icache['index']['result'][$id];
4277         }
4278
4279         $sql_result = $this->db->query(
4280             "SELECT uid".
4281             " FROM ".get_table_name('messages').
4282             " WHERE user_id=?".
4283             " AND cache_key=?".
4284             " AND idx=?",
4285             $_SESSION['user_id'], $key, $id);
4286
4287         if ($sql_arr = $this->db->fetch_assoc($sql_result))
4288             return intval($sql_arr['uid']);
4289
4290         return null;
4291     }
4292
4293
4294     /**
4295      * @param string $key Cache key
4296      * @param int    $uid Message UID
4297      * @return int Message (sequence) ID
4298      * @access private
4299      */
4300     private function get_cache_uid2id($key, $uid)
4301     {
5cf5ee 4302         if (!$this->messages_caching)
29983c 4303             return null;
A 4304
4305         if (array_key_exists('index', $this->icache)
4306             && $this->icache['index']['key'] == $key
4307         ) {
4308             return array_search($uid, $this->icache['index']['result']);
4309         }
4310
4311         $sql_result = $this->db->query(
4312             "SELECT idx".
4313             " FROM ".get_table_name('messages').
4314             " WHERE user_id=?".
4315             " AND cache_key=?".
4316             " AND uid=?",
4317             $_SESSION['user_id'], $key, $uid);
4318
4319         if ($sql_arr = $this->db->fetch_assoc($sql_result))
4320             return intval($sql_arr['idx']);
4321
4322         return null;
59c216 4323     }
A 4324
4325
4326     /* --------------------------------
4327      *   encoding/decoding methods
4328      * --------------------------------*/
4329
4330     /**
4331      * Split an address list into a structured array list
4332      *
5c461b 4333      * @param string  $input  Input string
A 4334      * @param int     $max    List only this number of addresses
4335      * @param boolean $decode Decode address strings
59c216 4336      * @return array  Indexed list of addresses
A 4337      */
4338     function decode_address_list($input, $max=null, $decode=true)
4339     {
4340         $a = $this->_parse_address_list($input, $decode);
4341         $out = array();
4342         // Special chars as defined by RFC 822 need to in quoted string (or escaped).
4343         $special_chars = '[\(\)\<\>\\\.\[\]@,;:"]';
c43517 4344
59c216 4345         if (!is_array($a))
A 4346             return $out;
4347
4348         $c = count($a);
4349         $j = 0;
4350
4351         foreach ($a as $val) {
4352             $j++;
4353             $address = trim($val['address']);
435c31 4354             $name    = trim($val['name']);
59c216 4355
A 4356             if ($name && $address && $name != $address)
4357                 $string = sprintf('%s <%s>', preg_match("/$special_chars/", $name) ? '"'.addcslashes($name, '"').'"' : $name, $address);
4358             else if ($address)
4359                 $string = $address;
4360             else if ($name)
4361                 $string = $name;
c43517 4362
e99991 4363             $out[$j] = array(
A 4364                 'name'   => $name,
59c216 4365                 'mailto' => $address,
A 4366                 'string' => $string
4367             );
4368
4369             if ($max && $j==$max)
4370                 break;
4371         }
c43517 4372
59c216 4373         return $out;
A 4374     }
4375
4376
4377     /**
4378      * Decode a message header value
4379      *
5c461b 4380      * @param string  $input         Header value
A 4381      * @param boolean $remove_quotas Remove quotes if necessary
59c216 4382      * @return string Decoded string
A 4383      */
4384     function decode_header($input, $remove_quotes=false)
4385     {
4386         $str = rcube_imap::decode_mime_string((string)$input, $this->default_charset);
2aa2b3 4387         if ($str[0] == '"' && $remove_quotes)
59c216 4388             $str = str_replace('"', '', $str);
c43517 4389
59c216 4390         return $str;
A 4391     }
4392
4393
4394     /**
4395      * Decode a mime-encoded string to internal charset
4396      *
4397      * @param string $input    Header value
4398      * @param string $fallback Fallback charset if none specified
4399      *
4400      * @return string Decoded string
4401      * @static
4402      */
4403     public static function decode_mime_string($input, $fallback=null)
4404     {
8df56e 4405         if (!empty($fallback)) {
A 4406             $default_charset = $fallback;
4407         }
4408         else {
4409             $default_charset = rcmail::get_instance()->config->get('default_charset', 'ISO-8859-1');
4410         }
59c216 4411
c43517 4412         // rfc: all line breaks or other characters not found
59c216 4413         // in the Base64 Alphabet must be ignored by decoding software
c43517 4414         // delete all blanks between MIME-lines, differently we can
59c216 4415         // receive unnecessary blanks and broken utf-8 symbols
A 4416         $input = preg_replace("/\?=\s+=\?/", '?==?', $input);
4417
8df56e 4418         // encoded-word regexp
A 4419         $re = '/=\?([^?]+)\?([BbQq])\?([^?\n]*)\?=/';
4420
4421         // Find all RFC2047's encoded words
4422         if (preg_match_all($re, $input, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {
4423             // Initialize variables
4424             $tmp   = array();
4425             $out   = '';
4426             $start = 0;
4427
4428             foreach ($matches as $idx => $m) {
4429                 $pos      = $m[0][1];
4430                 $charset  = $m[1][0];
4431                 $encoding = $m[2][0];
4432                 $text     = $m[3][0];
4433                 $length   = strlen($m[0][0]);
4434
59c216 4435                 // Append everything that is before the text to be decoded
8df56e 4436                 if ($start != $pos) {
A 4437                     $substr = substr($input, $start, $pos-$start);
4438                     $out   .= rcube_charset_convert($substr, $default_charset);
4439                     $start  = $pos;
4440                 }
4441                 $start += $length;
59c216 4442
8df56e 4443                 // Per RFC2047, each string part "MUST represent an integral number
A 4444                 // of characters . A multi-octet character may not be split across
4445                 // adjacent encoded-words." However, some mailers break this, so we
4446                 // try to handle characters spanned across parts anyway by iterating
4447                 // through and aggregating sequential encoded parts with the same
4448                 // character set and encoding, then perform the decoding on the
4449                 // aggregation as a whole.
59c216 4450
8df56e 4451                 $tmp[] = $text;
A 4452                 if ($next_match = $matches[$idx+1]) {
4453                     if ($next_match[0][1] == $start
4454                         && $next_match[1][0] == $charset
4455                         && $next_match[2][0] == $encoding
4456                     ) {
4457                         continue;
4458                     }
4459                 }
59c216 4460
8df56e 4461                 $count = count($tmp);
A 4462                 $text  = '';
4463
4464                 // Decode and join encoded-word's chunks
4465                 if ($encoding == 'B' || $encoding == 'b') {
4466                     // base64 must be decoded a segment at a time
4467                     for ($i=0; $i<$count; $i++)
4468                         $text .= base64_decode($tmp[$i]);
4469                 }
4470                 else { //if ($encoding == 'Q' || $encoding == 'q') {
4471                     // quoted printable can be combined and processed at once
4472                     for ($i=0; $i<$count; $i++)
4473                         $text .= $tmp[$i];
4474
4475                     $text = str_replace('_', ' ', $text);
4476                     $text = quoted_printable_decode($text);
4477                 }
4478
4479                 $out .= rcube_charset_convert($text, $charset);
4480                 $tmp = array();
59c216 4481             }
A 4482
8df56e 4483             // add the last part of the input string
A 4484             if ($start != strlen($input)) {
4485                 $out .= rcube_charset_convert(substr($input, $start), $default_charset);
4486             }
59c216 4487
A 4488             // return the results
4489             return $out;
4490         }
4491
4492         // no encoding information, use fallback
8df56e 4493         return rcube_charset_convert($input, $default_charset);
59c216 4494     }
A 4495
4496
4497     /**
4498      * Decode a mime part
4499      *
5c461b 4500      * @param string $input    Input string
A 4501      * @param string $encoding Part encoding
59c216 4502      * @return string Decoded string
A 4503      */
4504     function mime_decode($input, $encoding='7bit')
4505     {
4506         switch (strtolower($encoding)) {
4507         case 'quoted-printable':
4508             return quoted_printable_decode($input);
4509         case 'base64':
4510             return base64_decode($input);
4511         case 'x-uuencode':
4512         case 'x-uue':
4513         case 'uue':
4514         case 'uuencode':
4515             return convert_uudecode($input);
4516         case '7bit':
4517         default:
4518             return $input;
4519         }
4520     }
4521
4522
4523     /**
4524      * Convert body charset to RCMAIL_CHARSET according to the ctype_parameters
4525      *
5c461b 4526      * @param string $body        Part body to decode
A 4527      * @param string $ctype_param Charset to convert from
59c216 4528      * @return string Content converted to internal charset
A 4529      */
4530     function charset_decode($body, $ctype_param)
4531     {
4532         if (is_array($ctype_param) && !empty($ctype_param['charset']))
4533             return rcube_charset_convert($body, $ctype_param['charset']);
4534
4535         // defaults to what is specified in the class header
4536         return rcube_charset_convert($body,  $this->default_charset);
4537     }
4538
4539
4540     /* --------------------------------
4541      *         private methods
4542      * --------------------------------*/
4543
4544     /**
4545      * Validate the given input and save to local properties
5c461b 4546      *
A 4547      * @param string $sort_field Sort column
4548      * @param string $sort_order Sort order
59c216 4549      * @access private
A 4550      */
4551     private function _set_sort_order($sort_field, $sort_order)
4552     {
4553         if ($sort_field != null)
4554             $this->sort_field = asciiwords($sort_field);
4555         if ($sort_order != null)
4556             $this->sort_order = strtoupper($sort_order) == 'DESC' ? 'DESC' : 'ASC';
4557     }
4558
29983c 4559
59c216 4560     /**
A 4561      * Sort mailboxes first by default folders and then in alphabethical order
5c461b 4562      *
A 4563      * @param array $a_folders Mailboxes list
59c216 4564      * @access private
A 4565      */
4566     private function _sort_mailbox_list($a_folders)
4567     {
4568         $a_out = $a_defaults = $folders = array();
4569
4570         $delimiter = $this->get_hierarchy_delimiter();
4571
4572         // find default folders and skip folders starting with '.'
4573         foreach ($a_folders as $i => $folder) {
4574             if ($folder[0] == '.')
4575                 continue;
4576
4577             if (($p = array_search($folder, $this->default_folders)) !== false && !$a_defaults[$p])
4578                 $a_defaults[$p] = $folder;
4579             else
a44682 4580                 $folders[$folder] = rcube_charset_convert($folder, 'UTF7-IMAP');
59c216 4581         }
A 4582
4583         // sort folders and place defaults on the top
4584         asort($folders, SORT_LOCALE_STRING);
4585         ksort($a_defaults);
4586         $folders = array_merge($a_defaults, array_keys($folders));
4587
c43517 4588         // finally we must rebuild the list to move
59c216 4589         // subfolders of default folders to their place...
A 4590         // ...also do this for the rest of folders because
4591         // asort() is not properly sorting case sensitive names
4592         while (list($key, $folder) = each($folders)) {
c43517 4593             // set the type of folder name variable (#1485527)
59c216 4594             $a_out[] = (string) $folder;
A 4595             unset($folders[$key]);
c43517 4596             $this->_rsort($folder, $delimiter, $folders, $a_out);
59c216 4597         }
A 4598
4599         return $a_out;
4600     }
4601
4602
4603     /**
4604      * @access private
4605      */
4606     private function _rsort($folder, $delimiter, &$list, &$out)
4607     {
4608         while (list($key, $name) = each($list)) {
4609             if (strpos($name, $folder.$delimiter) === 0) {
c43517 4610                 // set the type of folder name variable (#1485527)
59c216 4611                 $out[] = (string) $name;
A 4612                 unset($list[$key]);
4613                 $this->_rsort($name, $delimiter, $list, $out);
4614             }
4615         }
c43517 4616         reset($list);
59c216 4617     }
A 4618
4619
4620     /**
d08333 4621      * @param int    $uid     Message UID
A 4622      * @param string $mailbox Mailbox name
29983c 4623      * @return int Message (sequence) ID
59c216 4624      * @access private
A 4625      */
d08333 4626     private function _uid2id($uid, $mailbox=NULL)
59c216 4627     {
d08333 4628         if (!strlen($mailbox)) {
A 4629             $mailbox = $this->mailbox;
29983c 4630         }
59c216 4631
d08333 4632         if (!isset($this->uid_id_map[$mailbox][$uid])) {
A 4633             if (!($id = $this->get_cache_uid2id($mailbox.'.msg', $uid)))
4634                 $id = $this->conn->UID2ID($mailbox, $uid);
4635
4636             $this->uid_id_map[$mailbox][$uid] = $id;
4637         }
4638
4639         return $this->uid_id_map[$mailbox][$uid];
59c216 4640     }
A 4641
29983c 4642
59c216 4643     /**
d08333 4644      * @param int    $id      Message (sequence) ID
A 4645      * @param string $mailbox Mailbox name
4646      *
29983c 4647      * @return int Message UID
59c216 4648      * @access private
A 4649      */
d08333 4650     private function _id2uid($id, $mailbox=null)
59c216 4651     {
d08333 4652         if (!strlen($mailbox)) {
A 4653             $mailbox = $this->mailbox;
4654         }
59c216 4655
d08333 4656         if ($uid = array_search($id, (array)$this->uid_id_map[$mailbox])) {
59c216 4657             return $uid;
d08333 4658         }
59c216 4659
d08333 4660         if (!($uid = $this->get_cache_id2uid($mailbox.'.msg', $id))) {
A 4661             $uid = $this->conn->ID2UID($mailbox, $id);
4662         }
29983c 4663
d08333 4664         $this->uid_id_map[$mailbox][$uid] = $id;
c43517 4665
59c216 4666         return $uid;
A 4667     }
4668
4669
4670     /**
4671      * Subscribe/unsubscribe a list of mailboxes and update local cache
4672      * @access private
4673      */
4674     private function _change_subscription($a_mboxes, $mode)
4675     {
4676         $updated = false;
4677
4678         if (is_array($a_mboxes))
d08333 4679             foreach ($a_mboxes as $i => $mailbox) {
59c216 4680                 $a_mboxes[$i] = $mailbox;
A 4681
d08333 4682                 if ($mode == 'subscribe')
59c216 4683                     $updated = $this->conn->subscribe($mailbox);
d08333 4684                 else if ($mode == 'unsubscribe')
59c216 4685                     $updated = $this->conn->unsubscribe($mailbox);
A 4686             }
4687
3253b2 4688         // clear cached mailbox list(s)
59c216 4689         if ($updated) {
ccc059 4690             $this->clear_cache('mailboxes', true);
59c216 4691         }
A 4692
4693         return $updated;
4694     }
4695
4696
4697     /**
4698      * Increde/decrese messagecount for a specific mailbox
4699      * @access private
4700      */
d08333 4701     private function _set_messagecount($mailbox, $mode, $increment)
59c216 4702     {
A 4703         $mode = strtoupper($mode);
4704         $a_mailbox_cache = $this->get_cache('messagecount');
c43517 4705
59c216 4706         if (!is_array($a_mailbox_cache[$mailbox]) || !isset($a_mailbox_cache[$mailbox][$mode]) || !is_numeric($increment))
A 4707             return false;
c43517 4708
59c216 4709         // add incremental value to messagecount
A 4710         $a_mailbox_cache[$mailbox][$mode] += $increment;
c43517 4711
59c216 4712         // there's something wrong, delete from cache
A 4713         if ($a_mailbox_cache[$mailbox][$mode] < 0)
4714             unset($a_mailbox_cache[$mailbox][$mode]);
4715
4716         // write back to cache
4717         $this->update_cache('messagecount', $a_mailbox_cache);
c43517 4718
59c216 4719         return true;
A 4720     }
4721
4722
4723     /**
4724      * Remove messagecount of a specific mailbox from cache
4725      * @access private
4726      */
d08333 4727     private function _clear_messagecount($mailbox, $mode=null)
59c216 4728     {
A 4729         $a_mailbox_cache = $this->get_cache('messagecount');
4730
4731         if (is_array($a_mailbox_cache[$mailbox])) {
c309cd 4732             if ($mode) {
A 4733                 unset($a_mailbox_cache[$mailbox][$mode]);
4734             }
4735             else {
4736                 unset($a_mailbox_cache[$mailbox]);
4737             }
59c216 4738             $this->update_cache('messagecount', $a_mailbox_cache);
A 4739         }
4740     }
4741
4742
4743     /**
4744      * Split RFC822 header string into an associative array
4745      * @access private
4746      */
4747     private function _parse_headers($headers)
4748     {
4749         $a_headers = array();
4750         $headers = preg_replace('/\r?\n(\t| )+/', ' ', $headers);
4751         $lines = explode("\n", $headers);
4752         $c = count($lines);
4753
4754         for ($i=0; $i<$c; $i++) {
4755             if ($p = strpos($lines[$i], ': ')) {
4756                 $field = strtolower(substr($lines[$i], 0, $p));
4757                 $value = trim(substr($lines[$i], $p+1));
4758                 if (!empty($value))
4759                     $a_headers[$field] = $value;
4760             }
4761         }
c43517 4762
59c216 4763         return $a_headers;
A 4764     }
4765
4766
4767     /**
4768      * @access private
4769      */
4770     private function _parse_address_list($str, $decode=true)
4771     {
4772         // remove any newlines and carriage returns before
6c68cb 4773         $str = preg_replace('/\r?\n(\s|\t)?/', ' ', $str);
A 4774
4775         // extract list items, remove comments
4776         $str = self::explode_header_string(',;', $str, true);
59c216 4777         $result = array();
A 4778
6d0ada 4779         // simplified regexp, supporting quoted local part
A 4780         $email_rx = '(\S+|("\s*(?:[^"\f\n\r\t\v\b\s]+\s*)+"))@\S+';
4781
6c68cb 4782         foreach ($str as $key => $val) {
435c31 4783             $name    = '';
A 4784             $address = '';
4785             $val     = trim($val);
59c216 4786
6d0ada 4787             if (preg_match('/(.*)<('.$email_rx.')>$/', $val, $m)) {
435c31 4788                 $address = $m[2];
A 4789                 $name    = trim($m[1]);
4790             }
218589 4791             else if (preg_match('/^('.$email_rx.')$/', $val, $m)) {
435c31 4792                 $address = $m[1];
A 4793                 $name    = '';
4794             }
4795             else {
4796                 $name = $val;
59c216 4797             }
c43517 4798
435c31 4799             // dequote and/or decode name
A 4800             if ($name) {
6d0ada 4801                 if ($name[0] == '"' && $name[strlen($name)-1] == '"') {
435c31 4802                     $name = substr($name, 1, -1);
A 4803                     $name = stripslashes($name);
4804                 }
7bdd3e 4805                 if ($decode) {
435c31 4806                     $name = $this->decode_header($name);
A 4807                 }
4808             }
4809
4810             if (!$address && $name) {
4811                 $address = $name;
4812             }
4813
4814             if ($address) {
4815                 $result[$key] = array('name' => $name, 'address' => $address);
4816             }
59c216 4817         }
c43517 4818
59c216 4819         return $result;
4e17e6 4820     }
6d969b 4821
7f1da4 4822
A 4823     /**
6c68cb 4824      * Explodes header (e.g. address-list) string into array of strings
A 4825      * using specified separator characters with proper handling
4826      * of quoted-strings and comments (RFC2822)
4827      *
4828      * @param string $separator       String containing separator characters
4829      * @param string $str             Header string
4830      * @param bool   $remove_comments Enable to remove comments
4831      *
4832      * @return array Header items
4833      */
4834     static function explode_header_string($separator, $str, $remove_comments=false)
4835     {
4836         $length  = strlen($str);
4837         $result  = array();
4838         $quoted  = false;
4839         $comment = 0;
4840         $out     = '';
4841
4842         for ($i=0; $i<$length; $i++) {
4843             // we're inside a quoted string
4844             if ($quoted) {
4845                 if ($str[$i] == '"') {
4846                     $quoted = false;
4847                 }
4848                 else if ($str[$i] == '\\') {
4849                     if ($comment <= 0) {
4850                         $out .= '\\';
4851                     }
4852                     $i++;
4853                 }
4854             }
4855             // we're inside a comment string
4856             else if ($comment > 0) {
4857                     if ($str[$i] == ')') {
4858                         $comment--;
4859                     }
4860                     else if ($str[$i] == '(') {
4861                         $comment++;
4862                     }
4863                     else if ($str[$i] == '\\') {
4864                         $i++;
4865                     }
4866                     continue;
4867             }
4868             // separator, add to result array
4869             else if (strpos($separator, $str[$i]) !== false) {
4870                     if ($out) {
4871                         $result[] = $out;
4872                     }
4873                     $out = '';
4874                     continue;
4875             }
4876             // start of quoted string
4877             else if ($str[$i] == '"') {
4878                     $quoted = true;
4879             }
4880             // start of comment
4881             else if ($remove_comments && $str[$i] == '(') {
4882                     $comment++;
4883             }
4884
4885             if ($comment <= 0) {
4886                 $out .= $str[$i];
4887             }
4888         }
4889
4890         if ($out && $comment <= 0) {
4891             $result[] = $out;
4892         }
4893
4894         return $result;
4895     }
4896
4897
4898     /**
7f1da4 4899      * This is our own debug handler for the IMAP connection
A 4900      * @access public
4901      */
4902     public function debug_handler(&$imap, $message)
4903     {
4904         write_log('imap', $message);
4905     }
4906
6d969b 4907 }  // end class rcube_imap
4e17e6 4908
8d4bcd 4909
T 4910 /**
4911  * Class representing a message part
6d969b 4912  *
T 4913  * @package Mail
8d4bcd 4914  */
T 4915 class rcube_message_part
4916 {
59c216 4917     var $mime_id = '';
A 4918     var $ctype_primary = 'text';
4919     var $ctype_secondary = 'plain';
4920     var $mimetype = 'text/plain';
4921     var $disposition = '';
4922     var $filename = '';
4923     var $encoding = '8bit';
4924     var $charset = '';
4925     var $size = 0;
4926     var $headers = array();
4927     var $d_parameters = array();
4928     var $ctype_parameters = array();
8d4bcd 4929
59c216 4930     function __clone()
A 4931     {
4932         if (isset($this->parts))
4933             foreach ($this->parts as $idx => $part)
4934                 if (is_object($part))
4935                     $this->parts[$idx] = clone $part;
4936     }
8d4bcd 4937 }
4e17e6 4938
T 4939
7e93ff 4940 /**
59c216 4941  * Class for sorting an array of rcube_mail_header objects in a predetermined order.
7e93ff 4942  *
6d969b 4943  * @package Mail
7e93ff 4944  * @author Eric Stadtherr
T 4945  */
4946 class rcube_header_sorter
4947 {
59c216 4948     var $sequence_numbers = array();
c43517 4949
59c216 4950     /**
A 4951      * Set the predetermined sort order.
4952      *
5c461b 4953      * @param array $seqnums Numerically indexed array of IMAP message sequence numbers
59c216 4954      */
A 4955     function set_sequence_numbers($seqnums)
4956     {
4957         $this->sequence_numbers = array_flip($seqnums);
4958     }
4959
4960     /**
4961      * Sort the array of header objects
4962      *
5c461b 4963      * @param array $headers Array of rcube_mail_header objects indexed by UID
59c216 4964      */
A 4965     function sort_headers(&$headers)
4966     {
4967         /*
4968         * uksort would work if the keys were the sequence number, but unfortunately
4969         * the keys are the UIDs.  We'll use uasort instead and dereference the value
4970         * to get the sequence number (in the "id" field).
c43517 4971         *
A 4972         * uksort($headers, array($this, "compare_seqnums"));
59c216 4973         */
A 4974         uasort($headers, array($this, "compare_seqnums"));
4975     }
c43517 4976
59c216 4977     /**
A 4978      * Sort method called by uasort()
5c461b 4979      *
A 4980      * @param rcube_mail_header $a
4981      * @param rcube_mail_header $b
59c216 4982      */
A 4983     function compare_seqnums($a, $b)
4984     {
4985         // First get the sequence number from the header object (the 'id' field).
4986         $seqa = $a->id;
4987         $seqb = $b->id;
c43517 4988
59c216 4989         // then find each sequence number in my ordered list
A 4990         $posa = isset($this->sequence_numbers[$seqa]) ? intval($this->sequence_numbers[$seqa]) : -1;
4991         $posb = isset($this->sequence_numbers[$seqb]) ? intval($this->sequence_numbers[$seqb]) : -1;
c43517 4992
59c216 4993         // return the relative position as the comparison value
A 4994         return $posa - $posb;
4995     }
7e93ff 4996 }