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