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

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