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