2 commits - lib/ext lib/kolab_sync_data_email.php

Aleksander Machniak machniak at kolabsys.com
Fri Nov 7 09:41:17 CET 2014


 lib/ext/Roundcube/bootstrap.php                |   12 
 lib/ext/Roundcube/html.php                     |   24 
 lib/ext/Roundcube/rcube.php                    |  212 ++++--
 lib/ext/Roundcube/rcube_addressbook.php        |   56 +
 lib/ext/Roundcube/rcube_cache.php              |   86 +-
 lib/ext/Roundcube/rcube_cache_shared.php       |   40 -
 lib/ext/Roundcube/rcube_charset.php            |   26 
 lib/ext/Roundcube/rcube_contacts.php           |  160 ++--
 lib/ext/Roundcube/rcube_csv2vcard.php          |  175 ++++
 lib/ext/Roundcube/rcube_db.php                 |    4 
 lib/ext/Roundcube/rcube_db_oracle.php          |    2 
 lib/ext/Roundcube/rcube_html2text.php          |    2 
 lib/ext/Roundcube/rcube_image.php              |  154 +++-
 lib/ext/Roundcube/rcube_imap.php               |  524 +++++++++++---
 lib/ext/Roundcube/rcube_imap_cache.php         |  208 +++--
 lib/ext/Roundcube/rcube_imap_generic.php       |  548 +++++++++------
 lib/ext/Roundcube/rcube_imap_search.php        |  231 ++++++
 lib/ext/Roundcube/rcube_ldap.php               |  112 ++-
 lib/ext/Roundcube/rcube_ldap_generic.php       |  878 +------------------------
 lib/ext/Roundcube/rcube_message.php            |  277 ++++++-
 lib/ext/Roundcube/rcube_message_header.php     |   21 
 lib/ext/Roundcube/rcube_mime.php               |   87 +-
 lib/ext/Roundcube/rcube_output.php             |   48 -
 lib/ext/Roundcube/rcube_plugin.php             |   36 -
 lib/ext/Roundcube/rcube_plugin_api.php         |   73 +-
 lib/ext/Roundcube/rcube_result_index.php       |    2 
 lib/ext/Roundcube/rcube_result_multifolder.php |  337 +++++++++
 lib/ext/Roundcube/rcube_result_thread.php      |    2 
 lib/ext/Roundcube/rcube_session.php            |   42 -
 lib/ext/Roundcube/rcube_spellchecker.php       |   38 -
 lib/ext/Roundcube/rcube_storage.php            |   97 ++
 lib/ext/Roundcube/rcube_tnef_decoder.php       |  341 +++++++++
 lib/ext/Roundcube/rcube_user.php               |  102 +-
 lib/ext/Roundcube/rcube_utils.php              |   52 +
 lib/ext/Roundcube/rcube_washtml.php            |    8 
 lib/ext/tnef_decoder.php                       |  331 ---------
 lib/kolab_sync_data_email.php                  |   13 
 37 files changed, 3219 insertions(+), 2142 deletions(-)

New commits:
commit 4a39caf31e02cd2a7cb01665408f6b5f726dcc66
Author: Aleksander Machniak <machniak at kolabsys.com>
Date:   Fri Nov 7 09:40:40 2014 +0100

    Don't set $part->body, use new rcube_message::get_part_body()

diff --git a/lib/kolab_sync_data_email.php b/lib/kolab_sync_data_email.php
index 8c3a820..9920276 100644
--- a/lib/kolab_sync_data_email.php
+++ b/lib/kolab_sync_data_email.php
@@ -1284,22 +1284,13 @@ class kolab_sync_data_email extends kolab_sync_data implements Syncroton_Data_ID
             return '';
         }
 
-        if (empty($part->ctype_parameters) || empty($part->ctype_parameters['charset'])) {
-            $part->ctype_parameters['charset'] = $message->headers->charset;
-        }
-
-        // fetch part if not available
-        if (!isset($part->body)) {
-            $part->body = $message->get_part_content($part->mime_id);
-        }
+        $body = $message->get_part_body($part->mime_id, true);
 
         // message is cached but not exists, or other error
-        if ($part->body === false) {
+        if ($body === false) {
             return '';
         }
 
-        $body = $part->body;
-
         if ($html) {
             if ($part->ctype_secondary == 'html') {
                 // charset was converted to UTF-8 in rcube_storage::get_message_part(),


commit 0a7d936a05647ba054d880c20cd57ad60c2ce8ac
Author: Aleksander Machniak <machniak at kolabsys.com>
Date:   Fri Nov 7 09:40:28 2014 +0100

    Update Roundcube Framework

diff --git a/lib/ext/Roundcube/bootstrap.php b/lib/ext/Roundcube/bootstrap.php
index 27c124a..fe9c389 100644
--- a/lib/ext/Roundcube/bootstrap.php
+++ b/lib/ext/Roundcube/bootstrap.php
@@ -3,7 +3,7 @@
 /*
  +-----------------------------------------------------------------------+
  | This file is part of the Roundcube PHP suite                          |
- | Copyright (C) 2005-2013, The Roundcube Dev Team                       |
+ | Copyright (C) 2005-2014, The Roundcube Dev Team                       |
  |                                                                       |
  | Licensed under the GNU General Public License version 3 or            |
  | any later version with exceptions for skins & plugins.                |
@@ -54,11 +54,11 @@ foreach ($config as $optname => $optval) {
 }
 
 // framework constants
-define('RCUBE_VERSION', '1.0.2');
+define('RCUBE_VERSION', '1.1-git');
 define('RCUBE_CHARSET', 'UTF-8');
 
 if (!defined('RCUBE_LIB_DIR')) {
-    define('RCUBE_LIB_DIR', dirname(__FILE__).DIRECTORY_SEPARATOR);
+    define('RCUBE_LIB_DIR', __DIR__ . '/');
 }
 
 if (!defined('RCUBE_INSTALL_PATH')) {
@@ -464,16 +464,14 @@ function rcube_autoload($classname)
             '/Net_(.+)/',
             '/Auth_(.+)/',
             '/^html_.+/',
-            '/^rcube(.*)/',
-            '/^utf8$/',
+            '/^rcube(.*)/'
         ),
         array(
             'Mail/\\1',
             'Net/\\1',
             'Auth/\\1',
             'Roundcube/html',
-            'Roundcube/rcube\\1',
-            'utf8.class',
+            'Roundcube/rcube\\1'
         ),
         $classname
     );
diff --git a/lib/ext/Roundcube/html.php b/lib/ext/Roundcube/html.php
index 31bacbf..f18cad0 100644
--- a/lib/ext/Roundcube/html.php
+++ b/lib/ext/Roundcube/html.php
@@ -32,7 +32,7 @@ class html
 
     public static $doctype = 'xhtml';
     public static $lc_tags = true;
-    public static $common_attrib = array('id','class','style','title','align','unselectable');
+    public static $common_attrib = array('id','class','style','title','align','unselectable','tabindex','role');
     public static $containers = array('iframe','div','span','p','h1','h2','h3','ul','form','textarea','table','thead','tbody','tr','th','td','style','script');
 
 
@@ -218,7 +218,7 @@ class html
             $attr = array('src' => $attr);
         }
         return self::tag('iframe', $attr, $cont, array_merge(self::$common_attrib,
-            array('src','name','width','height','border','frameborder','onload')));
+            array('src','name','width','height','border','frameborder','onload','allowfullscreen')));
     }
 
     /**
@@ -283,10 +283,11 @@ class html
                 continue;
             }
 
-            // ignore not allowed attributes
+            // ignore not allowed attributes, except aria-* and data-*
             if (!empty($allowed)) {
                 $is_data_attr = @substr_compare($key, 'data-', 0, 5) === 0;
-                if (!isset($allowed_f[$key]) && (!$is_data_attr || !isset($allowed_f['data-*']))) {
+                $is_aria_attr = @substr_compare($key, 'aria-', 0, 5) === 0;
+                if (!$is_aria_attr && !$is_data_attr && !isset($allowed_f[$key])) {
                     continue;
                 }
             }
@@ -836,7 +837,7 @@ class html_table extends html
         if (!empty($this->header)) {
             $rowcontent = '';
             foreach ($this->header as $c => $col) {
-                $rowcontent .= self::tag($this->_col_tagname(), $col->attrib, $col->content);
+                $rowcontent .= self::tag($this->_head_tagname(), $col->attrib, $col->content);
             }
             $thead = $this->tagname == 'table' ? self::tag('thead', null, self::tag('tr', null, $rowcontent, parent::$common_attrib)) :
                 self::tag($this->_row_tagname(), array('class' => 'thead'), $rowcontent, parent::$common_attrib);
@@ -889,7 +890,16 @@ class html_table extends html
     private function _row_tagname()
     {
         static $row_tagnames = array('table' => 'tr', 'ul' => 'li', '*' => 'div');
-        return $row_tagnames[$this->tagname] ? $row_tagnames[$this->tagname] : $row_tagnames['*'];
+        return $row_tagnames[$this->tagname] ?: $row_tagnames['*'];
+    }
+
+    /**
+     * Getter for the corresponding tag name for table row elements
+     */
+    private function _head_tagname()
+    {
+        static $head_tagnames = array('table' => 'th', '*' => 'span');
+        return $head_tagnames[$this->tagname] ?: $head_tagnames['*'];
     }
 
     /**
@@ -898,7 +908,7 @@ class html_table extends html
     private function _col_tagname()
     {
         static $col_tagnames = array('table' => 'td', '*' => 'span');
-        return $col_tagnames[$this->tagname] ? $col_tagnames[$this->tagname] : $col_tagnames['*'];
+        return $col_tagnames[$this->tagname] ?: $col_tagnames['*'];
     }
 
 }
diff --git a/lib/ext/Roundcube/rcube.php b/lib/ext/Roundcube/rcube.php
index 87103be..03f4963 100644
--- a/lib/ext/Roundcube/rcube.php
+++ b/lib/ext/Roundcube/rcube.php
@@ -3,8 +3,8 @@
 /*
  +-----------------------------------------------------------------------+
  | This file is part of the Roundcube Webmail client                     |
- | Copyright (C) 2008-2012, The Roundcube Dev Team                       |
- | Copyright (C) 2011-2012, Kolab Systems AG                             |
+ | Copyright (C) 2008-2014, The Roundcube Dev Team                       |
+ | Copyright (C) 2011-2014, Kolab Systems AG                             |
  |                                                                       |
  | Licensed under the GNU General Public License version 3 or            |
  | any later version with exceptions for skins & plugins.                |
@@ -94,6 +94,13 @@ class rcube
      */
     public $plugins;
 
+    /**
+     * Instance of rcube_user class.
+     *
+     * @var rcube_user
+     */
+    public $user;
+
 
     /* private/protected vars */
     protected $texts;
@@ -165,9 +172,13 @@ class rcube
     public function get_dbh()
     {
         if (!$this->db) {
-            $config_all = $this->config->all();
-            $this->db = rcube_db::factory($config_all['db_dsnw'], $config_all['db_dsnr'], $config_all['db_persistent']);
-            $this->db->set_debug((bool)$config_all['sql_debug']);
+            $this->db = rcube_db::factory(
+                $this->config->get('db_dsnw'),
+                $this->config->get('db_dsnr'),
+                $this->config->get('db_persistent')
+            );
+
+            $this->db->set_debug((bool)$this->config->get('sql_debug'));
         }
 
         return $this->db;
@@ -348,40 +359,18 @@ class rcube
         // for backward compat. (deprecated, will be removed)
         $this->imap = $this->storage;
 
-        // enable caching of mail data
-        $storage_cache  = $this->config->get("{$driver}_cache");
-        $messages_cache = $this->config->get('messages_cache');
-        // for backward compatybility
-        if ($storage_cache === null && $messages_cache === null && $this->config->get('enable_caching')) {
-            $storage_cache  = 'db';
-            $messages_cache = true;
-        }
-
-        if ($storage_cache) {
-            $this->storage->set_caching($storage_cache);
-        }
-        if ($messages_cache) {
-            $this->storage->set_messages_caching(true);
-        }
-
-        // set pagesize from config
-        $pagesize = $this->config->get('mail_pagesize');
-        if (!$pagesize) {
-            $pagesize = $this->config->get('pagesize', 50);
-        }
-        $this->storage->set_pagesize($pagesize);
-
         // set class options
         $options = array(
-            'auth_type'   => $this->config->get("{$driver}_auth_type", 'check'),
-            'auth_cid'    => $this->config->get("{$driver}_auth_cid"),
-            'auth_pw'     => $this->config->get("{$driver}_auth_pw"),
-            'debug'       => (bool) $this->config->get("{$driver}_debug"),
-            'force_caps'  => (bool) $this->config->get("{$driver}_force_caps"),
-            'disabled_caps' => $this->config->get("{$driver}_disabled_caps"),
-            'timeout'     => (int) $this->config->get("{$driver}_timeout"),
-            'skip_deleted' => (bool) $this->config->get('skip_deleted'),
-            'driver'      => $driver,
+            'auth_type'      => $this->config->get("{$driver}_auth_type", 'check'),
+            'auth_cid'       => $this->config->get("{$driver}_auth_cid"),
+            'auth_pw'        => $this->config->get("{$driver}_auth_pw"),
+            'debug'          => (bool) $this->config->get("{$driver}_debug"),
+            'force_caps'     => (bool) $this->config->get("{$driver}_force_caps"),
+            'disabled_caps'  => $this->config->get("{$driver}_disabled_caps"),
+            'socket_options' => $this->config->get("{$driver}_conn_options"),
+            'timeout'        => (int) $this->config->get("{$driver}_timeout"),
+            'skip_deleted'   => (bool) $this->config->get('skip_deleted'),
+            'driver'         => $driver,
         );
 
         if (!empty($_SESSION['storage_host'])) {
@@ -400,32 +389,89 @@ class rcube
 
         $this->storage->set_options($options);
         $this->set_storage_prop();
-    }
 
+        // subscribe to 'storage_connected' hook for session logging
+        if ($this->config->get('imap_log_session', false)) {
+            $this->plugins->register_hook('storage_connected', array($this, 'storage_log_session'));
+        }
+    }
 
     /**
      * Set storage parameters.
-     * This must be done AFTER connecting to the server!
      */
     protected function set_storage_prop()
     {
         $storage = $this->get_storage();
 
+        // set pagesize from config
+        $pagesize = $this->config->get('mail_pagesize');
+        if (!$pagesize) {
+            $pagesize = $this->config->get('pagesize', 50);
+        }
+
+        $storage->set_pagesize($pagesize);
         $storage->set_charset($this->config->get('default_charset', RCUBE_CHARSET));
 
-        if ($default_folders = $this->config->get('default_folders')) {
-            $storage->set_default_folders($default_folders);
+        // enable caching of mail data
+        $driver         = $this->config->get('storage_driver', 'imap');
+        $storage_cache  = $this->config->get("{$driver}_cache");
+        $messages_cache = $this->config->get('messages_cache');
+        // for backward compatybility
+        if ($storage_cache === null && $messages_cache === null && $this->config->get('enable_caching')) {
+            $storage_cache  = 'db';
+            $messages_cache = true;
+        }
+
+        if ($storage_cache) {
+            $storage->set_caching($storage_cache);
+        }
+        if ($messages_cache) {
+            $storage->set_messages_caching(true);
+        }
+    }
+
+
+    /**
+     * Set special folders type association.
+     * This must be done AFTER connecting to the server!
+     */
+    protected function set_special_folders()
+    {
+        $storage = $this->get_storage();
+        $folders = $storage->get_special_folders(true);
+        $prefs   = array();
+
+        // check SPECIAL-USE flags on IMAP folders
+        foreach ($folders as $type => $folder) {
+            $idx = $type . '_mbox';
+            if ($folder !== $this->config->get($idx)) {
+                $prefs[$idx] = $folder;
+            }
         }
-        if (isset($_SESSION['mbox'])) {
-            $storage->set_folder($_SESSION['mbox']);
+
+        // Some special folders differ, update user preferences
+        if (!empty($prefs) && $this->user) {
+            $this->user->save_prefs($prefs);
         }
-        if (isset($_SESSION['page'])) {
-            $storage->set_page($_SESSION['page']);
+
+        // create default folders (on login)
+        if ($this->config->get('create_default_folders')) {
+            $storage->create_default_folders();
         }
     }
 
 
     /**
+     * Callback for IMAP connection events to log session identifiers
+     */
+    public function storage_log_session($args)
+    {
+        if (!empty($args['session']) && session_id()) {
+            $this->write_log('imap_session', $args['session']);
+        }
+    }
+
+    /**
      * Create session object and start the session.
      */
     public function session_init()
@@ -796,12 +842,19 @@ class rcube
          * upon decryption; see http://php.net/mcrypt_generic#68082
          */
         $clear = pack("a*H2", $clear, "80");
+        $ckey  = $this->config->get_crypto_key($key);
 
-        if (function_exists('mcrypt_module_open') &&
+        if (function_exists('openssl_encrypt')) {
+            $method = 'DES-EDE3-CBC';
+            $opts   = defined('OPENSSL_RAW_DATA') ? OPENSSL_RAW_DATA : true;
+            $iv     = $this->create_iv(openssl_cipher_iv_length($method));
+            $cipher = $iv . openssl_encrypt($clear, $method, $ckey, $opts, $iv);
+        }
+        else if (function_exists('mcrypt_module_open') &&
             ($td = mcrypt_module_open(MCRYPT_TripleDES, "", MCRYPT_MODE_CBC, ""))
         ) {
             $iv = $this->create_iv(mcrypt_enc_get_iv_size($td));
-            mcrypt_generic_init($td, $this->config->get_crypto_key($key), $iv);
+            mcrypt_generic_init($td, $ckey, $iv);
             $cipher = $iv . mcrypt_generic($td, $clear);
             mcrypt_generic_deinit($td);
             mcrypt_module_close($td);
@@ -812,13 +865,13 @@ class rcube
             if (function_exists('des')) {
                 $des_iv_size = 8;
                 $iv = $this->create_iv($des_iv_size);
-                $cipher = $iv . des($this->config->get_crypto_key($key), $clear, 1, 1, $iv);
+                $cipher = $iv . des($ckey, $clear, 1, 1, $iv);
             }
             else {
                 self::raise_error(array(
                     'code' => 500, 'type' => 'php',
                     'file' => __FILE__, 'line' => __LINE__,
-                    'message' => "Could not perform encryption; make sure Mcrypt is installed or lib/des.inc is available"
+                    'message' => "Could not perform encryption; make sure OpenSSL or Mcrypt or lib/des.inc is available"
                     ), true, true);
             }
         }
@@ -843,12 +896,27 @@ class rcube
         }
 
         $cipher = $base64 ? base64_decode($cipher) : $cipher;
+        $ckey   = $this->config->get_crypto_key($key);
+
+        if (function_exists('openssl_decrypt')) {
+            $method  = 'DES-EDE3-CBC';
+            $opts    = defined('OPENSSL_RAW_DATA') ? OPENSSL_RAW_DATA : true;
+            $iv_size = openssl_cipher_iv_length($method);
+            $iv      = substr($cipher, 0, $iv_size);
 
-        if (function_exists('mcrypt_module_open') &&
+            // session corruption? (#1485970)
+            if (strlen($iv) < $iv_size) {
+                return '';
+            }
+
+            $cipher = substr($cipher, $iv_size);
+            $clear  = openssl_decrypt($cipher, $method, $ckey, $opts, $iv);
+        }
+        else if (function_exists('mcrypt_module_open') &&
             ($td = mcrypt_module_open(MCRYPT_TripleDES, "", MCRYPT_MODE_CBC, ""))
         ) {
             $iv_size = mcrypt_enc_get_iv_size($td);
-            $iv = substr($cipher, 0, $iv_size);
+            $iv      = substr($cipher, 0, $iv_size);
 
             // session corruption? (#1485970)
             if (strlen($iv) < $iv_size) {
@@ -856,7 +924,7 @@ class rcube
             }
 
             $cipher = substr($cipher, $iv_size);
-            mcrypt_generic_init($td, $this->config->get_crypto_key($key), $iv);
+            mcrypt_generic_init($td, $ckey, $iv);
             $clear = mdecrypt_generic($td, $cipher);
             mcrypt_generic_deinit($td);
             mcrypt_module_close($td);
@@ -866,15 +934,15 @@ class rcube
 
             if (function_exists('des')) {
                 $des_iv_size = 8;
-                $iv = substr($cipher, 0, $des_iv_size);
+                $iv     = substr($cipher, 0, $des_iv_size);
                 $cipher = substr($cipher, $des_iv_size);
-                $clear = des($this->config->get_crypto_key($key), $cipher, 0, 1, $iv);
+                $clear  = des($ckey, $cipher, 0, 1, $iv);
             }
             else {
                 self::raise_error(array(
                     'code' => 500, 'type' => 'php',
                     'file' => __FILE__, 'line' => __LINE__,
-                    'message' => "Could not perform decryption; make sure Mcrypt is installed or lib/des.inc is available"
+                    'message' => "Could not perform decryption; make sure OpenSSL or Mcrypt or lib/des.inc is available"
                     ), true, true);
             }
         }
@@ -1086,8 +1154,12 @@ class rcube
             $line = var_export($line, true);
         }
 
-        $date_format = self::$instance ? self::$instance->config->get('log_date_format') : null;
-        $log_driver  = self::$instance ? self::$instance->config->get('log_driver') : null;
+        $date_format = $log_driver = $session_key = null;
+        if (self::$instance) {
+            $date_format = self::$instance->config->get('log_date_format');
+            $log_driver  = self::$instance->config->get('log_driver');
+            $session_key = intval(self::$instance->config->get('log_session_id', 8));
+        }
 
         if (empty($date_format)) {
             $date_format = 'd-M-Y H:i:s O';
@@ -1105,6 +1177,11 @@ class rcube
                 return true;
         }
 
+        // add session ID to the log
+        if ($session_key > 0 && ($sess = session_id())) {
+            $line = '<' . substr($sess, 0, $session_key) . '> ' . $line;
+        }
+
         if ($log_driver == 'syslog') {
             $prio = $name == 'errors' ? LOG_ERR : LOG_INFO;
             syslog($prio, $line);
@@ -1180,8 +1257,8 @@ class rcube
         }
 
         // installer
-        if (class_exists('rcube_install', false)) {
-            $rci = rcube_install::get_instance();
+        if (class_exists('rcmail_install', false)) {
+            $rci = rcmail_install::get_instance();
             $rci->raise_error($arg);
             return;
         }
@@ -1205,6 +1282,9 @@ class rcube
 
             exit(1);
         }
+        else if ($cli) {
+            fwrite(STDERR, 'ERROR: ' . $arg['message']);
+        }
     }
 
 
@@ -1302,6 +1382,20 @@ class rcube
         self::write_log($dest, sprintf("%s: %0.4f sec", $label, $diff));
     }
 
+    /**
+     * Setter for system user object
+     *
+     * @param rcube_user Current user instance
+     */
+    public function set_user($user)
+    {
+        if (is_object($user)) {
+            $this->user = $user;
+
+            // overwrite config with user preferences
+            $this->config->set_user_prefs((array)$this->user->get_prefs());
+        }
+    }
 
     /**
      * Getter for logged user ID.
diff --git a/lib/ext/Roundcube/rcube_addressbook.php b/lib/ext/Roundcube/rcube_addressbook.php
index 4d9fa3d..69027b0 100644
--- a/lib/ext/Roundcube/rcube_addressbook.php
+++ b/lib/ext/Roundcube/rcube_addressbook.php
@@ -557,6 +557,62 @@ abstract class rcube_addressbook
     }
 
     /**
+     * Build contact display name for autocomplete listing
+     *
+     * @param array  Hash array with contact data as key-value pairs
+     * @param string Optional email address
+     * @param string Optional name (self::compose_list_name() result)
+     * @param string Optional template to use (defaults to the 'contact_search_name' config option)
+     *
+     * @return string Display name
+     */
+    public static function compose_search_name($contact, $email = null, $name = null, $templ = null)
+    {
+        static $template;
+
+        if (empty($templ) && !isset($template)) {  // cache this
+            $template = rcube::get_instance()->config->get('contact_search_name');
+            if (empty($template)) {
+                $template = '{name} <{email}>';
+            }
+        }
+
+        $result = $templ ?: $template;
+
+        if (preg_match_all('/\{[a-z]+\}/', $result, $matches)) {
+            foreach ($matches[0] as $key) {
+                $key   = trim($key, '{}');
+                $value = '';
+
+                switch ($key) {
+                case 'name':
+                    $value = $name ?: self::compose_list_name($contact);
+                    break;
+
+                case 'email':
+                    $value = $email;
+                    break;
+                }
+
+                if (empty($value)) {
+                    $value = strpos($key, ':') ? $contact[$key] : self::get_col_values($key, $contact, true);
+                    if (is_array($value)) {
+                        $value = $value[0];
+                    }
+                }
+
+                $result = str_replace('{' . $key . '}', $value, $result);
+            }
+        }
+
+        $result = preg_replace('/\s+/', ' ', $result);
+        $result = preg_replace('/\s*(<>|\(\)|\[\])/', '', $result);
+        $result = trim($result, '/ ');
+
+        return $result;
+    }
+
+    /**
      * Create a unique key for sorting contacts
      */
     public static function compose_contact_key($contact, $sort_col)
diff --git a/lib/ext/Roundcube/rcube_cache.php b/lib/ext/Roundcube/rcube_cache.php
index a708cb2..7210ce6 100644
--- a/lib/ext/Roundcube/rcube_cache.php
+++ b/lib/ext/Roundcube/rcube_cache.php
@@ -45,6 +45,7 @@ class rcube_cache
     private $cache         = array();
     private $cache_changes = array();
     private $cache_sums    = array();
+    private $max_packet    = -1;
 
 
     /**
@@ -74,7 +75,7 @@ class rcube_cache
         else {
             $this->type  = 'db';
             $this->db    = $rcube->get_dbh();
-            $this->table = $this->db->table_name('cache');
+            $this->table = $this->db->table_name('cache', true);
         }
 
         // convert ttl string to seconds
@@ -196,10 +197,10 @@ class rcube_cache
     {
         if ($this->type == 'db' && $this->db && $this->ttl) {
             $this->db->query(
-                "DELETE FROM ".$this->table.
-                " WHERE user_id = ?".
-                " AND cache_key LIKE ?".
-                " AND expires < " . $this->db->now(),
+                "DELETE FROM {$this->table}".
+                " WHERE `user_id` = ?".
+                " AND `cache_key` LIKE ?".
+                " AND `expires` < " . $this->db->now(),
                 $this->userid,
                 $this->prefix.'.%');
         }
@@ -214,7 +215,7 @@ class rcube_cache
         $rcube = rcube::get_instance();
         $db    = $rcube->get_dbh();
 
-        $db->query("DELETE FROM " . $db->table_name('cache') . " WHERE expires < " . $db->now());
+        $db->query("DELETE FROM " . $db->table_name('cache', true) . " WHERE `expires` < " . $db->now());
     }
 
 
@@ -283,13 +284,12 @@ class rcube_cache
         }
         else {
             $sql_result = $this->db->limitquery(
-                "SELECT data, cache_key".
-                " FROM " . $this->table.
-                " WHERE user_id = ?".
-                " AND cache_key = ?".
+                "SELECT `data`, `cache_key`".
+                " FROM {$this->table}".
+                " WHERE `user_id` = ? AND `cache_key` = ?".
                 // for better performance we allow more records for one key
                 // get the newer one
-                " ORDER BY created DESC",
+                " ORDER BY `created` DESC",
                 0, 1, $this->userid, $this->prefix.'.'.$key);
 
             if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
@@ -319,7 +319,7 @@ class rcube_cache
      * Writes single cache record into DB.
      *
      * @param string $key  Cache key name
-     * @param mxied  $data Serialized cache data 
+     * @param mixed  $data Serialized cache data
      *
      * @param boolean True on success, False on failure
      */
@@ -329,6 +329,12 @@ class rcube_cache
             return false;
         }
 
+        // don't attempt to write too big data sets
+        if (strlen($data) > $this->max_packet_size()) {
+            trigger_error("rcube_cache: max_packet_size ($this->max_packet) exceeded for key $key. Tried to write " . strlen($data) . " bytes", E_USER_WARNING);
+            return false;
+        }
+
         if ($this->type == 'memcache' || $this->type == 'apc') {
             return $this->add_record($this->ckey($key), $data);
         }
@@ -339,9 +345,8 @@ class rcube_cache
         // Remove NULL rows (here we don't need to check if the record exist)
         if ($data == 'N;') {
             $this->db->query(
-                "DELETE FROM " . $this->table.
-                " WHERE user_id = ?".
-                " AND cache_key = ?",
+                "DELETE FROM {$this->table}".
+                " WHERE `user_id` = ? AND `cache_key` = ?",
                 $this->userid, $key);
 
             return true;
@@ -350,12 +355,12 @@ class rcube_cache
         // update existing cache record
         if ($key_exists) {
             $result = $this->db->query(
-                "UPDATE " . $this->table.
-                " SET created = " . $this->db->now().
-                    ", expires = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL').
-                    ", data = ?".
-                " WHERE user_id = ?".
-                " AND cache_key = ?",
+                "UPDATE {$this->table}".
+                " SET `created` = " . $this->db->now().
+                    ", `expires` = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL').
+                    ", `data` = ?".
+                " WHERE `user_id` = ?".
+                " AND `cache_key` = ?",
                 $data, $this->userid, $key);
         }
         // add new cache record
@@ -363,8 +368,8 @@ class rcube_cache
             // for better performance we allow more records for one key
             // so, no need to check if record exist (see rcube_cache::read_record())
             $result = $this->db->query(
-                "INSERT INTO " . $this->table.
-                " (created, expires, user_id, cache_key, data)".
+                "INSERT INTO {$this->table}".
+                " (`created`, `expires`, `user_id`, `cache_key`, `data`)".
                 " VALUES (" . $this->db->now() . ", " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL') . ", ?, ?, ?)",
                 $this->userid, $key, $data);
         }
@@ -414,20 +419,19 @@ class rcube_cache
 
         // Remove all keys (in specified cache)
         if ($key === null) {
-            $where = " AND cache_key LIKE " . $this->db->quote($this->prefix.'.%');
+            $where = " AND `cache_key` LIKE " . $this->db->quote($this->prefix.'.%');
         }
         // Remove keys by name prefix
         else if ($prefix_mode) {
-            $where = " AND cache_key LIKE " . $this->db->quote($this->prefix.'.'.$key.'%');
+            $where = " AND `cache_key` LIKE " . $this->db->quote($this->prefix.'.'.$key.'%');
         }
         // Remove one key by name
         else {
-            $where = " AND cache_key = " . $this->db->quote($this->prefix.'.'.$key);
+            $where = " AND `cache_key` = " . $this->db->quote($this->prefix.'.'.$key);
         }
 
         $this->db->query(
-            "DELETE FROM " . $this->table.
-            " WHERE user_id = ?" . $where,
+            "DELETE FROM {$this->table} WHERE `user_id` = ?" . $where,
             $this->userid);
     }
 
@@ -591,4 +595,30 @@ class rcube_cache
 
         return $this->packed ? @unserialize($data) : $data;
     }
+
+    /**
+     * Determine the maximum size for cache data to be written
+     */
+    private function max_packet_size()
+    {
+        if ($this->max_packet < 0) {
+            $this->max_packet = 2097152; // default/max is 2 MB
+
+            if ($this->type == 'db') {
+                $value = $this->db->get_variable('max_allowed_packet', 1048500);
+                $this->max_packet = min($value, $this->max_packet) - 2000;
+            }
+            else if ($this->type == 'memcache') {
+                $stats = $this->db->getStats();
+                $remaining = $stats['limit_maxbytes'] - $stats['bytes'];
+                $this->max_packet = min($remaining / 5, $this->max_packet);
+            }
+            else if ($this->type == 'apc' && function_exists('apc_sma_info')) {
+                $stats = apc_sma_info();
+                $this->max_packet = min($stats['avail_mem'] / 5, $this->max_packet);
+            }
+        }
+
+        return $this->max_packet;
+    }
 }
diff --git a/lib/ext/Roundcube/rcube_cache_shared.php b/lib/ext/Roundcube/rcube_cache_shared.php
index 8f25740..a2bf092 100644
--- a/lib/ext/Roundcube/rcube_cache_shared.php
+++ b/lib/ext/Roundcube/rcube_cache_shared.php
@@ -72,7 +72,7 @@ class rcube_cache_shared
         else {
             $this->type  = 'db';
             $this->db    = $rcube->get_dbh();
-            $this->table = $this->db->table_name('cache_shared');
+            $this->table = $this->db->table_name('cache_shared', true);
         }
 
         // convert ttl string to seconds
@@ -193,9 +193,9 @@ class rcube_cache_shared
     {
         if ($this->type == 'db' && $this->db && $this->ttl) {
             $this->db->query(
-                "DELETE FROM " . $this->table
-                . " WHERE cache_key LIKE ?"
-                . " AND expires < " . $this->db->now(),
+                "DELETE FROM {$this->table}"
+                . " WHERE `cache_key` LIKE ?"
+                . " AND `expires` < " . $this->db->now(),
                 $this->prefix . '.%');
         }
     }
@@ -209,7 +209,7 @@ class rcube_cache_shared
         $rcube = rcube::get_instance();
         $db    = $rcube->get_dbh();
 
-        $db->query("DELETE FROM " . $db->table_name('cache_shared') . " WHERE expires < " . $db->now());
+        $db->query("DELETE FROM " . $db->table_name('cache_shared', true) . " WHERE `expires` < " . $db->now());
     }
 
 
@@ -278,12 +278,12 @@ class rcube_cache_shared
         }
         else {
             $sql_result = $this->db->limitquery(
-                "SELECT data, cache_key".
-                " FROM " . $this->table .
-                " WHERE cache_key = ?".
+                "SELECT `data`, `cache_key`".
+                " FROM {$this->table}" .
+                " WHERE `cache_key` = ?".
                 // for better performance we allow more records for one key
                 // get the newer one
-                " ORDER BY created DESC",
+                " ORDER BY `created` DESC",
                 0, 1, $this->prefix . '.' . $key);
 
             if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
@@ -331,18 +331,18 @@ class rcube_cache_shared
 
         // Remove NULL rows (here we don't need to check if the record exist)
         if ($data == 'N;') {
-            $this->db->query("DELETE FROM " . $this->table . " WHERE cache_key = ?", $key);
+            $this->db->query("DELETE FROM {$this->table} WHERE `cache_key` = ?", $key);
             return true;
         }
 
         // update existing cache record
         if ($key_exists) {
             $result = $this->db->query(
-                "UPDATE " . $this->table .
-                " SET created = " . $this->db->now() .
-                    ", expires = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL') .
-                    ", data = ?".
-                " WHERE cache_key = ?",
+                "UPDATE {$this->table}" .
+                " SET `created` = " . $this->db->now() .
+                    ", `expires` = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL') .
+                    ", `data` = ?".
+                " WHERE `cache_key` = ?",
                 $data, $key);
         }
         // add new cache record
@@ -350,8 +350,8 @@ class rcube_cache_shared
             // for better performance we allow more records for one key
             // so, no need to check if record exist (see rcube_cache::read_record())
             $result = $this->db->query(
-                "INSERT INTO ".$this->table.
-                " (created, expires, cache_key, data)".
+                "INSERT INTO {$this->table}".
+                " (`created`, `expires`, `cache_key`, `data`)".
                 " VALUES (".$this->db->now().", " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL') . ", ?, ?)",
                 $key, $data);
         }
@@ -401,15 +401,15 @@ class rcube_cache_shared
 
         // Remove all keys (in specified cache)
         if ($key === null) {
-            $where = " WHERE cache_key LIKE " . $this->db->quote($this->prefix.'.%');
+            $where = " WHERE `cache_key` LIKE " . $this->db->quote($this->prefix.'.%');
         }
         // Remove keys by name prefix
         else if ($prefix_mode) {
-            $where = " WHERE cache_key LIKE " . $this->db->quote($this->prefix.'.'.$key.'%');
+            $where = " WHERE `cache_key` LIKE " . $this->db->quote($this->prefix.'.'.$key.'%');
         }
         // Remove one key by name
         else {
-            $where = " WHERE cache_key = " . $this->db->quote($this->prefix.'.'.$key);
+            $where = " WHERE `cache_key` = " . $this->db->quote($this->prefix.'.'.$key);
         }
 
         $this->db->query("DELETE FROM " . $this->table . $where);
diff --git a/lib/ext/Roundcube/rcube_charset.php b/lib/ext/Roundcube/rcube_charset.php
index ffec673..d6ca3c0 100644
--- a/lib/ext/Roundcube/rcube_charset.php
+++ b/lib/ext/Roundcube/rcube_charset.php
@@ -273,17 +273,8 @@ class rcube_charset
             else if ($from == 'ISO-8859-1' && function_exists('utf8_encode')) {
                 return utf8_encode($str);
             }
-            else if (class_exists('utf8')) {
-                if (!$conv) {
-                    $conv = new utf8($from);
-                }
-                else {
-                    $conv->loadCharset($from);
-                }
-
-                if ($_str = $conv->strToUtf8($str)) {
-                    return $_str;
-                }
+            else  {
+                user_error("No suitable function found for UTF-8 encoding", E_USER_WARNING);
             }
         }
 
@@ -298,17 +289,8 @@ class rcube_charset
             else if ($to == 'ISO-8859-1' && function_exists('utf8_decode')) {
                 return utf8_decode($str);
             }
-            else if (class_exists('utf8')) {
-                if (!$conv) {
-                    $conv = new utf8($to);
-                }
-                else {
-                    $conv->loadCharset($from);
-                }
-
-                if ($_str = $conv->strToUtf8($str)) {
-                    return $_str;
-                }
+            else {
+                user_error("No suitable function found for UTF-8 decoding", E_USER_WARNING);
             }
         }
 
diff --git a/lib/ext/Roundcube/rcube_contacts.php b/lib/ext/Roundcube/rcube_contacts.php
index 5e1a40e..bd3a3f8 100644
--- a/lib/ext/Roundcube/rcube_contacts.php
+++ b/lib/ext/Roundcube/rcube_contacts.php
@@ -167,11 +167,9 @@ class rcube_contacts extends rcube_addressbook
         }
 
         $sql_result = $this->db->query(
-            "SELECT * FROM ".$this->db->table_name($this->db_groups).
-            " WHERE del<>1".
-            " AND user_id=?".
-            $sql_filter.
-            " ORDER BY name",
+            "SELECT * FROM " . $this->db->table_name($this->db_groups, true)
+            . " WHERE `del` <> 1 AND `user_id` = ?" . $sql_filter
+            . " ORDER BY `name`",
             $this->user_id);
 
         while ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
@@ -192,10 +190,8 @@ class rcube_contacts extends rcube_addressbook
     function get_group($group_id)
     {
         $sql_result = $this->db->query(
-            "SELECT * FROM ".$this->db->table_name($this->db_groups).
-            " WHERE del<>1".
-            " AND contactgroup_id=?".
-            " AND user_id=?",
+            "SELECT * FROM " . $this->db->table_name($this->db_groups, true)
+            . " WHERE `del` <> 1 AND `contactgroup_id` = ? AND `user_id` = ?",
             $group_id, $this->user_id);
 
         if ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
@@ -228,25 +224,25 @@ class rcube_contacts extends rcube_addressbook
         $length = $subset != 0 ? abs($subset) : $this->page_size;
 
         if ($this->group_id)
-            $join = " LEFT JOIN ".$this->db->table_name($this->db_groupmembers)." AS m".
-                " ON (m.contact_id = c.".$this->primary_key.")";
+            $join = " LEFT JOIN " . $this->db->table_name($this->db_groupmembers, true) . " AS m".
+                " ON (m.`contact_id` = c.`".$this->primary_key."`)";
 
         $order_col = (in_array($this->sort_col, $this->table_cols) ? $this->sort_col : 'name');
-        $order_cols = array('c.'.$order_col);
+        $order_cols = array("c.`$order_col`");
         if ($order_col == 'firstname')
-            $order_cols[] = 'c.surname';
+            $order_cols[] = 'c.`surname`';
         else if ($order_col == 'surname')
-            $order_cols[] = 'c.firstname';
+            $order_cols[] = 'c.`firstname`';
         if ($order_col != 'name')
-            $order_cols[] = 'c.name';
-        $order_cols[] = 'c.email';
+            $order_cols[] = 'c.`name`';
+        $order_cols[] = 'c.`email`';
 
         $sql_result = $this->db->limitquery(
-            "SELECT * FROM ".$this->db->table_name($this->db_name)." AS c" .
+            "SELECT * FROM " . $this->db->table_name($this->db_name, true) . " AS c" .
             $join .
-            " WHERE c.del<>1" .
-                " AND c.user_id=?" .
-                ($this->group_id ? " AND m.contactgroup_id=?" : "").
+            " WHERE c.`del` <> 1" .
+                " AND c.`user_id` = ?" .
+                ($this->group_id ? " AND m.`contactgroup_id` = ?" : "").
                 ($this->filter ? " AND (".$this->filter.")" : "") .
             " ORDER BY ". $this->db->concat($order_cols) .
             " " . $this->sort_order,
@@ -442,7 +438,7 @@ class rcube_contacts extends rcube_addressbook
 
             // build WHERE clause
             $ids = $this->db->array2list($ids, 'integer');
-            $where = 'c.' . $this->primary_key.' IN ('.$ids.')';
+            $where = 'c.`' . $this->primary_key.'` IN ('.$ids.')';
             // reset counter
             unset($this->cache['count']);
 
@@ -486,17 +482,17 @@ class rcube_contacts extends rcube_addressbook
     private function _count()
     {
         if ($this->group_id)
-            $join = " LEFT JOIN ".$this->db->table_name($this->db_groupmembers)." AS m".
-                " ON (m.contact_id=c.".$this->primary_key.")";
+            $join = " LEFT JOIN " . $this->db->table_name($this->db_groupmembers, true) . " AS m".
+                " ON (m.`contact_id` = c.`".$this->primary_key."`)";
 
         // count contacts for this user
         $sql_result = $this->db->query(
-            "SELECT COUNT(c.contact_id) AS rows".
-            " FROM ".$this->db->table_name($this->db_name)." AS c".
+            "SELECT COUNT(c.`contact_id`) AS rows".
+            " FROM " . $this->db->table_name($this->db_name, true) . " AS c".
                 $join.
-            " WHERE c.del<>1".
-            " AND c.user_id=?".
-            ($this->group_id ? " AND m.contactgroup_id=?" : "").
+            " WHERE c.`del` <> 1".
+            " AND c.`user_id` = ?".
+            ($this->group_id ? " AND m.`contactgroup_id` = ?" : "").
             ($this->filter ? " AND (".$this->filter.")" : ""),
             $this->user_id,
             $this->group_id
@@ -534,10 +530,10 @@ class rcube_contacts extends rcube_addressbook
             return $assoc ? $first : $this->result;
 
         $this->db->query(
-            "SELECT * FROM ".$this->db->table_name($this->db_name).
-            " WHERE contact_id=?".
-                " AND user_id=?".
-                " AND del<>1",
+            "SELECT * FROM " . $this->db->table_name($this->db_name, true).
+            " WHERE `contact_id` = ?".
+                " AND `user_id` = ?".
+                " AND `del` <> 1",
             $id,
             $this->user_id
         );
@@ -566,9 +562,11 @@ class rcube_contacts extends rcube_addressbook
           return $results;
 
       $sql_result = $this->db->query(
-        "SELECT cgm.contactgroup_id, cg.name FROM " . $this->db->table_name($this->db_groupmembers) . " AS cgm" .
-        " LEFT JOIN " . $this->db->table_name($this->db_groups) . " AS cg ON (cgm.contactgroup_id = cg.contactgroup_id AND cg.del<>1)" .
-        " WHERE cgm.contact_id=?",
+        "SELECT cgm.`contactgroup_id`, cg.`name` "
+        . " FROM " . $this->db->table_name($this->db_groupmembers, true) . " AS cgm"
+        . " LEFT JOIN " . $this->db->table_name($this->db_groups, true) . " AS cg"
+            . " ON (cgm.`contactgroup_id` = cg.`contactgroup_id` AND cg.`del` <> 1)"
+        . " WHERE cgm.`contact_id` = ?",
         $id
       );
       while ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
@@ -636,8 +634,8 @@ class rcube_contacts extends rcube_addressbook
 
         if (!$existing->count && !empty($a_insert_cols)) {
             $this->db->query(
-                "INSERT INTO ".$this->db->table_name($this->db_name).
-                " (user_id, changed, del, ".join(', ', $a_insert_cols).")".
+                "INSERT INTO " . $this->db->table_name($this->db_name, true).
+                " (`user_id`, `changed`, `del`, ".join(', ', $a_insert_cols).")".
                 " VALUES (".intval($this->user_id).", ".$this->db->now().", 0, ".join(', ', $a_insert_values).")"
             );
 
@@ -671,11 +669,11 @@ class rcube_contacts extends rcube_addressbook
 
         if (!empty($write_sql)) {
             $this->db->query(
-                "UPDATE ".$this->db->table_name($this->db_name).
-                " SET changed=".$this->db->now().", ".join(', ', $write_sql).
-                " WHERE contact_id=?".
-                    " AND user_id=?".
-                    " AND del<>1",
+                "UPDATE " . $this->db->table_name($this->db_name, true).
+                " SET `changed` = ".$this->db->now().", ".join(', ', $write_sql).
+                " WHERE `contact_id` = ?".
+                    " AND `user_id` = ?".
+                    " AND `del` <> 1",
                 $id,
                 $this->user_id
             );
@@ -771,10 +769,10 @@ class rcube_contacts extends rcube_addressbook
 
         // flag record as deleted (always)
         $this->db->query(
-            "UPDATE ".$this->db->table_name($this->db_name).
-            " SET del=1, changed=".$this->db->now().
-            " WHERE user_id=?".
-                " AND contact_id IN ($ids)",
+            "UPDATE " . $this->db->table_name($this->db_name, true).
+            " SET `del` = 1, `changed` = ".$this->db->now().
+            " WHERE `user_id` = ?".
+                " AND `contact_id` IN ($ids)",
             $this->user_id
         );
 
@@ -798,10 +796,10 @@ class rcube_contacts extends rcube_addressbook
 
         // clear deleted flag
         $this->db->query(
-            "UPDATE ".$this->db->table_name($this->db_name).
-            " SET del=0, changed=".$this->db->now().
-            " WHERE user_id=?".
-                " AND contact_id IN ($ids)",
+            "UPDATE " . $this->db->table_name($this->db_name, true).
+            " SET `del` = 0, `changed` = ".$this->db->now().
+            " WHERE `user_id` = ?".
+                " AND `contact_id` IN ($ids)",
             $this->user_id
         );
 
@@ -822,16 +820,18 @@ class rcube_contacts extends rcube_addressbook
     {
         $this->cache = null;
 
-        $this->db->query("UPDATE " . $this->db->table_name($this->db_name)
-            . " SET del = 1, changed = " . $this->db->now()
-            . " WHERE user_id = ?", $this->user_id);
+        $now = $this->db->now();
+
+        $this->db->query("UPDATE " . $this->db->table_name($this->db_name, true)
+            . " SET `del` = 1, `changed` = $now"
+            . " WHERE `user_id` = ?", $this->user_id);
 
         $count = $this->db->affected_rows();
 
         if ($with_groups) {
-            $this->db->query("UPDATE " . $this->db->table_name($this->db_groups)
-                . " SET del = 1, changed = " . $this->db->now()
-                . " WHERE user_id = ?", $this->user_id);
+            $this->db->query("UPDATE " . $this->db->table_name($this->db_groups, true)
+                . " SET `del` = 1, `changed` = $now"
+                . " WHERE `user_id` = ?", $this->user_id);
 
             $count += $this->db->affected_rows();
         }
@@ -854,13 +854,14 @@ class rcube_contacts extends rcube_addressbook
         $name = $this->unique_groupname($name);
 
         $this->db->query(
-            "INSERT INTO ".$this->db->table_name($this->db_groups).
-            " (user_id, changed, name)".
+            "INSERT INTO " . $this->db->table_name($this->db_groups, true).
+            " (`user_id`, `changed`, `name`)".
             " VALUES (".intval($this->user_id).", ".$this->db->now().", ".$this->db->quote($name).")"
         );
 
-        if ($insert_id = $this->db->insert_id($this->db_groups))
+        if ($insert_id = $this->db->insert_id($this->db_groups)) {
             $result = array('id' => $insert_id, 'name' => $name);
+        }
 
         return $result;
     }
@@ -876,10 +877,10 @@ class rcube_contacts extends rcube_addressbook
     {
         // flag group record as deleted
         $this->db->query(
-            "UPDATE " . $this->db->table_name($this->db_groups)
-            . " SET del = 1, changed = " . $this->db->now()
-            . " WHERE contactgroup_id = ?"
-            . " AND user_id = ?",
+            "UPDATE " . $this->db->table_name($this->db_groups, true)
+            . " SET `del` = 1, `changed` = " . $this->db->now()
+            . " WHERE `contactgroup_id` = ?"
+                . " AND `user_id` = ?",
             $gid, $this->user_id
         );
 
@@ -901,10 +902,10 @@ class rcube_contacts extends rcube_addressbook
         $name = $this->unique_groupname($newname);
 
         $sql_result = $this->db->query(
-            "UPDATE ".$this->db->table_name($this->db_groups).
-            " SET name=?, changed=".$this->db->now().
-            " WHERE contactgroup_id=?".
-            " AND user_id=?",
+            "UPDATE " . $this->db->table_name($this->db_groups, true).
+            " SET `name` = ?, `changed` = ".$this->db->now().
+            " WHERE `contactgroup_id` = ?".
+                " AND `user_id` = ?",
             $name, $gid, $this->user_id
         );
 
@@ -930,9 +931,9 @@ class rcube_contacts extends rcube_addressbook
 
         // get existing assignments ...
         $sql_result = $this->db->query(
-            "SELECT contact_id FROM ".$this->db->table_name($this->db_groupmembers).
-            " WHERE contactgroup_id=?".
-                " AND contact_id IN (".$this->db->array2list($ids, 'integer').")",
+            "SELECT `contact_id` FROM " . $this->db->table_name($this->db_groupmembers, true).
+            " WHERE `contactgroup_id` = ?".
+                " AND `contact_id` IN (".$this->db->array2list($ids, 'integer').")",
             $group_id
         );
         while ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
@@ -943,8 +944,8 @@ class rcube_contacts extends rcube_addressbook
 
         foreach ($ids as $contact_id) {
             $this->db->query(
-                "INSERT INTO ".$this->db->table_name($this->db_groupmembers).
-                " (contactgroup_id, contact_id, created)".
+                "INSERT INTO " . $this->db->table_name($this->db_groupmembers, true).
+                " (`contactgroup_id`, `contact_id`, `created`)".
                 " VALUES (?, ?, ".$this->db->now().")",
                 $group_id,
                 $contact_id
@@ -976,9 +977,9 @@ class rcube_contacts extends rcube_addressbook
         $ids = $this->db->array2list($ids, 'integer');
 
         $sql_result = $this->db->query(
-            "DELETE FROM ".$this->db->table_name($this->db_groupmembers).
-            " WHERE contactgroup_id=?".
-                " AND contact_id IN ($ids)",
+            "DELETE FROM " . $this->db->table_name($this->db_groupmembers, true).
+            " WHERE `contactgroup_id` = ?".
+                " AND `contact_id` IN ($ids)",
             $group_id
         );
 
@@ -999,10 +1000,10 @@ class rcube_contacts extends rcube_addressbook
 
         do {
             $sql_result = $this->db->query(
-                "SELECT 1 FROM ".$this->db->table_name($this->db_groups).
-                " WHERE del<>1".
-                    " AND user_id=?".
-                    " AND name=?",
+                "SELECT 1 FROM " . $this->db->table_name($this->db_groups, true).
+                " WHERE `del` <> 1".
+                    " AND `user_id` = ?".
+                    " AND `name` = ?",
                 $this->user_id,
                 $checkname);
 
@@ -1014,5 +1015,4 @@ class rcube_contacts extends rcube_addressbook
 
         return $checkname;
     }
-
 }
diff --git a/lib/ext/Roundcube/rcube_csv2vcard.php b/lib/ext/Roundcube/rcube_csv2vcard.php
index 06bc387..b7d1591 100644
--- a/lib/ext/Roundcube/rcube_csv2vcard.php
+++ b/lib/ext/Roundcube/rcube_csv2vcard.php
@@ -149,6 +149,13 @@ class rcube_csv2vcard
 
         // GMail
         'groups'                => 'groups',
+        'group_membership'      => 'groups',
+        'given_name'            => 'firstname',
+        'additional_name'       => 'middlename',
+        'family_name'           => 'surname',
+        'name'                  => 'displayname',
+        'name_prefix'           => 'prefix',
+        'name_suffix'           => 'suffix',
     );
 
     /**
@@ -272,12 +279,95 @@ class rcube_csv2vcard
         'work_mobile'       => "Work Mobile",
         'work_title'        => "Work Title",
         'work_zip'          => "Work Zip",
-        'groups'            => "Group",
+        'group'             => "Group",
+
+        // GMail
+        'groups'            => "Groups",
+        'group_membership'  => "Group Membership",
+        'given_name'        => "Given Name",
+        'additional_name'   => "Additional Name",
+        'family_name'       => "Family Name",
+        'name'              => "Name",
+        'name_prefix'       => "Name Prefix",
+        'name_suffix'       => "Name Suffix",
+    );
+
+    /**
+     * Special fields map for GMail format
+     *
+     * @var array
+     */
+    protected $gmail_label_map = array(
+        'E-mail' => array(
+            'Value' => array(
+                'home' => 'email:home',
+                'work' => 'email:work',
+            ),
+        ),
+        'Phone' => array(
+            'Value' => array(
+                'home'    => 'phone:home',
+                'homefax' => 'phone:homefax',
+                'main'    => 'phone:pref',
+                'pager'   => 'phone:pager',
+                'mobile'  => 'phone:cell',
+                'work'    => 'phone:work',
+                'workfax' => 'phone:workfax',
+            ),
+        ),
+        'Relation' => array(
+            'Value' => array(
+                'spouse' => 'spouse',
+            ),
+        ),
+        'Website' => array(
+            'Value' => array(
+                'profile'  => 'website:profile',
+                'blog'     => 'website:blog',
+                'homepage' => 'website:homepage',
+                'work'     => 'website:work',
+            ),
+        ),
+        'Address' => array(
+            'Street' => array(
+                'home' => 'street:home',
+                'work' => 'street:work',
+            ),
+            'City' => array(
+                'home' => 'locality:home',
+                'work' => 'locality:work',
+            ),
+            'Region' => array(
+                'home' => 'region:home',
+                'work' => 'region:work',
+            ),
+            'Postal Code' => array(
+                'home' => 'zipcode:home',
+                'work' => 'zipcode:work',
+            ),
+            'Country' => array(
+                'home' => 'country:home',
+                'work' => 'country:work',
+            ),
+        ),
+        'Organization' => array(
+            'Name' => array(
+                '' => 'organization',
+            ),
+            'Title' => array(
+                '' => 'jobtitle',
+            ),
+            'Department' => array(
+                '' => 'department',
+            ),
+        ),
     );
 
+
     protected $local_label_map = array();
-    protected $vcards = array();
-    protected $map = array();
+    protected $vcards          = array();
+    protected $map             = array();
+    protected $gmail_map       = array();
 
 
     /**
@@ -308,16 +398,24 @@ class rcube_csv2vcard
     public function import($csv)
     {
         // convert to UTF-8
-        $head     = substr($csv, 0, 4096);
-        $charset  = rcube_charset::detect($head, RCUBE_CHARSET);
-        $csv      = rcube_charset::convert($csv, $charset);
-        $head     = '';
+        $head      = substr($csv, 0, 4096);
+        $charset   = rcube_charset::detect($head, RCUBE_CHARSET);
+        $csv       = rcube_charset::convert($csv, $charset);
+        $csv       = preg_replace(array('/^[\xFE\xFF]{2}/', '/^\xEF\xBB\xBF/', '/^\x00+/'), '', $csv); // also remove BOM
+        $head      = '';
+        $prev_line = false;
 
-        $this->map = array();
+        $this->map       = array();
+        $this->gmail_map = array();
 
         // Parse file
         foreach (preg_split("/[\r\n]+/", $csv) as $line) {
+            if (!empty($prev_line)) {
+                $line = '"' . $line;
+            }
+
             $elements = $this->parse_line($line);
+
             if (empty($elements)) {
                 continue;
             }
@@ -331,7 +429,28 @@ class rcube_csv2vcard
             }
             // Parse data row
             else {
+                // handle multiline elements (e.g. Gmail)
+                if (!empty($prev_line)) {
+                    $first = array_shift($elements);
+
+                    if ($first[0] == '"') {
+                        $prev_line[count($prev_line)-1] = '"' . $prev_line[count($prev_line)-1] . "\n" . substr($first, 1);
+                    }
+                    else {
+                        $prev_line[count($prev_line)-1] .= "\n" . $first;
+                    }
+
+                    $elements = array_merge($prev_line, $elements);
+                }
+
+                $last_element = $elements[count($elements)-1];
+                if ($last_element[0] == '"') {
+                    $elements[count($elements)-1] = substr($last_element, 1);
+                    $prev_line = $elements;
+                    continue;
+                }
                 $this->csv_to_vcard($elements);
+                $prev_line = false;
             }
         }
     }
@@ -389,6 +508,7 @@ class rcube_csv2vcard
                 $map1[$i] = $this->csv2vcard_map[$label];
             }
         }
+
         // check localized labels
         if (!empty($this->local_label_map)) {
             for ($i = 0; $i < $size; $i++) {
@@ -406,6 +526,22 @@ class rcube_csv2vcard
         }
 
         $this->map = count($map1) >= count($map2) ? $map1 : $map2;
+
+        // support special Gmail format
+        foreach ($this->gmail_label_map as $key => $items) {
+            $num = 1;
+            while (($_key = "$key $num - Type") && ($found = array_search($_key, $elements)) !== false) {
+                $this->gmail_map["$key:$num"] = array('_key' => $key, '_idx' => $found);
+                foreach (array_keys($items) as $item_key) {
+                    $_key = "$key $num - $item_key";
+                    if (($found = array_search($_key, $elements)) !== false) {
+                        $this->gmail_map["$key:$num"][$item_key] = $found;
+                    }
+                }
+
+                $num++;
+            }
+        }
     }
 
     /**
@@ -421,6 +557,22 @@ class rcube_csv2vcard
             }
         }
 
+        // Gmail format support
+        foreach ($this->gmail_map as $idx => $item) {
+            $type = preg_replace('/[^a-z]/', '', strtolower($data[$item['_idx']]));
+            $key  = $item['_key'];
+
+            unset($item['_idx']);
+            unset($item['_key']);
+
+            foreach ($item as $item_key => $item_idx) {
+                $value = $data[$item_idx];
+                if ($value !== null && $value !== '' && ($data_idx = $this->gmail_label_map[$key][$item_key][$type])) {
+                    $contact[$data_idx] = $value;
+                }
+            }
+        }
+
         if (empty($contact)) {
             return;
         }
@@ -430,9 +582,14 @@ class rcube_csv2vcard
             $contact['birthday'] = $contact['birthday-y'] .'-' .$contact['birthday-m'] . '-' . $contact['birthday-d'];
         }
 
-        // categories/groups separator in vCard is ',' not ';'
         if (!empty($contact['groups'])) {
+            // categories/groups separator in vCard is ',' not ';'
             $contact['groups'] = str_replace(';', ',', $contact['groups']);
+
+            // remove "* " added by GMail
+            if (!empty($this->gmail_map)) {
+                $contact['groups'] = str_replace('* ', '', $contact['groups']);
+            }
         }
 
         // Empty dates, e.g. "0/0/00", "0000-00-00 00:00:00"
diff --git a/lib/ext/Roundcube/rcube_db.php b/lib/ext/Roundcube/rcube_db.php
index 1e6a206..5a76f69 100644
--- a/lib/ext/Roundcube/rcube_db.php
+++ b/lib/ext/Roundcube/rcube_db.php
@@ -1063,6 +1063,10 @@ class rcube_db
      */
     public function table_name($table, $quoted = false)
     {
+        // let plugins alter the table name (#1489837)
+        $plugin = rcube::get_instance()->plugins->exec_hook('db_table_name', array('table' => $table));
+        $table = $plugin['table'];
+
         // add prefix to the table name if configured
         if (($prefix = $this->options['table_prefix']) && strpos($table, $prefix) !== 0) {
             $table = $prefix . $table;
diff --git a/lib/ext/Roundcube/rcube_db_oracle.php b/lib/ext/Roundcube/rcube_db_oracle.php
index 338eb2e..362beb0 100644
--- a/lib/ext/Roundcube/rcube_db_oracle.php
+++ b/lib/ext/Roundcube/rcube_db_oracle.php
@@ -500,7 +500,7 @@ class rcube_db_oracle extends rcube_db
 
         foreach (explode("\n", $sql) as $line) {
             $tok = strtolower(trim($line));
-            if (preg_match('/^--/', $line) || $tok == '') {
+            if (preg_match('/^--/', $line) || $tok == '' || $tok == '/') {
                 continue;
             }
 
diff --git a/lib/ext/Roundcube/rcube_html2text.php b/lib/ext/Roundcube/rcube_html2text.php
index 8628371..499c4b0 100644
--- a/lib/ext/Roundcube/rcube_html2text.php
+++ b/lib/ext/Roundcube/rcube_html2text.php
@@ -423,7 +423,7 @@ class rcube_html2text
         // Variables used for building the link list
         $this->_link_list = array();
 
-        $text = trim(stripslashes($this->html));
+        $text = $this->html;
 
         // Convert HTML to TXT
         $this->_converter($text);
diff --git a/lib/ext/Roundcube/rcube_image.php b/lib/ext/Roundcube/rcube_image.php
index 122b5f4..9c9f08b 100644
--- a/lib/ext/Roundcube/rcube_image.php
+++ b/lib/ext/Roundcube/rcube_image.php
@@ -59,11 +59,13 @@ class rcube_image
             $height  = $imsize[1];
             $gd_type = $imsize['2'];
             $type    = image_type_to_extension($imsize['2'], false);
+            $channels = $imsize['channels'];
         }
 
         // use ImageMagick
         if (!$type && ($data = $this->identify())) {
             list($type, $width, $height) = $data;
+            $channels = null;
         }
 
         if ($type) {
@@ -72,8 +74,11 @@ class rcube_image
                 'gd_type' => $gd_type,
                 'width'   => $width,
                 'height'  => $height,
+                'channels' => $channels,
             );
         }
+
+        return null;
     }
 
     /**
@@ -102,10 +107,10 @@ class rcube_image
         }
 
         // use Imagemagick
-        if ($convert) {
-            $p['out']  = $filename;
-            $p['in']   = $this->image_file;
-            $type      = $props['type'];
+        if ($convert || class_exists('Imagick', false)) {
+            $p['out'] = $filename;
+            $p['in']  = $this->image_file;
+            $type     = $props['type'];
 
             if (!$type && ($data = $this->identify())) {
                 $type = $data[0];
@@ -129,26 +134,49 @@ class rcube_image
                 $result = ($this->image_file == $filename || copy($this->image_file, $filename)) ? '' : false;
             }
             else {
-                if ($scale >= 1) {
-                    $width  = $props['width'];
-                    $height = $props['height'];
-                }
-                else {
-                    $width  = intval($props['width']  * $scale);
-                    $height = intval($props['height'] * $scale);
-                }
-
                 $valid_types = "bmp,eps,gif,jp2,jpg,png,svg,tif";
 
-                $p += array(
-                    'type'    => $type,
-                    'quality' => 75,
-                    'size'    => $width . 'x' . $height,
-                );
-
                 if (in_array($type, explode(',', $valid_types))) { // Valid type?
-                    $result = rcube::exec($convert . ' 2>&1 -flatten -auto-orient -colorspace sRGB -strip'
-                        . ' -quality {quality} -resize {size} {intype}:{in} {type}:{out}', $p);
+                    if ($scale >= 1) {
+                        $width  = $props['width'];
+                        $height = $props['height'];
+                    }
+                    else {
+                        $width  = intval($props['width']  * $scale);
+                        $height = intval($props['height'] * $scale);
+                    }
+
+                    // use ImageMagick in command line
+                    if ($convert) {
+                        $p += array(
+                            'type'    => $type,
+                            'quality' => 75,
+                            'size'    => $width . 'x' . $height,
+                        );
+
+                        $result = rcube::exec($convert . ' 2>&1 -flatten -auto-orient -colorspace sRGB -strip'
+                            . ' -quality {quality} -resize {size} {intype}:{in} {type}:{out}', $p);
+                    }
+                    // use PHP's Imagick class
+                    else {
+                        try {
+                            $image = new Imagick($this->image_file);
+                            $image = $image->flattenImages();
+
+                            $image->setImageColorspace(Imagick::COLORSPACE_SRGB);
+                            $image->setImageCompressionQuality(75);
+                            $image->setImageFormat($type);
+                            $image->stripImage();
+                            $image->scaleImage($width, $height);
+
+                            if ($image->writeImage($filename)) {
+                                $result = '';
+                            }
+                        }
+                        catch (Exception $e) {
+                            rcube::raise_error($e, true, false);
+                        }
+                    }
                 }
             }
 
@@ -158,6 +186,11 @@ class rcube_image
             }
         }
 
+        // do we have enough memory? (#1489937)
+        if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN' && !$this->mem_check($props)) {
+            return false;
+        }
+
         // use GD extension
         if ($props['gd_type']) {
             if ($props['gd_type'] == IMAGETYPE_JPEG && function_exists('imagecreatefromjpeg')) {
@@ -206,7 +239,7 @@ class rcube_image
                 $image = $new_image;
 
                 // fix rotation of image if EXIF data exists and specifies rotation (GD strips the EXIF data)
-                if ($this->image_file && function_exists('exif_read_data')) {
+                if ($this->image_file && $type == 'jpg' && function_exists('exif_read_data')) {
                     $exif = exif_read_data($this->image_file);
                     if ($exif && $exif['Orientation']) {
                         switch ($exif['Orientation']) {
@@ -267,7 +300,7 @@ class rcube_image
             }
         }
 
-        // use ImageMagick
+        // use ImageMagick in command line
         if ($convert) {
             $p['in']   = $this->image_file;
             $p['out']  = $filename;
@@ -276,14 +309,40 @@ class rcube_image
             $result = rcube::exec($convert . ' 2>&1 -colorspace sRGB -strip -quality 75 {in} {type}:{out}', $p);
 
             if ($result === '') {
-                @chmod($filename, 0600);
+                chmod($filename, 0600);
                 return true;
             }
         }
 
+        // use PHP's Imagick class
+        if (class_exists('Imagick', false)) {
+            try {
+                $image = new Imagick($this->image_file);
+
+                $image->setImageColorspace(Imagick::COLORSPACE_SRGB);
+                $image->setImageCompressionQuality(75);
+                $image->setImageFormat(self::$extensions[$type]);
+                $image->stripImage();
+
+                if ($image->writeImage($filename)) {
+                    @chmod($filename, 0600);
+                    return true;
+                }
+            }
+            catch (Exception $e) {
+                rcube::raise_error($e, true, false);
+            }
+        }
+
         // use GD extension (TIFF isn't supported)
         $props = $this->props();
 
+        // do we have enough memory? (#1489937)
+        if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN' && !$this->mem_check($props)) {
+            return false;
+        }
+
+
         if ($props['gd_type']) {
             if ($props['gd_type'] == IMAGETYPE_JPEG && function_exists('imagecreatefromjpeg')) {
                 $image = imagecreatefromjpeg($this->image_file);
@@ -320,12 +379,26 @@ class rcube_image
     }
 
     /**
-     * Identify command handler.
+     * Checks if image format conversion is supported
+     *
+     * @return boolean True if specified format can be converted to another format
+     */
+    public static function is_convertable($mimetype = null)
+    {
+        $rcube = rcube::get_instance();
+
+        // @TODO: check if specified mimetype is really supported
+        return class_exists('Imagick', false) || $rcube->config->get('im_convert_path');
+    }
+
+    /**
+     * ImageMagick based image properties read.
      */
     private function identify()
     {
         $rcube = rcube::get_instance();
 
+        // use ImageMagick in command line
         if ($cmd = $rcube->config->get('im_identify_path')) {
             $args = array('in' => $this->image_file, 'format' => "%m %[fx:w] %[fx:h]");
             $id   = rcube::exec($cmd. ' 2>/dev/null -format {format} {in}', $args);
@@ -334,6 +407,37 @@ class rcube_image
                 return explode(' ', strtolower($id));
             }
         }
+
+        // use PHP's Imagick class
+        if (class_exists('Imagick', false)) {
+            try {
+                $image = new Imagick($this->image_file);
+
+                return array(
+                    strtolower($image->getImageFormat()),
+                    $image->getImageWidth(),
+                    $image->getImageHeight(),
+                );
+            }
+            catch (Exception $e) {}
+        }
     }
 
+    /**
+     * Check if we have enough memory to load specified image
+     */
+    private function mem_check($props)
+    {
+        // image size is unknown, we can't calculate required memory
+        if (!$props['width']) {
+            return true;
+        }
+
+        // channels: CMYK - 4, RGB - 3
+        $multip = ($props['channels'] ?: 3) + 1;
+
+        // calculate image size in memory (in bytes)
+        $size = $props['width'] * $props['height'] * $multip;
+        return rcube_utils::mem_check($size);
+    }
 }
diff --git a/lib/ext/Roundcube/rcube_imap.php b/lib/ext/Roundcube/rcube_imap.php
index 18cf46d..a1fd874 100644
--- a/lib/ext/Roundcube/rcube_imap.php
+++ b/lib/ext/Roundcube/rcube_imap.php
@@ -56,6 +56,7 @@ class rcube_imap extends rcube_storage
      */
     protected $icache = array();
 
+    protected $plugins;
     protected $list_page = 1;
     protected $delimiter;
     protected $namespace;
@@ -82,6 +83,7 @@ class rcube_imap extends rcube_storage
     public function __construct()
     {
         $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
@@ -110,13 +112,13 @@ class rcube_imap extends rcube_storage
     /**
      * 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 +149,7 @@ class rcube_imap extends rcube_storage
 
         $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 +172,20 @@ class rcube_imap extends rcube_storage
         $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
@@ -332,6 +346,10 @@ class rcube_imap extends rcube_storage
         $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);
+        }
     }
 
 
@@ -757,7 +775,7 @@ class rcube_imap extends rcube_storage
         $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);
         }
 
@@ -945,6 +963,75 @@ class rcube_imap extends rcube_storage
             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);
@@ -1111,6 +1198,7 @@ class rcube_imap extends rcube_storage
         }
 
         foreach ($headers as $h) {
+            $h->folder = $folder;
             $a_msg_headers[$h->uid] = $h;
         }
 
@@ -1234,8 +1322,13 @@ class rcube_imap extends rcube_storage
                 return new rcube_result_index($folder, '* SORT');
             }
 
+            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?
-            if (($this->search_set instanceof rcube_result_index)
+            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))
             ) {
@@ -1291,7 +1384,7 @@ class rcube_imap extends rcube_storage
     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
@@ -1410,26 +1503,75 @@ class rcube_imap extends rcube_storage
      * 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';
         }
 
-        if (!strlen($folder)) {
+        if ((is_array($folder) && empty($folder)) || (!is_array($folder) && !strlen($folder))) {
             $folder = $this->folder;
         }
 
-        $results = $this->search_index($folder, $str, $charset, $sort_field);
+        $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();
+
+            // disable threading
+            $this->threading = false;
+
+            $searcher = new rcube_imap_search($this->options, $this->conn);
 
-        $this->set_search_set(array($str, $results, $charset, $sort_field,
-            $this->threading || $this->search_sorted ? true : false));
+            // 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,
+                $search,
+                $charset ? $charset : $this->default_charset,
+                $sort_field && $this->get_capability('SORT') ? $sort_field : null,
+                $this->threading
+            );
+        }
+        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);
+        }
+
+        $sorted = $this->threading || $this->search_sorted || $plugin['search_sorted'] ? true : false;
+
+        $this->set_search_set(array($search, $results, $charset, $sort_field, $sorted));
+
+        return $results;
     }
 
 
@@ -1443,20 +1585,27 @@ class rcube_imap extends rcube_storage
      */
     public function search_once($folder = null, $str = 'ALL')
     {
+        if (!$this->check_connection()) {
+            return new rcube_result_index();
+        }
+
         if (!$str) {
             $str = 'ALL';
         }
 
-        if (!strlen($folder)) {
-            $folder = $this->folder;
+        // 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);
         }
-
-        if (!$this->check_connection()) {
-            return new rcube_result_index();
+        else {
+            $folder = is_array($folder) ? $folder[0] : $folder;
+            if (!strlen($folder)) {
+                $folder = $this->folder;
+            }
+            $index = $this->conn->search($folder, $str, true);
         }
 
-        $index = $this->conn->search($folder, $str, true);
-
         return $index;
     }
 
@@ -1500,7 +1649,7 @@ class rcube_imap extends rcube_storage
             // 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;
@@ -1514,7 +1663,7 @@ class rcube_imap extends rcube_storage
             // 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()) {
@@ -1529,7 +1678,7 @@ class rcube_imap extends rcube_storage
         // 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;
@@ -1547,7 +1696,7 @@ class rcube_imap extends rcube_storage
      *
      * @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)) {
@@ -1556,12 +1705,15 @@ class rcube_imap extends rcube_storage
                 $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);
             }
@@ -1583,12 +1735,30 @@ class rcube_imap extends rcube_storage
     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;
+            }
+        }
+    }
+
 
     /**
      * Return message headers object of a specific message
@@ -1601,6 +1771,11 @@ class rcube_imap extends rcube_storage
      */
     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;
         }
@@ -1615,6 +1790,9 @@ class rcube_imap extends rcube_storage
         else {
             $headers = $this->conn->fetchHeader(
                 $folder, $uid, true, true, $this->get_fetch_headers());
+
+            if (is_object($headers))
+                $headers->folder = $folder;
         }
 
         return $headers;
@@ -1636,6 +1814,11 @@ class rcube_imap extends rcube_storage
             $folder = $this->folder;
         }
 
+        // decode combined UID-folder identifier
+        if (preg_match('/^\d+-.+/', $uid)) {
+            list($uid, $folder) = explode('-', $uid, 2);
+        }
+
         // Check internal cache
         if (!empty($this->icache['message'])) {
             if (($headers = $this->icache['message']) && $headers->uid == $uid) {
@@ -2282,6 +2465,8 @@ class rcube_imap extends rcube_storage
                     $this->clear_message_cache($folder, $all_mode ? null : explode(',', $uids));
                 }
             }
+
+            $this->set_search_dirty($folder);
         }
 
         return $result;
@@ -2329,6 +2514,17 @@ class rcube_imap extends rcube_storage
         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;
@@ -2365,19 +2561,7 @@ class rcube_imap extends rcube_storage
             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
@@ -2392,6 +2576,9 @@ class rcube_imap extends rcube_storage
         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)) {
@@ -2408,8 +2595,8 @@ class rcube_imap extends rcube_storage
                 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);
                 }
             }
 
@@ -2448,18 +2635,6 @@ class rcube_imap extends rcube_storage
             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
         $copied = $this->conn->copy($uids, $from_mbox, $to_mbox);
 
@@ -2508,13 +2683,15 @@ class rcube_imap extends rcube_storage
             // 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));
                 }
             }
@@ -2614,7 +2791,7 @@ class rcube_imap extends rcube_storage
         }
 
         // 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'])) {
@@ -2746,7 +2923,7 @@ class rcube_imap extends rcube_storage
         }
 
         // 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'])) {
@@ -2827,7 +3004,7 @@ class rcube_imap extends rcube_storage
      * @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();
@@ -2904,14 +3081,15 @@ class rcube_imap extends rcube_storage
 
     /**
      * 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;
@@ -2975,16 +3153,27 @@ class rcube_imap extends rcube_storage
      *
      * @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);
+
+        // it's quite often situation that we're trying to create and subscribe
+        // a folder that already exist, but is unsubscribed
+        if (!$result) {
+            if ($this->get_response_code() == rcube_storage::ALREADYEXISTS
+                || preg_match('/already exists/i', $this->get_error_str())
+            ) {
+                $result = true;
+            }
+        }
 
         // try to subscribe it
         if ($result) {
@@ -3109,19 +3298,86 @@ class rcube_imap extends rcube_storage
 
 
     /**
-     * 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);
+        $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'));
+
+        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;
+                        }
+                    }
+                }
             }
-            else if (!$this->folder_exists($folder, true)) {
-                $this->subscribe($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;
     }
 
 
@@ -3133,13 +3389,13 @@ class rcube_imap extends rcube_storage
      *
      * @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;
@@ -3150,10 +3406,24 @@ class rcube_imap extends rcube_storage
         }
 
         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)) {
@@ -3364,7 +3634,7 @@ class rcube_imap extends rcube_storage
         $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'])) {
@@ -3682,7 +3952,9 @@ class rcube_imap extends rcube_storage
             // @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);
+                    foreach ($result as $fldr => $data) {
+                        $res[$fldr] = array_merge((array) $res[$fldr], $data);
+                    }
                 }
             }
         }
@@ -3899,60 +4171,82 @@ class rcube_imap extends rcube_storage
      */
     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 and skip folders starting with '.'
         foreach ($a_folders as $folder) {
-            if ($folder[0] == '.') {
-                continue;
-            }
-
-            if (!$skip_default && ($p = array_search($folder, $this->default_folders)) !== false && !$a_defaults[$p]) {
-                $a_defaults[$p] = $folder;
-            }
-            else {
-                $folders[$folder] = rcube_charset::convert($folder, 'UTF7-IMAP');
+            if ($folder[0] != '.') {
+                // 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
diff --git a/lib/ext/Roundcube/rcube_imap_cache.php b/lib/ext/Roundcube/rcube_imap_cache.php
index e49e778..81df076 100644
--- a/lib/ext/Roundcube/rcube_imap_cache.php
+++ b/lib/ext/Roundcube/rcube_imap_cache.php
@@ -125,6 +125,11 @@ class rcube_imap_cache
 
         // cache all possible information by default
         $this->mode = self::MODE_INDEX | self::MODE_MESSAGE;
+
+        // database tables
+        $this->index_table    = $db->table_name('cache_index', true);
+        $this->thread_table   = $db->table_name('cache_thread', true);
+        $this->messages_table = $db->table_name('cache_messages', true);
     }
 
 
@@ -333,11 +338,11 @@ class rcube_imap_cache
         if ($this->mode & self::MODE_MESSAGE) {
             // Fetch messages from cache
             $sql_result = $this->db->query(
-                "SELECT uid, data, flags"
-                ." FROM ".$this->db->table_name('cache_messages')
-                ." WHERE user_id = ?"
-                    ." AND mailbox = ?"
-                    ." AND uid IN (".$this->db->array2list($msgs, 'integer').")",
+                "SELECT `uid`, `data`, `flags`"
+                ." FROM {$this->messages_table}"
+                ." WHERE `user_id` = ?"
+                    ." AND `mailbox` = ?"
+                    ." AND `uid` IN (".$this->db->array2list($msgs, 'integer').")",
                 $this->userid, $mailbox);
 
             $msgs = array_flip($msgs);
@@ -400,11 +405,11 @@ class rcube_imap_cache
 
         if ($this->mode & self::MODE_MESSAGE) {
             $sql_result = $this->db->query(
-                "SELECT flags, data"
-                ." FROM ".$this->db->table_name('cache_messages')
-                ." WHERE user_id = ?"
-                    ." AND mailbox = ?"
-                    ." AND uid = ?",
+                "SELECT `flags`, `data`"
+                ." FROM {$this->messages_table}"
+                ." WHERE `user_id` = ?"
+                    ." AND `mailbox` = ?"
+                    ." AND `uid` = ?",
                     $this->userid, $mailbox, (int)$uid);
 
             if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
@@ -480,11 +485,11 @@ class rcube_imap_cache
         // here will work as select, assume row exist if affected_rows=0)
         if (!$force) {
             $res = $this->db->query(
-                "UPDATE ".$this->db->table_name('cache_messages')
-                ." SET flags = ?, data = ?, expires = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
-                ." WHERE user_id = ?"
-                    ." AND mailbox = ?"
-                    ." AND uid = ?",
+                "UPDATE {$this->messages_table}"
+                ." SET `flags` = ?, `data` = ?, `expires` = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
+                ." WHERE `user_id` = ?"
+                    ." AND `mailbox` = ?"
+                    ." AND `uid` = ?",
                 $flags, $msg, $this->userid, $mailbox, (int) $message->uid);
 
             if ($this->db->affected_rows($res)) {
@@ -496,8 +501,8 @@ class rcube_imap_cache
 
         // insert new record
         $res = $this->db->query(
-            "INSERT INTO ".$this->db->table_name('cache_messages')
-            ." (user_id, mailbox, uid, flags, expires, data)"
+            "INSERT INTO {$this->messages_table}"
+            ." (`user_id`, `mailbox`, `uid`, `flags`, `expires`, `data`)"
             ." VALUES (?, ?, ?, ?, ". ($this->ttl ? $this->db->now($this->ttl) : 'NULL') . ", ?)",
             $this->userid, $mailbox, (int) $message->uid, $flags, $msg);
 
@@ -505,12 +510,12 @@ class rcube_imap_cache
         // thanks to ignore_key_errors "duplicate row" errors will be ignored
         if ($force && !$res && !$this->db->is_error($res)) {
             $this->db->query(
-                "UPDATE ".$this->db->table_name('cache_messages')
-                ." SET expires = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
-                    .", flags = ?, data = ?"
-                ." WHERE user_id = ?"
-                    ." AND mailbox = ?"
-                    ." AND uid = ?",
+                "UPDATE {$this->messages_table}"
+                ." SET `expires` = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
+                    .", `flags` = ?, `data` = ?"
+                ." WHERE `user_id` = ?"
+                    ." AND `mailbox` = ?"
+                    ." AND `uid` = ?",
                 $flags, $msg, $this->userid, $mailbox, (int) $message->uid);
         }
 
@@ -557,14 +562,16 @@ class rcube_imap_cache
             }
         }
 
+        $binary_check = $this->db->db_provider == 'oracle' ? "BITAND(`flags`, %d)" : "(`flags` & %d)";
+
         $this->db->query(
-            "UPDATE ".$this->db->table_name('cache_messages')
-            ." SET expires = ". ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
-            .", flags = flags ".($enabled ? "+ $idx" : "- $idx")
-            ." WHERE user_id = ?"
-                ." AND mailbox = ?"
-                .(!empty($uids) ? " AND uid IN (".$this->db->array2list($uids, 'integer').")" : "")
-                ." AND (flags & $idx) ".($enabled ? "= 0" : "= $idx"),
+            "UPDATE {$this->messages_table}"
+            ." SET `expires` = ". ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
+            .", `flags` = `flags` ".($enabled ? "+ $idx" : "- $idx")
+            ." WHERE `user_id` = ?"
+                ." AND `mailbox` = ?"
+                .(!empty($uids) ? " AND `uid` IN (".$this->db->array2list($uids, 'integer').")" : "")
+                ." AND " . sprintf($binary_check, $idx) . ($enabled ? " = 0" : " = $idx"),
             $this->userid, $mailbox);
     }
 
@@ -583,8 +590,8 @@ class rcube_imap_cache
 
         if (!strlen($mailbox)) {
             $this->db->query(
-                "DELETE FROM ".$this->db->table_name('cache_messages')
-                ." WHERE user_id = ?",
+                "DELETE FROM {$this->messages_table}"
+                ." WHERE `user_id` = ?",
                 $this->userid);
         }
         else {
@@ -597,10 +604,10 @@ class rcube_imap_cache
             }
 
             $this->db->query(
-                "DELETE FROM ".$this->db->table_name('cache_messages')
-                ." WHERE user_id = ?"
-                    ." AND mailbox = ?"
-                    .($uids !== null ? " AND uid IN (".$this->db->array2list((array)$uids, 'integer').")" : ""),
+                "DELETE FROM {$this->messages_table}"
+                ." WHERE `user_id` = ?"
+                    ." AND `mailbox` = ?"
+                    .($uids !== null ? " AND `uid` IN (".$this->db->array2list((array)$uids, 'integer').")" : ""),
                 $this->userid, $mailbox);
         }
     }
@@ -619,18 +626,18 @@ class rcube_imap_cache
         // otherwise use 'valid' flag to not loose HIGHESTMODSEQ value
         if ($remove) {
             $this->db->query(
-                "DELETE FROM ".$this->db->table_name('cache_index')
-                ." WHERE user_id = ?"
-                    .(strlen($mailbox) ? " AND mailbox = ".$this->db->quote($mailbox) : ""),
+                "DELETE FROM {$this->index_table}"
+                ." WHERE `user_id` = ?"
+                    .(strlen($mailbox) ? " AND `mailbox` = ".$this->db->quote($mailbox) : ""),
                 $this->userid
             );
         }
         else {
             $this->db->query(
-                "UPDATE ".$this->db->table_name('cache_index')
-                ." SET valid = 0"
-                ." WHERE user_id = ?"
-                    .(strlen($mailbox) ? " AND mailbox = ".$this->db->quote($mailbox) : ""),
+                "UPDATE {$this->index_table}"
+                ." SET `valid` = 0"
+                ." WHERE `user_id` = ?"
+                    .(strlen($mailbox) ? " AND `mailbox` = ".$this->db->quote($mailbox) : ""),
                 $this->userid
             );
         }
@@ -654,9 +661,9 @@ class rcube_imap_cache
     function remove_thread($mailbox = null)
     {
         $this->db->query(
-            "DELETE FROM ".$this->db->table_name('cache_thread')
-            ." WHERE user_id = ?"
-                .(strlen($mailbox) ? " AND mailbox = ".$this->db->quote($mailbox) : ""),
+            "DELETE FROM {$this->thread_table}"
+            ." WHERE `user_id` = ?"
+                .(strlen($mailbox) ? " AND `mailbox` = ".$this->db->quote($mailbox) : ""),
             $this->userid
         );
 
@@ -692,15 +699,16 @@ class rcube_imap_cache
     {
         $rcube = rcube::get_instance();
         $db    = $rcube->get_dbh();
+        $now   = $db->now();
 
-        $db->query("DELETE FROM ".$db->table_name('cache_messages')
-              ." WHERE expires < " . $db->now());
+        $db->query("DELETE FROM " . $db->table_name('cache_messages', true)
+              ." WHERE `expires` < $now");
 
-        $db->query("DELETE FROM ".$db->table_name('cache_index')
-              ." WHERE expires < " . $db->now());
+        $db->query("DELETE FROM " . $db->table_name('cache_index', true)
+              ." WHERE `expires` < $now");
 
-        $db->query("DELETE FROM ".$db->table_name('cache_thread')
-              ." WHERE expires < " . $db->now());
+        $db->query("DELETE FROM ".$db->table_name('cache_thread', true)
+              ." WHERE `expires` < $now");
     }
 
 
@@ -711,10 +719,10 @@ class rcube_imap_cache
     {
         // Get index from DB
         $sql_result = $this->db->query(
-            "SELECT data, valid"
-            ." FROM ".$this->db->table_name('cache_index')
-            ." WHERE user_id = ?"
-                ." AND mailbox = ?",
+            "SELECT `data`, `valid`"
+            ." FROM {$this->index_table}"
+            ." WHERE `user_id` = ?"
+                ." AND `mailbox` = ?",
             $this->userid, $mailbox);
 
         if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
@@ -748,10 +756,10 @@ class rcube_imap_cache
     {
         // Get thread from DB
         $sql_result = $this->db->query(
-            "SELECT data"
-            ." FROM ".$this->db->table_name('cache_thread')
-            ." WHERE user_id = ?"
-                ." AND mailbox = ?",
+            "SELECT `data`"
+            ." FROM {$this->thread_table}"
+            ." WHERE `user_id` = ?"
+                ." AND `mailbox` = ?",
             $this->userid, $mailbox);
 
         if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
@@ -789,14 +797,16 @@ class rcube_imap_cache
             (int) $mbox_data['UIDNEXT'],
             $modseq ? $modseq : $mbox_data['HIGHESTMODSEQ'],
         );
-        $data = implode('@', $data);
+
+        $data    = implode('@', $data);
+        $expires = $this->ttl ? $this->db->now($this->ttl) : 'NULL';
 
         if ($exists) {
             $res = $this->db->query(
-                "UPDATE ".$this->db->table_name('cache_index')
-                ." SET data = ?, valid = 1, expires = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
-                ." WHERE user_id = ?"
-                    ." AND mailbox = ?",
+                "UPDATE {$this->index_table}"
+                ." SET `data` = ?, `valid` = 1, `expires` = $expires"
+                ." WHERE `user_id` = ?"
+                    ." AND `mailbox` = ?",
                 $data, $this->userid, $mailbox);
 
             if ($this->db->affected_rows($res)) {
@@ -807,19 +817,19 @@ class rcube_imap_cache
         $this->db->set_option('ignore_key_errors', true);
 
         $res = $this->db->query(
-            "INSERT INTO ".$this->db->table_name('cache_index')
-            ." (user_id, mailbox, valid, expires, data)"
-            ." VALUES (?, ?, 1, ". ($this->ttl ? $this->db->now($this->ttl) : 'NULL') .", ?)",
+            "INSERT INTO {$this->index_table}"
+            ." (`user_id`, `mailbox`, `valid`, `expires`, `data`)"
+            ." VALUES (?, ?, 1, $expires, ?)",
             $this->userid, $mailbox, $data);
 
         // race-condition, insert failed so try update (#1489146)
         // thanks to ignore_key_errors "duplicate row" errors will be ignored
         if (!$exists && !$res && !$this->db->is_error($res)) {
             $res = $this->db->query(
-                "UPDATE ".$this->db->table_name('cache_index')
-                ." SET data = ?, valid = 1, expires = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
-                ." WHERE user_id = ?"
-                    ." AND mailbox = ?",
+                "UPDATE {$this->index_table}"
+                ." SET `data` = ?, `valid` = 1, `expires` = $expires"
+                ." WHERE `user_id` = ?"
+                    ." AND `mailbox` = ?",
                 $data, $this->userid, $mailbox);
         }
 
@@ -838,16 +848,16 @@ class rcube_imap_cache
             (int) $mbox_data['UIDVALIDITY'],
             (int) $mbox_data['UIDNEXT'],
         );
-        $data = implode('@', $data);
 
-        $expires = ($this->ttl ? $this->db->now($this->ttl) : 'NULL');
+        $data    = implode('@', $data);
+        $expires = $this->ttl ? $this->db->now($this->ttl) : 'NULL';
 
         if ($exists) {
             $res = $this->db->query(
-                "UPDATE ".$this->db->table_name('cache_thread')
-                ." SET data = ?, expires = $expires"
-                ." WHERE user_id = ?"
-                    ." AND mailbox = ?",
+                "UPDATE {$this->thread_table}"
+                ." SET `data` = ?, `expires` = $expires"
+                ." WHERE `user_id` = ?"
+                    ." AND `mailbox` = ?",
                 $data, $this->userid, $mailbox);
 
             if ($this->db->affected_rows($res)) {
@@ -858,8 +868,8 @@ class rcube_imap_cache
         $this->db->set_option('ignore_key_errors', true);
 
         $res = $this->db->query(
-            "INSERT INTO ".$this->db->table_name('cache_thread')
-            ." (user_id, mailbox, expires, data)"
+            "INSERT INTO {$this->thread_table}"
+            ." (`user_id`, `mailbox`, `expires`, `data`)"
             ." VALUES (?, ?, $expires, ?)",
             $this->userid, $mailbox, $data);
 
@@ -867,10 +877,10 @@ class rcube_imap_cache
         // thanks to ignore_key_errors "duplicate row" errors will be ignored
         if (!$exists && !$res && !$this->db->is_error($res)) {
             $this->db->query(
-                "UPDATE ".$this->db->table_name('cache_thread')
-                ." SET expires = $expires, data = ?"
-                ." WHERE user_id = ?"
-                    ." AND mailbox = ?",
+                "UPDATE {$this->thread_table}"
+                ." SET `expires` = $expires, `data` = ?"
+                ." WHERE `user_id` = ?"
+                    ." AND `mailbox` = ?",
                 $data, $this->userid, $mailbox);
         }
 
@@ -1086,10 +1096,10 @@ class rcube_imap_cache
         // Get known UIDs
         if ($this->mode & self::MODE_MESSAGE) {
             $sql_result = $this->db->query(
-                "SELECT uid"
-                ." FROM ".$this->db->table_name('cache_messages')
-                ." WHERE user_id = ?"
-                    ." AND mailbox = ?",
+                "SELECT `uid`"
+                ." FROM {$this->messages_table}"
+                ." WHERE `user_id` = ?"
+                    ." AND `mailbox` = ?",
                 $this->userid, $mailbox);
 
             while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
@@ -1125,12 +1135,12 @@ class rcube_imap_cache
                     }
 
                     $this->db->query(
-                        "UPDATE ".$this->db->table_name('cache_messages')
-                        ." SET flags = ?, expires = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
-                        ." WHERE user_id = ?"
-                            ." AND mailbox = ?"
-                            ." AND uid = ?"
-                            ." AND flags <> ?",
+                        "UPDATE {$this->messages_table}"
+                        ." SET `flags` = ?, `expires` = " . ($this->ttl ? $this->db->now($this->ttl) : 'NULL')
+                        ." WHERE `user_id` = ?"
+                            ." AND `mailbox` = ?"
+                            ." AND `uid` = ?"
+                            ." AND `flags` <> ?",
                         $flags, $this->userid, $mailbox, $uid, $flags);
                 }
             }
@@ -1235,13 +1245,15 @@ class rcube_imap_cache
     private function message_object_prepare(&$msg, &$size = 0)
     {
         // Remove body too big
-        if ($msg->body && ($length = strlen($msg->body))) {
-            $size += $length;
+        if (isset($msg->body)) {
+            $length = strlen($msg->body);
 
-            if ($size > $this->threshold * 1024) {
-                $size -= $length;
+            if ($msg->body_modified || $size + $length > $this->threshold * 1024) {
                 unset($msg->body);
             }
+            else {
+                $size += $length;
+            }
         }
 
         // Fix mimetype which might be broken by some code when message is displayed
diff --git a/lib/ext/Roundcube/rcube_imap_generic.php b/lib/ext/Roundcube/rcube_imap_generic.php
index f465ac1..d78b526 100644
--- a/lib/ext/Roundcube/rcube_imap_generic.php
+++ b/lib/ext/Roundcube/rcube_imap_generic.php
@@ -50,17 +50,17 @@ class rcube_imap_generic
 
     public static $mupdate;
 
-    private $fp;
-    private $host;
-    private $logged = false;
-    private $capability = array();
-    private $capability_readed = false;
-    private $prefs;
-    private $cmd_tag;
-    private $cmd_num = 0;
-    private $resourceid;
-    private $_debug = false;
-    private $_debug_handler = false;
+    protected $fp;
+    protected $host;
+    protected $logged = false;
+    protected $capability = array();
+    protected $capability_readed = false;
+    protected $prefs;
+    protected $cmd_tag;
+    protected $cmd_num = 0;
+    protected $resourceid;
+    protected $_debug = false;
+    protected $_debug_handler = false;
 
     const ERROR_OK = 0;
     const ERROR_NO = -1;
@@ -352,7 +352,7 @@ class rcube_imap_generic
      *
      * @return bool True if connection is closed
      */
-    private function eof()
+    protected function eof()
     {
         if (!is_resource($this->fp)) {
             return true;
@@ -375,7 +375,7 @@ class rcube_imap_generic
     /**
      * Closes connection stream.
      */
-    private function closeSocket()
+    protected function closeSocket()
     {
         @fclose($this->fp);
         $this->fp = null;
@@ -421,7 +421,7 @@ class rcube_imap_generic
         return false;
     }
 
-    private function hasCapability($name)
+    protected function hasCapability($name)
     {
         if (empty($this->capability) || $name == '') {
             return false;
@@ -723,110 +723,38 @@ class rcube_imap_generic
         // configure
         $this->set_prefs($options);
 
-        $auth_method = $this->prefs['auth_type'];
-        $result      = false;
-
-        // initialize connection
-        $this->error    = '';
-        $this->errornum = self::ERROR_OK;
-        $this->selected = null;
-        $this->user     = $user;
         $this->host     = $host;
+        $this->user     = $user;
         $this->logged   = false;
+        $this->selected = null;
 
         // check input
         if (empty($host)) {
             $this->setError(self::ERROR_BAD, "Empty host");
             return false;
         }
+
         if (empty($user)) {
             $this->setError(self::ERROR_NO, "Empty user");
             return false;
         }
+
         if (empty($password)) {
             $this->setError(self::ERROR_NO, "Empty password");
             return false;
         }
 
-        if (!$this->prefs['port']) {
-            $this->prefs['port'] = 143;
-        }
-        // check for SSL
-        if ($this->prefs['ssl_mode'] && $this->prefs['ssl_mode'] != 'tls') {
-            $host = $this->prefs['ssl_mode'] . '://' . $host;
-        }
-
-        if ($this->prefs['timeout'] <= 0) {
-            $this->prefs['timeout'] = max(0, intval(ini_get('default_socket_timeout')));
-        }
-
         // Connect
-        $this->fp = @fsockopen($host, $this->prefs['port'], $errno, $errstr, $this->prefs['timeout']);
-
-        if (!$this->fp) {
-            if (!$errstr) {
-                $errstr = "Unknown reason (fsockopen() function disabled?)";
-            }
-            $this->setError(self::ERROR_BAD, sprintf("Could not connect to %s:%d: %s", $host, $this->prefs['port'], $errstr));
-            return false;
-        }
-
-        if ($this->prefs['timeout'] > 0) {
-            stream_set_timeout($this->fp, $this->prefs['timeout']);
-        }
-
-        $line = trim(fgets($this->fp, 8192));
-
-        if ($this->_debug) {
-            // set connection identifier for debug output
-            preg_match('/#([0-9]+)/', (string)$this->fp, $m);
-            $this->resourceid = strtoupper(substr(md5($m[1].$this->user.microtime()), 0, 4));
-
-            if ($line)
-                $this->debug('S: '. $line);
-        }
-
-        // Connected to wrong port or connection error?
-        if (!preg_match('/^\* (OK|PREAUTH)/i', $line)) {
-            if ($line)
-                $error = sprintf("Wrong startup greeting (%s:%d): %s", $host, $this->prefs['port'], $line);
-            else
-                $error = sprintf("Empty startup greeting (%s:%d)", $host, $this->prefs['port']);
-
-            $this->setError(self::ERROR_BAD, $error);
-            $this->closeConnection();
+        if (!$this->_connect($host)) {
             return false;
         }
 
-        // RFC3501 [7.1] optional CAPABILITY response
-        if (preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches)) {
-            $this->parseCapability($matches[1], true);
-        }
-
-        // TLS connection
-        if ($this->prefs['ssl_mode'] == 'tls' && $this->getCapability('STARTTLS')) {
-            $res = $this->execute('STARTTLS');
-
-            if ($res[0] != self::ERROR_OK) {
-                $this->closeConnection();
-                return false;
-            }
-
-            if (!stream_socket_enable_crypto($this->fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
-                $this->setError(self::ERROR_BAD, "Unable to negotiate TLS");
-                $this->closeConnection();
-                return false;
-            }
-
-            // Now we're secure, capabilities need to be reread
-            $this->clearCapability();
-        }
-
         // Send ID info
         if (!empty($this->prefs['ident']) && $this->getCapability('ID')) {
             $this->id($this->prefs['ident']);
         }
 
+        $auth_method  = $this->prefs['auth_type'];
         $auth_methods = array();
         $result       = null;
 
@@ -901,6 +829,103 @@ class rcube_imap_generic
     }
 
     /**
+     * Connects to IMAP server.
+     *
+     * @param string $host Server hostname or IP
+     *
+     * @return bool True on success, False on failure
+     */
+    protected function _connect($host)
+    {
+        // initialize connection
+        $this->error    = '';
+        $this->errornum = self::ERROR_OK;
+
+        if (!$this->prefs['port']) {
+            $this->prefs['port'] = 143;
+        }
+
+        // check for SSL
+        if ($this->prefs['ssl_mode'] && $this->prefs['ssl_mode'] != 'tls') {
+            $host = $this->prefs['ssl_mode'] . '://' . $host;
+        }
+
+        if ($this->prefs['timeout'] <= 0) {
+            $this->prefs['timeout'] = max(0, intval(ini_get('default_socket_timeout')));
+        }
+
+        if (!empty($this->prefs['socket_options'])) {
+            $context  = stream_context_create($this->prefs['socket_options']);
+            $this->fp = stream_socket_client($host . ':' . $this->prefs['port'], $errno, $errstr,
+                $this->prefs['timeout'], STREAM_CLIENT_CONNECT, $context);
+        }
+        else {
+            $this->fp = @fsockopen($host, $this->prefs['port'], $errno, $errstr, $this->prefs['timeout']);
+        }
+
+        if (!$this->fp) {
+            $this->setError(self::ERROR_BAD, sprintf("Could not connect to %s:%d: %s",
+                $host, $this->prefs['port'], $errstr ?: "Unknown reason"));
+
+            return false;
+        }
+
+        if ($this->prefs['timeout'] > 0) {
+            stream_set_timeout($this->fp, $this->prefs['timeout']);
+        }
+
+        $line = trim(fgets($this->fp, 8192));
+
+        if ($this->_debug) {
+            // set connection identifier for debug output
+            preg_match('/#([0-9]+)/', (string) $this->fp, $m);
+            $this->resourceid = strtoupper(substr(md5($m[1].$this->user.microtime()), 0, 4));
+
+            if ($line) {
+                $this->debug('S: '. $line);
+            }
+        }
+
+        // Connected to wrong port or connection error?
+        if (!preg_match('/^\* (OK|PREAUTH)/i', $line)) {
+            if ($line)
+                $error = sprintf("Wrong startup greeting (%s:%d): %s", $host, $this->prefs['port'], $line);
+            else
+                $error = sprintf("Empty startup greeting (%s:%d)", $host, $this->prefs['port']);
+
+            $this->setError(self::ERROR_BAD, $error);
+            $this->closeConnection();
+            return false;
+        }
+
+        // RFC3501 [7.1] optional CAPABILITY response
+        if (preg_match('/\[CAPABILITY ([^]]+)\]/i', $line, $matches)) {
+            $this->parseCapability($matches[1], true);
+        }
+
+        // TLS connection
+        if ($this->prefs['ssl_mode'] == 'tls' && $this->getCapability('STARTTLS')) {
+            $res = $this->execute('STARTTLS');
+
+            if ($res[0] != self::ERROR_OK) {
+                $this->closeConnection();
+                return false;
+            }
+
+            if (!stream_socket_enable_crypto($this->fp, true, STREAM_CRYPTO_METHOD_TLS_CLIENT)) {
+                $this->setError(self::ERROR_BAD, "Unable to negotiate TLS");
+                $this->closeConnection();
+                return false;
+            }
+
+            // Now we're secure, capabilities need to be reread
+            $this->clearCapability();
+        }
+
+        return true;
+    }
+
+    /**
      * Initializes environment
      */
     protected function set_prefs($prefs)
@@ -1191,13 +1216,20 @@ class rcube_imap_generic
      * Folder creation (CREATE)
      *
      * @param string $mailbox Mailbox name
+     * @param array  $types    Optional folder types (RFC 6154)
      *
      * @return bool True on success, False on error
      */
-    function createFolder($mailbox)
+    function createFolder($mailbox, $types = null)
     {
-        $result = $this->execute('CREATE', array($this->escape($mailbox)),
-            self::COMMAND_NORESPONSE);
+        $args = array($this->escape($mailbox));
+
+        // RFC 6154: CREATE-SPECIAL-USE
+        if (!empty($types) && $this->getCapability('CREATE-SPECIAL-USE')) {
+            $args[] = '(USE (' . implode(' ', $types) . '))';
+        }
+
+        $result = $this->execute('CREATE', $args, self::COMMAND_NORESPONSE);
 
         return ($result == self::ERROR_OK);
     }
@@ -1261,15 +1293,15 @@ class rcube_imap_generic
      *
      * @param string $ref         Reference name
      * @param string $mailbox     Mailbox name
-     * @param array  $status_opts (see self::_listMailboxes)
+     * @param array  $return_opts (see self::_listMailboxes)
      * @param array  $select_opts (see self::_listMailboxes)
      *
-     * @return array List of mailboxes or hash of options if $status_opts argument
-     *               is non-empty.
+     * @return array|bool List of mailboxes or hash of options if STATUS/MYROGHTS response
+     *                    is requested, False on error.
      */
-    function listMailboxes($ref, $mailbox, $status_opts=array(), $select_opts=array())
+    function listMailboxes($ref, $mailbox, $return_opts=array(), $select_opts=array())
     {
-        return $this->_listMailboxes($ref, $mailbox, false, $status_opts, $select_opts);
+        return $this->_listMailboxes($ref, $mailbox, false, $return_opts, $select_opts);
     }
 
     /**
@@ -1277,14 +1309,14 @@ class rcube_imap_generic
      *
      * @param string $ref         Reference name
      * @param string $mailbox     Mailbox name
-     * @param array  $status_opts (see self::_listMailboxes)
+     * @param array  $return_opts (see self::_listMailboxes)
      *
-     * @return array List of mailboxes or hash of options if $status_opts argument
-     *               is non-empty.
+     * @return array|bool List of mailboxes or hash of options if STATUS/MYROGHTS response
+     *                    is requested, False on error.
      */
-    function listSubscribed($ref, $mailbox, $status_opts=array())
+    function listSubscribed($ref, $mailbox, $return_opts=array())
     {
-        return $this->_listMailboxes($ref, $mailbox, true, $status_opts, NULL);
+        return $this->_listMailboxes($ref, $mailbox, true, $return_opts, NULL);
     }
 
     /**
@@ -1293,22 +1325,25 @@ class rcube_imap_generic
      * @param string $ref         Reference name
      * @param string $mailbox     Mailbox name
      * @param bool   $subscribed  Enables returning subscribed mailboxes only
-     * @param array  $status_opts List of STATUS options (RFC5819: LIST-STATUS)
-     *                            Possible: MESSAGES, RECENT, UIDNEXT, UIDVALIDITY, UNSEEN
+     * @param array  $return_opts List of RETURN options (RFC5819: LIST-STATUS, RFC5258: LIST-EXTENDED)
+     *                            Possible: MESSAGES, RECENT, UIDNEXT, UIDVALIDITY, UNSEEN,
+     *                                      MYRIGHTS, SUBSCRIBED, CHILDREN
      * @param array  $select_opts List of selection options (RFC5258: LIST-EXTENDED)
-     *                            Possible: SUBSCRIBED, RECURSIVEMATCH, REMOTE
+     *                            Possible: SUBSCRIBED, RECURSIVEMATCH, REMOTE,
+     *                                      SPECIAL-USE (RFC6154)
      *
-     * @return array List of mailboxes or hash of options if $status_ops argument
-     *               is non-empty.
+     * @return array|bool List of mailboxes or hash of options if STATUS/MYROGHTS response
+     *                    is requested, False on error.
      */
-    private function _listMailboxes($ref, $mailbox, $subscribed=false,
-        $status_opts=array(), $select_opts=array())
+    protected function _listMailboxes($ref, $mailbox, $subscribed=false,
+        $return_opts=array(), $select_opts=array())
     {
         if (!strlen($mailbox)) {
             $mailbox = '*';
         }
 
         $args = array();
+        $rets = array();
 
         if (!empty($select_opts) && $this->getCapability('LIST-EXTENDED')) {
             $select_opts = (array) $select_opts;
@@ -1319,11 +1354,29 @@ class rcube_imap_generic
         $args[] = $this->escape($ref);
         $args[] = $this->escape($mailbox);
 
-        if (!empty($status_opts) && $this->getCapability('LIST-STATUS')) {
-            $status_opts = (array) $status_opts;
-            $lstatus = true;
+        if (!empty($return_opts) && $this->getCapability('LIST-EXTENDED')) {
+            $ext_opts    = array('SUBSCRIBED', 'CHILDREN');
+            $rets        = array_intersect($return_opts, $ext_opts);
+            $return_opts = array_diff($return_opts, $rets);
+        }
 
-            $args[] = 'RETURN (STATUS (' . implode(' ', $status_opts) . '))';
+        if (!empty($return_opts) && $this->getCapability('LIST-STATUS')) {
+            $lstatus     = true;
+            $status_opts = array('MESSAGES', 'RECENT', 'UIDNEXT', 'UIDVALIDITY', 'UNSEEN');
+            $opts        = array_diff($return_opts, $status_opts);
+            $status_opts = array_diff($return_opts, $opts);
+
+            if (!empty($status_opts)) {
+                $rets[] = 'STATUS (' . implode(' ', $status_opts) . ')';
+            }
+
+            if (!empty($opts)) {
+                $rets = array_merge($rets, $opts);
+            }
+        }
+
+        if (!empty($rets)) {
+            $args[] = 'RETURN (' . implode(' ', $rets) . ')';
         }
 
         list($code, $response) = $this->execute($subscribed ? 'LSUB' : 'LIST', $args);
@@ -1343,9 +1396,10 @@ class rcube_imap_generic
                 $line = substr($response, $last, $pos - $last);
                 $last = $pos + 2;
 
-                if (!preg_match('/^\* (LIST|LSUB|STATUS) /i', $line, $m)) {
+                if (!preg_match('/^\* (LIST|LSUB|STATUS|MYRIGHTS) /i', $line, $m)) {
                     continue;
                 }
+
                 $cmd  = strtoupper($m[1]);
                 $line = substr($line, strlen($m[0]));
 
@@ -1376,13 +1430,20 @@ class rcube_imap_generic
                                 $this->data['LIST'][$mailbox], $opts));
                     }
                 }
-                // * STATUS <mailbox> (<result>)
-                else if ($cmd == 'STATUS') {
-                    list($mailbox, $status) = $this->tokenizeResponse($line, 2);
-
-                    for ($i=0, $len=count($status); $i<$len; $i += 2) {
-                        list($name, $value) = $this->tokenizeResponse($status, 2);
-                        $folders[$mailbox][$name] = $value;
+                else if ($lstatus) {
+                    // * STATUS <mailbox> (<result>)
+                    if ($cmd == 'STATUS') {
+                        list($mailbox, $status) = $this->tokenizeResponse($line, 2);
+
+                        for ($i=0, $len=count($status); $i<$len; $i += 2) {
+                            list($name, $value) = $this->tokenizeResponse($status, 2);
+                            $folders[$mailbox][$name] = $value;
+                        }
+                    }
+                    // * MYRIGHTS <mailbox> <acl>
+                    else if ($cmd == 'MYRIGHTS') {
+                        list($mailbox, $acl)  = $this->tokenizeResponse($line, 2);
+                        $folders[$mailbox]['MYRIGHTS'] = $acl;
                     }
                 }
             }
@@ -1569,23 +1630,23 @@ class rcube_imap_generic
      *
      * @param string $mailbox    Mailbox name
      * @param string $field      Field to sort by (ARRIVAL, CC, DATE, FROM, SIZE, SUBJECT, TO)
-     * @param string $add        Searching criteria
+     * @param string $criteria   Searching criteria
      * @param bool   $return_uid Enables UID SORT usage
      * @param string $encoding   Character set
      *
      * @return rcube_result_index Response data
      */
-    function sort($mailbox, $field, $add='', $return_uid=false, $encoding = 'US-ASCII')
+    function sort($mailbox, $field = 'ARRIVAL', $criteria = '', $return_uid = false, $encoding = 'US-ASCII')
     {
-        $field = strtoupper($field);
+        $old_sel   = $this->selected;
+        $supported = array('ARRIVAL', 'CC', 'DATE', 'FROM', 'SIZE', 'SUBJECT', 'TO');
+        $field     = strtoupper($field);
+
         if ($field == 'INTERNALDATE') {
             $field = 'ARRIVAL';
         }
 
-        $fields = array('ARRIVAL' => 1,'CC' => 1,'DATE' => 1,
-            'FROM' => 1, 'SIZE' => 1, 'SUBJECT' => 1, 'TO' => 1);
-
-        if (!$fields[$field]) {
+        if (!in_array($field, $supported)) {
             return new rcube_result_index($mailbox);
         }
 
@@ -1593,18 +1654,21 @@ class rcube_imap_generic
             return new rcube_result_index($mailbox);
         }
 
+        // return empty result when folder is empty and we're just after SELECT
+        if ($old_sel != $mailbox && !$this->data['EXISTS']) {
+            return new rcube_result_index($mailbox, '* SORT');
+        }
+
         // RFC 5957: SORT=DISPLAY
         if (($field == 'FROM' || $field == 'TO') && $this->getCapability('SORT=DISPLAY')) {
             $field = 'DISPLAY' . $field;
         }
 
-        // message IDs
-        if (!empty($add)) {
-            $add = $this->compressMessageSet($add);
-        }
+        $encoding = $encoding ? trim($encoding) : 'US-ASCII';
+        $criteria = $criteria ? 'ALL ' . trim($criteria) : 'ALL';
 
         list($code, $response) = $this->execute($return_uid ? 'UID SORT' : 'SORT',
-            array("($field)", $encoding, !empty($add) ? $add : 'ALL'));
+            array("($field)", $encoding, $criteria));
 
         if ($code != self::ERROR_OK) {
             $response = null;
@@ -1634,7 +1698,7 @@ class rcube_imap_generic
 
         // return empty result when folder is empty and we're just after SELECT
         if ($old_sel != $mailbox && !$this->data['EXISTS']) {
-            return new rcube_result_thread($mailbox);
+            return new rcube_result_thread($mailbox, '* THREAD');
         }
 
         $encoding  = $encoding ? trim($encoding) : 'US-ASCII';
@@ -1962,7 +2026,7 @@ class rcube_imap_generic
      *
      * @return bool True on success, False on failure
      */
-    private function modFlag($mailbox, $messages, $flag, $mod = '+')
+    protected function modFlag($mailbox, $messages, $flag, $mod = '+')
     {
         if (!$this->select($mailbox)) {
             return false;
@@ -1977,8 +2041,14 @@ class rcube_imap_generic
             $flag = $this->flags[strtoupper($flag)];
         }
 
-        if (!$flag || !in_array($flag, (array) $this->data['PERMANENTFLAGS'])
-            || !in_array('\\*', (array) $this->data['PERMANENTFLAGS'])
+        if (!$flag) {
+            return false;
+        }
+
+        // if PERMANENTFLAGS is not specified all flags are allowed
+        if (!empty($this->data['PERMANENTFLAGS'])
+            && !in_array($flag, (array) $this->data['PERMANENTFLAGS'])
+            && !in_array('\\*', (array) $this->data['PERMANENTFLAGS'])
         ) {
             return false;
         }
@@ -2499,52 +2569,63 @@ class rcube_imap_generic
             return false;
         }
 
-        switch ($encoding) {
-        case 'base64':
-            $mode = 1;
-            break;
-        case 'quoted-printable':
-            $mode = 2;
-            break;
-        case 'x-uuencode':
-        case 'x-uue':
-        case 'uue':
-        case 'uuencode':
-            $mode = 3;
-            break;
-        default:
-            $mode = 0;
-        }
-
-        // Use BINARY extension when possible (and safe)
-        $binary     = $mode && preg_match('/^[0-9.]+$/', $part) && $this->hasCapability('BINARY');
-        $fetch_mode = $binary ? 'BINARY' : 'BODY';
-        $partial    = $max_bytes ? sprintf('<0.%d>', $max_bytes) : '';
+        $binary    = true;
 
-        // format request
-        $key     = $this->nextTag();
-        $request = $key . ($is_uid ? ' UID' : '') . " FETCH $id ($fetch_mode.PEEK[$part]$partial)";
-        $result  = false;
-        $found   = false;
+        do {
+            if (!$initiated) {
+                switch ($encoding) {
+                case 'base64':
+                    $mode = 1;
+                    break;
+                case 'quoted-printable':
+                    $mode = 2;
+                    break;
+                case 'x-uuencode':
+                case 'x-uue':
+                case 'uue':
+                case 'uuencode':
+                    $mode = 3;
+                    break;
+                default:
+                    $mode = 0;
+                }
 
-        // send request
-        if (!$this->putLine($request)) {
-            $this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
-            return false;
-        }
+                // Use BINARY extension when possible (and safe)
+                $binary     = $binary && $mode && preg_match('/^[0-9.]+$/', $part) && $this->hasCapability('BINARY');
+                $fetch_mode = $binary ? 'BINARY' : 'BODY';
+                $partial    = $max_bytes ? sprintf('<0.%d>', $max_bytes) : '';
+
+                // format request
+                $key       = $this->nextTag();
+                $request   = $key . ($is_uid ? ' UID' : '') . " FETCH $id ($fetch_mode.PEEK[$part]$partial)";
+                $result    = false;
+                $found     = false;
+                $initiated = true;
+
+                // send request
+                if (!$this->putLine($request)) {
+                    $this->setError(self::ERROR_COMMAND, "Unable to send command: $request");
+                    return false;
+                }
 
-        if ($binary) {
-            // WARNING: Use $formatted argument with care, this may break binary data stream
-            $mode = -1;
-        }
+                if ($binary) {
+                    // WARNING: Use $formatted argument with care, this may break binary data stream
+                    $mode = -1;
+                }
+            }
 
-        do {
             $line = trim($this->readLine(1024));
 
             if (!$line) {
                 break;
             }
 
+            // handle UNKNOWN-CTE response - RFC 3516, try again with standard BODY request
+            if ($binary && !$found && preg_match('/^' . $key . ' NO \[UNKNOWN-CTE\]/i', $line)) {
+                $binary = $initiated = false;
+                continue;
+            }
+
             // skip irrelevant untagged responses (we have a result already)
             if ($found || !preg_match('/^\* ([0-9]+) FETCH (.*)$/', $line, $m)) {
                 continue;
@@ -2605,7 +2686,7 @@ class rcube_imap_generic
 
                     // BASE64
                     if ($mode == 1) {
-                        $line = rtrim($line, "\t\r\n\0\x0B");
+                        $line = preg_replace('|[^a-zA-Z0-9+=/]|', '', $line);
                         // create chunks with proper length for base64 decoding
                         $line = $prev.$line;
                         $length = strlen($line);
@@ -2650,7 +2731,7 @@ class rcube_imap_generic
                     }
                 }
             }
-        } while (!$this->startsWith($line, $key, true));
+        } while (!$this->startsWith($line, $key, true) || !$initiated);
 
         if ($result !== false) {
             if ($file) {
@@ -2820,59 +2901,66 @@ class rcube_imap_generic
     /**
      * Returns QUOTA information
      *
+     * @param string $mailbox Mailbox name
+     *
      * @return array Quota information
      */
-    function getQuota()
-    {
-        /*
-         * GETQUOTAROOT "INBOX"
-         * QUOTAROOT INBOX user/rchijiiwa1
-         * QUOTA user/rchijiiwa1 (STORAGE 654 9765)
-         * OK Completed
-         */
-        $result      = false;
-        $quota_lines = array();
-        $key         = $this->nextTag();
-        $command     = $key . ' GETQUOTAROOT INBOX';
-
-        // get line(s) containing quota info
-        if ($this->putLine($command)) {
-            do {
-                $line = rtrim($this->readLine(5000));
-                if (preg_match('/^\* QUOTA /', $line)) {
-                    $quota_lines[] = $line;
-                }
-            } while (!$this->startsWith($line, $key, true, true));
-        }
-        else {
-            $this->setError(self::ERROR_COMMAND, "Unable to send command: $command");
+    function getQuota($mailbox = null)
+    {
+        if ($mailbox === null || $mailbox === '') {
+            $mailbox = 'INBOX';
         }
 
-        // return false if not found, parse if found
+        // a0001 GETQUOTAROOT INBOX
+        // * QUOTAROOT INBOX user/sample
+        // * QUOTA user/sample (STORAGE 654 9765)
+        // a0001 OK Completed
+
+        list($code, $response) = $this->execute('GETQUOTAROOT', array($this->escape($mailbox)));
+
+        $result   = false;
         $min_free = PHP_INT_MAX;
-        foreach ($quota_lines as $key => $quota_line) {
-            $quota_line   = str_replace(array('(', ')'), '', $quota_line);
-            $parts        = explode(' ', $quota_line);
-            $storage_part = array_search('STORAGE', $parts);
+        $all      = array();
 
-            if (!$storage_part) {
-                continue;
-            }
+        if ($code == self::ERROR_OK) {
+            foreach (explode("\n", $response) as $line) {
+                if (preg_match('/^\* QUOTA /', $line)) {
+                    list(, , $quota_root) = $this->tokenizeResponse($line, 3);
+
+                    while ($line) {
+                        list($type, $used, $total) = $this->tokenizeResponse($line, 1);
+                        $type = strtolower($type);
+
+                        if ($type && $total) {
+                            $all[$quota_root][$type]['used']  = intval($used);
+                            $all[$quota_root][$type]['total'] = intval($total);
+                        }
+                    }
+
+                    if (empty($all[$quota_root]['storage'])) {
+                        continue;
+                    }
 
-            $used  = intval($parts[$storage_part+1]);
-            $total = intval($parts[$storage_part+2]);
-            $free  = $total - $used;
+                    $used  = $all[$quota_root]['storage']['used'];
+                    $total = $all[$quota_root]['storage']['total'];
+                    $free  = $total - $used;
 
-            // return lowest available space from all quotas
-            if ($free < $min_free) {
-                $min_free          = $free;
-                $result['used']    = $used;
-                $result['total']   = $total;
-                $result['percent'] = min(100, round(($used/max(1,$total))*100));
-                $result['free']    = 100 - $result['percent'];
+                    // calculate lowest available space from all storage quotas
+                    if ($free < $min_free) {
+                        $min_free          = $free;
+                        $result['used']    = $used;
+                        $result['total']   = $total;
+                        $result['percent'] = min(100, round(($used/max(1,$total))*100));
+                        $result['free']    = 100 - $result['percent'];
+                    }
+                }
             }
         }
 
+        if (!empty($result)) {
+            $result['all'] = $all;
+        }
+
         return $result;
     }
 
@@ -3128,8 +3216,9 @@ class rcube_imap_generic
                 for ($i=0; $i<$size; $i++) {
                     if (isset($mbox) && is_array($data[$i])) {
                         $size_sub = count($data[$i]);
-                        for ($x=0; $x<$size_sub; $x++) {
-                            $result[$mbox][$data[$i][$x]] = $data[$i][++$x];
+                        for ($x=0; $x<$size_sub; $x+=2) {
+                            if ($data[$i][$x+1] !== null)
+                                $result[$mbox][$data[$i][$x]] = $data[$i][$x+1];
                         }
                         unset($data[$i]);
                     }
@@ -3147,7 +3236,8 @@ class rcube_imap_generic
                         }
                     }
                     else if (isset($mbox)) {
-                        $result[$mbox][$data[$i]] = $data[++$i];
+                        if ($data[++$i] !== null)
+                            $result[$mbox][$data[$i-1]] = $data[$i];
                         unset($data[$i]);
                         unset($data[$i-1]);
                     }
@@ -3292,10 +3382,10 @@ class rcube_imap_generic
                         for ($x=0, $len=count($attribs); $x<$len;) {
                             $attr  = $attribs[$x++];
                             $value = $attribs[$x++];
-                            if ($attr == 'value.priv') {
+                            if ($attr == 'value.priv' && $value !== null) {
                                 $result[$mbox]['/private' . $entry] = $value;
                             }
-                            else if ($attr == 'value.shared') {
+                            else if ($attr == 'value.shared' && $value !== null) {
                                 $result[$mbox]['/shared' . $entry] = $value;
                             }
                         }
@@ -3665,7 +3755,7 @@ class rcube_imap_generic
         return $result;
     }
 
-    private function _xor($string, $string2)
+    protected function _xor($string, $string2)
     {
         $result = '';
         $size   = strlen($string);
@@ -3684,7 +3774,7 @@ class rcube_imap_generic
      *
      * @return string Space-separated list of flags
      */
-    private function flagsToStr($flags)
+    protected function flagsToStr($flags)
     {
         foreach ((array)$flags as $idx => $flag) {
             if ($flag = $this->flags[strtoupper($flag)]) {
@@ -3736,7 +3826,7 @@ class rcube_imap_generic
     /**
      * CAPABILITY response parser
      */
-    private function parseCapability($str, $trusted=false)
+    protected function parseCapability($str, $trusted=false)
     {
         $str = preg_replace('/^\* CAPABILITY /i', '', $str);
 
@@ -3813,7 +3903,7 @@ class rcube_imap_generic
      *
      * @since 0.5-stable
      */
-    private function debug($message)
+    protected function debug($message)
     {
         if (($len = strlen($message)) > self::DEBUG_LINE_LENGTH) {
             $diff    = $len - self::DEBUG_LINE_LENGTH;
diff --git a/lib/ext/Roundcube/rcube_imap_search.php b/lib/ext/Roundcube/rcube_imap_search.php
new file mode 100644
index 0000000..365d78f
--- /dev/null
+++ b/lib/ext/Roundcube/rcube_imap_search.php
@@ -0,0 +1,231 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | This file is part of the Roundcube Webmail client                     |
+ |                                                                       |
+ | Copyright (C) 2013, The Roundcube Dev Team                            |
+ | Copyright (C) 2014, Kolab Systems AG                                  |
+ |                                                                       |
+ | Licensed under the GNU General Public License version 3 or            |
+ | any later version with exceptions for skins & plugins.                |
+ | See the README file for a full license statement.                     |
+ |                                                                       |
+ | PURPOSE:                                                              |
+ |   Execute (multi-threaded) searches in multiple IMAP folders          |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube at gmail.com>                        |
+ +-----------------------------------------------------------------------+
+*/
+
+/**
+ * Class to control search jobs on multiple IMAP folders.
+ *
+ * @package    Framework
+ * @subpackage Storage
+ * @author     Thomas Bruederli <roundcube at gmail.com>
+ */
+class rcube_imap_search
+{
+    public $options = array();
+
+    protected $jobs      = array();
+    protected $timelimit = 0;
+    protected $results;
+    protected $conn;
+
+    /**
+     * Default constructor
+     */
+    public function __construct($options, $conn)
+    {
+        $this->options = $options;
+        $this->conn    = $conn;
+    }
+
+    /**
+     * Invoke search request to IMAP server
+     *
+     * @param  array   $folders    List of IMAP folders to search in
+     * @param  string  $str        Search criteria
+     * @param  string  $charset    Search charset
+     * @param  string  $sort_field Header field to sort by
+     * @param  boolean $threading  True if threaded listing is active
+     */
+    public function exec($folders, $str, $charset = null, $sort_field = null, $threading=null)
+    {
+        $start   = floor(microtime(true));
+        $results = new rcube_result_multifolder($folders);
+
+        // start a search job for every folder to search in
+        foreach ($folders as $folder) {
+            // a complete result for this folder already exists
+            $result = $this->results ? $this->results->get_set($folder) : false;
+            if ($result && !$result->incomplete) {
+                $results->add($result);
+            }
+            else {
+                $search = is_array($str) && $str[$folder] ? $str[$folder] : $str;
+                $job = new rcube_imap_search_job($folder, $search, $charset, $sort_field, $threading);
+                $job->worker = $this;
+                $this->jobs[] = $job;
+            }
+        }
+
+        // execute jobs and gather results
+        foreach ($this->jobs as $job) {
+            // only run search if within the configured time limit
+            // TODO: try to estimate the required time based on folder size and previous search performance
+            if (!$this->timelimit || floor(microtime(true)) - $start < $this->timelimit) {
+                $job->run();
+            }
+
+            // add result (may have ->incomplete flag set)
+            $results->add($job->get_result());
+        }
+
+        return $results;
+    }
+
+    /**
+     * Setter for timelimt property
+     */
+    public function set_timelimit($seconds)
+    {
+        $this->timelimit = $seconds;
+    }
+
+    /**
+     * Setter for previous (potentially incomplete) search results
+     */
+    public function set_results($res)
+    {
+        $this->results = $res;
+    }
+
+    /**
+     * Get connection to the IMAP server
+     * (used for single-thread mode)
+     */
+    public function get_imap()
+    {
+        return $this->conn;
+    }
+}
+
+
+/**
+ * Stackable item to run the search on a specific IMAP folder
+ */
+class rcube_imap_search_job /* extends Stackable */
+{
+    private $folder;
+    private $search;
+    private $charset;
+    private $sort_field;
+    private $threading;
+    private $searchset;
+    private $result;
+    private $pagesize = 100;
+
+    public function __construct($folder, $str, $charset = null, $sort_field = null, $threading=false)
+    {
+        $this->folder     = $folder;
+        $this->search     = $str;
+        $this->charset    = $charset;
+        $this->sort_field = $sort_field;
+        $this->threading  = $threading;
+
+        $this->result = new rcube_result_index($folder);
+        $this->result->incomplete = true;
+    }
+
+    public function run()
+    {
+        $this->result = $this->search_index();
+    }
+
+    /**
+     * Copy of rcube_imap::search_index()
+     */
+    protected function search_index()
+    {
+        $criteria = $this->search;
+        $charset  = $this->charset;
+        $imap     = $this->worker->get_imap();
+
+        if (!$imap->connected()) {
+            trigger_error("No IMAP connection for $this->folder", E_USER_WARNING);
+
+            if ($this->threading) {
+                return new rcube_result_thread($this->folder);
+            }
+            else {
+                return new rcube_result_index($this->folder);
+            }
+        }
+
+        if ($this->worker->options['skip_deleted'] && !preg_match('/UNDELETED/', $criteria)) {
+            $criteria = 'UNDELETED '.$criteria;
+        }
+
+        // unset CHARSET if criteria string is ASCII, this way
+        // SEARCH won't be re-sent after "unsupported charset" response
+        if ($charset && $charset != 'US-ASCII' && is_ascii($criteria)) {
+            $charset = 'US-ASCII';
+        }
+
+        if ($this->threading) {
+            $threads = $imap->thread($this->folder, $this->threading, $criteria, true, $charset);
+
+            // Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
+            // but I've seen that Courier doesn't support UTF-8)
+            if ($threads->is_error() && $charset && $charset != 'US-ASCII') {
+                $threads = $imap->thread($this->folder, $this->threading,
+                    rcube_imap::convert_criteria($criteria, $charset), true, 'US-ASCII');
+            }
+
+            return $threads;
+        }
+
+        if ($this->sort_field) {
+            $messages = $imap->sort($this->folder, $this->sort_field, $criteria, true, $charset);
+
+            // Error, try with US-ASCII (RFC5256: SORT/THREAD must support US-ASCII and UTF-8,
+            // but I've seen Courier with disabled UTF-8 support)
+            if ($messages->is_error() && $charset && $charset != 'US-ASCII') {
+                $messages = $imap->sort($this->folder, $this->sort_field,
+                    rcube_imap::convert_criteria($criteria, $charset), true, 'US-ASCII');
+            }
+        }
+
+        if (!$messages || $messages->is_error()) {
+            $messages = $imap->search($this->folder,
+                ($charset && $charset != 'US-ASCII' ? "CHARSET $charset " : '') . $criteria, true);
+
+            // Error, try with US-ASCII (some servers may support only US-ASCII)
+            if ($messages->is_error() && $charset && $charset != 'US-ASCII') {
+                $messages = $imap->search($this->folder,
+                    rcube_imap::convert_criteria($criteria, $charset), true);
+            }
+        }
+
+        return $messages;
+    }
+
+    public function get_search_set()
+    {
+        return array(
+            $this->search,
+            $this->result,
+            $this->charset,
+            $this->sort_field,
+            $this->threading,
+        );
+    }
+
+    public function get_result()
+    {
+        return $this->result;
+    }
+}
diff --git a/lib/ext/Roundcube/rcube_ldap.php b/lib/ext/Roundcube/rcube_ldap.php
index b3872e2..6805c49 100644
--- a/lib/ext/Roundcube/rcube_ldap.php
+++ b/lib/ext/Roundcube/rcube_ldap.php
@@ -65,6 +65,8 @@ class rcube_ldap extends rcube_addressbook
     private $base_dn        = '';
     private $groups_base_dn = '';
     private $group_url;
+    private $group_data;
+    private $group_search_cache;
     private $cache;
 
 
@@ -101,8 +103,8 @@ class rcube_ldap extends rcube_addressbook
             // add group name attrib to the list of attributes to be fetched
             $fetch_attributes[] = $this->prop['groups']['name_attr'];
         }
-        if (is_array($p['group_filters']) && count($p['group_filters'])) {
-            $this->groups = true;
+        if (is_array($p['group_filters'])) {
+            $this->groups = $this->groups || count($p['group_filters']);
 
             foreach ($p['group_filters'] as $k => $group_filter) {
                 // set default name attribute to cn
@@ -232,8 +234,7 @@ class rcube_ldap extends rcube_addressbook
 
         // initialize ldap wrapper object
         $this->ldap = new rcube_ldap_generic($this->prop);
-        $this->ldap->set_cache($this->cache);
-        $this->ldap->set_debug($this->debug);
+        $this->ldap->config_set(array('cache' => $this->cache, 'debug' => $this->debug));
 
         $this->_connect();
     }
@@ -320,8 +321,7 @@ class rcube_ldap extends rcube_addressbook
                             // we need to use a separate LDAP connection
                             if (!empty($this->prop['vlv'])) {
                                 $ldap = new rcube_ldap_generic($this->prop);
-                                $ldap->set_debug($this->debug);
-                                $ldap->set_cache($this->cache);
+                                $ldap->config_set(array('cache' => $this->cache, 'debug' => $this->debug));
                                 if (!$ldap->connect($host)) {
                                     continue;
                                 }
@@ -335,7 +335,7 @@ class rcube_ldap extends rcube_addressbook
                         $res = $ldap->search($search_base_dn, $search_filter, 'sub', $search_attribs);
                         if ($res) {
                             $res->rewind();
-                            $replaces['%dn'] = $res->get_dn();
+                            $replaces['%dn'] = key($res->entries(TRUE));
 
                             // add more replacements from 'search_bind_attrib' config
                             if ($search_bind_attrib) {
@@ -383,7 +383,7 @@ class rcube_ldap extends rcube_addressbook
                         $this->prop['groups'][$k] = strtr($this->prop['groups'][$k], $replaces);
                 }
 
-                if (!empty($this->prop['group_filters'])) {
+                if (is_array($this->prop['group_filters'])) {
                     foreach ($this->prop['group_filters'] as $i => $gf) {
                         if (!empty($gf['base_dn']))
                             $this->prop['group_filters'][$i]['base_dn'] = strtr($gf['base_dn'], $replaces);
@@ -906,7 +906,6 @@ class rcube_ldap extends rcube_addressbook
         return $this->result;
     }
 
-
     /**
      * Get a specific contact record
      *
@@ -948,6 +947,23 @@ class rcube_ldap extends rcube_addressbook
         return $assoc ? $res : $this->result;
     }
 
+    /**
+     * Returns the last error occurred (e.g. when updating/inserting failed)
+     *
+     * @return array Hash array with the following fields: type, message
+     */
+    function get_error()
+    {
+        $err = $this->error;
+
+        // check ldap connection for errors
+        if (!$err && $this->ldap->get_error()) {
+            $err = array(self::ERROR_SEARCH, $this->ldap->get_error());
+        }
+
+        return $err;
+    }
+
 
     /**
      * Check the given data before saving.
@@ -1066,7 +1082,7 @@ class rcube_ldap extends rcube_addressbook
             }
         }
 
-        if (!$this->ldap->add($dn, $newentry)) {
+        if (!$this->ldap->add_entry($dn, $newentry)) {
             $this->set_error(self::ERROR_SAVING, 'errorsaving');
             return false;
         }
@@ -1078,7 +1094,7 @@ class rcube_ldap extends rcube_addressbook
                 'objectClass' => (array) $this->prop['sub_fields'][$xidx],
             );
 
-            $this->ldap->add($xdn, $xf);
+            $this->ldap->add_entry($xdn, $xf);
         }
 
         $dn = self::dn_encode($dn);
@@ -1221,7 +1237,7 @@ class rcube_ldap extends rcube_addressbook
         if (!empty($subdeldata)) {
             foreach ($subdeldata as $fld => $val) {
                 $subdn = $fld.'='.rcube_ldap_generic::quote_string($val).','.$dn;
-                if (!$this->ldap->delete($subdn)) {
+                if (!$this->ldap->delete_entry($subdn)) {
                     return false;
                 }
             }
@@ -1265,7 +1281,7 @@ class rcube_ldap extends rcube_addressbook
                     $fld => $val,
                     'objectClass' => (array) $this->prop['sub_fields'][$fld],
                 );
-                $this->ldap->add($subdn, $xf);
+                $this->ldap->add_entry($subdn, $xf);
             }
         }
 
@@ -1295,7 +1311,7 @@ class rcube_ldap extends rcube_addressbook
             if ($this->sub_filter) {
                 if ($entries = $this->ldap->list_entries($dn, $this->sub_filter)) {
                     foreach ($entries as $entry) {
-                        if (!$this->ldap->delete($entry['dn'])) {
+                        if (!$this->ldap->delete_entry($entry['dn'])) {
                             $this->set_error(self::ERROR_SAVING, 'errorsaving');
                             return false;
                         }
@@ -1304,12 +1320,12 @@ class rcube_ldap extends rcube_addressbook
             }
 
             // Delete the record.
-            if (!$this->ldap->delete($dn)) {
+            if (!$this->ldap->delete_entry($dn)) {
                 $this->set_error(self::ERROR_SAVING, 'errorsaving');
                 return false;
             }
 
-            // remove contact from all groups where he was member
+            // remove contact from all groups where he was a member
             if ($this->groups) {
                 $dn = self::dn_encode($dn);
                 $group_ids = $this->get_record_groups($dn);
@@ -1342,7 +1358,7 @@ class rcube_ldap extends rcube_addressbook
 
         if ($with_groups && $this->groups && ($groups = $this->_fetch_groups()) && count($groups)) {
             foreach ($groups as $group) {
-                $this->ldap->delete($group['dn']);
+                $this->ldap->delete_entry($group['dn']);
             }
 
             if ($this->cache) {
@@ -1410,6 +1426,16 @@ class rcube_ldap extends rcube_addressbook
             $fieldmap['name'] = $this->group_data['name_attr'] ? $this->group_data['name_attr'] : $this->prop['groups']['name_attr'];
         }
 
+        // assign object type from object class mapping
+        if (!empty($this->prop['class_type_map'])) {
+            foreach (array_map('strtolower', (array)$rec['objectclass']) as $objcls) {
+                if (!empty($this->prop['class_type_map'][$objcls])) {
+                    $out['_type'] = $this->prop['class_type_map'][$objcls];
+                    break;
+                }
+            }
+        }
+
         foreach ($fieldmap as $rf => $lf)
         {
             for ($i=0; $i < $rec[$lf]['count']; $i++) {
@@ -1557,7 +1583,7 @@ class rcube_ldap extends rcube_addressbook
         $this->debug = $dbg;
 
         if ($this->ldap) {
-            $this->ldap->set_debug($dbg);
+            $this->ldap->config_set('debug', $dbg);
         }
     }
 
@@ -1594,12 +1620,12 @@ class rcube_ldap extends rcube_addressbook
             return array();
         }
 
-        $group_cache = $this->_fetch_groups();
+        $group_cache = $this->_fetch_groups($search, $mode);
         $groups      = array();
 
         if ($search) {
             foreach ($group_cache as $group) {
-                if ($this->compare_search_value('name', $group['name'], $search, $mode)) {
+                if ($this->compare_search_value('name', $group['name'], mb_strtolower($search), $mode)) {
                     $groups[] = $group;
                 }
             }
@@ -1614,10 +1640,19 @@ class rcube_ldap extends rcube_addressbook
     /**
      * Fetch groups from server
      */
-    private function _fetch_groups($vlv_page = null)
+    private function _fetch_groups($search = null, $mode = 0, $vlv_page = null)
     {
+        // reset group search cache
+        if ($search !== null && $vlv_page === null) {
+            $this->group_search_cache = null;
+        }
+        // return in-memory cache from previous search results
+        else if (is_array($this->group_search_cache) && $vlv_page === null) {
+            return $this->group_search_cache;
+        }
+
         // special case: list groups from 'group_filters' config
-        if ($vlv_page === null && !empty($this->prop['group_filters'])) {
+        if ($vlv_page === null && $search === null && is_array($this->prop['group_filters'])) {
             $groups = array();
             $rcube  = rcube::get_instance();
 
@@ -1634,7 +1669,7 @@ class rcube_ldap extends rcube_addressbook
             return $groups;
         }
 
-        if ($this->cache && $vlv_page === null && ($groups = $this->cache->get('groups')) !== null) {
+        if ($this->cache && $search === null && $vlv_page === null && ($groups = $this->cache->get('groups')) !== null) {
             return $groups;
         }
 
@@ -1656,12 +1691,26 @@ class rcube_ldap extends rcube_addressbook
             }
 
             $ldap = clone $this->ldap;
-            $ldap->set_config($this->prop['groups']);
+            $ldap->config_set($this->prop['groups']);
             $ldap->set_vlv_page($vlv_page+1, $page_size);
         }
 
-        $attrs     = array_unique(array('dn', 'objectClass', $name_attr, $email_attr, $sort_attr));
-        $ldap_data = $ldap->search($base_dn, $filter, $scope, $attrs, $this->prop['groups']);
+        $props = array('sort' => $this->prop['groups']['sort']);
+        $attrs = array_unique(array('dn', 'objectClass', $name_attr, $email_attr, $sort_attr));
+
+        // add search filter
+        if ($search !== null) {
+            // set wildcards
+            $wp = $ws = '';
+            if (!empty($this->prop['fuzzy_search']) && $mode != 1) {
+                $ws = '*';
+                $wp = !$mode ? '*' : '';
+            }
+            $filter = "(&$filter($name_attr=$wp" . rcube_ldap_generic::quote_string($search) . "$ws))";
+            $props['search'] = $wp . $search . $ws;
+        }
+
+        $ldap_data = $ldap->search($base_dn, $filter, $scope, $attrs, $props);
 
         if ($ldap_data === false) {
             return array();
@@ -1698,7 +1747,7 @@ class rcube_ldap extends rcube_addressbook
 
         // call recursively until we have fetched all groups
         while ($this->prop['groups']['vlv'] && $group_count == $page_size) {
-            $next_page   = $this->_fetch_groups(++$vlv_page);
+            $next_page   = $this->_fetch_groups($search, $mode, ++$vlv_page);
             $groups      = array_merge($groups, $next_page);
             $group_count = count($next_page);
         }
@@ -1709,9 +1758,12 @@ class rcube_ldap extends rcube_addressbook
         }
 
         // cache this
-        if ($this->cache) {
+        if ($this->cache && $search === null) {
             $this->cache->set('groups', $groups);
         }
+        else if ($search !== null) {
+            $this->group_search_cache = $groups;
+        }
 
         return $groups;
     }
@@ -1780,7 +1832,7 @@ class rcube_ldap extends rcube_addressbook
             $member_attr => '',
         );
 
-        if (!$this->ldap->add($new_dn, $new_entry)) {
+        if (!$this->ldap->add_entry($new_dn, $new_entry)) {
             $this->set_error(self::ERROR_SAVING, 'errorsaving');
             return false;
         }
@@ -1803,7 +1855,7 @@ class rcube_ldap extends rcube_addressbook
         $group_cache = $this->_fetch_groups();
         $del_dn      = $group_cache[$group_id]['dn'];
 
-        if (!$this->ldap->delete($del_dn)) {
+        if (!$this->ldap->delete_entry($del_dn)) {
             $this->set_error(self::ERROR_SAVING, 'errorsaving');
             return false;
         }
diff --git a/lib/ext/Roundcube/rcube_ldap_generic.php b/lib/ext/Roundcube/rcube_ldap_generic.php
index 1cd0b5a..a76ad6d 100644
--- a/lib/ext/Roundcube/rcube_ldap_generic.php
+++ b/lib/ext/Roundcube/rcube_ldap_generic.php
@@ -5,8 +5,8 @@
  | Roundcube/rcube_ldap_generic.php                                      |
  |                                                                       |
  | This file is part of the Roundcube Webmail client                     |
- | Copyright (C) 2006-2013, The Roundcube Dev Team                       |
- | Copyright (C) 2012-2013, Kolab Systems AG                             |
+ | Copyright (C) 2006-2014, The Roundcube Dev Team                       |
+ | Copyright (C) 2012-2014, Kolab Systems AG                             |
  |                                                                       |
  | Licensed under the GNU General Public License version 3 or            |
  | any later version with exceptions for skins & plugins.                |
@@ -21,464 +21,136 @@
  +-----------------------------------------------------------------------+
 */
 
-
-/*
-  LDAP connection properties
-  --------------------------
-
-  $prop = array(
-      'host'            => '<ldap-server-address>',
-      // or
-      'hosts'           => array('directory.verisign.com'),
-      'port'            => 389,
-      'use_tls'         => true|false,
-      'ldap_version'    => 3,             // using LDAPv3
-      'auth_method'     => '',            // SASL authentication method (for proxy auth), e.g. DIGEST-MD5
-      'attributes'      => array('dn'),   // List of attributes to read from the server
-      'vlv'             => false,         // Enable Virtual List View to more efficiently fetch paginated data (if server supports it)
-      'config_root_dn'  => 'cn=config',   // Root DN to read config (e.g. vlv indexes) from
-      'numsub_filter'   => '(objectClass=organizationalUnit)',   // with VLV, we also use numSubOrdinates to query the total number of records. Set this filter to get all numSubOrdinates attributes for counting
-      'sizelimit'       => '0',           // Enables you to limit the count of entries fetched. Setting this to 0 means no limit.
-      'timelimit'       => '0',           // Sets the number of seconds how long is spend on the search. Setting this to 0 means no limit.
-      'network_timeout' => 10,            // The timeout (in seconds) for connect + bind arrempts. This is only supported in PHP >= 5.3.0 with OpenLDAP 2.x
-      'referrals'       => true|false,    // Sets the LDAP_OPT_REFERRALS option. Mostly used in multi-domain Active Directory setups
-  );
-*/
-
 /**
  * Model class to access an LDAP directories
  *
  * @package    Framework
  * @subpackage LDAP
  */
-class rcube_ldap_generic
+class rcube_ldap_generic extends Net_LDAP3
 {
-    const UPDATE_MOD_ADD = 1;
-    const UPDATE_MOD_DELETE = 2;
-    const UPDATE_MOD_REPLACE = 4;
-    const UPDATE_MOD_FULL = 7;
-
-    public $conn;
-    public $vlv_active = false;
-
     /** private properties */
     protected $cache = null;
-    protected $config = array();
     protected $attributes = array('dn');
-    protected $entries = null;
-    protected $result = null;
-    protected $debug = false;
-    protected $list_page = 1;
-    protected $page_size = 10;
-    protected $vlv_config = null;
-
+    protected $error;
 
-    /**
-    * Object constructor
-    *
-    * @param array $p LDAP connection properties
-    */
-    function __construct($p)
+    function __construct($config = null)
     {
-        $this->config = $p;
+        parent::__construct($config);
 
-        if (is_array($p['attributes']))
-            $this->attributes = $p['attributes'];
-
-        if (!is_array($p['hosts']) && !empty($p['host']))
-            $this->config['hosts'] = array($p['host']);
+        $this->config_set('log_hook', array($this, 'log'));
     }
 
     /**
-     * Activate/deactivate debug mode
-     *
-     * @param boolean $dbg True if LDAP commands should be logged
+     * Establish a connection to the LDAP server
      */
-    public function set_debug($dbg = true)
+    public function connect($host = null)
     {
-        $this->debug = $dbg;
-    }
+        // Net_LDAP3 does not support IDNA yet
+        // also parse_host() here is very Roundcube specific
+        $host = rcube_utils::idn_to_ascii(rcube_utils::parse_host($host));
 
-    /**
-     * Set connection options
-     *
-     * @param mixed $opt Option name as string or hash array with multiple options
-     * @param mixed $val Option value
-     */
-    public function set_config($opt, $val = null)
-    {
-        if (is_array($opt))
-            $this->config = array_merge($this->config, $opt);
-        else
-            $this->config[$opt] = $value;
+        return parent::connect($host);
     }
 
     /**
-     * Enable caching by passing an instance of rcube_cache to be used by this object
+     * Get a specific LDAP entry, identified by its DN
      *
-     * @param object rcube_cache Instance or False to disable caching
-     */
-    public function set_cache($cache_engine)
-    {
-        $this->cache = $cache_engine;
-    }
-
-    /**
-     * Set properties for VLV-based paging
+     * @param string $dn Record identifier
      *
-     * @param  number $page  Page number to list (starting at 1)
-     * @param  number $size  Number of entries to display on one page
+     * @return array Hash array
      */
-    public function set_vlv_page($page, $size = 10)
+    function get_entry($dn)
     {
-        $this->list_page = $page;
-        $this->page_size = $size;
+        return parent::get_entry($dn, $this->attributes);
     }
 
     /**
-    * Establish a connection to the LDAP server
-    */
-    public function connect($host = null)
+     * Prints debug/error info to the log
+     */
+    public function log($level, $msg)
     {
-        if (!function_exists('ldap_connect')) {
-            rcube::raise_error(array('code' => 100, 'type' => 'ldap',
-                'file' => __FILE__, 'line' => __LINE__,
-                'message' => "No ldap support in this installation of PHP"),
-                true);
-            return false;
-        }
-
-        if (is_resource($this->conn) && $this->config['host'] == $host)
-            return true;
+        $msg = implode("\n", $msg);
 
-        if (empty($this->config['ldap_version']))
-            $this->config['ldap_version'] = 3;
-
-        // iterate over hosts if none specified
-        if (!$host) {
-            if (!is_array($this->config['hosts']))
-                $this->config['hosts'] = array($this->config['hosts']);
-
-            foreach ($this->config['hosts'] as $host) {
-                if ($this->connect($host)) {
-                    return true;
-                }
-            }
-
-            return false;
-        }
-
-        // open connection to the given $host
-        $host     = rcube_utils::idn_to_ascii(rcube_utils::parse_host($host));
-        $hostname = $host . ($this->config['port'] ? ':'.$this->config['port'] : '');
-
-        $this->_debug("C: Connect to $hostname [{$this->config['name']}]");
-
-        if ($lc = @ldap_connect($host, $this->config['port'])) {
-            if ($this->config['use_tls'] === true) {
-                if (!ldap_start_tls($lc)) {
-                    return false;
-                }
+        switch ($level) {
+        case LOG_DEBUG:
+        case LOG_INFO:
+        case LOG_NOTICE:
+            if ($this->config['debug']) {
+                rcube::write_log('ldap', $msg);
             }
+            break;
 
-            $this->_debug("S: OK");
-
-            ldap_set_option($lc, LDAP_OPT_PROTOCOL_VERSION, $this->config['ldap_version']);
-            $this->config['host'] = $host;
-            $this->conn = $lc;
-
-            if (!empty($this->config['network_timeout']))
-                ldap_set_option($lc, LDAP_OPT_NETWORK_TIMEOUT, $this->config['network_timeout']);
-
-            if (isset($this->config['referrals']))
-                ldap_set_option($lc, LDAP_OPT_REFERRALS, $this->config['referrals']);
-
-            if (isset($this->config['dereference']))
-                ldap_set_option($lc, LDAP_OPT_DEREF, $this->config['dereference']);
-        }
-        else {
-            $this->_debug("S: NOT OK");
-        }
+        case LOG_EMERGE:
+        case LOG_ALERT:
+        case LOG_CRIT:
+            rcube::raise_error($msg, true, true);
+            break;
 
-        if (!is_resource($this->conn)) {
-            rcube::raise_error(array('code' => 100, 'type' => 'ldap',
-                'file' => __FILE__, 'line' => __LINE__,
-                'message' => "Could not connect to any LDAP server, last tried $hostname"),
-                true);
-            return false;
+        case LOG_ERR:
+        case LOG_WARNING:
+            $this->error = $msg;
+            rcube::raise_error($msg, true, false);
+            break;
         }
-
-        return true;
     }
 
     /**
-     * Bind connection with (SASL-) user and password
+     * Returns the last LDAP error occurred
      *
-     * @param string $authc Authentication user
-     * @param string $pass  Bind password
-     * @param string $authz Autorization user
-     *
-     * @return boolean True on success, False on error
+     * @return mixed Error message string or null if no error occured
      */
-    public function sasl_bind($authc, $pass, $authz=null)
+    function get_error()
     {
-        if (!$this->conn) {
-            return false;
-        }
-
-        if (!function_exists('ldap_sasl_bind')) {
-            rcube::raise_error(array('code' => 100, 'type' => 'ldap',
-                'file' => __FILE__, 'line' => __LINE__,
-                'message' => "Unable to bind: ldap_sasl_bind() not exists"),
-                true);
-            return false;
-        }
-
-        if (!empty($authz)) {
-            $authz = 'u:' . $authz;
-        }
-
-        if (!empty($this->config['auth_method'])) {
-            $method = $this->config['auth_method'];
-        }
-        else {
-            $method = 'DIGEST-MD5';
-        }
-
-        $this->_debug("C: SASL Bind [mech: $method, authc: $authc, authz: $authz, pass: **** [" . strlen($pass) . "]");
-
-        if (ldap_sasl_bind($this->conn, NULL, $pass, $method, NULL, $authc, $authz)) {
-            $this->_debug("S: OK");
-            return true;
-        }
-
-        $this->_debug("S: ".ldap_error($this->conn));
-
-        rcube::raise_error(array(
-            'code' => ldap_errno($this->conn), 'type' => 'ldap',
-            'file' => __FILE__, 'line' => __LINE__,
-            'message' => "SASL Bind failed for authcid=$authc ".ldap_error($this->conn)),
-            true);
-        return false;
+        return $this->error;
     }
 
     /**
-     * Bind connection with DN and password
-     *
-     * @param string $dn   Bind DN
-     * @param string $pass Bind password
-     *
-     * @return boolean True on success, False on error
+     * @deprecated
      */
-    public function bind($dn, $pass)
-    {
-        if (!$this->conn) {
-            return false;
-        }
-
-        $this->_debug("C: Bind $dn, pass: **** [" . strlen($pass) . "]");
-
-        if (@ldap_bind($this->conn, $dn, $pass)) {
-            $this->_debug("S: OK");
-            return true;
-        }
-
-        $this->_debug("S: ".ldap_error($this->conn));
-
-        rcube::raise_error(array(
-            'code' => ldap_errno($this->conn), 'type' => 'ldap',
-            'file' => __FILE__, 'line' => __LINE__,
-            'message' => "Bind failed for dn=$dn: ".ldap_error($this->conn)),
-            true);
-
-        return false;
-    }
-
-    /**
-     * Close connection to LDAP server
-     */
-    public function close()
+    public function set_debug($dbg = true)
     {
-        if ($this->conn) {
-            $this->_debug("C: Close");
-            ldap_unbind($this->conn);
-            $this->conn = null;
-        }
+        $this->config['debug'] = (bool) $dbg;
     }
 
     /**
-     * Return the last result set
-     *
-     * @return object rcube_ldap_result Result object
+     * @deprecated
      */
-    function get_result()
+    public function set_cache($cache_engine)
     {
-        return $this->result;
+        $this->config['cache'] = $cache_engine;
     }
 
     /**
-     * Get a specific LDAP entry, identified by its DN
-     *
-     * @param string $dn Record identifier
-     * @return array     Hash array
+     * @deprecated
      */
-    function get_entry($dn)
-    {
-        $rec = null;
-
-        if ($this->conn && $dn) {
-            $this->_debug("C: Read $dn [(objectclass=*)]");
-
-            if ($ldap_result = @ldap_read($this->conn, $dn, '(objectclass=*)', $this->attributes)) {
-                $this->_debug("S: OK");
-
-                if ($entry = ldap_first_entry($this->conn, $ldap_result)) {
-                    $rec = ldap_get_attributes($this->conn, $entry);
-                }
-            }
-            else {
-                $this->_debug("S: ".ldap_error($this->conn));
-            }
-
-            if (!empty($rec)) {
-                $rec['dn'] = $dn; // Add in the dn for the entry.
-            }
-        }
-
-        return $rec;
-    }
-
-    /**
-     * Execute the LDAP search based on the stored credentials
-     *
-     * @param string $base_dn  The base DN to query
-     * @param string $filter   The LDAP filter for search
-     * @param string $scope    The LDAP scope (list|sub|base)
-     * @param array  $attrs    List of entry attributes to read
-     * @param array  $prop     Hash array with query configuration properties:
-     *   - sort: array of sort attributes (has to be in sync with the VLV index)
-     *   - search: search string used for VLV controls
-     * @param boolean $count_only Set to true if only entry count is requested
-     *
-     * @return mixed  rcube_ldap_result object or number of entries (if count_only=true) or false on error
-     */
-    public function search($base_dn, $filter = '', $scope = 'sub', $attrs = array('dn'), $prop = array(), $count_only = false)
+    public static function scope2func($scope, &$ns_function = null)
     {
-        if (!$this->conn) {
-            return false;
-        }
-
-        if (empty($filter)) {
-            $filter = '(objectclass=*)';
-        }
-
-        $this->_debug("C: Search $base_dn for $filter");
-
-        $function = self::scope2func($scope, $ns_function);
-
-        // find available VLV index for this query
-        if (!$count_only && ($vlv_sort = $this->_find_vlv($base_dn, $filter, $scope, $prop['sort']))) {
-            // when using VLV, we get the total count by...
-            // ...either reading numSubOrdinates attribute
-            if (($sub_filter = $this->config['numsub_filter']) &&
-                ($result_count = @$ns_function($this->conn, $base_dn, $sub_filter, array('numSubOrdinates'), 0, 0, 0))
-            ) {
-                $counts = ldap_get_entries($this->conn, $result_count);
-                for ($vlv_count = $j = 0; $j < $counts['count']; $j++)
-                    $vlv_count += $counts[$j]['numsubordinates'][0];
-                $this->_debug("D: total numsubordinates = " . $vlv_count);
-            }
-            // ...or by fetching all records dn and count them
-            else if (!function_exists('ldap_parse_virtuallist_control')) {
-                $vlv_count = $this->search($base_dn, $filter, $scope, array('dn'), $prop, true);
-            }
-
-            $this->vlv_active = $this->_vlv_set_controls($vlv_sort, $this->list_page, $this->page_size, $prop['search']);
-        }
-        else {
-            $this->vlv_active = false;
-        }
-
-        // only fetch dn for count (should keep the payload low)
-        if ($ldap_result = @$function($this->conn, $base_dn, $filter,
-            $attrs, 0, (int)$this->config['sizelimit'], (int)$this->config['timelimit'])
-        ) {
-            // when running on a patched PHP we can use the extended functions
-            // to retrieve the total count from the LDAP search result
-            if ($this->vlv_active && function_exists('ldap_parse_virtuallist_control')) {
-                if (ldap_parse_result($this->conn, $ldap_result, $errcode, $matcheddn, $errmsg, $referrals, $serverctrls)) {
-                    ldap_parse_virtuallist_control($this->conn, $serverctrls, $last_offset, $vlv_count, $vresult);
-                    $this->_debug("S: VLV result: last_offset=$last_offset; content_count=$vlv_count");
-                }
-                else {
-                    $this->_debug("S: ".($errmsg ? $errmsg : ldap_error($this->conn)));
-                }
-            }
-            else if ($this->debug) {
-                $this->_debug("S: ".ldap_count_entries($this->conn, $ldap_result)." record(s) found");
-            }
-
-            $this->result = new rcube_ldap_result($this->conn, $ldap_result, $base_dn, $filter, $vlv_count);
-
-            return $count_only ? $this->result->count() : $this->result;
-        }
-        else {
-            $this->_debug("S: ".ldap_error($this->conn));
-        }
-
-        return false;
+        return self::scope_to_function($scope, $ns_function);
     }
 
     /**
-     * Modify an LDAP entry on the server
-     *
-     * @param string $dn      Entry DN
-     * @param array  $params  Hash array of entry attributes
-     * @param int    $mode    Update mode (UPDATE_MOD_ADD | UPDATE_MOD_DELETE | UPDATE_MOD_REPLACE)
+     * @deprecated
      */
-    public function modify($dn, $parms, $mode = 255)
+    public function set_config($opt, $val = null)
     {
-        // TODO: implement this
-
-        return false;
+        $this->config_set($opt, $val);
     }
 
     /**
-     * Wrapper for ldap_add()
-     *
-     * @see ldap_add()
+     * @deprecated
      */
     public function add($dn, $entry)
     {
-        $this->_debug("C: Add $dn: ".print_r($entry, true));
-
-        $res = ldap_add($this->conn, $dn, $entry);
-        if ($res === false) {
-            $this->_debug("S: ".ldap_error($this->conn));
-            return false;
-        }
-
-        $this->_debug("S: OK");
-        return true;
+        return $this->add_entry($dn, $entry);
     }
 
     /**
-     * Wrapper for ldap_delete()
-     *
-     * @see ldap_delete()
+     * @deprecated
      */
     public function delete($dn)
     {
-        $this->_debug("C: Delete $dn");
-
-        $res = ldap_delete($this->conn, $dn);
-        if ($res === false) {
-            $this->_debug("S: ".ldap_error($this->conn));
-            return false;
-        }
-
-        $this->_debug("S: OK");
-        return true;
+        return $this->delete_entry($dn);
     }
 
     /**
@@ -491,7 +163,7 @@ class rcube_ldap_generic
         $this->_debug("C: Replace $dn: ".print_r($entry, true));
 
         if (!ldap_mod_replace($this->conn, $dn, $entry)) {
-            $this->_debug("S: ".ldap_error($this->conn));
+            $this->_error("ldap_mod_replace() failed with " . ldap_error($this->conn));
             return false;
         }
 
@@ -509,7 +181,7 @@ class rcube_ldap_generic
         $this->_debug("C: Add $dn: ".print_r($entry, true));
 
         if (!ldap_mod_add($this->conn, $dn, $entry)) {
-            $this->_debug("S: ".ldap_error($this->conn));
+            $this->_error("ldap_mod_add() failed with " . ldap_error($this->conn));
             return false;
         }
 
@@ -527,7 +199,7 @@ class rcube_ldap_generic
         $this->_debug("C: Delete $dn: ".print_r($entry, true));
 
         if (!ldap_mod_del($this->conn, $dn, $entry)) {
-            $this->_debug("S: ".ldap_error($this->conn));
+            $this->_error("ldap_mod_del() failed with " . ldap_error($this->conn));
             return false;
         }
 
@@ -545,7 +217,7 @@ class rcube_ldap_generic
         $this->_debug("C: Rename $dn to $newrdn");
 
         if (!ldap_rename($this->conn, $dn, $newrdn, $newparent, $deleteoldrdn)) {
-            $this->_debug("S: ".ldap_error($this->conn));
+            $this->_error("ldap_rename() failed with " . ldap_error($this->conn));
             return false;
         }
 
@@ -568,7 +240,7 @@ class rcube_ldap_generic
             $list = ldap_get_entries($this->conn, $result);
 
             if ($list === false) {
-                $this->_debug("S: ".ldap_error($this->conn));
+                $this->_error("ldap_get_entries() failed with " . ldap_error($this->conn));
                 return array();
             }
 
@@ -578,7 +250,7 @@ class rcube_ldap_generic
             $this->_debug("S: $count record(s)");
         }
         else {
-            $this->_debug("S: ".ldap_error($this->conn));
+            $this->_error("ldap_list() failed with " . ldap_error($this->conn));
         }
 
         return $list;
@@ -595,12 +267,9 @@ class rcube_ldap_generic
         $this->_debug("C: Read $dn [{$filter}]");
 
         if ($this->conn && $dn) {
-            if (!$attributes)
-                $attributes = $this->attributes;
-
             $result = @ldap_read($this->conn, $dn, $filter, $attributes, 0, (int)$this->config['sizelimit'], (int)$this->config['timelimit']);
             if ($result === false) {
-                $this->_debug("S: ".ldap_error($this->conn));
+                $this->_error("ldap_read() failed with " . ldap_error($this->conn));
                 return false;
             }
 
@@ -612,97 +281,11 @@ class rcube_ldap_generic
     }
 
     /**
-     * Choose the right PHP function according to scope property
-     *
-     * @param string $scope         The LDAP scope (sub|base|list)
-     * @param string $ns_function   Function to be used for numSubOrdinates queries
-     * @return string  PHP function to be used to query directory
-     */
-    public static function scope2func($scope, &$ns_function = null)
-    {
-        switch ($scope) {
-          case 'sub':
-            $function = $ns_function  = 'ldap_search';
-            break;
-          case 'base':
-            $function = $ns_function = 'ldap_read';
-            break;
-          default:
-            $function = 'ldap_list';
-            $ns_function = 'ldap_read';
-            break;
-        }
-
-        return $function;
-    }
-
-    /**
-     * Convert the given scope integer value to a string representation
-     */
-    public static function scopeint2str($scope)
-    {
-        switch ($scope) {
-            case 2:  return 'sub';
-            case 1:  return 'one';
-            case 0:  return 'base';
-            default: $this->_debug("Scope $scope is not a valid scope integer");
-        }
-
-        return '';
-    }
-
-    /**
-     * Escapes the given value according to RFC 2254 so that it can be safely used in LDAP filters.
-     *
-     * @param string $val Value to quote
-     * @return string The escaped value
-     */
-    public static function escape_value($val)
-    {
-        return strtr($str, array('*'=>'\2a', '('=>'\28', ')'=>'\29',
-            '\\'=>'\5c', '/'=>'\2f'));
-    }
-
-    /**
-     * Escapes a DN value according to RFC 2253
-     *
-     * @param string $dn DN value o quote
-     * @return string The escaped value
-     */
-    public static function escape_dn($dn)
-    {
-        return strtr($str, array(','=>'\2c', '='=>'\3d', '+'=>'\2b',
-            '<'=>'\3c', '>'=>'\3e', ';'=>'\3b', '\\'=>'\5c',
-            '"'=>'\22', '#'=>'\23'));
-    }
-
-    /**
-     * Normalize a LDAP result by converting entry attributes arrays into single values
-     *
-     * @param array $result LDAP result set fetched with ldap_get_entries()
-     * @return array        Hash array with normalized entries, indexed by their DNs
-     */
-    public static function normalize_result($result)
-    {
-        if (!is_array($result)) {
-            return array();
-        }
-
-        $entries  = array();
-        for ($i = 0; $i < $result['count']; $i++) {
-            $key = $result[$i]['dn'] ? $result[$i]['dn'] : $i;
-            $entries[$key] = self::normalize_entry($result[$i]);
-        }
-
-        return $entries;
-    }
-
-    /**
      * Turn an LDAP entry into a regular PHP array with attributes as keys.
      *
      * @param array $entry Attributes array as retrieved from ldap_get_attributes() or ldap_get_entries()
      *
-     * @return array       Hash array with attributes as keys
+     * @return array Hash array with attributes as keys
      */
     public static function normalize_entry($entry)
     {
@@ -733,328 +316,7 @@ class rcube_ldap_generic
 
         return $rec;
     }
-
-    /**
-     * Set server controls for Virtual List View (paginated listing)
-     */
-    private function _vlv_set_controls($sort, $list_page, $page_size, $search = null)
-    {
-        $sort_ctrl = array('oid' => "1.2.840.113556.1.4.473",  'value' => self::_sort_ber_encode((array)$sort));
-        $vlv_ctrl  = array('oid' => "2.16.840.1.113730.3.4.9", 'value' => self::_vlv_ber_encode(($offset = ($list_page-1) * $page_size + 1), $page_size, $search), 'iscritical' => true);
-
-        $this->_debug("C: Set controls sort=" . join(' ', unpack('H'.(strlen($sort_ctrl['value'])*2), $sort_ctrl['value'])) . " ($sort[0]);"
-            . " vlv=" . join(' ', (unpack('H'.(strlen($vlv_ctrl['value'])*2), $vlv_ctrl['value']))) . " ($offset/$page_size; $search)");
-
-        if (!ldap_set_option($this->conn, LDAP_OPT_SERVER_CONTROLS, array($sort_ctrl, $vlv_ctrl))) {
-            $this->_debug("S: ".ldap_error($this->conn));
-            $this->set_error(self::ERROR_SEARCH, 'vlvnotsupported');
-            return false;
-        }
-
-        return true;
-    }
-
-    /**
-     * Returns unified attribute name (resolving aliases)
-     */
-    private static function _attr_name($namev)
-    {
-        // list of known attribute aliases
-        static $aliases = array(
-            'gn' => 'givenname',
-            'rfc822mailbox' => 'email',
-            'userid' => 'uid',
-            'emailaddress' => 'email',
-            'pkcs9email' => 'email',
-        );
-
-        list($name, $limit) = explode(':', $namev, 2);
-        $suffix = $limit ? ':'.$limit : '';
-
-        return (isset($aliases[$name]) ? $aliases[$name] : $name) . $suffix;
-    }
-
-    /**
-     * Quotes attribute value string
-     *
-     * @param string $str Attribute value
-     * @param bool   $dn  True if the attribute is a DN
-     *
-     * @return string Quoted string
-     */
-    public static function quote_string($str, $dn=false)
-    {
-        // take firt entry if array given
-        if (is_array($str))
-            $str = reset($str);
-
-        if ($dn)
-            $replace = array(','=>'\2c', '='=>'\3d', '+'=>'\2b', '<'=>'\3c',
-                '>'=>'\3e', ';'=>'\3b', '\\'=>'\5c', '"'=>'\22', '#'=>'\23');
-        else
-            $replace = array('*'=>'\2a', '('=>'\28', ')'=>'\29', '\\'=>'\5c',
-                '/'=>'\2f');
-
-        return strtr($str, $replace);
-    }
-
-    /**
-     * Prints debug info to the log
-     */
-    private function _debug($str)
-    {
-        if ($this->debug && class_exists('rcube')) {
-            rcube::write_log('ldap', $str);
-        }
-    }
-
-
-    /*****************  Virtual List View (VLV) related utility functions  **************** */
-
-    /**
-     * Return the search string value to be used in VLV controls
-     */
-    private function _vlv_search($sort, $search)
-    {
-        foreach ($search as $attr => $value) {
-            if (!in_array(strtolower($attr), $sort)) {
-                $this->_debug("d: Cannot use VLV search using attribute not indexed: $attr (not in " . var_export($sort, true) . ")");
-                return null;
-            } else {
-                return $value;
-            }
-        }
-    }
-
-    /**
-     * Find a VLV index matching the given query attributes
-     *
-     * @return string Sort attribute or False if no match
-     */
-    private function _find_vlv($base_dn, $filter, $scope, $sort_attrs = null)
-    {
-        if (!$this->config['vlv'] || $scope == 'base') {
-            return false;
-        }
-
-        // get vlv config
-        $vlv_config = $this->_read_vlv_config();
-
-        if ($vlv = $vlv_config[$base_dn]) {
-            $this->_debug("D: Found a VLV for $base_dn");
-
-            if ($vlv['filter'] == strtolower($filter) || stripos($filter, '(&'.$vlv['filter'].'(') === 0) {
-                $this->_debug("D: Filter matches");
-                if ($vlv['scope'] == $scope) {
-                    // Not passing any sort attributes means you don't care
-                    if (empty($sort_attrs) || in_array($sort_attrs, $vlv['sort'])) {
-                        return $vlv['sort'][0];
-                    }
-                }
-                else {
-                    $this->_debug("D: Scope does not match");
-                }
-            }
-            else {
-                $this->_debug("D: Filter does not match");
-            }
-        }
-        else {
-            $this->_debug("D: No VLV for $base_dn");
-        }
-
-        return false;
-    }
-
-    /**
-     * Return VLV indexes and searches including necessary configuration
-     * details.
-     */
-    private function _read_vlv_config()
-    {
-        if (empty($this->config['vlv']) || empty($this->config['config_root_dn'])) {
-            return array();
-        }
-        // return hard-coded VLV config
-        else if (is_array($this->config['vlv'])) {
-            return $this->config['vlv'];
-        }
-
-        // return cached result
-        if (is_array($this->vlv_config)) {
-            return $this->vlv_config;
-        }
-
-        if ($this->cache && ($cached_config = $this->cache->get('vlvconfig'))) {
-            $this->vlv_config = $cached_config;
-            return $this->vlv_config;
-        }
-
-        $this->vlv_config = array();
-
-        $ldap_result = ldap_search($this->conn, $this->config['config_root_dn'], '(objectclass=vlvsearch)', array('*'), 0, 0, 0);
-        $vlv_searches = new rcube_ldap_result($this->conn, $ldap_result, $this->config['config_root_dn'], '(objectclass=vlvsearch)');
-
-        if ($vlv_searches->count() < 1) {
-            $this->_debug("D: Empty result from search for '(objectclass=vlvsearch)' on '$config_root_dn'");
-            return array();
-        }
-
-        foreach ($vlv_searches->entries(true) as $vlv_search_dn => $vlv_search_attrs) {
-            // Multiple indexes may exist
-            $ldap_result = ldap_search($this->conn, $vlv_search_dn, '(objectclass=vlvindex)', array('*'), 0, 0, 0);
-            $vlv_indexes = new rcube_ldap_result($this->conn, $ldap_result, $vlv_search_dn, '(objectclass=vlvindex)');
-
-            // Reset this one for each VLV search.
-            $_vlv_sort = array();
-            foreach ($vlv_indexes->entries(true) as $vlv_index_dn => $vlv_index_attrs) {
-                $_vlv_sort[] = explode(' ', $vlv_index_attrs['vlvsort']);
-            }
-
-            $this->vlv_config[$vlv_search_attrs['vlvbase']] = array(
-                'scope'  => self::scopeint2str($vlv_search_attrs['vlvscope']),
-                'filter' => strtolower($vlv_search_attrs['vlvfilter']),
-                'sort'   => $_vlv_sort,
-            );
-        }
-
-        // cache this
-        if ($this->cache)
-            $this->cache->set('vlvconfig', $this->vlv_config);
-
-        $this->_debug("D: Refreshed VLV config: " . var_export($this->vlv_config, true));
-
-        return $this->vlv_config;
-    }
-
-    /**
-     * Generate BER encoded string for Virtual List View option
-     *
-     * @param integer List offset (first record)
-     * @param integer Records per page
-     *
-     * @return string BER encoded option value
-     */
-    private static function _vlv_ber_encode($offset, $rpp, $search = '')
-    {
-        /*
-            this string is ber-encoded, php will prefix this value with:
-            04 (octet string) and 10 (length of 16 bytes)
-            the code behind this string is broken down as follows:
-            30 = ber sequence with a length of 0e (14) bytes following
-            02 = type integer (in two's complement form) with 2 bytes following (beforeCount): 01 00 (ie 0)
-            02 = type integer (in two's complement form) with 2 bytes following (afterCount):  01 18 (ie 25-1=24)
-            a0 = type context-specific/constructed with a length of 06 (6) bytes following
-            02 = type integer with 2 bytes following (offset): 01 01 (ie 1)
-            02 = type integer with 2 bytes following (contentCount):  01 00
-
-            with a search string present:
-            81 = type context-specific/constructed with a length of 04 (4) bytes following (the length will change here)
-            81 indicates a user string is present where as a a0 indicates just a offset search
-            81 = type context-specific/constructed with a length of 06 (6) bytes following
-
-            The following info was taken from the ISO/IEC 8825-1:2003 x.690 standard re: the
-            encoding of integer values (note: these values are in
-            two-complement form so since offset will never be negative bit 8 of the
-            leftmost octet should never by set to 1):
-            8.3.2: If the contents octets of an integer value encoding consist
-            of more than one octet, then the bits of the first octet (rightmost)
-            and bit 8 of the second (to the left of first octet) octet:
-                a) shall not all be ones; and
-                b) shall not all be zero
-        */
-
-        if ($search) {
-            $search = preg_replace('/[^-[:alpha:] ,.()0-9]+/', '', $search);
-            $ber_val = self::_string2hex($search);
-            $str = self::_ber_addseq($ber_val, '81');
-        }
-        else {
-            // construct the string from right to left
-            $str = "020100"; # contentCount
-
-            $ber_val = self::_ber_encode_int($offset);  // returns encoded integer value in hex format
-
-            // calculate octet length of $ber_val
-            $str = self::_ber_addseq($ber_val, '02') . $str;
-
-            // now compute length over $str
-            $str = self::_ber_addseq($str, 'a0');
-        }
-
-        // now tack on records per page
-        $str = "020100" . self::_ber_addseq(self::_ber_encode_int($rpp-1), '02') . $str;
-
-        // now tack on sequence identifier and length
-        $str = self::_ber_addseq($str, '30');
-
-        return pack('H'.strlen($str), $str);
-    }
-
-    /**
-     * create ber encoding for sort control
-     *
-     * @param array List of cols to sort by
-     * @return string BER encoded option value
-     */
-    private static function _sort_ber_encode($sortcols)
-    {
-        $str = '';
-        foreach (array_reverse((array)$sortcols) as $col) {
-            $ber_val = self::_string2hex($col);
-
-            // 30 = ber sequence with a length of octet value
-            // 04 = octet string with a length of the ascii value
-            $oct = self::_ber_addseq($ber_val, '04');
-            $str = self::_ber_addseq($oct, '30') . $str;
-        }
-
-        // now tack on sequence identifier and length
-        $str = self::_ber_addseq($str, '30');
-
-        return pack('H'.strlen($str), $str);
-    }
-
-    /**
-     * Add BER sequence with correct length and the given identifier
-     */
-    private static function _ber_addseq($str, $identifier)
-    {
-        $len = dechex(strlen($str)/2);
-        if (strlen($len) % 2 != 0)
-            $len = '0'.$len;
-
-        return $identifier . $len . $str;
-    }
-
-    /**
-     * Returns BER encoded integer value in hex format
-     */
-    private static function _ber_encode_int($offset)
-    {
-        $val = dechex($offset);
-        $prefix = '';
-
-        // check if bit 8 of high byte is 1
-        if (preg_match('/^[89abcdef]/', $val))
-            $prefix = '00';
-
-        if (strlen($val)%2 != 0)
-            $prefix .= '0';
-
-        return $prefix . $val;
-    }
-
-    /**
-     * Returns ascii string encoded in hex
-     */
-    private static function _string2hex($str)
-    {
-        $hex = '';
-        for ($i=0; $i < strlen($str); $i++) {
-            $hex .= dechex(ord($str[$i]));
-        }
-        return $hex;
-    }
-
 }
+
+// for backward compat.
+class rcube_ldap_result extends Net_LDAP3_Result {}
diff --git a/lib/ext/Roundcube/rcube_message.php b/lib/ext/Roundcube/rcube_message.php
index f24ec3e..169d00c 100644
--- a/lib/ext/Roundcube/rcube_message.php
+++ b/lib/ext/Roundcube/rcube_message.php
@@ -3,7 +3,7 @@
 /*
  +-----------------------------------------------------------------------+
  | This file is part of the Roundcube Webmail client                     |
- | Copyright (C) 2008-2010, The Roundcube Dev Team                       |
+ | Copyright (C) 2008-2014, The Roundcube Dev Team                       |
  |                                                                       |
  | Licensed under the GNU General Public License version 3 or            |
  | any later version with exceptions for skins & plugins.                |
@@ -61,6 +61,8 @@ class rcube_message
     public $sender = null;
     public $is_safe = false;
 
+    const BODY_MAX_SIZE = 1048576; // 1MB
+
 
     /**
      * __construct
@@ -74,6 +76,11 @@ class rcube_message
      */
     function __construct($uid, $folder = null)
     {
+        // decode combined UID-folder identifier
+        if (preg_match('/^\d+-.+/', $uid)) {
+            list($uid, $folder) = explode('-', $uid, 2);
+        }
+
         $this->uid  = $uid;
         $this->app  = rcube::get_instance();
         $this->storage = $this->app->get_storage();
@@ -171,6 +178,7 @@ class rcube_message
      * @param boolean  $formatted         Enables formatting of text/* parts bodies
      *
      * @return string Part content
+     * @deprecated
      */
     public function get_part_content($mime_id, $fp = null, $skip_charset_conv = false, $max_bytes = 0, $formatted = true)
     {
@@ -193,6 +201,127 @@ class rcube_message
 
 
     /**
+     * Get content of a specific part of this message
+     *
+     * @param string  $mime_id   Part ID
+     * @param boolean $formatted Enables formatting of text/* parts bodies
+     * @param int     $max_bytes Only return/read this number of bytes
+     * @param mixed   $mode      NULL to return a string, -1 to print body
+     *                           or file pointer to save the body into
+     *
+     * @return string|bool Part content or operation status
+     */
+    public function get_part_body($mime_id, $formatted = false, $max_bytes = 0, $mode = null)
+    {
+        if (!($part = $this->mime_parts[$mime_id])) {
+            return;
+        }
+
+        // only text parts can be formatted
+        $formatted = $formatted && $part->ctype_primary == 'text';
+
+        // part body not fetched yet... save in memory if it's small enough
+        if ($part->body === null && is_numeric($mime_id) && $part->size < self::BODY_MAX_SIZE) {
+            $this->storage->set_folder($this->folder);
+            // Warning: body here should be always unformatted
+            $part->body = $this->storage->get_message_part($this->uid, $mime_id, $part,
+                null, null, true, 0, false);
+        }
+
+        // body stored in message structure (winmail/inline-uuencode)
+        if ($part->body !== null || $part->encoding == 'stream') {
+            $body = $part->body;
+
+            if ($formatted && $body) {
+                $body = self::format_part_body($body, $part, $this->headers->charset);
+            }
+
+            if ($max_bytes && strlen($body) > $max_bytes) {
+                $body = substr($body, 0, $max_bytes);
+            }
+
+            if (is_resource($mode)) {
+                if ($body !== false) {
+                    fwrite($mode, $body);
+                    rewind($mode);
+                }
+
+                return $body !== false;
+            }
+
+            if ($mode === -1) {
+                if ($body !== false) {
+                    print($body);
+                }
+
+                return $body !== false;
+            }
+
+            return $body;
+        }
+
+        // get the body from IMAP
+        $this->storage->set_folder($this->folder);
+
+        $body = $this->storage->get_message_part($this->uid, $mime_id, $part,
+            $mode === -1, is_resource($mode) ? $mode : null,
+            !($mode && $formatted), $max_bytes, $mode && $formatted);
+
+        if (is_resource($mode)) {
+            rewind($mode);
+            return $body !== false;
+        }
+
+        if (!$mode && $body && $formatted) {
+            $body = self::format_part_body($body, $part, $this->headers->charset);
+        }
+
+        return $body;
+    }
+
+
+    /**
+     * Format text message part for display
+     *
+     * @param string             $body            Part body
+     * @param rcube_message_part $part            Part object
+     * @param string             $default_charset Fallback charset if part charset is not specified
+     *
+     * @return string Formatted body
+     */
+    public static function format_part_body($body, $part, $default_charset = null)
+    {
+        // remove useless characters
+        $body = preg_replace('/[\t\r\0\x0B]+\n/', "\n", $body);
+
+        // remove NULL characters if any (#1486189)
+        if (strpos($body, "\x00") !== false) {
+            $body = str_replace("\x00", '', $body);
+        }
+
+        // detect charset...
+        if (!$part->charset || strtoupper($part->charset) == 'US-ASCII') {
+            // try to extract charset information from HTML meta tag (#1488125)
+            if ($part->ctype_secondary == 'html' && preg_match('/<meta[^>]+charset=([a-z0-9-_]+)/i', $body, $m)) {
+                $part->charset = strtoupper($m[1]);
+            }
+            else if ($default_charset) {
+                $part->charset = $default_charset;
+            }
+            else {
+                $rcube         = rcube::get_instance();
+                $part->charset = $rcube->config->get('default_charset', RCUBE_CHARSET);
+            }
+        }
+
+        // ..convert charset encoding
+        $body = rcube_charset::convert($body, $part->charset);
+
+        return $body;
+    }
+
+
+    /**
      * Determine if the message contains a HTML part. This must to be
      * a real part not an attachment (or its part)
      *
@@ -288,7 +417,7 @@ class rcube_message
         // check all message parts
         foreach ($this->mime_parts as $pid => $part) {
             if ($part->mimetype == 'text/html') {
-                return $this->get_part_content($pid);
+                return $this->get_part_body($pid, true);
             }
         }
     }
@@ -309,10 +438,10 @@ class rcube_message
         // check all message parts
         foreach ($this->mime_parts as $mime_id => $part) {
             if ($part->mimetype == 'text/plain') {
-                return $this->get_part_content($mime_id);
+                return $this->get_part_body($mime_id, true);
             }
             else if ($part->mimetype == 'text/html') {
-                $out = $this->get_part_content($mime_id);
+                $out = $this->get_part_body($mime_id, true);
 
                 // create instance of html2text class
                 $txt = new rcube_html2text($out);
@@ -366,7 +495,7 @@ class rcube_message
 
             // parse headers from message/rfc822 part
             if (!isset($structure->headers['subject']) && !isset($structure->headers['from'])) {
-                list($headers, ) = explode("\r\n\r\n", $this->get_part_content($structure->mime_id, null, true, 32768));
+                list($headers, ) = explode("\r\n\r\n", $this->get_part_body($structure->mime_id, false, 32768));
                 $structure->headers = rcube_mime::parse_headers($headers);
             }
         }
@@ -720,20 +849,18 @@ class rcube_message
      */
     function tnef_decode(&$part)
     {
-        // @TODO: attachment may be huge, hadle it via file
-        if (!isset($part->body)) {
-            $this->storage->set_folder($this->folder);
-            $part->body = $this->storage->get_message_part($this->uid, $part->mime_id, $part);
-        }
+        // @TODO: attachment may be huge, handle body via file
+        $body     = $this->get_part_body($part->mime_id);
+        $tnef     = new rcube_tnef_decoder;
+        $tnef_arr = $tnef->decompress($body);
+        $parts    = array();
 
-        $parts = array();
-        $tnef = new tnef_decoder;
-        $tnef_arr = $tnef->decompress($part->body);
+        unset($body);
 
         foreach ($tnef_arr as $pid => $winatt) {
             $tpart = new rcube_message_part;
 
-            $tpart->filename        = trim($winatt['name']);
+            $tpart->filename        = $this->fix_attachment_name(trim($winatt['name']), $part);
             $tpart->encoding        = 'stream';
             $tpart->ctype_primary   = trim(strtolower($winatt['type']));
             $tpart->ctype_secondary = trim(strtolower($winatt['subtype']));
@@ -758,55 +885,109 @@ class rcube_message
      */
     function uu_decode(&$part)
     {
-        // @TODO: messages may be huge, hadle body via file
-        if (!isset($part->body)) {
-            $this->storage->set_folder($this->folder);
-            $part->body = $this->storage->get_message_part($this->uid, $part->mime_id, $part);
-        }
+        // @TODO: messages may be huge, handle body via file
+        $part->body = $this->get_part_body($part->mime_id);
+        $parts      = array();
+        $pid        = 0;
 
-        $parts = array();
         // FIXME: line length is max.65?
-        $uu_regexp = '/begin [0-7]{3,4} ([^\n]+)\n/s';
+        $uu_regexp_begin = '/begin [0-7]{3,4} ([^\r\n]+)\r?\n/s';
+        $uu_regexp_end   = '/`\r?\nend((\r?\n)|($))/s';
+
+        while (preg_match($uu_regexp_begin, $part->body, $matches, PREG_OFFSET_CAPTURE)) {
+            $startpos = $matches[0][1];
+
+            if (!preg_match($uu_regexp_end, $part->body, $m, PREG_OFFSET_CAPTURE, $startpos)) {
+                break;
+            }
+
+            $endpos    = $m[0][1];
+            $begin_len = strlen($matches[0][0]);
+            $end_len   = strlen($m[0][0]);
+
+            // extract attachment body
+            $filebody = substr($part->body, $startpos + $begin_len, $endpos - $startpos - $begin_len - 1);
+            $filebody = str_replace("\r\n", "\n", $filebody);
 
-        if (preg_match_all($uu_regexp, $part->body, $matches, PREG_SET_ORDER)) {
-            // update message content-type
-            $part->ctype_primary   = 'multipart';
-            $part->ctype_secondary = 'mixed';
-            $part->mimetype        = $part->ctype_primary . '/' . $part->ctype_secondary;
-            $uu_endstring = "`\nend\n";
+            // remove attachment body from the message body
+            $part->body = substr_replace($part->body, '', $startpos, $endpos + $end_len - $startpos);
+            // mark body as modified so it will not be cached by rcube_imap_cache
+            $part->body_modified = true;
 
             // add attachments to the structure
-            foreach ($matches as $pid => $att) {
-                $startpos = strpos($part->body, $att[1]) + strlen($att[1]) + 1; // "\n"
-                $endpos = strpos($part->body, $uu_endstring);
-                $filebody = substr($part->body, $startpos, $endpos-$startpos);
+            $uupart = new rcube_message_part;
+            $uupart->filename = trim($matches[1][0]);
+            $uupart->encoding = 'stream';
+            $uupart->body     = convert_uudecode($filebody);
+            $uupart->size     = strlen($uupart->body);
+            $uupart->mime_id  = 'uu.' . $part->mime_id . '.' . $pid;
+
+            $ctype = rcube_mime::file_content_type($uupart->body, $uupart->filename, 'application/octet-stream', true);
+            $uupart->mimetype = $ctype;
+            list($uupart->ctype_primary, $uupart->ctype_secondary) = explode('/', $ctype);
+
+            $parts[] = $uupart;
+            $pid++;
+        }
 
-                // remove attachments bodies from the message body
-                $part->body = substr_replace($part->body, "", $startpos, $endpos+strlen($uu_endstring)-$startpos);
+        return $parts;
+    }
 
-                $uupart = new rcube_message_part;
+    /**
+     * Fix attachment name encoding if needed/possible
+     */
+    protected function fix_attachment_name($name, $part)
+    {
+        if ($name == rcube_charset::clean($name)) {
+            return $name;
+        }
 
-                $uupart->filename = trim($att[1]);
-                $uupart->encoding = 'stream';
-                $uupart->body     = convert_uudecode($filebody);
-                $uupart->size     = strlen($uupart->body);
-                $uupart->mime_id  = 'uu.' . $part->mime_id . '.' . $pid;
+        // find charset from part or its parent(s)
+        if ($part->charset) {
+            $charsets[] = $part->charset;
+        }
+        else {
+            // check first part (common case)
+            $n = strpos($part->mime_id, '.') ? preg_replace('/\.[0-9]+$/', '', $part->mime_id) . '.1' : 1;
+            if (($_part = $this->mime_parts[$n]) && $_part->charset) {
+                $charsets[] = $_part->charset;
+            }
 
-                $ctype = rcube_mime::file_content_type($uupart->body, $uupart->filename, 'application/octet-stream', true);
-                $uupart->mimetype = $ctype;
-                list($uupart->ctype_primary, $uupart->ctype_secondary) = explode('/', $ctype);
+            // check parents' charset
+            $items = explode('.', $part->mime_id);
+            for ($i = count($items)-1; $i > 0; $i--) {
+                $last   = array_pop($items);
+                $parent = $this->mime_parts[join('.', $items)];
 
-                $parts[] = $uupart;
-                unset($matches[$pid]);
+                if ($parent && $parent->charset) {
+                    $charsets[] = $parent->charset;
+                }
             }
+        }
 
-            // remove attachments bodies from the message body
-            $part->body = preg_replace($uu_regexp, '', $part->body);
+        if ($this->headers->charset) {
+            $charsets[] = $this->headers->charset;
         }
 
-        return $parts;
-    }
+        if (empty($charsets)) {
+            $rcube      = rcube::get_instance();
+            $charsets[] = rcube_charset::detect($name, $rcube->config->get('default_charset', RCUBE_CHARSET));
+        }
+
+        foreach (array_unique($charsets) as $charset) {
+            $_name = rcube_charset::convert($name, $charset);
 
+            if ($_name == rcube_charset::clean($_name)) {
+                if (!$part->charset) {
+                    $part->charset = $charset;
+                }
+
+                return $_name;
+            }
+        }
+
+        return $name;
+    }
 
     /**
      * Deprecated methods (to be removed)
diff --git a/lib/ext/Roundcube/rcube_message_header.php b/lib/ext/Roundcube/rcube_message_header.php
index 2c5e2b6..2b795e5 100644
--- a/lib/ext/Roundcube/rcube_message_header.php
+++ b/lib/ext/Roundcube/rcube_message_header.php
@@ -167,6 +167,13 @@ class rcube_message_header
     public $mdn_to;
 
     /**
+     * IMAP folder this message is stored in
+     *
+     * @var string
+     */
+    public $folder;
+
+    /**
      * Other message headers
      *
      * @var array
@@ -189,6 +196,8 @@ class rcube_message_header
         'reply-to'  => 'replyto',
         'cc'        => 'cc',
         'bcc'       => 'bcc',
+        'mbox'      => 'folder',
+        'folder'    => 'folder',
         'content-transfer-encoding' => 'encoding',
         'in-reply-to'               => 'in_reply_to',
         'content-type'              => 'ctype',
@@ -216,8 +225,16 @@ class rcube_message_header
         }
 
         if ($decode) {
-            $value = rcube_mime::decode_header($value, $this->charset);
-            $value = rcube_charset::clean($value);
+            if (is_array($value)) {
+                foreach ($value as $key => $val) {
+                    $value[$key] = rcube_mime::decode_header($val, $this->charset);
+                    $value[$key] = rcube_charset::clean($val);
+                }
+            }
+            else {
+                $value = rcube_mime::decode_header($value, $this->charset);
+                $value = rcube_charset::clean($value);
+            }
         }
 
         return $value;
diff --git a/lib/ext/Roundcube/rcube_mime.php b/lib/ext/Roundcube/rcube_mime.php
index 4d43a89..3f2fcc3 100644
--- a/lib/ext/Roundcube/rcube_mime.php
+++ b/lib/ext/Roundcube/rcube_mime.php
@@ -3,8 +3,8 @@
 /*
  +-----------------------------------------------------------------------+
  | This file is part of the Roundcube Webmail client                     |
- | Copyright (C) 2005-2012, The Roundcube Dev Team                       |
- | Copyright (C) 2011-2012, Kolab Systems AG                             |
+ | Copyright (C) 2005-2014, The Roundcube Dev Team                       |
+ | Copyright (C) 2011-2014, Kolab Systems AG                             |
  |                                                                       |
  | Licensed under the GNU General Public License version 3 or            |
  | any later version with exceptions for skins & plugins.                |
@@ -39,7 +39,6 @@ class rcube_mime
         self::$default_charset = $default_charset;
     }
 
-
     /**
      * Returns message/object character set name
      *
@@ -58,7 +57,6 @@ class rcube_mime
         return RCUBE_CHARSET;
     }
 
-
     /**
      * Parse the given raw message source and return a structure
      * of rcube_message_part objects.
@@ -75,7 +73,6 @@ class rcube_mime
         return self::structure_part($struct);
     }
 
-
     /**
      * Recursive method to convert a Mail_mimeDecode part into a rcube_message_part object
      *
@@ -88,28 +85,30 @@ class rcube_mime
     private static function structure_part($part, $count=0, $parent='')
     {
         $struct = new rcube_message_part;
-        $struct->mime_id = $part->mime_id ? $part->mime_id : (empty($parent) ? (string)$count : "$parent.$count");
-        $struct->headers = $part->headers;
-        $struct->ctype_primary = $part->ctype_primary;
-        $struct->ctype_secondary = $part->ctype_secondary;
-        $struct->mimetype = $part->ctype_primary . '/' . $part->ctype_secondary;
+        $struct->mime_id          = $part->mime_id ?: (empty($parent) ? (string)$count : "$parent.$count");
+        $struct->headers          = $part->headers;
+        $struct->mimetype         = $part->ctype_primary . '/' . $part->ctype_secondary;
+        $struct->ctype_primary    = $part->ctype_primary;
+        $struct->ctype_secondary  = $part->ctype_secondary;
         $struct->ctype_parameters = $part->ctype_parameters;
 
-        if ($part->headers['content-transfer-encoding'])
+        if ($part->headers['content-transfer-encoding']) {
             $struct->encoding = $part->headers['content-transfer-encoding'];
-        if ($part->ctype_parameters['charset'])
+        }
+
+        if ($part->ctype_parameters['charset']) {
             $struct->charset = $part->ctype_parameters['charset'];
+        }
 
-        $part_charset = $struct->charset ? $struct->charset : self::get_charset();
+        $part_charset = $struct->charset ?: self::get_charset();
 
         // determine filename
         if (($filename = $part->d_parameters['filename']) || ($filename = $part->ctype_parameters['name'])) {
             $struct->filename = rcube_mime::decode_mime_string($filename, $part_charset);
         }
 
-        // copy part body and convert it to UTF-8 if necessary
-        $struct->body = $part->ctype_primary == 'text' || !$part->ctype_parameters['charset'] ? rcube_charset::convert($part->body, $part_charset) : $part->body;
-        $struct->size = strlen($part->body);
+        $struct->body        = $part->body;
+        $struct->size        = strlen($part->body);
         $struct->disposition = $part->disposition;
 
         foreach ((array)$part->parts as $child_part) {
@@ -119,7 +118,6 @@ class rcube_mime
         return $struct;
     }
 
-
     /**
      * Split an address list into a structured array list
      *
@@ -169,7 +167,6 @@ class rcube_mime
         return $out;
     }
 
-
     /**
      * Decode a message header value
      *
@@ -185,7 +182,6 @@ class rcube_mime
         return $str;
     }
 
-
     /**
      * Decode a mime-encoded string to internal charset
      *
@@ -282,12 +278,12 @@ class rcube_mime
         return rcube_charset::convert($input, $default_charset);
     }
 
-
     /**
      * Decode a mime part
      *
      * @param string $input    Input string
      * @param string $encoding Part encoding
+     *
      * @return string Decoded string
      */
     public static function decode($input, $encoding = '7bit')
@@ -308,10 +304,8 @@ class rcube_mime
         }
     }
 
-
     /**
      * Split RFC822 header string into an associative array
-     * @access private
      */
     public static function parse_headers($headers)
     {
@@ -332,7 +326,6 @@ class rcube_mime
         return $a_headers;
     }
 
-
     /**
      * @access private
      */
@@ -402,7 +395,6 @@ class rcube_mime
         return $result;
     }
 
-
     /**
      * Explodes header (e.g. address-list) string into array of strings
      * using specified separator characters with proper handling
@@ -477,19 +469,20 @@ class rcube_mime
         return $result;
     }
 
-
     /**
      * Interpret a format=flowed message body according to RFC 2646
      *
-     * @param string  $text Raw body formatted as flowed text
+     * @param string $text Raw body formatted as flowed text
+     * @param string $mark Mark each flowed line with specified character
      *
      * @return string Interpreted text with unwrapped lines and stuffed space removed
      */
-    public static function unfold_flowed($text)
+    public static function unfold_flowed($text, $mark = null)
     {
-        $text = preg_split('/\r?\n/', $text);
-        $last = -1;
+        $text    = preg_split('/\r?\n/', $text);
+        $last    = -1;
         $q_level = 0;
+        $marks   = array();
 
         foreach ($text as $idx => $line) {
             if (preg_match('/^(>+)/', $line, $m)) {
@@ -509,6 +502,10 @@ class rcube_mime
                 ) {
                     $text[$last] .= $line;
                     unset($text[$idx]);
+
+                    if ($mark) {
+                        $marks[$last] = true;
+                    }
                 }
                 else {
                     $last = $idx;
@@ -521,7 +518,7 @@ class rcube_mime
                 }
                 else {
                     // remove space-stuffing
-                    $line = preg_replace('/^\s/', '', $line);
+                    $line = preg_replace('/^ /', '', $line);
 
                     if (isset($text[$last]) && $line
                         && $text[$last] != '-- '
@@ -529,6 +526,10 @@ class rcube_mime
                     ) {
                         $text[$last] .= $line;
                         unset($text[$idx]);
+
+                        if ($mark) {
+                            $marks[$last] = true;
+                        }
                     }
                     else {
                         $text[$idx] = $line;
@@ -539,15 +540,20 @@ class rcube_mime
             $q_level = $q;
         }
 
+        if (!empty($marks)) {
+            foreach (array_keys($marks) as $mk) {
+                $text[$mk] = $mark . $text[$mk];
+            }
+        }
+
         return implode("\r\n", $text);
     }
 
-
     /**
      * Wrap the given text to comply with RFC 2646
      *
-     * @param string $text Text to wrap
-     * @param int $length Length
+     * @param string $text    Text to wrap
+     * @param int    $length  Length
      * @param string $charset Character encoding of $text
      *
      * @return string Wrapped text
@@ -580,7 +586,6 @@ class rcube_mime
         return implode("\r\n", $text);
     }
 
-
     /**
      * Improved wordwrap function with multibyte support.
      * The code is based on Zend_Text_MultiByte::wordWrap().
@@ -700,7 +705,6 @@ class rcube_mime
         return implode($break, $result);
     }
 
-
     /**
      * A method to guess the mime_type of an attachment.
      *
@@ -719,8 +723,8 @@ class rcube_mime
     {
         static $mime_ext = array();
 
-        $mime_type = null;
-        $config = rcube::get_instance()->config;
+        $mime_type  = null;
+        $config     = rcube::get_instance()->config;
         $mime_magic = $config->get('mime_magic');
 
         if (!$skip_suffix && empty($mime_ext)) {
@@ -774,12 +778,13 @@ class rcube_mime
         return $mime_type;
     }
 
-
     /**
      * Get mimetype => file extension mapping
      *
-     * @param string  Mime-Type to get extensions for
-     * @return array  List of extensions matching the given mimetype or a hash array with ext -> mimetype mappings if $mimetype is not given
+     * @param string Mime-Type to get extensions for
+     *
+     * @return array List of extensions matching the given mimetype or a hash array
+     *               with ext -> mimetype mappings if $mimetype is not given
      */
     public static function get_mime_extensions($mimetype = null)
     {
@@ -807,6 +812,7 @@ class rcube_mime
             $file_paths[] = '/etc/httpd2/mime.types';
             $file_paths[] = '/etc/apache/mime.types';
             $file_paths[] = '/etc/apache2/mime.types';
+            $file_paths[] = '/etc/nginx/mime.types';
             $file_paths[] = '/usr/local/etc/httpd/conf/mime.types';
             $file_paths[] = '/usr/local/etc/apache/conf/mime.types';
         }
@@ -871,7 +877,6 @@ class rcube_mime
         return $mimetype ? $mime_types[$mimetype] : $mime_extensions;
     }
 
-
     /**
      * Detect image type of the given binary data by checking magic numbers.
      *
diff --git a/lib/ext/Roundcube/rcube_output.php b/lib/ext/Roundcube/rcube_output.php
index 7ccf9a0..55a38b2 100644
--- a/lib/ext/Roundcube/rcube_output.php
+++ b/lib/ext/Roundcube/rcube_output.php
@@ -3,7 +3,7 @@
 /*
  +-----------------------------------------------------------------------+
  | This file is part of the Roundcube PHP suite                          |
- | Copyright (C) 2005-2012 The Roundcube Dev Team                        |
+ | Copyright (C) 2005-2014 The Roundcube Dev Team                        |
  |                                                                       |
  | Licensed under the GNU General Public License version 3 or            |
  | any later version with exceptions for skins & plugins.                |
@@ -31,6 +31,7 @@ abstract class rcube_output
     protected $config;
     protected $charset = RCUBE_CHARSET;
     protected $env = array();
+    protected $skins = array();
 
 
     /**
@@ -43,20 +44,21 @@ abstract class rcube_output
         $this->browser = new rcube_browser();
     }
 
-
     /**
      * Magic getter
      */
     public function __get($var)
     {
-        // allow read-only access to $env
-        if ($var == 'env')
-            return $this->env;
+        // allow read-only access to some members
+        switch ($var) {
+            case 'env':     return $this->env;
+            case 'skins':   return $this->skins;
+            case 'charset': return $this->charset;
+        }
 
         return null;
     }
 
-
     /**
      * Setter for output charset.
      * To be specified in a meta tag and sent as http-header
@@ -68,7 +70,6 @@ abstract class rcube_output
         $this->charset = $charset;
     }
 
-
     /**
      * Getter for output charset
      *
@@ -79,7 +80,6 @@ abstract class rcube_output
         return $this->charset;
     }
 
-
     /**
      * Set environment variable
      *
@@ -91,7 +91,6 @@ abstract class rcube_output
         $this->env[$name] = $value;
     }
 
-
     /**
      * Environment variable getter.
      *
@@ -104,7 +103,6 @@ abstract class rcube_output
         return $this->env[$name];
     }
 
-
     /**
      * Delete all stored env variables and commands
      */
@@ -113,7 +111,6 @@ abstract class rcube_output
         $this->env = array();
     }
 
-
     /**
      * Invoke display_message command
      *
@@ -125,7 +122,6 @@ abstract class rcube_output
      */
     abstract function show_message($message, $type = 'notice', $vars = null, $override = true, $timeout = 0);
 
-
     /**
      * Redirect to a certain url.
      *
@@ -134,13 +130,11 @@ abstract class rcube_output
      */
     abstract function redirect($p = array(), $delay = 1);
 
-
     /**
      * Send output to the client.
      */
     abstract function send();
 
-
     /**
      * Send HTTP headers to prevent caching a page
      */
@@ -153,9 +147,6 @@ abstract class rcube_output
         header("Expires: ".gmdate("D, d M Y H:i:s")." GMT");
         header("Last-Modified: ".gmdate("D, d M Y H:i:s")." GMT");
 
-        // Request browser to disable DNS prefetching (CVE-2010-0464)
-        header("X-DNS-Prefetch-Control: off");
-
         // We need to set the following headers to make downloads work using IE in HTTPS mode.
         if ($this->browser->ie && rcube_utils::https_check()) {
             header('Pragma: private');
@@ -174,14 +165,32 @@ abstract class rcube_output
      */
     public function future_expire_header($offset = 2600000)
     {
-        if (headers_sent())
+        if (headers_sent()) {
             return;
+        }
 
         header("Expires: " . gmdate("D, d M Y H:i:s", time()+$offset) . " GMT");
         header("Cache-Control: max-age=$offset");
         header("Pragma: ");
     }
 
+    /**
+     * Send browser compatibility/security/etc. headers
+     */
+    public function common_headers()
+    {
+        if (headers_sent()) {
+            return;
+        }
+
+        // Unlock IE compatibility mode
+        if ($this->browser->ie) {
+            header('X-UA-Compatible: IE=edge');
+        }
+
+        // Request browser to disable DNS prefetching (CVE-2010-0464)
+        header("X-DNS-Prefetch-Control: off");
+    }
 
     /**
      * Show error page and terminate script execution
@@ -196,7 +205,6 @@ abstract class rcube_output
         exit(-1);
     }
 
-
     /**
      * Create an edit field for inclusion on a form
      *
@@ -249,7 +257,6 @@ abstract class rcube_output
         return $out;
     }
 
-
     /**
      * Convert a variable into a javascript object notation
      *
@@ -265,5 +272,4 @@ abstract class rcube_output
         // that's why we have @ here
         return @json_encode($input);
     }
-
 }
diff --git a/lib/ext/Roundcube/rcube_plugin.php b/lib/ext/Roundcube/rcube_plugin.php
index f0af953..e240448 100644
--- a/lib/ext/Roundcube/rcube_plugin.php
+++ b/lib/ext/Roundcube/rcube_plugin.php
@@ -3,7 +3,7 @@
 /*
  +-----------------------------------------------------------------------+
  | This file is part of the Roundcube Webmail client                     |
- | Copyright (C) 2008-2012, The Roundcube Dev Team                       |
+ | Copyright (C) 2008-2014, The Roundcube Dev Team                       |
  |                                                                       |
  | Licensed under the GNU General Public License version 3 or            |
  | any later version with exceptions for skins & plugins.                |
@@ -71,6 +71,7 @@ abstract class rcube_plugin
     protected $home;
     protected $urlbase;
     private $mytask;
+    private $loaded_config = array();
 
 
     /**
@@ -94,7 +95,15 @@ abstract class rcube_plugin
     /**
      * Provide information about this
      *
-     * @return array Meta information about a plugin or false if not implemented
+     * @return array Meta information about a plugin or false if not implemented:
+     * As hash array with the following keys:
+     *      name: The plugin name
+     *    vendor: Name of the plugin developer
+     *   version: Plugin version name
+     *   license: License name (short form according to http://spdx.org/licenses/)
+     *       uri: The URL to the plugin homepage or source repository
+     *   src_uri: Direct download URL to the source code of this plugin
+     *   require: List of plugins required for this one (as array of plugin names)
      */
     public static function info()
     {
@@ -113,6 +122,17 @@ abstract class rcube_plugin
     }
 
     /**
+     * Attempt to load the given plugin which is optional for the current plugin
+     *
+     * @param string Plugin name
+     * @return boolean True on success, false on failure
+     */
+    public function include_plugin($plugin_name)
+    {
+        return $this->api->load_plugin($plugin_name, true, false);
+    }
+
+    /**
      * Load local config file from plugins directory.
      * The loaded values are patched over the global configuration.
      *
@@ -122,6 +142,12 @@ abstract class rcube_plugin
      */
     public function load_config($fname = 'config.inc.php')
     {
+        if (in_array($fname, $this->loaded_config)) {
+            return true;
+        }
+
+        $this->loaded_config[] = $fname;
+
         $fpath = $this->home.'/'.$fname;
         $rcube = rcube::get_instance();
 
@@ -394,7 +420,11 @@ abstract class rcube_plugin
     public function local_skin_path()
     {
         $rcube = rcube::get_instance();
-        foreach (array($rcube->config->get('skin'), 'larry') as $skin) {
+        $skins = array_keys((array)$rcube->output->skins);
+        if (empty($skins)) {
+            $skins = (array) $rcube->config->get('skin');
+        }
+        foreach ($skins as $skin) {
             $skin_path = 'skins/' . $skin;
             if (is_dir(realpath(slashify($this->home) . $skin_path))) {
                 break;
diff --git a/lib/ext/Roundcube/rcube_plugin_api.php b/lib/ext/Roundcube/rcube_plugin_api.php
index 617e921..92a5636 100644
--- a/lib/ext/Roundcube/rcube_plugin_api.php
+++ b/lib/ext/Roundcube/rcube_plugin_api.php
@@ -38,6 +38,7 @@ class rcube_plugin_api
     public $handlers              = array();
     public $allowed_prefs         = array();
     public $allowed_session_prefs = array();
+    public $active_plugins        = array();
 
     protected $plugins = array();
     protected $tasks = array();
@@ -45,7 +46,7 @@ class rcube_plugin_api
     protected $actionmap = array();
     protected $objectsmap = array();
     protected $template_contents = array();
-    protected $active_hook = false;
+    protected $exec_stack = array();
 
     // Deprecated names of hooks, will be removed after 0.5-stable release
     protected $deprecated_hooks = array(
@@ -169,10 +170,11 @@ class rcube_plugin_api
      *
      * @param string Plugin name
      * @param boolean Force loading of the plugin even if it doesn't match the filter
+     * @param boolean Require loading of the plugin, error if it doesn't exist
      *
      * @return boolean True on success, false if not loaded or failure
      */
-    public function load_plugin($plugin_name, $force = false)
+    public function load_plugin($plugin_name, $force = false, $require = true)
     {
         static $plugins_dir;
 
@@ -186,10 +188,9 @@ class rcube_plugin_api
             return true;
         }
 
-        $fn = $plugins_dir . DIRECTORY_SEPARATOR . $plugin_name
-            . DIRECTORY_SEPARATOR . $plugin_name . '.php';
+        $fn = "$plugins_dir/$plugin_name/$plugin_name.php";
 
-        if (file_exists($fn)) {
+        if (is_readable($fn)) {
             if (!class_exists($plugin_name, false)) {
                 include $fn;
             }
@@ -197,6 +198,8 @@ class rcube_plugin_api
             // instantiate class if exists
             if (class_exists($plugin_name, false)) {
                 $plugin = new $plugin_name($this);
+                $this->active_plugins[] = $plugin_name;
+
                 // check inheritance...
                 if (is_subclass_of($plugin, 'rcube_plugin')) {
                     // ... task, request type and framed mode
@@ -222,7 +225,7 @@ class rcube_plugin_api
                     true, false);
             }
         }
-        else {
+        elseif ($require) {
             rcube::raise_error(array('code' => 520, 'type' => 'php',
                 'file' => __FILE__, 'line' => __LINE__,
                 'message' => "Failed to load plugin file $fn"), true, false);
@@ -233,7 +236,7 @@ class rcube_plugin_api
 
     /**
      * Get information about a specific plugin.
-     * This is either provided by a plugin's info() method or extracted from a package.xml or a composer.json file
+     * This is either provided my a plugin's info() method or extracted from a package.xml or a composer.json file
      *
      * @param string Plugin name
      * @return array Meta information about a plugin or False if plugin was not found
@@ -252,6 +255,9 @@ class rcube_plugin_api
         'GPL-3.0'    => 'http://www.gnu.org/licenses/gpl-3.0.html',
         'GPL-3.0+'   => 'http://www.gnu.org/licenses/gpl.html',
         'GPL-2.0+'   => 'http://www.gnu.org/licenses/gpl.html',
+        'AGPLv3'     => 'http://www.gnu.org/licenses/agpl.html',
+        'AGPLv3+'    => 'http://www.gnu.org/licenses/agpl.html',
+        'AGPL-3.0'   => 'http://www.gnu.org/licenses/agpl.html',
         'LGPL'       => 'http://www.gnu.org/licenses/lgpl.html',
         'LGPLv2'     => 'http://www.gnu.org/licenses/lgpl-2.0.html',
         'LGPLv2.1'   => 'http://www.gnu.org/licenses/lgpl-2.1.html',
@@ -272,24 +278,33 @@ class rcube_plugin_api
       );
 
       $dir = dir($this->dir);
-      $fn = unslashify($dir->path) . DIRECTORY_SEPARATOR . $plugin_name . DIRECTORY_SEPARATOR . $plugin_name . '.php';
+      $fn = unslashify($dir->path) . "/$plugin_name/$plugin_name.php";
       $info = false;
 
-      if (!class_exists($plugin_name))
-        include($fn);
+      if (!class_exists($plugin_name, false)) {
+        if (is_readable($fn))
+          include($fn);
+        else
+          return false;
+      }
 
       if (class_exists($plugin_name))
-        $info = call_user_func(array($plugin_name, 'info'));
+        $info = $plugin_name::info();
 
       // fall back to composer.json file
       if (!$info) {
         $composer = INSTALL_PATH . "/plugins/$plugin_name/composer.json";
-        if (file_exists($composer) && ($json = @json_decode(file_get_contents($composer), true))) {
+        if (is_readable($composer) && ($json = @json_decode(file_get_contents($composer), true))) {
           list($info['vendor'], $info['name']) = explode('/', $json['name']);
           $info['version'] = $json['version'];
           $info['license'] = $json['license'];
-          if ($license_uri = $license_uris[$info['license']])
-            $info['license_uri'] = $license_uri;
+          $info['uri'] = $json['homepage'];
+          $info['require'] = array_filter(array_keys((array)$json['require']), function($pname) {
+            if (strpos($pname, '/') == false)
+              return false;
+            list($vendor, $name) = explode('/', $pname);
+            return !($name == 'plugin-installer' || $vendor == 'pear-pear');
+          });
         }
 
         // read local composer.lock file (once)
@@ -313,7 +328,7 @@ class rcube_plugin_api
       // fall back to package.xml file
       if (!$info) {
         $package = INSTALL_PATH . "/plugins/$plugin_name/package.xml";
-        if (file_exists($package) && ($file = file_get_contents($package))) {
+        if (is_readable($package) && ($file = file_get_contents($package))) {
           $doc = new DOMDocument();
           $doc->loadXML($file);
           $xpath = new DOMXPath($doc);
@@ -337,11 +352,19 @@ class rcube_plugin_api
           $deps = $xpath->evaluate('//rc:package/rc:dependencies/rc:required/rc:package/rc:name');
           for ($i = 0; $i < $deps->length; $i++) {
             $dn = $deps->item($i)->nodeValue;
-            $info['requires'][] = $dn;
+            $info['require'][] = $dn;
           }
         }
       }
 
+      // At least provide the name
+      if (!$info && class_exists($plugin_name)) {
+        $info = array('name' => $plugin_name, 'version' => '--');
+      }
+      else if ($info['license'] && empty($info['license_uri']) && ($license_uri = $license_uris[$info['license']])) {
+        $info['license_uri'] = $license_uri;
+      }
+
       return $info;
     }
 
@@ -399,8 +422,10 @@ class rcube_plugin_api
             $args = array('arg' => $args);
         }
 
+        // TODO: avoid recusion by checking in_array($hook, $this->exec_stack) ?
+
         $args += array('abort' => false);
-        $this->active_hook = $hook;
+        array_push($this->exec_stack, $hook);
 
         foreach ((array)$this->handlers[$hook] as $callback) {
             $ret = call_user_func($callback, $args);
@@ -413,7 +438,7 @@ class rcube_plugin_api
             }
         }
 
-        $this->active_hook = false;
+        array_pop($this->exec_stack);
         return $args;
     }
 
@@ -549,7 +574,7 @@ class rcube_plugin_api
      */
     public function is_processing($hook = null)
     {
-        return $this->active_hook && (!$hook || $this->active_hook == $hook);
+        return count($this->exec_stack) > 0 && (!$hook || in_array($hook, $this->exec_stack));
     }
 
     /**
@@ -601,6 +626,16 @@ class rcube_plugin_api
     }
 
     /**
+     * Returns loaded plugin
+     *
+     * @return rcube_plugin Plugin instance
+     */
+    public function get_plugin($name)
+    {
+        return $this->plugins[$name];
+    }
+
+    /**
      * Callback for template_container hooks
      *
      * @param array $attrib
diff --git a/lib/ext/Roundcube/rcube_result_index.php b/lib/ext/Roundcube/rcube_result_index.php
index 058f25c..ffc1ad7 100644
--- a/lib/ext/Roundcube/rcube_result_index.php
+++ b/lib/ext/Roundcube/rcube_result_index.php
@@ -26,6 +26,8 @@
  */
 class rcube_result_index
 {
+    public $incomplete = false;
+
     protected $raw_data;
     protected $mailbox;
     protected $meta = array();
diff --git a/lib/ext/Roundcube/rcube_result_multifolder.php b/lib/ext/Roundcube/rcube_result_multifolder.php
new file mode 100644
index 0000000..786ee85
--- /dev/null
+++ b/lib/ext/Roundcube/rcube_result_multifolder.php
@@ -0,0 +1,337 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | This file is part of the Roundcube Webmail client                     |
+ | Copyright (C) 2005-2011, The Roundcube Dev Team                       |
+ | Copyright (C) 2011, Kolab Systems AG                                  |
+ |                                                                       |
+ | Licensed under the GNU General Public License version 3 or            |
+ | any later version with exceptions for skins & plugins.                |
+ | See the README file for a full license statement.                     |
+ |                                                                       |
+ | PURPOSE:                                                              |
+ |   SORT/SEARCH/ESEARCH response handler                                |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube at gmail.com>                        |
+ +-----------------------------------------------------------------------+
+*/
+
+/**
+ * Class holding a set of rcube_result_index instances that together form a
+ * result set of a multi-folder search
+ *
+ * @package    Framework
+ * @subpackage Storage
+ */
+class rcube_result_multifolder
+{
+    public $multi      = true;
+    public $sets       = array();
+    public $incomplete = false;
+    public $folder;
+
+    protected $meta    = array();
+    protected $index   = array();
+    protected $folders = array();
+    protected $order   = 'ASC';
+    protected $sorting;
+
+
+    /**
+     * Object constructor.
+     */
+    public function __construct($folders = array())
+    {
+        $this->folders = $folders;
+        $this->meta    = array('count' => 0);
+    }
+
+
+    /**
+     * Initializes object with SORT command response
+     *
+     * @param string $data IMAP response string
+     */
+    public function add($result)
+    {
+        $this->sets[] = $result;
+
+        if ($result->count()) {
+            $this->append_result($result);
+        }
+        else if ($result->incomplete) {
+            $this->incomplete = true;
+        }
+    }
+
+    /**
+     * Append message UIDs from the given result to our index
+     */
+    protected function append_result($result)
+    {
+        $this->meta['count'] += $result->count();
+
+        // append UIDs to global index
+        $folder = $result->get_parameters('MAILBOX');
+        $index  = array_map(function($uid) use ($folder) { return $uid . '-' . $folder; }, $result->get());
+
+        $this->index = array_merge($this->index, $index);
+    }
+
+    /**
+     * Store a global index of (sorted) message UIDs
+     */
+    public function set_message_index($headers, $sort_field, $sort_order)
+    {
+        $this->index = array();
+        foreach ($headers as $header) {
+            $this->index[] = $header->uid . '-' . $header->folder;
+        }
+
+        $this->sorting = $sort_field;
+        $this->order   = $sort_order;
+    }
+
+    /**
+     * Checks the result from IMAP command
+     *
+     * @return bool True if the result is an error, False otherwise
+     */
+    public function is_error()
+    {
+        return false;
+    }
+
+
+    /**
+     * Checks if the result is empty
+     *
+     * @return bool True if the result is empty, False otherwise
+     */
+    public function is_empty()
+    {
+        return empty($this->sets) || $this->meta['count'] == 0;
+    }
+
+
+    /**
+     * Returns number of elements in the result
+     *
+     * @return int Number of elements
+     */
+    public function count()
+    {
+        return $this->meta['count'];
+    }
+
+
+    /**
+     * Returns number of elements in the result.
+     * Alias for count() for compatibility with rcube_result_thread
+     *
+     * @return int Number of elements
+     */
+    public function count_messages()
+    {
+        return $this->count();
+    }
+
+
+    /**
+     * Reverts order of elements in the result
+     */
+    public function revert()
+    {
+        $this->order = $this->order == 'ASC' ? 'DESC' : 'ASC';
+        $this->index = array();
+
+        // revert order in all sub-sets
+        foreach ($this->sets as $set) {
+            if ($this->order != $set->get_parameters('ORDER')) {
+                $set->revert();
+            }
+
+            $folder = $set->get_parameters('MAILBOX');
+            $index  = array_map(function($uid) use ($folder) { return $uid . '-' . $folder; }, $set->get());
+
+            $this->index = array_merge($this->index, $index);
+        }
+    }
+
+
+    /**
+     * Check if the given message ID exists in the object
+     *
+     * @param int  $msgid     Message ID
+     * @param bool $get_index When enabled element's index will be returned.
+     *                        Elements are indexed starting with 0
+     * @return mixed False if message ID doesn't exist, True if exists or
+     *               index of the element if $get_index=true
+     */
+    public function exists($msgid, $get_index = false)
+    {
+        if (!empty($this->folder)) {
+            $msgid .= '-' . $this->folder;
+        }
+
+        return array_search($msgid, $this->index);
+    }
+
+
+    /**
+     * Filters data set. Removes elements listed in $ids list.
+     *
+     * @param array $ids List of IDs to remove.
+     * @param string $folder IMAP folder
+     */
+    public function filter($ids = array(), $folder = null)
+    {
+        $this->meta['count'] = 0;
+        foreach ($this->sets as $set) {
+            if ($set->get_parameters('MAILBOX') == $folder) {
+                $set->filter($ids);
+            }
+
+            $this->meta['count'] += $set->count();
+        }
+    }
+
+    /**
+     * Slices data set.
+     *
+     * @param $offset Offset (as for PHP's array_slice())
+     * @param $length Number of elements (as for PHP's array_slice())
+     *
+     */
+    public function slice($offset, $length)
+    {
+        $data = array_slice($this->get(), $offset, $length);
+
+        $this->index = $data;
+        $this->meta['count'] = count($data);
+    }
+
+    /**
+     * Filters data set. Removes elements not listed in $ids list.
+     *
+     * @param array $ids List of IDs to keep.
+     */
+    public function intersect($ids = array())
+    {
+        // not implemented
+    }
+
+    /**
+     * Return all messages in the result.
+     *
+     * @return array List of message IDs
+     */
+    public function get()
+    {
+        return $this->index;
+    }
+
+
+    /**
+     * Return all messages in the result.
+     *
+     * @return array List of message IDs
+     */
+    public function get_compressed()
+    {
+        return '';
+    }
+
+
+    /**
+     * Return result element at specified index
+     *
+     * @param int|string  $index  Element's index or "FIRST" or "LAST"
+     *
+     * @return int Element value
+     */
+    public function get_element($idx)
+    {
+        switch ($idx) {
+            case 'FIRST': return $this->index[0];
+            case 'LAST':  return end($this->index);
+            default:      return $this->index[$idx];
+        }
+    }
+
+
+    /**
+     * Returns response parameters, e.g. ESEARCH's MIN/MAX/COUNT/ALL/MODSEQ
+     * or internal data e.g. MAILBOX, ORDER
+     *
+     * @param string $param  Parameter name
+     *
+     * @return array|string Response parameters or parameter value
+     */
+    public function get_parameters($param=null)
+    {
+        $params = array(
+            'SORT'    => $this->sorting,
+            'ORDER'   => $this->order,
+            'MAILBOX' => $this->folders,
+        );
+
+        if ($param !== null) {
+            return $params[$param];
+        }
+
+        return $params;
+    }
+
+    /**
+     * Returns the stored result object for a particular folder
+     *
+     * @param string $folder  Folder name
+     * @return false|obejct rcube_result_* instance of false if none found
+     */
+    public function get_set($folder)
+    {
+        foreach ($this->sets as $set) {
+            if ($set->get_parameters('MAILBOX') == $folder) {
+                return $set;
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * Returns length of internal data representation
+     *
+     * @return int Data length
+     */
+    protected function length()
+    {
+        return $this->count();
+    }
+
+
+    /* Serialize magic methods */
+
+    public function __sleep()
+    {
+        return array('sets','folders','sorting','order');
+    }
+
+    public function __wakeup()
+    {
+        // restore index from saved result sets
+        $this->meta = array('count' => 0);
+
+        foreach ($this->sets as $result) {
+            if ($result->count()) {
+                $this->append_result($result);
+            }
+            else if ($result->incomplete) {
+                $this->incomplete = true;
+            }
+        }
+    }
+
+}
diff --git a/lib/ext/Roundcube/rcube_result_thread.php b/lib/ext/Roundcube/rcube_result_thread.php
index ceaaf59..1687616 100644
--- a/lib/ext/Roundcube/rcube_result_thread.php
+++ b/lib/ext/Roundcube/rcube_result_thread.php
@@ -26,6 +26,8 @@
  */
 class rcube_result_thread
 {
+    public $incomplete = false;
+
     protected $raw_data;
     protected $mailbox;
     protected $meta = array();
diff --git a/lib/ext/Roundcube/rcube_session.php b/lib/ext/Roundcube/rcube_session.php
index caca262..8306a06 100644
--- a/lib/ext/Roundcube/rcube_session.php
+++ b/lib/ext/Roundcube/rcube_session.php
@@ -3,7 +3,7 @@
 /*
  +-----------------------------------------------------------------------+
  | This file is part of the Roundcube Webmail client                     |
- | Copyright (C) 2005-2012, The Roundcube Dev Team                       |
+ | Copyright (C) 2005-2014, The Roundcube Dev Team                       |
  | Copyright (C) 2011, Kolab Systems AG                                  |
  |                                                                       |
  | Licensed under the GNU General Public License version 3 or            |
@@ -47,6 +47,13 @@ class rcube_session
     private $storage;
     private $memcache;
 
+    /**
+     * Blocks session data from being written to database.
+     * Can be used if write-race conditions are to be expected
+     * @var boolean
+     */
+    public $nowrite = false;
+
 
     /**
      * Default constructor
@@ -96,6 +103,8 @@ class rcube_session
                 array($this, 'db_write'),
                 array($this, 'db_destroy'),
                 array($this, 'gc'));
+
+            $this->table_name = $this->db->table_name('session', true);
         }
     }
 
@@ -168,9 +177,8 @@ class rcube_session
     public function db_read($key)
     {
         $sql_result = $this->db->query(
-            "SELECT vars, ip, changed, " . $this->db->now() . " AS ts"
-            . " FROM " . $this->db->table_name('session')
-            . " WHERE sess_id = ?", $key);
+            "SELECT `vars`, `ip`, `changed`, " . $this->db->now() . " AS ts"
+            . " FROM {$this->table_name} WHERE `sess_id` = ?", $key);
 
         if ($sql_result && ($sql_arr = $this->db->fetch_assoc($sql_result))) {
             $this->time_diff = time() - strtotime($sql_arr['ts']);
@@ -197,9 +205,11 @@ class rcube_session
      */
     public function db_write($key, $vars)
     {
-        $now   = $this->db->now();
-        $table = $this->db->table_name('session');
-        $ts    = microtime(true);
+        $now = $this->db->now();
+        $ts  = microtime(true);
+
+        if ($this->nowrite)
+            return true;
 
         // no session row in DB (db_read() returns false)
         if (!$this->key) {
@@ -217,17 +227,18 @@ class rcube_session
             $newvars = $this->_fixvars($vars, $oldvars);
 
             if ($newvars !== $oldvars) {
-                $this->db->query("UPDATE $table "
-                    . "SET changed = $now, vars = ? WHERE sess_id = ?",
+                $this->db->query("UPDATE {$this->table_name} "
+                    . "SET `changed` = $now, `vars` = ? WHERE `sess_id` = ?",
                     base64_encode($newvars), $key);
             }
             else if ($ts - $this->changed + $this->time_diff > $this->lifetime / 2) {
-                $this->db->query("UPDATE $table SET changed = $now"
-                    . " WHERE sess_id = ?", $key);
+                $this->db->query("UPDATE {$this->table_name} SET `changed` = $now"
+                    . " WHERE `sess_id` = ?", $key);
             }
         }
         else {
-            $this->db->query("INSERT INTO $table (sess_id, vars, ip, created, changed)"
+            $this->db->query("INSERT INTO {$this->table_name}"
+                . " (`sess_id`, `vars`, `ip`, `created`, `changed`)"
                 . " VALUES (?, ?, ?, $now, $now)",
                 $key, base64_encode($vars), (string)$this->ip);
         }
@@ -280,8 +291,7 @@ class rcube_session
     public function db_destroy($key)
     {
         if ($key) {
-            $this->db->query(sprintf("DELETE FROM %s WHERE sess_id = ?",
-                $this->db->table_name('session')), $key);
+            $this->db->query("DELETE FROM {$this->table_name} WHERE `sess_id` = ?", $key);
         }
 
         return true;
@@ -397,8 +407,8 @@ class rcube_session
         if ($this->gc_enabled) {
             // just delete all expired sessions
             if ($this->storage == 'db') {
-                $this->db->query("DELETE FROM " . $this->db->table_name('session')
-                    . " WHERE changed < " . $this->db->now(-$this->gc_enabled));
+                $this->db->query("DELETE FROM {$this->table_name}"
+                    . " WHERE `changed` < " . $this->db->now(-$this->gc_enabled));
             }
 
             foreach ($this->gc_handlers as $fct) {
diff --git a/lib/ext/Roundcube/rcube_spellchecker.php b/lib/ext/Roundcube/rcube_spellchecker.php
index e9a3607..0627807 100644
--- a/lib/ext/Roundcube/rcube_spellchecker.php
+++ b/lib/ext/Roundcube/rcube_spellchecker.php
@@ -226,7 +226,18 @@ class rcube_spellchecker
             else {
                 $word = mb_substr($this->content, $item[1], $item[2], RCUBE_CHARSET);
             }
-            $result[$word] = is_array($item[4]) ? implode("\t", $item[4]) : $item[4];
+
+            if (is_array($item[4])) {
+                $suggestions = $item[4];
+            }
+            else if (empty($item[4])) {
+                $suggestions = array();
+            }
+            else {
+                $suggestions = explode("\t", $item[4]);
+            }
+
+            $result[$word] = $suggestions;
         }
 
         return $result;
@@ -349,25 +360,25 @@ class rcube_spellchecker
         if ($this->have_dict) {
             if (!empty($this->dict)) {
                 $this->rc->db->query(
-                    "UPDATE ".$this->rc->db->table_name('dictionary')
-                    ." SET data = ?"
-                    ." WHERE user_id " . ($plugin['userid'] ? "= ".$this->rc->db->quote($plugin['userid']) : "IS NULL")
-                        ." AND " . $this->rc->db->quote_identifier('language') . " = ?",
+                    "UPDATE " . $this->rc->db->table_name('dictionary', true)
+                    ." SET `data` = ?"
+                    ." WHERE `user_id` " . ($plugin['userid'] ? "= ".$this->rc->db->quote($plugin['userid']) : "IS NULL")
+                        ." AND `language` = ?",
                     implode(' ', $plugin['dictionary']), $plugin['language']);
             }
             // don't store empty dict
             else {
                 $this->rc->db->query(
-                    "DELETE FROM " . $this->rc->db->table_name('dictionary')
-                    ." WHERE user_id " . ($plugin['userid'] ? "= ".$this->rc->db->quote($plugin['userid']) : "IS NULL")
-                        ." AND " . $this->rc->db->quote_identifier('language') . " = ?",
+                    "DELETE FROM " . $this->rc->db->table_name('dictionary', true)
+                    ." WHERE `user_id` " . ($plugin['userid'] ? "= ".$this->rc->db->quote($plugin['userid']) : "IS NULL")
+                        ." AND `language` = ?",
                     $plugin['language']);
             }
         }
         else if (!empty($this->dict)) {
             $this->rc->db->query(
-                "INSERT INTO " .$this->rc->db->table_name('dictionary')
-                ." (user_id, " . $this->rc->db->quote_identifier('language') . ", data) VALUES (?, ?, ?)",
+                "INSERT INTO " . $this->rc->db->table_name('dictionary', true)
+                ." (`user_id`, `language`, `data`) VALUES (?, ?, ?)",
                 $plugin['userid'], $plugin['language'], implode(' ', $plugin['dictionary']));
         }
     }
@@ -392,9 +403,9 @@ class rcube_spellchecker
         if (empty($plugin['abort'])) {
             $dict = array();
             $sql_result = $this->rc->db->query(
-                "SELECT data FROM ".$this->rc->db->table_name('dictionary')
-                ." WHERE user_id ". ($plugin['userid'] ? "= ".$this->rc->db->quote($plugin['userid']) : "IS NULL")
-                    ." AND " . $this->rc->db->quote_identifier('language') . " = ?",
+                "SELECT `data` FROM " . $this->rc->db->table_name('dictionary', true)
+                ." WHERE `user_id` ". ($plugin['userid'] ? "= ".$this->rc->db->quote($plugin['userid']) : "IS NULL")
+                    ." AND `language` = ?",
                 $plugin['language']);
 
             if ($sql_arr = $this->rc->db->fetch_assoc($sql_result)) {
@@ -416,5 +427,4 @@ class rcube_spellchecker
 
         return $this->dict;
     }
-
 }
diff --git a/lib/ext/Roundcube/rcube_storage.php b/lib/ext/Roundcube/rcube_storage.php
index c09f053..ccb28c6 100644
--- a/lib/ext/Roundcube/rcube_storage.php
+++ b/lib/ext/Roundcube/rcube_storage.php
@@ -35,9 +35,15 @@ abstract class rcube_storage
      */
     public $conn;
 
+    /**
+     * List of supported special folder types
+     *
+     * @var array
+     */
+    public static $folder_types = array('drafts', 'sent', 'junk', 'trash');
+
     protected $folder = 'INBOX';
     protected $default_charset = 'ISO-8859-1';
-    protected $default_folders = array('INBOX');
     protected $search_set;
     protected $options = array('auth_type' => 'check');
     protected $page_size = 10;
@@ -146,6 +152,19 @@ abstract class rcube_storage
 
 
     /**
+     * Get connection/class option
+     *
+     * @param string $name Option name
+     *
+     * @param mixed Option value
+     */
+    public function get_option($name)
+    {
+        return $this->options[$name];
+    }
+
+
+    /**
      * Activate/deactivate debug mode.
      *
      * @param boolean $dbg True if conversation with the server should be logged
@@ -167,24 +186,6 @@ abstract class rcube_storage
 
 
     /**
-     * This list of folders will be listed above all other folders
-     *
-     * @param  array $arr Indexed list of folder names
-     */
-    public function set_default_folders($arr)
-    {
-        if (is_array($arr)) {
-            $this->default_folders = $arr;
-
-            // add inbox if not included
-            if (!in_array('INBOX', $this->default_folders)) {
-                array_unshift($this->default_folders, 'INBOX');
-            }
-        }
-    }
-
-
-    /**
      * Set internal folder reference.
      * All operations will be perfomed on this folder.
      *
@@ -858,24 +859,70 @@ abstract class rcube_storage
      */
     public function create_default_folders()
     {
+        $rcube = rcube::get_instance();
+
         // create default folders if they do not exist
-        foreach ($this->default_folders as $folder) {
-            if (!$this->folder_exists($folder)) {
-                $this->create_folder($folder, true);
+        foreach (self::$folder_types as $type) {
+            if ($folder = $rcube->config->get($type . '_mbox')) {
+                if (!$this->folder_exists($folder)) {
+                    $this->create_folder($folder, true, $type);
+                }
+                else if (!$this->folder_exists($folder, true)) {
+                    $this->subscribe($folder);
+                }
             }
-            else if (!$this->folder_exists($folder, true)) {
-                $this->subscribe($folder);
+        }
+    }
+
+
+    /**
+     * Check if specified folder is a special folder
+     */
+    public function is_special_folder($name)
+    {
+        return $name == 'INBOX' || in_array($name, $this->get_special_folders());
+    }
+
+
+    /**
+     * Return configured special folders
+     */
+    public function get_special_folders($forced = false)
+    {
+        // getting config might be expensive, store special folders in memory
+        if (!isset($this->icache['special-folders'])) {
+            $rcube = rcube::get_instance();
+            $this->icache['special-folders'] = array();
+
+            foreach (self::$folder_types as $type) {
+                if ($folder = $rcube->config->get($type . '_mbox')) {
+                    $this->icache['special-folders'][$type] = $folder;
+                }
             }
         }
+
+        return $this->icache['special-folders'];
+    }
+
+
+    /**
+     * Set special folder associations stored in backend
+     */
+    public function set_special_folders($specials)
+    {
+        // should be overriden by storage class if backend supports special folders (SPECIAL-USE)
+        unset($this->icache['special-folders']);
     }
 
 
     /**
      * Get mailbox quota information.
      *
+     * @param string $folder  Folder name
+     *
      * @return mixed Quota info or False if not supported
      */
-    abstract function get_quota();
+    abstract function get_quota($folder = null);
 
 
     /* -----------------------------------------
diff --git a/lib/ext/Roundcube/rcube_tnef_decoder.php b/lib/ext/Roundcube/rcube_tnef_decoder.php
new file mode 100644
index 0000000..f047ba0
--- /dev/null
+++ b/lib/ext/Roundcube/rcube_tnef_decoder.php
@@ -0,0 +1,341 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | This file is part of the Roundcube Webmail client                     |
+ | Copyright (C) 2008-2014, The Roundcube Dev Team                       |
+ | Copyright (C) 2002-2010, The Horde Project (http://www.horde.org/)    |
+ |                                                                       |
+ | Licensed under the GNU General Public License version 3 or            |
+ | any later version with exceptions for skins & plugins.                |
+ | See the README file for a full license statement.                     |
+ |                                                                       |
+ | PURPOSE:                                                              |
+ |   MS-TNEF format decoder                                              |
+ +-----------------------------------------------------------------------+
+ | Author: Jan Schneider <jan at horde.org>                                 |
+ | Author: Michael Slusarz <slusarz at horde.org>                           |
+ +-----------------------------------------------------------------------+
+*/
+
+/**
+ * MS-TNEF format decoder based on code by:
+ *   Graham Norbury <gnorbury at bondcar.com>
+ * Original design by:
+ *   Thomas Boll <tb at boll.ch>, Mark Simpson <damned at world.std.com>
+ *
+ * @package    Framework
+ * @subpackage Storage
+ */
+class rcube_tnef_decoder
+{
+    const SIGNATURE = 0x223e9f78;
+    const LVL_MESSAGE = 0x01;
+    const LVL_ATTACHMENT = 0x02;
+
+    const ASUBJECT = 0x88004;
+    const AMCLASS = 0x78008;
+    const ATTACHDATA = 0x6800f;
+    const AFILENAME = 0x18010;
+    const ARENDDATA = 0x69002;
+    const AMAPIATTRS = 0x69005;
+    const AVERSION = 0x89006;
+
+    const MAPI_NULL = 0x0001;
+    const MAPI_SHORT = 0x0002;
+    const MAPI_INT = 0x0003;
+    const MAPI_FLOAT = 0x0004;
+    const MAPI_DOUBLE = 0x0005;
+    const MAPI_CURRENCY = 0x0006;
+    const MAPI_APPTIME = 0x0007;
+    const MAPI_ERROR = 0x000a;
+    const MAPI_BOOLEAN = 0x000b;
+    const MAPI_OBJECT = 0x000d;
+    const MAPI_INT8BYTE = 0x0014;
+    const MAPI_STRING = 0x001e;
+    const MAPI_UNICODE_STRING = 0x001f;
+    const MAPI_SYSTIME = 0x0040;
+    const MAPI_CLSID = 0x0048;
+    const MAPI_BINARY = 0x0102;
+
+    const MAPI_ATTACH_LONG_FILENAME = 0x3707;
+    const MAPI_ATTACH_MIME_TAG = 0x370E;
+
+    const MAPI_NAMED_TYPE_ID = 0x0000;
+    const MAPI_NAMED_TYPE_STRING = 0x0001;
+    const MAPI_MV_FLAG = 0x1000;
+
+    /**
+     * Decompress the data.
+     *
+     * @param string $data   The data to decompress.
+     * @param array $params  An array of arguments needed to decompress the
+     *                       data.
+     *
+     * @return mixed  The decompressed data.
+     */
+    public function decompress($data, $params = array())
+    {
+        $out = array();
+
+        if ($this->_geti($data, 32) == self::SIGNATURE) {
+            $this->_geti($data, 16);
+
+            while (strlen($data) > 0) {
+                switch ($this->_geti($data, 8)) {
+                case self::LVL_MESSAGE:
+                    $this->_decodeMessage($data);
+                    break;
+
+                case self::LVL_ATTACHMENT:
+                    $this->_decodeAttachment($data, $out);
+                    break;
+                }
+            }
+        }
+
+        return array_reverse($out);
+    }
+
+    /**
+     * TODO
+     *
+     * @param string &$data  The data string.
+     * @param integer $bits  How many bits to retrieve.
+     *
+     * @return TODO
+     */
+    protected function _getx(&$data, $bits)
+    {
+        $value = null;
+
+        if (strlen($data) >= $bits) {
+            $value = substr($data, 0, $bits);
+            $data = substr_replace($data, '', 0, $bits);
+        }
+
+        return $value;
+    }
+
+    /**
+     * TODO
+     *
+     * @param string &$data  The data string.
+     * @param integer $bits  How many bits to retrieve.
+     *
+     * @return TODO
+     */
+    protected function _geti(&$data, $bits)
+    {
+        $bytes = $bits / 8;
+        $value = null;
+
+        if (strlen($data) >= $bytes) {
+            $value = ord($data[0]);
+            if ($bytes >= 2) {
+                $value += (ord($data[1]) << 8);
+            }
+            if ($bytes >= 4) {
+                $value += (ord($data[2]) << 16) + (ord($data[3]) << 24);
+            }
+            $data = substr_replace($data, '', 0, $bytes);
+        }
+
+        return $value;
+    }
+
+    /**
+     * TODO
+     *
+     * @param string &$data      The data string.
+     * @param string $attribute  TODO
+     */
+    protected function _decodeAttribute(&$data, $attribute)
+    {
+        /* Data. */
+        $this->_getx($data, $this->_geti($data, 32));
+
+        /* Checksum. */
+        $this->_geti($data, 16);
+    }
+
+    /**
+     * TODO
+     *
+     * @param string $data             The data string.
+     * @param array &$attachment_data  TODO
+     */
+    protected function _extractMapiAttributes($data, &$attachment_data)
+    {
+        /* Number of attributes. */
+        $number = $this->_geti($data, 32);
+
+        while ((strlen($data) > 0) && $number--) {
+            $have_mval = false;
+            $num_mval = 1;
+            $named_id = $value = null;
+            $attr_type = $this->_geti($data, 16);
+            $attr_name = $this->_geti($data, 16);
+
+            if (($attr_type & self::MAPI_MV_FLAG) != 0) {
+                $have_mval = true;
+                $attr_type = $attr_type & ~self::MAPI_MV_FLAG;
+            }
+
+            if (($attr_name >= 0x8000) && ($attr_name < 0xFFFE)) {
+                $this->_getx($data, 16);
+                $named_type = $this->_geti($data, 32);
+
+                switch ($named_type) {
+                case self::MAPI_NAMED_TYPE_ID:
+                    $named_id = $this->_geti($data, 32);
+                    $attr_name = $named_id;
+                    break;
+
+                case self::MAPI_NAMED_TYPE_STRING:
+                    $attr_name = 0x9999;
+                    $idlen = $this->_geti($data, 32);
+                    $datalen = $idlen + ((4 - ($idlen % 4)) % 4);
+                    $named_id = substr($this->_getx($data, $datalen), 0, $idlen);
+                    break;
+                }
+            }
+
+            if ($have_mval) {
+                $num_mval = $this->_geti($data, 32);
+            }
+
+            switch ($attr_type) {
+            case self::MAPI_SHORT:
+                $value = $this->_geti($data, 16);
+                break;
+
+            case self::MAPI_INT:
+            case self::MAPI_BOOLEAN:
+                for ($i = 0; $i < $num_mval; $i++) {
+                    $value = $this->_geti($data, 32);
+                }
+                break;
+
+            case self::MAPI_FLOAT:
+            case self::MAPI_ERROR:
+                $value = $this->_getx($data, 4);
+                break;
+
+            case self::MAPI_DOUBLE:
+            case self::MAPI_APPTIME:
+            case self::MAPI_CURRENCY:
+            case self::MAPI_INT8BYTE:
+            case self::MAPI_SYSTIME:
+                $value = $this->_getx($data, 8);
+                break;
+
+            case self::MAPI_STRING:
+            case self::MAPI_UNICODE_STRING:
+            case self::MAPI_BINARY:
+            case self::MAPI_OBJECT:
+                $num_vals = ($have_mval) ? $num_mval : $this->_geti($data, 32);
+                for ($i = 0; $i < $num_vals; $i++) {
+                    $length = $this->_geti($data, 32);
+
+                    /* Pad to next 4 byte boundary. */
+                    $datalen = $length + ((4 - ($length % 4)) % 4);
+
+                    if ($attr_type == self::MAPI_STRING) {
+                        --$length;
+                    }
+
+                    /* Read and truncate to length. */
+                    $value = substr($this->_getx($data, $datalen), 0, $length);
+                }
+                break;
+            }
+
+            /* Store any interesting attributes. */
+            switch ($attr_name) {
+            case self::MAPI_ATTACH_LONG_FILENAME:
+                $value = str_replace("\0", '', $value);
+                /* Used in preference to AFILENAME value. */
+                $attachment_data[0]['name'] = preg_replace('/.*[\/](.*)$/', '\1', $value);
+                break;
+
+            case self::MAPI_ATTACH_MIME_TAG:
+                $value = str_replace("\0", '', $value);
+                /* Is this ever set, and what is format? */
+                $attachment_data[0]['type']    = preg_replace('/^(.*)\/.*/', '\1', $value);
+                $attachment_data[0]['subtype'] = preg_replace('/.*\/(.*)$/', '\1', $value);
+                break;
+            }
+        }
+    }
+
+    /**
+     * TODO
+     *
+     * @param string &$data  The data string.
+     */
+    protected function _decodeMessage(&$data)
+    {
+        $this->_decodeAttribute($data, $this->_geti($data, 32));
+    }
+
+    /**
+     * TODO
+     *
+     * @param string &$data            The data string.
+     * @param array &$attachment_data  TODO
+     */
+    protected function _decodeAttachment(&$data, &$attachment_data)
+    {
+        $attribute = $this->_geti($data, 32);
+
+        switch ($attribute) {
+        case self::ARENDDATA:
+            /* Marks start of new attachment. */
+            $this->_getx($data, $this->_geti($data, 32));
+
+            /* Checksum */
+            $this->_geti($data, 16);
+
+            /* Add a new default data block to hold details of this
+               attachment. Reverse order is easier to handle later! */
+            array_unshift($attachment_data, array('type'    => 'application',
+                                                  'subtype' => 'octet-stream',
+                                                  'name'    => 'unknown',
+                                                  'stream'  => ''));
+            break;
+
+        case self::AFILENAME:
+            $value = $this->_getx($data, $this->_geti($data, 32));
+            $value = str_replace("\0", '', $value);
+            /* Strip path. */
+            $attachment_data[0]['name'] = preg_replace('/.*[\/](.*)$/', '\1', $value);
+
+            /* Checksum */
+            $this->_geti($data, 16);
+            break;
+
+        case self::ATTACHDATA:
+            /* The attachment itself. */
+            $length = $this->_geti($data, 32);
+            $attachment_data[0]['size'] = $length;
+            $attachment_data[0]['stream'] = $this->_getx($data, $length);
+
+            /* Checksum */
+            $this->_geti($data, 16);
+            break;
+
+        case self::AMAPIATTRS:
+            $length = $this->_geti($data, 32);
+            $value = $this->_getx($data, $length);
+
+            /* Checksum */
+            $this->_geti($data, 16);
+            $this->_extractMapiAttributes($value, $attachment_data);
+            break;
+
+        default:
+            $this->_decodeAttribute($data, $attribute);
+        }
+    }
+}
diff --git a/lib/ext/Roundcube/rcube_user.php b/lib/ext/Roundcube/rcube_user.php
index e232736..b2110df 100644
--- a/lib/ext/Roundcube/rcube_user.php
+++ b/lib/ext/Roundcube/rcube_user.php
@@ -67,7 +67,8 @@ class rcube_user
 
         if ($id && !$sql_arr) {
             $sql_result = $this->db->query(
-                "SELECT * FROM ".$this->db->table_name('users')." WHERE user_id = ?", $id);
+                "SELECT * FROM " . $this->db->table_name('users', true)
+                . " WHERE `user_id` = ?", $id);
             $sql_arr = $this->db->fetch_assoc($sql_result);
         }
 
@@ -189,10 +190,9 @@ class rcube_user
         $save_prefs = serialize($save_prefs);
 
         $this->db->query(
-            "UPDATE ".$this->db->table_name('users').
-            " SET preferences = ?".
-                ", language = ?".
-            " WHERE user_id = ?",
+            "UPDATE ".$this->db->table_name('users', true).
+            " SET `preferences` = ?, `language` = ?".
+            " WHERE `user_id` = ?",
             $save_prefs,
             $_SESSION['language'],
             $this->ID);
@@ -243,7 +243,7 @@ class rcube_user
         $id = (int)$id;
         // cache identities for better performance
         if (!array_key_exists($id, $this->identities)) {
-            $result = $this->list_identities($id ? 'AND identity_id = ' . $id : '');
+            $result = $this->list_identities($id ? "AND `identity_id` = $id" : '');
             $this->identities[$id] = $result[0];
         }
 
@@ -264,10 +264,10 @@ class rcube_user
         $result = array();
 
         $sql_result = $this->db->query(
-            "SELECT * FROM ".$this->db->table_name('identities').
-            " WHERE del <> 1 AND user_id = ?".
+            "SELECT * FROM ".$this->db->table_name('identities', true).
+            " WHERE `del` <> 1 AND `user_id` = ?".
             ($sql_add ? " ".$sql_add : "").
-            " ORDER BY ".$this->db->quote_identifier('standard')." DESC, name ASC, identity_id ASC",
+            " ORDER BY `standard` DESC, `name` ASC, `email` ASC, `identity_id` ASC",
             $this->ID);
 
         while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
@@ -308,11 +308,11 @@ class rcube_user
         $query_params[] = $iid;
         $query_params[] = $this->ID;
 
-        $sql = "UPDATE ".$this->db->table_name('identities').
-            " SET changed = ".$this->db->now().", ".join(', ', $query_cols).
-            " WHERE identity_id = ?".
-                " AND user_id = ?".
-                " AND del <> 1";
+        $sql = "UPDATE ".$this->db->table_name('identities', true).
+            " SET `changed` = ".$this->db->now().", ".join(', ', $query_cols).
+            " WHERE `identity_id` = ?".
+                " AND `user_id` = ?".
+                " AND `del` <> 1";
 
         call_user_func_array(array($this->db, 'query'),
             array_merge(array($sql), $query_params));
@@ -341,11 +341,11 @@ class rcube_user
             $insert_cols[]   = $this->db->quote_identifier($col);
             $insert_values[] = $value;
         }
-        $insert_cols[]   = 'user_id';
+        $insert_cols[]   = $this->db->quote_identifier('user_id');
         $insert_values[] = $this->ID;
 
-        $sql = "INSERT INTO ".$this->db->table_name('identities').
-            " (changed, ".join(', ', $insert_cols).")".
+        $sql = "INSERT INTO ".$this->db->table_name('identities', true).
+            " (`changed`, ".join(', ', $insert_cols).")".
             " VALUES (".$this->db->now().", ".join(', ', array_pad(array(), sizeof($insert_values), '?')).")";
 
         call_user_func_array(array($this->db, 'query'),
@@ -369,8 +369,8 @@ class rcube_user
             return false;
 
         $sql_result = $this->db->query(
-            "SELECT count(*) AS ident_count FROM ".$this->db->table_name('identities').
-            " WHERE user_id = ? AND del <> 1",
+            "SELECT count(*) AS ident_count FROM ".$this->db->table_name('identities', true).
+            " WHERE `user_id` = ? AND `del` <> 1",
             $this->ID);
 
         $sql_arr = $this->db->fetch_assoc($sql_result);
@@ -380,10 +380,10 @@ class rcube_user
             return -1;
 
         $this->db->query(
-            "UPDATE ".$this->db->table_name('identities').
-            " SET del = 1, changed = ".$this->db->now().
-            " WHERE user_id = ?".
-                " AND identity_id = ?",
+            "UPDATE ".$this->db->table_name('identities', true).
+            " SET `del` = 1, `changed` = ".$this->db->now().
+            " WHERE `user_id` = ?".
+                " AND `identity_id` = ?",
             $this->ID,
             $iid);
 
@@ -402,11 +402,9 @@ class rcube_user
     {
         if ($this->ID && $iid) {
             $this->db->query(
-                "UPDATE ".$this->db->table_name('identities').
-                " SET ".$this->db->quote_identifier('standard')." = '0'".
-                " WHERE user_id = ?".
-                    " AND identity_id <> ?".
-                    " AND del <> 1",
+                "UPDATE ".$this->db->table_name('identities', true).
+                " SET `standard` = '0'".
+                " WHERE `user_id` = ? AND `identity_id` <> ?",
                 $this->ID,
                 $iid);
 
@@ -422,9 +420,9 @@ class rcube_user
     {
         if ($this->ID) {
             $this->db->query(
-                "UPDATE ".$this->db->table_name('users').
-                " SET last_login = ".$this->db->now().
-                " WHERE user_id = ?",
+                "UPDATE ".$this->db->table_name('users', true).
+                " SET `last_login` = ".$this->db->now().
+                " WHERE `user_id` = ?",
                 $this->ID);
         }
     }
@@ -453,17 +451,17 @@ class rcube_user
         $config = rcube::get_instance()->config;
 
         // query for matching user name
-        $sql_result = $dbh->query("SELECT * FROM " . $dbh->table_name('users')
-            ." WHERE mail_host = ? AND username = ?", $host, $user);
+        $sql_result = $dbh->query("SELECT * FROM " . $dbh->table_name('users', true)
+            ." WHERE `mail_host` = ? AND `username` = ?", $host, $user);
 
         $sql_arr = $dbh->fetch_assoc($sql_result);
 
         // username not found, try aliases from identities
         if (empty($sql_arr) && $config->get('user_aliases') && strpos($user, '@')) {
             $sql_result = $dbh->limitquery("SELECT u.*"
-                ." FROM " . $dbh->table_name('users') . " u"
-                ." JOIN " . $dbh->table_name('identities') . " i ON (i.user_id = u.user_id)"
-                ." WHERE email = ? AND del <> 1", 0, 1, $user);
+                ." FROM " . $dbh->table_name('users', true) . " u"
+                ." JOIN " . $dbh->table_name('identities', true) . " i ON (i.`user_id` = u.`user_id`)"
+                ." WHERE `email` = ? AND `del` <> 1", 0, 1, $user);
 
             $sql_arr = $dbh->fetch_assoc($sql_result);
         }
@@ -510,8 +508,8 @@ class rcube_user
         }
 
         $dbh->query(
-            "INSERT INTO ".$dbh->table_name('users').
-            " (created, last_login, username, mail_host, language)".
+            "INSERT INTO ".$dbh->table_name('users', true).
+            " (`created`, `last_login`, `username`, `mail_host`, `language`)".
             " VALUES (".$dbh->now().", ".$dbh->now().", ?, ?, ?)",
             $data['user'],
             $data['host'],
@@ -643,11 +641,10 @@ class rcube_user
         $result = array();
 
         $sql_result = $this->db->query(
-            "SELECT search_id AS id, ".$this->db->quote_identifier('name')
-            ." FROM ".$this->db->table_name('searches')
-            ." WHERE user_id = ?"
-                ." AND ".$this->db->quote_identifier('type')." = ?"
-            ." ORDER BY ".$this->db->quote_identifier('name'),
+            "SELECT `search_id` AS id, `name`"
+            ." FROM ".$this->db->table_name('searches', true)
+            ." WHERE `user_id` = ? AND `type` = ?"
+            ." ORDER BY `name`",
             (int) $this->ID, (int) $type);
 
         while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
@@ -675,12 +672,10 @@ class rcube_user
         }
 
         $sql_result = $this->db->query(
-            "SELECT ".$this->db->quote_identifier('name')
-                .", ".$this->db->quote_identifier('data')
-                .", ".$this->db->quote_identifier('type')
-            ." FROM ".$this->db->table_name('searches')
-            ." WHERE user_id = ?"
-                ." AND search_id = ?",
+            "SELECT `name`, `data`, `type`"
+            . " FROM ".$this->db->table_name('searches', true)
+            . " WHERE `user_id` = ?"
+                ." AND `search_id` = ?",
             (int) $this->ID, (int) $id);
 
         while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
@@ -709,9 +704,9 @@ class rcube_user
             return false;
 
         $this->db->query(
-            "DELETE FROM ".$this->db->table_name('searches')
-            ." WHERE user_id = ?"
-                ." AND search_id = ?",
+            "DELETE FROM ".$this->db->table_name('searches', true)
+            ." WHERE `user_id` = ?"
+                ." AND `search_id` = ?",
             (int) $this->ID, $sid);
 
         return $this->db->affected_rows();
@@ -739,7 +734,7 @@ class rcube_user
         $insert_cols[]   = $this->db->quote_identifier('data');
         $insert_values[] = serialize($data['data']);
 
-        $sql = "INSERT INTO ".$this->db->table_name('searches')
+        $sql = "INSERT INTO ".$this->db->table_name('searches', true)
             ." (".join(', ', $insert_cols).")"
             ." VALUES (".join(', ', array_pad(array(), sizeof($insert_values), '?')).")";
 
@@ -748,5 +743,4 @@ class rcube_user
 
         return $this->db->insert_id('searches');
     }
-
 }
diff --git a/lib/ext/Roundcube/rcube_utils.php b/lib/ext/Roundcube/rcube_utils.php
index 00999ba..a51247e 100644
--- a/lib/ext/Roundcube/rcube_utils.php
+++ b/lib/ext/Roundcube/rcube_utils.php
@@ -103,13 +103,14 @@ class rcube_utils
             }
 
             foreach ($domain_array as $part) {
-                if (!preg_match('/^(([A-Za-z0-9][A-Za-z0-9-]{0,61}[A-Za-z0-9])|([A-Za-z0-9]))$/', $part)) {
+                if (!preg_match('/^((xn--)?([A-Za-z0-9][A-Za-z0-9-]{0,61}[A-Za-z0-9])|([A-Za-z0-9]))$/', $part)) {
                     return false;
                 }
             }
 
             // last domain part
-            if (preg_match('/[^a-zA-Z]/', array_pop($domain_array))) {
+            $last_part = array_pop($domain_array);
+            if (strpos($last_part, 'xn--') !== 0 && preg_match('/[^a-zA-Z]/', $last_part)) {
                 return false;
             }
 
@@ -119,17 +120,6 @@ class rcube_utils
                 return true;
             }
 
-            if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN' && version_compare(PHP_VERSION, '5.3.0', '<')) {
-                $lookup = array();
-                @exec("nslookup -type=MX " . escapeshellarg($domain_part) . " 2>&1", $lookup);
-                foreach ($lookup as $line) {
-                    if (strpos($line, 'MX preference')) {
-                        return true;
-                    }
-                }
-                return false;
-            }
-
             // find MX record(s)
             if (!function_exists('getmxrr') || getmxrr($domain_part, $mx_records)) {
                 return true;
@@ -795,7 +785,7 @@ class rcube_utils
      *
      * @return object DateTime instance or false on failure
      */
-    public static function anytodatetime($date)
+    public static function anytodatetime($date, $timezone = null)
     {
         if (is_object($date) && is_a($date, 'DateTime')) {
             return $date;
@@ -807,7 +797,7 @@ class rcube_utils
         // try to parse string with DateTime first
         if (!empty($date)) {
             try {
-                $dt = new DateTime($date);
+                $dt = new DateTime($date, $timezone);
             }
             catch (Exception $e) {
                 // ignore
@@ -1079,7 +1069,37 @@ class rcube_utils
             return (bool) preg_match('!^[a-z]:[\\\\/]!i', $path);
         }
         else {
-            return $path[0] == DIRECTORY_SEPARATOR;
+            return $path[0] == '/';
+        }
+    }
+
+    /**
+     * Resolve relative URL
+     *
+     * @param string $url Relative URL
+     *
+     * @return string Absolute URL
+     */
+    public static function resolve_url($url)
+    {
+        // prepend protocol://hostname:port
+        if (!preg_match('|^https?://|', $url)) {
+            $schema       = 'http';
+            $default_port = 80;
+
+            if (self::https_check()) {
+                $schema       = 'https';
+                $default_port = 443;
+            }
+
+            $prefix = $schema . '://' . preg_replace('/:\d+$/', '', $_SERVER['HTTP_HOST']);
+            if ($_SERVER['SERVER_PORT'] != $default_port) {
+                $prefix .= ':' . $_SERVER['SERVER_PORT'];
+            }
+
+            $url = $prefix . ($url[0] == '/' ? '' : '/') . $url;
         }
+
+        return $url;
     }
 }
diff --git a/lib/ext/Roundcube/rcube_washtml.php b/lib/ext/Roundcube/rcube_washtml.php
index 9842943..97ab56c 100644
--- a/lib/ext/Roundcube/rcube_washtml.php
+++ b/lib/ext/Roundcube/rcube_washtml.php
@@ -95,6 +95,7 @@ class rcube_washtml
         'ins', 'label', 'legend', 'li', 'map', 'menu', 'nobr', 'ol', 'p', 'pre', 'q',
         's', 'samp', 'small', 'span', 'strike', 'strong', 'sub', 'sup', 'table',
         'tbody', 'td', 'tfoot', 'th', 'thead', 'tr', 'tt', 'u', 'ul', 'var', 'wbr', 'img',
+        'video', 'source',
         // form elements
         'button', 'input', 'textarea', 'select', 'option', 'optgroup'
     );
@@ -246,7 +247,10 @@ class rcube_washtml
                 $quot = strpos($style, '"') !== false ? "'" : '"';
                 $t .= ' style=' . $quot . $style . $quot;
             }
-            else if ($key == 'background' || ($key == 'src' && strtolower($node->tagName) == 'img')) { //check tagName anyway
+            else if ($key == 'background'
+                || ($key == 'src' && preg_match('/^(img|source)$/i', $node->tagName))
+                || ($key == 'poster' && strtolower($node->tagName) == 'video')
+            ) {
                 if (($src = $this->config['cid_map'][$value])
                     || ($src = $this->config['cid_map'][$this->config['base_url'].$value])
                 ) {
@@ -374,7 +378,7 @@ class rcube_washtml
         $this->max_nesting_level = (int) @ini_get('xdebug.max_nesting_level');
 
         // Use optimizations if supported
-        if (version_compare(PHP_VERSION, '5.4.0', '>=')) {
+        if (PHP_VERSION_ID >= 50400) {
             @$node->loadHTML($html, LIBXML_PARSEHUGE | LIBXML_COMPACT);
         }
         else {
diff --git a/lib/ext/tnef_decoder.php b/lib/ext/tnef_decoder.php
deleted file mode 100644
index e6ccc23..0000000
--- a/lib/ext/tnef_decoder.php
+++ /dev/null
@@ -1,331 +0,0 @@
-<?php
-/**
- * The Horde's class allows MS-TNEF data to be displayed.
- *
- * The TNEF rendering is based on code by:
- *   Graham Norbury <gnorbury at bondcar.com>
- * Original design by:
- *   Thomas Boll <tb at boll.ch>, Mark Simpson <damned at world.std.com>
- *
- * Copyright 2002-2010 The Horde Project (http://www.horde.org/)
- *
- * See the enclosed file COPYING for license information (LGPL). If you
- * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
- *
- * @author  Jan Schneider <jan at horde.org>
- * @author  Michael Slusarz <slusarz at horde.org>
- * @package Horde_Compress
- */
-class tnef_decoder
-{
-    const SIGNATURE = 0x223e9f78;
-    const LVL_MESSAGE = 0x01;
-    const LVL_ATTACHMENT = 0x02;
-
-    const ASUBJECT = 0x88004;
-    const AMCLASS = 0x78008;
-    const ATTACHDATA = 0x6800f;
-    const AFILENAME = 0x18010;
-    const ARENDDATA = 0x69002;
-    const AMAPIATTRS = 0x69005;
-    const AVERSION = 0x89006;
-
-    const MAPI_NULL = 0x0001;
-    const MAPI_SHORT = 0x0002;
-    const MAPI_INT = 0x0003;
-    const MAPI_FLOAT = 0x0004;
-    const MAPI_DOUBLE = 0x0005;
-    const MAPI_CURRENCY = 0x0006;
-    const MAPI_APPTIME = 0x0007;
-    const MAPI_ERROR = 0x000a;
-    const MAPI_BOOLEAN = 0x000b;
-    const MAPI_OBJECT = 0x000d;
-    const MAPI_INT8BYTE = 0x0014;
-    const MAPI_STRING = 0x001e;
-    const MAPI_UNICODE_STRING = 0x001f;
-    const MAPI_SYSTIME = 0x0040;
-    const MAPI_CLSID = 0x0048;
-    const MAPI_BINARY = 0x0102;
-
-    const MAPI_ATTACH_LONG_FILENAME = 0x3707;
-    const MAPI_ATTACH_MIME_TAG = 0x370E;
-
-    const MAPI_NAMED_TYPE_ID = 0x0000;
-    const MAPI_NAMED_TYPE_STRING = 0x0001;
-    const MAPI_MV_FLAG = 0x1000;
-
-    /**
-     * Decompress the data.
-     *
-     * @param string $data   The data to decompress.
-     * @param array $params  An array of arguments needed to decompress the
-     *                       data.
-     *
-     * @return mixed  The decompressed data.
-     */
-    public function decompress($data, $params = array())
-    {
-        $out = array();
-
-        if ($this->_geti($data, 32) == self::SIGNATURE) {
-            $this->_geti($data, 16);
-
-            while (strlen($data) > 0) {
-                switch ($this->_geti($data, 8)) {
-                case self::LVL_MESSAGE:
-                    $this->_decodeMessage($data);
-                    break;
-
-                case self::LVL_ATTACHMENT:
-                    $this->_decodeAttachment($data, $out);
-                    break;
-                }
-            }
-        }
-
-        return array_reverse($out);
-    }
-
-    /**
-     * TODO
-     *
-     * @param string &$data  The data string.
-     * @param integer $bits  How many bits to retrieve.
-     *
-     * @return TODO
-     */
-    protected function _getx(&$data, $bits)
-    {
-        $value = null;
-
-        if (strlen($data) >= $bits) {
-            $value = substr($data, 0, $bits);
-            $data = substr_replace($data, '', 0, $bits);
-        }
-
-        return $value;
-    }
-
-    /**
-     * TODO
-     *
-     * @param string &$data  The data string.
-     * @param integer $bits  How many bits to retrieve.
-     *
-     * @return TODO
-     */
-    protected function _geti(&$data, $bits)
-    {
-        $bytes = $bits / 8;
-        $value = null;
-
-        if (strlen($data) >= $bytes) {
-            $value = ord($data[0]);
-            if ($bytes >= 2) {
-                $value += (ord($data[1]) << 8);
-            }
-            if ($bytes >= 4) {
-                $value += (ord($data[2]) << 16) + (ord($data[3]) << 24);
-            }
-            $data = substr_replace($data, '', 0, $bytes);
-        }
-
-        return $value;
-    }
-
-    /**
-     * TODO
-     *
-     * @param string &$data      The data string.
-     * @param string $attribute  TODO
-     */
-    protected function _decodeAttribute(&$data, $attribute)
-    {
-        /* Data. */
-        $this->_getx($data, $this->_geti($data, 32));
-
-        /* Checksum. */
-        $this->_geti($data, 16);
-    }
-
-    /**
-     * TODO
-     *
-     * @param string $data             The data string.
-     * @param array &$attachment_data  TODO
-     */
-    protected function _extractMapiAttributes($data, &$attachment_data)
-    {
-        /* Number of attributes. */
-        $number = $this->_geti($data, 32);
-
-        while ((strlen($data) > 0) && $number--) {
-            $have_mval = false;
-            $num_mval = 1;
-            $named_id = $value = null;
-            $attr_type = $this->_geti($data, 16);
-            $attr_name = $this->_geti($data, 16);
-
-            if (($attr_type & self::MAPI_MV_FLAG) != 0) {
-                $have_mval = true;
-                $attr_type = $attr_type & ~self::MAPI_MV_FLAG;
-            }
-
-            if (($attr_name >= 0x8000) && ($attr_name < 0xFFFE)) {
-                $this->_getx($data, 16);
-                $named_type = $this->_geti($data, 32);
-
-                switch ($named_type) {
-                case self::MAPI_NAMED_TYPE_ID:
-                    $named_id = $this->_geti($data, 32);
-                    $attr_name = $named_id;
-                    break;
-
-                case self::MAPI_NAMED_TYPE_STRING:
-                    $attr_name = 0x9999;
-                    $idlen = $this->_geti($data, 32);
-                    $datalen = $idlen + ((4 - ($idlen % 4)) % 4);
-                    $named_id = substr($this->_getx($data, $datalen), 0, $idlen);
-                    break;
-                }
-            }
-
-            if ($have_mval) {
-                $num_mval = $this->_geti($data, 32);
-            }
-
-            switch ($attr_type) {
-            case self::MAPI_SHORT:
-                $value = $this->_geti($data, 16);
-                break;
-
-            case self::MAPI_INT:
-            case self::MAPI_BOOLEAN:
-                for ($i = 0; $i < $num_mval; $i++) {
-                    $value = $this->_geti($data, 32);
-                }
-                break;
-
-            case self::MAPI_FLOAT:
-            case self::MAPI_ERROR:
-                $value = $this->_getx($data, 4);
-                break;
-
-            case self::MAPI_DOUBLE:
-            case self::MAPI_APPTIME:
-            case self::MAPI_CURRENCY:
-            case self::MAPI_INT8BYTE:
-            case self::MAPI_SYSTIME:
-                $value = $this->_getx($data, 8);
-                break;
-
-            case self::MAPI_STRING:
-            case self::MAPI_UNICODE_STRING:
-            case self::MAPI_BINARY:
-            case self::MAPI_OBJECT:
-                $num_vals = ($have_mval) ? $num_mval : $this->_geti($data, 32);
-                for ($i = 0; $i < $num_vals; $i++) {
-                    $length = $this->_geti($data, 32);
-
-                    /* Pad to next 4 byte boundary. */
-                    $datalen = $length + ((4 - ($length % 4)) % 4);
-
-                    if ($attr_type == self::MAPI_STRING) {
-                        --$length;
-                    }
-
-                    /* Read and truncate to length. */
-                    $value = substr($this->_getx($data, $datalen), 0, $length);
-                }
-                break;
-            }
-
-            /* Store any interesting attributes. */
-            switch ($attr_name) {
-            case self::MAPI_ATTACH_LONG_FILENAME:
-                $value = str_replace("\0", '', $value);
-                /* Used in preference to AFILENAME value. */
-                $attachment_data[0]['name'] = preg_replace('/.*[\/](.*)$/', '\1', $value);
-                break;
-
-            case self::MAPI_ATTACH_MIME_TAG:
-                $value = str_replace("\0", '', $value);
-                /* Is this ever set, and what is format? */
-                $attachment_data[0]['type']    = preg_replace('/^(.*)\/.*/', '\1', $value);
-                $attachment_data[0]['subtype'] = preg_replace('/.*\/(.*)$/', '\1', $value);
-                break;
-            }
-        }
-    }
-
-    /**
-     * TODO
-     *
-     * @param string &$data  The data string.
-     */
-    protected function _decodeMessage(&$data)
-    {
-        $this->_decodeAttribute($data, $this->_geti($data, 32));
-    }
-
-    /**
-     * TODO
-     *
-     * @param string &$data            The data string.
-     * @param array &$attachment_data  TODO
-     */
-    protected function _decodeAttachment(&$data, &$attachment_data)
-    {
-        $attribute = $this->_geti($data, 32);
-
-        switch ($attribute) {
-        case self::ARENDDATA:
-            /* Marks start of new attachment. */
-            $this->_getx($data, $this->_geti($data, 32));
-
-            /* Checksum */
-            $this->_geti($data, 16);
-
-            /* Add a new default data block to hold details of this
-               attachment. Reverse order is easier to handle later! */
-            array_unshift($attachment_data, array('type'    => 'application',
-                                                  'subtype' => 'octet-stream',
-                                                  'name'    => 'unknown',
-                                                  'stream'  => ''));
-            break;
-
-        case self::AFILENAME:
-            $value = $this->_getx($data, $this->_geti($data, 32));
-            $value = str_replace("\0", '', $value);
-            /* Strip path. */
-            $attachment_data[0]['name'] = preg_replace('/.*[\/](.*)$/', '\1', $value);
-
-            /* Checksum */
-            $this->_geti($data, 16);
-            break;
-
-        case self::ATTACHDATA:
-            /* The attachment itself. */
-            $length = $this->_geti($data, 32);
-            $attachment_data[0]['size'] = $length;
-            $attachment_data[0]['stream'] = $this->_getx($data, $length);
-
-            /* Checksum */
-            $this->_geti($data, 16);
-            break;
-
-        case self::AMAPIATTRS:
-            $length = $this->_geti($data, 32);
-            $value = $this->_getx($data, $length);
-
-            /* Checksum */
-            $this->_geti($data, 16);
-            $this->_extractMapiAttributes($value, $attachment_data);
-            break;
-
-        default:
-            $this->_decodeAttribute($data, $attribute);
-        }
-    }
-
-}





More information about the commits mailing list