source: github/program/include/rcube_imap.php @ 2537686

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