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

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