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