From 3412e50b54e3daac8745234e21ab6e72be0ed165 Mon Sep 17 00:00:00 2001
From: Thomas Bruederli <thomas@roundcube.net>
Date: Wed, 04 Jun 2014 11:20:33 -0400
Subject: [PATCH] Fix attachment menu structure and aria-attributes

---
 program/lib/Roundcube/rcube_imap.php |  725 ++++++++++++++++++++++++++++++++++++++-----------------
 1 files changed, 504 insertions(+), 221 deletions(-)

diff --git a/program/lib/Roundcube/rcube_imap.php b/program/lib/Roundcube/rcube_imap.php
index 0aa059c..4204354 100644
--- a/program/lib/Roundcube/rcube_imap.php
+++ b/program/lib/Roundcube/rcube_imap.php
@@ -70,7 +70,7 @@
     protected $search_sort_field = '';
     protected $search_threads = false;
     protected $search_sorted = false;
-    protected $options = array('auth_method' => 'check');
+    protected $options = array('auth_type' => 'check');
     protected $caching = false;
     protected $messages_caching = false;
     protected $threading = false;
@@ -308,14 +308,7 @@
      */
     public function set_folder($folder)
     {
-        if ($this->folder == $folder) {
-            return;
-        }
-
         $this->folder = $folder;
-
-        // clear messagecount cache for this folder
-        $this->clear_messagecount($folder);
     }
 
 
@@ -339,6 +332,10 @@
         $this->search_sort_field = $set[3];
         $this->search_sorted     = $set[4];
         $this->search_threads    = is_a($this->search_set, 'rcube_result_thread');
+
+        if (is_a($this->search_set, 'rcube_result_multifolder')) {
+            $this->set_threading(false);
+        }
     }
 
 
@@ -398,10 +395,10 @@
     public function check_permflag($flag)
     {
         $flag       = strtoupper($flag);
-        $imap_flag  = $this->conn->flags[$flag];
         $perm_flags = $this->get_permflags($this->folder);
+        $imap_flag  = $this->conn->flags[$flag];
 
-        return in_array_nocase($imap_flag, $perm_flags);
+        return $imap_flag && !empty($perm_flags) && in_array_nocase($imap_flag, $perm_flags);
     }
 
 
@@ -417,17 +414,7 @@
         if (!strlen($folder)) {
             return array();
         }
-/*
-        Checking PERMANENTFLAGS is rather rare, so we disable caching of it
-        Re-think when we'll use it for more than only MDNSENT flag
 
-        $cache_key = 'mailboxes.permanentflags.' . $folder;
-        $permflags = $this->get_cache($cache_key);
-
-        if ($permflags !== null) {
-            return explode(' ', $permflags);
-        }
-*/
         if (!$this->check_connection()) {
             return array();
         }
@@ -442,10 +429,7 @@
         if (!is_array($permflags)) {
             $permflags = array();
         }
-/*
-        // Store permflags as string to limit cached object size
-        $this->update_cache($cache_key, implode(' ', $permflags));
-*/
+
         return $permflags;
     }
 
@@ -626,7 +610,7 @@
         }
 
         if ($mode == 'THREADS') {
-            $res   = $this->fetch_threads($folder, $force);
+            $res   = $this->threads($folder);
             $count = $res->count();
 
             if ($status) {
@@ -656,11 +640,11 @@
                     $keys[] = 'ALL';
                 }
                 if ($status) {
-                    $keys[]   = 'MAX';
+                    $keys[] = 'MAX';
                 }
             }
 
-            // @TODO: if $force==false && $mode == 'ALL' we could try to use cache index here
+            // @TODO: if $mode == 'ALL' we could try to use cache index here
 
             // get message count using (E)SEARCH
             // not very performant but more precise (using UNDELETED)
@@ -696,6 +680,41 @@
         $this->update_cache('messagecount', $a_folder_cache);
 
         return (int)$count;
+    }
+
+
+    /**
+     * Public method for listing message flags
+     *
+     * @param string $folder  Folder name
+     * @param array  $uids    Message UIDs
+     * @param int    $mod_seq Optional MODSEQ value (of last flag update)
+     *
+     * @return array Indexed array with message flags
+     */
+    public function list_flags($folder, $uids, $mod_seq = null)
+    {
+        if (!strlen($folder)) {
+            $folder = $this->folder;
+        }
+
+        if (!$this->check_connection()) {
+            return array();
+        }
+
+        // @TODO: when cache was synchronized in this request
+        // we might already have asked for flag updates, use it.
+
+        $flags  = $this->conn->fetch($folder, $uids, true, array('FLAGS'), $mod_seq);
+        $result = array();
+
+        if (!empty($flags)) {
+            foreach ($flags as $message) {
+                $result[$message->uid] = $message->flags;
+            }
+        }
+
+        return $result;
     }
 
 
@@ -791,7 +810,7 @@
             $threads = $mcache->get_thread($folder);
         }
         else {
-            $threads = $this->fetch_threads($folder);
+            $threads = $this->threads($folder);
         }
 
         return $this->fetch_thread_headers($folder, $threads, $page, $slice);
@@ -800,32 +819,47 @@
     /**
      * Method for fetching threads data
      *
-     * @param  string $folder  Folder name
-     * @param  bool   $force   Use IMAP server, no cache
+     * @param  string $folder Folder name
      *
      * @return rcube_imap_thread Thread data object
      */
-    function fetch_threads($folder, $force = false)
+    function threads($folder)
     {
-        if (!$force && ($mcache = $this->get_mcache_engine())) {
+        if ($mcache = $this->get_mcache_engine()) {
             // don't store in self's internal cache, cache has it's own internal cache
             return $mcache->get_thread($folder);
         }
 
-        if (empty($this->icache['threads'])) {
-            if (!$this->check_connection()) {
-                return new rcube_result_thread();
+        if (!empty($this->icache['threads'])) {
+            if ($this->icache['threads']->get_parameters('MAILBOX') == $folder) {
+                return $this->icache['threads'];
             }
-
-            // get all threads
-            $result = $this->conn->thread($folder, $this->threading,
-                $this->options['skip_deleted'] ? 'UNDELETED' : '', true);
-
-            // add to internal (fast) cache
-            $this->icache['threads'] = $result;
         }
 
-        return $this->icache['threads'];
+        // get all threads
+        $result = $this->threads_direct($folder);
+
+        // add to internal (fast) cache
+        return $this->icache['threads'] = $result;
+    }
+
+
+    /**
+     * Method for direct fetching of threads data
+     *
+     * @param  string $folder Folder name
+     *
+     * @return rcube_imap_thread Thread data object
+     */
+    function threads_direct($folder)
+    {
+        if (!$this->check_connection()) {
+            return new rcube_result_thread();
+        }
+
+        // get all threads
+        return $this->conn->thread($folder, $this->threading,
+            $this->options['skip_deleted'] ? 'UNDELETED' : '', true);
     }
 
 
@@ -915,6 +949,75 @@
             return array();
         }
 
+        // gather messages from a multi-folder search
+        if ($this->search_set->multi) {
+            $page_size = $this->page_size;
+            $sort_field = $this->sort_field;
+            $search_set = $this->search_set;
+
+            // prepare paging
+            $cnt   = $search_set->count();
+            $from  = ($page-1) * $page_size;
+            $to    = $from + $page_size;
+            $slice_length = min($page_size, $cnt - $from);
+
+            // fetch resultset headers, sort and slice them
+            if (!empty($sort_field)) {
+                $this->sort_field = null;
+                $this->page_size = 1000;  // fetch up to 1000 matching messages per folder
+                $this->threading = false;
+
+                $a_msg_headers = array();
+                foreach ($search_set->sets as $resultset) {
+                    if (!$resultset->is_empty()) {
+                        $this->search_set = $resultset;
+                        $this->search_threads = $resultset instanceof rcube_result_thread;
+                        $a_msg_headers = array_merge($a_msg_headers, $this->list_search_messages($resultset->get_parameters('MAILBOX'), 1));
+                    }
+                }
+
+                // sort headers
+                if (!empty($a_msg_headers)) {
+                    $a_msg_headers = $this->conn->sortHeaders($a_msg_headers, $sort_field, $this->sort_order);
+                }
+
+                // store (sorted) message index
+                $search_set->set_message_index($a_msg_headers, $sort_field, $this->sort_order);
+
+                // only return the requested part of the set
+                $a_msg_headers = array_slice(array_values($a_msg_headers), $from, $slice_length);
+            }
+            else {
+                if ($this->sort_order != $search_set->get_parameters('ORDER')) {
+                    $search_set->revert();
+                }
+
+                // slice resultset first...
+                $fetch = array();
+                foreach (array_slice($search_set->get(), $from, $slice_length) as $msg_id) {
+                    list($uid, $folder) = explode('-', $msg_id, 2);
+                    $fetch[$folder][] = $uid;
+                }
+
+                // ... and fetch the requested set of headers
+                $a_msg_headers = array();
+                foreach ($fetch as $folder => $a_index) {
+                    $a_msg_headers = array_merge($a_msg_headers, array_values($this->fetch_headers($folder, $a_index)));
+                }
+            }
+
+            if ($slice) {
+                $a_msg_headers = array_slice($a_msg_headers, -$slice, $slice);
+            }
+
+            // restore members
+            $this->sort_field = $sort_field;
+            $this->page_size = $page_size;
+            $this->search_set = $search_set;
+
+            return $a_msg_headers;
+        }
+
         // use saved messages from searching
         if ($this->threading) {
             return $this->list_search_thread_messages($folder, $page, $slice);
@@ -981,7 +1084,7 @@
             // use memory less expensive (and quick) method for big result set
             $index = clone $this->index('', $this->sort_field, $this->sort_order);
             // get messages uids for one page...
-            $index->slice($start_msg, min($cnt-$from, $this->page_size));
+            $index->slice($from, min($cnt-$from, $this->page_size));
 
             if ($slice) {
                 $index->slice(-$slice, $slice);
@@ -1081,6 +1184,7 @@
         }
 
         foreach ($headers as $h) {
+            $h->folder = $folder;
             $a_msg_headers[$h->uid] = $h;
         }
 
@@ -1180,12 +1284,15 @@
      * @param string $folder     Folder to get index from
      * @param string $sort_field Sort column
      * @param string $sort_order Sort order [ASC, DESC]
+     * @param bool   $no_threads Get not threaded index
+     * @param bool   $no_search  Get index not limited to search result (optionally)
      *
      * @return rcube_result_index|rcube_result_thread List of messages (UIDs)
      */
-    public function index($folder = '', $sort_field = NULL, $sort_order = NULL)
-    {
-        if ($this->threading) {
+    public function index($folder = '', $sort_field = NULL, $sort_order = NULL,
+        $no_threads = false, $no_search = false
+    ) {
+        if (!$no_threads && $this->threading) {
             return $this->thread_index($folder, $sort_field, $sort_order);
         }
 
@@ -1197,43 +1304,55 @@
 
         // we have a saved search result, get index from there
         if ($this->search_string) {
-            if ($this->search_threads) {
-                $this->search($folder, $this->search_string, $this->search_charset, $this->sort_field);
+            if ($this->search_set->is_empty()) {
+                return new rcube_result_index($folder, '* SORT');
             }
 
-            // use message index sort as default sorting
-            if (!$this->sort_field || $this->search_sorted) {
-                if ($this->sort_field && $this->search_sort_field != $this->sort_field) {
-                    $this->search($folder, $this->search_string, $this->search_charset, $this->sort_field);
-                }
+            if ($this->search_set instanceof rcube_result_multifolder) {
+                $index = $this->search_set;
+                $index->folder = $folder;
+                // TODO: handle changed sorting
+            }
+            // search result is an index with the same sorting?
+            else if (($this->search_set instanceof rcube_result_index)
+                && ((!$this->sort_field && !$this->search_sorted) ||
+                    ($this->search_sorted && $this->search_sort_field == $this->sort_field))
+            ) {
                 $index = $this->search_set;
             }
-            else if (!$this->check_connection()) {
-                return new rcube_result_index();
-            }
-            else {
-                $index = $this->conn->index($folder, $this->search_set->get(),
-                    $this->sort_field, $this->options['skip_deleted'], true, true);
+            // $no_search is enabled when we are not interested in
+            // fetching index for search result, e.g. to sort
+            // threaded search result we can use full mailbox index.
+            // This makes possible to use index from cache
+            else if (!$no_search) {
+                if (!$this->sort_field) {
+                    // No sorting needed, just build index from the search result
+                    // @TODO: do we need to sort by UID here?
+                    $search = $this->search_set->get_compressed();
+                    $index  = new rcube_result_index($folder, '* ESEARCH ALL ' . $search);
+                }
+                else {
+                    $index = $this->index_direct($folder, $this->search_charset,
+                        $this->sort_field, $this->search_set);
+                }
             }
 
-            if ($this->sort_order != $index->get_parameters('ORDER')) {
-                $index->revert();
-            }
+            if (isset($index)) {
+                if ($this->sort_order != $index->get_parameters('ORDER')) {
+                    $index->revert();
+                }
 
-            return $index;
+                return $index;
+            }
         }
 
         // check local cache
         if ($mcache = $this->get_mcache_engine()) {
-            $index = $mcache->get_index($folder, $this->sort_field, $this->sort_order);
-        }
-        // fetch from IMAP server
-        else {
-            $index = $this->index_direct(
-                $folder, $this->sort_field, $this->sort_order);
+            return $mcache->get_index($folder, $this->sort_field, $this->sort_order);
         }
 
-        return $index;
+        // fetch from IMAP server
+        return $this->index_direct($folder, $this->sort_field, $this->sort_order);
     }
 
 
@@ -1241,22 +1360,24 @@
      * Return sorted list of message UIDs ignoring current search settings.
      * Doesn't uses cache by default.
      *
-     * @param string $folder     Folder to get index from
-     * @param string $sort_field Sort column
-     * @param string $sort_order Sort order [ASC, DESC]
-     * @param bool   $skip_cache Disables cache usage
+     * @param string         $folder     Folder to get index from
+     * @param string         $sort_field Sort column
+     * @param string         $sort_order Sort order [ASC, DESC]
+     * @param rcube_result_* $search     Optional messages set to limit the result
      *
      * @return rcube_result_index Sorted list of message UIDs
      */
-    public function index_direct($folder, $sort_field = null, $sort_order = null, $skip_cache = true)
+    public function index_direct($folder, $sort_field = null, $sort_order = null, $search = null)
     {
-        if (!$skip_cache && ($mcache = $this->get_mcache_engine())) {
-            $index = $mcache->get_index($folder, $sort_field, $sort_order);
+        if (!empty($search)) {
+            $search = $this->search_set->get_compressed();
         }
+
         // use message index sort as default sorting
-        else if (!$sort_field) {
+        if (!$sort_field) {
             // use search result from count() if possible
-            if ($this->options['skip_deleted'] && !empty($this->icache['undeleted_idx'])
+            if (empty($search) && $this->options['skip_deleted']
+                && !empty($this->icache['undeleted_idx'])
                 && $this->icache['undeleted_idx']->get_parameters('ALL') !== null
                 && $this->icache['undeleted_idx']->get_parameters('MAILBOX') == $folder
             ) {
@@ -1266,8 +1387,12 @@
                 return new rcube_result_index();
             }
             else {
-                $index = $this->conn->search($folder,
-                    'ALL' .($this->options['skip_deleted'] ? ' UNDELETED' : ''), true);
+                $query = $this->options['skip_deleted'] ? 'UNDELETED' : '';
+                if ($search) {
+                    $query = trim($query . ' UID ' . $search);
+                }
+
+                $index = $this->conn->search($folder, $query, true);
             }
         }
         else if (!$this->check_connection()) {
@@ -1276,13 +1401,18 @@
         // fetch complete message index
         else {
             if ($this->get_capability('SORT')) {
-                $index = $this->conn->sort($folder, $sort_field,
-                    $this->options['skip_deleted'] ? 'UNDELETED' : '', true);
+                $query = $this->options['skip_deleted'] ? 'UNDELETED' : '';
+                if ($search) {
+                    $query = trim($query . ' UID ' . $search);
+                }
+
+                $index = $this->conn->sort($folder, $sort_field, $query, true);
             }
 
             if (empty($index) || $index->is_error()) {
-                $index = $this->conn->index($folder, "1:*", $sort_field,
-                    $this->options['skip_deleted'], false, true);
+                $index = $this->conn->index($folder, $search ? $search : "1:*",
+                    $sort_field, $this->options['skip_deleted'],
+                    $search ? true : false, true);
             }
         }
 
@@ -1315,7 +1445,7 @@
         }
         else {
             // get all threads (default sort order)
-            $threads = $this->fetch_threads($folder);
+            $threads = $this->threads($folder);
         }
 
         $this->set_sort_order($sort_field, $sort_order);
@@ -1326,9 +1456,10 @@
 
 
     /**
-     * Sort threaded result, using THREAD=REFS method
+     * Sort threaded result, using THREAD=REFS method if available.
+     * If not, use any method and re-sort the result in THREAD=REFS way.
      *
-     * @param rcube_result_thread $threads  Threads result set
+     * @param rcube_result_thread $threads Threads result set
      */
     protected function sort_threads($threads)
     {
@@ -1340,17 +1471,16 @@
         // THREAD=REFERENCES:     sorting by sent date of root message
         // THREAD=REFS:           sorting by the most recent date in each thread
 
-        if ($this->sort_field && ($this->sort_field != 'date' || $this->get_capability('THREAD') != 'REFS')) {
-            $index = $this->index_direct($this->folder, $this->sort_field, $this->sort_order, false);
+        if ($this->threading != 'REFS' || ($this->sort_field && $this->sort_field != 'date')) {
+            $sortby = $this->sort_field ? $this->sort_field : 'date';
+            $index  = $this->index($this->folder, $sortby, $this->sort_order, true, true);
 
             if (!$index->is_empty()) {
                 $threads->sort($index);
             }
         }
-        else {
-            if ($this->sort_order != $threads->get_parameters('ORDER')) {
-                $threads->revert();
-            }
+        else if ($this->sort_order != $threads->get_parameters('ORDER')) {
+            $threads->revert();
         }
     }
 
@@ -1362,7 +1492,7 @@
      * @param  string  $str        Search criteria
      * @param  string  $charset    Search charset
      * @param  string  $sort_field Header field to sort by
-     *
+     * @return rcube_result_index  Search result object
      * @todo: Search criteria should be provided in non-IMAP format, eg. array
      */
     public function search($folder='', $str='ALL', $charset=NULL, $sort_field=NULL)
@@ -1371,14 +1501,48 @@
             $str = 'ALL';
         }
 
-        if (!strlen($folder)) {
-            $folder = $this->folder;
-        }
+        // multi-folder search
+        if (is_array($folder) && count($folder) > 1 && $str != 'ALL') {
+            new rcube_result_index; // trigger autoloader and make these classes available for threaded context
+            new rcube_result_thread;
 
-        $results = $this->search_index($folder, $str, $charset, $sort_field);
+            // connect IMAP to have all the required classes and settings loaded
+            $this->check_connection();
+
+            // disable threading
+            $this->threading = false;
+
+            $searcher = new rcube_imap_search($this->options, $this->conn);
+
+            // set limit to not exceed the client's request timeout
+            $searcher->set_timelimit(60);
+
+            // continue existing incomplete search
+            if (!empty($this->search_set) && $this->search_set->incomplete && $str == $this->search_string) {
+                $searcher->set_results($this->search_set);
+            }
+
+            // execute the search
+            $results = $searcher->exec(
+                $folder,
+                $str,
+                $charset ? $charset : $this->default_charset,
+                $sort_field && $this->get_capability('SORT') ? $sort_field : null,
+                $this->threading
+            );
+        }
+        else {
+            $folder = is_array($folder) ? $folder[0] : $folder;
+            if (!strlen($folder)) {
+                $folder = $this->folder;
+            }
+            $results = $this->search_index($folder, $str, $charset, $sort_field);
+        }
 
         $this->set_search_set(array($str, $results, $charset, $sort_field,
             $this->threading || $this->search_sorted ? true : false));
+
+        return $results;
     }
 
 
@@ -1392,19 +1556,26 @@
      */
     public function search_once($folder = null, $str = 'ALL')
     {
-        if (!$str) {
-            return 'ALL';
-        }
-
-        if (!strlen($folder)) {
-            $folder = $this->folder;
-        }
-
         if (!$this->check_connection()) {
             return new rcube_result_index();
         }
 
-        $index = $this->conn->search($folder, $str, true);
+        if (!$str) {
+            $str = 'ALL';
+        }
+
+        // multi-folder search
+        if (is_array($folder) && count($folder) > 1) {
+            $searcher = new rcube_imap_search($this->options, $this->conn);
+            $index = $searcher->exec($folder, $str, $this->default_charset);
+        }
+        else {
+            $folder = is_array($folder) ? $folder[0] : $folder;
+            if (!strlen($folder)) {
+                $folder = $this->folder;
+            }
+            $index = $this->conn->search($folder, $str, true);
+        }
 
         return $index;
     }
@@ -1423,8 +1594,6 @@
      */
     protected function search_index($folder, $criteria='ALL', $charset=NULL, $sort_field=NULL)
     {
-        $orig_criteria = $criteria;
-
         if (!$this->check_connection()) {
             if ($this->threading) {
                 return new rcube_result_thread();
@@ -1451,7 +1620,7 @@
             // but I've seen that Courier doesn't support UTF-8)
             if ($threads->is_error() && $charset && $charset != 'US-ASCII') {
                 $threads = $this->conn->thread($folder, $this->threading,
-                    $this->convert_criteria($criteria, $charset), true, 'US-ASCII');
+                    self::convert_criteria($criteria, $charset), true, 'US-ASCII');
             }
 
             return $threads;
@@ -1465,7 +1634,7 @@
             // but I've seen Courier with disabled UTF-8 support)
             if ($messages->is_error() && $charset && $charset != 'US-ASCII') {
                 $messages = $this->conn->sort($folder, $sort_field,
-                    $this->convert_criteria($criteria, $charset), true, 'US-ASCII');
+                    self::convert_criteria($criteria, $charset), true, 'US-ASCII');
             }
 
             if (!$messages->is_error()) {
@@ -1480,7 +1649,7 @@
         // Error, try with US-ASCII (some servers may support only US-ASCII)
         if ($messages->is_error() && $charset && $charset != 'US-ASCII') {
             $messages = $this->conn->search($folder,
-                $this->convert_criteria($criteria, $charset), true);
+                self::convert_criteria($criteria, $charset), true);
         }
 
         $this->search_sorted = false;
@@ -1498,7 +1667,7 @@
      *
      * @return string  Search string
      */
-    protected function convert_criteria($str, $charset, $dest_charset='US-ASCII')
+    public static function convert_criteria($str, $charset, $dest_charset='US-ASCII')
     {
         // convert strings to US_ASCII
         if (preg_match_all('/\{([0-9]+)\}\r\n/', $str, $matches, PREG_OFFSET_CAPTURE)) {
@@ -1534,10 +1703,28 @@
     public function refresh_search()
     {
         if (!empty($this->search_string)) {
-            $this->search('', $this->search_string, $this->search_charset, $this->search_sort_field);
+            $this->search(
+                is_object($this->search_set) ? $this->search_set->get_parameters('MAILBOX') : '',
+                $this->search_string,
+                $this->search_charset,
+                $this->search_sort_field
+            );
         }
 
         return $this->get_search_set();
+    }
+
+    /**
+     * Flag certain result subsets as 'incomplete'.
+     * For subsequent refresh_search() calls to only refresh the updated parts.
+     */
+    protected function set_search_dirty($folder)
+    {
+        if ($this->search_set && is_a($this->search_set, 'rcube_result_multifolder')) {
+            if ($subset = $this->search_set->get_set($folder)) {
+                $subset->incomplete = $this->search_set->incomplete = true;
+            }
+        }
     }
 
 
@@ -1552,6 +1739,11 @@
      */
     public function get_message_headers($uid, $folder = null, $force = false)
     {
+        // decode combined UID-folder identifier
+        if (preg_match('/^\d+-.+/', $uid)) {
+            list($uid, $folder) = explode('-', $uid, 2);
+        }
+
         if (!strlen($folder)) {
             $folder = $this->folder;
         }
@@ -1566,6 +1758,9 @@
         else {
             $headers = $this->conn->fetchHeader(
                 $folder, $uid, true, true, $this->get_fetch_headers());
+
+            if (is_object($headers))
+                $headers->folder = $folder;
         }
 
         return $headers;
@@ -1585,6 +1780,11 @@
     {
         if (!strlen($folder)) {
             $folder = $this->folder;
+        }
+
+        // decode combined UID-folder identifier
+        if (preg_match('/^\d+-.+/', $uid)) {
+            list($uid, $folder) = explode('-', $uid, 2);
         }
 
         // Check internal cache
@@ -2065,17 +2265,18 @@
     /**
      * Fetch message body of a specific message from the server
      *
-     * @param  int                $uid    Message UID
-     * @param  string             $part   Part number
-     * @param  rcube_message_part $o_part Part object created by get_structure()
-     * @param  mixed              $print  True to print part, ressource to write part contents in
-     * @param  resource           $fp     File pointer to save the message part
-     * @param  boolean            $skip_charset_conv Disables charset conversion
-     * @param  int                $max_bytes  Only read this number of bytes
+     * @param int                Message UID
+     * @param string             Part number
+     * @param rcube_message_part Part object created by get_structure()
+     * @param mixed              True to print part, resource to write part contents in
+     * @param resource           File pointer to save the message part
+     * @param boolean            Disables charset conversion
+     * @param int                Only read this number of bytes
+     * @param boolean            Enables formatting of text/* parts bodies
      *
      * @return string Message/part body if not printed
      */
-    public function get_message_part($uid, $part=1, $o_part=NULL, $print=NULL, $fp=NULL, $skip_charset_conv=false, $max_bytes=0)
+    public function get_message_part($uid, $part=1, $o_part=NULL, $print=NULL, $fp=NULL, $skip_charset_conv=false, $max_bytes=0, $formatted=true)
     {
         if (!$this->check_connection()) {
             return null;
@@ -2094,8 +2295,9 @@
         }
 
         if ($o_part && $o_part->size) {
+            $formatted = $formatted && $o_part->ctype_primary == 'text';
             $body = $this->conn->handlePartBody($this->folder, $uid, true,
-                $part ? $part : 'TEXT', $o_part->encoding, $print, $fp, $o_part->ctype_primary == 'text', $max_bytes);
+                $part ? $part : 'TEXT', $o_part->encoding, $print, $fp, $formatted, $max_bytes);
         }
 
         if ($fp || $print) {
@@ -2105,7 +2307,7 @@
         // convert charset (if text or message part)
         if ($body && preg_match('/^(text|message)$/', $o_part->ctype_primary)) {
             // Remove NULL characters if any (#1486189)
-            if (strpos($body, "\x00") !== false) {
+            if ($formatted && strpos($body, "\x00") !== false) {
                 $body = str_replace("\x00", '', $body);
             }
 
@@ -2231,6 +2433,8 @@
                     $this->clear_message_cache($folder, $all_mode ? null : explode(',', $uids));
                 }
             }
+
+            $this->set_search_dirty($folder);
         }
 
         return $result;
@@ -2240,13 +2444,14 @@
     /**
      * Append a mail message (source) to a specific folder
      *
-     * @param string  $folder  Target folder
-     * @param string  $message The message source string or filename
-     * @param string  $headers Headers string if $message contains only the body
-     * @param boolean $is_file True if $message is a filename
-     * @param array   $flags   Message flags
-     * @param mixed   $date    Message internal date
-     * @param bool    $binary  Enables BINARY append
+     * @param string       $folder  Target folder
+     * @param string|array $message The message source string or filename
+     *                              or array (of strings and file pointers)
+     * @param string       $headers Headers string if $message contains only the body
+     * @param boolean      $is_file True if $message is a filename
+     * @param array        $flags   Message flags
+     * @param mixed        $date    Message internal date
+     * @param bool         $binary  Enables BINARY append
      *
      * @return int|bool Appended message UID or True on success, False on error
      */
@@ -2277,6 +2482,17 @@
         if ($saved) {
             // increase messagecount of the target folder
             $this->set_messagecount($folder, 'ALL', 1);
+
+            rcube::get_instance()->plugins->exec_hook('message_saved', array(
+                    'folder'  => $folder,
+                    'message' => $message,
+                    'headers' => $headers,
+                    'is_file' => $is_file,
+                    'flags'   => $flags,
+                    'date'    => $date,
+                    'binary'  => $binary,
+                    'result'  => $saved,
+            ));
         }
 
         return $saved;
@@ -2313,19 +2529,7 @@
             return false;
         }
 
-        // make sure folder exists
-        if ($to_mbox != 'INBOX' && !$this->folder_exists($to_mbox)) {
-            if (in_array($to_mbox, $this->default_folders)) {
-                if (!$this->create_folder($to_mbox, true)) {
-                    return false;
-                }
-            }
-            else {
-                return false;
-            }
-        }
-
-        $config = rcube::get_instance()->config;
+        $config   = rcube::get_instance()->config;
         $to_trash = $to_mbox == $config->get('trash_mbox');
 
         // flag messages as read before moving them
@@ -2340,6 +2544,9 @@
         if ($moved) {
             $this->clear_messagecount($from_mbox);
             $this->clear_messagecount($to_mbox);
+
+            $this->set_search_dirty($from_mbox);
+            $this->set_search_dirty($to_mbox);
         }
         // moving failed
         else if ($to_trash && $config->get('delete_always', false)) {
@@ -2356,8 +2563,8 @@
                 if ($this->search_threads || $all_mode) {
                     $this->refresh_search();
                 }
-                else {
-                    $this->search_set->filter(explode(',', $uids));
+                else if (!$this->search_set->incomplete) {
+                    $this->search_set->filter(explode(',', $uids), $this->folder);
                 }
             }
 
@@ -2394,18 +2601,6 @@
 
         if (!$this->check_connection()) {
             return false;
-        }
-
-        // make sure folder exists
-        if ($to_mbox != 'INBOX' && !$this->folder_exists($to_mbox)) {
-            if (in_array($to_mbox, $this->default_folders)) {
-                if (!$this->create_folder($to_mbox, true)) {
-                    return false;
-                }
-            }
-            else {
-                return false;
-            }
         }
 
         // copy messages
@@ -2456,13 +2651,15 @@
             // unset threads internal cache
             unset($this->icache['threads']);
 
+            $this->set_search_dirty($folder);
+
             // remove message ids from search set
             if ($this->search_set && $folder == $this->folder) {
                 // threads are too complicated to just remove messages from set
                 if ($this->search_threads || $all_mode) {
                     $this->refresh_search();
                 }
-                else {
+                else if (!$this->search_set->incomplete) {
                     $this->search_set->filter(explode(',', $uids));
                 }
             }
@@ -2639,7 +2836,6 @@
 
         if ($list_extended) {
             // unsubscribe non-existent folders, remove from the list
-            // we can do this only when LIST response is available
             if (is_array($a_folders) && $name == '*' && !empty($this->conn->data['LIST'])) {
                 foreach ($a_folders as $idx => $folder) {
                     if (($opts = $this->conn->data['LIST'][$folder])
@@ -2652,19 +2848,14 @@
             }
         }
         else {
-            // unsubscribe non-existent folders, remove them from the list,
-            // we can do this only when LIST response is available
-            if (is_array($a_folders) && $name == '*' && !empty($this->conn->data['LIST'])) {
-                foreach ($a_folders as $idx => $folder) {
-                    if (!isset($this->conn->data['LIST'][$folder])
-                        || in_array('\\Noselect', $this->conn->data['LIST'][$folder])
-                    ) {
-                        // Some servers returns \Noselect for existing folders
-                        if (!$this->folder_exists($folder)) {
-                            $this->conn->unsubscribe($folder);
-                            unset($a_folders[$idx]);
-                        }
-                    }
+            // unsubscribe non-existent folders, remove them from the list
+            if (is_array($a_folders) && !empty($a_folders) && $name == '*') {
+                $existing    = $this->list_folders($root, $name);
+                $nonexisting = array_diff($a_folders, $existing);
+                $a_folders   = array_diff($a_folders, $nonexisting);
+
+                foreach ($nonexisting as $folder) {
+                    $this->conn->unsubscribe($folder);
                 }
             }
         }
@@ -2727,7 +2918,7 @@
 
         // filter folders list according to rights requirements
         if ($rights && $this->get_capability('ACL')) {
-            $a_folders = $this->filter_rights($a_folders, $rights);
+            $a_mboxes = $this->filter_rights($a_mboxes, $rights);
         }
 
         // filter folders and sort them
@@ -2783,7 +2974,6 @@
      */
     private function list_folders_update(&$result, $type = null)
     {
-        $delim     = $this->get_hierarchy_delimiter();
         $namespace = $this->get_namespace();
         $search    = array();
 
@@ -2833,12 +3023,21 @@
 
     /**
      * Filter the given list of folders according to access rights
+     *
+     * For performance reasons we assume user has full rights
+     * on all personal folders.
      */
     protected function filter_rights($a_folders, $rights)
     {
         $regex = '/('.$rights.')/';
+
         foreach ($a_folders as $idx => $folder) {
+            if ($this->folder_namespace($folder) == 'personal') {
+                continue;
+            }
+
             $myrights = join('', (array)$this->my_rights($folder));
+
             if ($myrights !== null && !preg_match($regex, $myrights)) {
                 unset($a_folders[$idx]);
             }
@@ -2921,16 +3120,17 @@
      *
      * @param string  $folder    New folder name
      * @param boolean $subscribe True if the new folder should be subscribed
+     * @param string  $type      Optional folder type (junk, trash, drafts, sent, archive)
      *
      * @return boolean True on success
      */
-    public function create_folder($folder, $subscribe=false)
+    public function create_folder($folder, $subscribe = false, $type = null)
     {
         if (!$this->check_connection()) {
             return false;
         }
 
-        $result = $this->conn->createFolder($folder);
+        $result = $this->conn->createFolder($folder, $type ? array("\\" . ucfirst($type)) : null);
 
         // try to subscribe it
         if ($result) {
@@ -3055,19 +3255,84 @@
 
 
     /**
-     * Create all folders specified as default
+     * Detect special folder associations stored in storage backend
      */
-    public function create_default_folders()
+    public function get_special_folders($forced = false)
     {
-        // create default folders if they do not exist
-        foreach ($this->default_folders as $folder) {
-            if (!$this->folder_exists($folder)) {
-                $this->create_folder($folder, true);
-            }
-            else if (!$this->folder_exists($folder, true)) {
-                $this->subscribe($folder);
+        $result = parent::get_special_folders();
+
+        if (isset($this->icache['special-use'])) {
+            return array_merge($result, $this->icache['special-use']);
+        }
+
+        if (!$forced || !$this->get_capability('SPECIAL-USE')) {
+            return $result;
+        }
+
+        if (!$this->check_connection()) {
+            return $result;
+        }
+
+        $types   = array_map(function($value) { return "\\" . ucfirst($value); }, rcube_storage::$folder_types);
+        $special = array();
+
+        // request \Subscribed flag in LIST response as performance improvement for folder_exists()
+        $folders = $this->conn->listMailboxes('', '*', array('SUBSCRIBED'), array('SPECIAL-USE'));
+
+        foreach ($folders as $folder) {
+            if ($flags = $this->conn->data['LIST'][$folder]) {
+                foreach ($types as $type) {
+                    if (in_array($type, $flags)) {
+                        $type           = strtolower(substr($type, 1));
+                        $special[$type] = $folder;
+                    }
+                }
             }
         }
+
+        $this->icache['special-use'] = $special;
+        unset($this->icache['special-folders']);
+
+        return array_merge($result, $special);
+    }
+
+
+    /**
+     * Set special folder associations stored in storage backend
+     */
+    public function set_special_folders($specials)
+    {
+        if (!$this->get_capability('SPECIAL-USE') || !$this->get_capability('METADATA')) {
+            return false;
+        }
+
+        if (!$this->check_connection()) {
+            return false;
+        }
+
+        $folders = $this->get_special_folders(true);
+        $old     = (array) $this->icache['special-use'];
+
+        foreach ($specials as $type => $folder) {
+            if (in_array($type, rcube_storage::$folder_types)) {
+                $old_folder = $old[$type];
+                if ($old_folder !== $folder) {
+                    // unset old-folder metadata
+                    if ($old_folder !== null) {
+                        $this->delete_metadata($old_folder, array('/private/specialuse'));
+                    }
+                    // set new folder metadata
+                    if ($folder) {
+                        $this->set_metadata($folder, array('/private/specialuse' => "\\" . ucfirst($type)));
+                    }
+                }
+            }
+        }
+
+        $this->icache['special-use'] = $specials;
+        unset($this->icache['special-folders']);
+
+        return true;
     }
 
 
@@ -3079,13 +3344,13 @@
      *
      * @return boolean TRUE or FALSE
      */
-    public function folder_exists($folder, $subscription=false)
+    public function folder_exists($folder, $subscription = false)
     {
         if ($folder == 'INBOX') {
             return true;
         }
 
-        $key  = $subscription ? 'subscribed' : 'existing';
+        $key = $subscription ? 'subscribed' : 'existing';
 
         if (is_array($this->icache[$key]) && in_array($folder, $this->icache[$key])) {
             return true;
@@ -3096,10 +3361,24 @@
         }
 
         if ($subscription) {
-            $a_folders = $this->conn->listSubscribed('', $folder);
+            // It's possible we already called LIST command, check LIST data
+            if (!empty($this->conn->data['LIST']) && !empty($this->conn->data['LIST'][$folder])
+                && in_array('\\Subscribed', $this->conn->data['LIST'][$folder])
+            ) {
+                $a_folders = array($folder);
+            }
+            else {
+                $a_folders = $this->conn->listSubscribed('', $folder);
+            }
         }
         else {
-            $a_folders = $this->conn->listMailboxes('', $folder);
+            // It's possible we already called LIST command, check LIST data
+            if (!empty($this->conn->data['LIST']) && isset($this->conn->data['LIST'][$folder])) {
+                $a_folders = array($folder);
+            }
+            else {
+                $a_folders = $this->conn->listMailboxes('', $folder);
+            }
         }
 
         if (is_array($a_folders) && in_array($folder, $a_folders)) {
@@ -3310,7 +3589,7 @@
         $options['name']       = $folder;
         $options['attributes'] = $this->folder_attributes($folder, true);
         $options['namespace']  = $this->folder_namespace($folder);
-        $options['special']    = in_array($folder, $this->default_folders);
+        $options['special']    = $this->is_special_folder($folder);
 
         // Set 'noselect' flag
         if (is_array($options['attributes'])) {
@@ -3372,7 +3651,6 @@
     {
         if (!empty($this->options['fetch_headers'])) {
             $headers = explode(' ', $this->options['fetch_headers']);
-            $headers = array_map('strtoupper', $headers);
         }
         else {
             $headers = array();
@@ -3382,7 +3660,7 @@
             $headers = array_merge($headers, $this->all_headers);
         }
 
-        return implode(' ', array_unique($headers));
+        return $headers;
     }
 
 
@@ -3695,7 +3973,7 @@
     {
         if ($this->caching && !$this->cache) {
             $rcube = rcube::get_instance();
-            $ttl = $rcube->config->get('message_cache_lifetime', '10d');
+            $ttl   = $rcube->config->get('imap_cache_ttl', '10d');
             $this->cache = $rcube->get_cache('IMAP', $this->caching, $ttl);
         }
 
@@ -3743,21 +4021,6 @@
         }
     }
 
-    /**
-     * Delete outdated cache entries
-     */
-    public function expunge_cache()
-    {
-        if ($this->mcache) {
-            $ttl = rcube::get_instance()->config->get('message_cache_lifetime', '10d');
-            $this->mcache->expunge($ttl);
-        }
-
-        if ($this->cache) {
-            $this->cache->expunge();
-        }
-    }
-
 
     /* --------------------------------
      *   message caching methods
@@ -3766,12 +4029,17 @@
     /**
      * Enable or disable messages caching
      *
-     * @param boolean $set Flag
+     * @param boolean $set  Flag
+     * @param int     $mode Cache mode
      */
-    public function set_messages_caching($set)
+    public function set_messages_caching($set, $mode = null)
     {
         if ($set) {
             $this->messages_caching = true;
+
+            if ($mode && ($cache = $this->get_mcache_engine())) {
+                $cache->set_mode($mode);
+            }
         }
         else {
             if ($this->mcache) {
@@ -3791,8 +4059,10 @@
         if ($this->messages_caching && !$this->mcache) {
             $rcube = rcube::get_instance();
             if (($dbh = $rcube->get_dbh()) && ($userid = $rcube->get_user_id())) {
+                $ttl       = $rcube->config->get('messages_cache_ttl', '10d');
+                $threshold = $rcube->config->get('messages_cache_threshold', 50);
                 $this->mcache = new rcube_imap_cache(
-                    $dbh, $this, $userid, $this->options['skip_deleted']);
+                    $dbh, $this, $userid, $this->options['skip_deleted'], $ttl, $threshold);
             }
         }
 
@@ -3804,13 +4074,22 @@
      * Clears the messages cache.
      *
      * @param string $folder Folder name
-     * @param array  $uids    Optional message UIDs to remove from cache
+     * @param array  $uids   Optional message UIDs to remove from cache
      */
     protected function clear_message_cache($folder = null, $uids = null)
     {
         if ($mcache = $this->get_mcache_engine()) {
             $mcache->clear($folder, $uids);
         }
+    }
+
+
+    /**
+     * Delete outdated cache entries
+     */
+    function cache_gc()
+    {
+        rcube_imap_cache::gc();
     }
 
 
@@ -3838,21 +4117,25 @@
     /**
      * Sort folders first by default folders and then in alphabethical order
      *
-     * @param array $a_folders Folders list
+     * @param array $a_folders    Folders list
+     * @param bool  $skip_default Skip default folders handling
+     *
+     * @return array Sorted list
      */
-    protected function sort_folder_list($a_folders)
+    public function sort_folder_list($a_folders, $skip_default = false)
     {
         $a_out = $a_defaults = $folders = array();
 
         $delimiter = $this->get_hierarchy_delimiter();
+        $specials  = array_merge(array('INBOX'), array_values($this->get_special_folders()));
 
         // find default folders and skip folders starting with '.'
-        foreach ($a_folders as $i => $folder) {
+        foreach ($a_folders as $folder) {
             if ($folder[0] == '.') {
                 continue;
             }
 
-            if (($p = array_search($folder, $this->default_folders)) !== false && !$a_defaults[$p]) {
+            if (!$skip_default && ($p = array_search($folder, $specials)) !== false && !$a_defaults[$p]) {
                 $a_defaults[$p] = $folder;
             }
             else {
@@ -4107,9 +4390,9 @@
         return $this->index($folder, $sort_field, $sort_order);
     }
 
-    public function message_index_direct($folder, $sort_field = null, $sort_order = null, $skip_cache = true)
+    public function message_index_direct($folder, $sort_field = null, $sort_order = null)
     {
-        return $this->index_direct($folder, $sort_field, $sort_order, $skip_cache);
+        return $this->index_direct($folder, $sort_field, $sort_order);
     }
 
     public function list_mailboxes($root='', $name='*', $filter=null, $rights=null, $skip_sort=false)

--
Gitblit v1.9.1