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