alecpl
2010-08-09 ecc28ce02fcfd3eaf0356c0e70dd6b929bc81771
commit | author | age
4e17e6 1 <?php
T 2
3 /*
4  +-----------------------------------------------------------------------+
47124c 5  | program/include/rcube_imap.php                                        |
4e17e6 6  |                                                                       |
T 7  | This file is part of the RoundCube Webmail client                     |
f52c93 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;
141             raise_error(array('code' => 403, 'type' => 'imap',
142                 'file' => __FILE__, 'line' => __LINE__,
143                 'message' => $this->conn->error), true, false);
144         }
145
94a6c6 146         return false;
4e17e6 147     }
T 148
149
59c216 150     /**
A 151      * Close IMAP connection
152      * Usually done on script shutdown
153      *
154      * @access public
155      */
156     function close()
c43517 157     {
59c216 158         if ($this->conn && $this->conn->connected())
A 159             $this->conn->close();
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)) {
2242             if ($is_file) {
2243                 $separator = rcmail::get_instance()->config->header_delimiter();
2244                 $saved = $this->conn->appendFromFile($mailbox, $message,
2245                     $headers, $separator.$separator);
2246             }
2247             else
2248                 $saved = $this->conn->append($mailbox, $message);
4e17e6 2249         }
f8a846 2250
59c216 2251         if ($saved) {
A 2252             // increase messagecount of the target mailbox
2253             $this->_set_messagecount($mailbox, 'ALL', 1);
8d4bcd 2254         }
59c216 2255
A 2256         return $saved;
8d4bcd 2257     }
T 2258
2259
59c216 2260     /**
A 2261      * Move a message from one mailbox to another
2262      *
2263      * @param mixed  Message UIDs as array or comma-separated string, or '*'
2264      * @param string Target mailbox
2265      * @param string Source mailbox
2266      * @return boolean True on success, False on error
2267      */
2268     function move_message($uids, $to_mbox, $from_mbox='')
4e17e6 2269     {
59c216 2270         $fbox = $from_mbox;
A 2271         $tbox = $to_mbox;
2272         $to_mbox = $this->mod_mailbox($to_mbox);
2273         $from_mbox = $from_mbox ? $this->mod_mailbox($from_mbox) : $this->mailbox;
c58c0a 2274
59c216 2275         list($uids, $all_mode) = $this->_parse_uids($uids, $from_mbox);
41fa0b 2276
59c216 2277         // exit if no message uids are specified
A 2278         if (empty($uids))
2279             return false;
2280
2281         // make sure mailbox exists
2282         if ($to_mbox != 'INBOX' && !$this->mailbox_exists($tbox, true)) {
2283             if (in_array($tbox, $this->default_folders))
2284                 $this->create_mailbox($tbox, true);
2285             else
2286                 return false;
2287         }
2288
2289         // flag messages as read before moving them
2290         $config = rcmail::get_instance()->config;
2291         if ($config->get('read_when_deleted') && $tbox == $config->get('trash_mbox')) {
2292             // don't flush cache (4th argument)
2293             $this->set_flag($uids, 'SEEN', $fbox, true);
2294         }
2295
2296         // move messages
2297         $move = $this->conn->move($uids, $from_mbox, $to_mbox);
2298         $moved = !($move === false || $move < 0);
2299
2300         // send expunge command in order to have the moved message
2301         // really deleted from the source mailbox
2302         if ($moved) {
2303             $this->_expunge($from_mbox, false, $uids);
2304             $this->_clear_messagecount($from_mbox);
2305             $this->_clear_messagecount($to_mbox);
2306         }
2307         // moving failed
2308         else if ($config->get('delete_always', false) && $tbox == $config->get('trash_mbox')) {
2309             $moved = $this->delete_message($uids, $fbox);
2310         }
2311
2312         if ($moved) {
2313             // unset threads internal cache
2314             unset($this->icache['threads']);
2315
2316             // remove message ids from search set
2317             if ($this->search_set && $from_mbox == $this->mailbox) {
2318                 // threads are too complicated to just remove messages from set
2319                 if ($this->search_threads || $all_mode)
2320                     $this->refresh_search();
2321                 else {
2322                     $uids = explode(',', $uids);
2323                     foreach ($uids as $uid)
2324                         $a_mids[] = $this->_uid2id($uid, $from_mbox);
2325                     $this->search_set = array_diff($this->search_set, $a_mids);
2326                 }
2327             }
2328
2329             // update cached message headers
2330             $cache_key = $from_mbox.'.msg';
2331             if ($all_mode || ($start_index = $this->get_message_cache_index_min($cache_key, $uids))) {
2332                 // clear cache from the lowest index on
2333                 $this->clear_message_cache($cache_key, $all_mode ? 1 : $start_index);
2334             }
2335         }
2336
2337         return $moved;
2338     }
2339
2340
2341     /**
2342      * Copy a message from one mailbox to another
2343      *
2344      * @param mixed  Message UIDs as array or comma-separated string, or '*'
2345      * @param string Target mailbox
2346      * @param string Source mailbox
2347      * @return boolean True on success, False on error
2348      */
2349     function copy_message($uids, $to_mbox, $from_mbox='')
2350     {
2351         $fbox = $from_mbox;
2352         $tbox = $to_mbox;
2353         $to_mbox = $this->mod_mailbox($to_mbox);
2354         $from_mbox = $from_mbox ? $this->mod_mailbox($from_mbox) : $this->mailbox;
2355
2356         list($uids, $all_mode) = $this->_parse_uids($uids, $from_mbox);
2357
2358         // exit if no message uids are specified
2359         if (empty($uids))
2360             return false;
2361
2362         // make sure mailbox exists
2363         if ($to_mbox != 'INBOX' && !$this->mailbox_exists($tbox, true)) {
2364             if (in_array($tbox, $this->default_folders))
2365                 $this->create_mailbox($tbox, true);
2366             else
2367                 return false;
2368         }
2369
2370         // copy messages
2371         $copy = $this->conn->copy($uids, $from_mbox, $to_mbox);
2372         $copied = !($copy === false || $copy < 0);
2373
2374         if ($copied) {
2375             $this->_clear_messagecount($to_mbox);
2376         }
2377
2378         return $copied;
2379     }
2380
2381
2382     /**
2383      * Mark messages as deleted and expunge mailbox
2384      *
2385      * @param mixed  Message UIDs as array or comma-separated string, or '*'
2386      * @param string Source mailbox
2387      * @return boolean True on success, False on error
2388      */
2389     function delete_message($uids, $mbox_name='')
2390     {
2391         $mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
2392
2393         list($uids, $all_mode) = $this->_parse_uids($uids, $mailbox);
2394
2395         // exit if no message uids are specified
2396         if (empty($uids))
2397             return false;
2398
2399         $deleted = $this->conn->delete($mailbox, $uids);
2400
2401         if ($deleted) {
2402             // send expunge command in order to have the deleted message
2403             // really deleted from the mailbox
2404             $this->_expunge($mailbox, false, $uids);
2405             $this->_clear_messagecount($mailbox);
2406             unset($this->uid_id_map[$mailbox]);
2407
2408             // unset threads internal cache
2409             unset($this->icache['threads']);
c43517 2410
59c216 2411             // remove message ids from search set
A 2412             if ($this->search_set && $mailbox == $this->mailbox) {
2413                 // threads are too complicated to just remove messages from set
2414                 if ($this->search_threads || $all_mode)
2415                     $this->refresh_search();
2416                 else {
2417                     $uids = explode(',', $uids);
2418                     foreach ($uids as $uid)
2419                         $a_mids[] = $this->_uid2id($uid, $mailbox);
2420                     $this->search_set = array_diff($this->search_set, $a_mids);
2421                 }
2422             }
2423
2424             // remove deleted messages from cache
2425             $cache_key = $mailbox.'.msg';
2426             if ($all_mode || ($start_index = $this->get_message_cache_index_min($cache_key, $uids))) {
2427                 // clear cache from the lowest index on
2428                 $this->clear_message_cache($cache_key, $all_mode ? 1 : $start_index);
2429             }
2430         }
2431
2432         return $deleted;
2433     }
2434
2435
2436     /**
2437      * Clear all messages in a specific mailbox
2438      *
2439      * @param string Mailbox name
2440      * @return int Above 0 on success
2441      */
2442     function clear_mailbox($mbox_name=NULL)
2443     {
2444         $mailbox = !empty($mbox_name) ? $this->mod_mailbox($mbox_name) : $this->mailbox;
2445         $msg_count = $this->_messagecount($mailbox, 'ALL');
c43517 2446
59c216 2447         if (!$msg_count) {
A 2448             return 0;
4e17e6 2449         }
c43517 2450
59c216 2451         $cleared = $this->conn->clearFolder($mailbox);
c43517 2452
59c216 2453         // make sure the message count cache is cleared as well
A 2454         if ($cleared) {
c43517 2455             $this->clear_message_cache($mailbox.'.msg');
59c216 2456             $a_mailbox_cache = $this->get_cache('messagecount');
A 2457             unset($a_mailbox_cache[$mailbox]);
2458             $this->update_cache('messagecount', $a_mailbox_cache);
2459         }
c43517 2460
59c216 2461         return $cleared;
A 2462     }
2463
2464
2465     /**
2466      * Send IMAP expunge command and clear cache
2467      *
2468      * @param string Mailbox name
2469      * @param boolean False if cache should not be cleared
2470      * @return boolean True on success
2471      */
2472     function expunge($mbox_name='', $clear_cache=true)
2473     {
2474         $mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
2475         return $this->_expunge($mailbox, $clear_cache);
2476     }
2477
2478
2479     /**
2480      * Send IMAP expunge command and clear cache
2481      *
2482      * @param string      Mailbox name
2483      * @param boolean  False if cache should not be cleared
2484      * @param mixed    Message UIDs as array or comma-separated string, or '*'
2485      * @return boolean True on success
2486      * @access private
2487      * @see rcube_imap::expunge()
2488      */
2489     private function _expunge($mailbox, $clear_cache=true, $uids=NULL)
2490     {
c43517 2491         if ($uids && $this->get_capability('UIDPLUS'))
59c216 2492             $a_uids = is_array($uids) ? join(',', $uids) : $uids;
A 2493         else
2494             $a_uids = NULL;
2495
2496         $result = $this->conn->expunge($mailbox, $a_uids);
2497
2498         if ($result>=0 && $clear_cache) {
2499             $this->clear_message_cache($mailbox.'.msg');
2500             $this->_clear_messagecount($mailbox);
2501         }
c43517 2502
59c216 2503         return $result;
A 2504     }
2505
2506
2507     /**
2508      * Parse message UIDs input
2509      *
2510      * @param mixed  UIDs array or comma-separated list or '*' or '1:*'
2511      * @param string Mailbox name
c43517 2512      * @return array Two elements array with UIDs converted to list and ALL flag
59c216 2513      * @access private
A 2514      */
2515     private function _parse_uids($uids, $mailbox)
2516     {
2517         if ($uids === '*' || $uids === '1:*') {
2518             if (empty($this->search_set)) {
2519                 $uids = '1:*';
2520                 $all = true;
2521             }
2522             // get UIDs from current search set
2523             // @TODO: skip fetchUIDs() and work with IDs instead of UIDs (?)
2524             else {
2525                 if ($this->search_threads)
2526                     $uids = $this->conn->fetchUIDs($mailbox, array_keys($this->search_set['depth']));
2527                 else
2528                     $uids = $this->conn->fetchUIDs($mailbox, $this->search_set);
c43517 2529
59c216 2530                 // save ID-to-UID mapping in local cache
A 2531                 if (is_array($uids))
2532                     foreach ($uids as $id => $uid)
2533                         $this->uid_id_map[$mailbox][$uid] = $id;
2534
2535                 $uids = join(',', $uids);
2536             }
2537         }
2538         else {
2539             if (is_array($uids))
2540                 $uids = join(',', $uids);
2541
2542             if (preg_match('/[^0-9,]/', $uids))
2543                 $uids = '';
2544         }
2545
2546         return array($uids, (bool) $all);
2547     }
2548
2549
2550     /**
2551      * Translate UID to message ID
2552      *
2553      * @param int    Message UID
2554      * @param string Mailbox name
2555      * @return int   Message ID
2556      */
c43517 2557     function get_id($uid, $mbox_name=NULL)
59c216 2558     {
A 2559         $mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
2560         return $this->_uid2id($uid, $mailbox);
2561     }
2562
2563
2564     /**
2565      * Translate message number to UID
2566      *
2567      * @param int    Message ID
2568      * @param string Mailbox name
2569      * @return int   Message UID
2570      */
2571     function get_uid($id,$mbox_name=NULL)
2572     {
2573         $mailbox = $mbox_name ? $this->mod_mailbox($mbox_name) : $this->mailbox;
2574         return $this->_id2uid($id, $mailbox);
2575     }
2576
2577
2578
2579     /* --------------------------------
2580      *        folder managment
2581      * --------------------------------*/
2582
2583     /**
6f4e7d 2584      * Public method for listing subscribed folders
59c216 2585      *
A 2586      * Converts mailbox name with root dir first
2587      *
2588      * @param   string  Optional root folder
2589      * @param   string  Optional filter for mailbox listing
2590      * @return  array   List of mailboxes/folders
2591      * @access  public
2592      */
2593     function list_mailboxes($root='', $filter='*')
2594     {
2595         $a_out = array();
2596         $a_mboxes = $this->_list_mailboxes($root, $filter);
2597
f0485a 2598         foreach ($a_mboxes as $idx => $mbox_row) {
A 2599             if ($name = $this->mod_mailbox($mbox_row, 'out'))
59c216 2600                 $a_out[] = $name;
f0485a 2601             unset($a_mboxes[$idx]);
59c216 2602         }
A 2603
2604         // INBOX should always be available
2605         if (!in_array('INBOX', $a_out))
2606             array_unshift($a_out, 'INBOX');
2607
2608         // sort mailboxes
2609         $a_out = $this->_sort_mailbox_list($a_out);
2610
2611         return $a_out;
2612     }
2613
2614
2615     /**
2616      * Private method for mailbox listing
2617      *
2618      * @return  array   List of mailboxes/folders
2619      * @see     rcube_imap::list_mailboxes()
2620      * @access  private
2621      */
2622     private function _list_mailboxes($root='', $filter='*')
2623     {
c43517 2624         // get cached folder list
59c216 2625         $a_mboxes = $this->get_cache('mailboxes');
A 2626         if (is_array($a_mboxes))
2627             return $a_mboxes;
2628
6f4e7d 2629         $a_defaults = $a_out = array();
A 2630
59c216 2631         // Give plugins a chance to provide a list of mailboxes
e6ce00 2632         $data = rcmail::get_instance()->plugins->exec_hook('mailboxes_list',
6f4e7d 2633             array('root' => $root, 'filter' => $filter, 'mode' => 'LSUB'));
c43517 2634
59c216 2635         if (isset($data['folders'])) {
A 2636             $a_folders = $data['folders'];
2637         }
2638         else {
2639             // retrieve list of folders from IMAP server
2640             $a_folders = $this->conn->listSubscribed($this->mod_mailbox($root), $filter);
2641         }
c43517 2642
59c216 2643         if (!is_array($a_folders) || !sizeof($a_folders))
A 2644             $a_folders = array();
2645
2646         // write mailboxlist to cache
2647         $this->update_cache('mailboxes', $a_folders);
c43517 2648
59c216 2649         return $a_folders;
A 2650     }
2651
2652
2653     /**
2654      * Get a list of all folders available on the IMAP server
c43517 2655      *
59c216 2656      * @param string IMAP root dir
6f4e7d 2657      * @param string Optional filter for mailbox listing
59c216 2658      * @return array Indexed array with folder names
A 2659      */
6f4e7d 2660     function list_unsubscribed($root='', $filter='*')
59c216 2661     {
6f4e7d 2662         // Give plugins a chance to provide a list of mailboxes
e6ce00 2663         $data = rcmail::get_instance()->plugins->exec_hook('mailboxes_list',
6f4e7d 2664             array('root' => $root, 'filter' => $filter, 'mode' => 'LIST'));
c43517 2665
6f4e7d 2666         if (isset($data['folders'])) {
A 2667             $a_mboxes = $data['folders'];
2668         }
2669         else {
2670             // retrieve list of folders from IMAP server
2671             $a_mboxes = $this->conn->listMailboxes($this->mod_mailbox($root), $filter);
2672         }
64e3e8 2673
6f4e7d 2674         $a_folders = array();
A 2675         if (!is_array($a_mboxes))
2676             $a_mboxes = array();
59c216 2677
A 2678         // modify names with root dir
f0485a 2679         foreach ($a_mboxes as $idx => $mbox_name) {
a44682 2680             if ($name = $this->mod_mailbox($mbox_name, 'out'))
59c216 2681                 $a_folders[] = $name;
f0485a 2682             unset($a_mboxes[$idx]);
59c216 2683         }
f0485a 2684
A 2685         // INBOX should always be available
2686         if (!in_array('INBOX', $a_folders))
2687             array_unshift($a_folders, 'INBOX');
59c216 2688
A 2689         // filter folders and sort them
a44682 2690         $a_folders = $this->_sort_mailbox_list($a_folders);
A 2691         return $a_folders;
59c216 2692     }
A 2693
2694
2695     /**
2696      * Get mailbox quota information
2697      * added by Nuny
c43517 2698      *
59c216 2699      * @return mixed Quota info or False if not supported
A 2700      */
2701     function get_quota()
2702     {
2703         if ($this->get_capability('QUOTA'))
2704             return $this->conn->getQuota();
c43517 2705
59c216 2706         return false;
A 2707     }
2708
2709
2710     /**
2711      * Subscribe to a specific mailbox(es)
2712      *
2713      * @param array Mailbox name(s)
2714      * @return boolean True on success
c43517 2715      */
59c216 2716     function subscribe($a_mboxes)
A 2717     {
2718         if (!is_array($a_mboxes))
2719             $a_mboxes = array($a_mboxes);
2720
2721         // let this common function do the main work
2722         return $this->_change_subscription($a_mboxes, 'subscribe');
2723     }
2724
2725
2726     /**
2727      * Unsubscribe mailboxes
2728      *
2729      * @param array Mailbox name(s)
2730      * @return boolean True on success
2731      */
2732     function unsubscribe($a_mboxes)
2733     {
2734         if (!is_array($a_mboxes))
2735             $a_mboxes = array($a_mboxes);
2736
2737         // let this common function do the main work
2738         return $this->_change_subscription($a_mboxes, 'unsubscribe');
2739     }
2740
2741
2742     /**
2743      * Create a new mailbox on the server and register it in local cache
2744      *
2745      * @param string  New mailbox name (as utf-7 string)
2746      * @param boolean True if the new mailbox should be subscribed
2747      * @param string  Name of the created mailbox, false on error
2748      */
2749     function create_mailbox($name, $subscribe=false)
2750     {
2751         $result = false;
c43517 2752
59c216 2753         // reduce mailbox name to 100 chars
A 2754         $name = substr($name, 0, 100);
2755         $abs_name = $this->mod_mailbox($name);
2756         $result = $this->conn->createFolder($abs_name);
2757
2758         // try to subscribe it
2759         if ($result && $subscribe)
2760             $this->subscribe($name);
2761
2762         return $result ? $name : false;
2763     }
2764
2765
2766     /**
2767      * Set a new name to an existing mailbox
2768      *
2769      * @param string Mailbox to rename (as utf-7 string)
2770      * @param string New mailbox name (as utf-7 string)
2771      * @return string Name of the renames mailbox, False on error
2772      */
2773     function rename_mailbox($mbox_name, $new_name)
2774     {
2775         $result = false;
2776
2777         // encode mailbox name and reduce it to 100 chars
2778         $name = substr($new_name, 0, 100);
2779
2780         // make absolute path
2781         $mailbox = $this->mod_mailbox($mbox_name);
2782         $abs_name = $this->mod_mailbox($name);
c43517 2783
59c216 2784         // check if mailbox is subscribed
A 2785         $a_subscribed = $this->_list_mailboxes();
2786         $subscribed = in_array($mailbox, $a_subscribed);
c43517 2787
59c216 2788         // unsubscribe folder
A 2789         if ($subscribed)
2790             $this->conn->unsubscribe($mailbox);
2791
2792         if (strlen($abs_name))
2793             $result = $this->conn->renameFolder($mailbox, $abs_name);
2794
2795         if ($result) {
2796             $delm = $this->get_hierarchy_delimiter();
677e1f 2797
59c216 2798             // check if mailbox children are subscribed
A 2799             foreach ($a_subscribed as $c_subscribed)
2800                 if (preg_match('/^'.preg_quote($mailbox.$delm, '/').'/', $c_subscribed)) {
2801                     $this->conn->unsubscribe($c_subscribed);
2802                     $this->conn->subscribe(preg_replace('/^'.preg_quote($mailbox, '/').'/',
2803                         $abs_name, $c_subscribed));
2804                 }
2805
2806             // clear cache
2807             $this->clear_message_cache($mailbox.'.msg');
677e1f 2808             $this->clear_cache('mailboxes');
59c216 2809         }
A 2810
2811         // try to subscribe it
2812         if ($result && $subscribed)
2813             $this->conn->subscribe($abs_name);
2814
2815         return $result ? $name : false;
2816     }
2817
2818
2819     /**
2820      * Remove mailboxes from server
2821      *
2822      * @param string Mailbox name(s) string/array
2823      * @return boolean True on success
2824      */
2825     function delete_mailbox($mbox_name)
2826     {
2827         $deleted = false;
2828
2829         if (is_array($mbox_name))
2830             $a_mboxes = $mbox_name;
2831         else if (is_string($mbox_name) && strlen($mbox_name))
2832             $a_mboxes = explode(',', $mbox_name);
2833
2834         if (is_array($a_mboxes)) {
2835             foreach ($a_mboxes as $mbox_name) {
2836                 $mailbox = $this->mod_mailbox($mbox_name);
2837                 $sub_mboxes = $this->conn->listMailboxes($this->mod_mailbox(''),
f0485a 2838                     $mbox_name . $this->delimiter . '*');
59c216 2839
A 2840                 // unsubscribe mailbox before deleting
2841                 $this->conn->unsubscribe($mailbox);
2842
2843                 // send delete command to server
2844                 $result = $this->conn->deleteFolder($mailbox);
c43517 2845                 if ($result) {
59c216 2846                     $deleted = true;
A 2847                     $this->clear_message_cache($mailbox.'.msg');
2848                 }
bb8012 2849
59c216 2850                 foreach ($sub_mboxes as $c_mbox) {
A 2851                     if ($c_mbox != 'INBOX') {
2852                         $this->conn->unsubscribe($c_mbox);
2853                         $result = $this->conn->deleteFolder($c_mbox);
c43517 2854                         if ($result) {
59c216 2855                             $deleted = true;
A 2856                             $this->clear_message_cache($c_mbox.'.msg');
2857                         }
2858                     }
2859                 }
2860             }
2861         }
2862
2863         // clear mailboxlist cache
2864         if ($deleted)
2865             $this->clear_cache('mailboxes');
2866
2867         return $deleted;
2868     }
2869
2870
2871     /**
2872      * Create all folders specified as default
2873      */
2874     function create_default_folders()
2875     {
2876         // create default folders if they do not exist
2877         foreach ($this->default_folders as $folder) {
2878             if (!$this->mailbox_exists($folder))
2879                 $this->create_mailbox($folder, true);
2880             else if (!$this->mailbox_exists($folder, true))
2881                 $this->subscribe($folder);
2882         }
2883     }
2884
2885
2886     /**
2887      * Checks if folder exists and is subscribed
2888      *
2889      * @param string   Folder name
2890      * @param boolean  Enable subscription checking
2891      * @return boolean TRUE or FALSE
2892      */
2893     function mailbox_exists($mbox_name, $subscription=false)
2894     {
2895         if ($mbox_name) {
2896             if ($mbox_name == 'INBOX')
2897                 return true;
2898
16378f 2899             $key = $subscription ? 'subscribed' : 'existing';
A 2900             if (is_array($this->icache[$key]) && in_array($mbox_name, $this->icache[$key]))
2901                 return true;
2902
59c216 2903             if ($subscription) {
f0485a 2904                 $a_folders = $this->conn->listSubscribed($this->mod_mailbox(''), $mbox_name);
59c216 2905             }
A 2906             else {
d824ac 2907                 $a_folders = $this->conn->listMailboxes($this->mod_mailbox(''), $mbox_name);
f0485a 2908             }
c43517 2909
f0485a 2910             if (is_array($a_folders) && in_array($this->mod_mailbox($mbox_name), $a_folders)) {
16378f 2911                 $this->icache[$key][] = $mbox_name;
59c216 2912                 return true;
A 2913             }
2914         }
2915
2916         return false;
2917     }
2918
2919
2920     /**
2921      * Modify folder name for input/output according to root dir and namespace
2922      *
2923      * @param string  Folder name
2924      * @param string  Mode
2925      * @return string Folder name
2926      */
2927     function mod_mailbox($mbox_name, $mode='in')
2928     {
2929         if ($mbox_name == 'INBOX')
2930             return $mbox_name;
2931
2932         if (!empty($this->root_dir)) {
2933             if ($mode=='in')
2934                 $mbox_name = $this->root_dir.$this->delimiter.$mbox_name;
2935             else if (!empty($mbox_name)) // $mode=='out'
2936                 $mbox_name = substr($mbox_name, strlen($this->root_dir)+1);
2937         }
c43517 2938
59c216 2939         return $mbox_name;
A 2940     }
2941
2942
2943     /* --------------------------------
2944      *   internal caching methods
2945      * --------------------------------*/
2946
2947     /**
2948      * @access public
2949      */
2950     function set_caching($set)
2951     {
2952         if ($set && is_object($this->db))
2953             $this->caching_enabled = true;
2954         else
2955             $this->caching_enabled = false;
2956     }
2957
2958     /**
2959      * @access public
2960      */
2961     function get_cache($key)
2962     {
2963         // read cache (if it was not read before)
2964         if (!count($this->cache) && $this->caching_enabled) {
2965             return $this->_read_cache_record($key);
2966         }
c43517 2967
59c216 2968         return $this->cache[$key];
A 2969     }
2970
2971     /**
2972      * @access private
2973      */
2974     private function update_cache($key, $data)
2975     {
2976         $this->cache[$key] = $data;
2977         $this->cache_changed = true;
2978         $this->cache_changes[$key] = true;
2979     }
2980
2981     /**
2982      * @access private
2983      */
2984     private function write_cache()
2985     {
2986         if ($this->caching_enabled && $this->cache_changed) {
2987             foreach ($this->cache as $key => $data) {
2988                 if ($this->cache_changes[$key])
2989                     $this->_write_cache_record($key, serialize($data));
2990             }
2991         }
2992     }
2993
2994     /**
2995      * @access public
2996      */
2997     function clear_cache($key=NULL)
2998     {
2999         if (!$this->caching_enabled)
3000             return;
c43517 3001
59c216 3002         if ($key===NULL) {
A 3003             foreach ($this->cache as $key => $data)
3004                 $this->_clear_cache_record($key);
3005
3006             $this->cache = array();
3007             $this->cache_changed = false;
3008             $this->cache_changes = array();
3009         }
3010         else {
3011             $this->_clear_cache_record($key);
3012             $this->cache_changes[$key] = false;
3013             unset($this->cache[$key]);
3014         }
3015     }
3016
3017     /**
3018      * @access private
3019      */
3020     private function _read_cache_record($key)
3021     {
3022         if ($this->db) {
3023             // get cached data from DB
3024             $sql_result = $this->db->query(
3025                 "SELECT cache_id, data, cache_key ".
3026                 "FROM ".get_table_name('cache').
3027                 " WHERE user_id=? ".
3028                 "AND cache_key LIKE 'IMAP.%'",
3029                 $_SESSION['user_id']);
3030
3031             while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
3032                 $sql_key = preg_replace('/^IMAP\./', '', $sql_arr['cache_key']);
3033                 $this->cache_keys[$sql_key] = $sql_arr['cache_id'];
3034                 if (!isset($this->cache[$sql_key]))
3035                     $this->cache[$sql_key] = $sql_arr['data'] ? unserialize($sql_arr['data']) : false;
3036             }
3037         }
3038
3039         return $this->cache[$key];
3040     }
3041
3042     /**
3043      * @access private
3044      */
3045     private function _write_cache_record($key, $data)
3046     {
3047         if (!$this->db)
3048             return false;
3049
3050         // update existing cache record
3051         if ($this->cache_keys[$key]) {
3052             $this->db->query(
3053                 "UPDATE ".get_table_name('cache').
3054                 " SET created=". $this->db->now().", data=? ".
3055                 "WHERE user_id=? ".
3056                 "AND cache_key=?",
3057                 $data,
3058                 $_SESSION['user_id'],
3059                 'IMAP.'.$key);
3060         }
3061         // add new cache record
3062         else {
3063             $this->db->query(
3064                 "INSERT INTO ".get_table_name('cache').
3065                 " (created, user_id, cache_key, data) ".
3066                 "VALUES (".$this->db->now().", ?, ?, ?)",
3067                 $_SESSION['user_id'],
3068                 'IMAP.'.$key,
3069                 $data);
3070
3071             // get cache entry ID for this key
3072             $sql_result = $this->db->query(
3073                 "SELECT cache_id ".
3074                 "FROM ".get_table_name('cache').
3075                 " WHERE user_id=? ".
3076                 "AND cache_key=?",
3077                 $_SESSION['user_id'],
3078                 'IMAP.'.$key);
3079
3080             if ($sql_arr = $this->db->fetch_assoc($sql_result))
3081                 $this->cache_keys[$key] = $sql_arr['cache_id'];
3082         }
3083     }
3084
3085     /**
3086      * @access private
3087      */
3088     private function _clear_cache_record($key)
3089     {
3090         $this->db->query(
3091             "DELETE FROM ".get_table_name('cache').
3092             " WHERE user_id=? ".
3093             "AND cache_key=?",
3094             $_SESSION['user_id'],
3095             'IMAP.'.$key);
c43517 3096
59c216 3097         unset($this->cache_keys[$key]);
A 3098     }
3099
3100
3101
3102     /* --------------------------------
3103      *   message caching methods
3104      * --------------------------------*/
c43517 3105
59c216 3106     /**
A 3107      * Checks if the cache is up-to-date
3108      *
3109      * @param string Mailbox name
3110      * @param string Internal cache key
3111      * @return int   Cache status: -3 = off, -2 = incomplete, -1 = dirty, 1 = OK
3112      */
3113     private function check_cache_status($mailbox, $cache_key)
3114     {
3115         if (!$this->caching_enabled)
3116             return -3;
3117
3118         $cache_index = $this->get_message_cache_index($cache_key);
3119         $msg_count = $this->_messagecount($mailbox);
3120         $cache_count = count($cache_index);
3121
3122         // empty mailbox
3123         if (!$msg_count)
3124             return $cache_count ? -2 : 1;
3125
3126         // @TODO: We've got one big performance problem in cache status checking method
3127         // E.g. mailbox contains 1000 messages, in cache table we've got first 100
3128         // of them. Now if we want to display only that 100 (which we've got)
3129         // check_cache_status returns 'incomplete' and messages are fetched
3130         // from IMAP instead of DB.
3131
3132         if ($cache_count==$msg_count) {
3133             if ($this->skip_deleted) {
3134                 $h_index = $this->conn->fetchHeaderIndex($mailbox, "1:*", 'UID', $this->skip_deleted);
3135
3136                 if (empty($h_index))
3137                     return -2;
3138
3139                 if (sizeof($h_index) == $cache_count) {
3140                     $cache_index = array_flip($cache_index);
3141                     foreach ($h_index as $idx => $uid)
3142                         unset($cache_index[$uid]);
3143
3144                     if (empty($cache_index))
3145                         return 1;
3146                 }
3147                 return -2;
3148             } else {
3149                 // get UID of message with highest index
3150                 $uid = $this->conn->ID2UID($mailbox, $msg_count);
3151                 $cache_uid = array_pop($cache_index);
c43517 3152
59c216 3153                 // uids of highest message matches -> cache seems OK
A 3154                 if ($cache_uid == $uid)
3155                     return 1;
3156             }
3157             // cache is dirty
3158             return -1;
3159         }
3160         // if cache count differs less than 10% report as dirty
3161         else if (abs($msg_count - $cache_count) < $msg_count/10)
3162             return -1;
3163         else
3164             return -2;
3165     }
3166
3167     /**
3168      * @access private
3169      */
3170     private function get_message_cache($key, $from, $to, $sort_field, $sort_order)
3171     {
3172         $cache_key = "$key:$from:$to:$sort_field:$sort_order";
c43517 3173
59c216 3174         // use idx sort as default sorting
A 3175         if (!$sort_field || !in_array($sort_field, $this->db_header_fields)) {
3176             $sort_field = 'idx';
3177         }
c43517 3178
59c216 3179         if ($this->caching_enabled && !isset($this->cache[$cache_key])) {
A 3180             $this->cache[$cache_key] = array();
3181             $sql_result = $this->db->limitquery(
3182                 "SELECT idx, uid, headers".
3183                 " FROM ".get_table_name('messages').
3184                 " WHERE user_id=?".
3185                 " AND cache_key=?".
3186                 " ORDER BY ".$this->db->quoteIdentifier($sort_field)." ".strtoupper($sort_order),
3187                 $from,
3188                 $to - $from,
3189                 $_SESSION['user_id'],
3190                 $key);
3191
3192             while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
3193                 $uid = $sql_arr['uid'];
3194                 $this->cache[$cache_key][$uid] =  $this->db->decode(unserialize($sql_arr['headers']));
3195
3196                 // featch headers if unserialize failed
3197                 if (empty($this->cache[$cache_key][$uid]))
3198                     $this->cache[$cache_key][$uid] = $this->conn->fetchHeader(
3199                             preg_replace('/.msg$/', '', $key), $uid, true, $this->fetch_add_headers);
3200             }
3201         }
3202
3203         return $this->cache[$cache_key];
3204     }
3205
3206     /**
3207      * @access private
3208      */
3209     private function &get_cached_message($key, $uid)
3210     {
3211         $internal_key = 'message';
c43517 3212
59c216 3213         if ($this->caching_enabled && !isset($this->icache[$internal_key][$uid])) {
A 3214             $sql_result = $this->db->query(
3215                 "SELECT idx, headers, structure".
3216                 " FROM ".get_table_name('messages').
3217                 " WHERE user_id=?".
3218                 " AND cache_key=?".
3219                 " AND uid=?",
3220                 $_SESSION['user_id'],
3221                 $key,
3222                 $uid);
3223
3224             if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
3225                 $this->uid_id_map[preg_replace('/\.msg$/', '', $key)][$uid] = $sql_arr['idx'];
3226                 $this->icache[$internal_key][$uid] = $this->db->decode(unserialize($sql_arr['headers']));
3227                 if (is_object($this->icache[$internal_key][$uid]) && !empty($sql_arr['structure']))
3228                     $this->icache[$internal_key][$uid]->structure = $this->db->decode(unserialize($sql_arr['structure']));
3229             }
3230         }
3231
3232         return $this->icache[$internal_key][$uid];
3233     }
3234
3235     /**
3236      * @access private
c43517 3237      */
59c216 3238     private function get_message_cache_index($key, $force=false, $sort_field='idx', $sort_order='ASC')
A 3239     {
3240         static $sa_message_index = array();
c43517 3241
59c216 3242         // empty key -> empty array
A 3243         if (!$this->caching_enabled || empty($key))
3244             return array();
c43517 3245
59c216 3246         if (!empty($sa_message_index[$key]) && !$force)
A 3247             return $sa_message_index[$key];
3248
3249         // use idx sort as default
3250         if (!$sort_field || !in_array($sort_field, $this->db_header_fields))
3251             $sort_field = 'idx';
c43517 3252
59c216 3253         $sa_message_index[$key] = array();
A 3254         $sql_result = $this->db->query(
3255             "SELECT idx, uid".
3256             " FROM ".get_table_name('messages').
3257             " WHERE user_id=?".
3258             " AND cache_key=?".
3259             " ORDER BY ".$this->db->quote_identifier($sort_field)." ".$sort_order,
3260             $_SESSION['user_id'],
3261             $key);
3262
3263         while ($sql_arr = $this->db->fetch_assoc($sql_result))
3264             $sa_message_index[$key][$sql_arr['idx']] = $sql_arr['uid'];
c43517 3265
59c216 3266         return $sa_message_index[$key];
A 3267     }
3268
3269     /**
3270      * @access private
3271      */
3272     private function add_message_cache($key, $index, $headers, $struct=null, $force=false)
3273     {
3274         if (empty($key) || !is_object($headers) || empty($headers->uid))
3275             return;
3276
3277         // add to internal (fast) cache
3278         $this->icache['message'][$headers->uid] = clone $headers;
3279         $this->icache['message'][$headers->uid]->structure = $struct;
3280
3281         // no further caching
3282         if (!$this->caching_enabled)
3283             return;
c43517 3284
175d8e 3285         // check for an existing record (probably headers are cached but structure not)
59c216 3286         if (!$force) {
A 3287             $sql_result = $this->db->query(
3288                 "SELECT message_id".
3289                 " FROM ".get_table_name('messages').
3290                 " WHERE user_id=?".
3291                 " AND cache_key=?".
3292                 " AND uid=?",
3293                 $_SESSION['user_id'],
3294                 $key,
3295                 $headers->uid);
3296
3297             if ($sql_arr = $this->db->fetch_assoc($sql_result))
3298                 $message_id = $sql_arr['message_id'];
3299         }
3300
3301         // update cache record
3302         if ($message_id) {
3303             $this->db->query(
3304                 "UPDATE ".get_table_name('messages').
3305                 " SET idx=?, headers=?, structure=?".
3306                 " WHERE message_id=?",
3307                 $index,
3308                 serialize($this->db->encode(clone $headers)),
3309                 is_object($struct) ? serialize($this->db->encode(clone $struct)) : NULL,
3310                 $message_id
3311             );
3312         }
3313         else { // insert new record
3314             $this->db->query(
3315                 "INSERT INTO ".get_table_name('messages').
3316                 " (user_id, del, cache_key, created, idx, uid, subject, ".
3317                 $this->db->quoteIdentifier('from').", ".
3318                 $this->db->quoteIdentifier('to').", ".
3319                 "cc, date, size, headers, structure)".
3320                 " VALUES (?, 0, ?, ".$this->db->now().", ?, ?, ?, ?, ?, ?, ".
3321                 $this->db->fromunixtime($headers->timestamp).", ?, ?, ?)",
3322                 $_SESSION['user_id'],
3323                 $key,
3324                 $index,
3325                 $headers->uid,
3326                 (string)mb_substr($this->db->encode($this->decode_header($headers->subject, true)), 0, 128),
3327                 (string)mb_substr($this->db->encode($this->decode_header($headers->from, true)), 0, 128),
3328                 (string)mb_substr($this->db->encode($this->decode_header($headers->to, true)), 0, 128),
3329                 (string)mb_substr($this->db->encode($this->decode_header($headers->cc, true)), 0, 128),
3330                 (int)$headers->size,
3331                 serialize($this->db->encode(clone $headers)),
3332                 is_object($struct) ? serialize($this->db->encode(clone $struct)) : NULL
3333             );
3334         }
3335     }
c43517 3336
59c216 3337     /**
A 3338      * @access private
3339      */
3340     private function remove_message_cache($key, $ids, $idx=false)
3341     {
3342         if (!$this->caching_enabled)
3343             return;
c43517 3344
59c216 3345         $this->db->query(
A 3346             "DELETE FROM ".get_table_name('messages').
3347             " WHERE user_id=?".
3348             " AND cache_key=?".
3349             " AND ".($idx ? "idx" : "uid")." IN (".$this->db->array2list($ids, 'integer').")",
3350             $_SESSION['user_id'],
3351             $key);
3352     }
3353
3354     /**
3355      * @access private
3356      */
3357     private function clear_message_cache($key, $start_index=1)
3358     {
3359         if (!$this->caching_enabled)
3360             return;
c43517 3361
59c216 3362         $this->db->query(
A 3363             "DELETE FROM ".get_table_name('messages').
3364             " WHERE user_id=?".
3365             " AND cache_key=?".
3366             " AND idx>=?",
3367             $_SESSION['user_id'], $key, $start_index);
3368     }
3369
3370     /**
3371      * @access private
3372      */
3373     private function get_message_cache_index_min($key, $uids=NULL)
3374     {
3375         if (!$this->caching_enabled)
3376             return;
c43517 3377
59c216 3378         if (!empty($uids) && !is_array($uids)) {
A 3379             if ($uids == '*' || $uids == '1:*')
3380                 $uids = NULL;
3381             else
3382                 $uids = explode(',', $uids);
3383         }
3384
3385         $sql_result = $this->db->query(
3386             "SELECT MIN(idx) AS minidx".
3387             " FROM ".get_table_name('messages').
3388             " WHERE  user_id=?".
3389             " AND    cache_key=?"
3390             .(!empty($uids) ? " AND uid IN (".$this->db->array2list($uids, 'integer').")" : ''),
3391             $_SESSION['user_id'],
3392             $key);
3393
3394         if ($sql_arr = $this->db->fetch_assoc($sql_result))
3395             return $sql_arr['minidx'];
3396         else
c43517 3397             return 0;
59c216 3398     }
A 3399
3400
3401     /* --------------------------------
3402      *   encoding/decoding methods
3403      * --------------------------------*/
3404
3405     /**
3406      * Split an address list into a structured array list
3407      *
3408      * @param string  Input string
3409      * @param int     List only this number of addresses
3410      * @param boolean Decode address strings
3411      * @return array  Indexed list of addresses
3412      */
3413     function decode_address_list($input, $max=null, $decode=true)
3414     {
3415         $a = $this->_parse_address_list($input, $decode);
3416         $out = array();
3417         // Special chars as defined by RFC 822 need to in quoted string (or escaped).
3418         $special_chars = '[\(\)\<\>\\\.\[\]@,;:"]';
c43517 3419
59c216 3420         if (!is_array($a))
A 3421             return $out;
3422
3423         $c = count($a);
3424         $j = 0;
3425
3426         foreach ($a as $val) {
3427             $j++;
3428             $address = trim($val['address']);
3429             $name = trim($val['name']);
3430
3431             if (preg_match('/^[\'"]/', $name) && preg_match('/[\'"]$/', $name))
3432                 $name = preg_replace(array('/^[\'"]/', '/[\'"]$/'), '', $name);
3433
3434             if ($name && $address && $name != $address)
3435                 $string = sprintf('%s <%s>', preg_match("/$special_chars/", $name) ? '"'.addcslashes($name, '"').'"' : $name, $address);
3436             else if ($address)
3437                 $string = $address;
3438             else if ($name)
3439                 $string = $name;
c43517 3440
59c216 3441             $out[$j] = array('name' => $name,
A 3442                 'mailto' => $address,
3443                 'string' => $string
3444             );
3445
3446             if ($max && $j==$max)
3447                 break;
3448         }
c43517 3449
59c216 3450         return $out;
A 3451     }
3452
3453
3454     /**
3455      * Decode a message header value
3456      *
3457      * @param string  Header value
3458      * @param boolean Remove quotes if necessary
3459      * @return string Decoded string
3460      */
3461     function decode_header($input, $remove_quotes=false)
3462     {
3463         $str = rcube_imap::decode_mime_string((string)$input, $this->default_charset);
3464         if ($str{0}=='"' && $remove_quotes)
3465             $str = str_replace('"', '', $str);
c43517 3466
59c216 3467         return $str;
A 3468     }
3469
3470
3471     /**
3472      * Decode a mime-encoded string to internal charset
3473      *
3474      * @param string $input    Header value
3475      * @param string $fallback Fallback charset if none specified
3476      *
3477      * @return string Decoded string
3478      * @static
3479      */
3480     public static function decode_mime_string($input, $fallback=null)
3481     {
3482         // Initialize variable
3483         $out = '';
3484
3485         // Iterate instead of recursing, this way if there are too many values we don't have stack overflows
c43517 3486         // rfc: all line breaks or other characters not found
59c216 3487         // in the Base64 Alphabet must be ignored by decoding software
c43517 3488         // delete all blanks between MIME-lines, differently we can
59c216 3489         // receive unnecessary blanks and broken utf-8 symbols
A 3490         $input = preg_replace("/\?=\s+=\?/", '?==?', $input);
3491
3492         // Check if there is stuff to decode
3493         if (strpos($input, '=?') !== false) {
c43517 3494             // Loop through the string to decode all occurences of =? ?= into the variable $out
59c216 3495             while(($pos = strpos($input, '=?')) !== false) {
A 3496                 // Append everything that is before the text to be decoded
3497                 $out .= substr($input, 0, $pos);
3498
3499                 // Get the location of the text to decode
3500                 $end_cs_pos = strpos($input, "?", $pos+2);
3501                 $end_en_pos = strpos($input, "?", $end_cs_pos+1);
3502                 $end_pos = strpos($input, "?=", $end_en_pos+1);
3503
3504                 // Extract the encoded string
3505                 $encstr = substr($input, $pos+2, ($end_pos-$pos-2));
3506                 // Extract the remaining string
3507                 $input = substr($input, $end_pos+2);
3508
3509                 // Decode the string fragement
3510                 $out .= rcube_imap::_decode_mime_string_part($encstr);
3511             }
3512
3513             // Deocde the rest (if any)
3514             if (strlen($input) != 0)
3515                 $out .= rcube_imap::decode_mime_string($input, $fallback);
3516
3517             // return the results
3518             return $out;
3519         }
3520
3521         // no encoding information, use fallback
c43517 3522         return rcube_charset_convert($input,
59c216 3523             !empty($fallback) ? $fallback : rcmail::get_instance()->config->get('default_charset', 'ISO-8859-1'));
A 3524     }
3525
3526
3527     /**
3528      * Decode a part of a mime-encoded string
3529      *
3530      * @access private
3531      */
3532     private function _decode_mime_string_part($str)
3533     {
3534         $a = explode('?', $str);
3535         $count = count($a);
3536
3537         // should be in format "charset?encoding?base64_string"
3538         if ($count >= 3) {
3539             for ($i=2; $i<$count; $i++)
3540                 $rest .= $a[$i];
3541
3542             if (($a[1]=='B') || ($a[1]=='b'))
3543                 $rest = base64_decode($rest);
3544             else if (($a[1]=='Q') || ($a[1]=='q')) {
3545                 $rest = str_replace('_', ' ', $rest);
3546                 $rest = quoted_printable_decode($rest);
3547             }
3548
3549             return rcube_charset_convert($rest, $a[0]);
3550         }
3551
c43517 3552         // we dont' know what to do with this
59c216 3553         return $str;
A 3554     }
3555
3556
3557     /**
3558      * Decode a mime part
3559      *
3560      * @param string Input string
3561      * @param string Part encoding
3562      * @return string Decoded string
3563      */
3564     function mime_decode($input, $encoding='7bit')
3565     {
3566         switch (strtolower($encoding)) {
3567         case 'quoted-printable':
3568             return quoted_printable_decode($input);
3569         case 'base64':
3570             return base64_decode($input);
3571         case 'x-uuencode':
3572         case 'x-uue':
3573         case 'uue':
3574         case 'uuencode':
3575             return convert_uudecode($input);
3576         case '7bit':
3577         default:
3578             return $input;
3579         }
3580     }
3581
3582
3583     /**
3584      * Convert body charset to RCMAIL_CHARSET according to the ctype_parameters
3585      *
3586      * @param string Part body to decode
3587      * @param string Charset to convert from
3588      * @return string Content converted to internal charset
3589      */
3590     function charset_decode($body, $ctype_param)
3591     {
3592         if (is_array($ctype_param) && !empty($ctype_param['charset']))
3593             return rcube_charset_convert($body, $ctype_param['charset']);
3594
3595         // defaults to what is specified in the class header
3596         return rcube_charset_convert($body,  $this->default_charset);
3597     }
3598
3599
3600     /* --------------------------------
3601      *         private methods
3602      * --------------------------------*/
3603
3604     /**
3605      * Validate the given input and save to local properties
3606      * @access private
3607      */
3608     private function _set_sort_order($sort_field, $sort_order)
3609     {
3610         if ($sort_field != null)
3611             $this->sort_field = asciiwords($sort_field);
3612         if ($sort_order != null)
3613             $this->sort_order = strtoupper($sort_order) == 'DESC' ? 'DESC' : 'ASC';
3614     }
3615
3616     /**
3617      * Sort mailboxes first by default folders and then in alphabethical order
3618      * @access private
3619      */
3620     private function _sort_mailbox_list($a_folders)
3621     {
3622         $a_out = $a_defaults = $folders = array();
3623
3624         $delimiter = $this->get_hierarchy_delimiter();
3625
3626         // find default folders and skip folders starting with '.'
3627         foreach ($a_folders as $i => $folder) {
3628             if ($folder[0] == '.')
3629                 continue;
3630
3631             if (($p = array_search($folder, $this->default_folders)) !== false && !$a_defaults[$p])
3632                 $a_defaults[$p] = $folder;
3633             else
a44682 3634                 $folders[$folder] = rcube_charset_convert($folder, 'UTF7-IMAP');
59c216 3635         }
A 3636
3637         // sort folders and place defaults on the top
3638         asort($folders, SORT_LOCALE_STRING);
3639         ksort($a_defaults);
3640         $folders = array_merge($a_defaults, array_keys($folders));
3641
c43517 3642         // finally we must rebuild the list to move
59c216 3643         // subfolders of default folders to their place...
A 3644         // ...also do this for the rest of folders because
3645         // asort() is not properly sorting case sensitive names
3646         while (list($key, $folder) = each($folders)) {
c43517 3647             // set the type of folder name variable (#1485527)
59c216 3648             $a_out[] = (string) $folder;
A 3649             unset($folders[$key]);
c43517 3650             $this->_rsort($folder, $delimiter, $folders, $a_out);
59c216 3651         }
A 3652
3653         return $a_out;
3654     }
3655
3656
3657     /**
3658      * @access private
3659      */
3660     private function _rsort($folder, $delimiter, &$list, &$out)
3661     {
3662         while (list($key, $name) = each($list)) {
3663             if (strpos($name, $folder.$delimiter) === 0) {
c43517 3664                 // set the type of folder name variable (#1485527)
59c216 3665                 $out[] = (string) $name;
A 3666                 unset($list[$key]);
3667                 $this->_rsort($name, $delimiter, $list, $out);
3668             }
3669         }
c43517 3670         reset($list);
59c216 3671     }
A 3672
3673
3674     /**
3675      * @access private
3676      */
3677     private function _uid2id($uid, $mbox_name=NULL)
3678     {
3679         if (!$mbox_name)
3680             $mbox_name = $this->mailbox;
c43517 3681
59c216 3682         if (!isset($this->uid_id_map[$mbox_name][$uid]))
A 3683             $this->uid_id_map[$mbox_name][$uid] = $this->conn->UID2ID($mbox_name, $uid);
3684
3685         return $this->uid_id_map[$mbox_name][$uid];
3686     }
3687
3688     /**
3689      * @access private
3690      */
3691     private function _id2uid($id, $mbox_name=NULL)
3692     {
3693         if (!$mbox_name)
3694             $mbox_name = $this->mailbox;
3695
3696         if ($uid = array_search($id, (array)$this->uid_id_map[$mbox_name]))
3697             return $uid;
3698
3699         $uid = $this->conn->ID2UID($mbox_name, $id);
3700         $this->uid_id_map[$mbox_name][$uid] = $id;
c43517 3701
59c216 3702         return $uid;
A 3703     }
3704
3705
3706     /**
3707      * Subscribe/unsubscribe a list of mailboxes and update local cache
3708      * @access private
3709      */
3710     private function _change_subscription($a_mboxes, $mode)
3711     {
3712         $updated = false;
3713
3714         if (is_array($a_mboxes))
3715             foreach ($a_mboxes as $i => $mbox_name) {
3716                 $mailbox = $this->mod_mailbox($mbox_name);
3717                 $a_mboxes[$i] = $mailbox;
3718
3719                 if ($mode=='subscribe')
3720                     $updated = $this->conn->subscribe($mailbox);
3721                 else if ($mode=='unsubscribe')
3722                     $updated = $this->conn->unsubscribe($mailbox);
3723             }
3724
3725         // get cached mailbox list
3726         if ($updated) {
3727             $a_mailbox_cache = $this->get_cache('mailboxes');
3728             if (!is_array($a_mailbox_cache))
3729                 return $updated;
3730
3731             // modify cached list
3732             if ($mode=='subscribe')
3733                 $a_mailbox_cache = array_merge($a_mailbox_cache, $a_mboxes);
3734             else if ($mode=='unsubscribe')
3735                 $a_mailbox_cache = array_diff($a_mailbox_cache, $a_mboxes);
3736
3737             // write mailboxlist to cache
3738             $this->update_cache('mailboxes', $this->_sort_mailbox_list($a_mailbox_cache));
3739         }
3740
3741         return $updated;
3742     }
3743
3744
3745     /**
3746      * Increde/decrese messagecount for a specific mailbox
3747      * @access private
3748      */
3749     private function _set_messagecount($mbox_name, $mode, $increment)
3750     {
3751         $a_mailbox_cache = false;
3752         $mailbox = $mbox_name ? $mbox_name : $this->mailbox;
3753         $mode = strtoupper($mode);
3754
3755         $a_mailbox_cache = $this->get_cache('messagecount');
c43517 3756
59c216 3757         if (!is_array($a_mailbox_cache[$mailbox]) || !isset($a_mailbox_cache[$mailbox][$mode]) || !is_numeric($increment))
A 3758             return false;
c43517 3759
59c216 3760         // add incremental value to messagecount
A 3761         $a_mailbox_cache[$mailbox][$mode] += $increment;
c43517 3762
59c216 3763         // there's something wrong, delete from cache
A 3764         if ($a_mailbox_cache[$mailbox][$mode] < 0)
3765             unset($a_mailbox_cache[$mailbox][$mode]);
3766
3767         // write back to cache
3768         $this->update_cache('messagecount', $a_mailbox_cache);
c43517 3769
59c216 3770         return true;
A 3771     }
3772
3773
3774     /**
3775      * Remove messagecount of a specific mailbox from cache
3776      * @access private
3777      */
3778     private function _clear_messagecount($mbox_name='')
3779     {
3780         $a_mailbox_cache = false;
3781         $mailbox = $mbox_name ? $mbox_name : $this->mailbox;
3782
3783         $a_mailbox_cache = $this->get_cache('messagecount');
3784
3785         if (is_array($a_mailbox_cache[$mailbox])) {
3786             unset($a_mailbox_cache[$mailbox]);
3787             $this->update_cache('messagecount', $a_mailbox_cache);
3788         }
3789     }
3790
3791
3792     /**
3793      * Split RFC822 header string into an associative array
3794      * @access private
3795      */
3796     private function _parse_headers($headers)
3797     {
3798         $a_headers = array();
3799         $headers = preg_replace('/\r?\n(\t| )+/', ' ', $headers);
3800         $lines = explode("\n", $headers);
3801         $c = count($lines);
3802
3803         for ($i=0; $i<$c; $i++) {
3804             if ($p = strpos($lines[$i], ': ')) {
3805                 $field = strtolower(substr($lines[$i], 0, $p));
3806                 $value = trim(substr($lines[$i], $p+1));
3807                 if (!empty($value))
3808                     $a_headers[$field] = $value;
3809             }
3810         }
c43517 3811
59c216 3812         return $a_headers;
A 3813     }
3814
3815
3816     /**
3817      * @access private
3818      */
3819     private function _parse_address_list($str, $decode=true)
3820     {
3821         // remove any newlines and carriage returns before
3822         $a = rcube_explode_quoted_string('[,;]', preg_replace( "/[\r\n]/", " ", $str));
3823         $result = array();
3824
3825         foreach ($a as $key => $val) {
3826             $val = preg_replace("/([\"\w])</", "$1 <", $val);
3827             $sub_a = rcube_explode_quoted_string(' ', $decode ? $this->decode_header($val) : $val);
3828             $result[$key]['name'] = '';
3829
3830             foreach ($sub_a as $k => $v) {
3831                 // use angle brackets in regexp to not handle names with @ sign
3832                 if (preg_match('/^<\S+@\S+>$/', $v))
3833                     $result[$key]['address'] = trim($v, '<>');
3834                 else
3835                     $result[$key]['name'] .= (empty($result[$key]['name'])?'':' ').str_replace("\"",'',stripslashes($v));
3836             }
c43517 3837
59c216 3838             if (empty($result[$key]['name']))
c43517 3839                 $result[$key]['name'] = $result[$key]['address'];
59c216 3840             elseif (empty($result[$key]['address']))
A 3841                 $result[$key]['address'] = $result[$key]['name'];
3842         }
c43517 3843
59c216 3844         return $result;
4e17e6 3845     }
6d969b 3846
T 3847 }  // end class rcube_imap
4e17e6 3848
8d4bcd 3849
T 3850 /**
3851  * Class representing a message part
6d969b 3852  *
T 3853  * @package Mail
8d4bcd 3854  */
T 3855 class rcube_message_part
3856 {
59c216 3857     var $mime_id = '';
A 3858     var $ctype_primary = 'text';
3859     var $ctype_secondary = 'plain';
3860     var $mimetype = 'text/plain';
3861     var $disposition = '';
3862     var $filename = '';
3863     var $encoding = '8bit';
3864     var $charset = '';
3865     var $size = 0;
3866     var $headers = array();
3867     var $d_parameters = array();
3868     var $ctype_parameters = array();
8d4bcd 3869
59c216 3870     function __clone()
A 3871     {
3872         if (isset($this->parts))
3873             foreach ($this->parts as $idx => $part)
3874                 if (is_object($part))
3875                     $this->parts[$idx] = clone $part;
3876     }
8d4bcd 3877 }
4e17e6 3878
T 3879
7e93ff 3880 /**
59c216 3881  * Class for sorting an array of rcube_mail_header objects in a predetermined order.
7e93ff 3882  *
6d969b 3883  * @package Mail
7e93ff 3884  * @author Eric Stadtherr
T 3885  */
3886 class rcube_header_sorter
3887 {
59c216 3888     var $sequence_numbers = array();
c43517 3889
59c216 3890     /**
A 3891      * Set the predetermined sort order.
3892      *
3893      * @param array Numerically indexed array of IMAP message sequence numbers
3894      */
3895     function set_sequence_numbers($seqnums)
3896     {
3897         $this->sequence_numbers = array_flip($seqnums);
3898     }
3899
3900     /**
3901      * Sort the array of header objects
3902      *
3903      * @param array Array of rcube_mail_header objects indexed by UID
3904      */
3905     function sort_headers(&$headers)
3906     {
3907         /*
3908         * uksort would work if the keys were the sequence number, but unfortunately
3909         * the keys are the UIDs.  We'll use uasort instead and dereference the value
3910         * to get the sequence number (in the "id" field).
c43517 3911         *
A 3912         * uksort($headers, array($this, "compare_seqnums"));
59c216 3913         */
A 3914         uasort($headers, array($this, "compare_seqnums"));
3915     }
c43517 3916
59c216 3917     /**
A 3918      * Sort method called by uasort()
3919      */
3920     function compare_seqnums($a, $b)
3921     {
3922         // First get the sequence number from the header object (the 'id' field).
3923         $seqa = $a->id;
3924         $seqb = $b->id;
c43517 3925
59c216 3926         // then find each sequence number in my ordered list
A 3927         $posa = isset($this->sequence_numbers[$seqa]) ? intval($this->sequence_numbers[$seqa]) : -1;
3928         $posb = isset($this->sequence_numbers[$seqb]) ? intval($this->sequence_numbers[$seqb]) : -1;
c43517 3929
59c216 3930         // return the relative position as the comparison value
A 3931         return $posa - $posb;
3932     }
7e93ff 3933 }