source: subversion/trunk/roundcubemail/program/include/rcube_imap.php @ 4014

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