alecpl
2011-05-06 e22740508c02fc531096027c3a97a1cb1ca4209f
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     /**
A 2524      * Returns the whole message source as string
2525      *
5c461b 2526      * @param int $uid Message UID
59c216 2527      * @return string Message source string
A 2528      */
2529     function &get_raw_body($uid)
4e17e6 2530     {
59c216 2531         return $this->conn->handlePartBody($this->mailbox, $uid, true);
4e17e6 2532     }
e5686f 2533
A 2534
59c216 2535     /**
A 2536      * Returns the message headers as string
2537      *
5c461b 2538      * @param int $uid  Message UID
59c216 2539      * @return string Message headers string
A 2540      */
2541     function &get_raw_headers($uid)
e5686f 2542     {
59c216 2543         return $this->conn->fetchPartHeader($this->mailbox, $uid, true);
e5686f 2544     }
c43517 2545
8d4bcd 2546
59c216 2547     /**
A 2548      * Sends the whole message source to stdout
2549      *
5c461b 2550      * @param int $uid Message UID
c43517 2551      */
59c216 2552     function print_raw_body($uid)
8d4bcd 2553     {
59c216 2554         $this->conn->handlePartBody($this->mailbox, $uid, true, NULL, NULL, true);
8d4bcd 2555     }
4e17e6 2556
T 2557
59c216 2558     /**
A 2559      * Set message flag to one or several messages
2560      *
5c461b 2561      * @param mixed   $uids       Message UIDs as array or comma-separated string, or '*'
A 2562      * @param string  $flag       Flag to set: SEEN, UNDELETED, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
d08333 2563      * @param string  $mailbox    Folder name
5c461b 2564      * @param boolean $skip_cache True to skip message cache clean up
d08333 2565      *
c309cd 2566      * @return boolean  Operation status
59c216 2567      */
d08333 2568     function set_flag($uids, $flag, $mailbox=null, $skip_cache=false)
4e17e6 2569     {
d08333 2570         if (!strlen($mailbox)) {
A 2571             $mailbox = $this->mailbox;
2572         }
48958e 2573
59c216 2574         $flag = strtoupper($flag);
A 2575         list($uids, $all_mode) = $this->_parse_uids($uids, $mailbox);
cff886 2576
59c216 2577         if (strpos($flag, 'UN') === 0)
A 2578             $result = $this->conn->unflag($mailbox, $uids, substr($flag, 2));
cff886 2579         else
59c216 2580             $result = $this->conn->flag($mailbox, $uids, $flag);
fa4cd2 2581
c309cd 2582         if ($result) {
59c216 2583             // reload message headers if cached
A 2584             if ($this->caching_enabled && !$skip_cache) {
2585                 $cache_key = $mailbox.'.msg';
2586                 if ($all_mode)
2587                     $this->clear_message_cache($cache_key);
2588                 else
2589                     $this->remove_message_cache($cache_key, explode(',', $uids));
15e00b 2590             }
c309cd 2591
A 2592             // clear cached counters
2593             if ($flag == 'SEEN' || $flag == 'UNSEEN') {
2594                 $this->_clear_messagecount($mailbox, 'SEEN');
2595                 $this->_clear_messagecount($mailbox, 'UNSEEN');
2596             }
2597             else if ($flag == 'DELETED') {
2598                 $this->_clear_messagecount($mailbox, 'DELETED');
2599             }
4e17e6 2600         }
T 2601
59c216 2602         return $result;
4e17e6 2603     }
T 2604
fa4cd2 2605
59c216 2606     /**
A 2607      * Remove message flag for one or several messages
2608      *
d08333 2609      * @param mixed  $uids    Message UIDs as array or comma-separated string, or '*'
A 2610      * @param string $flag    Flag to unset: SEEN, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
2611      * @param string $mailbox Folder name
2612      *
59c216 2613      * @return int   Number of flagged messages, -1 on failure
A 2614      * @see set_flag
2615      */
d08333 2616     function unset_flag($uids, $flag, $mailbox=null)
fa4cd2 2617     {
d08333 2618         return $this->set_flag($uids, 'UN'.$flag, $mailbox);
15e00b 2619     }
A 2620
2621
59c216 2622     /**
A 2623      * Append a mail message (source) to a specific mailbox
2624      *
d08333 2625      * @param string  $mailbox Target mailbox
A 2626      * @param string  $message The message source string or filename
2627      * @param string  $headers Headers string if $message contains only the body
2628      * @param boolean $is_file True if $message is a filename
59c216 2629      *
A 2630      * @return boolean True on success, False on error
2631      */
d08333 2632     function save_message($mailbox, &$message, $headers='', $is_file=false)
15e00b 2633     {
d08333 2634         if (!strlen($mailbox)) {
A 2635             $mailbox = $this->mailbox;
2636         }
4e17e6 2637
59c216 2638         // make sure mailbox exists
d08333 2639         if ($this->mailbox_exists($mailbox)) {
272a7e 2640             if ($is_file)
A 2641                 $saved = $this->conn->appendFromFile($mailbox, $message, $headers);
59c216 2642             else
A 2643                 $saved = $this->conn->append($mailbox, $message);
4e17e6 2644         }
f8a846 2645
59c216 2646         if ($saved) {
A 2647             // increase messagecount of the target mailbox
2648             $this->_set_messagecount($mailbox, 'ALL', 1);
8d4bcd 2649         }
59c216 2650
A 2651         return $saved;
8d4bcd 2652     }
T 2653
2654
59c216 2655     /**
A 2656      * Move a message from one mailbox to another
2657      *
5c461b 2658      * @param mixed  $uids      Message UIDs as array or comma-separated string, or '*'
A 2659      * @param string $to_mbox   Target mailbox
2660      * @param string $from_mbox Source mailbox
59c216 2661      * @return boolean True on success, False on error
A 2662      */
2663     function move_message($uids, $to_mbox, $from_mbox='')
4e17e6 2664     {
d08333 2665         if (!strlen($from_mbox)) {
A 2666             $from_mbox = $this->mailbox;
2667         }
c58c0a 2668
d08333 2669         if ($to_mbox === $from_mbox) {
af3c04 2670             return false;
d08333 2671         }
af3c04 2672
59c216 2673         list($uids, $all_mode) = $this->_parse_uids($uids, $from_mbox);
41fa0b 2674
59c216 2675         // exit if no message uids are specified
A 2676         if (empty($uids))
2677             return false;
2678
2679         // make sure mailbox exists
d08333 2680         if ($to_mbox != 'INBOX' && !$this->mailbox_exists($to_mbox)) {
A 2681             if (in_array($to_mbox, $this->default_folders))
2682                 $this->create_mailbox($to_mbox, true);
59c216 2683             else
A 2684                 return false;
2685         }
2686
2687         $config = rcmail::get_instance()->config;
d08333 2688         $to_trash = $to_mbox == $config->get('trash_mbox');
A 2689
2690         // flag messages as read before moving them
2691         if ($to_trash && $config->get('read_when_deleted')) {
59c216 2692             // don't flush cache (4th argument)
d08333 2693             $this->set_flag($uids, 'SEEN', $from_mbox, true);
59c216 2694         }
A 2695
2696         // move messages
93272e 2697         $moved = $this->conn->move($uids, $from_mbox, $to_mbox);
59c216 2698
A 2699         // send expunge command in order to have the moved message
2700         // really deleted from the source mailbox
2701         if ($moved) {
2702             $this->_expunge($from_mbox, false, $uids);
2703             $this->_clear_messagecount($from_mbox);
2704             $this->_clear_messagecount($to_mbox);
2705         }
2706         // moving failed
d08333 2707         else if ($to_trash && $config->get('delete_always', false)) {
A 2708             $moved = $this->delete_message($uids, $from_mbox);
59c216 2709         }
9e81b5 2710
59c216 2711         if ($moved) {
A 2712             // unset threads internal cache
2713             unset($this->icache['threads']);
2714
2715             // remove message ids from search set
2716             if ($this->search_set && $from_mbox == $this->mailbox) {
2717                 // threads are too complicated to just remove messages from set
2718                 if ($this->search_threads || $all_mode)
2719                     $this->refresh_search();
2720                 else {
2721                     $uids = explode(',', $uids);
2722                     foreach ($uids as $uid)
2723                         $a_mids[] = $this->_uid2id($uid, $from_mbox);
2724                     $this->search_set = array_diff($this->search_set, $a_mids);
2725                 }
2726             }
2727
2728             // update cached message headers
2729             $cache_key = $from_mbox.'.msg';
2730             if ($all_mode || ($start_index = $this->get_message_cache_index_min($cache_key, $uids))) {
2731                 // clear cache from the lowest index on
2732                 $this->clear_message_cache($cache_key, $all_mode ? 1 : $start_index);
2733             }
2734         }
2735
2736         return $moved;
2737     }
2738
2739
2740     /**
2741      * Copy a message from one mailbox to another
2742      *
5c461b 2743      * @param mixed  $uids      Message UIDs as array or comma-separated string, or '*'
A 2744      * @param string $to_mbox   Target mailbox
2745      * @param string $from_mbox Source mailbox
59c216 2746      * @return boolean True on success, False on error
A 2747      */
2748     function copy_message($uids, $to_mbox, $from_mbox='')
2749     {
d08333 2750         if (!strlen($from_mbox)) {
A 2751             $from_mbox = $this->mailbox;
2752         }
59c216 2753
A 2754         list($uids, $all_mode) = $this->_parse_uids($uids, $from_mbox);
2755
2756         // exit if no message uids are specified
93272e 2757         if (empty($uids)) {
59c216 2758             return false;
93272e 2759         }
59c216 2760
A 2761         // make sure mailbox exists
d08333 2762         if ($to_mbox != 'INBOX' && !$this->mailbox_exists($to_mbox)) {
A 2763             if (in_array($to_mbox, $this->default_folders))
2764                 $this->create_mailbox($to_mbox, true);
59c216 2765             else
A 2766                 return false;
2767         }
2768
2769         // copy messages
93272e 2770         $copied = $this->conn->copy($uids, $from_mbox, $to_mbox);
59c216 2771
A 2772         if ($copied) {
2773             $this->_clear_messagecount($to_mbox);
2774         }
2775
2776         return $copied;
2777     }
2778
2779
2780     /**
2781      * Mark messages as deleted and expunge mailbox
2782      *
d08333 2783      * @param mixed  $uids    Message UIDs as array or comma-separated string, or '*'
A 2784      * @param string $mailbox Source mailbox
2785      *
59c216 2786      * @return boolean True on success, False on error
A 2787      */
d08333 2788     function delete_message($uids, $mailbox='')
59c216 2789     {
d08333 2790         if (!strlen($mailbox)) {
A 2791             $mailbox = $this->mailbox;
2792         }
59c216 2793
A 2794         list($uids, $all_mode) = $this->_parse_uids($uids, $mailbox);
2795
2796         // exit if no message uids are specified
2797         if (empty($uids))
2798             return false;
2799
2800         $deleted = $this->conn->delete($mailbox, $uids);
2801
2802         if ($deleted) {
2803             // send expunge command in order to have the deleted message
2804             // really deleted from the mailbox
2805             $this->_expunge($mailbox, false, $uids);
2806             $this->_clear_messagecount($mailbox);
2807             unset($this->uid_id_map[$mailbox]);
2808
2809             // unset threads internal cache
2810             unset($this->icache['threads']);
c43517 2811
59c216 2812             // remove message ids from search set
A 2813             if ($this->search_set && $mailbox == $this->mailbox) {
2814                 // threads are too complicated to just remove messages from set
2815                 if ($this->search_threads || $all_mode)
2816                     $this->refresh_search();
2817                 else {
2818                     $uids = explode(',', $uids);
2819                     foreach ($uids as $uid)
2820                         $a_mids[] = $this->_uid2id($uid, $mailbox);
2821                     $this->search_set = array_diff($this->search_set, $a_mids);
2822                 }
2823             }
2824
2825             // remove deleted messages from cache
2826             $cache_key = $mailbox.'.msg';
2827             if ($all_mode || ($start_index = $this->get_message_cache_index_min($cache_key, $uids))) {
2828                 // clear cache from the lowest index on
2829                 $this->clear_message_cache($cache_key, $all_mode ? 1 : $start_index);
2830             }
2831         }
2832
2833         return $deleted;
2834     }
2835
2836
2837     /**
2838      * Clear all messages in a specific mailbox
2839      *
d08333 2840      * @param string $mailbox Mailbox name
A 2841      *
59c216 2842      * @return int Above 0 on success
A 2843      */
d08333 2844     function clear_mailbox($mailbox=null)
59c216 2845     {
d08333 2846         if (!strlen($mailbox)) {
A 2847             $mailbox = $this->mailbox;
2848         }
c43517 2849
01bdfd 2850         // SELECT will set messages count for clearFolder()
A 2851         if ($this->conn->select($mailbox)) {
2852             $cleared = $this->conn->clearFolder($mailbox);
4e17e6 2853         }
c43517 2854
59c216 2855         // make sure the message count cache is cleared as well
A 2856         if ($cleared) {
c43517 2857             $this->clear_message_cache($mailbox.'.msg');
59c216 2858             $a_mailbox_cache = $this->get_cache('messagecount');
A 2859             unset($a_mailbox_cache[$mailbox]);
2860             $this->update_cache('messagecount', $a_mailbox_cache);
2861         }
c43517 2862
59c216 2863         return $cleared;
A 2864     }
2865
2866
2867     /**
2868      * Send IMAP expunge command and clear cache
2869      *
d08333 2870      * @param string  $mailbox     Mailbox name
5c461b 2871      * @param boolean $clear_cache False if cache should not be cleared
d08333 2872      *
59c216 2873      * @return boolean True on success
A 2874      */
d08333 2875     function expunge($mailbox='', $clear_cache=true)
59c216 2876     {
d08333 2877         if (!strlen($mailbox)) {
A 2878             $mailbox = $this->mailbox;
2879         }
2880
59c216 2881         return $this->_expunge($mailbox, $clear_cache);
A 2882     }
2883
2884
2885     /**
2886      * Send IMAP expunge command and clear cache
2887      *
5c461b 2888      * @param string  $mailbox     Mailbox name
A 2889      * @param boolean $clear_cache False if cache should not be cleared
2890      * @param mixed   $uids        Message UIDs as array or comma-separated string, or '*'
59c216 2891      * @return boolean True on success
A 2892      * @access private
2893      * @see rcube_imap::expunge()
2894      */
2895     private function _expunge($mailbox, $clear_cache=true, $uids=NULL)
2896     {
c43517 2897         if ($uids && $this->get_capability('UIDPLUS'))
59c216 2898             $a_uids = is_array($uids) ? join(',', $uids) : $uids;
A 2899         else
2900             $a_uids = NULL;
2901
90f81a 2902         // force mailbox selection and check if mailbox is writeable
A 2903         // to prevent a situation when CLOSE is executed on closed
2904         // or EXPUNGE on read-only mailbox
2905         $result = $this->conn->select($mailbox);
2906         if (!$result) {
2907             return false;
2908         }
2909         if (!$this->conn->data['READ-WRITE']) {
2910             $this->conn->setError(rcube_imap_generic::ERROR_READONLY, "Mailbox is read-only");
2911             return false;
2912         }
2913
e232ac 2914         // CLOSE(+SELECT) should be faster than EXPUNGE
A 2915         if (empty($a_uids) || $a_uids == '1:*')
2916             $result = $this->conn->close();
2917         else
2918             $result = $this->conn->expunge($mailbox, $a_uids);
59c216 2919
854cf2 2920         if ($result && $clear_cache) {
59c216 2921             $this->clear_message_cache($mailbox.'.msg');
A 2922             $this->_clear_messagecount($mailbox);
2923         }
c43517 2924
59c216 2925         return $result;
A 2926     }
2927
2928
2929     /**
2930      * Parse message UIDs input
2931      *
5c461b 2932      * @param mixed  $uids    UIDs array or comma-separated list or '*' or '1:*'
A 2933      * @param string $mailbox Mailbox name
c43517 2934      * @return array Two elements array with UIDs converted to list and ALL flag
59c216 2935      * @access private
A 2936      */
2937     private function _parse_uids($uids, $mailbox)
2938     {
2939         if ($uids === '*' || $uids === '1:*') {
2940             if (empty($this->search_set)) {
2941                 $uids = '1:*';
2942                 $all = true;
2943             }
2944             // get UIDs from current search set
2945             // @TODO: skip fetchUIDs() and work with IDs instead of UIDs (?)
2946             else {
2947                 if ($this->search_threads)
2948                     $uids = $this->conn->fetchUIDs($mailbox, array_keys($this->search_set['depth']));
2949                 else
2950                     $uids = $this->conn->fetchUIDs($mailbox, $this->search_set);
c43517 2951
59c216 2952                 // save ID-to-UID mapping in local cache
A 2953                 if (is_array($uids))
2954                     foreach ($uids as $id => $uid)
2955                         $this->uid_id_map[$mailbox][$uid] = $id;
2956
2957                 $uids = join(',', $uids);
2958             }
2959         }
2960         else {
2961             if (is_array($uids))
2962                 $uids = join(',', $uids);
2963
2964             if (preg_match('/[^0-9,]/', $uids))
2965                 $uids = '';
2966         }
2967
2968         return array($uids, (bool) $all);
2969     }
2970
2971
2972     /**
2973      * Translate UID to message ID
2974      *
d08333 2975      * @param int    $uid     Message UID
A 2976      * @param string $mailbox Mailbox name
2977      *
59c216 2978      * @return int   Message ID
A 2979      */
d08333 2980     function get_id($uid, $mailbox=null)
59c216 2981     {
d08333 2982         if (!strlen($mailbox)) {
A 2983             $mailbox = $this->mailbox;
2984         }
2985
59c216 2986         return $this->_uid2id($uid, $mailbox);
A 2987     }
2988
2989
2990     /**
2991      * Translate message number to UID
2992      *
d08333 2993      * @param int    $id      Message ID
A 2994      * @param string $mailbox Mailbox name
2995      *
59c216 2996      * @return int   Message UID
A 2997      */
d08333 2998     function get_uid($id, $mailbox=null)
59c216 2999     {
d08333 3000         if (!strlen($mailbox)) {
A 3001             $mailbox = $this->mailbox;
3002         }
3003
59c216 3004         return $this->_id2uid($id, $mailbox);
A 3005     }
3006
3007
3008
3009     /* --------------------------------
3010      *        folder managment
3011      * --------------------------------*/
3012
3013     /**
6f4e7d 3014      * Public method for listing subscribed folders
59c216 3015      *
5c461b 3016      * @param   string  $root   Optional root folder
94bdcc 3017      * @param   string  $name   Optional name pattern
A 3018      * @param   string  $filter Optional filter
d08333 3019      *
59c216 3020      * @return  array   List of mailboxes/folders
A 3021      * @access  public
3022      */
94bdcc 3023     function list_mailboxes($root='', $name='*', $filter=null)
59c216 3024     {
94bdcc 3025         $a_mboxes = $this->_list_mailboxes($root, $name, $filter);
59c216 3026
A 3027         // INBOX should always be available
94bdcc 3028         if ((!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)) {
d08333 3029             array_unshift($a_mboxes, 'INBOX');
94bdcc 3030         }
59c216 3031
A 3032         // sort mailboxes
d08333 3033         $a_mboxes = $this->_sort_mailbox_list($a_mboxes);
59c216 3034
d08333 3035         return $a_mboxes;
59c216 3036     }
A 3037
3038
3039     /**
3040      * Private method for mailbox listing
3041      *
5c461b 3042      * @param   string  $root   Optional root folder
94bdcc 3043      * @param   string  $name   Optional name pattern
A 3044      * @param   mixed   $filter Optional filter
3045      *
59c216 3046      * @return  array   List of mailboxes/folders
A 3047      * @see     rcube_imap::list_mailboxes()
3048      * @access  private
3049      */
94bdcc 3050     private function _list_mailboxes($root='', $name='*', $filter=null)
59c216 3051     {
94bdcc 3052         $cache_key = 'mailboxes';
A 3053         if (!empty($filter)) {
091b02 3054             $cache_key .= '.'.substr((is_string($filter) ? $filter : serialize($filter)), 0, 90);
94bdcc 3055         }
A 3056
c43517 3057         // get cached folder list
94bdcc 3058         $a_mboxes = $this->get_cache($cache_key);
A 3059         if (is_array($a_mboxes)) {
59c216 3060             return $a_mboxes;
94bdcc 3061         }
59c216 3062
6f4e7d 3063         $a_defaults = $a_out = array();
A 3064
59c216 3065         // Give plugins a chance to provide a list of mailboxes
e6ce00 3066         $data = rcmail::get_instance()->plugins->exec_hook('mailboxes_list',
94bdcc 3067             array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LSUB'));
c43517 3068
59c216 3069         if (isset($data['folders'])) {
A 3070             $a_folders = $data['folders'];
3071         }
3072         else {
3870be 3073             // Server supports LIST-EXTENDED, we can use selection options
f75f65 3074             $config = rcmail::get_instance()->config;
A 3075             // #1486225: Some dovecot versions returns wrong result using LIST-EXTENDED
3076             if (!$config->get('imap_force_lsub') && $this->get_capability('LIST-EXTENDED')) {
3870be 3077                 // This will also set mailbox options, LSUB doesn't do that
94bdcc 3078                 $a_folders = $this->conn->listMailboxes($root, $name,
3870be 3079                     NULL, array('SUBSCRIBED'));
A 3080
3081                 // remove non-existent folders
3082                 if (is_array($a_folders)) {
3083                     foreach ($a_folders as $idx => $folder) {
3084                         if ($this->conn->data['LIST'] && ($opts = $this->conn->data['LIST'][$folder])
3085                             && in_array('\\NonExistent', $opts)
3086                         ) {
3087                             unset($a_folders[$idx]);
3088                         } 
3089                     }
3090                 }
3091             }
3092             // retrieve list of folders from IMAP server using LSUB
3093             else {
94bdcc 3094                 $a_folders = $this->conn->listSubscribed($root, $name);
3870be 3095             }
59c216 3096         }
c43517 3097
94bdcc 3098         if (!is_array($a_folders) || !sizeof($a_folders)) {
59c216 3099             $a_folders = array();
94bdcc 3100         }
59c216 3101
A 3102         // write mailboxlist to cache
94bdcc 3103         $this->update_cache($cache_key, $a_folders);
c43517 3104
59c216 3105         return $a_folders;
A 3106     }
3107
3108
3109     /**
3110      * Get a list of all folders available on the IMAP server
c43517 3111      *
5c461b 3112      * @param string $root   IMAP root dir
94bdcc 3113      * @param string  $name   Optional name pattern
A 3114      * @param mixed   $filter Optional filter
3115      *
59c216 3116      * @return array Indexed array with folder names
A 3117      */
94bdcc 3118     function list_unsubscribed($root='', $name='*', $filter=null)
59c216 3119     {
94bdcc 3120         // @TODO: caching
6f4e7d 3121         // Give plugins a chance to provide a list of mailboxes
e6ce00 3122         $data = rcmail::get_instance()->plugins->exec_hook('mailboxes_list',
94bdcc 3123             array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LIST'));
c43517 3124
6f4e7d 3125         if (isset($data['folders'])) {
A 3126             $a_mboxes = $data['folders'];
3127         }
3128         else {
3129             // retrieve list of folders from IMAP server
94bdcc 3130             $a_mboxes = $this->conn->listMailboxes($root, $name);
6f4e7d 3131         }
64e3e8 3132
d08333 3133         if (!is_array($a_mboxes)) {
6f4e7d 3134             $a_mboxes = array();
59c216 3135         }
f0485a 3136
A 3137         // INBOX should always be available
94bdcc 3138         if ((!$filter || $filter == 'mail') && !in_array('INBOX', $a_mboxes)) {
d08333 3139             array_unshift($a_mboxes, 'INBOX');
94bdcc 3140         }
59c216 3141
A 3142         // filter folders and sort them
d08333 3143         $a_mboxes = $this->_sort_mailbox_list($a_mboxes);
A 3144
3145         return $a_mboxes;
59c216 3146     }
A 3147
3148
3149     /**
3150      * Get mailbox quota information
3151      * added by Nuny
c43517 3152      *
59c216 3153      * @return mixed Quota info or False if not supported
A 3154      */
3155     function get_quota()
3156     {
3157         if ($this->get_capability('QUOTA'))
3158             return $this->conn->getQuota();
c43517 3159
59c216 3160         return false;
A 3161     }
3162
3163
3164     /**
af3c04 3165      * Get mailbox size (size of all messages in a mailbox)
A 3166      *
d08333 3167      * @param string $mailbox Mailbox name
A 3168      *
af3c04 3169      * @return int Mailbox size in bytes, False on error
A 3170      */
d08333 3171     function get_mailbox_size($mailbox)
af3c04 3172     {
A 3173         // @TODO: could we try to use QUOTA here?
d08333 3174         $result = $this->conn->fetchHeaderIndex($mailbox, '1:*', 'SIZE', false);
af3c04 3175
A 3176         if (is_array($result))
3177             $result = array_sum($result);
3178
3179         return $result;
3180     }
3181
3182
3183     /**
59c216 3184      * Subscribe to a specific mailbox(es)
A 3185      *
5c461b 3186      * @param array $a_mboxes Mailbox name(s)
59c216 3187      * @return boolean True on success
c43517 3188      */
59c216 3189     function subscribe($a_mboxes)
A 3190     {
3191         if (!is_array($a_mboxes))
3192             $a_mboxes = array($a_mboxes);
3193
3194         // let this common function do the main work
3195         return $this->_change_subscription($a_mboxes, 'subscribe');
3196     }
3197
3198
3199     /**
3200      * Unsubscribe mailboxes
3201      *
5c461b 3202      * @param array $a_mboxes Mailbox name(s)
59c216 3203      * @return boolean True on success
A 3204      */
3205     function unsubscribe($a_mboxes)
3206     {
3207         if (!is_array($a_mboxes))
3208             $a_mboxes = array($a_mboxes);
3209
3210         // let this common function do the main work
3211         return $this->_change_subscription($a_mboxes, 'unsubscribe');
3212     }
3213
3214
3215     /**
3216      * Create a new mailbox on the server and register it in local cache
3217      *
d08333 3218      * @param string  $mailbox   New mailbox name
5c461b 3219      * @param boolean $subscribe True if the new mailbox should be subscribed
d08333 3220      *
A 3221      * @return boolean True on success
59c216 3222      */
d08333 3223     function create_mailbox($mailbox, $subscribe=false)
59c216 3224     {
d08333 3225         $result = $this->conn->createFolder($mailbox);
59c216 3226
A 3227         // try to subscribe it
3228         if ($result && $subscribe)
d08333 3229             $this->subscribe($mailbox);
59c216 3230
af3c04 3231         return $result;
59c216 3232     }
A 3233
3234
3235     /**
3236      * Set a new name to an existing mailbox
3237      *
d08333 3238      * @param string $mailbox  Mailbox to rename
A 3239      * @param string $new_name New mailbox name
0e1194 3240      *
af3c04 3241      * @return boolean True on success
59c216 3242      */
d08333 3243     function rename_mailbox($mailbox, $new_name)
59c216 3244     {
d08333 3245         if (!strlen($new_name)) {
A 3246             return false;
3247         }
59c216 3248
d08333 3249         $delm = $this->get_hierarchy_delimiter();
c43517 3250
0e1194 3251         // get list of subscribed folders
A 3252         if ((strpos($mailbox, '%') === false) && (strpos($mailbox, '*') === false)) {
d08333 3253             $a_subscribed = $this->_list_mailboxes('', $mailbox . $delm . '*');
A 3254             $subscribed   = $this->mailbox_exists($mailbox, true);
0e1194 3255         }
A 3256         else {
3257             $a_subscribed = $this->_list_mailboxes();
3258             $subscribed   = in_array($mailbox, $a_subscribed);
3259         }
59c216 3260
d08333 3261         $result = $this->conn->renameFolder($mailbox, $new_name);
59c216 3262
A 3263         if ($result) {
0e1194 3264             // unsubscribe the old folder, subscribe the new one
A 3265             if ($subscribed) {
3266                 $this->conn->unsubscribe($mailbox);
d08333 3267                 $this->conn->subscribe($new_name);
0e1194 3268             }
677e1f 3269
59c216 3270             // check if mailbox children are subscribed
0e1194 3271             foreach ($a_subscribed as $c_subscribed) {
59c216 3272                 if (preg_match('/^'.preg_quote($mailbox.$delm, '/').'/', $c_subscribed)) {
A 3273                     $this->conn->unsubscribe($c_subscribed);
3274                     $this->conn->subscribe(preg_replace('/^'.preg_quote($mailbox, '/').'/',
d08333 3275                         $new_name, $c_subscribed));
59c216 3276                 }
0e1194 3277             }
59c216 3278
A 3279             // clear cache
3280             $this->clear_message_cache($mailbox.'.msg');
94bdcc 3281             $this->clear_cache('/^mailboxes.*/', true);
59c216 3282         }
A 3283
af3c04 3284         return $result;
59c216 3285     }
A 3286
3287
3288     /**
0e1194 3289      * Remove mailbox from server
59c216 3290      *
d08333 3291      * @param string $mailbox Mailbox name
0e1194 3292      *
59c216 3293      * @return boolean True on success
A 3294      */
d08333 3295     function delete_mailbox($mailbox)
59c216 3296     {
d08333 3297         $delm = $this->get_hierarchy_delimiter();
59c216 3298
0e1194 3299         // get list of folders
A 3300         if ((strpos($mailbox, '%') === false) && (strpos($mailbox, '*') === false))
3301             $sub_mboxes = $this->list_unsubscribed('', $mailbox . $delm . '*');
3302         else
3303             $sub_mboxes = $this->list_unsubscribed();
59c216 3304
0e1194 3305         // send delete command to server
A 3306         $result = $this->conn->deleteFolder($mailbox);
59c216 3307
0e1194 3308         if ($result) {
A 3309             // unsubscribe mailbox
3310             $this->conn->unsubscribe($mailbox);
59c216 3311
0e1194 3312             foreach ($sub_mboxes as $c_mbox) {
A 3313                 if (preg_match('/^'.preg_quote($mailbox.$delm, '/').'/', $c_mbox)) {
3314                     $this->conn->unsubscribe($c_mbox);
3315                     if ($this->conn->deleteFolder($c_mbox)) {
3316                         $this->clear_message_cache($c_mbox.'.msg');
59c216 3317                     }
A 3318                 }
3319             }
0e1194 3320
A 3321             // clear mailbox-related cache
3322             $this->clear_message_cache($mailbox.'.msg');
94bdcc 3323             $this->clear_cache('/^mailboxes.*/', true);
59c216 3324         }
A 3325
0e1194 3326         return $result;
59c216 3327     }
A 3328
3329
3330     /**
3331      * Create all folders specified as default
3332      */
3333     function create_default_folders()
3334     {
3335         // create default folders if they do not exist
3336         foreach ($this->default_folders as $folder) {
3337             if (!$this->mailbox_exists($folder))
3338                 $this->create_mailbox($folder, true);
3339             else if (!$this->mailbox_exists($folder, true))
3340                 $this->subscribe($folder);
3341         }
3342     }
3343
3344
3345     /**
3346      * Checks if folder exists and is subscribed
3347      *
d08333 3348      * @param string   $mailbox      Folder name
5c461b 3349      * @param boolean  $subscription Enable subscription checking
d08333 3350      *
59c216 3351      * @return boolean TRUE or FALSE
A 3352      */
d08333 3353     function mailbox_exists($mailbox, $subscription=false)
59c216 3354     {
d08333 3355         if ($mailbox == 'INBOX') {
448409 3356             return true;
d08333 3357         }
59c216 3358
448409 3359         $key  = $subscription ? 'subscribed' : 'existing';
ad5881 3360
d08333 3361         if (is_array($this->icache[$key]) && in_array($mailbox, $this->icache[$key]))
448409 3362             return true;
16378f 3363
448409 3364         if ($subscription) {
d08333 3365             $a_folders = $this->conn->listSubscribed('', $mailbox);
448409 3366         }
A 3367         else {
d08333 3368             $a_folders = $this->conn->listMailboxes('', $mailbox);
af3c04 3369         }
c43517 3370
d08333 3371         if (is_array($a_folders) && in_array($mailbox, $a_folders)) {
A 3372             $this->icache[$key][] = $mailbox;
448409 3373             return true;
59c216 3374         }
A 3375
3376         return false;
3377     }
3378
3379
3380     /**
bbce3e 3381      * Returns the namespace where the folder is in
A 3382      *
d08333 3383      * @param string $mailbox Folder name
bbce3e 3384      *
A 3385      * @return string One of 'personal', 'other' or 'shared'
3386      * @access public
3387      */
d08333 3388     function mailbox_namespace($mailbox)
bbce3e 3389     {
d08333 3390         if ($mailbox == 'INBOX') {
bbce3e 3391             return 'personal';
A 3392         }
3393
3394         foreach ($this->namespace as $type => $namespace) {
3395             if (is_array($namespace)) {
3396                 foreach ($namespace as $ns) {
3397                     if (strlen($ns[0])) {
d08333 3398                         if ((strlen($ns[0])>1 && $mailbox == substr($ns[0], 0, -1))
A 3399                             || strpos($mailbox, $ns[0]) === 0
bbce3e 3400                         ) {
A 3401                             return $type;
3402                         }
3403                     }
3404                 }
3405             }
3406         }
3407
3408         return 'personal';
3409     }
3410
3411
3412     /**
d08333 3413      * Modify folder name according to namespace.
A 3414      * For output it removes prefix of the personal namespace if it's possible.
3415      * For input it adds the prefix. Use it before creating a folder in root
3416      * of the folders tree.
59c216 3417      *
d08333 3418      * @param string $mailbox Folder name
A 3419      * @param string $mode    Mode name (out/in)
3420      *
59c216 3421      * @return string Folder name
A 3422      */
d08333 3423     function mod_mailbox($mailbox, $mode = 'out')
59c216 3424     {
d08333 3425         if (!strlen($mailbox)) {
A 3426             return $mailbox;
3427         }
59c216 3428
d08333 3429         $prefix     = $this->namespace['prefix']; // see set_env()
A 3430         $prefix_len = strlen($prefix);
3431
3432         if (!$prefix_len) {
3433             return $mailbox;
3434         }
3435
3436         // remove prefix for output
3437         if ($mode == 'out') {
3438             if (substr($mailbox, 0, $prefix_len) === $prefix) {
3439                 return substr($mailbox, $prefix_len);
00290a 3440             }
A 3441         }
d08333 3442         // add prefix for input (e.g. folder creation)
00290a 3443         else {
d08333 3444             return $prefix . $mailbox;
59c216 3445         }
c43517 3446
d08333 3447         return $mailbox;
59c216 3448     }
A 3449
3450
103ddc 3451     /**
3870be 3452      * Gets folder options from LIST response, e.g. \Noselect, \Noinferiors
a5a4bf 3453      *
d08333 3454      * @param string $mailbox Folder name
A 3455      * @param bool   $force   Set to True if options should be refreshed
3456      *                        Options are available after LIST command only
a5a4bf 3457      *
A 3458      * @return array Options list
3459      */
d08333 3460     function mailbox_options($mailbox, $force=false)
a5a4bf 3461     {
d08333 3462         if ($mailbox == 'INBOX') {
a5a4bf 3463             return array();
A 3464         }
3465
d08333 3466         if (!is_array($this->conn->data['LIST']) || !is_array($this->conn->data['LIST'][$mailbox])) {
3870be 3467             if ($force) {
d08333 3468                 $this->conn->listMailboxes('', $mailbox);
3870be 3469             }
A 3470             else {
3471                 return array();
3472             }
a5a4bf 3473         }
A 3474
d08333 3475         $opts = $this->conn->data['LIST'][$mailbox];
a5a4bf 3476
A 3477         return is_array($opts) ? $opts : array();
3478     }
3479
3480
3481     /**
103ddc 3482      * Get message header names for rcube_imap_generic::fetchHeader(s)
A 3483      *
3484      * @return string Space-separated list of header names
3485      */
3486     private function get_fetch_headers()
3487     {
3488         $headers = explode(' ', $this->fetch_add_headers);
3489         $headers = array_map('strtoupper', $headers);
3490
3491         if ($this->caching_enabled || $this->get_all_headers)
3492             $headers = array_merge($headers, $this->all_headers);
3493
3494         return implode(' ', array_unique($headers));
3495     }
3496
3497
8b6eff 3498     /* -----------------------------------------
A 3499      *   ACL and METADATA/ANNOTATEMORE methods
3500      * ----------------------------------------*/
3501
3502     /**
3503      * Changes the ACL on the specified mailbox (SETACL)
3504      *
3505      * @param string $mailbox Mailbox name
3506      * @param string $user    User name
3507      * @param string $acl     ACL string
3508      *
3509      * @return boolean True on success, False on failure
3510      *
3511      * @access public
3512      * @since 0.5-beta
3513      */
3514     function set_acl($mailbox, $user, $acl)
3515     {
3516         if ($this->get_capability('ACL'))
3517             return $this->conn->setACL($mailbox, $user, $acl);
3518
3519         return false;
3520     }
3521
3522
3523     /**
3524      * Removes any <identifier,rights> pair for the
3525      * specified user from the ACL for the specified
3526      * mailbox (DELETEACL)
3527      *
3528      * @param string $mailbox Mailbox name
3529      * @param string $user    User name
3530      *
3531      * @return boolean True on success, False on failure
3532      *
3533      * @access public
3534      * @since 0.5-beta
3535      */
3536     function delete_acl($mailbox, $user)
3537     {
3538         if ($this->get_capability('ACL'))
3539             return $this->conn->deleteACL($mailbox, $user);
3540
3541         return false;
3542     }
3543
3544
3545     /**
3546      * Returns the access control list for mailbox (GETACL)
3547      *
3548      * @param string $mailbox Mailbox name
3549      *
3550      * @return array User-rights array on success, NULL on error
3551      * @access public
3552      * @since 0.5-beta
3553      */
3554     function get_acl($mailbox)
3555     {
3556         if ($this->get_capability('ACL'))
3557             return $this->conn->getACL($mailbox);
3558
3559         return NULL;
3560     }
3561
3562
3563     /**
3564      * Returns information about what rights can be granted to the
3565      * user (identifier) in the ACL for the mailbox (LISTRIGHTS)
3566      *
3567      * @param string $mailbox Mailbox name
3568      * @param string $user    User name
3569      *
3570      * @return array List of user rights
3571      * @access public
3572      * @since 0.5-beta
3573      */
3574     function list_rights($mailbox, $user)
3575     {
3576         if ($this->get_capability('ACL'))
3577             return $this->conn->listRights($mailbox, $user);
3578
3579         return NULL;
3580     }
3581
3582
3583     /**
3584      * Returns the set of rights that the current user has to
3585      * mailbox (MYRIGHTS)
3586      *
3587      * @param string $mailbox Mailbox name
3588      *
3589      * @return array MYRIGHTS response on success, NULL on error
3590      * @access public
3591      * @since 0.5-beta
3592      */
3593     function my_rights($mailbox)
3594     {
3595         if ($this->get_capability('ACL'))
3596             return $this->conn->myRights($mailbox);
3597
3598         return NULL;
3599     }
3600
3601
3602     /**
3603      * Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
3604      *
3605      * @param string $mailbox Mailbox name (empty for server metadata)
3606      * @param array  $entries Entry-value array (use NULL value as NIL)
3607      *
3608      * @return boolean True on success, False on failure
3609      * @access public
3610      * @since 0.5-beta
3611      */
3612     function set_metadata($mailbox, $entries)
3613     {
448409 3614         if ($this->get_capability('METADATA') ||
A 3615             (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
8b6eff 3616         ) {
A 3617             return $this->conn->setMetadata($mailbox, $entries);
3618         }
3619         else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
e22740 3620             foreach ((array)$entries as $entry => $value) {
8b6eff 3621                 list($ent, $attr) = $this->md2annotate($entry);
A 3622                 $entries[$entry] = array($ent, $attr, $value);
3623             }
3624             return $this->conn->setAnnotation($mailbox, $entries);
3625         }
3626
3627         return false;
3628     }
3629
3630
3631     /**
3632      * Unsets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
3633      *
3634      * @param string $mailbox Mailbox name (empty for server metadata)
3635      * @param array  $entries Entry names array
3636      *
3637      * @return boolean True on success, False on failure
3638      *
3639      * @access public
3640      * @since 0.5-beta
3641      */
3642     function delete_metadata($mailbox, $entries)
3643     {
3644         if ($this->get_capability('METADATA') || 
448409 3645             (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
8b6eff 3646         ) {
A 3647             return $this->conn->deleteMetadata($mailbox, $entries);
3648         }
3649         else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
e22740 3650             foreach ((array)$entries as $idx => $entry) {
8b6eff 3651                 list($ent, $attr) = $this->md2annotate($entry);
A 3652                 $entries[$idx] = array($ent, $attr, NULL);
3653             }
3654             return $this->conn->setAnnotation($mailbox, $entries);
3655         }
3656
3657         return false;
3658     }
3659
3660
3661     /**
3662      * Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION)
3663      *
3664      * @param string $mailbox Mailbox name (empty for server metadata)
3665      * @param array  $entries Entries
3666      * @param array  $options Command options (with MAXSIZE and DEPTH keys)
3667      *
3668      * @return array Metadata entry-value hash array on success, NULL on error
3669      *
3670      * @access public
3671      * @since 0.5-beta
3672      */
3673     function get_metadata($mailbox, $entries, $options=array())
3674     {
3675         if ($this->get_capability('METADATA') || 
e22740 3676             (!strlen($mailbox) && $this->get_capability('METADATA-SERVER'))
8b6eff 3677         ) {
A 3678             return $this->conn->getMetadata($mailbox, $entries, $options);
3679         }
3680         else if ($this->get_capability('ANNOTATEMORE') || $this->get_capability('ANNOTATEMORE2')) {
3681             $queries = array();
3682             $res     = array();
3683
3684             // Convert entry names
e22740 3685             foreach ((array)$entries as $entry) {
8b6eff 3686                 list($ent, $attr) = $this->md2annotate($entry);
A 3687                 $queries[$attr][] = $ent;
3688             }
3689
3690             // @TODO: Honor MAXSIZE and DEPTH options
3691             foreach ($queries as $attrib => $entry)
3692                 if ($result = $this->conn->getAnnotation($mailbox, $entry, $attrib))
3693                     $res = array_merge($res, $result);
3694
3695             return $res;
3696         }
3697
3698         return NULL;
3699     }
3700
3701
3702     /**
3703      * Converts the METADATA extension entry name into the correct
3704      * entry-attrib names for older ANNOTATEMORE version.
3705      *
e22740 3706      * @param string $entry Entry name
8b6eff 3707      *
A 3708      * @return array Entry-attribute list, NULL if not supported (?)
3709      */
e22740 3710     private function md2annotate($entry)
8b6eff 3711     {
A 3712         if (substr($entry, 0, 7) == '/shared') {
3713             return array(substr($entry, 7), 'value.shared');
3714         }
3715         else if (substr($entry, 0, 8) == '/private') {
3716             return array(substr($entry, 8), 'value.priv');
3717         }
3718
3719         // @TODO: log error
3720         return NULL;
3721     }
3722
3723
59c216 3724     /* --------------------------------
A 3725      *   internal caching methods
3726      * --------------------------------*/
3727
3728     /**
5c461b 3729      * Enable or disable caching
A 3730      *
3731      * @param boolean $set Flag
59c216 3732      * @access public
A 3733      */
3734     function set_caching($set)
3735     {
3736         if ($set && is_object($this->db))
3737             $this->caching_enabled = true;
3738         else
3739             $this->caching_enabled = false;
3740     }
3741
29983c 3742
59c216 3743     /**
5c461b 3744      * Returns cached value
A 3745      *
3746      * @param string $key Cache key
3747      * @return mixed
59c216 3748      * @access public
A 3749      */
3750     function get_cache($key)
3751     {
3752         // read cache (if it was not read before)
3753         if (!count($this->cache) && $this->caching_enabled) {
3754             return $this->_read_cache_record($key);
3755         }
c43517 3756
59c216 3757         return $this->cache[$key];
A 3758     }
3759
29983c 3760
59c216 3761     /**
5c461b 3762      * Update cache
A 3763      *
3764      * @param string $key  Cache key
3765      * @param mixed  $data Data
59c216 3766      * @access private
A 3767      */
3768     private function update_cache($key, $data)
3769     {
3770         $this->cache[$key] = $data;
3771         $this->cache_changed = true;
3772         $this->cache_changes[$key] = true;
3773     }
29983c 3774
59c216 3775
A 3776     /**
5c461b 3777      * Writes the cache
A 3778      *
59c216 3779      * @access private
A 3780      */
3781     private function write_cache()
3782     {
3783         if ($this->caching_enabled && $this->cache_changed) {
3784             foreach ($this->cache as $key => $data) {
3785                 if ($this->cache_changes[$key])
3786                     $this->_write_cache_record($key, serialize($data));
3787             }
3788         }
3789     }
29983c 3790
59c216 3791
A 3792     /**
5c461b 3793      * Clears the cache.
A 3794      *
94bdcc 3795      * @param string  $key          Cache key name or pattern
A 3796      * @param boolean $pattern_mode Enable it to clear all keys with name
3797      *                              matching PREG pattern in $key
59c216 3798      * @access public
A 3799      */
94bdcc 3800     function clear_cache($key=null, $pattern_mode=false)
59c216 3801     {
A 3802         if (!$this->caching_enabled)
3803             return;
c43517 3804
94bdcc 3805         if ($key === null) {
A 3806             foreach (array_keys($this->cache) as $key)
59c216 3807                 $this->_clear_cache_record($key);
A 3808
3809             $this->cache = array();
3810             $this->cache_changed = false;
3811             $this->cache_changes = array();
3812         }
94bdcc 3813         else if ($pattern_mode) {
A 3814             foreach (array_keys($this->cache) as $k) {
3815                 if (preg_match($key, $k)) {
3816                     $this->_clear_cache_record($k);
3817                     $this->cache_changes[$k] = false;
3818                     unset($this->cache[$key]);
3819                 }
3820             }
3821             if (!count($this->cache)) {
3822                 $this->cache_changed = false;
3823             }
3824         }
59c216 3825         else {
A 3826             $this->_clear_cache_record($key);
3827             $this->cache_changes[$key] = false;
3828             unset($this->cache[$key]);
3829         }
3830     }
29983c 3831
59c216 3832
A 3833     /**
5c461b 3834      * Returns cached entry
A 3835      *
3836      * @param string $key Cache key
3837      * @return mixed Cached value
59c216 3838      * @access private
A 3839      */
3840     private function _read_cache_record($key)
3841     {
3842         if ($this->db) {
3843             // get cached data from DB
3844             $sql_result = $this->db->query(
3845                 "SELECT cache_id, data, cache_key ".
3846                 "FROM ".get_table_name('cache').
3847                 " WHERE user_id=? ".
3848                 "AND cache_key LIKE 'IMAP.%'",
3849                 $_SESSION['user_id']);
3850
3851             while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
3852                 $sql_key = preg_replace('/^IMAP\./', '', $sql_arr['cache_key']);
3853                 $this->cache_keys[$sql_key] = $sql_arr['cache_id'];
3854                 if (!isset($this->cache[$sql_key]))
3855                     $this->cache[$sql_key] = $sql_arr['data'] ? unserialize($sql_arr['data']) : false;
3856             }
3857         }
3858
3859         return $this->cache[$key];
3860     }
29983c 3861
59c216 3862
A 3863     /**
5c461b 3864      * Writes single cache record
A 3865      *
3866      * @param string $key  Cache key
3867      * @param mxied  $data Cache value
59c216 3868      * @access private
A 3869      */
3870     private function _write_cache_record($key, $data)
3871     {
3872         if (!$this->db)
3873             return false;
3874
3875         // update existing cache record
3876         if ($this->cache_keys[$key]) {
3877             $this->db->query(
3878                 "UPDATE ".get_table_name('cache').
3879                 " SET created=". $this->db->now().", data=? ".
3880                 "WHERE user_id=? ".
3881                 "AND cache_key=?",
3882                 $data,
3883                 $_SESSION['user_id'],
3884                 'IMAP.'.$key);
3885         }
3886         // add new cache record
3887         else {
3888             $this->db->query(
3889                 "INSERT INTO ".get_table_name('cache').
3890                 " (created, user_id, cache_key, data) ".
3891                 "VALUES (".$this->db->now().", ?, ?, ?)",
3892                 $_SESSION['user_id'],
3893                 'IMAP.'.$key,
3894                 $data);
3895
3896             // get cache entry ID for this key
3897             $sql_result = $this->db->query(
3898                 "SELECT cache_id ".
3899                 "FROM ".get_table_name('cache').
3900                 " WHERE user_id=? ".
3901                 "AND cache_key=?",
3902                 $_SESSION['user_id'],
3903                 'IMAP.'.$key);
3904
3905             if ($sql_arr = $this->db->fetch_assoc($sql_result))
3906                 $this->cache_keys[$key] = $sql_arr['cache_id'];
3907         }
3908     }
29983c 3909
59c216 3910
A 3911     /**
5c461b 3912      * Clears cache for single record
A 3913      *
3914      * @param string $ket Cache key
59c216 3915      * @access private
A 3916      */
3917     private function _clear_cache_record($key)
3918     {
3919         $this->db->query(
3920             "DELETE FROM ".get_table_name('cache').
3921             " WHERE user_id=? ".
3922             "AND cache_key=?",
3923             $_SESSION['user_id'],
3924             'IMAP.'.$key);
c43517 3925
59c216 3926         unset($this->cache_keys[$key]);
A 3927     }
3928
3929
3930
3931     /* --------------------------------
3932      *   message caching methods
3933      * --------------------------------*/
c43517 3934
59c216 3935     /**
A 3936      * Checks if the cache is up-to-date
3937      *
5c461b 3938      * @param string $mailbox   Mailbox name
A 3939      * @param string $cache_key Internal cache key
59c216 3940      * @return int   Cache status: -3 = off, -2 = incomplete, -1 = dirty, 1 = OK
A 3941      */
3942     private function check_cache_status($mailbox, $cache_key)
3943     {
3944         if (!$this->caching_enabled)
3945             return -3;
3946
3947         $cache_index = $this->get_message_cache_index($cache_key);
3948         $msg_count = $this->_messagecount($mailbox);
3949         $cache_count = count($cache_index);
3950
3951         // empty mailbox
9ae29c 3952         if (!$msg_count) {
59c216 3953             return $cache_count ? -2 : 1;
9ae29c 3954         }
59c216 3955
93272e 3956         if ($cache_count == $msg_count) {
59c216 3957             if ($this->skip_deleted) {
9ae29c 3958                 if (!empty($this->icache['all_undeleted_idx'])) {
A 3959                     $uids = rcube_imap_generic::uncompressMessageSet($this->icache['all_undeleted_idx']);
3960                     $uids = array_flip($uids);
3961                     foreach ($cache_index as $uid) {
3962                         unset($uids[$uid]);
3963                     }
3964                 }
3965                 else {
3966                     // get all undeleted messages excluding cached UIDs
3967                     $uids = $this->search_once($mailbox, 'ALL UNDELETED NOT UID '.
3968                         rcube_imap_generic::compressMessageSet($cache_index));
3969                 }
3970                 if (empty($uids)) {
3971                     return 1;
3972                 }
59c216 3973             } else {
eacce9 3974                 // get UID of the message with highest index
A 3975                 $uid = $this->_id2uid($msg_count, $mailbox);
59c216 3976                 $cache_uid = array_pop($cache_index);
c43517 3977
59c216 3978                 // uids of highest message matches -> cache seems OK
9ae29c 3979                 if ($cache_uid == $uid) {
59c216 3980                     return 1;
9ae29c 3981                 }
59c216 3982             }
A 3983             // cache is dirty
3984             return -1;
3985         }
9ae29c 3986
59c216 3987         // if cache count differs less than 10% report as dirty
9ae29c 3988         return (abs($msg_count - $cache_count) < $msg_count/10) ? -1 : -2;
59c216 3989     }
A 3990
29983c 3991
59c216 3992     /**
5c461b 3993      * @param string $key Cache key
A 3994      * @param string $from
3995      * @param string $to
3996      * @param string $sort_field
3997      * @param string $sort_order
59c216 3998      * @access private
A 3999      */
4000     private function get_message_cache($key, $from, $to, $sort_field, $sort_order)
4001     {
eacce9 4002         if (!$this->caching_enabled)
A 4003             return NULL;
c43517 4004
59c216 4005         // use idx sort as default sorting
A 4006         if (!$sort_field || !in_array($sort_field, $this->db_header_fields)) {
4007             $sort_field = 'idx';
4008         }
c43517 4009
eacce9 4010         $result = array();
A 4011
4012         $sql_result = $this->db->limitquery(
59c216 4013                 "SELECT idx, uid, headers".
A 4014                 " FROM ".get_table_name('messages').
4015                 " WHERE user_id=?".
4016                 " AND cache_key=?".
4017                 " ORDER BY ".$this->db->quoteIdentifier($sort_field)." ".strtoupper($sort_order),
4018                 $from,
4019                 $to - $from,
4020                 $_SESSION['user_id'],
4021                 $key);
4022
eacce9 4023         while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
A 4024             $uid = intval($sql_arr['uid']);
4025             $result[$uid] = $this->db->decode(unserialize($sql_arr['headers']));
59c216 4026
eacce9 4027             // featch headers if unserialize failed
A 4028             if (empty($result[$uid]))
4029                 $result[$uid] = $this->conn->fetchHeader(
103ddc 4030                     preg_replace('/.msg$/', '', $key), $uid, true, false, $this->get_fetch_headers());
59c216 4031         }
A 4032
eacce9 4033         return $result;
59c216 4034     }
A 4035
29983c 4036
59c216 4037     /**
5c461b 4038      * @param string $key Cache key
29983c 4039      * @param int    $uid Message UID
5c461b 4040      * @return mixed
59c216 4041      * @access private
A 4042      */
4043     private function &get_cached_message($key, $uid)
4044     {
4045         $internal_key = 'message';
c43517 4046
59c216 4047         if ($this->caching_enabled && !isset($this->icache[$internal_key][$uid])) {
A 4048             $sql_result = $this->db->query(
eacce9 4049                 "SELECT idx, headers, structure, message_id".
59c216 4050                 " FROM ".get_table_name('messages').
A 4051                 " WHERE user_id=?".
4052                 " AND cache_key=?".
4053                 " AND uid=?",
4054                 $_SESSION['user_id'],
4055                 $key,
4056                 $uid);
4057
4058             if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
eacce9 4059                 $this->icache['message.id'][$uid] = intval($sql_arr['message_id']);
A 4060                 $this->uid_id_map[preg_replace('/\.msg$/', '', $key)][$uid] = intval($sql_arr['idx']);
59c216 4061                 $this->icache[$internal_key][$uid] = $this->db->decode(unserialize($sql_arr['headers']));
eacce9 4062
59c216 4063                 if (is_object($this->icache[$internal_key][$uid]) && !empty($sql_arr['structure']))
A 4064                     $this->icache[$internal_key][$uid]->structure = $this->db->decode(unserialize($sql_arr['structure']));
4065             }
4066         }
4067
4068         return $this->icache[$internal_key][$uid];
4069     }
4070
29983c 4071
59c216 4072     /**
29983c 4073      * @param string  $key        Cache key
A 4074      * @param string  $sort_field Sorting column
4075      * @param string  $sort_order Sorting order
4076      * @return array Messages index
59c216 4077      * @access private
c43517 4078      */
eacce9 4079     private function get_message_cache_index($key, $sort_field='idx', $sort_order='ASC')
59c216 4080     {
A 4081         if (!$this->caching_enabled || empty($key))
eacce9 4082             return NULL;
59c216 4083
A 4084         // use idx sort as default
4085         if (!$sort_field || !in_array($sort_field, $this->db_header_fields))
4086             $sort_field = 'idx';
c43517 4087
eacce9 4088         if (array_key_exists('index', $this->icache)
A 4089             && $this->icache['index']['key'] == $key
29983c 4090             && $this->icache['index']['sort_field'] == $sort_field
eacce9 4091         ) {
29983c 4092             if ($this->icache['index']['sort_order'] == $sort_order)
A 4093                 return $this->icache['index']['result'];
4094             else
4095                 return array_reverse($this->icache['index']['result'], true);
eacce9 4096         }
A 4097
4098         $this->icache['index'] = array(
29983c 4099             'result'     => array(),
A 4100             'key'        => $key,
4101             'sort_field' => $sort_field,
4102             'sort_order' => $sort_order,
eacce9 4103         );
A 4104
59c216 4105         $sql_result = $this->db->query(
A 4106             "SELECT idx, uid".
4107             " FROM ".get_table_name('messages').
4108             " WHERE user_id=?".
4109             " AND cache_key=?".
4110             " ORDER BY ".$this->db->quote_identifier($sort_field)." ".$sort_order,
4111             $_SESSION['user_id'],
4112             $key);
4113
4114         while ($sql_arr = $this->db->fetch_assoc($sql_result))
eacce9 4115             $this->icache['index']['result'][$sql_arr['idx']] = intval($sql_arr['uid']);
c43517 4116
eacce9 4117         return $this->icache['index']['result'];
59c216 4118     }
29983c 4119
59c216 4120
A 4121     /**
4122      * @access private
4123      */
eacce9 4124     private function add_message_cache($key, $index, $headers, $struct=null, $force=false, $internal_cache=false)
59c216 4125     {
A 4126         if (empty($key) || !is_object($headers) || empty($headers->uid))
4127             return;
4128
4129         // add to internal (fast) cache
eacce9 4130         if ($internal_cache) {
A 4131             $this->icache['message'][$headers->uid] = clone $headers;
4132             $this->icache['message'][$headers->uid]->structure = $struct;
4133         }
59c216 4134
A 4135         // no further caching
4136         if (!$this->caching_enabled)
4137             return;
c43517 4138
eacce9 4139         // known message id
A 4140         if (is_int($force) && $force > 0) {
4141             $message_id = $force;
4142         }
175d8e 4143         // check for an existing record (probably headers are cached but structure not)
eacce9 4144         else if (!$force) {
59c216 4145             $sql_result = $this->db->query(
A 4146                 "SELECT message_id".
4147                 " FROM ".get_table_name('messages').
4148                 " WHERE user_id=?".
4149                 " AND cache_key=?".
4150                 " AND uid=?",
4151                 $_SESSION['user_id'],
4152                 $key,
4153                 $headers->uid);
4154
4155             if ($sql_arr = $this->db->fetch_assoc($sql_result))
4156                 $message_id = $sql_arr['message_id'];
4157         }
4158
4159         // update cache record
4160         if ($message_id) {
4161             $this->db->query(
4162                 "UPDATE ".get_table_name('messages').
4163                 " SET idx=?, headers=?, structure=?".
4164                 " WHERE message_id=?",
4165                 $index,
4166                 serialize($this->db->encode(clone $headers)),
4167                 is_object($struct) ? serialize($this->db->encode(clone $struct)) : NULL,
4168                 $message_id
4169             );
4170         }
4171         else { // insert new record
4172             $this->db->query(
4173                 "INSERT INTO ".get_table_name('messages').
4174                 " (user_id, del, cache_key, created, idx, uid, subject, ".
4175                 $this->db->quoteIdentifier('from').", ".
4176                 $this->db->quoteIdentifier('to').", ".
4177                 "cc, date, size, headers, structure)".
4178                 " VALUES (?, 0, ?, ".$this->db->now().", ?, ?, ?, ?, ?, ?, ".
4179                 $this->db->fromunixtime($headers->timestamp).", ?, ?, ?)",
4180                 $_SESSION['user_id'],
4181                 $key,
4182                 $index,
4183                 $headers->uid,
4184                 (string)mb_substr($this->db->encode($this->decode_header($headers->subject, true)), 0, 128),
4185                 (string)mb_substr($this->db->encode($this->decode_header($headers->from, true)), 0, 128),
4186                 (string)mb_substr($this->db->encode($this->decode_header($headers->to, true)), 0, 128),
4187                 (string)mb_substr($this->db->encode($this->decode_header($headers->cc, true)), 0, 128),
4188                 (int)$headers->size,
4189                 serialize($this->db->encode(clone $headers)),
4190                 is_object($struct) ? serialize($this->db->encode(clone $struct)) : NULL
4191             );
4192         }
eacce9 4193
A 4194         unset($this->icache['index']);
59c216 4195     }
c43517 4196
29983c 4197
59c216 4198     /**
A 4199      * @access private
4200      */
4201     private function remove_message_cache($key, $ids, $idx=false)
4202     {
4203         if (!$this->caching_enabled)
4204             return;
c43517 4205
59c216 4206         $this->db->query(
A 4207             "DELETE FROM ".get_table_name('messages').
4208             " WHERE user_id=?".
4209             " AND cache_key=?".
4210             " AND ".($idx ? "idx" : "uid")." IN (".$this->db->array2list($ids, 'integer').")",
4211             $_SESSION['user_id'],
4212             $key);
eacce9 4213
A 4214         unset($this->icache['index']);
59c216 4215     }
29983c 4216
59c216 4217
A 4218     /**
5c461b 4219      * @param string $key         Cache key
A 4220      * @param int    $start_index Start index
59c216 4221      * @access private
A 4222      */
4223     private function clear_message_cache($key, $start_index=1)
4224     {
4225         if (!$this->caching_enabled)
4226             return;
c43517 4227
59c216 4228         $this->db->query(
A 4229             "DELETE FROM ".get_table_name('messages').
4230             " WHERE user_id=?".
4231             " AND cache_key=?".
4232             " AND idx>=?",
4233             $_SESSION['user_id'], $key, $start_index);
eacce9 4234
A 4235         unset($this->icache['index']);
59c216 4236     }
29983c 4237
59c216 4238
A 4239     /**
4240      * @access private
4241      */
4242     private function get_message_cache_index_min($key, $uids=NULL)
4243     {
4244         if (!$this->caching_enabled)
4245             return;
c43517 4246
59c216 4247         if (!empty($uids) && !is_array($uids)) {
A 4248             if ($uids == '*' || $uids == '1:*')
4249                 $uids = NULL;
4250             else
4251                 $uids = explode(',', $uids);
4252         }
4253
4254         $sql_result = $this->db->query(
4255             "SELECT MIN(idx) AS minidx".
4256             " FROM ".get_table_name('messages').
4257             " WHERE  user_id=?".
4258             " AND    cache_key=?"
4259             .(!empty($uids) ? " AND uid IN (".$this->db->array2list($uids, 'integer').")" : ''),
4260             $_SESSION['user_id'],
4261             $key);
4262
4263         if ($sql_arr = $this->db->fetch_assoc($sql_result))
4264             return $sql_arr['minidx'];
4265         else
c43517 4266             return 0;
29983c 4267     }
A 4268
4269
4270     /**
4271      * @param string $key Cache key
4272      * @param int    $id  Message (sequence) ID
4273      * @return int Message UID
4274      * @access private
4275      */
4276     private function get_cache_id2uid($key, $id)
4277     {
4278         if (!$this->caching_enabled)
4279             return null;
4280
4281         if (array_key_exists('index', $this->icache)
4282             && $this->icache['index']['key'] == $key
4283         ) {
4284             return $this->icache['index']['result'][$id];
4285         }
4286
4287         $sql_result = $this->db->query(
4288             "SELECT uid".
4289             " FROM ".get_table_name('messages').
4290             " WHERE user_id=?".
4291             " AND cache_key=?".
4292             " AND idx=?",
4293             $_SESSION['user_id'], $key, $id);
4294
4295         if ($sql_arr = $this->db->fetch_assoc($sql_result))
4296             return intval($sql_arr['uid']);
4297
4298         return null;
4299     }
4300
4301
4302     /**
4303      * @param string $key Cache key
4304      * @param int    $uid Message UID
4305      * @return int Message (sequence) ID
4306      * @access private
4307      */
4308     private function get_cache_uid2id($key, $uid)
4309     {
4310         if (!$this->caching_enabled)
4311             return null;
4312
4313         if (array_key_exists('index', $this->icache)
4314             && $this->icache['index']['key'] == $key
4315         ) {
4316             return array_search($uid, $this->icache['index']['result']);
4317         }
4318
4319         $sql_result = $this->db->query(
4320             "SELECT idx".
4321             " FROM ".get_table_name('messages').
4322             " WHERE user_id=?".
4323             " AND cache_key=?".
4324             " AND uid=?",
4325             $_SESSION['user_id'], $key, $uid);
4326
4327         if ($sql_arr = $this->db->fetch_assoc($sql_result))
4328             return intval($sql_arr['idx']);
4329
4330         return null;
59c216 4331     }
A 4332
4333
4334     /* --------------------------------
4335      *   encoding/decoding methods
4336      * --------------------------------*/
4337
4338     /**
4339      * Split an address list into a structured array list
4340      *
5c461b 4341      * @param string  $input  Input string
A 4342      * @param int     $max    List only this number of addresses
4343      * @param boolean $decode Decode address strings
59c216 4344      * @return array  Indexed list of addresses
A 4345      */
4346     function decode_address_list($input, $max=null, $decode=true)
4347     {
4348         $a = $this->_parse_address_list($input, $decode);
4349         $out = array();
4350         // Special chars as defined by RFC 822 need to in quoted string (or escaped).
4351         $special_chars = '[\(\)\<\>\\\.\[\]@,;:"]';
c43517 4352
59c216 4353         if (!is_array($a))
A 4354             return $out;
4355
4356         $c = count($a);
4357         $j = 0;
4358
4359         foreach ($a as $val) {
4360             $j++;
4361             $address = trim($val['address']);
435c31 4362             $name    = trim($val['name']);
59c216 4363
A 4364             if ($name && $address && $name != $address)
4365                 $string = sprintf('%s <%s>', preg_match("/$special_chars/", $name) ? '"'.addcslashes($name, '"').'"' : $name, $address);
4366             else if ($address)
4367                 $string = $address;
4368             else if ($name)
4369                 $string = $name;
c43517 4370
e99991 4371             $out[$j] = array(
A 4372                 'name'   => $name,
59c216 4373                 'mailto' => $address,
A 4374                 'string' => $string
4375             );
4376
4377             if ($max && $j==$max)
4378                 break;
4379         }
c43517 4380
59c216 4381         return $out;
A 4382     }
4383
4384
4385     /**
4386      * Decode a message header value
4387      *
5c461b 4388      * @param string  $input         Header value
A 4389      * @param boolean $remove_quotas Remove quotes if necessary
59c216 4390      * @return string Decoded string
A 4391      */
4392     function decode_header($input, $remove_quotes=false)
4393     {
4394         $str = rcube_imap::decode_mime_string((string)$input, $this->default_charset);
2aa2b3 4395         if ($str[0] == '"' && $remove_quotes)
59c216 4396             $str = str_replace('"', '', $str);
c43517 4397
59c216 4398         return $str;
A 4399     }
4400
4401
4402     /**
4403      * Decode a mime-encoded string to internal charset
4404      *
4405      * @param string $input    Header value
4406      * @param string $fallback Fallback charset if none specified
4407      *
4408      * @return string Decoded string
4409      * @static
4410      */
4411     public static function decode_mime_string($input, $fallback=null)
4412     {
8df56e 4413         if (!empty($fallback)) {
A 4414             $default_charset = $fallback;
4415         }
4416         else {
4417             $default_charset = rcmail::get_instance()->config->get('default_charset', 'ISO-8859-1');
4418         }
59c216 4419
c43517 4420         // rfc: all line breaks or other characters not found
59c216 4421         // in the Base64 Alphabet must be ignored by decoding software
c43517 4422         // delete all blanks between MIME-lines, differently we can
59c216 4423         // receive unnecessary blanks and broken utf-8 symbols
A 4424         $input = preg_replace("/\?=\s+=\?/", '?==?', $input);
4425
8df56e 4426         // encoded-word regexp
A 4427         $re = '/=\?([^?]+)\?([BbQq])\?([^?\n]*)\?=/';
4428
4429         // Find all RFC2047's encoded words
4430         if (preg_match_all($re, $input, $matches, PREG_OFFSET_CAPTURE | PREG_SET_ORDER)) {
4431             // Initialize variables
4432             $tmp   = array();
4433             $out   = '';
4434             $start = 0;
4435
4436             foreach ($matches as $idx => $m) {
4437                 $pos      = $m[0][1];
4438                 $charset  = $m[1][0];
4439                 $encoding = $m[2][0];
4440                 $text     = $m[3][0];
4441                 $length   = strlen($m[0][0]);
4442
59c216 4443                 // Append everything that is before the text to be decoded
8df56e 4444                 if ($start != $pos) {
A 4445                     $substr = substr($input, $start, $pos-$start);
4446                     $out   .= rcube_charset_convert($substr, $default_charset);
4447                     $start  = $pos;
4448                 }
4449                 $start += $length;
59c216 4450
8df56e 4451                 // Per RFC2047, each string part "MUST represent an integral number
A 4452                 // of characters . A multi-octet character may not be split across
4453                 // adjacent encoded-words." However, some mailers break this, so we
4454                 // try to handle characters spanned across parts anyway by iterating
4455                 // through and aggregating sequential encoded parts with the same
4456                 // character set and encoding, then perform the decoding on the
4457                 // aggregation as a whole.
59c216 4458
8df56e 4459                 $tmp[] = $text;
A 4460                 if ($next_match = $matches[$idx+1]) {
4461                     if ($next_match[0][1] == $start
4462                         && $next_match[1][0] == $charset
4463                         && $next_match[2][0] == $encoding
4464                     ) {
4465                         continue;
4466                     }
4467                 }
59c216 4468
8df56e 4469                 $count = count($tmp);
A 4470                 $text  = '';
4471
4472                 // Decode and join encoded-word's chunks
4473                 if ($encoding == 'B' || $encoding == 'b') {
4474                     // base64 must be decoded a segment at a time
4475                     for ($i=0; $i<$count; $i++)
4476                         $text .= base64_decode($tmp[$i]);
4477                 }
4478                 else { //if ($encoding == 'Q' || $encoding == 'q') {
4479                     // quoted printable can be combined and processed at once
4480                     for ($i=0; $i<$count; $i++)
4481                         $text .= $tmp[$i];
4482
4483                     $text = str_replace('_', ' ', $text);
4484                     $text = quoted_printable_decode($text);
4485                 }
4486
4487                 $out .= rcube_charset_convert($text, $charset);
4488                 $tmp = array();
59c216 4489             }
A 4490
8df56e 4491             // add the last part of the input string
A 4492             if ($start != strlen($input)) {
4493                 $out .= rcube_charset_convert(substr($input, $start), $default_charset);
4494             }
59c216 4495
A 4496             // return the results
4497             return $out;
4498         }
4499
4500         // no encoding information, use fallback
8df56e 4501         return rcube_charset_convert($input, $default_charset);
59c216 4502     }
A 4503
4504
4505     /**
4506      * Decode a mime part
4507      *
5c461b 4508      * @param string $input    Input string
A 4509      * @param string $encoding Part encoding
59c216 4510      * @return string Decoded string
A 4511      */
4512     function mime_decode($input, $encoding='7bit')
4513     {
4514         switch (strtolower($encoding)) {
4515         case 'quoted-printable':
4516             return quoted_printable_decode($input);
4517         case 'base64':
4518             return base64_decode($input);
4519         case 'x-uuencode':
4520         case 'x-uue':
4521         case 'uue':
4522         case 'uuencode':
4523             return convert_uudecode($input);
4524         case '7bit':
4525         default:
4526             return $input;
4527         }
4528     }
4529
4530
4531     /**
4532      * Convert body charset to RCMAIL_CHARSET according to the ctype_parameters
4533      *
5c461b 4534      * @param string $body        Part body to decode
A 4535      * @param string $ctype_param Charset to convert from
59c216 4536      * @return string Content converted to internal charset
A 4537      */
4538     function charset_decode($body, $ctype_param)
4539     {
4540         if (is_array($ctype_param) && !empty($ctype_param['charset']))
4541             return rcube_charset_convert($body, $ctype_param['charset']);
4542
4543         // defaults to what is specified in the class header
4544         return rcube_charset_convert($body,  $this->default_charset);
4545     }
4546
4547
4548     /* --------------------------------
4549      *         private methods
4550      * --------------------------------*/
4551
4552     /**
4553      * Validate the given input and save to local properties
5c461b 4554      *
A 4555      * @param string $sort_field Sort column
4556      * @param string $sort_order Sort order
59c216 4557      * @access private
A 4558      */
4559     private function _set_sort_order($sort_field, $sort_order)
4560     {
4561         if ($sort_field != null)
4562             $this->sort_field = asciiwords($sort_field);
4563         if ($sort_order != null)
4564             $this->sort_order = strtoupper($sort_order) == 'DESC' ? 'DESC' : 'ASC';
4565     }
4566
29983c 4567
59c216 4568     /**
A 4569      * Sort mailboxes first by default folders and then in alphabethical order
5c461b 4570      *
A 4571      * @param array $a_folders Mailboxes list
59c216 4572      * @access private
A 4573      */
4574     private function _sort_mailbox_list($a_folders)
4575     {
4576         $a_out = $a_defaults = $folders = array();
4577
4578         $delimiter = $this->get_hierarchy_delimiter();
4579
4580         // find default folders and skip folders starting with '.'
4581         foreach ($a_folders as $i => $folder) {
4582             if ($folder[0] == '.')
4583                 continue;
4584
4585             if (($p = array_search($folder, $this->default_folders)) !== false && !$a_defaults[$p])
4586                 $a_defaults[$p] = $folder;
4587             else
a44682 4588                 $folders[$folder] = rcube_charset_convert($folder, 'UTF7-IMAP');
59c216 4589         }
A 4590
4591         // sort folders and place defaults on the top
4592         asort($folders, SORT_LOCALE_STRING);
4593         ksort($a_defaults);
4594         $folders = array_merge($a_defaults, array_keys($folders));
4595
c43517 4596         // finally we must rebuild the list to move
59c216 4597         // subfolders of default folders to their place...
A 4598         // ...also do this for the rest of folders because
4599         // asort() is not properly sorting case sensitive names
4600         while (list($key, $folder) = each($folders)) {
c43517 4601             // set the type of folder name variable (#1485527)
59c216 4602             $a_out[] = (string) $folder;
A 4603             unset($folders[$key]);
c43517 4604             $this->_rsort($folder, $delimiter, $folders, $a_out);
59c216 4605         }
A 4606
4607         return $a_out;
4608     }
4609
4610
4611     /**
4612      * @access private
4613      */
4614     private function _rsort($folder, $delimiter, &$list, &$out)
4615     {
4616         while (list($key, $name) = each($list)) {
4617             if (strpos($name, $folder.$delimiter) === 0) {
c43517 4618                 // set the type of folder name variable (#1485527)
59c216 4619                 $out[] = (string) $name;
A 4620                 unset($list[$key]);
4621                 $this->_rsort($name, $delimiter, $list, $out);
4622             }
4623         }
c43517 4624         reset($list);
59c216 4625     }
A 4626
4627
4628     /**
d08333 4629      * @param int    $uid     Message UID
A 4630      * @param string $mailbox Mailbox name
29983c 4631      * @return int Message (sequence) ID
59c216 4632      * @access private
A 4633      */
d08333 4634     private function _uid2id($uid, $mailbox=NULL)
59c216 4635     {
d08333 4636         if (!strlen($mailbox)) {
A 4637             $mailbox = $this->mailbox;
29983c 4638         }
59c216 4639
d08333 4640         if (!isset($this->uid_id_map[$mailbox][$uid])) {
A 4641             if (!($id = $this->get_cache_uid2id($mailbox.'.msg', $uid)))
4642                 $id = $this->conn->UID2ID($mailbox, $uid);
4643
4644             $this->uid_id_map[$mailbox][$uid] = $id;
4645         }
4646
4647         return $this->uid_id_map[$mailbox][$uid];
59c216 4648     }
A 4649
29983c 4650
59c216 4651     /**
d08333 4652      * @param int    $id      Message (sequence) ID
A 4653      * @param string $mailbox Mailbox name
4654      *
29983c 4655      * @return int Message UID
59c216 4656      * @access private
A 4657      */
d08333 4658     private function _id2uid($id, $mailbox=null)
59c216 4659     {
d08333 4660         if (!strlen($mailbox)) {
A 4661             $mailbox = $this->mailbox;
4662         }
59c216 4663
d08333 4664         if ($uid = array_search($id, (array)$this->uid_id_map[$mailbox])) {
59c216 4665             return $uid;
d08333 4666         }
59c216 4667
d08333 4668         if (!($uid = $this->get_cache_id2uid($mailbox.'.msg', $id))) {
A 4669             $uid = $this->conn->ID2UID($mailbox, $id);
4670         }
29983c 4671
d08333 4672         $this->uid_id_map[$mailbox][$uid] = $id;
c43517 4673
59c216 4674         return $uid;
A 4675     }
4676
4677
4678     /**
4679      * Subscribe/unsubscribe a list of mailboxes and update local cache
4680      * @access private
4681      */
4682     private function _change_subscription($a_mboxes, $mode)
4683     {
4684         $updated = false;
4685
4686         if (is_array($a_mboxes))
d08333 4687             foreach ($a_mboxes as $i => $mailbox) {
59c216 4688                 $a_mboxes[$i] = $mailbox;
A 4689
d08333 4690                 if ($mode == 'subscribe')
59c216 4691                     $updated = $this->conn->subscribe($mailbox);
d08333 4692                 else if ($mode == 'unsubscribe')
59c216 4693                     $updated = $this->conn->unsubscribe($mailbox);
A 4694             }
4695
4696         // get cached mailbox list
4697         if ($updated) {
4698             $a_mailbox_cache = $this->get_cache('mailboxes');
4699             if (!is_array($a_mailbox_cache))
4700                 return $updated;
4701
4702             // modify cached list
d08333 4703             if ($mode == 'subscribe')
59c216 4704                 $a_mailbox_cache = array_merge($a_mailbox_cache, $a_mboxes);
d08333 4705             else if ($mode == 'unsubscribe')
59c216 4706                 $a_mailbox_cache = array_diff($a_mailbox_cache, $a_mboxes);
A 4707
4708             // write mailboxlist to cache
4709             $this->update_cache('mailboxes', $this->_sort_mailbox_list($a_mailbox_cache));
4710         }
4711
4712         return $updated;
4713     }
4714
4715
4716     /**
4717      * Increde/decrese messagecount for a specific mailbox
4718      * @access private
4719      */
d08333 4720     private function _set_messagecount($mailbox, $mode, $increment)
59c216 4721     {
A 4722         $mode = strtoupper($mode);
4723         $a_mailbox_cache = $this->get_cache('messagecount');
c43517 4724
59c216 4725         if (!is_array($a_mailbox_cache[$mailbox]) || !isset($a_mailbox_cache[$mailbox][$mode]) || !is_numeric($increment))
A 4726             return false;
c43517 4727
59c216 4728         // add incremental value to messagecount
A 4729         $a_mailbox_cache[$mailbox][$mode] += $increment;
c43517 4730
59c216 4731         // there's something wrong, delete from cache
A 4732         if ($a_mailbox_cache[$mailbox][$mode] < 0)
4733             unset($a_mailbox_cache[$mailbox][$mode]);
4734
4735         // write back to cache
4736         $this->update_cache('messagecount', $a_mailbox_cache);
c43517 4737
59c216 4738         return true;
A 4739     }
4740
4741
4742     /**
4743      * Remove messagecount of a specific mailbox from cache
4744      * @access private
4745      */
d08333 4746     private function _clear_messagecount($mailbox, $mode=null)
59c216 4747     {
A 4748         $a_mailbox_cache = $this->get_cache('messagecount');
4749
4750         if (is_array($a_mailbox_cache[$mailbox])) {
c309cd 4751             if ($mode) {
A 4752                 unset($a_mailbox_cache[$mailbox][$mode]);
4753             }
4754             else {
4755                 unset($a_mailbox_cache[$mailbox]);
4756             }
59c216 4757             $this->update_cache('messagecount', $a_mailbox_cache);
A 4758         }
4759     }
4760
4761
4762     /**
4763      * Split RFC822 header string into an associative array
4764      * @access private
4765      */
4766     private function _parse_headers($headers)
4767     {
4768         $a_headers = array();
4769         $headers = preg_replace('/\r?\n(\t| )+/', ' ', $headers);
4770         $lines = explode("\n", $headers);
4771         $c = count($lines);
4772
4773         for ($i=0; $i<$c; $i++) {
4774             if ($p = strpos($lines[$i], ': ')) {
4775                 $field = strtolower(substr($lines[$i], 0, $p));
4776                 $value = trim(substr($lines[$i], $p+1));
4777                 if (!empty($value))
4778                     $a_headers[$field] = $value;
4779             }
4780         }
c43517 4781
59c216 4782         return $a_headers;
A 4783     }
4784
4785
4786     /**
4787      * @access private
4788      */
4789     private function _parse_address_list($str, $decode=true)
4790     {
4791         // remove any newlines and carriage returns before
6c68cb 4792         $str = preg_replace('/\r?\n(\s|\t)?/', ' ', $str);
A 4793
4794         // extract list items, remove comments
4795         $str = self::explode_header_string(',;', $str, true);
59c216 4796         $result = array();
A 4797
6c68cb 4798         foreach ($str as $key => $val) {
435c31 4799             $name    = '';
A 4800             $address = '';
4801             $val     = trim($val);
59c216 4802
435c31 4803             if (preg_match('/(.*)<(\S+@\S+)>$/', $val, $m)) {
A 4804                 $address = $m[2];
4805                 $name    = trim($m[1]);
4806             }
4807             else if (preg_match('/^(\S+@\S+)$/', $val, $m)) {
4808                 $address = $m[1];
4809                 $name    = '';
4810             }
4811             else {
4812                 $name = $val;
59c216 4813             }
c43517 4814
435c31 4815             // dequote and/or decode name
A 4816             if ($name) {
4817                 if ($name[0] == '"') {
4818                     $name = substr($name, 1, -1);
4819                     $name = stripslashes($name);
4820                 }
7bdd3e 4821                 if ($decode) {
435c31 4822                     $name = $this->decode_header($name);
A 4823                 }
4824             }
4825
4826             if (!$address && $name) {
4827                 $address = $name;
4828             }
4829
4830             if ($address) {
4831                 $result[$key] = array('name' => $name, 'address' => $address);
4832             }
59c216 4833         }
c43517 4834
59c216 4835         return $result;
4e17e6 4836     }
6d969b 4837
7f1da4 4838
A 4839     /**
6c68cb 4840      * Explodes header (e.g. address-list) string into array of strings
A 4841      * using specified separator characters with proper handling
4842      * of quoted-strings and comments (RFC2822)
4843      *
4844      * @param string $separator       String containing separator characters
4845      * @param string $str             Header string
4846      * @param bool   $remove_comments Enable to remove comments
4847      *
4848      * @return array Header items
4849      */
4850     static function explode_header_string($separator, $str, $remove_comments=false)
4851     {
4852         $length  = strlen($str);
4853         $result  = array();
4854         $quoted  = false;
4855         $comment = 0;
4856         $out     = '';
4857
4858         for ($i=0; $i<$length; $i++) {
4859             // we're inside a quoted string
4860             if ($quoted) {
4861                 if ($str[$i] == '"') {
4862                     $quoted = false;
4863                 }
4864                 else if ($str[$i] == '\\') {
4865                     if ($comment <= 0) {
4866                         $out .= '\\';
4867                     }
4868                     $i++;
4869                 }
4870             }
4871             // we're inside a comment string
4872             else if ($comment > 0) {
4873                     if ($str[$i] == ')') {
4874                         $comment--;
4875                     }
4876                     else if ($str[$i] == '(') {
4877                         $comment++;
4878                     }
4879                     else if ($str[$i] == '\\') {
4880                         $i++;
4881                     }
4882                     continue;
4883             }
4884             // separator, add to result array
4885             else if (strpos($separator, $str[$i]) !== false) {
4886                     if ($out) {
4887                         $result[] = $out;
4888                     }
4889                     $out = '';
4890                     continue;
4891             }
4892             // start of quoted string
4893             else if ($str[$i] == '"') {
4894                     $quoted = true;
4895             }
4896             // start of comment
4897             else if ($remove_comments && $str[$i] == '(') {
4898                     $comment++;
4899             }
4900
4901             if ($comment <= 0) {
4902                 $out .= $str[$i];
4903             }
4904         }
4905
4906         if ($out && $comment <= 0) {
4907             $result[] = $out;
4908         }
4909
4910         return $result;
4911     }
4912
4913
4914     /**
7f1da4 4915      * This is our own debug handler for the IMAP connection
A 4916      * @access public
4917      */
4918     public function debug_handler(&$imap, $message)
4919     {
4920         write_log('imap', $message);
4921     }
4922
6d969b 4923 }  // end class rcube_imap
4e17e6 4924
8d4bcd 4925
T 4926 /**
4927  * Class representing a message part
6d969b 4928  *
T 4929  * @package Mail
8d4bcd 4930  */
T 4931 class rcube_message_part
4932 {
59c216 4933     var $mime_id = '';
A 4934     var $ctype_primary = 'text';
4935     var $ctype_secondary = 'plain';
4936     var $mimetype = 'text/plain';
4937     var $disposition = '';
4938     var $filename = '';
4939     var $encoding = '8bit';
4940     var $charset = '';
4941     var $size = 0;
4942     var $headers = array();
4943     var $d_parameters = array();
4944     var $ctype_parameters = array();
8d4bcd 4945
59c216 4946     function __clone()
A 4947     {
4948         if (isset($this->parts))
4949             foreach ($this->parts as $idx => $part)
4950                 if (is_object($part))
4951                     $this->parts[$idx] = clone $part;
4952     }
8d4bcd 4953 }
4e17e6 4954
T 4955
7e93ff 4956 /**
59c216 4957  * Class for sorting an array of rcube_mail_header objects in a predetermined order.
7e93ff 4958  *
6d969b 4959  * @package Mail
7e93ff 4960  * @author Eric Stadtherr
T 4961  */
4962 class rcube_header_sorter
4963 {
59c216 4964     var $sequence_numbers = array();
c43517 4965
59c216 4966     /**
A 4967      * Set the predetermined sort order.
4968      *
5c461b 4969      * @param array $seqnums Numerically indexed array of IMAP message sequence numbers
59c216 4970      */
A 4971     function set_sequence_numbers($seqnums)
4972     {
4973         $this->sequence_numbers = array_flip($seqnums);
4974     }
4975
4976     /**
4977      * Sort the array of header objects
4978      *
5c461b 4979      * @param array $headers Array of rcube_mail_header objects indexed by UID
59c216 4980      */
A 4981     function sort_headers(&$headers)
4982     {
4983         /*
4984         * uksort would work if the keys were the sequence number, but unfortunately
4985         * the keys are the UIDs.  We'll use uasort instead and dereference the value
4986         * to get the sequence number (in the "id" field).
c43517 4987         *
A 4988         * uksort($headers, array($this, "compare_seqnums"));
59c216 4989         */
A 4990         uasort($headers, array($this, "compare_seqnums"));
4991     }
c43517 4992
59c216 4993     /**
A 4994      * Sort method called by uasort()
5c461b 4995      *
A 4996      * @param rcube_mail_header $a
4997      * @param rcube_mail_header $b
59c216 4998      */
A 4999     function compare_seqnums($a, $b)
5000     {
5001         // First get the sequence number from the header object (the 'id' field).
5002         $seqa = $a->id;
5003         $seqb = $b->id;
c43517 5004
59c216 5005         // then find each sequence number in my ordered list
A 5006         $posa = isset($this->sequence_numbers[$seqa]) ? intval($this->sequence_numbers[$seqa]) : -1;
5007         $posb = isset($this->sequence_numbers[$seqb]) ? intval($this->sequence_numbers[$seqb]) : -1;
c43517 5008
59c216 5009         // return the relative position as the comparison value
A 5010         return $posa - $posb;
5011     }
7e93ff 5012 }