alecpl
2011-05-13 a208a4fa68d3c26c34ca3dae250267fec761675c
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;
A 55     private $mailbox = 'INBOX';
00290a 56     private $delimiter = NULL;
A 57     private $namespace = NULL;
59c216 58     private $sort_field = '';
A 59     private $sort_order = 'DESC';
60     private $caching_enabled = false;
61     private $default_charset = 'ISO-8859-1';
62     private $struct_charset = NULL;
63     private $default_folders = array('INBOX');
64     private $icache = array();
65     private $cache = array();
c43517 66     private $cache_keys = array();
59c216 67     private $cache_changes = array();
A 68     private $uid_id_map = array();
69     private $msg_headers = array();
70     public  $search_set = NULL;
71     public  $search_string = '';
72     private $search_charset = '';
73     private $search_sort_field = '';
74     private $search_threads = false;
c60978 75     private $search_sorted = false;
59c216 76     private $db_header_fields = array('idx', 'uid', 'subject', 'from', 'to', 'cc', 'date', 'size');
A 77     private $options = array('auth_method' => 'check');
78     private $host, $user, $pass, $port, $ssl;
103ddc 79
A 80     /**
81      * All (additional) headers used (in any way) by Roundcube
82      * Not listed here: DATE, FROM, TO, SUBJECT, CONTENT-TYPE, LIST-POST
83      * (used for messages listing) are hardcoded in rcube_imap_generic::fetchHeaders()
84      *
85      * @var array
86      * @see rcube_imap::fetch_add_headers
87      */
88     private $all_headers = array(
89         'REPLY-TO',
90         'IN-REPLY-TO',
91         'CC',
92         'BCC',
93         'MESSAGE-ID',
94         'CONTENT-TRANSFER-ENCODING',
95         'REFERENCES',
96         'X-PRIORITY',
97         'X-DRAFT-INFO',
98         'MAIL-FOLLOWUP-TO',
99         'MAIL-REPLY-TO',
100         'RETURN-PATH',
101     );
4e17e6 102
90f81a 103     const UNKNOWN       = 0;
A 104     const NOPERM        = 1;
105     const READONLY      = 2;
106     const TRYCREATE     = 3;
107     const INUSE         = 4;
108     const OVERQUOTA     = 5;
109     const ALREADYEXISTS = 6;
110     const NONEXISTENT   = 7;
111     const CONTACTADMIN  = 8;
112
4e17e6 113
59c216 114     /**
A 115      * Object constructor
116      *
117      * @param object DB Database connection
118      */
119     function __construct($db_conn)
4e17e6 120     {
59c216 121         $this->db = $db_conn;
A 122         $this->conn = new rcube_imap_generic();
4e17e6 123     }
T 124
30b302 125
59c216 126     /**
A 127      * Connect to an IMAP server
128      *
5c461b 129      * @param  string   $host    Host to connect
A 130      * @param  string   $user    Username for IMAP account
131      * @param  string   $pass    Password for IMAP account
132      * @param  integer  $port    Port to connect to
133      * @param  string   $use_ssl SSL schema (either ssl or tls) or null if plain connection
59c216 134      * @return boolean  TRUE on success, FALSE on failure
A 135      * @access public
136      */
137     function connect($host, $user, $pass, $port=143, $use_ssl=null)
4e17e6 138     {
175d8e 139         // check for OpenSSL support in PHP build
59c216 140         if ($use_ssl && extension_loaded('openssl'))
A 141             $this->options['ssl_mode'] = $use_ssl == 'imaps' ? 'ssl' : $use_ssl;
142         else if ($use_ssl) {
143             raise_error(array('code' => 403, 'type' => 'imap',
144                 'file' => __FILE__, 'line' => __LINE__,
145                 'message' => "OpenSSL not available"), true, false);
146             $port = 143;
147         }
4e17e6 148
59c216 149         $this->options['port'] = $port;
f28124 150
890eae 151         if ($this->options['debug']) {
7f1da4 152             $this->conn->setDebug(true, array($this, 'debug_handler'));
A 153
890eae 154             $this->options['ident'] = array(
A 155                 'name' => 'Roundcube Webmail',
156                 'version' => RCMAIL_VERSION,
157                 'php' => PHP_VERSION,
158                 'os' => PHP_OS,
159                 'command' => $_SERVER['REQUEST_URI'],
160             );
161         }
162
59c216 163         $attempt = 0;
A 164         do {
165             $data = rcmail::get_instance()->plugins->exec_hook('imap_connect',
166                 array('host' => $host, 'user' => $user, 'attempt' => ++$attempt));
c43517 167
59c216 168             if (!empty($data['pass']))
A 169                 $pass = $data['pass'];
b026c3 170
59c216 171             $this->conn->connect($data['host'], $data['user'], $pass, $this->options);
A 172         } while(!$this->conn->connected() && $data['retry']);
80fbda 173
59c216 174         $this->host = $data['host'];
A 175         $this->user = $data['user'];
176         $this->pass = $pass;
177         $this->port = $port;
178         $this->ssl  = $use_ssl;
520c36 179
59c216 180         if ($this->conn->connected()) {
00290a 181             // get namespace and delimiter
A 182             $this->set_env();
94a6c6 183             return true;
59c216 184         }
A 185         // write error log
186         else if ($this->conn->error) {
ad399a 187             if ($pass && $user) {
A 188                 $message = sprintf("Login failed for %s from %s. %s",
189                     $user, rcmail_remote_ip(), $this->conn->error);
190
b36491 191                 raise_error(array('code' => 403, 'type' => 'imap',
A 192                     'file' => __FILE__, 'line' => __LINE__,
ad399a 193                     'message' => $message), true, false);
A 194             }
59c216 195         }
A 196
94a6c6 197         return false;
4e17e6 198     }
T 199
30b302 200
59c216 201     /**
A 202      * Close IMAP connection
203      * Usually done on script shutdown
204      *
205      * @access public
206      */
207     function close()
c43517 208     {
e232ac 209         $this->conn->closeConnection();
59c216 210         $this->write_cache();
4e17e6 211     }
T 212
30b302 213
59c216 214     /**
A 215      * Close IMAP connection and re-connect
216      * This is used to avoid some strange socket errors when talking to Courier IMAP
217      *
218      * @access public
219      */
220     function reconnect()
520c36 221     {
dd8354 222         $this->conn->closeConnection();
A 223         $connected = $this->connect($this->host, $this->user, $this->pass, $this->port, $this->ssl);
c43517 224
59c216 225         // issue SELECT command to restore connection status
dd8354 226         if ($connected && strlen($this->mailbox))
59c216 227             $this->conn->select($this->mailbox);
520c36 228     }
T 229
8fcc3e 230
A 231     /**
232      * Returns code of last error
233      *
234      * @return int Error code
235      */
236     function get_error_code()
237     {
7f1da4 238         return $this->conn->errornum;
8fcc3e 239     }
A 240
241
242     /**
243      * Returns message of last error
244      *
245      * @return string Error message
246      */
247     function get_error_str()
248     {
7f1da4 249         return $this->conn->error;
90f81a 250     }
A 251
252
253     /**
254      * Returns code of last command response
255      *
256      * @return int Response code
257      */
258     function get_response_code()
259     {
260         switch ($this->conn->resultcode) {
261             case 'NOPERM':
262                 return self::NOPERM;
263             case 'READ-ONLY':
264                 return self::READONLY;
265             case 'TRYCREATE':
266                 return self::TRYCREATE;
267             case 'INUSE':
268                 return self::INUSE;
269             case 'OVERQUOTA':
270                 return self::OVERQUOTA;
271             case 'ALREADYEXISTS':
272                 return self::ALREADYEXISTS;
273             case 'NONEXISTENT':
274                 return self::NONEXISTENT;
275             case 'CONTACTADMIN':
276                 return self::CONTACTADMIN;
277             default:
278                 return self::UNKNOWN;
279         }
280     }
281
282
283     /**
284      * Returns last command response
285      *
286      * @return string Response
287      */
288     function get_response_str()
289     {
7f1da4 290         return $this->conn->result;
8fcc3e 291     }
30b302 292
8fcc3e 293
59c216 294     /**
A 295      * Set options to be used in rcube_imap_generic::connect()
5c461b 296      *
A 297      * @param array $opt Options array
59c216 298      */
A 299     function set_options($opt)
4e17e6 300     {
59c216 301         $this->options = array_merge($this->options, (array)$opt);
17b5fb 302     }
T 303
30b302 304
59c216 305     /**
A 306      * Set default message charset
307      *
308      * This will be used for message decoding if a charset specification is not available
309      *
5c461b 310      * @param  string $cs Charset string
59c216 311      * @access public
A 312      */
313     function set_charset($cs)
17b5fb 314     {
59c216 315         $this->default_charset = $cs;
4e17e6 316     }
T 317
30b302 318
59c216 319     /**
A 320      * This list of folders will be listed above all other folders
321      *
5c461b 322      * @param  array $arr Indexed list of folder names
59c216 323      * @access public
A 324      */
325     function set_default_mailboxes($arr)
4e17e6 326     {
59c216 327         if (is_array($arr)) {
A 328             $this->default_folders = $arr;
fa4cd2 329
59c216 330             // add inbox if not included
A 331             if (!in_array('INBOX', $this->default_folders))
332                 array_unshift($this->default_folders, 'INBOX');
333         }
4e17e6 334     }
T 335
30b302 336
59c216 337     /**
A 338      * Set internal mailbox reference.
339      *
340      * All operations will be perfomed on this mailbox/folder
341      *
d08333 342      * @param  string $mailbox Mailbox/Folder name
59c216 343      * @access public
A 344      */
d08333 345     function set_mailbox($mailbox)
4e17e6 346     {
59c216 347         if ($this->mailbox == $mailbox)
A 348             return;
4e17e6 349
59c216 350         $this->mailbox = $mailbox;
4e17e6 351
59c216 352         // clear messagecount cache for this mailbox
A 353         $this->_clear_messagecount($mailbox);
4e17e6 354     }
T 355
30b302 356
A 357     /**
358      * Forces selection of a mailbox
359      *
360      * @param  string $mailbox Mailbox/Folder name
361      * @access public
362      */
90f81a 363     function select_mailbox($mailbox=null)
30b302 364     {
d08333 365         if (!strlen($mailbox)) {
A 366             $mailbox = $this->mailbox;
367         }
30b302 368
A 369         $selected = $this->conn->select($mailbox);
370
371         if ($selected && $this->mailbox != $mailbox) {
372             // clear messagecount cache for this mailbox
373             $this->_clear_messagecount($mailbox);
374             $this->mailbox = $mailbox;
375         }
376     }
377
378
59c216 379     /**
A 380      * Set internal list page
381      *
5c461b 382      * @param  number $page Page number to list
59c216 383      * @access public
A 384      */
385     function set_page($page)
4e17e6 386     {
59c216 387         $this->list_page = (int)$page;
4e17e6 388     }
T 389
30b302 390
59c216 391     /**
A 392      * Set internal page size
393      *
5c461b 394      * @param  number $size Number of messages to display on one page
59c216 395      * @access public
A 396      */
397     function set_pagesize($size)
4e17e6 398     {
59c216 399         $this->page_size = (int)$size;
4e17e6 400     }
c43517 401
30b302 402
59c216 403     /**
A 404      * Save a set of message ids for future message listing methods
405      *
406      * @param  string  IMAP Search query
407      * @param  array   List of message ids or NULL if empty
408      * @param  string  Charset of search string
409      * @param  string  Sorting field
c60978 410      * @param  string  True if set is sorted (SORT was used for searching)
59c216 411      */
c60978 412     function set_search_set($str=null, $msgs=null, $charset=null, $sort_field=null, $threads=false, $sorted=false)
04c618 413     {
59c216 414         if (is_array($str) && $msgs == null)
A 415             list($str, $msgs, $charset, $sort_field, $threads) = $str;
ffd3e2 416         if ($msgs === false)
A 417             $msgs = array();
418         else if ($msgs != null && !is_array($msgs))
59c216 419             $msgs = explode(',', $msgs);
f52c93 420
59c216 421         $this->search_string     = $str;
A 422         $this->search_set        = $msgs;
423         $this->search_charset    = $charset;
424         $this->search_sort_field = $sort_field;
425         $this->search_threads    = $threads;
c60978 426         $this->search_sorted     = $sorted;
04c618 427     }
T 428
30b302 429
59c216 430     /**
A 431      * Return the saved search set as hash array
432      * @return array Search set
433      */
434     function get_search_set()
04c618 435     {
59c216 436         return array($this->search_string,
A 437             $this->search_set,
438             $this->search_charset,
439             $this->search_sort_field,
440             $this->search_threads,
c60978 441             $this->search_sorted,
59c216 442         );
04c618 443     }
4e17e6 444
30b302 445
59c216 446     /**
A 447      * Returns the currently used mailbox name
448      *
449      * @return  string Name of the mailbox/folder
450      * @access  public
451      */
452     function get_mailbox_name()
4e17e6 453     {
d08333 454         return $this->conn->connected() ? $this->mailbox : '';
1cded8 455     }
T 456
30b302 457
59c216 458     /**
A 459      * Returns the IMAP server's capability
460      *
5c461b 461      * @param   string  $cap Capability name
59c216 462      * @return  mixed   Capability value or TRUE if supported, FALSE if not
A 463      * @access  public
464      */
465     function get_capability($cap)
1cded8 466     {
59c216 467         return $this->conn->getCapability(strtoupper($cap));
f52c93 468     }
T 469
30b302 470
59c216 471     /**
A 472      * Sets threading flag to the best supported THREAD algorithm
473      *
5c461b 474      * @param  boolean  $enable TRUE to enable and FALSE
59c216 475      * @return string   Algorithm or false if THREAD is not supported
A 476      * @access public
477      */
478     function set_threading($enable=false)
f52c93 479     {
59c216 480         $this->threading = false;
c43517 481
600bb1 482         if ($enable && ($caps = $this->get_capability('THREAD'))) {
A 483             if (in_array('REFS', $caps))
59c216 484                 $this->threading = 'REFS';
600bb1 485             else if (in_array('REFERENCES', $caps))
59c216 486                 $this->threading = 'REFERENCES';
600bb1 487             else if (in_array('ORDEREDSUBJECT', $caps))
59c216 488                 $this->threading = 'ORDEREDSUBJECT';
A 489         }
490
491         return $this->threading;
492     }
493
30b302 494
59c216 495     /**
A 496      * Checks the PERMANENTFLAGS capability of the current mailbox
497      * and returns true if the given flag is supported by the IMAP server
498      *
5c461b 499      * @param   string  $flag Permanentflag name
29983c 500      * @return  boolean True if this flag is supported
59c216 501      * @access  public
A 502      */
503     function check_permflag($flag)
504     {
505         $flag = strtoupper($flag);
506         $imap_flag = $this->conn->flags[$flag];
a2e8cb 507         return (in_array_nocase($imap_flag, $this->conn->data['PERMANENTFLAGS']));
59c216 508     }
A 509
30b302 510
59c216 511     /**
A 512      * Returns the delimiter that is used by the IMAP server for folder separation
513      *
514      * @return  string  Delimiter string
515      * @access  public
516      */
517     function get_hierarchy_delimiter()
518     {
519         return $this->delimiter;
00290a 520     }
A 521
522
523     /**
524      * Get namespace
525      *
d08333 526      * @param string $name Namespace array index: personal, other, shared, prefix
A 527      *
00290a 528      * @return  array  Namespace data
A 529      * @access  public
530      */
d08333 531     function get_namespace($name=null)
00290a 532     {
d08333 533         $ns = $this->namespace;
A 534
535         if ($name) {
536             return isset($ns[$name]) ? $ns[$name] : null;
537         }
538
539         unset($ns['prefix']);
540         return $ns;
00290a 541     }
A 542
543
544     /**
545      * Sets delimiter and namespaces
546      *
547      * @access private
548      */
549     private function set_env()
550     {
551         if ($this->delimiter !== null && $this->namespace !== null) {
552             return;
553         }
554
555         if (isset($_SESSION['imap_namespace']) && isset($_SESSION['imap_delimiter'])) {
556             $this->namespace = $_SESSION['imap_namespace'];
557             $this->delimiter = $_SESSION['imap_delimiter'];
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 {
A 709                 if ($this->caching_enabled) {
710                     $keys[] = 'ALL';
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') {
A 725                 if ($need_uid && $this->caching_enabled) {
726                     // Save messages index for check_cache_status()
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
eacce9 841         if ($this->caching_enabled) {
A 842             // cache is OK, we can get messages from local cache
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
1323         if ($this->caching_enabled && $cache_key) {
1324             // cache is incomplete?
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
2107         if ($this->caching_enabled)
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                     }
2166                     else if (in_array('name', (array)$part[$i][2]) && (empty($part[$i][3]) || $part[$i][3]=='NIL')) {
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
A 2234         if (!empty($part[5]) && $part[5]!='NIL') {
2235             $struct->encoding = strtolower($part[5]);
2236             $struct->headers['content-transfer-encoding'] = $struct->encoding;
2237         }
c43517 2238
59c216 2239         // get part size
A 2240         if (!empty($part[6]) && $part[6]!='NIL')
2241             $struct->size = intval($part[6]);
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
2267         if (!empty($part[3]) && $part[3]!='NIL') {
2268             $struct->content_id = $part[3];
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
A 2587             if ($this->caching_enabled && !$skip_cache) {
2588                 $cache_key = $mailbox.'.msg';
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
3086                 // remove non-existent folders
3087                 if (is_array($a_folders)) {
3088                     foreach ($a_folders as $idx => $folder) {
3089                         if ($this->conn->data['LIST'] && ($opts = $this->conn->data['LIST'][$folder])
3090                             && in_array('\\NonExistent', $opts)
3091                         ) {
3092                             unset($a_folders[$idx]);
3093                         } 
3094                     }
3095                 }
3096             }
3097             // retrieve list of folders from IMAP server using LSUB
3098             else {
94bdcc 3099                 $a_folders = $this->conn->listSubscribed($root, $name);
3870be 3100             }
59c216 3101         }
c43517 3102
94bdcc 3103         if (!is_array($a_folders) || !sizeof($a_folders)) {
59c216 3104             $a_folders = array();
94bdcc 3105         }
59c216 3106
A 3107         // write mailboxlist to cache
94bdcc 3108         $this->update_cache($cache_key, $a_folders);
c43517 3109
59c216 3110         return $a_folders;
A 3111     }
3112
3113
3114     /**
3115      * Get a list of all folders available on the IMAP server
c43517 3116      *
5c461b 3117      * @param string $root   IMAP root dir
94bdcc 3118      * @param string  $name   Optional name pattern
A 3119      * @param mixed   $filter Optional filter
3120      *
59c216 3121      * @return array Indexed array with folder names
A 3122      */
94bdcc 3123     function list_unsubscribed($root='', $name='*', $filter=null)
59c216 3124     {
94bdcc 3125         // @TODO: caching
6f4e7d 3126         // Give plugins a chance to provide a list of mailboxes
e6ce00 3127         $data = rcmail::get_instance()->plugins->exec_hook('mailboxes_list',
94bdcc 3128             array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LIST'));
c43517 3129
6f4e7d 3130         if (isset($data['folders'])) {
A 3131             $a_mboxes = $data['folders'];
3132         }
3133         else {
3134             // retrieve list of folders from IMAP server
94bdcc 3135             $a_mboxes = $this->conn->listMailboxes($root, $name);
6f4e7d 3136         }
64e3e8 3137
d08333 3138         if (!is_array($a_mboxes)) {
6f4e7d 3139             $a_mboxes = array();
59c216 3140         }
f0485a 3141
A 3142         // INBOX should always be available
94bdcc 3143         if ((!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)) {
d08333 3144             array_unshift($a_mboxes, 'INBOX');
94bdcc 3145         }
59c216 3146
A 3147         // filter folders and sort them
d08333 3148         $a_mboxes = $this->_sort_mailbox_list($a_mboxes);
A 3149
3150         return $a_mboxes;
59c216 3151     }
A 3152
3153
3154     /**
3155      * Get mailbox quota information
3156      * added by Nuny
c43517 3157      *
59c216 3158      * @return mixed Quota info or False if not supported
A 3159      */
3160     function get_quota()
3161     {
3162         if ($this->get_capability('QUOTA'))
3163             return $this->conn->getQuota();
c43517 3164
59c216 3165         return false;
A 3166     }
3167
3168
3169     /**
af3c04 3170      * Get mailbox size (size of all messages in a mailbox)
A 3171      *
d08333 3172      * @param string $mailbox Mailbox name
A 3173      *
af3c04 3174      * @return int Mailbox size in bytes, False on error
A 3175      */
d08333 3176     function get_mailbox_size($mailbox)
af3c04 3177     {
A 3178         // @TODO: could we try to use QUOTA here?
d08333 3179         $result = $this->conn->fetchHeaderIndex($mailbox, '1:*', 'SIZE', false);
af3c04 3180
A 3181         if (is_array($result))
3182             $result = array_sum($result);
3183
3184         return $result;
3185     }
3186
3187
3188     /**
59c216 3189      * Subscribe to a specific mailbox(es)
A 3190      *
5c461b 3191      * @param array $a_mboxes Mailbox name(s)
59c216 3192      * @return boolean True on success
c43517 3193      */
59c216 3194     function subscribe($a_mboxes)
A 3195     {
3196         if (!is_array($a_mboxes))
3197             $a_mboxes = array($a_mboxes);
3198
3199         // let this common function do the main work
3200         return $this->_change_subscription($a_mboxes, 'subscribe');
3201     }
3202
3203
3204     /**
3205      * Unsubscribe mailboxes
3206      *
5c461b 3207      * @param array $a_mboxes Mailbox name(s)
59c216 3208      * @return boolean True on success
A 3209      */
3210     function unsubscribe($a_mboxes)
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, 'unsubscribe');
3217     }
3218
3219
3220     /**
3221      * Create a new mailbox on the server and register it in local cache
3222      *
d08333 3223      * @param string  $mailbox   New mailbox name
5c461b 3224      * @param boolean $subscribe True if the new mailbox should be subscribed
d08333 3225      *
A 3226      * @return boolean True on success
59c216 3227      */
d08333 3228     function create_mailbox($mailbox, $subscribe=false)
59c216 3229     {
d08333 3230         $result = $this->conn->createFolder($mailbox);
59c216 3231
A 3232         // try to subscribe it
3233         if ($result && $subscribe)
d08333 3234             $this->subscribe($mailbox);
59c216 3235
af3c04 3236         return $result;
59c216 3237     }
A 3238
3239
3240     /**
3241      * Set a new name to an existing mailbox
3242      *
d08333 3243      * @param string $mailbox  Mailbox to rename
A 3244      * @param string $new_name New mailbox name
0e1194 3245      *
af3c04 3246      * @return boolean True on success
59c216 3247      */
d08333 3248     function rename_mailbox($mailbox, $new_name)
59c216 3249     {
d08333 3250         if (!strlen($new_name)) {
A 3251             return false;
3252         }
59c216 3253
d08333 3254         $delm = $this->get_hierarchy_delimiter();
c43517 3255
0e1194 3256         // get list of subscribed folders
A 3257         if ((strpos($mailbox, '%') === false) && (strpos($mailbox, '*') === false)) {
d08333 3258             $a_subscribed = $this->_list_mailboxes('', $mailbox . $delm . '*');
A 3259             $subscribed   = $this->mailbox_exists($mailbox, true);
0e1194 3260         }
A 3261         else {
3262             $a_subscribed = $this->_list_mailboxes();
3263             $subscribed   = in_array($mailbox, $a_subscribed);
3264         }
59c216 3265
d08333 3266         $result = $this->conn->renameFolder($mailbox, $new_name);
59c216 3267
A 3268         if ($result) {
0e1194 3269             // unsubscribe the old folder, subscribe the new one
A 3270             if ($subscribed) {
3271                 $this->conn->unsubscribe($mailbox);
d08333 3272                 $this->conn->subscribe($new_name);
0e1194 3273             }
677e1f 3274
59c216 3275             // check if mailbox children are subscribed
0e1194 3276             foreach ($a_subscribed as $c_subscribed) {
59c216 3277                 if (preg_match('/^'.preg_quote($mailbox.$delm, '/').'/', $c_subscribed)) {
A 3278                     $this->conn->unsubscribe($c_subscribed);
3279                     $this->conn->subscribe(preg_replace('/^'.preg_quote($mailbox, '/').'/',
d08333 3280                         $new_name, $c_subscribed));
59c216 3281                 }
0e1194 3282             }
59c216 3283
A 3284             // clear cache
3285             $this->clear_message_cache($mailbox.'.msg');
94bdcc 3286             $this->clear_cache('/^mailboxes.*/', true);
59c216 3287         }
A 3288
af3c04 3289         return $result;
59c216 3290     }
A 3291
3292
3293     /**
0e1194 3294      * Remove mailbox from server
59c216 3295      *
d08333 3296      * @param string $mailbox Mailbox name
0e1194 3297      *
59c216 3298      * @return boolean True on success
A 3299      */
d08333 3300     function delete_mailbox($mailbox)
59c216 3301     {
d08333 3302         $delm = $this->get_hierarchy_delimiter();
59c216 3303
0e1194 3304         // get list of folders
A 3305         if ((strpos($mailbox, '%') === false) && (strpos($mailbox, '*') === false))
3306             $sub_mboxes = $this->list_unsubscribed('', $mailbox . $delm . '*');
3307         else
3308             $sub_mboxes = $this->list_unsubscribed();
59c216 3309
0e1194 3310         // send delete command to server
A 3311         $result = $this->conn->deleteFolder($mailbox);
59c216 3312
0e1194 3313         if ($result) {
A 3314             // unsubscribe mailbox
3315             $this->conn->unsubscribe($mailbox);
59c216 3316
0e1194 3317             foreach ($sub_mboxes as $c_mbox) {
A 3318                 if (preg_match('/^'.preg_quote($mailbox.$delm, '/').'/', $c_mbox)) {
3319                     $this->conn->unsubscribe($c_mbox);
3320                     if ($this->conn->deleteFolder($c_mbox)) {
3321                         $this->clear_message_cache($c_mbox.'.msg');
59c216 3322                     }
A 3323                 }
3324             }
0e1194 3325
A 3326             // clear mailbox-related cache
3327             $this->clear_message_cache($mailbox.'.msg');
94bdcc 3328             $this->clear_cache('/^mailboxes.*/', true);
59c216 3329         }
A 3330
0e1194 3331         return $result;
59c216 3332     }
A 3333
3334
3335     /**
3336      * Create all folders specified as default
3337      */
3338     function create_default_folders()
3339     {
3340         // create default folders if they do not exist
3341         foreach ($this->default_folders as $folder) {
3342             if (!$this->mailbox_exists($folder))
3343                 $this->create_mailbox($folder, true);
3344             else if (!$this->mailbox_exists($folder, true))
3345                 $this->subscribe($folder);
3346         }
3347     }
3348
3349
3350     /**
3351      * Checks if folder exists and is subscribed
3352      *
d08333 3353      * @param string   $mailbox      Folder name
5c461b 3354      * @param boolean  $subscription Enable subscription checking
d08333 3355      *
59c216 3356      * @return boolean TRUE or FALSE
A 3357      */
d08333 3358     function mailbox_exists($mailbox, $subscription=false)
59c216 3359     {
d08333 3360         if ($mailbox == 'INBOX') {
448409 3361             return true;
d08333 3362         }
59c216 3363
448409 3364         $key  = $subscription ? 'subscribed' : 'existing';
ad5881 3365
d08333 3366         if (is_array($this->icache[$key]) && in_array($mailbox, $this->icache[$key]))
448409 3367             return true;
16378f 3368
448409 3369         if ($subscription) {
d08333 3370             $a_folders = $this->conn->listSubscribed('', $mailbox);
448409 3371         }
A 3372         else {
d08333 3373             $a_folders = $this->conn->listMailboxes('', $mailbox);
af3c04 3374         }
c43517 3375
d08333 3376         if (is_array($a_folders) && in_array($mailbox, $a_folders)) {
A 3377             $this->icache[$key][] = $mailbox;
448409 3378             return true;
59c216 3379         }
A 3380
3381         return false;
3382     }
3383
3384
3385     /**
bbce3e 3386      * Returns the namespace where the folder is in
A 3387      *
d08333 3388      * @param string $mailbox Folder name
bbce3e 3389      *
A 3390      * @return string One of 'personal', 'other' or 'shared'
3391      * @access public
3392      */
d08333 3393     function mailbox_namespace($mailbox)
bbce3e 3394     {
d08333 3395         if ($mailbox == 'INBOX') {
bbce3e 3396             return 'personal';
A 3397         }
3398
3399         foreach ($this->namespace as $type => $namespace) {
3400             if (is_array($namespace)) {
3401                 foreach ($namespace as $ns) {
3402                     if (strlen($ns[0])) {
d08333 3403                         if ((strlen($ns[0])>1 && $mailbox == substr($ns[0], 0, -1))
A 3404                             || strpos($mailbox, $ns[0]) === 0
bbce3e 3405                         ) {
A 3406                             return $type;
3407                         }
3408                     }
3409                 }
3410             }
3411         }
3412
3413         return 'personal';
3414     }
3415
3416
3417     /**
d08333 3418      * Modify folder name according to namespace.
A 3419      * For output it removes prefix of the personal namespace if it's possible.
3420      * For input it adds the prefix. Use it before creating a folder in root
3421      * of the folders tree.
59c216 3422      *
d08333 3423      * @param string $mailbox Folder name
A 3424      * @param string $mode    Mode name (out/in)
3425      *
59c216 3426      * @return string Folder name
A 3427      */
d08333 3428     function mod_mailbox($mailbox, $mode = 'out')
59c216 3429     {
d08333 3430         if (!strlen($mailbox)) {
A 3431             return $mailbox;
3432         }
59c216 3433
d08333 3434         $prefix     = $this->namespace['prefix']; // see set_env()
A 3435         $prefix_len = strlen($prefix);
3436
3437         if (!$prefix_len) {
3438             return $mailbox;
3439         }
3440
3441         // remove prefix for output
3442         if ($mode == 'out') {
3443             if (substr($mailbox, 0, $prefix_len) === $prefix) {
3444                 return substr($mailbox, $prefix_len);
00290a 3445             }
A 3446         }
d08333 3447         // add prefix for input (e.g. folder creation)
00290a 3448         else {
d08333 3449             return $prefix . $mailbox;
59c216 3450         }
c43517 3451
d08333 3452         return $mailbox;
59c216 3453     }
A 3454
3455
103ddc 3456     /**
3870be 3457      * Gets folder options from LIST response, e.g. \Noselect, \Noinferiors
a5a4bf 3458      *
d08333 3459      * @param string $mailbox Folder name
A 3460      * @param bool   $force   Set to True if options should be refreshed
3461      *                        Options are available after LIST command only
a5a4bf 3462      *
A 3463      * @return array Options list
3464      */
d08333 3465     function mailbox_options($mailbox, $force=false)
a5a4bf 3466     {
d08333 3467         if ($mailbox == 'INBOX') {
a5a4bf 3468             return array();
A 3469         }
3470
d08333 3471         if (!is_array($this->conn->data['LIST']) || !is_array($this->conn->data['LIST'][$mailbox])) {
3870be 3472             if ($force) {
d08333 3473                 $this->conn->listMailboxes('', $mailbox);
3870be 3474             }
A 3475             else {
3476                 return array();
3477             }
a5a4bf 3478         }
A 3479
d08333 3480         $opts = $this->conn->data['LIST'][$mailbox];
a5a4bf 3481
A 3482         return is_array($opts) ? $opts : array();
3483     }
3484
3485
3486     /**
103ddc 3487      * Get message header names for rcube_imap_generic::fetchHeader(s)
A 3488      *
3489      * @return string Space-separated list of header names
3490      */
3491     private function get_fetch_headers()
3492     {
3493         $headers = explode(' ', $this->fetch_add_headers);
3494         $headers = array_map('strtoupper', $headers);
3495
3496         if ($this->caching_enabled || $this->get_all_headers)
3497             $headers = array_merge($headers, $this->all_headers);
3498
3499         return implode(' ', array_unique($headers));
3500     }
3501
3502
8b6eff 3503     /* -----------------------------------------
A 3504      *   ACL and METADATA/ANNOTATEMORE methods
3505      * ----------------------------------------*/
3506
3507     /**
3508      * Changes the ACL on the specified mailbox (SETACL)
3509      *
3510      * @param string $mailbox Mailbox name
3511      * @param string $user    User name
3512      * @param string $acl     ACL string
3513      *
3514      * @return boolean True on success, False on failure
3515      *
3516      * @access public
3517      * @since 0.5-beta
3518      */
3519     function set_acl($mailbox, $user, $acl)
3520     {
3521         if ($this->get_capability('ACL'))
3522             return $this->conn->setACL($mailbox, $user, $acl);
3523
3524         return false;
3525     }
3526
3527
3528     /**
3529      * Removes any <identifier,rights> pair for the
3530      * specified user from the ACL for the specified
3531      * mailbox (DELETEACL)
3532      *
3533      * @param string $mailbox Mailbox name
3534      * @param string $user    User name
3535      *
3536      * @return boolean True on success, False on failure
3537      *
3538      * @access public
3539      * @since 0.5-beta
3540      */
3541     function delete_acl($mailbox, $user)
3542     {
3543         if ($this->get_capability('ACL'))
3544             return $this->conn->deleteACL($mailbox, $user);
3545
3546         return false;
3547     }
3548
3549
3550     /**
3551      * Returns the access control list for mailbox (GETACL)
3552      *
3553      * @param string $mailbox Mailbox name
3554      *
3555      * @return array User-rights array on success, NULL on error
3556      * @access public
3557      * @since 0.5-beta
3558      */
3559     function get_acl($mailbox)
3560     {
3561         if ($this->get_capability('ACL'))
3562             return $this->conn->getACL($mailbox);
3563
3564         return NULL;
3565     }
3566
3567
3568     /**
3569      * Returns information about what rights can be granted to the
3570      * user (identifier) in the ACL for the mailbox (LISTRIGHTS)
3571      *
3572      * @param string $mailbox Mailbox name
3573      * @param string $user    User name
3574      *
3575      * @return array List of user rights
3576      * @access public
3577      * @since 0.5-beta
3578      */
3579     function list_rights($mailbox, $user)
3580     {
3581         if ($this->get_capability('ACL'))
3582             return $this->conn->listRights($mailbox, $user);
3583
3584         return NULL;
3585     }
3586
3587
3588     /**
3589      * Returns the set of rights that the current user has to
3590      * mailbox (MYRIGHTS)
3591      *
3592      * @param string $mailbox Mailbox name
3593      *
3594      * @return array MYRIGHTS response on success, NULL on error
3595      * @access public
3596      * @since 0.5-beta
3597      */
3598     function my_rights($mailbox)
3599     {
3600         if ($this->get_capability('ACL'))
3601             return $this->conn->myRights($mailbox);
3602
3603         return NULL;
3604     }
3605
3606
3607     /**
3608      * Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
3609      *
3610      * @param string $mailbox Mailbox name (empty for server metadata)
3611      * @param array  $entries Entry-value array (use NULL value as NIL)
3612      *
3613      * @return boolean True on success, False on failure
3614      * @access public
3615      * @since 0.5-beta
3616      */
3617     function set_metadata($mailbox, $entries)
3618     {
448409 3619         if ($this->get_capability('METADATA') ||
A 3620             (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
8b6eff 3621         ) {
A 3622             return $this->conn->setMetadata($mailbox, $entries);
3623         }
3624         else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
e22740 3625             foreach ((array)$entries as $entry => $value) {
8b6eff 3626                 list($ent, $attr) = $this->md2annotate($entry);
A 3627                 $entries[$entry] = array($ent, $attr, $value);
3628             }
3629             return $this->conn->setAnnotation($mailbox, $entries);
3630         }
3631
3632         return false;
3633     }
3634
3635
3636     /**
3637      * Unsets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
3638      *
3639      * @param string $mailbox Mailbox name (empty for server metadata)
3640      * @param array  $entries Entry names array
3641      *
3642      * @return boolean True on success, False on failure
3643      *
3644      * @access public
3645      * @since 0.5-beta
3646      */
3647     function delete_metadata($mailbox, $entries)
3648     {
3649         if ($this->get_capability('METADATA') || 
448409 3650             (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
8b6eff 3651         ) {
A 3652             return $this->conn->deleteMetadata($mailbox, $entries);
3653         }
3654         else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
e22740 3655             foreach ((array)$entries as $idx => $entry) {
8b6eff 3656                 list($ent, $attr) = $this->md2annotate($entry);
A 3657                 $entries[$idx] = array($ent, $attr, NULL);
3658             }
3659             return $this->conn->setAnnotation($mailbox, $entries);
3660         }
3661
3662         return false;
3663     }
3664
3665
3666     /**
3667      * Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION)
3668      *
3669      * @param string $mailbox Mailbox name (empty for server metadata)
3670      * @param array  $entries Entries
3671      * @param array  $options Command options (with MAXSIZE and DEPTH keys)
3672      *
3673      * @return array Metadata entry-value hash array on success, NULL on error
3674      *
3675      * @access public
3676      * @since 0.5-beta
3677      */
3678     function get_metadata($mailbox, $entries, $options=array())
3679     {
3680         if ($this->get_capability('METADATA') || 
e22740 3681             (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
8b6eff 3682         ) {
A 3683             return $this->conn->getMetadata($mailbox, $entries, $options);
3684         }
3685         else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
3686             $queries = array();
3687             $res     = array();
3688
3689             // Convert entry names
e22740 3690             foreach ((array)$entries as $entry) {
8b6eff 3691                 list($ent, $attr) = $this->md2annotate($entry);
A 3692                 $queries[$attr][] = $ent;
3693             }
3694
3695             // @TODO: Honor MAXSIZE and DEPTH options
3696             foreach ($queries as $attrib => $entry)
3697                 if ($result = $this->conn->getAnnotation($mailbox, $entry, $attrib))
3698                     $res = array_merge($res, $result);
3699
3700             return $res;
3701         }
3702
3703         return NULL;
3704     }
3705
3706
3707     /**
3708      * Converts the METADATA extension entry name into the correct
3709      * entry-attrib names for older ANNOTATEMORE version.
3710      *
e22740 3711      * @param string $entry Entry name
8b6eff 3712      *
A 3713      * @return array Entry-attribute list, NULL if not supported (?)
3714      */
e22740 3715     private function md2annotate($entry)
8b6eff 3716     {
A 3717         if (substr($entry, 0, 7) == '/shared') {
3718             return array(substr($entry, 7), 'value.shared');
3719         }
3720         else if (substr($entry, 0, 8) == '/private') {
3721             return array(substr($entry, 8), 'value.priv');
3722         }
3723
3724         // @TODO: log error
3725         return NULL;
3726     }
3727
3728
59c216 3729     /* --------------------------------
A 3730      *   internal caching methods
3731      * --------------------------------*/
3732
3733     /**
5c461b 3734      * Enable or disable caching
A 3735      *
3736      * @param boolean $set Flag
59c216 3737      * @access public
A 3738      */
3739     function set_caching($set)
3740     {
3741         if ($set && is_object($this->db))
3742             $this->caching_enabled = true;
3743         else
3744             $this->caching_enabled = false;
3745     }
3746
29983c 3747
59c216 3748     /**
5c461b 3749      * Returns cached value
A 3750      *
3751      * @param string $key Cache key
3752      * @return mixed
59c216 3753      * @access public
A 3754      */
3755     function get_cache($key)
3756     {
3757         // read cache (if it was not read before)
3758         if (!count($this->cache) && $this->caching_enabled) {
3759             return $this->_read_cache_record($key);
3760         }
c43517 3761
59c216 3762         return $this->cache[$key];
A 3763     }
3764
29983c 3765
59c216 3766     /**
5c461b 3767      * Update cache
A 3768      *
3769      * @param string $key  Cache key
3770      * @param mixed  $data Data
59c216 3771      * @access private
A 3772      */
3773     private function update_cache($key, $data)
3774     {
3775         $this->cache[$key] = $data;
3776         $this->cache_changed = true;
3777         $this->cache_changes[$key] = true;
3778     }
29983c 3779
59c216 3780
A 3781     /**
5c461b 3782      * Writes the cache
A 3783      *
59c216 3784      * @access private
A 3785      */
3786     private function write_cache()
3787     {
3788         if ($this->caching_enabled && $this->cache_changed) {
3789             foreach ($this->cache as $key => $data) {
3790                 if ($this->cache_changes[$key])
3791                     $this->_write_cache_record($key, serialize($data));
3792             }
3793         }
3794     }
29983c 3795
59c216 3796
A 3797     /**
5c461b 3798      * Clears the cache.
A 3799      *
94bdcc 3800      * @param string  $key          Cache key name or pattern
A 3801      * @param boolean $pattern_mode Enable it to clear all keys with name
3802      *                              matching PREG pattern in $key
59c216 3803      * @access public
A 3804      */
94bdcc 3805     function clear_cache($key=null, $pattern_mode=false)
59c216 3806     {
A 3807         if (!$this->caching_enabled)
3808             return;
c43517 3809
94bdcc 3810         if ($key === null) {
A 3811             foreach (array_keys($this->cache) as $key)
59c216 3812                 $this->_clear_cache_record($key);
A 3813
3814             $this->cache = array();
3815             $this->cache_changed = false;
3816             $this->cache_changes = array();
3817         }
94bdcc 3818         else if ($pattern_mode) {
A 3819             foreach (array_keys($this->cache) as $k) {
3820                 if (preg_match($key, $k)) {
3821                     $this->_clear_cache_record($k);
3822                     $this->cache_changes[$k] = false;
3823                     unset($this->cache[$key]);
3824                 }
3825             }
3826             if (!count($this->cache)) {
3827                 $this->cache_changed = false;
3828             }
3829         }
59c216 3830         else {
A 3831             $this->_clear_cache_record($key);
3832             $this->cache_changes[$key] = false;
3833             unset($this->cache[$key]);
3834         }
3835     }
29983c 3836
59c216 3837
A 3838     /**
5c461b 3839      * Returns cached entry
A 3840      *
3841      * @param string $key Cache key
3842      * @return mixed Cached value
59c216 3843      * @access private
A 3844      */
3845     private function _read_cache_record($key)
3846     {
3847         if ($this->db) {
3848             // get cached data from DB
3849             $sql_result = $this->db->query(
3850                 "SELECT cache_id, data, cache_key ".
3851                 "FROM ".get_table_name('cache').
3852                 " WHERE user_id=? ".
3853                 "AND cache_key LIKE 'IMAP.%'",
3854                 $_SESSION['user_id']);
3855
3856             while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
3857                 $sql_key = preg_replace('/^IMAP\./', '', $sql_arr['cache_key']);
3858                 $this->cache_keys[$sql_key] = $sql_arr['cache_id'];
3859                 if (!isset($this->cache[$sql_key]))
3860                     $this->cache[$sql_key] = $sql_arr['data'] ? unserialize($sql_arr['data']) : false;
3861             }
3862         }
3863
3864         return $this->cache[$key];
3865     }
29983c 3866
59c216 3867
A 3868     /**
5c461b 3869      * Writes single cache record
A 3870      *
3871      * @param string $key  Cache key
3872      * @param mxied  $data Cache value
59c216 3873      * @access private
A 3874      */
3875     private function _write_cache_record($key, $data)
3876     {
3877         if (!$this->db)
3878             return false;
3879
3880         // update existing cache record
3881         if ($this->cache_keys[$key]) {
3882             $this->db->query(
3883                 "UPDATE ".get_table_name('cache').
3884                 " SET created=". $this->db->now().", data=? ".
3885                 "WHERE user_id=? ".
3886                 "AND cache_key=?",
3887                 $data,
3888                 $_SESSION['user_id'],
3889                 'IMAP.'.$key);
3890         }
3891         // add new cache record
3892         else {
3893             $this->db->query(
3894                 "INSERT INTO ".get_table_name('cache').
3895                 " (created, user_id, cache_key, data) ".
3896                 "VALUES (".$this->db->now().", ?, ?, ?)",
3897                 $_SESSION['user_id'],
3898                 'IMAP.'.$key,
3899                 $data);
3900
3901             // get cache entry ID for this key
3902             $sql_result = $this->db->query(
3903                 "SELECT cache_id ".
3904                 "FROM ".get_table_name('cache').
3905                 " WHERE user_id=? ".
3906                 "AND cache_key=?",
3907                 $_SESSION['user_id'],
3908                 'IMAP.'.$key);
3909
3910             if ($sql_arr = $this->db->fetch_assoc($sql_result))
3911                 $this->cache_keys[$key] = $sql_arr['cache_id'];
3912         }
3913     }
29983c 3914
59c216 3915
A 3916     /**
5c461b 3917      * Clears cache for single record
A 3918      *
3919      * @param string $ket Cache key
59c216 3920      * @access private
A 3921      */
3922     private function _clear_cache_record($key)
3923     {
3924         $this->db->query(
3925             "DELETE FROM ".get_table_name('cache').
3926             " WHERE user_id=? ".
3927             "AND cache_key=?",
3928             $_SESSION['user_id'],
3929             'IMAP.'.$key);
c43517 3930
59c216 3931         unset($this->cache_keys[$key]);
A 3932     }
3933
3934
3935
3936     /* --------------------------------
3937      *   message caching methods
3938      * --------------------------------*/
c43517 3939
59c216 3940     /**
A 3941      * Checks if the cache is up-to-date
3942      *
5c461b 3943      * @param string $mailbox   Mailbox name
A 3944      * @param string $cache_key Internal cache key
59c216 3945      * @return int   Cache status: -3 = off, -2 = incomplete, -1 = dirty, 1 = OK
A 3946      */
3947     private function check_cache_status($mailbox, $cache_key)
3948     {
3949         if (!$this->caching_enabled)
3950             return -3;
3951
3952         $cache_index = $this->get_message_cache_index($cache_key);
3953         $msg_count = $this->_messagecount($mailbox);
3954         $cache_count = count($cache_index);
3955
3956         // empty mailbox
9ae29c 3957         if (!$msg_count) {
59c216 3958             return $cache_count ? -2 : 1;
9ae29c 3959         }
59c216 3960
93272e 3961         if ($cache_count == $msg_count) {
59c216 3962             if ($this->skip_deleted) {
9ae29c 3963                 if (!empty($this->icache['all_undeleted_idx'])) {
A 3964                     $uids = rcube_imap_generic::uncompressMessageSet($this->icache['all_undeleted_idx']);
3965                     $uids = array_flip($uids);
3966                     foreach ($cache_index as $uid) {
3967                         unset($uids[$uid]);
3968                     }
3969                 }
3970                 else {
3971                     // get all undeleted messages excluding cached UIDs
3972                     $uids = $this->search_once($mailbox, 'ALL UNDELETED NOT UID '.
3973                         rcube_imap_generic::compressMessageSet($cache_index));
3974                 }
3975                 if (empty($uids)) {
3976                     return 1;
3977                 }
59c216 3978             } else {
eacce9 3979                 // get UID of the message with highest index
A 3980                 $uid = $this->_id2uid($msg_count, $mailbox);
59c216 3981                 $cache_uid = array_pop($cache_index);
c43517 3982
59c216 3983                 // uids of highest message matches -> cache seems OK
9ae29c 3984                 if ($cache_uid == $uid) {
59c216 3985                     return 1;
9ae29c 3986                 }
59c216 3987             }
A 3988             // cache is dirty
3989             return -1;
3990         }
9ae29c 3991
59c216 3992         // if cache count differs less than 10% report as dirty
9ae29c 3993         return (abs($msg_count - $cache_count) < $msg_count/10) ? -1 : -2;
59c216 3994     }
A 3995
29983c 3996
59c216 3997     /**
5c461b 3998      * @param string $key Cache key
A 3999      * @param string $from
4000      * @param string $to
4001      * @param string $sort_field
4002      * @param string $sort_order
59c216 4003      * @access private
A 4004      */
4005     private function get_message_cache($key, $from, $to, $sort_field, $sort_order)
4006     {
eacce9 4007         if (!$this->caching_enabled)
A 4008             return NULL;
c43517 4009
59c216 4010         // use idx sort as default sorting
A 4011         if (!$sort_field || !in_array($sort_field, $this->db_header_fields)) {
4012             $sort_field = 'idx';
4013         }
c43517 4014
eacce9 4015         $result = array();
A 4016
4017         $sql_result = $this->db->limitquery(
59c216 4018                 "SELECT idx, uid, headers".
A 4019                 " FROM ".get_table_name('messages').
4020                 " WHERE user_id=?".
4021                 " AND cache_key=?".
4022                 " ORDER BY ".$this->db->quoteIdentifier($sort_field)." ".strtoupper($sort_order),
4023                 $from,
4024                 $to - $from,
4025                 $_SESSION['user_id'],
4026                 $key);
4027
eacce9 4028         while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
A 4029             $uid = intval($sql_arr['uid']);
4030             $result[$uid] = $this->db->decode(unserialize($sql_arr['headers']));
59c216 4031
eacce9 4032             // featch headers if unserialize failed
A 4033             if (empty($result[$uid]))
4034                 $result[$uid] = $this->conn->fetchHeader(
103ddc 4035                     preg_replace('/.msg$/', '', $key), $uid, true, false, $this->get_fetch_headers());
59c216 4036         }
A 4037
eacce9 4038         return $result;
59c216 4039     }
A 4040
29983c 4041
59c216 4042     /**
5c461b 4043      * @param string $key Cache key
29983c 4044      * @param int    $uid Message UID
5c461b 4045      * @return mixed
59c216 4046      * @access private
A 4047      */
4048     private function &get_cached_message($key, $uid)
4049     {
4050         $internal_key = 'message';
c43517 4051
59c216 4052         if ($this->caching_enabled && !isset($this->icache[$internal_key][$uid])) {
A 4053             $sql_result = $this->db->query(
eacce9 4054                 "SELECT idx, headers, structure, message_id".
59c216 4055                 " FROM ".get_table_name('messages').
A 4056                 " WHERE user_id=?".
4057                 " AND cache_key=?".
4058                 " AND uid=?",
4059                 $_SESSION['user_id'],
4060                 $key,
4061                 $uid);
4062
4063             if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
eacce9 4064                 $this->icache['message.id'][$uid] = intval($sql_arr['message_id']);
A 4065                 $this->uid_id_map[preg_replace('/\.msg$/', '', $key)][$uid] = intval($sql_arr['idx']);
59c216 4066                 $this->icache[$internal_key][$uid] = $this->db->decode(unserialize($sql_arr['headers']));
eacce9 4067
59c216 4068                 if (is_object($this->icache[$internal_key][$uid]) && !empty($sql_arr['structure']))
A 4069                     $this->icache[$internal_key][$uid]->structure = $this->db->decode(unserialize($sql_arr['structure']));
4070             }
4071         }
4072
4073         return $this->icache[$internal_key][$uid];
4074     }
4075
29983c 4076
59c216 4077     /**
29983c 4078      * @param string  $key        Cache key
A 4079      * @param string  $sort_field Sorting column
4080      * @param string  $sort_order Sorting order
4081      * @return array Messages index
59c216 4082      * @access private
c43517 4083      */
eacce9 4084     private function get_message_cache_index($key, $sort_field='idx', $sort_order='ASC')
59c216 4085     {
A 4086         if (!$this->caching_enabled || empty($key))
eacce9 4087             return NULL;
59c216 4088
A 4089         // use idx sort as default
4090         if (!$sort_field || !in_array($sort_field, $this->db_header_fields))
4091             $sort_field = 'idx';
c43517 4092
eacce9 4093         if (array_key_exists('index', $this->icache)
A 4094             && $this->icache['index']['key'] == $key
29983c 4095             && $this->icache['index']['sort_field'] == $sort_field
eacce9 4096         ) {
29983c 4097             if ($this->icache['index']['sort_order'] == $sort_order)
A 4098                 return $this->icache['index']['result'];
4099             else
4100                 return array_reverse($this->icache['index']['result'], true);
eacce9 4101         }
A 4102
4103         $this->icache['index'] = array(
29983c 4104             'result'     => array(),
A 4105             'key'        => $key,
4106             'sort_field' => $sort_field,
4107             'sort_order' => $sort_order,
eacce9 4108         );
A 4109
59c216 4110         $sql_result = $this->db->query(
A 4111             "SELECT idx, uid".
4112             " FROM ".get_table_name('messages').
4113             " WHERE user_id=?".
4114             " AND cache_key=?".
4115             " ORDER BY ".$this->db->quote_identifier($sort_field)." ".$sort_order,
4116             $_SESSION['user_id'],
4117             $key);
4118
4119         while ($sql_arr = $this->db->fetch_assoc($sql_result))
eacce9 4120             $this->icache['index']['result'][$sql_arr['idx']] = intval($sql_arr['uid']);
c43517 4121
eacce9 4122         return $this->icache['index']['result'];
59c216 4123     }
29983c 4124
59c216 4125
A 4126     /**
4127      * @access private
4128      */
eacce9 4129     private function add_message_cache($key, $index, $headers, $struct=null, $force=false, $internal_cache=false)
59c216 4130     {
A 4131         if (empty($key) || !is_object($headers) || empty($headers->uid))
4132             return;
4133
4134         // add to internal (fast) cache
eacce9 4135         if ($internal_cache) {
A 4136             $this->icache['message'][$headers->uid] = clone $headers;
4137             $this->icache['message'][$headers->uid]->structure = $struct;
4138         }
59c216 4139
A 4140         // no further caching
4141         if (!$this->caching_enabled)
4142             return;
c43517 4143
eacce9 4144         // known message id
A 4145         if (is_int($force) && $force > 0) {
4146             $message_id = $force;
4147         }
175d8e 4148         // check for an existing record (probably headers are cached but structure not)
eacce9 4149         else if (!$force) {
59c216 4150             $sql_result = $this->db->query(
A 4151                 "SELECT message_id".
4152                 " FROM ".get_table_name('messages').
4153                 " WHERE user_id=?".
4154                 " AND cache_key=?".
4155                 " AND uid=?",
4156                 $_SESSION['user_id'],
4157                 $key,
4158                 $headers->uid);
4159
4160             if ($sql_arr = $this->db->fetch_assoc($sql_result))
4161                 $message_id = $sql_arr['message_id'];
4162         }
4163
4164         // update cache record
4165         if ($message_id) {
4166             $this->db->query(
4167                 "UPDATE ".get_table_name('messages').
4168                 " SET idx=?, headers=?, structure=?".
4169                 " WHERE message_id=?",
4170                 $index,
4171                 serialize($this->db->encode(clone $headers)),
4172                 is_object($struct) ? serialize($this->db->encode(clone $struct)) : NULL,
4173                 $message_id
4174             );
4175         }
4176         else { // insert new record
4177             $this->db->query(
4178                 "INSERT INTO ".get_table_name('messages').
4179                 " (user_id, del, cache_key, created, idx, uid, subject, ".
4180                 $this->db->quoteIdentifier('from').", ".
4181                 $this->db->quoteIdentifier('to').", ".
4182                 "cc, date, size, headers, structure)".
4183                 " VALUES (?, 0, ?, ".$this->db->now().", ?, ?, ?, ?, ?, ?, ".
4184                 $this->db->fromunixtime($headers->timestamp).", ?, ?, ?)",
4185                 $_SESSION['user_id'],
4186                 $key,
4187                 $index,
4188                 $headers->uid,
4189                 (string)mb_substr($this->db->encode($this->decode_header($headers->subject, true)), 0, 128),
4190                 (string)mb_substr($this->db->encode($this->decode_header($headers->from, true)), 0, 128),
4191                 (string)mb_substr($this->db->encode($this->decode_header($headers->to, true)), 0, 128),
4192                 (string)mb_substr($this->db->encode($this->decode_header($headers->cc, true)), 0, 128),
4193                 (int)$headers->size,
4194                 serialize($this->db->encode(clone $headers)),
4195                 is_object($struct) ? serialize($this->db->encode(clone $struct)) : NULL
4196             );
4197         }
eacce9 4198
A 4199         unset($this->icache['index']);
59c216 4200     }
c43517 4201
29983c 4202
59c216 4203     /**
A 4204      * @access private
4205      */
4206     private function remove_message_cache($key, $ids, $idx=false)
4207     {
4208         if (!$this->caching_enabled)
4209             return;
c43517 4210
59c216 4211         $this->db->query(
A 4212             "DELETE FROM ".get_table_name('messages').
4213             " WHERE user_id=?".
4214             " AND cache_key=?".
4215             " AND ".($idx ? "idx" : "uid")." IN (".$this->db->array2list($ids, 'integer').")",
4216             $_SESSION['user_id'],
4217             $key);
eacce9 4218
A 4219         unset($this->icache['index']);
59c216 4220     }
29983c 4221
59c216 4222
A 4223     /**
5c461b 4224      * @param string $key         Cache key
A 4225      * @param int    $start_index Start index
59c216 4226      * @access private
A 4227      */
4228     private function clear_message_cache($key, $start_index=1)
4229     {
4230         if (!$this->caching_enabled)
4231             return;
c43517 4232
59c216 4233         $this->db->query(
A 4234             "DELETE FROM ".get_table_name('messages').
4235             " WHERE user_id=?".
4236             " AND cache_key=?".
4237             " AND idx>=?",
4238             $_SESSION['user_id'], $key, $start_index);
eacce9 4239
A 4240         unset($this->icache['index']);
59c216 4241     }
29983c 4242
59c216 4243
A 4244     /**
4245      * @access private
4246      */
4247     private function get_message_cache_index_min($key, $uids=NULL)
4248     {
4249         if (!$this->caching_enabled)
4250             return;
c43517 4251
59c216 4252         if (!empty($uids) && !is_array($uids)) {
A 4253             if ($uids == '*' || $uids == '1:*')
4254                 $uids = NULL;
4255             else
4256                 $uids = explode(',', $uids);
4257         }
4258
4259         $sql_result = $this->db->query(
4260             "SELECT MIN(idx) AS minidx".
4261             " FROM ".get_table_name('messages').
4262             " WHERE  user_id=?".
4263             " AND    cache_key=?"
4264             .(!empty($uids) ? " AND uid IN (".$this->db->array2list($uids, 'integer').")" : ''),
4265             $_SESSION['user_id'],
4266             $key);
4267
4268         if ($sql_arr = $this->db->fetch_assoc($sql_result))
4269             return $sql_arr['minidx'];
4270         else
c43517 4271             return 0;
29983c 4272     }
A 4273
4274
4275     /**
4276      * @param string $key Cache key
4277      * @param int    $id  Message (sequence) ID
4278      * @return int Message UID
4279      * @access private
4280      */
4281     private function get_cache_id2uid($key, $id)
4282     {
4283         if (!$this->caching_enabled)
4284             return null;
4285
4286         if (array_key_exists('index', $this->icache)
4287             && $this->icache['index']['key'] == $key
4288         ) {
4289             return $this->icache['index']['result'][$id];
4290         }
4291
4292         $sql_result = $this->db->query(
4293             "SELECT uid".
4294             " FROM ".get_table_name('messages').
4295             " WHERE user_id=?".
4296             " AND cache_key=?".
4297             " AND idx=?",
4298             $_SESSION['user_id'], $key, $id);
4299
4300         if ($sql_arr = $this->db->fetch_assoc($sql_result))
4301             return intval($sql_arr['uid']);
4302
4303         return null;
4304     }
4305
4306
4307     /**
4308      * @param string $key Cache key
4309      * @param int    $uid Message UID
4310      * @return int Message (sequence) ID
4311      * @access private
4312      */
4313     private function get_cache_uid2id($key, $uid)
4314     {
4315         if (!$this->caching_enabled)
4316             return null;
4317
4318         if (array_key_exists('index', $this->icache)
4319             && $this->icache['index']['key'] == $key
4320         ) {
4321             return array_search($uid, $this->icache['index']['result']);
4322         }
4323
4324         $sql_result = $this->db->query(
4325             "SELECT idx".
4326             " FROM ".get_table_name('messages').
4327             " WHERE user_id=?".
4328             " AND cache_key=?".
4329             " AND uid=?",
4330             $_SESSION['user_id'], $key, $uid);
4331
4332         if ($sql_arr = $this->db->fetch_assoc($sql_result))
4333             return intval($sql_arr['idx']);
4334
4335         return null;
59c216 4336     }
A 4337
4338
4339     /* --------------------------------
4340      *   encoding/decoding methods
4341      * --------------------------------*/
4342
4343     /**
4344      * Split an address list into a structured array list
4345      *
5c461b 4346      * @param string  $input  Input string
A 4347      * @param int     $max    List only this number of addresses
4348      * @param boolean $decode Decode address strings
59c216 4349      * @return array  Indexed list of addresses
A 4350      */
4351     function decode_address_list($input, $max=null, $decode=true)
4352     {
4353         $a = $this->_parse_address_list($input, $decode);
4354         $out = array();
4355         // Special chars as defined by RFC 822 need to in quoted string (or escaped).
4356         $special_chars = '[\(\)\<\>\\\.\[\]@,;:"]';
c43517 4357
59c216 4358         if (!is_array($a))
A 4359             return $out;
4360
4361         $c = count($a);
4362         $j = 0;
4363
4364         foreach ($a as $val) {
4365             $j++;
4366             $address = trim($val['address']);
435c31 4367             $name    = trim($val['name']);
59c216 4368
A 4369             if ($name && $address && $name != $address)
4370                 $string = sprintf('%s <%s>', preg_match("/$special_chars/", $name) ? '"'.addcslashes($name, '"').'"' : $name, $address);
4371             else if ($address)
4372                 $string = $address;
4373             else if ($name)
4374                 $string = $name;
c43517 4375
e99991 4376             $out[$j] = array(
A 4377                 'name'   => $name,
59c216 4378                 'mailto' => $address,
A 4379                 'string' => $string
4380             );
4381
4382             if ($max && $j==$max)
4383                 break;
4384         }
c43517 4385
59c216 4386         return $out;
A 4387     }
4388
4389
4390     /**
4391      * Decode a message header value
4392      *
5c461b 4393      * @param string  $input         Header value
A 4394      * @param boolean $remove_quotas Remove quotes if necessary
59c216 4395      * @return string Decoded string
A 4396      */
4397     function decode_header($input, $remove_quotes=false)
4398     {
4399         $str = rcube_imap::decode_mime_string((string)$input, $this->default_charset);
2aa2b3 4400         if ($str[0] == '"' && $remove_quotes)
59c216 4401             $str = str_replace('"', '', $str);
c43517 4402
59c216 4403         return $str;
A 4404     }
4405
4406
4407     /**
4408      * Decode a mime-encoded string to internal charset
4409      *
4410      * @param string $input    Header value
4411      * @param string $fallback Fallback charset if none specified
4412      *
4413      * @return string Decoded string
4414      * @static
4415      */
4416     public static function decode_mime_string($input, $fallback=null)
4417     {
8df56e 4418         if (!empty($fallback)) {
A 4419             $default_charset = $fallback;
4420         }
4421         else {
4422             $default_charset = rcmail::get_instance()->config->get('default_charset', 'ISO-8859-1');
4423         }
59c216 4424
c43517 4425         // rfc: all line breaks or other characters not found
59c216 4426         // in the Base64 Alphabet must be ignored by decoding software
c43517 4427         // delete all blanks between MIME-lines, differently we can
59c216 4428         // receive unnecessary blanks and broken utf-8 symbols
A 4429         $input = preg_replace("/\?=\s+=\?/", '?==?', $input);
4430
8df56e 4431         // encoded-word regexp
A 4432         $re = '/=\?([^?]+)\?([BbQq])\?([^?\n]*)\?=/';
4433
4434         // Find all RFC2047's encoded words
4435         if (preg_match_all($re, $input, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {
4436             // Initialize variables
4437             $tmp   = array();
4438             $out   = '';
4439             $start = 0;
4440
4441             foreach ($matches as $idx => $m) {
4442                 $pos      = $m[0][1];
4443                 $charset  = $m[1][0];
4444                 $encoding = $m[2][0];
4445                 $text     = $m[3][0];
4446                 $length   = strlen($m[0][0]);
4447
59c216 4448                 // Append everything that is before the text to be decoded
8df56e 4449                 if ($start != $pos) {
A 4450                     $substr = substr($input, $start, $pos-$start);
4451                     $out   .= rcube_charset_convert($substr, $default_charset);
4452                     $start  = $pos;
4453                 }
4454                 $start += $length;
59c216 4455
8df56e 4456                 // Per RFC2047, each string part "MUST represent an integral number
A 4457                 // of characters . A multi-octet character may not be split across
4458                 // adjacent encoded-words." However, some mailers break this, so we
4459                 // try to handle characters spanned across parts anyway by iterating
4460                 // through and aggregating sequential encoded parts with the same
4461                 // character set and encoding, then perform the decoding on the
4462                 // aggregation as a whole.
59c216 4463
8df56e 4464                 $tmp[] = $text;
A 4465                 if ($next_match = $matches[$idx+1]) {
4466                     if ($next_match[0][1] == $start
4467                         && $next_match[1][0] == $charset
4468                         && $next_match[2][0] == $encoding
4469                     ) {
4470                         continue;
4471                     }
4472                 }
59c216 4473
8df56e 4474                 $count = count($tmp);
A 4475                 $text  = '';
4476
4477                 // Decode and join encoded-word's chunks
4478                 if ($encoding == 'B' || $encoding == 'b') {
4479                     // base64 must be decoded a segment at a time
4480                     for ($i=0; $i<$count; $i++)
4481                         $text .= base64_decode($tmp[$i]);
4482                 }
4483                 else { //if ($encoding == 'Q' || $encoding == 'q') {
4484                     // quoted printable can be combined and processed at once
4485                     for ($i=0; $i<$count; $i++)
4486                         $text .= $tmp[$i];
4487
4488                     $text = str_replace('_', ' ', $text);
4489                     $text = quoted_printable_decode($text);
4490                 }
4491
4492                 $out .= rcube_charset_convert($text, $charset);
4493                 $tmp = array();
59c216 4494             }
A 4495
8df56e 4496             // add the last part of the input string
A 4497             if ($start != strlen($input)) {
4498                 $out .= rcube_charset_convert(substr($input, $start), $default_charset);
4499             }
59c216 4500
A 4501             // return the results
4502             return $out;
4503         }
4504
4505         // no encoding information, use fallback
8df56e 4506         return rcube_charset_convert($input, $default_charset);
59c216 4507     }
A 4508
4509
4510     /**
4511      * Decode a mime part
4512      *
5c461b 4513      * @param string $input    Input string
A 4514      * @param string $encoding Part encoding
59c216 4515      * @return string Decoded string
A 4516      */
4517     function mime_decode($input, $encoding='7bit')
4518     {
4519         switch (strtolower($encoding)) {
4520         case 'quoted-printable':
4521             return quoted_printable_decode($input);
4522         case 'base64':
4523             return base64_decode($input);
4524         case 'x-uuencode':
4525         case 'x-uue':
4526         case 'uue':
4527         case 'uuencode':
4528             return convert_uudecode($input);
4529         case '7bit':
4530         default:
4531             return $input;
4532         }
4533     }
4534
4535
4536     /**
4537      * Convert body charset to RCMAIL_CHARSET according to the ctype_parameters
4538      *
5c461b 4539      * @param string $body        Part body to decode
A 4540      * @param string $ctype_param Charset to convert from
59c216 4541      * @return string Content converted to internal charset
A 4542      */
4543     function charset_decode($body, $ctype_param)
4544     {
4545         if (is_array($ctype_param) && !empty($ctype_param['charset']))
4546             return rcube_charset_convert($body, $ctype_param['charset']);
4547
4548         // defaults to what is specified in the class header
4549         return rcube_charset_convert($body,  $this->default_charset);
4550     }
4551
4552
4553     /* --------------------------------
4554      *         private methods
4555      * --------------------------------*/
4556
4557     /**
4558      * Validate the given input and save to local properties
5c461b 4559      *
A 4560      * @param string $sort_field Sort column
4561      * @param string $sort_order Sort order
59c216 4562      * @access private
A 4563      */
4564     private function _set_sort_order($sort_field, $sort_order)
4565     {
4566         if ($sort_field != null)
4567             $this->sort_field = asciiwords($sort_field);
4568         if ($sort_order != null)
4569             $this->sort_order = strtoupper($sort_order) == 'DESC' ? 'DESC' : 'ASC';
4570     }
4571
29983c 4572
59c216 4573     /**
A 4574      * Sort mailboxes first by default folders and then in alphabethical order
5c461b 4575      *
A 4576      * @param array $a_folders Mailboxes list
59c216 4577      * @access private
A 4578      */
4579     private function _sort_mailbox_list($a_folders)
4580     {
4581         $a_out = $a_defaults = $folders = array();
4582
4583         $delimiter = $this->get_hierarchy_delimiter();
4584
4585         // find default folders and skip folders starting with '.'
4586         foreach ($a_folders as $i => $folder) {
4587             if ($folder[0] == '.')
4588                 continue;
4589
4590             if (($p = array_search($folder, $this->default_folders)) !== false && !$a_defaults[$p])
4591                 $a_defaults[$p] = $folder;
4592             else
a44682 4593                 $folders[$folder] = rcube_charset_convert($folder, 'UTF7-IMAP');
59c216 4594         }
A 4595
4596         // sort folders and place defaults on the top
4597         asort($folders, SORT_LOCALE_STRING);
4598         ksort($a_defaults);
4599         $folders = array_merge($a_defaults, array_keys($folders));
4600
c43517 4601         // finally we must rebuild the list to move
59c216 4602         // subfolders of default folders to their place...
A 4603         // ...also do this for the rest of folders because
4604         // asort() is not properly sorting case sensitive names
4605         while (list($key, $folder) = each($folders)) {
c43517 4606             // set the type of folder name variable (#1485527)
59c216 4607             $a_out[] = (string) $folder;
A 4608             unset($folders[$key]);
c43517 4609             $this->_rsort($folder, $delimiter, $folders, $a_out);
59c216 4610         }
A 4611
4612         return $a_out;
4613     }
4614
4615
4616     /**
4617      * @access private
4618      */
4619     private function _rsort($folder, $delimiter, &$list, &$out)
4620     {
4621         while (list($key, $name) = each($list)) {
4622             if (strpos($name, $folder.$delimiter) === 0) {
c43517 4623                 // set the type of folder name variable (#1485527)
59c216 4624                 $out[] = (string) $name;
A 4625                 unset($list[$key]);
4626                 $this->_rsort($name, $delimiter, $list, $out);
4627             }
4628         }
c43517 4629         reset($list);
59c216 4630     }
A 4631
4632
4633     /**
d08333 4634      * @param int    $uid     Message UID
A 4635      * @param string $mailbox Mailbox name
29983c 4636      * @return int Message (sequence) ID
59c216 4637      * @access private
A 4638      */
d08333 4639     private function _uid2id($uid, $mailbox=NULL)
59c216 4640     {
d08333 4641         if (!strlen($mailbox)) {
A 4642             $mailbox = $this->mailbox;
29983c 4643         }
59c216 4644
d08333 4645         if (!isset($this->uid_id_map[$mailbox][$uid])) {
A 4646             if (!($id = $this->get_cache_uid2id($mailbox.'.msg', $uid)))
4647                 $id = $this->conn->UID2ID($mailbox, $uid);
4648
4649             $this->uid_id_map[$mailbox][$uid] = $id;
4650         }
4651
4652         return $this->uid_id_map[$mailbox][$uid];
59c216 4653     }
A 4654
29983c 4655
59c216 4656     /**
d08333 4657      * @param int    $id      Message (sequence) ID
A 4658      * @param string $mailbox Mailbox name
4659      *
29983c 4660      * @return int Message UID
59c216 4661      * @access private
A 4662      */
d08333 4663     private function _id2uid($id, $mailbox=null)
59c216 4664     {
d08333 4665         if (!strlen($mailbox)) {
A 4666             $mailbox = $this->mailbox;
4667         }
59c216 4668
d08333 4669         if ($uid = array_search($id, (array)$this->uid_id_map[$mailbox])) {
59c216 4670             return $uid;
d08333 4671         }
59c216 4672
d08333 4673         if (!($uid = $this->get_cache_id2uid($mailbox.'.msg', $id))) {
A 4674             $uid = $this->conn->ID2UID($mailbox, $id);
4675         }
29983c 4676
d08333 4677         $this->uid_id_map[$mailbox][$uid] = $id;
c43517 4678
59c216 4679         return $uid;
A 4680     }
4681
4682
4683     /**
4684      * Subscribe/unsubscribe a list of mailboxes and update local cache
4685      * @access private
4686      */
4687     private function _change_subscription($a_mboxes, $mode)
4688     {
4689         $updated = false;
4690
4691         if (is_array($a_mboxes))
d08333 4692             foreach ($a_mboxes as $i => $mailbox) {
59c216 4693                 $a_mboxes[$i] = $mailbox;
A 4694
d08333 4695                 if ($mode == 'subscribe')
59c216 4696                     $updated = $this->conn->subscribe($mailbox);
d08333 4697                 else if ($mode == 'unsubscribe')
59c216 4698                     $updated = $this->conn->unsubscribe($mailbox);
A 4699             }
4700
4701         // get cached mailbox list
4702         if ($updated) {
4703             $a_mailbox_cache = $this->get_cache('mailboxes');
4704             if (!is_array($a_mailbox_cache))
4705                 return $updated;
4706
4707             // modify cached list
d08333 4708             if ($mode == 'subscribe')
59c216 4709                 $a_mailbox_cache = array_merge($a_mailbox_cache, $a_mboxes);
d08333 4710             else if ($mode == 'unsubscribe')
59c216 4711                 $a_mailbox_cache = array_diff($a_mailbox_cache, $a_mboxes);
A 4712
4713             // write mailboxlist to cache
4714             $this->update_cache('mailboxes', $this->_sort_mailbox_list($a_mailbox_cache));
4715         }
4716
4717         return $updated;
4718     }
4719
4720
4721     /**
4722      * Increde/decrese messagecount for a specific mailbox
4723      * @access private
4724      */
d08333 4725     private function _set_messagecount($mailbox, $mode, $increment)
59c216 4726     {
A 4727         $mode = strtoupper($mode);
4728         $a_mailbox_cache = $this->get_cache('messagecount');
c43517 4729
59c216 4730         if (!is_array($a_mailbox_cache[$mailbox]) || !isset($a_mailbox_cache[$mailbox][$mode]) || !is_numeric($increment))
A 4731             return false;
c43517 4732
59c216 4733         // add incremental value to messagecount
A 4734         $a_mailbox_cache[$mailbox][$mode] += $increment;
c43517 4735
59c216 4736         // there's something wrong, delete from cache
A 4737         if ($a_mailbox_cache[$mailbox][$mode] < 0)
4738             unset($a_mailbox_cache[$mailbox][$mode]);
4739
4740         // write back to cache
4741         $this->update_cache('messagecount', $a_mailbox_cache);
c43517 4742
59c216 4743         return true;
A 4744     }
4745
4746
4747     /**
4748      * Remove messagecount of a specific mailbox from cache
4749      * @access private
4750      */
d08333 4751     private function _clear_messagecount($mailbox, $mode=null)
59c216 4752     {
A 4753         $a_mailbox_cache = $this->get_cache('messagecount');
4754
4755         if (is_array($a_mailbox_cache[$mailbox])) {
c309cd 4756             if ($mode) {
A 4757                 unset($a_mailbox_cache[$mailbox][$mode]);
4758             }
4759             else {
4760                 unset($a_mailbox_cache[$mailbox]);
4761             }
59c216 4762             $this->update_cache('messagecount', $a_mailbox_cache);
A 4763         }
4764     }
4765
4766
4767     /**
4768      * Split RFC822 header string into an associative array
4769      * @access private
4770      */
4771     private function _parse_headers($headers)
4772     {
4773         $a_headers = array();
4774         $headers = preg_replace('/\r?\n(\t| )+/', ' ', $headers);
4775         $lines = explode("\n", $headers);
4776         $c = count($lines);
4777
4778         for ($i=0; $i<$c; $i++) {
4779             if ($p = strpos($lines[$i], ': ')) {
4780                 $field = strtolower(substr($lines[$i], 0, $p));
4781                 $value = trim(substr($lines[$i], $p+1));
4782                 if (!empty($value))
4783                     $a_headers[$field] = $value;
4784             }
4785         }
c43517 4786
59c216 4787         return $a_headers;
A 4788     }
4789
4790
4791     /**
4792      * @access private
4793      */
4794     private function _parse_address_list($str, $decode=true)
4795     {
4796         // remove any newlines and carriage returns before
6c68cb 4797         $str = preg_replace('/\r?\n(\s|\t)?/', ' ', $str);
A 4798
4799         // extract list items, remove comments
4800         $str = self::explode_header_string(',;', $str, true);
59c216 4801         $result = array();
A 4802
6c68cb 4803         foreach ($str as $key => $val) {
435c31 4804             $name    = '';
A 4805             $address = '';
4806             $val     = trim($val);
59c216 4807
435c31 4808             if (preg_match('/(.*)<(\S+@\S+)>$/', $val, $m)) {
A 4809                 $address = $m[2];
4810                 $name    = trim($m[1]);
4811             }
4812             else if (preg_match('/^(\S+@\S+)$/', $val, $m)) {
4813                 $address = $m[1];
4814                 $name    = '';
4815             }
4816             else {
4817                 $name = $val;
59c216 4818             }
c43517 4819
435c31 4820             // dequote and/or decode name
A 4821             if ($name) {
4822                 if ($name[0] == '"') {
4823                     $name = substr($name, 1, -1);
4824                     $name = stripslashes($name);
4825                 }
7bdd3e 4826                 if ($decode) {
435c31 4827                     $name = $this->decode_header($name);
A 4828                 }
4829             }
4830
4831             if (!$address && $name) {
4832                 $address = $name;
4833             }
4834
4835             if ($address) {
4836                 $result[$key] = array('name' => $name, 'address' => $address);
4837             }
59c216 4838         }
c43517 4839
59c216 4840         return $result;
4e17e6 4841     }
6d969b 4842
7f1da4 4843
A 4844     /**
6c68cb 4845      * Explodes header (e.g. address-list) string into array of strings
A 4846      * using specified separator characters with proper handling
4847      * of quoted-strings and comments (RFC2822)
4848      *
4849      * @param string $separator       String containing separator characters
4850      * @param string $str             Header string
4851      * @param bool   $remove_comments Enable to remove comments
4852      *
4853      * @return array Header items
4854      */
4855     static function explode_header_string($separator, $str, $remove_comments=false)
4856     {
4857         $length  = strlen($str);
4858         $result  = array();
4859         $quoted  = false;
4860         $comment = 0;
4861         $out     = '';
4862
4863         for ($i=0; $i<$length; $i++) {
4864             // we're inside a quoted string
4865             if ($quoted) {
4866                 if ($str[$i] == '"') {
4867                     $quoted = false;
4868                 }
4869                 else if ($str[$i] == '\\') {
4870                     if ($comment <= 0) {
4871                         $out .= '\\';
4872                     }
4873                     $i++;
4874                 }
4875             }
4876             // we're inside a comment string
4877             else if ($comment > 0) {
4878                     if ($str[$i] == ')') {
4879                         $comment--;
4880                     }
4881                     else if ($str[$i] == '(') {
4882                         $comment++;
4883                     }
4884                     else if ($str[$i] == '\\') {
4885                         $i++;
4886                     }
4887                     continue;
4888             }
4889             // separator, add to result array
4890             else if (strpos($separator, $str[$i]) !== false) {
4891                     if ($out) {
4892                         $result[] = $out;
4893                     }
4894                     $out = '';
4895                     continue;
4896             }
4897             // start of quoted string
4898             else if ($str[$i] == '"') {
4899                     $quoted = true;
4900             }
4901             // start of comment
4902             else if ($remove_comments && $str[$i] == '(') {
4903                     $comment++;
4904             }
4905
4906             if ($comment <= 0) {
4907                 $out .= $str[$i];
4908             }
4909         }
4910
4911         if ($out && $comment <= 0) {
4912             $result[] = $out;
4913         }
4914
4915         return $result;
4916     }
4917
4918
4919     /**
7f1da4 4920      * This is our own debug handler for the IMAP connection
A 4921      * @access public
4922      */
4923     public function debug_handler(&$imap, $message)
4924     {
4925         write_log('imap', $message);
4926     }
4927
6d969b 4928 }  // end class rcube_imap
4e17e6 4929
8d4bcd 4930
T 4931 /**
4932  * Class representing a message part
6d969b 4933  *
T 4934  * @package Mail
8d4bcd 4935  */
T 4936 class rcube_message_part
4937 {
59c216 4938     var $mime_id = '';
A 4939     var $ctype_primary = 'text';
4940     var $ctype_secondary = 'plain';
4941     var $mimetype = 'text/plain';
4942     var $disposition = '';
4943     var $filename = '';
4944     var $encoding = '8bit';
4945     var $charset = '';
4946     var $size = 0;
4947     var $headers = array();
4948     var $d_parameters = array();
4949     var $ctype_parameters = array();
8d4bcd 4950
59c216 4951     function __clone()
A 4952     {
4953         if (isset($this->parts))
4954             foreach ($this->parts as $idx => $part)
4955                 if (is_object($part))
4956                     $this->parts[$idx] = clone $part;
4957     }
8d4bcd 4958 }
4e17e6 4959
T 4960
7e93ff 4961 /**
59c216 4962  * Class for sorting an array of rcube_mail_header objects in a predetermined order.
7e93ff 4963  *
6d969b 4964  * @package Mail
7e93ff 4965  * @author Eric Stadtherr
T 4966  */
4967 class rcube_header_sorter
4968 {
59c216 4969     var $sequence_numbers = array();
c43517 4970
59c216 4971     /**
A 4972      * Set the predetermined sort order.
4973      *
5c461b 4974      * @param array $seqnums Numerically indexed array of IMAP message sequence numbers
59c216 4975      */
A 4976     function set_sequence_numbers($seqnums)
4977     {
4978         $this->sequence_numbers = array_flip($seqnums);
4979     }
4980
4981     /**
4982      * Sort the array of header objects
4983      *
5c461b 4984      * @param array $headers Array of rcube_mail_header objects indexed by UID
59c216 4985      */
A 4986     function sort_headers(&$headers)
4987     {
4988         /*
4989         * uksort would work if the keys were the sequence number, but unfortunately
4990         * the keys are the UIDs.  We'll use uasort instead and dereference the value
4991         * to get the sequence number (in the "id" field).
c43517 4992         *
A 4993         * uksort($headers, array($this, "compare_seqnums"));
59c216 4994         */
A 4995         uasort($headers, array($this, "compare_seqnums"));
4996     }
c43517 4997
59c216 4998     /**
A 4999      * Sort method called by uasort()
5c461b 5000      *
A 5001      * @param rcube_mail_header $a
5002      * @param rcube_mail_header $b
59c216 5003      */
A 5004     function compare_seqnums($a, $b)
5005     {
5006         // First get the sequence number from the header object (the 'id' field).
5007         $seqa = $a->id;
5008         $seqb = $b->id;
c43517 5009
59c216 5010         // then find each sequence number in my ordered list
A 5011         $posa = isset($this->sequence_numbers[$seqa]) ? intval($this->sequence_numbers[$seqa]) : -1;
5012         $posb = isset($this->sequence_numbers[$seqb]) ? intval($this->sequence_numbers[$seqb]) : -1;
c43517 5013
59c216 5014         // return the relative position as the comparison value
A 5015         return $posa - $posb;
5016     }
7e93ff 5017 }