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