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

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