From a03233cebafc62ec7d88a24856b8a7b37fef4381 Mon Sep 17 00:00:00 2001
From: Aleksander Machniak <alec@alec.pl>
Date: Wed, 07 Oct 2015 03:14:18 -0400
Subject: [PATCH] CS fixes

---
 program/lib/Roundcube/rcube_imap.php |  742 +++++++++++++++++++++++++++++++-------------------------
 1 files changed, 414 insertions(+), 328 deletions(-)

diff --git a/program/lib/Roundcube/rcube_imap.php b/program/lib/Roundcube/rcube_imap.php
index 5c30327..2940446 100644
--- a/program/lib/Roundcube/rcube_imap.php
+++ b/program/lib/Roundcube/rcube_imap.php
@@ -1,6 +1,6 @@
 <?php
 
-/*
+/**
  +-----------------------------------------------------------------------+
  | This file is part of the Roundcube Webmail client                     |
  | Copyright (C) 2005-2012, The Roundcube Dev Team                       |
@@ -56,14 +56,12 @@
      */
     protected $icache = array();
 
-    protected $list_page = 1;
+    protected $plugins;
     protected $delimiter;
     protected $namespace;
     protected $sort_field = '';
     protected $sort_order = 'DESC';
     protected $struct_charset;
-    protected $uid_id_map = array();
-    protected $msg_headers = array();
     protected $search_set;
     protected $search_string = '';
     protected $search_charset = '';
@@ -81,7 +79,8 @@
      */
     public function __construct()
     {
-        $this->conn = new rcube_imap_generic();
+        $this->conn    = new rcube_imap_generic();
+        $this->plugins = rcube::get_instance()->plugins;
 
         // Set namespace and delimiter from session,
         // so some methods would work before connection
@@ -92,7 +91,6 @@
             $this->delimiter = $_SESSION['imap_delimiter'];
         }
     }
-
 
     /**
      * Magic getter for backward compat.
@@ -106,17 +104,16 @@
         }
     }
 
-
     /**
      * Connect to an IMAP server
      *
-     * @param  string   $host    Host to connect
-     * @param  string   $user    Username for IMAP account
-     * @param  string   $pass    Password for IMAP account
-     * @param  integer  $port    Port to connect to
-     * @param  string   $use_ssl SSL schema (either ssl or tls) or null if plain connection
+     * @param string  $host    Host to connect
+     * @param string  $user    Username for IMAP account
+     * @param string  $pass    Password for IMAP account
+     * @param integer $port    Port to connect to
+     * @param string  $use_ssl SSL schema (either ssl or tls) or null if plain connection
      *
-     * @return boolean  TRUE on success, FALSE on failure
+     * @return boolean True on success, False on failure
      */
     public function connect($host, $user, $pass, $port=143, $use_ssl=null)
     {
@@ -147,7 +144,7 @@
 
         $attempt = 0;
         do {
-            $data = rcube::get_instance()->plugins->exec_hook('storage_connect',
+            $data = $this->plugins->exec_hook('storage_connect',
                 array_merge($this->options, array('host' => $host, 'user' => $user,
                     'attempt' => ++$attempt)));
 
@@ -170,8 +167,20 @@
         $this->connect_done = true;
 
         if ($this->conn->connected()) {
+            // check for session identifier
+            $session = null;
+            if (preg_match('/\s+SESSIONID=([^=\s]+)/', $this->conn->result, $m)) {
+                $session = $m[1];
+            }
+
             // get namespace and delimiter
             $this->set_env();
+
+            // trigger post-connect hook
+            $this->plugins->exec_hook('storage_connected', array(
+                'host' => $host, 'user' => $user, 'session' => $session
+            ));
+
             return true;
         }
         // write error log
@@ -189,7 +198,6 @@
         return false;
     }
 
-
     /**
      * Close IMAP connection.
      * Usually done on script shutdown
@@ -201,7 +209,6 @@
             $this->mcache->close();
         }
     }
-
 
     /**
      * Check connection state, connect if not connected.
@@ -224,7 +231,6 @@
         return $this->is_connected();
     }
 
-
     /**
      * Checks IMAP connection.
      *
@@ -234,7 +240,6 @@
     {
         return $this->conn->connected();
     }
-
 
     /**
      * Returns code of last error
@@ -246,7 +251,6 @@
         return $this->conn->errornum;
     }
 
-
     /**
      * Returns text of last error
      *
@@ -256,7 +260,6 @@
     {
         return $this->conn->error;
     }
-
 
     /**
      * Returns code of last command response
@@ -287,7 +290,6 @@
         }
     }
 
-
     /**
      * Activate/deactivate debug mode
      *
@@ -299,23 +301,21 @@
         $this->conn->setDebug($dbg, array($this, 'debug_handler'));
     }
 
-
     /**
      * Set internal folder reference.
      * All operations will be perfomed on this folder.
      *
-     * @param  string $folder Folder name
+     * @param string $folder Folder name
      */
     public function set_folder($folder)
     {
         $this->folder = $folder;
     }
 
-
     /**
      * Save a search result for future message listing methods
      *
-     * @param  array  $set  Search set, result from rcube_imap::get_search_set():
+     * @param array $set Search set, result from rcube_imap::get_search_set():
      *                      0 - searching criteria, string
      *                      1 - search result, rcube_result_index|rcube_result_thread
      *                      2 - searching character set, string
@@ -338,7 +338,6 @@
         }
     }
 
-
     /**
      * Return the saved search set as hash array
      *
@@ -358,7 +357,6 @@
             $this->search_sorted,
         );
     }
-
 
     /**
      * Returns the IMAP server's capability.
@@ -383,14 +381,13 @@
         return $_SESSION[$sess_key];
     }
 
-
     /**
      * Checks the PERMANENTFLAGS capability of the current folder
      * and returns true if the given flag is supported by the IMAP server
      *
-     * @param   string  $flag Permanentflag name
+     * @param string $flag Permanentflag name
      *
-     * @return  boolean True if this flag is supported
+     * @return boolean True if this flag is supported
      */
     public function check_permflag($flag)
     {
@@ -401,11 +398,10 @@
         return $imap_flag && !empty($perm_flags) && in_array_nocase($imap_flag, $perm_flags);
     }
 
-
     /**
      * Returns PERMANENTFLAGS of the specified folder
      *
-     * @param  string $folder Folder name
+     * @param string $folder Folder name
      *
      * @return array Flags
      */
@@ -433,25 +429,22 @@
         return $permflags;
     }
 
-
     /**
      * Returns the delimiter that is used by the IMAP server for folder separation
      *
-     * @return  string  Delimiter string
-     * @access  public
+     * @return string Delimiter string
      */
     public function get_hierarchy_delimiter()
     {
         return $this->delimiter;
     }
 
-
     /**
      * Get namespace
      *
      * @param string $name Namespace array index: personal, other, shared, prefix
      *
-     * @return  array  Namespace data
+     * @return array Namespace data
      */
     public function get_namespace($name = null)
     {
@@ -465,7 +458,6 @@
         return $ns;
     }
 
-
     /**
      * Sets delimiter and namespaces
      */
@@ -475,7 +467,7 @@
             return;
         }
 
-        $config = rcube::get_instance()->config;
+        $config         = rcube::get_instance()->config;
         $imap_personal  = $config->get('imap_ns_personal');
         $imap_other     = $config->get('imap_ns_other');
         $imap_shared    = $config->get('imap_ns_shared');
@@ -546,6 +538,51 @@
         $_SESSION['imap_delimiter'] = $this->delimiter;
     }
 
+    /**
+     * Returns IMAP server vendor name
+     *
+     * @return string Vendor name
+     * @since 1.2
+     */
+    public function get_vendor()
+    {
+        if ($_SESSION['imap_vendor'] !== null) {
+            return $_SESSION['imap_vendor'];
+        }
+
+        $config      = rcube::get_instance()->config;
+        $imap_vendor = $config->get('imap_vendor');
+
+        if ($imap_vendor) {
+            return $imap_vendor;
+        }
+
+        if (!$this->check_connection()) {
+            return;
+        }
+
+        if (($ident = $this->conn->data['ID']) === null) {
+            $ident = $this->conn->id(array(
+                    'name'    => 'Roundcube',
+                    'version' => RCUBE_VERSION,
+                    'php'     => PHP_VERSION,
+                    'os'      => PHP_OS,
+            ));
+        }
+
+        $vendor  = (string) (!empty($ident) ? $ident['name'] : '');
+        $ident   = strtolower($vendor . ' ' . $this->conn->data['GREETING']);
+        $vendors = array('cyrus', 'dovecot', 'uw-imap', 'gmail', 'hmail');
+
+        foreach ($vendors as $v) {
+            if (strpos($ident, $v) !== false) {
+                $vendor = $v;
+                break;
+            }
+        }
+
+        return $_SESSION['imap_vendor'] = $vendor;
+    }
 
     /**
      * Get message count for a specific folder
@@ -556,7 +593,7 @@
      * @param  boolean $status  Enables storing folder status info (max UID/count),
      *                          required for folder_status()
      *
-     * @return int     Number of messages
+     * @return int Number of messages
      */
     public function count($folder='', $mode='ALL', $force=false, $status=true)
     {
@@ -567,29 +604,31 @@
         return $this->countmessages($folder, $mode, $force, $status);
     }
 
-
     /**
-     * protected method for getting nr of messages
+     * Protected method for getting number of messages
      *
-     * @param string  $folder  Folder name
-     * @param string  $mode    Mode for count [ALL|THREADS|UNSEEN|RECENT|EXISTS]
-     * @param boolean $force   Force reading from server and update cache
-     * @param boolean $status  Enables storing folder status info (max UID/count),
-     *                         required for folder_status()
+     * @param string  $folder    Folder name
+     * @param string  $mode      Mode for count [ALL|THREADS|UNSEEN|RECENT|EXISTS]
+     * @param boolean $force     Force reading from server and update cache
+     * @param boolean $status    Enables storing folder status info (max UID/count),
+     *                           required for folder_status()
+     * @param boolean $no_search Ignore current search result
      *
      * @return int Number of messages
      * @see rcube_imap::count()
      */
-    protected function countmessages($folder, $mode='ALL', $force=false, $status=true)
+    protected function countmessages($folder, $mode = 'ALL', $force = false, $status = true, $no_search = false)
     {
         $mode = strtoupper($mode);
 
-        // count search set, assume search set is always up-to-date (don't check $force flag)
-        if ($this->search_string && $folder == $this->folder && ($mode == 'ALL' || $mode == 'THREADS')) {
+        // Count search set, assume search set is always up-to-date (don't check $force flag)
+        // @TODO: this could be handled in more reliable way, e.g. a separate method
+        //        maybe in rcube_imap_search
+        if (!$no_search && $this->search_string && $folder == $this->folder) {
             if ($mode == 'ALL') {
                 return $this->search_set->count_messages();
             }
-            else {
+            else if ($mode == 'THREADS') {
                 return $this->search_set->count();
             }
         }
@@ -682,7 +721,6 @@
         return (int)$count;
     }
 
-
     /**
      * Public method for listing message flags
      *
@@ -717,7 +755,6 @@
         return $result;
     }
 
-
     /**
      * Public method for listing headers
      *
@@ -727,7 +764,7 @@
      * @param   string   $sort_order Sort order [ASC|DESC]
      * @param   int      $slice      Number of slice items to extract from result array
      *
-     * @return  array    Indexed array with message header objects
+     * @return array Indexed array with message header objects
      */
     public function list_messages($folder='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
     {
@@ -738,7 +775,6 @@
         return $this->_list_messages($folder, $page, $sort_field, $sort_order, $slice);
     }
 
-
     /**
      * protected method for listing message headers
      *
@@ -748,8 +784,8 @@
      * @param   string   $sort_order Sort order [ASC|DESC]
      * @param   int      $slice      Number of slice items to extract from result array
      *
-     * @return  array    Indexed array with message header objects
-     * @see     rcube_imap::list_messages
+     * @return array Indexed array with message header objects
+     * @see rcube_imap::list_messages
      */
     protected function _list_messages($folder='', $page=NULL, $sort_field=NULL, $sort_order=NULL, $slice=0)
     {
@@ -761,7 +797,7 @@
         $page = $page ? $page : $this->list_page;
 
         // use saved message set
-        if ($this->search_string && $folder == $this->folder) {
+        if ($this->search_string) {
             return $this->list_search_messages($folder, $page, $slice);
         }
 
@@ -792,16 +828,15 @@
         return array_values($a_msg_headers);
     }
 
-
     /**
      * protected method for listing message headers using threads
      *
-     * @param   string   $folder     Folder name
-     * @param   int      $page       Current page to list
-     * @param   int      $slice      Number of slice items to extract from result array
+     * @param string $folder Folder name
+     * @param int    $page   Current page to list
+     * @param int    $slice  Number of slice items to extract from result array
      *
-     * @return  array    Indexed array with message header objects
-     * @see     rcube_imap::list_messages
+     * @return array Indexed array with message header objects
+     * @see rcube_imap::list_messages
      */
     protected function list_thread_messages($folder, $page, $slice=0)
     {
@@ -819,7 +854,7 @@
     /**
      * Method for fetching threads data
      *
-     * @param  string $folder Folder name
+     * @param string $folder Folder name
      *
      * @return rcube_imap_thread Thread data object
      */
@@ -843,11 +878,10 @@
         return $this->icache['threads'] = $result;
     }
 
-
     /**
      * Method for direct fetching of threads data
      *
-     * @param  string $folder Folder name
+     * @param string $folder Folder name
      *
      * @return rcube_imap_thread Thread data object
      */
@@ -862,7 +896,6 @@
             $this->options['skip_deleted'] ? 'UNDELETED' : '', true);
     }
 
-
     /**
      * protected method for fetching threaded messages headers
      *
@@ -871,7 +904,7 @@
      * @param int                 $page       List page number
      * @param int                 $slice      Number of threads to slice
      *
-     * @return array  Messages headers
+     * @return array Messages headers
      */
     protected function fetch_thread_headers($folder, $threads, $page, $slice=0)
     {
@@ -901,13 +934,12 @@
         return array_values($a_msg_headers);
     }
 
-
     /**
      * protected method for setting threaded messages flags:
      * depth, has_children and unread_children
      *
-     * @param  array               $headers  Reference to headers array indexed by message UID
-     * @param  rcube_result_thread $threads  Threads data object
+     * @param array               $headers  Reference to headers array indexed by message UID
+     * @param rcube_result_thread $threads  Threads data object
      *
      * @return array Message headers array indexed by message UID
      */
@@ -933,7 +965,6 @@
         }
     }
 
-
     /**
      * protected method for listing a set of message headers (search results)
      *
@@ -941,7 +972,7 @@
      * @param   int      $page     Current page to list
      * @param   int      $slice    Number of slice items to extract from result array
      *
-     * @return  array    Indexed array with message header objects
+     * @return array Indexed array with message header objects
      */
     protected function list_search_messages($folder, $page, $slice=0)
     {
@@ -951,7 +982,7 @@
 
         // gather messages from a multi-folder search
         if ($this->search_set->multi) {
-            $page_size = $this->page_size;
+            $page_size  = $this->page_size;
             $sort_field = $this->sort_field;
             $search_set = $this->search_set;
 
@@ -962,23 +993,26 @@
             $slice_length = min($page_size, $cnt - $from);
 
             // fetch resultset headers, sort and slice them
-            if (!empty($sort_field)) {
+            if (!empty($sort_field) && $search_set->get_parameters('SORT') != $sort_field) {
                 $this->sort_field = null;
-                $this->page_size = 1000;  // fetch up to 1000 matching messages per folder
-                $this->threading = false;
+                $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_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));
+
+                        $a_headers     =  $this->list_search_messages($resultset->get_parameters('MAILBOX'), 1);
+                        $a_msg_headers = array_merge($a_msg_headers, $a_headers);
+                        unset($a_headers);
                     }
                 }
 
                 // sort headers
                 if (!empty($a_msg_headers)) {
-                    $a_msg_headers = $this->conn->sortHeaders($a_msg_headers, $sort_field, $this->sort_order);
+                    $a_msg_headers = rcube_imap_generic::sortHeaders($a_msg_headers, $sort_field, $this->sort_order);
                 }
 
                 // store (sorted) message index
@@ -1012,7 +1046,7 @@
 
             // restore members
             $this->sort_field = $sort_field;
-            $this->page_size = $page_size;
+            $this->page_size  = $page_size;
             $this->search_set = $search_set;
 
             return $a_msg_headers;
@@ -1106,12 +1140,8 @@
                 return array();
             }
 
-            if (!$this->check_connection()) {
-                return array();
-            }
-
             // if not already sorted
-            $a_msg_headers = $this->conn->sortHeaders(
+            $a_msg_headers = rcube_imap_generic::sortHeaders(
                 $a_msg_headers, $this->sort_field, $this->sort_order);
 
             // only return the requested part of the set
@@ -1126,7 +1156,6 @@
         }
     }
 
-
     /**
      * protected method for listing a set of threaded message headers (search results)
      *
@@ -1134,7 +1163,7 @@
      * @param   int      $page       Current page to list
      * @param   int      $slice      Number of slice items to extract from result array
      *
-     * @return  array    Indexed array with message header objects
+     * @return array Indexed array with message header objects
      * @see rcube_imap::list_search_messages()
      */
     protected function list_search_thread_messages($folder, $page, $slice=0)
@@ -1149,7 +1178,6 @@
 
         return $this->fetch_thread_headers($folder, clone $this->search_set, $page, $slice);
     }
-
 
     /**
      * Fetches messages headers (by UID)
@@ -1198,7 +1226,6 @@
         return $a_msg_headers;
     }
 
-
     /**
      * Returns current status of a folder (compared to the last time use)
      *
@@ -1218,7 +1245,7 @@
         $old = $this->get_folder_stats($folder);
 
         // refresh message count -> will update
-        $this->countmessages($folder, 'ALL', true);
+        $this->countmessages($folder, 'ALL', true, true, true);
 
         $result = 0;
 
@@ -1246,7 +1273,6 @@
         return $result;
     }
 
-
     /**
      * Stores folder statistic data in session
      * @TODO: move to separate DB table (cache?)
@@ -1259,7 +1285,6 @@
     {
         $_SESSION['folders'][$folder][$name] = $data;
     }
-
 
     /**
      * Gets folder statistic data
@@ -1276,7 +1301,6 @@
 
         return array();
     }
-
 
     /**
      * Return sorted list of message UIDs
@@ -1355,7 +1379,6 @@
         return $this->index_direct($folder, $this->sort_field, $this->sort_order);
     }
 
-
     /**
      * Return sorted list of message UIDs ignoring current search settings.
      * Doesn't uses cache by default.
@@ -1370,7 +1393,7 @@
     public function index_direct($folder, $sort_field = null, $sort_order = null, $search = null)
     {
         if (!empty($search)) {
-            $search = $this->search_set->get_compressed();
+            $search = $search->get_compressed();
         }
 
         // use message index sort as default sorting
@@ -1423,7 +1446,6 @@
         return $index;
     }
 
-
     /**
      * Return index of threaded message UIDs
      *
@@ -1454,7 +1476,6 @@
         return $threads;
     }
 
-
     /**
      * Sort threaded result, using THREAD=REFS method if available.
      * If not, use any method and re-sort the result in THREAD=REFS way.
@@ -1484,28 +1505,43 @@
         }
     }
 
-
     /**
      * Invoke search request to IMAP server
      *
      * @param  string  $folder     Folder name to search in
-     * @param  string  $str        Search criteria
+     * @param  string  $search     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)
+    public function search($folder = '', $search = 'ALL', $charset = null, $sort_field = null)
     {
-        if (!$str) {
-            $str = 'ALL';
+        if (!$search) {
+            $search = 'ALL';
         }
 
-        // 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;
+        if ((is_array($folder) && empty($folder)) || (!is_array($folder) && !strlen($folder))) {
+            $folder = $this->folder;
+        }
 
+        $plugin = $this->plugins->exec_hook('imap_search_before', array(
+            'folder'     => $folder,
+            'search'     => $search,
+            'charset'    => $charset,
+            'sort_field' => $sort_field,
+            'threading'  => $this->threading,
+        ));
+
+        $folder     = $plugin['folder'];
+        $search     = $plugin['search'];
+        $charset    = $plugin['charset'];
+        $sort_field = $plugin['sort_field'];
+        $results    = $plugin['result'];
+
+        // multi-folder search
+        if (!$results && is_array($folder) && count($folder) > 1 && $search != 'ALL') {
             // connect IMAP to have all the required classes and settings loaded
             $this->check_connection();
 
@@ -1513,26 +1549,36 @@
             $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 && $search == $this->search_string) {
+                $searcher->set_results($this->search_set);
+            }
+
+            // execute the search
             $results = $searcher->exec(
                 $folder,
-                $str,
+                $search,
                 $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);
+        else if (!$results) {
+            $folder  = is_array($folder) ? $folder[0] : $folder;
+            $search  = is_array($search) ? $search[$folder] : $search;
+            $results = $this->search_index($folder, $search, $charset, $sort_field);
         }
 
-        $this->set_search_set(array($str, $results, $charset, $sort_field,
-            $this->threading || $this->search_sorted ? true : false));
-    }
+        $sorted = $this->threading || $this->search_sorted || $plugin['search_sorted'] ? true : false;
 
+        $this->set_search_set(array($search, $results, $charset, $sort_field, $sorted));
+
+        return $results;
+    }
 
     /**
      * Direct (real and simple) SEARCH request (without result sorting and caching).
@@ -1544,23 +1590,29 @@
      */
     public function search_once($folder = null, $str = 'ALL')
     {
-        if (!$str) {
-            $str = '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;
     }
-
 
     /**
      * protected search method
@@ -1638,7 +1690,6 @@
         return $messages;
     }
 
-
     /**
      * Converts charset of search criteria string
      *
@@ -1657,12 +1708,15 @@
                 $string_offset = $m[1] + strlen($m[0]) + 4; // {}\r\n
                 $string = substr($str, $string_offset - 1, $m[0]);
                 $string = rcube_charset::convert($string, $charset, $dest_charset);
-                if ($string === false) {
+
+                if ($string === false || !strlen($string)) {
                     continue;
                 }
+
                 $res .= substr($str, $last, $m[1] - $last - 1) . rcube_imap_generic::escape($string);
                 $last = $m[0] + $string_offset - 1;
             }
+
             if ($last < strlen($str)) {
                 $res .= substr($str, $last, strlen($str)-$last);
             }
@@ -1674,7 +1728,6 @@
 
         return $res;
     }
-
 
     /**
      * Refresh saved search set
@@ -1695,6 +1748,18 @@
         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;
+            }
+        }
+    }
 
     /**
      * Return message headers object of a specific message
@@ -1707,13 +1772,13 @@
      */
     public function get_message_headers($uid, $folder = null, $force = false)
     {
-        if (!strlen($folder)) {
-            $folder = $this->folder;
-        }
-
         // decode combined UID-folder identifier
         if (preg_match('/^\d+-.+/', $uid)) {
             list($uid, $folder) = explode('-', $uid, 2);
+        }
+
+        if (!strlen($folder)) {
+            $folder = $this->folder;
         }
 
         // get cached headers
@@ -1726,18 +1791,20 @@
         else {
             $headers = $this->conn->fetchHeader(
                 $folder, $uid, true, true, $this->get_fetch_headers());
+
+            if (is_object($headers))
+                $headers->folder = $folder;
         }
 
         return $headers;
     }
 
-
     /**
      * Fetch message headers and body structure from the IMAP server and build
-     * an object structure similar to the one generated by PEAR::Mail_mimeDecode
+     * an object structure.
      *
-     * @param int     $uid      Message UID to fetch
-     * @param string  $folder   Folder to read from
+     * @param int    $uid    Message UID to fetch
+     * @param string $folder Folder to read from
      *
      * @return object rcube_message_header Message data
      */
@@ -1795,7 +1862,7 @@
             $this->struct_charset = $this->structure_charset($structure);
         }
 
-        $headers->ctype = strtolower($headers->ctype);
+        $headers->ctype = @strtolower($headers->ctype);
 
         // Here we can recognize malformed BODYSTRUCTURE and
         // 1. [@TODO] parse the message in other way to create our own message structure
@@ -1816,8 +1883,8 @@
                 $structure[1] = $m[2];
             }
             else {
-                // Try to parse the message using Mail_mimeDecode package
-                // We need a better solution, Mail_mimeDecode parses message
+                // Try to parse the message using rcube_mime_decode.
+                // We need a better solution, it parses message
                 // in memory, which wouldn't work for very big messages,
                 // (it uses up to 10x more memory than the message size)
                 // it's also buggy and not actively developed
@@ -1857,7 +1924,6 @@
         return $this->icache['message'] = $headers;
     }
 
-
     /**
      * Build message part object
      *
@@ -1865,7 +1931,7 @@
      * @param int    $count
      * @param string $parent
      */
-    protected function structure_part($part, $count=0, $parent='', $mime_headers=null)
+    protected function structure_part($part, $count = 0, $parent = '', $mime_headers = null)
     {
         $struct = new rcube_message_part;
         $struct->mime_id = empty($parent) ? (string)$count : "$parent.$count";
@@ -1890,6 +1956,16 @@
             for ($i=1; $i<count($part); $i++) {
                 if (!is_array($part[$i])) {
                     $struct->ctype_secondary = strtolower($part[$i]);
+
+                    // read content type parameters
+                    if (is_array($part[$i+1])) {
+                        $struct->ctype_parameters = array();
+                        for ($j=0; $j<count($part[$i+1]); $j+=2) {
+                            $param = strtolower($part[$i+1][$j]);
+                            $struct->ctype_parameters[$param] = $part[$i+1][$j+1];
+                        }
+                    }
+
                     break;
                 }
             }
@@ -2073,14 +2149,13 @@
         return $struct;
     }
 
-
     /**
      * Set attachment filename from message part structure
      *
-     * @param  rcube_message_part $part    Part object
-     * @param  string             $headers Part's raw headers
+     * @param rcube_message_part $part    Part object
+     * @param string             $headers Part's raw headers
      */
-    protected function set_part_filename(&$part, $headers=null)
+    protected function set_part_filename(&$part, $headers = null)
     {
         if (!empty($part->d_parameters['filename'])) {
             $filename_mime = $part->d_parameters['filename'];
@@ -2208,11 +2283,10 @@
         }
     }
 
-
     /**
      * Get charset name from message structure (first part)
      *
-     * @param  array $structure Message structure
+     * @param array $structure Message structure
      *
      * @return string Charset name
      */
@@ -2241,7 +2315,8 @@
      *
      * @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, $formatted=true)
+    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;
@@ -2293,42 +2368,41 @@
         return $body;
     }
 
-
     /**
      * Returns the whole message source as string (or saves to a file)
      *
-     * @param int      $uid Message UID
-     * @param resource $fp  File pointer to save the message
+     * @param int      $uid  Message UID
+     * @param resource $fp   File pointer to save the message
+     * @param string   $part Optional message part ID
      *
      * @return string Message source string
      */
-    public function get_raw_body($uid, $fp=null)
+    public function get_raw_body($uid, $fp=null, $part = null)
     {
         if (!$this->check_connection()) {
             return null;
         }
 
         return $this->conn->handlePartBody($this->folder, $uid,
-            true, null, null, false, $fp);
+            true, $part, null, false, $fp);
     }
-
 
     /**
      * Returns the message headers as string
      *
-     * @param int $uid  Message UID
+     * @param int    $uid  Message UID
+     * @param string $part Optional message part ID
      *
      * @return string Message headers string
      */
-    public function get_raw_headers($uid)
+    public function get_raw_headers($uid, $part = null)
     {
         if (!$this->check_connection()) {
             return null;
         }
 
-        return $this->conn->fetchPartHeader($this->folder, $uid, true);
+        return $this->conn->fetchPartHeader($this->folder, $uid, true, $part);
     }
-
 
     /**
      * Sends the whole message source to stdout
@@ -2345,13 +2419,12 @@
         $this->conn->handlePartBody($this->folder, $uid, true, null, null, true, null, $formatted);
     }
 
-
     /**
      * Set message flag to one or several messages
      *
      * @param mixed   $uids       Message UIDs as array or comma-separated string, or '*'
      * @param string  $flag       Flag to set: SEEN, UNDELETED, DELETED, RECENT, ANSWERED, DRAFT, MDNSENT
-     * @param string  $folder    Folder name
+     * @param string  $folder     Folder name
      * @param boolean $skip_cache True to skip message cache clean up
      *
      * @return boolean  Operation status
@@ -2398,11 +2471,12 @@
                     $this->clear_message_cache($folder, $all_mode ? null : explode(',', $uids));
                 }
             }
+
+            $this->set_search_dirty($folder);
         }
 
         return $result;
     }
-
 
     /**
      * Append a mail message (source) to a specific folder
@@ -2445,11 +2519,21 @@
         if ($saved) {
             // increase messagecount of the target folder
             $this->set_messagecount($folder, 'ALL', 1);
+
+            $this->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;
     }
-
 
     /**
      * Move a message from one folder to another
@@ -2496,6 +2580,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)) {
@@ -2512,7 +2599,7 @@
                 if ($this->search_threads || $all_mode) {
                     $this->refresh_search();
                 }
-                else {
+                else if (!$this->search_set->incomplete) {
                     $this->search_set->filter(explode(',', $uids), $this->folder);
                 }
             }
@@ -2524,7 +2611,6 @@
 
         return $moved;
     }
-
 
     /**
      * Copy a message from one folder to another
@@ -2562,7 +2648,6 @@
         return $copied;
     }
 
-
     /**
      * Mark messages as deleted and expunge them
      *
@@ -2595,10 +2680,11 @@
             // really deleted from the folder
             $this->expunge_message($uids, $folder, false);
             $this->clear_messagecount($folder);
-            unset($this->uid_id_map[$folder]);
 
             // 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) {
@@ -2606,7 +2692,7 @@
                 if ($this->search_threads || $all_mode) {
                     $this->refresh_search();
                 }
-                else {
+                else if (!$this->search_set->incomplete) {
                     $this->search_set->filter(explode(',', $uids));
                 }
             }
@@ -2617,7 +2703,6 @@
 
         return $deleted;
     }
-
 
     /**
      * Send IMAP expunge command and clear cache
@@ -2706,7 +2791,7 @@
         }
 
         // Give plugins a chance to provide a list of folders
-        $data = rcube::get_instance()->plugins->exec_hook('storage_folders',
+        $data = $this->plugins->exec_hook('storage_folders',
             array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LSUB'));
 
         if (isset($data['folders'])) {
@@ -2741,15 +2826,14 @@
         return $a_mboxes;
     }
 
-
     /**
      * Method for direct folders listing (LSUB)
      *
-     * @param   string  $root   Optional root folder
-     * @param   string  $name   Optional name pattern
+     * @param string $root Optional root folder
+     * @param string $name Optional name pattern
      *
-     * @return  array   List of subscribed folders
-     * @see     rcube_imap::list_folders_subscribed()
+     * @return array List of subscribed folders
+     * @see rcube_imap::list_folders_subscribed()
      */
     public function list_folders_subscribed_direct($root='', $name='*')
     {
@@ -2764,42 +2848,47 @@
         $list_extended = !$config->get('imap_force_lsub') && $this->get_capability('LIST-EXTENDED');
         if ($list_extended) {
             // This will also set folder options, LSUB doesn't do that
-            $a_folders = $this->conn->listMailboxes($root, $name,
+            $result = $this->conn->listMailboxes($root, $name,
                 NULL, array('SUBSCRIBED'));
         }
         else {
             // retrieve list of folders from IMAP server using LSUB
-            $a_folders = $this->conn->listSubscribed($root, $name);
+            $result = $this->conn->listSubscribed($root, $name);
         }
 
-        if (!is_array($a_folders)) {
+        if (!is_array($result)) {
             return array();
         }
 
         // #1486796: some server configurations doesn't return folders in all namespaces
         if ($root == '' && $name == '*' && $config->get('imap_force_ns')) {
-            $this->list_folders_update($a_folders, ($list_extended ? 'ext-' : '') . 'subscribed');
+            $this->list_folders_update($result, ($list_extended ? 'ext-' : '') . 'subscribed');
+        }
+
+        // Remove hidden folders
+        if ($config->get('imap_skip_hidden_folders')) {
+            $result = array_filter($result, function($v) { return $v[0] != '.'; });
         }
 
         if ($list_extended) {
             // unsubscribe non-existent folders, remove from the list
-            if (is_array($a_folders) && $name == '*' && !empty($this->conn->data['LIST'])) {
-                foreach ($a_folders as $idx => $folder) {
+            if ($name == '*' && !empty($this->conn->data['LIST'])) {
+                foreach ($result as $idx => $folder) {
                     if (($opts = $this->conn->data['LIST'][$folder])
-                        && in_array('\\NonExistent', $opts)
+                        && in_array_nocase('\\NonExistent', $opts)
                     ) {
                         $this->conn->unsubscribe($folder);
-                        unset($a_folders[$idx]);
+                        unset($result[$idx]);
                     }
                 }
             }
         }
         else {
             // unsubscribe non-existent folders, remove them from the list
-            if (is_array($a_folders) && !empty($a_folders) && $name == '*') {
+            if (!empty($result) && $name == '*') {
                 $existing    = $this->list_folders($root, $name);
-                $nonexisting = array_diff($a_folders, $existing);
-                $a_folders   = array_diff($a_folders, $nonexisting);
+                $nonexisting = array_diff($result, $existing);
+                $result      = array_diff($result, $nonexisting);
 
                 foreach ($nonexisting as $folder) {
                     $this->conn->unsubscribe($folder);
@@ -2807,9 +2896,8 @@
             }
         }
 
-        return $a_folders;
+        return $result;
     }
-
 
     /**
      * Get a list of all folders available on the server
@@ -2838,7 +2926,7 @@
         }
 
         // Give plugins a chance to provide a list of folders
-        $data = rcube::get_instance()->plugins->exec_hook('storage_folders',
+        $data = $this->plugins->exec_hook('storage_folders',
             array('root' => $root, 'name' => $name, 'filter' => $filter, 'mode' => 'LIST'));
 
         if (isset($data['folders'])) {
@@ -2879,15 +2967,14 @@
         return $a_mboxes;
     }
 
-
     /**
      * Method for direct folders listing (LIST)
      *
-     * @param   string  $root   Optional root folder
-     * @param   string  $name   Optional name pattern
+     * @param string $root Optional root folder
+     * @param string $name Optional name pattern
      *
-     * @return  array   List of folders
-     * @see     rcube_imap::list_folders()
+     * @return array List of folders
+     * @see rcube_imap::list_folders()
      */
     public function list_folders_direct($root='', $name='*')
     {
@@ -2908,18 +2995,22 @@
             $this->list_folders_update($result);
         }
 
+        // Remove hidden folders
+        if ($config->get('imap_skip_hidden_folders')) {
+            $result = array_filter($result, function($v) { return $v[0] != '.'; });
+        }
+
         return $result;
     }
-
 
     /**
      * Fix folders list by adding folders from other namespaces.
      * Needed on some servers eg. Courier IMAP
      *
-     * @param array  $result  Reference to folders list
-     * @param string $type    Listing type (ext-subscribed, subscribed or all)
+     * @param array  $result Reference to folders list
+     * @param string $type   Listing type (ext-subscribed, subscribed or all)
      */
-    private function list_folders_update(&$result, $type = null)
+    protected function list_folders_update(&$result, $type = null)
     {
         $namespace = $this->get_namespace();
         $search    = array();
@@ -2967,7 +3058,6 @@
         }
     }
 
-
     /**
      * Filter the given list of folders according to access rights
      *
@@ -2993,22 +3083,21 @@
         return $a_folders;
     }
 
-
     /**
      * Get mailbox quota information
-     * added by Nuny
+     *
+     * @param string $folder Folder name
      *
      * @return mixed Quota info or False if not supported
      */
-    public function get_quota()
+    public function get_quota($folder = null)
     {
         if ($this->get_capability('QUOTA') && $this->check_connection()) {
-            return $this->conn->getQuota();
+            return $this->conn->getQuota($folder);
         }
 
         return false;
     }
-
 
     /**
      * Get folder size (size of all messages in a folder)
@@ -3019,8 +3108,22 @@
      */
     public function folder_size($folder)
     {
+        if (!strlen($folder)) {
+            return false;
+        }
+
         if (!$this->check_connection()) {
             return 0;
+        }
+
+        // On Cyrus we can use special folder annotation, which should be much faster
+        if ($this->get_vendor() == 'cyrus') {
+            $idx    = '/shared/vendor/cmu/cyrus-imapd/size';
+            $result = $this->get_metadata($folder, $idx, array(), true);
+
+            if (!empty($result) && is_numeric($result[$folder][$idx])) {
+                return $result[$folder][$idx];
+            }
         }
 
         // @TODO: could we try to use QUOTA here?
@@ -3032,7 +3135,6 @@
 
         return $result;
     }
-
 
     /**
      * Subscribe to a specific folder(s)
@@ -3047,7 +3149,6 @@
         return $this->change_subscription($folders, 'subscribe');
     }
 
-
     /**
      * Unsubscribe folder(s)
      *
@@ -3060,7 +3161,6 @@
         // let this common function do the main work
         return $this->change_subscription($folders, 'unsubscribe');
     }
-
 
     /**
      * Create a new folder on the server and register it in local cache
@@ -3091,7 +3191,6 @@
 
         return $result;
     }
-
 
     /**
      * Set a new name to an existing folder
@@ -3152,7 +3251,6 @@
         return $result;
     }
 
-
     /**
      * Remove folder from server
      *
@@ -3200,7 +3298,6 @@
         return $result;
     }
 
-
     /**
      * Detect special folder associations stored in storage backend
      */
@@ -3226,12 +3323,14 @@
         // 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;
+        if (!empty($folders)) {
+            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;
+                        }
                     }
                 }
             }
@@ -3242,7 +3341,6 @@
 
         return array_merge($result, $special);
     }
-
 
     /**
      * Set special folder associations stored in storage backend
@@ -3282,7 +3380,6 @@
         return true;
     }
 
-
     /**
      * Checks if folder exists and is subscribed
      *
@@ -3310,7 +3407,7 @@
         if ($subscription) {
             // 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])
+                && in_array_nocase('\\Subscribed', $this->conn->data['LIST'][$folder])
             ) {
                 $a_folders = array($folder);
             }
@@ -3335,7 +3432,6 @@
 
         return false;
     }
-
 
     /**
      * Returns the namespace where the folder is in
@@ -3366,7 +3462,6 @@
 
         return 'personal';
     }
-
 
     /**
      * Modify folder name according to namespace.
@@ -3406,7 +3501,6 @@
         return $folder;
     }
 
-
     /**
      * Gets folder attributes from LIST response, e.g. \Noselect, \Noinferiors
      *
@@ -3438,7 +3532,6 @@
 
         return is_array($opts) ? $opts : array();
     }
-
 
     /**
      * Gets connection (and current folder) data: UIDVALIDITY, EXISTS, RECENT,
@@ -3478,7 +3571,6 @@
 
         return $data;
     }
-
 
     /**
      * Returns extended information about the folder
@@ -3575,7 +3667,6 @@
         return $options;
     }
 
-
     /**
      * Synchronizes messages cache.
      *
@@ -3587,7 +3678,6 @@
             $mcache->synchronize($folder);
         }
     }
-
 
     /**
      * Get message header names for rcube_imap_generic::fetchHeader(s)
@@ -3640,7 +3730,6 @@
         return $this->conn->setACL($folder, $user, $acl);
     }
 
-
     /**
      * Removes any <identifier,rights> pair for the
      * specified user from the ACL for the specified
@@ -3665,7 +3754,6 @@
         return $this->conn->deleteACL($folder, $user);
     }
 
-
     /**
      * Returns the access control list for folder (GETACL)
      *
@@ -3686,7 +3774,6 @@
 
         return $this->conn->getACL($folder);
     }
-
 
     /**
      * Returns information about what rights can be granted to the
@@ -3711,7 +3798,6 @@
         return $this->conn->listRights($folder, $user);
     }
 
-
     /**
      * Returns the set of rights that the current user has to
      * folder (MYRIGHTS)
@@ -3733,7 +3819,6 @@
 
         return $this->conn->myRights($folder);
     }
-
 
     /**
      * Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
@@ -3768,7 +3853,6 @@
         return false;
     }
 
-
     /**
      * Unsets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
      *
@@ -3802,34 +3886,36 @@
         return false;
     }
 
-
     /**
      * Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION)
      *
-     * @param string $folder  Folder name (empty for server metadata)
-     * @param array  $entries Entries
-     * @param array  $options Command options (with MAXSIZE and DEPTH keys)
+     * @param string  $folder   Folder name (empty for server metadata)
+     * @param array   $entries  Entries
+     * @param array   $options  Command options (with MAXSIZE and DEPTH keys)
+     * @param bool    $force    Disables cache use
      *
      * @return array Metadata entry-value hash array on success, NULL on error
      * @since 0.5-beta
      */
-    public function get_metadata($folder, $entries, $options=array())
+    public function get_metadata($folder, $entries, $options = array(), $force = false)
     {
-        $entries = (array)$entries;
+        $entries = (array) $entries;
 
-        // create cache key
-        // @TODO: this is the simplest solution, but we do the same with folders list
-        //        maybe we should store data per-entry and merge on request
-        sort($options);
-        sort($entries);
-        $cache_key = 'mailboxes.metadata.' . $folder;
-        $cache_key .= '.' . md5(serialize($options).serialize($entries));
+        if (!$force) {
+            // create cache key
+            // @TODO: this is the simplest solution, but we do the same with folders list
+            //        maybe we should store data per-entry and merge on request
+            sort($options);
+            sort($entries);
+            $cache_key = 'mailboxes.metadata.' . $folder;
+            $cache_key .= '.' . md5(serialize($options).serialize($entries));
 
-        // get cached data
-        $cached_data = $this->get_cache($cache_key);
+            // get cached data
+            $cached_data = $this->get_cache($cache_key);
 
-        if (is_array($cached_data)) {
-            return $cached_data;
+            if (is_array($cached_data)) {
+                return $cached_data;
+            }
         }
 
         if (!$this->check_connection()) {
@@ -3853,20 +3939,28 @@
 
             // @TODO: Honor MAXSIZE and DEPTH options
             foreach ($queries as $attrib => $entry) {
-                if ($result = $this->conn->getAnnotation($folder, $entry, $attrib)) {
-                    $res = array_merge_recursive($res, $result);
+                $result = $this->conn->getAnnotation($folder, $entry, $attrib);
+
+                // an error, invalidate any previous getAnnotation() results
+                if (!is_array($result)) {
+                    return null;
+                }
+                else {
+                    foreach ($result as $fldr => $data) {
+                        $res[$fldr] = array_merge((array) $res[$fldr], $data);
+                    }
                 }
             }
         }
 
         if (isset($res)) {
-            $this->update_cache($cache_key, $res);
+            if (!$force) {
+                $this->update_cache($cache_key, $res);
+            }
+
             return $res;
         }
-
-        return null;
     }
-
 
     /**
      * Converts the METADATA extension entry name into the correct
@@ -3886,7 +3980,6 @@
         }
 
         // @TODO: log error
-        return null;
     }
 
 
@@ -3997,7 +4090,6 @@
         }
     }
 
-
     /**
      * Getter for messages cache object
      */
@@ -4016,7 +4108,6 @@
         return $this->mcache;
     }
 
-
     /**
      * Clears the messages cache.
      *
@@ -4029,7 +4120,6 @@
             $mcache->clear($folder, $uids);
         }
     }
-
 
     /**
      * Delete outdated cache entries
@@ -4060,7 +4150,6 @@
         }
     }
 
-
     /**
      * Sort folders first by default folders and then in alphabethical order
      *
@@ -4071,67 +4160,85 @@
      */
     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()));
+        $folders   = array();
 
-        // find default folders and skip folders starting with '.'
+        // convert names to UTF-8
         foreach ($a_folders as $folder) {
-            if ($folder[0] == '.') {
-                continue;
-            }
-
-            if (!$skip_default && ($p = array_search($folder, $specials)) !== false && !$a_defaults[$p]) {
-                $a_defaults[$p] = $folder;
-            }
-            else {
-                $folders[$folder] = rcube_charset::convert($folder, 'UTF7-IMAP');
-            }
+            // for better performance skip encoding conversion
+            // if the string does not look like UTF7-IMAP
+            $folders[$folder] = strpos($folder, '&') === false ? $folder : rcube_charset::convert($folder, 'UTF7-IMAP');
         }
 
-        // sort folders and place defaults on the top
-        asort($folders, SORT_LOCALE_STRING);
-        ksort($a_defaults);
-        $folders = array_merge($a_defaults, array_keys($folders));
+        // sort folders
+        // asort($folders, SORT_LOCALE_STRING) is not properly sorting case sensitive names
+        uasort($folders, array($this, 'sort_folder_comparator'));
 
-        // finally we must rebuild the list to move
-        // subfolders of default folders to their place...
-        // ...also do this for the rest of folders because
-        // asort() is not properly sorting case sensitive names
-        while (list($key, $folder) = each($folders)) {
-            // set the type of folder name variable (#1485527)
-            $a_out[] = (string) $folder;
-            unset($folders[$key]);
-            $this->rsort($folder, $delimiter, $folders, $a_out);
+        $folders = array_keys($folders);
+
+        if ($skip_default) {
+            return $folders;
         }
 
-        return $a_out;
+        // force the type of folder name variable (#1485527)
+        $folders  = array_map('strval', $folders);
+        $out      = array();
+
+        // finally we must put special folders on top and rebuild the list
+        // to move their subfolders where they belong...
+        $specials = array_unique(array_intersect($specials, $folders));
+        $folders  = array_merge($specials, array_diff($folders, $specials));
+
+        $this->sort_folder_specials(null, $folders, $specials, $out);
+
+        return $out;
     }
 
-
     /**
-     * Recursive method for sorting folders
+     * Recursive function to put subfolders of special folders in place
      */
-    protected function rsort($folder, $delimiter, &$list, &$out)
+    protected function sort_folder_specials($folder, &$list, &$specials, &$out)
     {
         while (list($key, $name) = each($list)) {
-            if (strpos($name, $folder.$delimiter) === 0) {
-                // set the type of folder name variable (#1485527)
-                $out[] = (string) $name;
+            if ($folder === null || strpos($name, $folder.$this->delimiter) === 0) {
+                $out[] = $name;
                 unset($list[$key]);
-                $this->rsort($name, $delimiter, $list, $out);
+
+                if (!empty($specials) && ($found = array_search($name, $specials)) !== false) {
+                    unset($specials[$found]);
+                    $this->sort_folder_specials($name, $list, $specials, $out);
+                }
             }
         }
+
         reset($list);
     }
 
+    /**
+     * Callback for uasort() that implements correct
+     * locale-aware case-sensitive sorting
+     */
+    protected function sort_folder_comparator($str1, $str2)
+    {
+        $path1 = explode($this->delimiter, $str1);
+        $path2 = explode($this->delimiter, $str2);
+
+        foreach ($path1 as $idx => $folder1) {
+            $folder2 = $path2[$idx];
+
+            if ($folder1 === $folder2) {
+                continue;
+            }
+
+            return strcoll($folder1, $folder2);
+        }
+    }
 
     /**
      * Find UID of the specified message sequence ID
      *
-     * @param int    $id       Message (sequence) ID
-     * @param string $folder   Folder name
+     * @param int    $id     Message (sequence) ID
+     * @param string $folder Folder name
      *
      * @return int Message UID
      */
@@ -4141,43 +4248,28 @@
             $folder = $this->folder;
         }
 
-        if ($uid = array_search($id, (array)$this->uid_id_map[$folder])) {
-            return $uid;
-        }
-
         if (!$this->check_connection()) {
             return null;
         }
 
-        $uid = $this->conn->ID2UID($folder, $id);
-
-        $this->uid_id_map[$folder][$uid] = $id;
-
-        return $uid;
+        return $this->conn->ID2UID($folder, $id);
     }
-
 
     /**
      * Subscribe/unsubscribe a list of folders and update local cache
      */
     protected function change_subscription($folders, $mode)
     {
-        $updated = false;
+        $updated = 0;
+        $folders = (array) $folders;
 
         if (!empty($folders)) {
             if (!$this->check_connection()) {
                 return false;
             }
 
-            foreach ((array)$folders as $i => $folder) {
-                $folders[$i] = $folder;
-
-                if ($mode == 'subscribe') {
-                    $updated = $this->conn->subscribe($folder);
-                }
-                else if ($mode == 'unsubscribe') {
-                    $updated = $this->conn->unsubscribe($folder);
-                }
+            foreach ($folders as $folder) {
+                $updated += (int) $this->conn->{$mode}($folder);
             }
         }
 
@@ -4186,9 +4278,8 @@
             $this->clear_cache('mailboxes', true);
         }
 
-        return $updated;
+        return $updated == count($folders) ? true : false;
     }
-
 
     /**
      * Increde/decrese messagecount for a specific folder
@@ -4220,7 +4311,6 @@
         return true;
     }
 
-
     /**
      * Remove messagecount of a specific folder from cache
      */
@@ -4238,7 +4328,6 @@
             $this->update_cache('messagecount', $a_folder_cache);
         }
     }
-
 
     /**
      * Converts date string/object into IMAP date/time format
@@ -4262,10 +4351,8 @@
         return $date->format('d-M-Y H:i:s O');
     }
 
-
     /**
      * This is our own debug handler for the IMAP connection
-     * @access public
      */
     public function debug_handler(&$imap, $message)
     {
@@ -4392,7 +4479,7 @@
         return $this->mod_folder($folder, $mode);
     }
 
-    public function mailbox_attributes($folder, $force=false)
+    public function mailbox_attributes($folder, $force = false)
     {
         return $this->folder_attributes($folder, $force);
     }
@@ -4412,9 +4499,8 @@
         return $this->folder_sync($folder);
     }
 
-    public function expunge($folder='', $clear_cache=true)
+    public function expunge($folder = '', $clear_cache = true)
     {
         return $this->expunge_folder($folder, $clear_cache);
     }
-
 }

--
Gitblit v1.9.1