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

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