lib/ext lib/plugins

Aleksander Machniak machniak at kolabsys.com
Fri Aug 22 09:13:54 CEST 2014


 lib/ext/Roundcube/bootstrap.php                                |    2 
 lib/ext/Roundcube/html.php                                     |   25 
 lib/ext/Roundcube/rcube.php                                    |   33 
 lib/ext/Roundcube/rcube_browser.php                            |   16 
 lib/ext/Roundcube/rcube_charset.php                            |   11 
 lib/ext/Roundcube/rcube_config.php                             |   14 
 lib/ext/Roundcube/rcube_contacts.php                           |    2 
 lib/ext/Roundcube/rcube_csv2vcard.php                          |    2 
 lib/ext/Roundcube/rcube_db.php                                 |   83 -
 lib/ext/Roundcube/rcube_db_mssql.php                           |   20 
 lib/ext/Roundcube/rcube_db_mysql.php                           |    4 
 lib/ext/Roundcube/rcube_db_pgsql.php                           |   25 
 lib/ext/Roundcube/rcube_db_sqlsrv.php                          |  121 -
 lib/ext/Roundcube/rcube_html2text.php                          |    7 
 lib/ext/Roundcube/rcube_image.php                              |   20 
 lib/ext/Roundcube/rcube_imap.php                               |    4 
 lib/ext/Roundcube/rcube_imap_cache.php                         |    5 
 lib/ext/Roundcube/rcube_imap_generic.php                       |   59 
 lib/ext/Roundcube/rcube_ldap.php                               |   11 
 lib/ext/Roundcube/rcube_ldap_generic.php                       |   17 
 lib/ext/Roundcube/rcube_mime.php                               |   24 
 lib/ext/Roundcube/rcube_plugin.php                             |    6 
 lib/ext/Roundcube/rcube_plugin_api.php                         |   10 
 lib/ext/Roundcube/rcube_result_index.php                       |   19 
 lib/ext/Roundcube/rcube_result_set.php                         |   30 
 lib/ext/Roundcube/rcube_result_thread.php                      |   35 
 lib/ext/Roundcube/rcube_smtp.php                               |   14 
 lib/ext/Roundcube/rcube_spellcheck_googie.php                  |    4 
 lib/ext/Roundcube/rcube_spellchecker.php                       |    2 
 lib/ext/Roundcube/rcube_storage.php                            |    5 
 lib/ext/Roundcube/rcube_string_replacer.php                    |    2 
 lib/ext/Roundcube/rcube_user.php                               |    4 
 lib/ext/Roundcube/rcube_utils.php                              |   75 -
 lib/ext/Roundcube/rcube_vcard.php                              |   66 
 lib/ext/Roundcube/rcube_washtml.php                            |  111 +
 lib/plugins/kolab_auth/config.inc.php.dist                     |    6 
 lib/plugins/kolab_auth/kolab_auth.php                          |   67 
 lib/plugins/kolab_auth/kolab_auth_ldap.php                     |   52 
 lib/plugins/kolab_auth/localization/en_US.inc                  |    8 
 lib/plugins/kolab_auth/localization/es_ES.inc                  |    7 
 lib/plugins/kolab_auth/localization/et_EE.inc                  |    7 
 lib/plugins/kolab_folders/kolab_folders.js                     |   88 +
 lib/plugins/kolab_folders/kolab_folders.php                    |  103 -
 lib/plugins/kolab_folders/localization/de_CH.inc               |    2 
 lib/plugins/kolab_folders/localization/de_DE.inc               |    2 
 lib/plugins/kolab_folders/localization/en_US.inc               |   10 
 lib/plugins/kolab_folders/localization/es_ES.inc               |    2 
 lib/plugins/kolab_folders/localization/et_EE.inc               |    2 
 lib/plugins/kolab_folders/localization/fr_FR.inc               |    2 
 lib/plugins/kolab_folders/localization/ja_JP.inc               |    2 
 lib/plugins/kolab_folders/localization/nl_NL.inc               |    4 
 lib/plugins/kolab_folders/localization/pl_PL.inc               |    2 
 lib/plugins/kolab_folders/localization/ru_RU.inc               |    2 
 lib/plugins/libkolab/SQL/mysql.initial.sql                     |   48 
 lib/plugins/libkolab/SQL/mysql/2014021000.sql                  |    9 
 lib/plugins/libkolab/SQL/mysql/2014032700.sql                  |    8 
 lib/plugins/libkolab/SQL/mysql/2014040900.sql                  |   16 
 lib/plugins/libkolab/bin/modcache.sh                           |   35 
 lib/plugins/libkolab/bin/randomcontacts.sh                     |  181 ++
 lib/plugins/libkolab/composer.json                             |   30 
 lib/plugins/libkolab/config.inc.php.dist                       |   29 
 lib/plugins/libkolab/js/folderlist.js                          |  264 +++
 lib/plugins/libkolab/lib/kolab_bonnie_api.php                  |   82 +
 lib/plugins/libkolab/lib/kolab_bonnie_api_client.php           |  239 +++
 lib/plugins/libkolab/lib/kolab_format.php                      |   69 
 lib/plugins/libkolab/lib/kolab_format_configuration.php        |  123 +
 lib/plugins/libkolab/lib/kolab_format_contact.php              |   19 
 lib/plugins/libkolab/lib/kolab_format_event.php                |   12 
 lib/plugins/libkolab/lib/kolab_format_file.php                 |    2 
 lib/plugins/libkolab/lib/kolab_format_note.php                 |   40 
 lib/plugins/libkolab/lib/kolab_format_task.php                 |   11 
 lib/plugins/libkolab/lib/kolab_format_xcal.php                 |  201 ++
 lib/plugins/libkolab/lib/kolab_storage.php                     |  534 ++++++-
 lib/plugins/libkolab/lib/kolab_storage_cache.php               |   29 
 lib/plugins/libkolab/lib/kolab_storage_cache_configuration.php |   28 
 lib/plugins/libkolab/lib/kolab_storage_config.php              |  723 ++++++++++
 lib/plugins/libkolab/lib/kolab_storage_folder.php              |  201 --
 lib/plugins/libkolab/lib/kolab_storage_folder_api.php          |  330 ++++
 lib/plugins/libkolab/lib/kolab_storage_folder_user.php         |  111 +
 lib/plugins/libkolab/lib/kolab_storage_folder_virtual.php      |   59 
 lib/plugins/libkolab/libkolab.php                              |   16 
 lib/plugins/libkolab/vendor/finediff.php                       |  688 +++++++++
 lib/plugins/libkolab/vendor/finediff_modifications.diff        |  121 +
 83 files changed, 4781 insertions(+), 733 deletions(-)

New commits:
commit 8374789041933ea20e68fafe4bf98f090602d139
Author: Aleksander Machniak <alec at alec.pl>
Date:   Fri Aug 22 09:13:33 2014 +0200

    Update Roundcube Framework and plugins

diff --git a/lib/ext/Roundcube/bootstrap.php b/lib/ext/Roundcube/bootstrap.php
index 6e51433..27c124a 100644
--- a/lib/ext/Roundcube/bootstrap.php
+++ b/lib/ext/Roundcube/bootstrap.php
@@ -54,7 +54,7 @@ foreach ($config as $optname => $optval) {
 }
 
 // framework constants
-define('RCUBE_VERSION', '1.0-git');
+define('RCUBE_VERSION', '1.0.2');
 define('RCUBE_CHARSET', 'UTF-8');
 
 if (!defined('RCUBE_LIB_DIR')) {
diff --git a/lib/ext/Roundcube/html.php b/lib/ext/Roundcube/html.php
index f6f744c..31bacbf 100644
--- a/lib/ext/Roundcube/html.php
+++ b/lib/ext/Roundcube/html.php
@@ -153,7 +153,7 @@ class html
             $attr = array('src' => $attr);
         }
         return self::tag('img', $attr + array('alt' => ''), null, array_merge(self::$common_attrib,
-            array('src','alt','width','height','border','usemap','onclick')));
+            array('src','alt','width','height','border','usemap','onclick','onerror')));
     }
 
     /**
@@ -269,19 +269,28 @@ class html
             return '';
         }
 
-        $allowed_f = array_flip((array)$allowed);
+        $allowed_f  = array_flip((array)$allowed);
         $attrib_arr = array();
+
         foreach ($attrib as $key => $value) {
             // skip size if not numeric
             if ($key == 'size' && !is_numeric($value)) {
                 continue;
             }
 
-            // ignore "internal" or not allowed attributes
-            if ($key == 'nl' || ($allowed && !isset($allowed_f[$key])) || $value === null) {
+            // ignore "internal" or empty attributes
+            if ($key == 'nl' || $value === null) {
                 continue;
             }
 
+            // ignore not allowed attributes
+            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-*']))) {
+                    continue;
+                }
+            }
+
             // skip empty eventhandlers
             if (preg_match('/^on[a-z]+/', $key) && !$value) {
                 continue;
@@ -677,8 +686,8 @@ class html_table extends html
      */
     public function __construct($attrib = array())
     {
-        $default_attrib = self::$doctype == 'xhtml' ? array('summary' => '', 'border' => 0) : array();
-        $this->attrib = array_merge($attrib, $default_attrib);
+        $default_attrib = self::$doctype == 'xhtml' ? array('summary' => '', 'border' => '0') : array();
+        $this->attrib   = array_merge($attrib, $default_attrib);
 
         if (!empty($attrib['tagname']) && $attrib['tagname'] != 'table') {
           $this->tagname = $attrib['tagname'];
@@ -880,7 +889,7 @@ 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['*'];
+        return $row_tagnames[$this->tagname] ? $row_tagnames[$this->tagname] : $row_tagnames['*'];
     }
 
     /**
@@ -889,7 +898,7 @@ class html_table extends html
     private function _col_tagname()
     {
         static $col_tagnames = array('table' => 'td', '*' => 'span');
-        return $col_tagnames[$this->tagname] ?: $col_tagnames['*'];
+        return $col_tagnames[$this->tagname] ? $col_tagnames[$this->tagname] : $col_tagnames['*'];
     }
 
 }
diff --git a/lib/ext/Roundcube/rcube.php b/lib/ext/Roundcube/rcube.php
index 503e29d..87103be 100644
--- a/lib/ext/Roundcube/rcube.php
+++ b/lib/ext/Roundcube/rcube.php
@@ -1114,7 +1114,20 @@ class rcube
         // log_driver == 'file' is assumed here
 
         $line = sprintf("[%s]: %s\n", $date, $line);
-        $log_dir  = self::$instance ? self::$instance->config->get('log_dir') : null;
+        $log_dir = null;
+
+        // per-user logging is activated
+        if (self::$instance && self::$instance->config->get('per_user_logging', false) && self::$instance->get_user_id()) {
+            $log_dir = self::$instance->get_user_log_dir();
+            if (empty($log_dir))
+                return false;
+        }
+        else if (!empty($log['dir'])) {
+            $log_dir = $log['dir'];
+        }
+        else if (self::$instance) {
+            $log_dir = self::$instance->config->get('log_dir');
+        }
 
         if (empty($log_dir)) {
             $log_dir = RCUBE_INSTALL_PATH . 'logs';
@@ -1352,6 +1365,17 @@ class rcube
         }
     }
 
+    /**
+     * Get the per-user log directory
+     */
+    protected function get_user_log_dir()
+    {
+        $log_dir = $this->config->get('log_dir', RCUBE_INSTALL_PATH . 'logs');
+        $user_name = $this->get_user_name();
+        $user_log_dir = $log_dir . '/' . $user_name;
+
+        return !empty($user_name) && is_writable($user_log_dir) ? $user_log_dir : false;
+    }
 
     /**
      * Getter for logged user language code.
@@ -1414,6 +1438,13 @@ class rcube
         ));
 
         if ($plugin['abort']) {
+            if (!empty($plugin['error'])) {
+                $error = $plugin['error'];
+            }
+            if (!empty($plugin['body_file'])) {
+                $body_file = $plugin['body_file'];
+            }
+
             return isset($plugin['result']) ? $plugin['result'] : false;
         }
 
diff --git a/lib/ext/Roundcube/rcube_browser.php b/lib/ext/Roundcube/rcube_browser.php
index e53e312..b9642d8 100644
--- a/lib/ext/Roundcube/rcube_browser.php
+++ b/lib/ext/Roundcube/rcube_browser.php
@@ -34,14 +34,20 @@ class rcube_browser
         $this->linux = strpos($HTTP_USER_AGENT, 'linux') != false;
         $this->unix  = strpos($HTTP_USER_AGENT, 'unix') != false;
 
-        $this->opera  = strpos($HTTP_USER_AGENT, 'opera') !== false;
+        $this->webkit = strpos($HTTP_USER_AGENT, 'applewebkit') !== false;
+        $this->opera  = strpos($HTTP_USER_AGENT, 'opera') !== false || ($this->webkit && strpos($HTTP_USER_AGENT, 'opr/') !== false);
         $this->ns     = strpos($HTTP_USER_AGENT, 'netscape') !== false;
-        $this->chrome = strpos($HTTP_USER_AGENT, 'chrome') !== false;
+        $this->chrome = !$this->opera && strpos($HTTP_USER_AGENT, 'chrome') !== false;
         $this->ie     = !$this->opera && (strpos($HTTP_USER_AGENT, 'compatible; msie') !== false || strpos($HTTP_USER_AGENT, 'trident/') !== false);
-        $this->safari = !$this->chrome && (strpos($HTTP_USER_AGENT, 'safari') !== false || strpos($HTTP_USER_AGENT, 'applewebkit') !== false);
-        $this->mz     = !$this->ie && !$this->safari && !$this->chrome && !$this->ns && strpos($HTTP_USER_AGENT, 'mozilla') !== false;
+        $this->safari = !$this->opera && !$this->chrome && ($this->webkit || strpos($HTTP_USER_AGENT, 'safari') !== false);
+        $this->mz     = !$this->ie && !$this->safari && !$this->chrome && !$this->ns && !$this->opera && strpos($HTTP_USER_AGENT, 'mozilla') !== false;
 
-        if (preg_match('/(chrome|msie|opera|version|khtml)(\s*|\/)([0-9.]+)/', $HTTP_USER_AGENT, $regs)) {
+        if ($this->opera) {
+            if (preg_match('/(opera|opr)\/([0-9.]+)/', $HTTP_USER_AGENT, $regs)) {
+                $this->ver = (float) $regs[2];
+            }
+        }
+        else if (preg_match('/(chrome|msie|version|khtml)(\s*|\/)([0-9.]+)/', $HTTP_USER_AGENT, $regs)) {
             $this->ver = (float) $regs[3];
         }
         else if (preg_match('/rv:([0-9.]+)/', $HTTP_USER_AGENT, $regs)) {
diff --git a/lib/ext/Roundcube/rcube_charset.php b/lib/ext/Roundcube/rcube_charset.php
index 8612e7f..ffec673 100644
--- a/lib/ext/Roundcube/rcube_charset.php
+++ b/lib/ext/Roundcube/rcube_charset.php
@@ -759,7 +759,12 @@ class rcube_charset
 
         // iconv/mbstring are much faster (especially with long strings)
         if (function_exists('mb_convert_encoding')) {
-            if (($res = mb_convert_encoding($input, 'UTF-8', 'UTF-8')) !== false) {
+            $msch = mb_substitute_character('none');
+            mb_substitute_character('none');
+            $res = mb_convert_encoding($input, 'UTF-8', 'UTF-8');
+            mb_substitute_character($msch);
+
+            if ($res !== false) {
                 return $res;
             }
         }
@@ -795,8 +800,8 @@ class rcube_charset
                 }
                 $seq = '';
                 $out .= $chr;
-            // first (or second) byte of multibyte sequence
             }
+            // first (or second) byte of multibyte sequence
             else if ($ord >= 0xC0) {
                 if (strlen($seq) > 1) {
                     $out .= preg_match($regexp, $seq) ? $seq : '';
@@ -806,8 +811,8 @@ class rcube_charset
                     $seq = '';
                 }
                 $seq .= $chr;
-            // next byte of multibyte sequence
             }
+            // next byte of multibyte sequence
             else if ($seq) {
                 $seq .= $chr;
             }
diff --git a/lib/ext/Roundcube/rcube_config.php b/lib/ext/Roundcube/rcube_config.php
index 0352e47..afe13e8 100644
--- a/lib/ext/Roundcube/rcube_config.php
+++ b/lib/ext/Roundcube/rcube_config.php
@@ -63,7 +63,7 @@ class rcube_config
             $this->paths = explode(PATH_SEPARATOR, $paths);
             // make all paths absolute
             foreach ($this->paths as $i => $path) {
-                if (!$this->_is_absolute($path)) {
+                if (!rcube_utils::is_absolute_path($path)) {
                     if ($realpath = realpath(RCUBE_INSTALL_PATH . $path)) {
                         $this->paths[$i] = unslashify($realpath) . '/';
                     }
@@ -243,8 +243,8 @@ class rcube_config
      */
     public function resolve_paths($file, $use_env = true)
     {
-        $files = array();
-        $abs_path = $this->_is_absolute($file);
+        $files    = array();
+        $abs_path = rcube_utils::is_absolute_path($file);
 
         foreach ($this->paths as $basepath) {
             $realpath = $abs_path ? $file : realpath($basepath . '/' . $file);
@@ -270,14 +270,6 @@ class rcube_config
     }
 
     /**
-     * Determine whether the given file path is absolute or relative
-     */
-    private function _is_absolute($path)
-    {
-        return $path[0] == DIRECTORY_SEPARATOR || preg_match('!^[a-z]:[\\\\/]!i', $path);
-    }
-
-    /**
      * Getter for a specific config parameter
      *
      * @param  string $name Parameter name
diff --git a/lib/ext/Roundcube/rcube_contacts.php b/lib/ext/Roundcube/rcube_contacts.php
index d215760..5e1a40e 100644
--- a/lib/ext/Roundcube/rcube_contacts.php
+++ b/lib/ext/Roundcube/rcube_contacts.php
@@ -264,7 +264,7 @@ class rcube_contacts extends rcube_addressbook
             if ($read_vcard)
                 $sql_arr = $this->convert_db_data($sql_arr);
             else {
-                $sql_arr['email'] = explode(self::SEPARATOR, $sql_arr['email']);
+                $sql_arr['email'] = $sql_arr['email'] ? explode(self::SEPARATOR, $sql_arr['email']) : array();
                 $sql_arr['email'] = array_map('trim', $sql_arr['email']);
             }
 
diff --git a/lib/ext/Roundcube/rcube_csv2vcard.php b/lib/ext/Roundcube/rcube_csv2vcard.php
index aa385dc..06bc387 100644
--- a/lib/ext/Roundcube/rcube_csv2vcard.php
+++ b/lib/ext/Roundcube/rcube_csv2vcard.php
@@ -56,7 +56,7 @@ class rcube_csv2vcard
         //'email_2_type'          => '',
         //'email_3_address'       => '', //@TODO
         //'email_3_type'          => '',
-        'email_address'         => 'email:main',
+        'email_address'         => 'email:pref',
         //'email_type'            => '',
         'first_name'            => 'firstname',
         'gender'                => 'gender',
diff --git a/lib/ext/Roundcube/rcube_db.php b/lib/ext/Roundcube/rcube_db.php
index 2828f26..a46df97 100644
--- a/lib/ext/Roundcube/rcube_db.php
+++ b/lib/ext/Roundcube/rcube_db.php
@@ -31,7 +31,6 @@ class rcube_db
     protected $db_dsnr;               // DSN for read operations
     protected $db_connected = false;  // Already connected ?
     protected $db_mode;               // Connection mode
-    protected $db_table_dsn_map = array();
     protected $dbh;                   // Connection handle
     protected $dbhs = array();
     protected $table_connections = array();
@@ -100,12 +99,15 @@ class rcube_db
         $this->db_dsnw  = $db_dsnw;
         $this->db_dsnr  = $db_dsnr;
         $this->db_pconn = $pconn;
-        $this->db_dsnw_noread = rcube::get_instance()->config->get('db_dsnw_noread', false);
 
         $this->db_dsnw_array = self::parse_dsn($db_dsnw);
         $this->db_dsnr_array = self::parse_dsn($db_dsnr);
 
-        $this->db_table_dsn_map = array_map(array($this, 'table_name'), rcube::get_instance()->config->get('db_table_dsn', array()));
+        $config = rcube::get_instance()->config;
+
+        $this->options['table_prefix']  = $config->get('db_prefix');
+        $this->options['dsnw_noread']   = $config->get('db_dsnw_noread', false);
+        $this->options['table_dsn_map'] = array_map(array($this, 'table_name'), $config->get('db_table_dsn', array()));
     }
 
     /**
@@ -206,7 +208,7 @@ class rcube_db
         // Already connected
         if ($this->db_connected) {
             // connected to db with the same or "higher" mode (if allowed)
-            if ($this->db_mode == $mode || $this->db_mode == 'w' && !$force && !$this->db_dsnw_noread) {
+            if ($this->db_mode == $mode || $this->db_mode == 'w' && !$force && !$this->options['dsnw_noread']) {
                 return;
             }
         }
@@ -241,14 +243,14 @@ class rcube_db
                 $table = $m[2];
 
                 // always use direct mapping
-                if ($this->db_table_dsn_map[$table]) {
-                    $mode = $this->db_table_dsn_map[$table];
+                if ($this->options['table_dsn_map'][$table]) {
+                    $mode = $this->options['table_dsn_map'][$table];
                     break;  // primary table rules
                 }
                 else if ($mode == 'r') {
                     // connected to db with the same or "higher" mode for this table
                     $db_mode = $this->table_connections[$table];
-                    if ($db_mode == 'w' && !$this->db_dsnw_noread) {
+                    if ($db_mode == 'w' && !$this->options['dsnw_noread']) {
                         $mode = $db_mode;
                     }
                 }
@@ -920,14 +922,8 @@ class rcube_db
      */
     public function table_name($table)
     {
-        static $rcube;
-
-        if (!$rcube) {
-            $rcube = rcube::get_instance();
-        }
-
         // add prefix to the table name if configured
-        if (($prefix = $rcube->config->get('db_prefix')) && strpos($table, $prefix) !== 0) {
+        if (($prefix = $this->options['table_prefix']) && strpos($table, $prefix) !== 0) {
             return $prefix . $table;
         }
 
@@ -953,7 +949,7 @@ class rcube_db
      */
     public function set_table_dsn($table, $mode)
     {
-        $this->db_table_dsn_map[$this->table_name($table)] = $mode;
+        $this->options['table_dsn_map'][$this->table_name($table)] = $mode;
     }
 
     /**
@@ -1129,4 +1125,61 @@ class rcube_db
 
         return $result;
     }
+
+    /**
+     * Execute the given SQL script
+     *
+     * @param string SQL queries to execute
+     *
+     * @return boolen True on success, False on error
+     */
+    public function exec_script($sql)
+    {
+        $sql  = $this->fix_table_names($sql);
+        $buff = '';
+
+        foreach (explode("\n", $sql) as $line) {
+            if (preg_match('/^--/', $line) || trim($line) == '')
+                continue;
+
+            $buff .= $line . "\n";
+            if (preg_match('/(;|^GO)$/', trim($line))) {
+                $this->query($buff);
+                $buff = '';
+                if ($this->db_error) {
+                    break;
+                }
+            }
+        }
+
+        return !$this->db_error;
+    }
+
+    /**
+     * Parse SQL file and fix table names according to table prefix
+     */
+    protected function fix_table_names($sql)
+    {
+        if (!$this->options['table_prefix']) {
+            return $sql;
+        }
+
+        $sql = preg_replace_callback(
+            '/((TABLE|TRUNCATE|(?<!ON )UPDATE|INSERT INTO|FROM'
+            . '| ON(?! (DELETE|UPDATE))|REFERENCES|CONSTRAINT|FOREIGN KEY|INDEX)'
+            . '\s+(IF (NOT )?EXISTS )?[`"]*)([^`"\( \r\n]+)/',
+            array($this, 'fix_table_names_callback'),
+            $sql
+        );
+
+        return $sql;
+    }
+
+    /**
+     * Preg_replace callback for fix_table_names()
+     */
+    protected function fix_table_names_callback($matches)
+    {
+        return $matches[1] . $this->options['table_prefix'] . $matches[count($matches)-1];
+    }
 }
diff --git a/lib/ext/Roundcube/rcube_db_mssql.php b/lib/ext/Roundcube/rcube_db_mssql.php
index 726e4b4..4138b14 100644
--- a/lib/ext/Roundcube/rcube_db_mssql.php
+++ b/lib/ext/Roundcube/rcube_db_mssql.php
@@ -167,4 +167,24 @@ class rcube_db_mssql extends rcube_db
 
         return $result;
     }
+
+    /**
+     * Parse SQL file and fix table names according to table prefix
+     */
+    protected function fix_table_names($sql)
+    {
+        if (!$this->options['table_prefix']) {
+            return $sql;
+        }
+
+        // replace sequence names, and other postgres-specific commands
+        $sql = preg_replace_callback(
+            '/((TABLE|(?<!ON )UPDATE|INSERT INTO|FROM(?! deleted)| ON(?! (DELETE|UPDATE|\[PRIMARY\]))'
+            . '|REFERENCES|CONSTRAINT|TRIGGER|INDEX)\s+(\[dbo\]\.)?[\[\]]*)([^\[\]\( \r\n]+)/',
+            array($this, 'fix_table_names_callback'),
+            $sql
+        );
+
+        return $sql;
+    }
 }
diff --git a/lib/ext/Roundcube/rcube_db_mysql.php b/lib/ext/Roundcube/rcube_db_mysql.php
index d3d0ac5..e6417cc 100644
--- a/lib/ext/Roundcube/rcube_db_mysql.php
+++ b/lib/ext/Roundcube/rcube_db_mysql.php
@@ -128,11 +128,11 @@ class rcube_db_mysql extends rcube_db
         $result = array();
 
         if (!empty($dsn['key'])) {
-            $result[PDO::MYSQL_ATTR_KEY] = $dsn['key'];
+            $result[PDO::MYSQL_ATTR_SSL_KEY] = $dsn['key'];
         }
 
         if (!empty($dsn['cipher'])) {
-            $result[PDO::MYSQL_ATTR_CIPHER] = $dsn['cipher'];
+            $result[PDO::MYSQL_ATTR_SSL_CIPHER] = $dsn['cipher'];
         }
 
         if (!empty($dsn['cert'])) {
diff --git a/lib/ext/Roundcube/rcube_db_pgsql.php b/lib/ext/Roundcube/rcube_db_pgsql.php
index 68bf6d8..a92d3cf 100644
--- a/lib/ext/Roundcube/rcube_db_pgsql.php
+++ b/lib/ext/Roundcube/rcube_db_pgsql.php
@@ -73,10 +73,9 @@ class rcube_db_pgsql extends rcube_db
         // Note: we support only one sequence per table
         // Note: The sequence name must be <table_name>_seq
         $sequence = $table . '_seq';
-        $rcube    = rcube::get_instance();
 
-        // return sequence name if configured
-        if ($prefix = $rcube->config->get('db_prefix')) {
+        // modify sequence name if prefix is configured
+        if ($prefix = $this->options['table_prefix']) {
             return $prefix . $sequence;
         }
 
@@ -190,4 +189,24 @@ class rcube_db_pgsql extends rcube_db
         return $result;
     }
 
+    /**
+     * Parse SQL file and fix table names according to table prefix
+     */
+    protected function fix_table_names($sql)
+    {
+        if (!$this->options['table_prefix']) {
+            return $sql;
+        }
+
+        $sql = parent::fix_table_names($sql);
+
+        // replace sequence names, and other postgres-specific commands
+        $sql = preg_replace_callback(
+            '/((SEQUENCE |RENAME TO |nextval\()["\']*)([^"\' \r\n]+)/',
+            array($this, 'fix_table_names_callback'),
+            $sql
+        );
+
+        return $sql;
+    }
 }
diff --git a/lib/ext/Roundcube/rcube_db_sqlsrv.php b/lib/ext/Roundcube/rcube_db_sqlsrv.php
index 4339f3d..7b64cce 100644
--- a/lib/ext/Roundcube/rcube_db_sqlsrv.php
+++ b/lib/ext/Roundcube/rcube_db_sqlsrv.php
@@ -24,126 +24,8 @@
  * @package    Framework
  * @subpackage Database
  */
-class rcube_db_sqlsrv extends rcube_db
+class rcube_db_sqlsrv extends rcube_db_mssql
 {
-    public $db_provider = 'mssql';
-
-    /**
-     * Object constructor
-     *
-     * @param string $db_dsnw DSN for read/write operations
-     * @param string $db_dsnr Optional DSN for read only operations
-     * @param bool   $pconn   Enables persistent connections
-     */
-    public function __construct($db_dsnw, $db_dsnr = '', $pconn = false)
-    {
-        parent::__construct($db_dsnw, $db_dsnr, $pconn);
-
-        $this->options['identifier_start'] = '[';
-        $this->options['identifier_end'] = ']';
-    }
-
-    /**
-     * Driver-specific configuration of database connection
-     *
-     * @param array $dsn DSN for DB connections
-     * @param PDO   $dbh Connection handler
-     */
-    protected function conn_configure($dsn, $dbh)
-    {
-        // Set date format in case of non-default language (#1488918)
-        $dbh->query("SET DATEFORMAT ymd");
-    }
-
-    /**
-     * Return SQL function for current time and date
-     *
-     * @param int $interval Optional interval (in seconds) to add/subtract
-     *
-     * @return string SQL function to use in query
-     */
-    public function now($interval = 0)
-    {
-        if ($interval) {
-            $interval = intval($interval);
-            return "dateadd(second, $interval, getdate())";
-        }
-
-        return "getdate()";
-    }
-
-    /**
-     * Return SQL statement to convert a field value into a unix timestamp
-     *
-     * This method is deprecated and should not be used anymore due to limitations
-     * of timestamp functions in Mysql (year 2038 problem)
-     *
-     * @param string $field Field name
-     *
-     * @return string SQL statement to use in query
-     * @deprecated
-     */
-    public function unixtimestamp($field)
-    {
-        return "DATEDIFF(second, '19700101', $field) + DATEDIFF(second, GETDATE(), GETUTCDATE())";
-    }
-
-    /**
-     * Abstract SQL statement for value concatenation
-     *
-     * @return string SQL statement to be used in query
-     */
-    public function concat(/* col1, col2, ... */)
-    {
-        $args = func_get_args();
-
-        if (is_array($args[0])) {
-            $args = $args[0];
-        }
-
-        return '(' . join('+', $args) . ')';
-    }
-
-    /**
-     * Adds TOP (LIMIT,OFFSET) clause to the query
-     *
-     * @param string $query  SQL query
-     * @param int    $limit  Number of rows
-     * @param int    $offset Offset
-     *
-     * @return string SQL query
-     */
-    protected function set_limit($query, $limit = 0, $offset = 0)
-    {
-        $limit  = intval($limit);
-        $offset = intval($offset);
-        $end    = $offset + $limit;
-
-        // query without OFFSET
-        if (!$offset) {
-            $query = preg_replace('/^SELECT\s/i', "SELECT TOP $limit ", $query);
-            return $query;
-        }
-
-        $orderby = stristr($query, 'ORDER BY');
-        $offset += 1;
-
-        if ($orderby !== false) {
-            $query = trim(substr($query, 0, -1 * strlen($orderby)));
-        }
-        else {
-            // it shouldn't happen, paging without sorting has not much sense
-            // @FIXME: I don't know how to build paging query without ORDER BY
-            $orderby = "ORDER BY 1";
-        }
-
-        $query = preg_replace('/^SELECT\s/i', '', $query);
-        $query = "WITH paging AS (SELECT ROW_NUMBER() OVER ($orderby) AS [RowNumber], $query)"
-            . " SELECT * FROM paging WHERE [RowNumber] BETWEEN $offset AND $end ORDER BY [RowNumber]";
-
-        return $query;
-    }
-
     /**
      * Returns PDO DSN string from DSN array
      */
@@ -158,6 +40,7 @@ class rcube_db_sqlsrv extends rcube_db
             if ($dsn['port']) {
                 $host .= ',' . $dsn['port'];
             }
+
             $params[] = 'Server=' . $host;
         }
 
diff --git a/lib/ext/Roundcube/rcube_html2text.php b/lib/ext/Roundcube/rcube_html2text.php
index 01362e6..8628371 100644
--- a/lib/ext/Roundcube/rcube_html2text.php
+++ b/lib/ext/Roundcube/rcube_html2text.php
@@ -473,6 +473,9 @@ class rcube_html2text
         // Replace known html entities
         $text = html_entity_decode($text, ENT_QUOTES, $this->charset);
 
+        // Replace unicode nbsp to regular spaces
+        $text = preg_replace('/\xC2\xA0/', ' ', $text);
+
         // Remove unknown/unhandled entities (this cannot be done in search-and-replace block)
         $text = preg_replace('/&([a-zA-Z0-9]{2,6}|#[0-9]{2,4});/', '', $text);
 
@@ -616,6 +619,10 @@ class rcube_html2text
 
                     break;
                 }
+                // abort on invalid tag structure (e.g. no closing tag found)
+                else {
+                    break;
+                }
             }
             while ($end || $next);
         }
diff --git a/lib/ext/Roundcube/rcube_image.php b/lib/ext/Roundcube/rcube_image.php
index 4e4caae..122b5f4 100644
--- a/lib/ext/Roundcube/rcube_image.php
+++ b/lib/ext/Roundcube/rcube_image.php
@@ -166,7 +166,7 @@ class rcube_image
             }
             else if($props['gd_type'] == IMAGETYPE_GIF && function_exists('imagecreatefromgif')) {
                 $image = imagecreatefromgif($this->image_file);
-                $type  = 'gid';
+                $type  = 'gif';
             }
             else if($props['gd_type'] == IMAGETYPE_PNG && function_exists('imagecreatefrompng')) {
                 $image = imagecreatefrompng($this->image_file);
@@ -205,6 +205,24 @@ class rcube_image
                 imagecopyresampled($new_image, $image, 0, 0, 0, 0, $width, $height, $props['width'], $props['height']);
                 $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')) {
+                    $exif = exif_read_data($this->image_file);
+                    if ($exif && $exif['Orientation']) {
+                        switch ($exif['Orientation']) {
+                            case 3:
+                                $image = imagerotate($image, 180, 0);
+                                break;
+                            case 6:
+                                $image = imagerotate($image, -90, 0);
+                                break;
+                            case 8:
+                                $image = imagerotate($image, 90, 0);
+                                break;
+                        }
+                    }
+                }
+
                 if ($props['gd_type'] == IMAGETYPE_JPEG) {
                     $result = imagejpeg($image, $filename, 75);
                 }
diff --git a/lib/ext/Roundcube/rcube_imap.php b/lib/ext/Roundcube/rcube_imap.php
index 4c3bf6f..18cf46d 100644
--- a/lib/ext/Roundcube/rcube_imap.php
+++ b/lib/ext/Roundcube/rcube_imap.php
@@ -1444,7 +1444,7 @@ class rcube_imap extends rcube_storage
     public function search_once($folder = null, $str = 'ALL')
     {
         if (!$str) {
-            return 'ALL';
+            $str = 'ALL';
         }
 
         if (!strlen($folder)) {
@@ -1679,7 +1679,7 @@ class rcube_imap extends rcube_storage
             $this->struct_charset = $this->structure_charset($structure);
         }
 
-        $headers->ctype = strtolower($headers->ctype);
+        $headers->ctype = @strtolower($headers->ctype);
 
         // Here we can recognize malformed BODYSTRUCTURE and
         // 1. [@TODO] parse the message in other way to create our own message structure
diff --git a/lib/ext/Roundcube/rcube_imap_cache.php b/lib/ext/Roundcube/rcube_imap_cache.php
index 0c3edea..e49e778 100644
--- a/lib/ext/Roundcube/rcube_imap_cache.php
+++ b/lib/ext/Roundcube/rcube_imap_cache.php
@@ -171,7 +171,7 @@ class rcube_imap_cache
         // Seek in internal cache
         if (array_key_exists('index', $this->icache[$mailbox])) {
             // The index was fetched from database already, but not validated yet
-            if (!array_key_exists('object', $this->icache[$mailbox]['index'])) {
+            if (empty($this->icache[$mailbox]['index']['validated'])) {
                 $index = $this->icache[$mailbox]['index'];
             }
             // We've got a valid index
@@ -248,6 +248,7 @@ class rcube_imap_cache
         }
 
         $this->icache[$mailbox]['index'] = array(
+            'validated'  => true,
             'object'     => $data,
             'sort_field' => $sort_field,
             'modseq'     => !empty($index['modseq']) ? $index['modseq'] : $mbox_data['HIGHESTMODSEQ']
@@ -890,6 +891,8 @@ class rcube_imap_cache
             return false;
         }
 
+        $index['validated'] = true;
+
         // Get mailbox data (UIDVALIDITY, counters, etc.) for status check
         $mbox_data = $this->imap->folder_data($mailbox);
 
diff --git a/lib/ext/Roundcube/rcube_imap_generic.php b/lib/ext/Roundcube/rcube_imap_generic.php
index f9a62f0..f465ac1 100644
--- a/lib/ext/Roundcube/rcube_imap_generic.php
+++ b/lib/ext/Roundcube/rcube_imap_generic.php
@@ -73,6 +73,7 @@ class rcube_imap_generic
     const COMMAND_NORESPONSE = 1;
     const COMMAND_CAPABILITY = 2;
     const COMMAND_LASTLINE   = 4;
+    const COMMAND_ANONYMIZED = 8;
 
     const DEBUG_LINE_LENGTH = 4098; // 4KB + 2B for \r\n
 
@@ -88,16 +89,28 @@ class rcube_imap_generic
      *
      * @param string $string Command string
      * @param bool   $endln  True if CRLF need to be added at the end of command
+     * @param bool   $anonymized Don't write the given data to log but a placeholder
      *
      * @param int Number of bytes sent, False on error
      */
-    function putLine($string, $endln=true)
+    function putLine($string, $endln=true, $anonymized=false)
     {
         if (!$this->fp)
             return false;
 
         if ($this->_debug) {
-            $this->debug('C: '. rtrim($string));
+            // anonymize the sent command for logging
+            $cut = $endln ? 2 : 0;
+            if ($anonymized && preg_match('/^(A\d+ (?:[A-Z]+ )+)(.+)/', $string, $m)) {
+                $log = $m[1] . sprintf('****** [%d]', strlen($m[2]) - $cut);
+            }
+            else if ($anonymized) {
+                $log = sprintf('****** [%d]', strlen($string) - $cut);
+            }
+            else {
+                $log = rtrim($string);
+            }
+            $this->debug('C: ' . $log);
         }
 
         $res = fwrite($this->fp, $string . ($endln ? "\r\n" : ''));
@@ -116,10 +129,11 @@ class rcube_imap_generic
      *
      * @param string $string Command string
      * @param bool   $endln  True if CRLF need to be added at the end of command
+     * @param bool   $anonymized Don't write the given data to log but a placeholder
      *
      * @return int|bool Number of bytes sent, False on error
      */
-    function putLineC($string, $endln=true)
+    function putLineC($string, $endln=true, $anonymized=false)
     {
         if (!$this->fp) {
             return false;
@@ -138,7 +152,7 @@ class rcube_imap_generic
                         $parts[$i+1] = sprintf("{%d+}\r\n", $matches[1]);
                     }
 
-                    $bytes = $this->putLine($parts[$i].$parts[$i+1], false);
+                    $bytes = $this->putLine($parts[$i].$parts[$i+1], false, $anonymized);
                     if ($bytes === false)
                         return false;
                     $res += $bytes;
@@ -153,7 +167,7 @@ class rcube_imap_generic
                     $i++;
                 }
                 else {
-                    $bytes = $this->putLine($parts[$i], false);
+                    $bytes = $this->putLine($parts[$i], false, $anonymized);
                     if ($bytes === false)
                         return false;
                     $res += $bytes;
@@ -519,7 +533,7 @@ class rcube_imap_generic
                 $reply = base64_encode($user . ' ' . $hash);
 
                 // send result
-                $this->putLine($reply);
+                $this->putLine($reply, true, true);
             }
             else {
                 // RFC2831: DIGEST-MD5
@@ -537,7 +551,7 @@ class rcube_imap_generic
                     base64_decode($challenge), $this->host, 'imap', $user));
 
                 // send result
-                $this->putLine($reply);
+                $this->putLine($reply, true, true);
                 $line = trim($this->readReply());
 
                 if ($line[0] == '+') {
@@ -577,7 +591,7 @@ class rcube_imap_generic
             // RFC 4959 (SASL-IR): save one round trip
             if ($this->getCapability('SASL-IR')) {
                 list($result, $line) = $this->execute("AUTHENTICATE PLAIN", array($reply),
-                    self::COMMAND_LASTLINE | self::COMMAND_CAPABILITY);
+                    self::COMMAND_LASTLINE | self::COMMAND_CAPABILITY | self::COMMAND_ANONYMIZED);
             }
             else {
                 $this->putLine($this->nextTag() . " AUTHENTICATE PLAIN");
@@ -588,7 +602,7 @@ class rcube_imap_generic
                 }
 
                 // send result, get reply and process it
-                $this->putLine($reply);
+                $this->putLine($reply, true, true);
                 $line = $this->readReply();
                 $result = $this->parseResult($line);
             }
@@ -619,7 +633,7 @@ class rcube_imap_generic
     function login($user, $password)
     {
         list($code, $response) = $this->execute('LOGIN', array(
-            $this->escape($user), $this->escape($password)), self::COMMAND_CAPABILITY);
+            $this->escape($user), $this->escape($password)), self::COMMAND_CAPABILITY | self::COMMAND_ANONYMIZED);
 
         // re-set capabilities list if untagged CAPABILITY response provided
         if (preg_match('/\* CAPABILITY (.+)/i', $response, $matches)) {
@@ -1840,8 +1854,8 @@ class rcube_imap_generic
                         $result[$id] = '';
                     }
                 } else if ($mode == 2) {
-                    if (preg_match('/(UID|RFC822\.SIZE) ([0-9]+)/', $line, $matches)) {
-                        $result[$id] = trim($matches[2]);
+                    if (preg_match('/' . $index_field . ' ([0-9]+)/', $line, $matches)) {
+                        $result[$id] = trim($matches[1]);
                     } else {
                         $result[$id] = 0;
                     }
@@ -1950,10 +1964,6 @@ class rcube_imap_generic
      */
     private function modFlag($mailbox, $messages, $flag, $mod = '+')
     {
-        if ($mod != '+' && $mod != '-') {
-            $mod = '+';
-        }
-
         if (!$this->select($mailbox)) {
             return false;
         }
@@ -1963,12 +1973,25 @@ class rcube_imap_generic
             return false;
         }
 
+        if ($this->flags[strtoupper($flag)]) {
+            $flag = $this->flags[strtoupper($flag)];
+        }
+
+        if (!$flag || !in_array($flag, (array) $this->data['PERMANENTFLAGS'])
+            || !in_array('\\*', (array) $this->data['PERMANENTFLAGS'])
+        ) {
+            return false;
+        }
+
         // Clear internal status cache
         if ($flag == 'SEEN') {
             unset($this->data['STATUS:'.$mailbox]['UNSEEN']);
         }
 
-        $flag   = $this->flags[strtoupper($flag)];
+        if ($mod != '+' && $mod != '-') {
+            $mod = '+';
+        }
+
         $result = $this->execute('UID STORE', array(
             $this->compressMessageSet($messages), $mod . 'FLAGS.SILENT', "($flag)"),
             self::COMMAND_NORESPONSE);
@@ -3419,7 +3442,7 @@ class rcube_imap_generic
         }
 
         // Send command
-        if (!$this->putLineC($query)) {
+        if (!$this->putLineC($query, true, ($options & self::COMMAND_ANONYMIZED))) {
             $this->setError(self::ERROR_COMMAND, "Unable to send command: $query");
             return $noresp ? self::ERROR_COMMAND : array(self::ERROR_COMMAND, '');
         }
diff --git a/lib/ext/Roundcube/rcube_ldap.php b/lib/ext/Roundcube/rcube_ldap.php
index 0da3e2c..b3872e2 100644
--- a/lib/ext/Roundcube/rcube_ldap.php
+++ b/lib/ext/Roundcube/rcube_ldap.php
@@ -377,10 +377,11 @@ class rcube_ldap extends rcube_addressbook
                 // replace placeholders in filter settings
                 if (!empty($this->prop['filter']))
                     $this->prop['filter'] = strtr($this->prop['filter'], $replaces);
-                if (!empty($this->prop['groups']['filter']))
-                    $this->prop['groups']['filter'] = strtr($this->prop['groups']['filter'], $replaces);
-                if (!empty($this->prop['groups']['member_filter']))
-                    $this->prop['groups']['member_filter'] = strtr($this->prop['groups']['member_filter'], $replaces);
+
+                foreach (array('base_dn','filter','member_filter') as $k) {
+                    if (!empty($this->prop['groups'][$k]))
+                        $this->prop['groups'][$k] = strtr($this->prop['groups'][$k], $replaces);
+                }
 
                 if (!empty($this->prop['group_filters'])) {
                     foreach ($this->prop['group_filters'] as $i => $gf) {
@@ -554,7 +555,7 @@ class rcube_ldap extends rcube_addressbook
         }
         else {
             $prop    = $this->group_id ? $this->group_data : $this->prop;
-            $base_dn = $this->group_id ? $this->group_base_dn : $this->base_dn;
+            $base_dn = $this->group_id ? $prop['base_dn'] : $this->base_dn;
 
             // use global search filter
             if (!empty($this->filter))
diff --git a/lib/ext/Roundcube/rcube_ldap_generic.php b/lib/ext/Roundcube/rcube_ldap_generic.php
index 923a12a..1cd0b5a 100644
--- a/lib/ext/Roundcube/rcube_ldap_generic.php
+++ b/lib/ext/Roundcube/rcube_ldap_generic.php
@@ -175,9 +175,11 @@ class rcube_ldap_generic
         $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))
-                    continue;
+            if ($this->config['use_tls'] === true) {
+                if (!ldap_start_tls($lc)) {
+                    return false;
+                }
+            }
 
             $this->_debug("S: OK");
 
@@ -186,10 +188,13 @@ class rcube_ldap_generic
             $this->conn = $lc;
 
             if (!empty($this->config['network_timeout']))
-              ldap_set_option($lc, LDAP_OPT_NETWORK_TIMEOUT, $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");
@@ -240,7 +245,7 @@ class rcube_ldap_generic
             $method = 'DIGEST-MD5';
         }
 
-        $this->_debug("C: SASL Bind [mech: $method, authc: $authc, authz: $authz, pass: $pass]");
+        $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");
@@ -271,7 +276,7 @@ class rcube_ldap_generic
             return false;
         }
 
-        $this->_debug("C: Bind $dn [pass: $pass]");
+        $this->_debug("C: Bind $dn, pass: **** [" . strlen($pass) . "]");
 
         if (@ldap_bind($this->conn, $dn, $pass)) {
             $this->_debug("S: OK");
diff --git a/lib/ext/Roundcube/rcube_mime.php b/lib/ext/Roundcube/rcube_mime.php
index a931c27..4d43a89 100644
--- a/lib/ext/Roundcube/rcube_mime.php
+++ b/lib/ext/Roundcube/rcube_mime.php
@@ -366,6 +366,9 @@ class rcube_mime
                 $address = 'MAILER-DAEMON';
                 $name    = substr($val, 0, -strlen($m[1]));
             }
+            else if (preg_match('/('.$email_rx.')/', $val, $m)) {
+                $name = $m[1];
+            }
             else {
                 $name = $val;
             }
@@ -378,14 +381,20 @@ class rcube_mime
                 }
                 if ($decode) {
                     $name = self::decode_header($name, $fallback);
+                    // some clients encode addressee name with quotes around it
+                    if ($name[0] == '"' && $name[strlen($name)-1] == '"') {
+                        $name = substr($name, 1, -1);
+                    }
                 }
             }
 
             if (!$address && $name) {
                 $address = $name;
+                $name    = '';
             }
 
             if ($address) {
+                $address      = self::fix_email($address);
                 $result[$key] = array('name' => $name, 'address' => $address);
             }
         }
@@ -881,4 +890,19 @@ class rcube_mime
         return 'image/' . $type;
     }
 
+    /**
+     * Try to fix invalid email addresses
+     */
+    public static function fix_email($email)
+    {
+        $parts = rcube_utils::explode_quoted_string('@', $email);
+        foreach ($parts as $idx => $part) {
+            // remove redundant quoting (#1490040)
+            if ($part[0] == '"' && preg_match('/^"([a-zA-Z0-9._+=-]+)"$/', $part, $m)) {
+                $parts[$idx] = $m[1];
+            }
+        }
+
+        return implode('@', $parts);
+    }
 }
diff --git a/lib/ext/Roundcube/rcube_plugin.php b/lib/ext/Roundcube/rcube_plugin.php
index aa6d837..f0af953 100644
--- a/lib/ext/Roundcube/rcube_plugin.php
+++ b/lib/ext/Roundcube/rcube_plugin.php
@@ -125,13 +125,17 @@ abstract class rcube_plugin
         $fpath = $this->home.'/'.$fname;
         $rcube = rcube::get_instance();
 
-        if (is_file($fpath) && !$rcube->config->load_from_file($fpath)) {
+        if (($is_local = is_file($fpath)) && !$rcube->config->load_from_file($fpath)) {
             rcube::raise_error(array(
                 'code' => 527, 'type' => 'php',
                 'file' => __FILE__, 'line' => __LINE__,
                 'message' => "Failed to load config from $fpath"), true, false);
             return false;
         }
+        else if (!$is_local) {
+            // Search plugin_name.inc.php file in any configured path
+            return $rcube->config->load_from_file($this->ID . '.inc.php');
+        }
 
         return true;
     }
diff --git a/lib/ext/Roundcube/rcube_plugin_api.php b/lib/ext/Roundcube/rcube_plugin_api.php
index 461c3cc..617e921 100644
--- a/lib/ext/Roundcube/rcube_plugin_api.php
+++ b/lib/ext/Roundcube/rcube_plugin_api.php
@@ -182,7 +182,7 @@ class rcube_plugin_api
         }
 
         // plugin already loaded
-        if ($this->plugins[$plugin_name] || class_exists($plugin_name, false)) {
+        if ($this->plugins[$plugin_name]) {
             return true;
         }
 
@@ -190,7 +190,9 @@ class rcube_plugin_api
             . DIRECTORY_SEPARATOR . $plugin_name . '.php';
 
         if (file_exists($fn)) {
-            include $fn;
+            if (!class_exists($plugin_name, false)) {
+                include $fn;
+            }
 
             // instantiate class if exists
             if (class_exists($plugin_name, false)) {
@@ -231,7 +233,7 @@ class rcube_plugin_api
 
     /**
      * Get information about a specific plugin.
-     * This is either provided my a plugin's info() method or extracted from a package.xml or a composer.json file
+     * This is either provided by 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
@@ -277,7 +279,7 @@ class rcube_plugin_api
         include($fn);
 
       if (class_exists($plugin_name))
-        $info = $plugin_name::info();
+        $info = call_user_func(array($plugin_name, 'info'));
 
       // fall back to composer.json file
       if (!$info) {
diff --git a/lib/ext/Roundcube/rcube_result_index.php b/lib/ext/Roundcube/rcube_result_index.php
index 5f592c5..058f25c 100644
--- a/lib/ext/Roundcube/rcube_result_index.php
+++ b/lib/ext/Roundcube/rcube_result_index.php
@@ -231,29 +231,13 @@ class rcube_result_index
 
 
     /**
-     * Filters data set. Removes elements listed in $ids list.
+     * Filters data set. Removes elements not listed in $ids list.
      *
      * @param array $ids List of IDs to remove.
      */
     public function filter($ids = array())
     {
         $data = $this->get();
-        $data = array_diff($data, $ids);
-
-        $this->meta          = array();
-        $this->meta['count'] = count($data);
-        $this->raw_data      = implode(self::SEPARATOR_ELEMENT, $data);
-    }
-
-
-    /**
-     * Filters data set. Removes elements not listed in $ids list.
-     *
-     * @param array $ids List of IDs to keep.
-     */
-    public function intersect($ids = array())
-    {
-        $data = $this->get();
         $data = array_intersect($data, $ids);
 
         $this->meta          = array();
@@ -332,6 +316,7 @@ class rcube_result_index
         if (empty($this->raw_data)) {
             return array();
         }
+
         return explode(self::SEPARATOR_ELEMENT, $this->raw_data);
     }
 
diff --git a/lib/ext/Roundcube/rcube_result_set.php b/lib/ext/Roundcube/rcube_result_set.php
index a4b070e..82502ce 100644
--- a/lib/ext/Roundcube/rcube_result_set.php
+++ b/lib/ext/Roundcube/rcube_result_set.php
@@ -25,7 +25,7 @@
  * @package    Framework
  * @subpackage Addressbook
  */
-class rcube_result_set implements Iterator
+class rcube_result_set implements Iterator, ArrayAccess
 {
     public $count = 0;
     public $first = 0;
@@ -61,6 +61,34 @@ class rcube_result_set implements Iterator
         $this->current = $i;
     }
 
+    /*** Implement PHP ArrayAccess interface ***/
+
+    public function offsetSet($offset, $value)
+    {
+        if (is_null($offset)) {
+            $offset = count($this->records);
+            $this->records[] = $value;
+        }
+        else {
+            $this->records[$offset] = $value;
+        }
+    }
+
+    public function offsetExists($offset)
+    {
+        return isset($this->records[$offset]);
+    }
+
+    public function offsetUnset($offset)
+    {
+        unset($this->records[$offset]);
+    }
+
+    public function offsetGet($offset)
+    {
+        return $this->records[$offset];
+    }
+
     /***  PHP 5 Iterator interface  ***/
 
     function rewind()
diff --git a/lib/ext/Roundcube/rcube_result_thread.php b/lib/ext/Roundcube/rcube_result_thread.php
index 7657550..ceaaf59 100644
--- a/lib/ext/Roundcube/rcube_result_thread.php
+++ b/lib/ext/Roundcube/rcube_result_thread.php
@@ -453,7 +453,7 @@ class rcube_result_thread
 
         // when sorting search result it's good to make the index smaller
         if ($index->count() != $this->count_messages()) {
-            $index->intersect($this->get());
+            $index->filter($this->get());
         }
 
         $result  = array_fill_keys($index->get(), null);
@@ -606,33 +606,39 @@ class rcube_result_thread
         // arrays handling is much more expensive
         // For the following structure: THREAD (2)(3 6 (4 23)(44 7 96))
         // -- 2
-        //
         // -- 3
         //     \-- 6
         //         |-- 4
         //         |    \-- 23
         //         |
         //         \-- 44
-        //              \-- 7
-        //                   \-- 96
+        //               \-- 7
+        //                    \-- 96
         //
         // The output will be: 2,3^1:6^2:4^3:23^2:44^3:7^4:96
 
         if ($str[$begin] != '(') {
-            $stop = $begin + strspn($str, '1234567890', $begin, $end - $begin);
-            $msg  = substr($str, $begin, $stop - $begin);
-            if (!$msg) {
+            // find next bracket
+            $stop      = $begin + strcspn($str, '()', $begin, $end - $begin);
+            $messages  = explode(' ', trim(substr($str, $begin, $stop - $begin)));
+
+            if (empty($messages)) {
                 return $node;
             }
 
-            $this->meta['messages']++;
-
-            $node .= ($depth ? self::SEPARATOR_ITEM.$depth.self::SEPARATOR_LEVEL : '').$msg;
+            foreach ($messages as $msg) {
+                if ($msg) {
+                    $node .= ($depth ? self::SEPARATOR_ITEM.$depth.self::SEPARATOR_LEVEL : '').$msg;
+                    $this->meta['messages']++;
+                    $depth++;
+                }
+            }
 
-            if ($stop + 1 < $end) {
-                $node .= $this->parse_thread($str, $stop + 1, $end, $depth + 1);
+            if ($stop < $end) {
+                $node .= $this->parse_thread($str, $stop, $end, $depth);
             }
-        } else {
+        }
+        else {
             $off = $begin;
             while ($off < $end) {
                 $start = $off;
@@ -649,7 +655,8 @@ class rcube_result_thread
                     if ($p1 !== false && $p1 < $p) {
                         $off = $p1 + 1;
                         $n++;
-                    } else {
+                    }
+                    else {
                         $off = $p + 1;
                         $n--;
                     }
diff --git a/lib/ext/Roundcube/rcube_smtp.php b/lib/ext/Roundcube/rcube_smtp.php
index 60b1389..70f15dc 100644
--- a/lib/ext/Roundcube/rcube_smtp.php
+++ b/lib/ext/Roundcube/rcube_smtp.php
@@ -29,6 +29,7 @@ class rcube_smtp
     private $conn = null;
     private $response;
     private $error;
+    private $anonymize_log = 0;
 
     // define headers delimiter
     const SMTP_MIME_CRLF = "\r\n";
@@ -67,6 +68,7 @@ class rcube_smtp
             'smtp_auth_type' => $rcube->config->get('smtp_auth_type'),
             'smtp_helo_host' => $rcube->config->get('smtp_helo_host'),
             'smtp_timeout'   => $rcube->config->get('smtp_timeout'),
+            'smtp_conn_options'   => $rcube->config->get('smtp_conn_options'),
             'smtp_auth_callbacks' => array(),
         ));
 
@@ -106,10 +108,11 @@ class rcube_smtp
         // IDNA Support
         $smtp_host = rcube_utils::idn_to_ascii($smtp_host);
 
-        $this->conn = new Net_SMTP($smtp_host, $smtp_port, $helo_host);
+        $this->conn = new Net_SMTP($smtp_host, $smtp_port, $helo_host, false, 0, $CONFIG['smtp_conn_options']);
 
         if ($rcube->config->get('smtp_debug')) {
             $this->conn->setDebug(true, array($this, 'debug_handler'));
+            $this->anonymize_log = 0;
         }
 
         // register authentication methods
@@ -329,6 +332,15 @@ class rcube_smtp
      */
     public function debug_handler(&$smtp, $message)
     {
+        // catch AUTH commands and set anonymization flag for subsequent sends
+        if (preg_match('/^Send: AUTH ([A-Z]+)/', $message, $m)) {
+            $this->anonymize_log = $m[1] == 'LOGIN' ? 2 : 1;
+        }
+        // anonymize this log entry
+        else if ($this->anonymize_log > 0 && strpos($message, 'Send:') === 0 && --$this->anonymize_log == 0) {
+            $message = sprintf('Send: ****** [%d]', strlen($message) - 8);
+        }
+
         if (($len = strlen($message)) > self::DEBUG_LINE_LENGTH) {
             $diff    = $len - self::DEBUG_LINE_LENGTH;
             $message = substr($message, 0, self::DEBUG_LINE_LENGTH)
diff --git a/lib/ext/Roundcube/rcube_spellcheck_googie.php b/lib/ext/Roundcube/rcube_spellcheck_googie.php
index 3777942..f9e450f 100644
--- a/lib/ext/Roundcube/rcube_spellcheck_googie.php
+++ b/lib/ext/Roundcube/rcube_spellcheck_googie.php
@@ -56,6 +56,10 @@ class rcube_spellcheck_googie extends rcube_spellcheck_engine
     {
         $this->content = $text;
 
+        if (empty($text)) {
+            return $this->matches = array();
+        }
+
         // spell check uri is configured
         $url = rcube::get_instance()->config->get('spellcheck_uri');
 
diff --git a/lib/ext/Roundcube/rcube_spellchecker.php b/lib/ext/Roundcube/rcube_spellchecker.php
index 5b77bda..e9a3607 100644
--- a/lib/ext/Roundcube/rcube_spellchecker.php
+++ b/lib/ext/Roundcube/rcube_spellchecker.php
@@ -262,7 +262,7 @@ class rcube_spellchecker
     public function is_exception($word)
     {
         // Contain only symbols (e.g. "+9,0", "2:2")
-        if (!$word || preg_match('/^[0-9@#$%^&_+~*=:;?!,.-]+$/', $word))
+        if (!$word || preg_match('/^[0-9@#$%^&_+~*<>=:;?!,.-]+$/', $word))
             return true;
 
         // Contain symbols (e.g. "g@@gle"), all symbols excluding separators
diff --git a/lib/ext/Roundcube/rcube_storage.php b/lib/ext/Roundcube/rcube_storage.php
index ca65af1..c09f053 100644
--- a/lib/ext/Roundcube/rcube_storage.php
+++ b/lib/ext/Roundcube/rcube_storage.php
@@ -613,7 +613,7 @@ abstract class rcube_storage
     /**
      * Parse message UIDs input
      *
-     * @param mixed  $uids  UIDs array or comma-separated list or '*' or '1:*'
+     * @param mixed $uids UIDs array or comma-separated list or '*' or '1:*'
      *
      * @return array Two elements array with UIDs converted to list and ALL flag
      */
@@ -633,6 +633,9 @@ abstract class rcube_storage
             if (is_array($uids)) {
                 $uids = join(',', $uids);
             }
+            else if (strpos($uids, ':')) {
+                $uids = join(',', rcube_imap_generic::uncompressMessageSet($uids));
+            }
 
             if (preg_match('/[^0-9,]/', $uids)) {
                 $uids = '';
diff --git a/lib/ext/Roundcube/rcube_string_replacer.php b/lib/ext/Roundcube/rcube_string_replacer.php
index 77b91d1..ce61e53 100644
--- a/lib/ext/Roundcube/rcube_string_replacer.php
+++ b/lib/ext/Roundcube/rcube_string_replacer.php
@@ -42,7 +42,7 @@ class rcube_string_replacer
         // Support unicode/punycode in top-level domain part
         $utf_domain = '[^?&@"\'\\/()<>\s\r\t\n]+\\.?([^\\x00-\\x2f\\x3b-\\x40\\x5b-\\x60\\x7b-\\x7f]{2,}|xn--[a-zA-Z0-9]{2,})';
         $url1       = '.:;,';
-        $url2       = 'a-zA-Z0-9%=#$@+?|!&\\/_~\\[\\]\\(\\){}\*-';
+        $url2       = 'a-zA-Z0-9%=#$@+?|!&\\/_~\\[\\]\\(\\){}\*\x80-\xFE-';
 
         $this->link_pattern = "/([\w]+:\/\/|\W[Ww][Ww][Ww]\.|^[Ww][Ww][Ww]\.)($utf_domain([$url1]*[$url2]+)*)/";
         $this->mailto_pattern = "/("
diff --git a/lib/ext/Roundcube/rcube_user.php b/lib/ext/Roundcube/rcube_user.php
index 1d5a905..e232736 100644
--- a/lib/ext/Roundcube/rcube_user.php
+++ b/lib/ext/Roundcube/rcube_user.php
@@ -125,8 +125,10 @@ class rcube_user
      */
     function get_prefs()
     {
+        $prefs = array();
+
         if (!empty($this->language))
-            $prefs = array('language' => $this->language);
+            $prefs['language'] = $this->language;
 
         if ($this->ID) {
             // Preferences from session (write-master is unavailable)
diff --git a/lib/ext/Roundcube/rcube_utils.php b/lib/ext/Roundcube/rcube_utils.php
index c48cd80..00999ba 100644
--- a/lib/ext/Roundcube/rcube_utils.php
+++ b/lib/ext/Roundcube/rcube_utils.php
@@ -593,18 +593,18 @@ class rcube_utils
      */
     public static function https_check($port=null, $use_https=true)
     {
-        global $RCMAIL;
-
         if (!empty($_SERVER['HTTPS']) && strtolower($_SERVER['HTTPS']) != 'off') {
             return true;
         }
-        if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO']) && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https') {
+        if (!empty($_SERVER['HTTP_X_FORWARDED_PROTO'])
+            && strtolower($_SERVER['HTTP_X_FORWARDED_PROTO']) == 'https'
+            && in_array($_SERVER['REMOTE_ADDR'], rcube::get_instance()->config->get('proxy_whitelist', array()))) {
             return true;
         }
         if ($port && $_SERVER['SERVER_PORT'] == $port) {
             return true;
         }
-        if ($use_https && isset($RCMAIL) && $RCMAIL->config->get('use_https')) {
+        if ($use_https && rcube::get_instance()->config->get('use_https')) {
             return true;
         }
 
@@ -683,13 +683,22 @@ class rcube_utils
      */
     public static function remote_addr()
     {
-        if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
-            $hosts = explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'], 2);
-            return $hosts[0];
-        }
+        // Check if any of the headers are set first to improve performance
+        if (!empty($_SERVER['HTTP_X_FORWARDED_FOR']) || !empty($_SERVER['HTTP_X_REAL_IP'])) {
+            $proxy_whitelist = rcube::get_instance()->config->get('proxy_whitelist', array());
+            if (in_array($_SERVER['REMOTE_ADDR'], $proxy_whitelist)) {
+                if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) {
+                    foreach(array_reverse(explode(',', $_SERVER['HTTP_X_FORWARDED_FOR'])) as $forwarded_ip) {
+                        if (!in_array($forwarded_ip, $proxy_whitelist)) {
+                            return $forwarded_ip;
+                        }
+                    }
+                }
 
-        if (!empty($_SERVER['HTTP_X_REAL_IP'])) {
-            return $_SERVER['HTTP_X_REAL_IP'];
+                if (!empty($_SERVER['HTTP_X_REAL_IP'])) {
+                    return $_SERVER['HTTP_X_REAL_IP'];
+                }
+            }
         }
 
         if (!empty($_SERVER['REMOTE_ADDR'])) {
@@ -919,7 +928,7 @@ class rcube_utils
 
     /**
      * Normalize the given string for fulltext search.
-     * Currently only optimized for Latin-1 characters; to be extended
+     * Currently only optimized for ISO-8859-1 and ISO-8859-2 characters; to be extended
      *
      * @param string  Input string (UTF-8)
      * @param boolean True to return list of words as array
@@ -940,15 +949,32 @@ class rcube_utils
         // split by words
         $arr = self::tokenize_string($str);
 
+        // detect character set
+        if (utf8_encode(utf8_decode($str)) == $str) {
+            // ISO-8859-1 (or ASCII)
+            preg_match_all('/./u', 'äâàåáãæçéêëèïîìíñöôòøõóüûùúýÿ', $keys);
+            preg_match_all('/./',  'aaaaaaaceeeeiiiinoooooouuuuyy', $values);
+
+            $mapping = array_combine($keys[0], $values[0]);
+            $mapping = array_merge($mapping, array('ß' => 'ss', 'ae' => 'a', 'oe' => 'o', 'ue' => 'u'));
+        }
+        else if (rcube_charset::convert(rcube_charset::convert($str, 'UTF-8', 'ISO-8859-2'), 'ISO-8859-2', 'UTF-8') == $str) {
+            // ISO-8859-2
+            preg_match_all('/./u', 'ąáâäćçčéęëěíîłľĺńňóôöŕřśšşťţůúűüźžżý', $keys);
+            preg_match_all('/./',  'aaaaccceeeeiilllnnooorrsssttuuuuzzzy', $values);
+
+            $mapping = array_combine($keys[0], $values[0]);
+            $mapping = array_merge($mapping, array('ß' => 'ss', 'ae' => 'a', 'oe' => 'o', 'ue' => 'u'));
+        }
+
         foreach ($arr as $i => $part) {
-            if (utf8_encode(utf8_decode($part)) == $part) {  // is latin-1 ?
-                $arr[$i] = utf8_encode(strtr(strtolower(strtr(utf8_decode($part),
-                    'ÇçäâàåéêëèïîìÅÉöôòüûùÿøØáíóúñÑÁÂÀãÃÊËÈÍÎÏÓÔõÕÚÛÙýÝ',
-                    'ccaaaaeeeeiiiaeooouuuyooaiounnaaaaaeeeiiioooouuuyy')),
-                    array('ß' => 'ss', 'ae' => 'a', 'oe' => 'o', 'ue' => 'u')));
+            $part = mb_strtolower($part);
+
+            if (!empty($mapping)) {
+                $part = strtr($part, $mapping);
             }
-            else
-                $arr[$i] = mb_strtolower($part);
+
+            $arr[$i] = $part;
         }
 
         return $as_array ? $arr : join(" ", $arr);
@@ -1030,7 +1056,6 @@ class rcube_utils
         }
     }
 
-
     /**
      * Find out if the string content means true or false
      *
@@ -1045,4 +1070,16 @@ class rcube_utils
         return !in_array($str, array('false', '0', 'no', 'off', 'nein', ''), true);
     }
 
+    /**
+     * OS-dependent absolute path detection
+     */
+    public static function is_absolute_path($path)
+    {
+        if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
+            return (bool) preg_match('!^[a-z]:[\\\\/]!i', $path);
+        }
+        else {
+            return $path[0] == DIRECTORY_SEPARATOR;
+        }
+    }
 }
diff --git a/lib/ext/Roundcube/rcube_vcard.php b/lib/ext/Roundcube/rcube_vcard.php
index a54ee7e..96add11 100644
--- a/lib/ext/Roundcube/rcube_vcard.php
+++ b/lib/ext/Roundcube/rcube_vcard.php
@@ -110,7 +110,7 @@ class rcube_vcard
     public function load($vcard, $charset = RCUBE_CHARSET, $detect = false)
     {
         self::$values_decoded = false;
-        $this->raw = self::vcard_decode($vcard);
+        $this->raw = self::vcard_decode(self::cleanup($vcard));
 
         // resolve charset parameters
         if ($charset == null) {
@@ -149,6 +149,11 @@ class rcube_vcard
             $this->email[0] = $this->email[$pref_index];
             $this->email[$pref_index] = $tmp;
         }
+
+        // fix broken vcards from Outlook that only supply ORG but not the required N or FN properties
+        if (!strlen(trim($this->displayname . $this->surname . $this->firstname)) && strlen($this->organization)) {
+            $this->displayname = $this->organization;
+        }
     }
 
     /**
@@ -491,7 +496,7 @@ class rcube_vcard
 
             if (preg_match('/^END:VCARD$/i', $line)) {
                 // parse vcard
-                $obj = new rcube_vcard(self::cleanup($vcard_block), $charset, true, self::$fieldmap);
+                $obj = new rcube_vcard($vcard_block, $charset, true, self::$fieldmap);
                 // FN and N is required by vCard format (RFC 2426)
                 // on import we can be less restrictive, let's addressbook decide
                 if (!empty($obj->displayname) || !empty($obj->surname) || !empty($obj->firstname) || !empty($obj->email)) {
@@ -527,9 +532,9 @@ class rcube_vcard
         // Cleanup
         $vcard = preg_replace(array(
                 // convert special types (like Skype) to normal type='skype' classes with this simple regex ;)
-                '/item(\d+)\.(TEL|EMAIL|URL)([^:]*?):(.*?)item\1.X-ABLabel:(?:_\$!<)?([\w-() ]*)(?:>!\$_)?./s',
-                '/^item\d*\.X-AB.*$/m',  // remove cruft like item1.X-AB*
-                '/^item\d*\./m',         // remove item1.ADR instead of ADR
+                '/item(\d+)\.(TEL|EMAIL|URL)([^:]*?):(.*?)item\1.X-ABLabel:(?:_\$!<)?([\w-() ]*)(?:>!\$_)?./si',
+                '/^item\d*\.X-AB.*$/mi',  // remove cruft like item1.X-AB*
+                '/^item\d*\./mi',         // remove item1.ADR instead of ADR
                 '/\n+/',                 // remove empty lines
                 '/^(N:[^;\R]*)$/m',      // if N doesn't have any semicolons, add some
             ),
@@ -589,29 +594,34 @@ class rcube_vcard
     private static function vcard_decode($vcard)
     {
         // Perform RFC2425 line unfolding and split lines
-        $vcard = preg_replace(array("/\r/", "/\n\s+/"), '', $vcard);
-        $lines = explode("\n", $vcard);
-        $data  = array();
+        $vcard  = preg_replace(array("/\r/", "/\n\s+/"), '', $vcard);
+        $lines  = explode("\n", $vcard);
+        $result = array();
 
         for ($i=0; $i < count($lines); $i++) {
-            if (!preg_match('/^([^:]+):(.+)$/', $lines[$i], $line))
+            if (!($pos = strpos($lines[$i], ':'))) {
                 continue;
+            }
+
+            $prefix = substr($lines[$i], 0, $pos);
+            $data   = substr($lines[$i], $pos+1);
 
-            if (preg_match('/^(BEGIN|END)$/i', $line[1]))
+            if (preg_match('/^(BEGIN|END)$/i', $prefix)) {
                 continue;
+            }
 
             // convert 2.1-style "EMAIL;internet;home:" to 3.0-style "EMAIL;TYPE=internet;TYPE=home:"
-            if ($data['VERSION'][0] == "2.1"
-                && preg_match('/^([^;]+);([^:]+)/', $line[1], $regs2)
+            if ($result['VERSION'][0] == "2.1"
+                && preg_match('/^([^;]+);([^:]+)/', $prefix, $regs2)
                 && !preg_match('/^TYPE=/i', $regs2[2])
             ) {
-                $line[1] = $regs2[1];
+                $prefix = $regs2[1];
                 foreach (explode(';', $regs2[2]) as $prop) {
-                    $line[1] .= ';' . (strpos($prop, '=') ? $prop : 'TYPE='.$prop);
+                    $prefix .= ';' . (strpos($prop, '=') ? $prop : 'TYPE='.$prop);
                 }
             }
 
-            if (preg_match_all('/([^\\;]+);?/', $line[1], $regs2)) {
+            if (preg_match_all('/([^\\;]+);?/', $prefix, $regs2)) {
                 $entry = array();
                 $field = strtoupper($regs2[1][0]);
                 $enc   = null;
@@ -624,10 +634,10 @@ class rcube_vcard
                             // add next line(s) to value string if QP line end detected
                             if ($value == 'QUOTED-PRINTABLE') {
                                 while (preg_match('/=$/', $lines[$i])) {
-                                    $line[2] .= "\n" . $lines[++$i];
+                                    $data .= "\n" . $lines[++$i];
                                 }
                             }
-                            $enc = $value;
+                            $enc = $value == 'BASE64' ? 'B' : $value;
                         }
                         else {
                             $lc_key = strtolower($key);
@@ -647,20 +657,30 @@ class rcube_vcard
                         // should we use vCard 3.0 instead?
                         // $entry['base64'] = true;
                     }
-                    $line[2] = self::decode_value($line[2], $enc ? $enc : 'base64');
+
+                    $data = self::decode_value($data, $enc ? $enc : 'base64');
+                }
+                else if ($field == 'PHOTO') {
+                    // vCard 4.0 data URI, "PHOTO:data:image/jpeg;base64,..."
+                    if (preg_match('/^data:[a-z\/_-]+;base64,/i', $data, $m)) {
+                        $entry['encoding'] = $enc = 'B';
+                        $data = substr($data, strlen($m[0]));
+                        $data = self::decode_value($data, 'base64');
+                    }
                 }
 
                 if ($enc != 'B' && empty($entry['base64'])) {
-                    $line[2] = self::vcard_unquote($line[2]);
+                    $data = self::vcard_unquote($data);
                 }
 
-                $entry = array_merge($entry, (array) $line[2]);
-                $data[$field][] = $entry;
+                $entry = array_merge($entry, (array) $data);
+                $result[$field][] = $entry;
             }
         }
 
-        unset($data['VERSION']);
-        return $data;
+        unset($result['VERSION']);
+
+        return $result;
     }
 
     /**
diff --git a/lib/ext/Roundcube/rcube_washtml.php b/lib/ext/Roundcube/rcube_washtml.php
index 5a5b3dc..9842943 100644
--- a/lib/ext/Roundcube/rcube_washtml.php
+++ b/lib/ext/Roundcube/rcube_washtml.php
@@ -171,7 +171,7 @@ class rcube_washtml
      */
     private function wash_style($style)
     {
-        $s = '';
+        $result = array();
 
         foreach (explode(';', $style) as $declaration) {
             if (preg_match('/^\s*([a-z\-]+)\s*:\s*(.*)\s*$/i', $declaration, $match)) {
@@ -179,54 +179,48 @@ class rcube_washtml
                 $str   = $match[2];
                 $value = '';
 
-                while (sizeof($str) > 0 &&
-                    preg_match('/^(url\(\s*[\'"]?([^\'"\)]*)[\'"]?\s*\)'./*1,2*/
-                        '|rgb\(\s*[0-9]+\s*,\s*[0-9]+\s*,\s*[0-9]+\s*\)'.
-                        '|-?[0-9.]+\s*(em|ex|px|cm|mm|in|pt|pc|deg|rad|grad|ms|s|hz|khz|%)?'.
-                        '|#[0-9a-f]{3,6}'.
-                        '|[a-z0-9", -]+'.
-                        ')\s*/i', $str, $match)
-                ) {
-                    if ($match[2]) {
-                        if (($src = $this->config['cid_map'][$match[2]])
-                            || ($src = $this->config['cid_map'][$this->config['base_url'].$match[2]])
-                        ) {
-                            $value .= ' url('.htmlspecialchars($src, ENT_QUOTES) . ')';
-                        }
-                        else if (preg_match('!^(https?:)?//[a-z0-9/._+-]+$!i', $match[2], $url)) {
-                            if ($this->config['allow_remote']) {
-                                $value .= ' url('.htmlspecialchars($url[0], ENT_QUOTES).')';
+                foreach ($this->explode_style($str) as $val) {
+                    if (preg_match('/^url\(/i', $val)) {
+                        if (preg_match('/^url\(\s*[\'"]?([^\'"\)]*)[\'"]?\s*\)/iu', $val, $match)) {
+                            $url = $match[1];
+                            if (($src = $this->config['cid_map'][$url])
+                                || ($src = $this->config['cid_map'][$this->config['base_url'].$url])
+                            ) {
+                                $value .= ' url('.htmlspecialchars($src, ENT_QUOTES) . ')';
                             }
-                            else {
-                                $this->extlinks = true;
+                            else if (preg_match('!^(https?:)?//[a-z0-9/._+-]+$!i', $url, $m)) {
+                                if ($this->config['allow_remote']) {
+                                    $value .= ' url('.htmlspecialchars($m[0], ENT_QUOTES).')';
+                                }
+                                else {
+                                    $this->extlinks = true;
+                                }
+                            }
+                            else if (preg_match('/^data:.+/i', $url)) { // RFC2397
+                                $value .= ' url('.htmlspecialchars($url, ENT_QUOTES).')';
                             }
-                        }
-                        else if (preg_match('/^data:.+/i', $match[2])) { // RFC2397
-                            $value .= ' url('.htmlspecialchars($match[2], ENT_QUOTES).')';
                         }
                     }
-                    else {
+                    else if (!preg_match('/^(behavior|expression)/i', $val)) {
                         // whitelist ?
-                        $value .= ' ' . $match[0];
+                        $value .= ' ' . $val;
 
                         // #1488535: Fix size units, so width:800 would be changed to width:800px
-                        if (preg_match('/(left|right|top|bottom|width|height)/i', $cssid)
-                            && preg_match('/^[0-9]+$/', $match[0])
+                        if (preg_match('/^(left|right|top|bottom|width|height)/i', $cssid)
+                            && preg_match('/^[0-9]+$/', $val)
                         ) {
                             $value .= 'px';
                         }
                     }
-
-                    $str = substr($str, strlen($match[0]));
                 }
 
                 if (isset($value[0])) {
-                    $s .= ($s?' ':'') . $cssid . ':' . $value . ';';
+                    $result[] = $cssid . ':' . $value;
                 }
             }
         }
 
-        return $s;
+        return implode('; ', $result);
     }
 
     /**
@@ -283,10 +277,12 @@ class rcube_washtml
 
     /**
      * The main loop that recurse on a node tree.
-     * It output only allowed tags with allowed attributes
-     * and allowed inline styles
+     * It output only allowed tags with allowed attributes and allowed inline styles
+     *
+     * @param DOMNode $node  HTML element
+     * @param int     $level Recurrence level (safe initial value found empirically)
      */
-    private function dumpHtml($node, $level = 0)
+    private function dumpHtml($node, $level = 20)
     {
         if (!$node->hasChildNodes()) {
             return '';
@@ -460,7 +456,7 @@ class rcube_washtml
         // Remove invalid HTML comments (#1487759)
         // Don't remove valid conditional comments
         // Don't remove MSOutlook (<!-->) conditional comments (#1489004)
-        $html = preg_replace('/<!--[^->\[\n]+>/', '', $html);
+        $html = preg_replace('/<!--[^-<>\[\n]+>/', '', $html);
 
         // fix broken nested lists
         self::fix_broken_lists($html);
@@ -576,4 +572,49 @@ class rcube_washtml
             }
         }
     }
+
+    /**
+     * Explode css style value
+     */
+    protected function explode_style($style)
+    {
+        $style = trim($style);
+
+        // first remove comments
+        $pos = 0;
+        while (($pos = strpos($style, '/*', $pos)) !== false) {
+            $end = strpos($style, '*/', $pos+2);
+
+            if ($end === false) {
+                $style = substr($style, 0, $pos);
+            }
+            else {
+                $style = substr_replace($style, '', $pos, $end - $pos + 2);
+            }
+        }
+
+        $strlen = strlen($style);
+        $result = array();
+
+        // explode value
+        for ($p=$i=0; $i < $strlen; $i++) {
+            if (($style[$i] == "\"" || $style[$i] == "'") && $style[$i-1] != "\\") {
+                if ($q == $style[$i]) {
+                    $q = false;
+                }
+                else if (!$q) {
+                    $q = $style[$i];
+                }
+            }
+
+            if (!$q && $style[$i] == ' ' && !preg_match('/[,\(]/', $style[$i-1])) {
+                $result[] = substr($style, $p, $i - $p);
+                $p = $i + 1;
+            }
+        }
+
+        $result[] = (string) substr($style, $p);
+
+        return $result;
+    }
 }
diff --git a/lib/plugins/kolab_auth/config.inc.php.dist b/lib/plugins/kolab_auth/config.inc.php.dist
index e7b9d15..785fb78 100644
--- a/lib/plugins/kolab_auth/config.inc.php.dist
+++ b/lib/plugins/kolab_auth/config.inc.php.dist
@@ -29,12 +29,16 @@ $rcmail_config['kolab_auth_name']         = array('name', 'cn');
 $rcmail_config['kolab_auth_email']        = array('email');
 $rcmail_config['kolab_auth_organization'] = array('organization');
 
+// Template for user names displayed in the UI.
+// You can use all attributes from the 'fieldmap' property of the 'kolab_auth_addressbook' configuration
+$rcmail_config['kolab_auth_user_displayname'] = '{name} ({ou})';
+
 // Login and password of the admin user. Enables "Login As" feature.
 $rcmail_config['kolab_auth_admin_login']    = '';
 $rcmail_config['kolab_auth_admin_password'] = '';
 
 // Enable audit logging for abuse of administrative privileges.
-$rcmail_config['kolab_auth_auditlog'] = true;
+$rcmail_config['kolab_auth_auditlog'] = false;
 
 // Role field (from fieldmap configuration)
 $rcmail_config['kolab_auth_role'] = 'role';
diff --git a/lib/plugins/kolab_auth/kolab_auth.php b/lib/plugins/kolab_auth/kolab_auth.php
index 79b1018..2b685a7 100644
--- a/lib/plugins/kolab_auth/kolab_auth.php
+++ b/lib/plugins/kolab_auth/kolab_auth.php
@@ -31,6 +31,7 @@
 class kolab_auth extends rcube_plugin
 {
     static $ldap;
+    private $username;
     private $data = array();
 
     public function init()
@@ -56,11 +57,12 @@ class kolab_auth extends rcube_plugin
         // Hook to modify some configuration, e.g. ldap
         $this->add_hook('config_get', array($this, 'config_get'));
 
-        // Enable debug logs per-user, this enables logging only after
-        // user has logged in
-        if (!empty($_SESSION['username']) && $rcmail->config->get('kolab_auth_auditlog')) {
-            $this->add_hook('write_log', array($this, 'write_log'));
+        // Hook to modify logging directory
+        $this->add_hook('write_log', array($this, 'write_log'));
+        $this->username = $_SESSION['username'];
 
+        // Enable debug logs (per-user), when logged as another user
+        if (!empty($_SESSION['kolab_auth_admin']) && $rcmail->config->get('kolab_auth_auditlog')) {
             $rcmail->config->set('debug_level', 1);
             $rcmail->config->set('devel_mode', true);
             $rcmail->config->set('smtp_log', true);
@@ -166,8 +168,9 @@ class kolab_auth extends rcube_plugin
 
         if (!empty($role_settings)) {
             foreach ($role_settings as $role_dn => $settings) {
+                $role_dn = self::parse_ldap_vars($role_dn);
                 if (!empty($role_settings[$role_dn])) {
-                        $role_settings[$role_dn] = array_merge((array)$role_settings[$role_dn], $settings);
+                    $role_settings[$role_dn] = array_merge((array)$role_settings[$role_dn], $settings);
                 } else {
                     $role_settings[$role_dn] = $settings;
                 }
@@ -223,7 +226,7 @@ class kolab_auth extends rcube_plugin
 
             if (!empty($role_plugins[$role_dn])) {
                 foreach ((array)$role_plugins[$role_dn] as $plugin) {
-                    $this->require_plugin($plugin);
+                    $this->api->load_plugin($plugin);
                 }
             }
         }
@@ -241,37 +244,29 @@ class kolab_auth extends rcube_plugin
             return $args;
         }
 
-        $line = sprintf("[%s]: %s\n", $args['date'], $args['line']);
-
         // log_driver == 'file' is assumed here
         $log_dir  = $rcmail->config->get('log_dir', RCUBE_INSTALL_PATH . 'logs');
-        $log_path = $log_dir.'/'.strtolower($_SESSION['kolab_auth_admin']).'/'.strtolower($_SESSION['username']);
 
-        // Append original username + target username
-        if (!is_dir($log_path)) {
+        // Append original username + target username for audit-logging
+        if ($rcmail->config->get('kolab_auth_auditlog') && !empty($_SESSION['kolab_auth_admin'])) {
+            $args['dir'] = $log_dir . '/' . strtolower($_SESSION['kolab_auth_admin']) . '/' . strtolower($this->username);
+
             // Attempt to create the directory
-            if (@mkdir($log_path, 0750, true)) {
-                $log_dir = $log_path;
+            if (!is_dir($args['dir'])) {
+                @mkdir($args['dir'], 0750, true);
             }
         }
-        else {
-            $log_dir = $log_path;
-        }
-
-        // try to open specific log file for writing
-        $logfile = $log_dir.'/'.$args['name'];
-
-        if ($fp = fopen($logfile, 'a')) {
-            fwrite($fp, $line);
-            fflush($fp);
-            fclose($fp);
-        }
-        else {
-            trigger_error("Error writing to log file $logfile; Please check permissions", E_USER_WARNING);
+        // Define the user log directory if a username is provided
+        else if ($rcmail->config->get('per_user_logging') && !empty($this->username)) {
+            $user_log_dir = $log_dir . '/' . strtolower($this->username);
+            if (is_writable($user_log_dir)) {
+                $args['dir'] = $user_log_dir;
+            }
+            else if ($args['name'] != 'errors') {
+                $args['abort'] = true;  // don't log if unauthenticed
+            }
         }
 
-        $args['abort'] = true;
-
         return $args;
     }
 
@@ -353,6 +348,9 @@ class kolab_auth extends rcube_plugin
             return $args;
         }
 
+        // temporarily set the current username to the one submitted
+        $this->username = $user;
+
         $ldap = self::ldap();
         if (!$ldap || !$ldap->ready) {
             $args['abort'] = true;
@@ -483,7 +481,7 @@ class kolab_auth extends rcube_plugin
                 return $args;
             }
 
-            $args['user'] = $loginas;
+            $args['user'] = $this->username = $loginas;
 
             // Mark session to use SASL proxy for IMAP authentication
             $_SESSION['kolab_auth_admin']    = strtolower($origname);
@@ -506,7 +504,7 @@ class kolab_auth extends rcube_plugin
             $this->data['user_login'] = is_array($record[$login_attr]) ? $record[$login_attr][0] : $record[$login_attr];
         }
         if ($this->data['user_login']) {
-            $args['user'] = $this->data['user_login'];
+            $args['user'] = $this->username = $this->data['user_login'];
         }
 
         // User name for identity (first log in)
@@ -539,6 +537,9 @@ class kolab_auth extends rcube_plugin
                 $args['user'], $origname, rcube_utils::remote_ip()));
         }
 
+        // load per-user settings/plugins
+        $this->load_user_role_plugins_and_settings();
+
         return $args;
     }
 
@@ -583,8 +584,8 @@ class kolab_auth extends rcube_plugin
             $admin_login = $rcmail->decrypt($_SESSION['kolab_auth_login']);
             $admin_pass  = $rcmail->decrypt($_SESSION['kolab_auth_password']);
 
-            $args['options']['smtp_auth_cid'] = $admin_login;
-            $args['options']['smtp_auth_pw']  = $admin_pass;
+            $args['smtp_auth_cid'] = $admin_login;
+            $args['smtp_auth_pw']  = $admin_pass;
         }
 
         return $args;
diff --git a/lib/plugins/kolab_auth/kolab_auth_ldap.php b/lib/plugins/kolab_auth/kolab_auth_ldap.php
index 2f2ca14..303bbf3 100644
--- a/lib/plugins/kolab_auth/kolab_auth_ldap.php
+++ b/lib/plugins/kolab_auth/kolab_auth_ldap.php
@@ -28,17 +28,22 @@
 class kolab_auth_ldap extends rcube_ldap_generic
 {
     private $icache = array();
+    private $conf = array();
+    private $fieldmap = array();
 
 
     function __construct($p)
     {
         $rcmail = rcube::get_instance();
 
-        $this->debug    = (bool) $rcmail->config->get('ldap_debug');
+        $this->conf = $p;
+        $this->conf['kolab_auth_user_displayname'] = $rcmail->config->get('kolab_auth_user_displayname', '{name}');
+
         $this->fieldmap = $p['fieldmap'];
         $this->fieldmap['uid'] = 'uid';
 
         $p['attributes'] = array_values($this->fieldmap);
+        $p['debug']      = (bool) $rcmail->config->get('ldap_debug');
 
         // Connect to the server (with bind)
         parent::__construct($p);
@@ -154,7 +159,7 @@ class kolab_auth_ldap extends rcube_ldap_generic
         foreach ($result as $entry) {
             $entry = rcube_ldap_generic::normalize_entry($entry);
             if (!$entry['dn']) {
-                $entry['dn'] = $result->get_dn();
+                $entry['dn'] = key($result->entries(true));
             }
             $groups[$entry['dn']] = $entry[$name_attr];
         }
@@ -197,7 +202,9 @@ class kolab_auth_ldap extends rcube_ldap_generic
         foreach ($this->fieldmap as $field => $attr) {
             if (array_key_exists($field, $entry)) {
                 $entry[$attr] = $entry[$field];
-                unset($entry[$field]);
+                if ($attr != $field) {
+                    unset($entry[$field]);
+                }
             }
         }
 
@@ -213,13 +220,13 @@ class kolab_auth_ldap extends rcube_ldap_generic
      *                          0 - partial (*abc*),
      *                          1 - strict (=),
      *                          2 - prefix (abc*)
-     * @param boolean $select   True if results are requested, False if count only
      * @param array   $required List of fields that cannot be empty
      * @param int     $limit    Number of records
+     * @param int     $count    Returns the number of records found
      *
      * @return array List or false on error
      */
-    function search($fields, $value, $mode=1, $required = array(), $limit = 0)
+    function dosearch($fields, $value, $mode=1, $required = array(), $limit = 0, &$count = 0)
     {
         if (empty($fields)) {
             return array();
@@ -296,13 +303,14 @@ class kolab_auth_ldap extends rcube_ldap_generic
         $attrs   = array_values($this->fieldmap);
         $list    = array();
 
-        if ($result = parent::search($base_dn, $filter, $scope, $attrs)) {
+        if ($result = $this->search($base_dn, $filter, $scope, $attrs)) {
+            $count = $result->count();
             $i = 0;
             foreach ($result as $entry) {
                 if ($limit && $limit <= $i) {
                     break;
                 }
-                $dn        = $result->get_dn();
+                $dn        = key($result->entries(true));
                 $entry     = rcube_ldap_generic::normalize_entry($entry);
                 $list[$dn] = $this->field_mapping($dn, $entry);
                 $i++;
@@ -329,11 +337,26 @@ class kolab_auth_ldap extends rcube_ldap_generic
 
         // fields mapping
         foreach ($this->fieldmap as $field => $attr) {
-            if (isset($entry[$attr])) {
+            // $entry might be indexed by lower-case attribute names
+            $attr_lc = strtolower($attr);
+            if (isset($entry[$attr_lc])) {
+                $entry[$field] = $entry[$attr_lc];
+            }
+            else if (isset($entry[$attr])) {
                 $entry[$field] = $entry[$attr];
             }
         }
 
+        // compose display name according to config
+        if (empty($this->fieldmap['displayname'])) {
+            $entry['displayname'] = rcube_addressbook::compose_search_name(
+                $entry,
+                $entry['email'],
+                $entry['name'],
+                $this->conf['kolab_auth_user_displayname']
+            );
+        }
+
         return $entry;
     }
 
@@ -488,6 +511,19 @@ class kolab_auth_ldap extends rcube_ldap_generic
     }
 
     /**
+     * Register additional fields
+     */
+    public function extend_fieldmap($map)
+    {
+        foreach ((array)$map as $name => $attr) {
+            if (!in_array($attr, $this->attributes)) {
+                $this->attributes[]    = $attr;
+                $this->fieldmap[$name] = $attr;
+            }
+        }
+    }
+
+    /**
      * HTML-safe DN string encoding
      *
      * @param string $str DN string
diff --git a/lib/plugins/kolab_auth/localization/en_US.inc b/lib/plugins/kolab_auth/localization/en_US.inc
index e1adb3f..2a7b246 100644
--- a/lib/plugins/kolab_auth/localization/en_US.inc
+++ b/lib/plugins/kolab_auth/localization/en_US.inc
@@ -1,5 +1,13 @@
 <?php
 
+/**
+ * Localizations for the Kolab Auth plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_auth/
+ */
+
 $labels['loginas'] = 'Login As';
 
 ?>
diff --git a/lib/plugins/kolab_auth/localization/es_ES.inc b/lib/plugins/kolab_auth/localization/es_ES.inc
index acb6c35..ed203e6 100644
--- a/lib/plugins/kolab_auth/localization/es_ES.inc
+++ b/lib/plugins/kolab_auth/localization/es_ES.inc
@@ -1,2 +1,9 @@
 <?php
+/**
+ * Localizations for the Kolab Auth plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_auth/
+ */
 ?>
diff --git a/lib/plugins/kolab_auth/localization/et_EE.inc b/lib/plugins/kolab_auth/localization/et_EE.inc
index acb6c35..ed203e6 100644
--- a/lib/plugins/kolab_auth/localization/et_EE.inc
+++ b/lib/plugins/kolab_auth/localization/et_EE.inc
@@ -1,2 +1,9 @@
 <?php
+/**
+ * Localizations for the Kolab Auth plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_auth/
+ */
 ?>
diff --git a/lib/plugins/kolab_folders/kolab_folders.js b/lib/plugins/kolab_folders/kolab_folders.js
index 7cabfdd..ac50543 100644
--- a/lib/plugins/kolab_folders/kolab_folders.js
+++ b/lib/plugins/kolab_folders/kolab_folders.js
@@ -1,9 +1,11 @@
 /**
  * Client script for the Kolab folder management/listing extension
  *
- * @version @package_version@
  * @author Aleksander Machniak <machniak at kolabsys.com>
  *
+ * @licstart  The following is the entire license notice for the
+ * JavaScript code in this file.
+ *
  * Copyright (C) 2011, Kolab Systems AG <contact at kolabsys.com>
  *
  * This program is free software: you can redistribute it and/or modify
@@ -18,9 +20,36 @@
  *
  * You should have received a copy of the GNU Affero General Public License
  * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @licend  The above is the entire license notice
+ * for the JavaScript code in this file.
  */
 
-$(document).ready(function() {
+window.rcmail && rcmail.env.action == 'folders' && rcmail.addEventListener('init', function() {
+    var filter = $(rcmail.gui_objects.foldersfilter),
+        optgroup = $('<optgroup>').attr('label', rcmail.gettext('kolab_folders.folderctype'));
+
+    // remove disabled namespaces
+    filter.children('option').each(function(i, opt) {
+        $.each(rcmail.env.skip_roots || [], function() {
+            if (opt.value == this) {
+                $(opt).remove();
+            }
+        });
+    });
+
+    // add type options to the filter
+    $.each(rcmail.env.foldertypes, function() {
+        optgroup.append($('<option>').attr('value', 'type-' + this).text(rcmail.gettext('kolab_folders.foldertype' + this)));
+    });
+
+    // overwrite default onchange handler
+    filter.attr('onchange', '')
+        .on('change', function() { return kolab_folders_filter(this.value); })
+        .append(optgroup);
+});
+
+window.rcmail && rcmail.env.action != 'folders' && $(document).ready(function() {
     // IE doesn't allow setting OPTION's display/visibility
     // We'll need to remove SELECT's options, see below
     if (bw.ie) {
@@ -63,3 +92,58 @@ $(document).ready(function() {
         }
     }).change();
 });
+
+function kolab_folders_filter(filter)
+{
+    var type = filter.match(/^type-([a-z]+)$/) ? RegExp.$1 : null;
+
+    rcmail.subscription_list.reset_search();
+
+    if (!type) {
+        // clear type filter
+        if (rcmail.folder_filter_type) {
+            $('li', rcmail.subscription_list.container).removeData('filtered').show();
+            rcmail.folder_filter_type = null;
+        }
+
+        // apply namespace filter
+        rcmail.folder_filter(filter);
+    }
+    else {
+        rcmail.folder_filter_type = type;
+        rcmail.subscription_list.container.children('li').each(function() {
+            kolab_folder_filter_match(this, type);
+        });
+    }
+
+    return false;
+}
+
+function kolab_folder_filter_match(elem, type)
+{
+    var found = 0, cl = elem.className || '',
+        $elem = $(elem),
+        children = $('ul', elem).children('li');
+
+    // subfolders...
+    children.each(function() {
+        found += kolab_folder_filter_match(this, type);
+    });
+
+    if (found || cl.match(new RegExp('type-' + type))
+        || (type == 'mail' && !children.length && !cl.match(/(^| )type-([a-z]+)/))
+    ) {
+        if (found || !$elem.is('.virtual')) {
+            found++;
+        }
+    }
+
+    if (found) {
+        $elem.removeData('filtered').show();
+    }
+    else {
+        $elem.data('filtered', true).hide();
+    }
+
+    return found;
+}
diff --git a/lib/plugins/kolab_folders/kolab_folders.php b/lib/plugins/kolab_folders/kolab_folders.php
index 6de4ed3..510388c 100644
--- a/lib/plugins/kolab_folders/kolab_folders.php
+++ b/lib/plugins/kolab_folders/kolab_folders.php
@@ -104,6 +104,32 @@ class kolab_folders extends rcube_plugin
             return $args;
         }
 
+        // load translations
+        $this->add_texts('localization/', false);
+
+        // Add javascript script to the client
+        $this->include_script('kolab_folders.js');
+
+        $this->add_label('folderctype');
+        foreach ($this->types as $type) {
+            $this->add_label('foldertype' . $type);
+        }
+
+        $skip_namespace = $this->rc->config->get('kolab_skip_namespace');
+        $skip_roots     = array();
+
+        if (!empty($skip_namespace)) {
+            $storage = $this->rc->get_storage();
+            foreach ((array)$skip_namespace as $ns) {
+                foreach((array)$storage->get_namespace($ns) as $root) {
+                    $skip_roots[] = rtrim($root[0], $root[1]);
+                }
+            }
+        }
+
+        $this->rc->output->set_env('skip_roots', $skip_roots);
+        $this->rc->output->set_env('foldertypes', $this->types);
+
         // get folders types
         $folderdata = kolab_storage::folders_typedata();
 
@@ -111,23 +137,37 @@ class kolab_folders extends rcube_plugin
             return $args;
         }
 
-        $table = $args['table'];
-
         // Add type-based style for table rows
         // See kolab_folders::folder_class_name()
-        for ($i=1, $cnt=$table->size(); $i<=$cnt; $i++) {
-            $attrib = $table->get_row_attribs($i);
-            $folder = $attrib['foldername']; // UTF7-IMAP
-            $type   = $folderdata[$folder];
+        if ($table = $args['table']) {
+            for ($i=1, $cnt=$table->size(); $i<=$cnt; $i++) {
+                $attrib = $table->get_row_attribs($i);
+                $folder = $attrib['foldername']; // UTF7-IMAP
+                $type   = $folderdata[$folder];
+
+                if (!$type) {
+                    $type = 'mail';
+                }
 
-            if (!$type) {
-                $type = 'mail';
+                $class_name = self::folder_class_name($type);
+                $attrib['class'] = trim($attrib['class'] . ' ' . $class_name);
+                $table->set_row_attribs($attrib, $i);
             }
+        }
 
-            $class_name = self::folder_class_name($type);
+        // Add type-based class for list items
+        if (is_array($args['list'])) {
+            foreach ((array)$args['list'] as $k => $item) {
+                $folder = $item['folder_imap']; // UTF7-IMAP
+                $type   = $folderdata[$folder];
+
+                if (!$type) {
+                    $type = 'mail';
+                }
 
-            $attrib['class'] = trim($attrib['class'] . ' ' . $class_name);
-            $table->set_row_attribs($attrib, $i);
+                $class_name = self::folder_class_name($type);
+                $args['list'][$k]['class'] = trim($item['class'] . ' ' . $class_name);
+            }
         }
 
         return $args;
@@ -331,16 +371,7 @@ class kolab_folders extends rcube_plugin
             return $args;
         }
 
-        // Load configuration
-        $this->load_config();
-
-        // Check that configuration is not disabled
-        $dont_override  = (array) $this->rc->config->get('dont_override', array());
-
-        // special handling for 'default_folders'
-        if (in_array('default_folders', $dont_override)) {
-            return $args;
-        }
+        $dont_override = (array) $this->rc->config->get('dont_override', array());
 
         // map config option name to kolab folder type annotation
         $opts = array(
@@ -354,7 +385,7 @@ class kolab_folders extends rcube_plugin
         foreach ($opts as $opt_name => $type) {
             $new = $args['prefs'][$opt_name];
             $old = $this->rc->config->get($opt_name);
-            if ($new === $old) {
+            if (!strlen($new) || $new === $old || in_array($opt_name, $dont_override)) {
                 unset($opts[$opt_name]);
             }
         }
@@ -371,24 +402,22 @@ class kolab_folders extends rcube_plugin
 
         foreach ($opts as $opt_name => $type) {
             $foldername = $args['prefs'][$opt_name];
-            if (strlen($foldername)) {
 
-                // get all folders of specified type
-                $folders = array_intersect($folderdata, array($type));
+            // get all folders of specified type
+            $folders = array_intersect($folderdata, array($type));
 
-                // folder already annotated with specified type
-                if (!empty($folders[$foldername])) {
-                    continue;
-                }
+            // folder already annotated with specified type
+            if (!empty($folders[$foldername])) {
+                continue;
+            }
 
-                // set type to the new folder
-                $this->set_folder_type($foldername, $type);
+            // set type to the new folder
+            $this->set_folder_type($foldername, $type);
 
-                // unset old folder(s) type annotation
-                list($maintype, $subtype) = explode('.', $type);
-                foreach (array_keys($folders) as $folder) {
-                    $this->set_folder_type($folder, $maintype);
-                }
+            // unset old folder(s) type annotation
+            list($maintype, $subtype) = explode('.', $type);
+            foreach (array_keys($folders) as $folder) {
+                $this->set_folder_type($folder, $maintype);
             }
         }
 
@@ -567,7 +596,7 @@ class kolab_folders extends rcube_plugin
 
                     // activate folder
                     if ($activate) {
-                        kolab_storage::set_state($foldername, true);
+                        kolab_storage::folder_activate($foldername, true);
                     }
                 }
             }
diff --git a/lib/plugins/kolab_folders/localization/de_CH.inc b/lib/plugins/kolab_folders/localization/de_CH.inc
index b53eea0..ebac855 100644
--- a/lib/plugins/kolab_folders/localization/de_CH.inc
+++ b/lib/plugins/kolab_folders/localization/de_CH.inc
@@ -1,7 +1,7 @@
 <?php
 $labels['folderctype'] = 'Ordnerinhalt';
 $labels['foldertypemail'] = 'E-Mail';
-$labels['foldertypeevent'] = 'Kalender'; // Events?
+$labels['foldertypeevent'] = 'Kalender';
 $labels['foldertypejournal'] = 'Journal';
 $labels['foldertypetask'] = 'Aufgaben';
 $labels['foldertypenote'] = 'Notizen';
diff --git a/lib/plugins/kolab_folders/localization/de_DE.inc b/lib/plugins/kolab_folders/localization/de_DE.inc
index 60c6741..9921e25 100644
--- a/lib/plugins/kolab_folders/localization/de_DE.inc
+++ b/lib/plugins/kolab_folders/localization/de_DE.inc
@@ -1,7 +1,7 @@
 <?php
 $labels['folderctype'] = 'Ordnerinhalt';
 $labels['foldertypemail'] = 'E-Mail';
-$labels['foldertypeevent'] = 'Kalender'; // Events?
+$labels['foldertypeevent'] = 'Kalender';
 $labels['foldertypejournal'] = 'Journal';
 $labels['foldertypetask'] = 'Aufgaben';
 $labels['foldertypenote'] = 'Notizen';
diff --git a/lib/plugins/kolab_folders/localization/en_US.inc b/lib/plugins/kolab_folders/localization/en_US.inc
index 856f59d..0d8d86c 100644
--- a/lib/plugins/kolab_folders/localization/en_US.inc
+++ b/lib/plugins/kolab_folders/localization/en_US.inc
@@ -1,10 +1,18 @@
 <?php
 
+/**
+ * Localizations for the Kolab Folders plugin
+ *
+ * Copyright (C) 2014, Kolab Systems AG
+ *
+ * For translation see https://www.transifex.com/projects/p/kolab/resource/kolab_folders/
+ */
+
 $labels = array();
 
 $labels['folderctype'] = 'Content type';
 $labels['foldertypemail'] = 'Mail';
-$labels['foldertypeevent'] = 'Calendar'; // Events?
+$labels['foldertypeevent'] = 'Calendar';
 $labels['foldertypejournal'] = 'Journal';
 $labels['foldertypetask'] = 'Tasks';
 $labels['foldertypenote'] = 'Notes';
diff --git a/lib/plugins/kolab_folders/localization/es_ES.inc b/lib/plugins/kolab_folders/localization/es_ES.inc
index 91fe5b4..cdbc62c 100644
--- a/lib/plugins/kolab_folders/localization/es_ES.inc
+++ b/lib/plugins/kolab_folders/localization/es_ES.inc
@@ -1,5 +1,5 @@
 <?php
-$labels['foldertypeevent'] = ''; // Events?
+$labels['foldertypeevent'] = 'Calendar';
 $labels['foldertypetask'] = 'Tareas';
 $labels['foldertypenote'] = 'Notas';
 $labels['foldertypecontact'] = 'Contactos';
diff --git a/lib/plugins/kolab_folders/localization/et_EE.inc b/lib/plugins/kolab_folders/localization/et_EE.inc
index e39fe4a..efb51b6 100644
--- a/lib/plugins/kolab_folders/localization/et_EE.inc
+++ b/lib/plugins/kolab_folders/localization/et_EE.inc
@@ -1,3 +1,3 @@
 <?php
-$labels['foldertypeevent'] = ''; // Events?
+$labels['foldertypeevent'] = 'Calendar';
 ?>
diff --git a/lib/plugins/kolab_folders/localization/fr_FR.inc b/lib/plugins/kolab_folders/localization/fr_FR.inc
index 9ddece1..ac3d2dd 100644
--- a/lib/plugins/kolab_folders/localization/fr_FR.inc
+++ b/lib/plugins/kolab_folders/localization/fr_FR.inc
@@ -1,7 +1,7 @@
 <?php
 $labels['folderctype'] = 'Type de contenu';
 $labels['foldertypemail'] = 'Courriel';
-$labels['foldertypeevent'] = 'Calendrier'; // Events?
+$labels['foldertypeevent'] = 'Calendrier';
 $labels['foldertypejournal'] = 'Journal';
 $labels['foldertypetask'] = 'Tâches';
 $labels['foldertypenote'] = 'Notes';
diff --git a/lib/plugins/kolab_folders/localization/ja_JP.inc b/lib/plugins/kolab_folders/localization/ja_JP.inc
index 14f3692..2ac10c2 100644
--- a/lib/plugins/kolab_folders/localization/ja_JP.inc
+++ b/lib/plugins/kolab_folders/localization/ja_JP.inc
@@ -1,7 +1,7 @@
 <?php
 $labels['folderctype'] = 'コンテンツタイプ';
 $labels['foldertypemail'] = 'メール';
-$labels['foldertypeevent'] = 'カレンダー'; // Events?
+$labels['foldertypeevent'] = 'カレンダー';
 $labels['foldertypejournal'] = 'ジャーナル';
 $labels['foldertypetask'] = 'タスク';
 $labels['foldertypenote'] = 'ノート';
diff --git a/lib/plugins/kolab_folders/localization/nl_NL.inc b/lib/plugins/kolab_folders/localization/nl_NL.inc
index e0dffcd..4946a24 100644
--- a/lib/plugins/kolab_folders/localization/nl_NL.inc
+++ b/lib/plugins/kolab_folders/localization/nl_NL.inc
@@ -1,14 +1,14 @@
 <?php
 $labels['folderctype'] = 'Inhoudstype';
 $labels['foldertypemail'] = 'Mail';
-$labels['foldertypeevent'] = 'Agenda'; // Events?
+$labels['foldertypeevent'] = 'Agenda';
 $labels['foldertypejournal'] = 'Dagboek';
 $labels['foldertypetask'] = 'Taken';
 $labels['foldertypenote'] = 'Notities';
 $labels['foldertypecontact'] = 'Adresboek';
 $labels['foldertypeconfiguration'] = 'Configuratie';
 $labels['foldertypefile'] = 'Bestanden';
-$labels['foldertypefreebusy'] = 'Free/Busy';
+$labels['foldertypefreebusy'] = 'Vrij/Bezet';
 $labels['default'] = 'Standaard';
 $labels['inbox'] = 'Inbox';
 $labels['drafts'] = 'Concepten';
diff --git a/lib/plugins/kolab_folders/localization/pl_PL.inc b/lib/plugins/kolab_folders/localization/pl_PL.inc
index f6d98a5..0b13bdd 100644
--- a/lib/plugins/kolab_folders/localization/pl_PL.inc
+++ b/lib/plugins/kolab_folders/localization/pl_PL.inc
@@ -1,7 +1,7 @@
 <?php
 $labels['folderctype'] = 'Typ treści';
 $labels['foldertypemail'] = 'Poczta';
-$labels['foldertypeevent'] = 'Kalendarz'; // Events?
+$labels['foldertypeevent'] = 'Kalendarz';
 $labels['foldertypejournal'] = 'Dziennik';
 $labels['foldertypetask'] = 'Zadania';
 $labels['foldertypenote'] = 'Notatki';
diff --git a/lib/plugins/kolab_folders/localization/ru_RU.inc b/lib/plugins/kolab_folders/localization/ru_RU.inc
index 3e55480..83df476 100644
--- a/lib/plugins/kolab_folders/localization/ru_RU.inc
+++ b/lib/plugins/kolab_folders/localization/ru_RU.inc
@@ -1,7 +1,7 @@
 <?php
 $labels['folderctype'] = 'Тип ящика';
 $labels['foldertypemail'] = 'Почта';
-$labels['foldertypeevent'] = 'Календарь'; // Events?
+$labels['foldertypeevent'] = 'Календарь';
 $labels['foldertypejournal'] = 'Журнал';
 $labels['foldertypetask'] = 'Задачи';
 $labels['foldertypenote'] = 'Заметки';
diff --git a/lib/plugins/libkolab/SQL/mysql.initial.sql b/lib/plugins/libkolab/SQL/mysql.initial.sql
index 40b8631..2aa046d 100644
--- a/lib/plugins/libkolab/SQL/mysql.initial.sql
+++ b/lib/plugins/libkolab/SQL/mysql.initial.sql
@@ -1,7 +1,7 @@
 /**
  * libkolab database schema
  *
- * @version 1.0
+ * @version 1.1
  * @author Thomas Bruederli
  * @licence GNU AGPL
  **/
@@ -29,15 +29,20 @@ CREATE TABLE `kolab_cache_contact` (
   `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
   `created` DATETIME DEFAULT NULL,
   `changed` DATETIME DEFAULT NULL,
-  `data` TEXT NOT NULL,
+  `data` LONGTEXT NOT NULL,
   `xml` LONGBLOB NOT NULL,
   `tags` VARCHAR(255) NOT NULL,
   `words` TEXT NOT NULL,
   `type` VARCHAR(32) CHARACTER SET ascii NOT NULL,
+  `name` VARCHAR(255) NOT NULL,
+  `firstname` VARCHAR(255) NOT NULL,
+  `surname` VARCHAR(255) NOT NULL,
+  `email` VARCHAR(255) NOT NULL,
   CONSTRAINT `fk_kolab_cache_contact_folder` FOREIGN KEY (`folder_id`)
     REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
   PRIMARY KEY(`folder_id`,`msguid`),
-  INDEX `contact_type` (`folder_id`,`type`)
+  INDEX `contact_type` (`folder_id`,`type`),
+  INDEX `contact_uid2msguid` (`folder_id`,`uid`,`msguid`)
 ) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
 
 DROP TABLE IF EXISTS `kolab_cache_event`;
@@ -48,7 +53,7 @@ CREATE TABLE `kolab_cache_event` (
   `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
   `created` DATETIME DEFAULT NULL,
   `changed` DATETIME DEFAULT NULL,
-  `data` TEXT NOT NULL,
+  `data` LONGTEXT NOT NULL,
   `xml` LONGBLOB NOT NULL,
   `tags` VARCHAR(255) NOT NULL,
   `words` TEXT NOT NULL,
@@ -56,7 +61,8 @@ CREATE TABLE `kolab_cache_event` (
   `dtend` DATETIME,
   CONSTRAINT `fk_kolab_cache_event_folder` FOREIGN KEY (`folder_id`)
     REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
-  PRIMARY KEY(`folder_id`,`msguid`)
+  PRIMARY KEY(`folder_id`,`msguid`),
+  INDEX `event_uid2msguid` (`folder_id`,`uid`,`msguid`)
 ) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
 
 DROP TABLE IF EXISTS `kolab_cache_task`;
@@ -67,7 +73,7 @@ CREATE TABLE `kolab_cache_task` (
   `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
   `created` DATETIME DEFAULT NULL,
   `changed` DATETIME DEFAULT NULL,
-  `data` TEXT NOT NULL,
+  `data` LONGTEXT NOT NULL,
   `xml` LONGBLOB NOT NULL,
   `tags` VARCHAR(255) NOT NULL,
   `words` TEXT NOT NULL,
@@ -75,7 +81,8 @@ CREATE TABLE `kolab_cache_task` (
   `dtend` DATETIME,
   CONSTRAINT `fk_kolab_cache_task_folder` FOREIGN KEY (`folder_id`)
     REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
-  PRIMARY KEY(`folder_id`,`msguid`)
+  PRIMARY KEY(`folder_id`,`msguid`),
+  INDEX `task_uid2msguid` (`folder_id`,`uid`,`msguid`)
 ) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
 
 DROP TABLE IF EXISTS `kolab_cache_journal`;
@@ -86,7 +93,7 @@ CREATE TABLE `kolab_cache_journal` (
   `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
   `created` DATETIME DEFAULT NULL,
   `changed` DATETIME DEFAULT NULL,
-  `data` TEXT NOT NULL,
+  `data` LONGTEXT NOT NULL,
   `xml` LONGBLOB NOT NULL,
   `tags` VARCHAR(255) NOT NULL,
   `words` TEXT NOT NULL,
@@ -94,7 +101,8 @@ CREATE TABLE `kolab_cache_journal` (
   `dtend` DATETIME,
   CONSTRAINT `fk_kolab_cache_journal_folder` FOREIGN KEY (`folder_id`)
     REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
-  PRIMARY KEY(`folder_id`,`msguid`)
+  PRIMARY KEY(`folder_id`,`msguid`),
+  INDEX `journal_uid2msguid` (`folder_id`,`uid`,`msguid`)
 ) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
 
 DROP TABLE IF EXISTS `kolab_cache_note`;
@@ -105,13 +113,14 @@ CREATE TABLE `kolab_cache_note` (
   `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
   `created` DATETIME DEFAULT NULL,
   `changed` DATETIME DEFAULT NULL,
-  `data` TEXT NOT NULL,
+  `data` LONGTEXT NOT NULL,
   `xml` LONGBLOB NOT NULL,
   `tags` VARCHAR(255) NOT NULL,
   `words` TEXT NOT NULL,
   CONSTRAINT `fk_kolab_cache_note_folder` FOREIGN KEY (`folder_id`)
     REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
-  PRIMARY KEY(`folder_id`,`msguid`)
+  PRIMARY KEY(`folder_id`,`msguid`),
+  INDEX `note_uid2msguid` (`folder_id`,`uid`,`msguid`)
 ) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
 
 DROP TABLE IF EXISTS `kolab_cache_file`;
@@ -122,7 +131,7 @@ CREATE TABLE `kolab_cache_file` (
   `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
   `created` DATETIME DEFAULT NULL,
   `changed` DATETIME DEFAULT NULL,
-  `data` TEXT NOT NULL,
+  `data` LONGTEXT NOT NULL,
   `xml` LONGBLOB NOT NULL,
   `tags` VARCHAR(255) NOT NULL,
   `words` TEXT NOT NULL,
@@ -130,7 +139,8 @@ CREATE TABLE `kolab_cache_file` (
   CONSTRAINT `fk_kolab_cache_file_folder` FOREIGN KEY (`folder_id`)
     REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
   PRIMARY KEY(`folder_id`,`msguid`),
-  INDEX `folder_filename` (`folder_id`, `filename`)
+  INDEX `folder_filename` (`folder_id`, `filename`),
+  INDEX `file_uid2msguid` (`folder_id`,`uid`,`msguid`)
 ) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
 
 DROP TABLE IF EXISTS `kolab_cache_configuration`;
@@ -141,7 +151,7 @@ CREATE TABLE `kolab_cache_configuration` (
   `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
   `created` DATETIME DEFAULT NULL,
   `changed` DATETIME DEFAULT NULL,
-  `data` TEXT NOT NULL,
+  `data` LONGTEXT NOT NULL,
   `xml` LONGBLOB NOT NULL,
   `tags` VARCHAR(255) NOT NULL,
   `words` TEXT NOT NULL,
@@ -149,7 +159,8 @@ CREATE TABLE `kolab_cache_configuration` (
   CONSTRAINT `fk_kolab_cache_configuration_folder` FOREIGN KEY (`folder_id`)
     REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
   PRIMARY KEY(`folder_id`,`msguid`),
-  INDEX `configuration_type` (`folder_id`,`type`)
+  INDEX `configuration_type` (`folder_id`,`type`),
+  INDEX `configuration_uid2msguid` (`folder_id`,`uid`,`msguid`)
 ) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
 
 DROP TABLE IF EXISTS `kolab_cache_freebusy`;
@@ -160,7 +171,7 @@ CREATE TABLE `kolab_cache_freebusy` (
   `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
   `created` DATETIME DEFAULT NULL,
   `changed` DATETIME DEFAULT NULL,
-  `data` TEXT NOT NULL,
+  `data` LONGTEXT NOT NULL,
   `xml` LONGBLOB NOT NULL,
   `tags` VARCHAR(255) NOT NULL,
   `words` TEXT NOT NULL,
@@ -168,8 +179,9 @@ CREATE TABLE `kolab_cache_freebusy` (
   `dtend` DATETIME,
   CONSTRAINT `fk_kolab_cache_freebusy_folder` FOREIGN KEY (`folder_id`)
     REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
-  PRIMARY KEY(`folder_id`,`msguid`)
+  PRIMARY KEY(`folder_id`,`msguid`),
+  INDEX `freebusy_uid2msguid` (`folder_id`,`uid`,`msguid`)
 ) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
 
 
-INSERT INTO `system` (`name`, `value`) VALUES ('libkolab-version', '2013121100');
+INSERT INTO `system` (`name`, `value`) VALUES ('libkolab-version', '2014021000');
diff --git a/lib/plugins/libkolab/SQL/mysql/2014021000.sql b/lib/plugins/libkolab/SQL/mysql/2014021000.sql
new file mode 100644
index 0000000..31ce699
--- /dev/null
+++ b/lib/plugins/libkolab/SQL/mysql/2014021000.sql
@@ -0,0 +1,9 @@
+ALTER TABLE `kolab_cache_contact` ADD `name` VARCHAR(255) NOT NULL,
+  ADD `firstname` VARCHAR(255) NOT NULL,
+  ADD `surname` VARCHAR(255) NOT NULL,
+  ADD `email` VARCHAR(255) NOT NULL;
+
+-- updating or clearing all contacts caches is required.
+-- either run `bin/modcache.sh update --type=contact` or execute the following query:
+--   DELETE FROM `kolab_folders` WHERE `type`='contact';
+
diff --git a/lib/plugins/libkolab/SQL/mysql/2014032700.sql b/lib/plugins/libkolab/SQL/mysql/2014032700.sql
new file mode 100644
index 0000000..a45fae3
--- /dev/null
+++ b/lib/plugins/libkolab/SQL/mysql/2014032700.sql
@@ -0,0 +1,8 @@
+ALTER TABLE `kolab_cache_configuration` ADD INDEX `configuration_uid2msguid` (`folder_id`, `uid`, `msguid`);
+ALTER TABLE `kolab_cache_contact` ADD INDEX `contact_uid2msguid` (`folder_id`, `uid`, `msguid`);
+ALTER TABLE `kolab_cache_event` ADD INDEX `event_uid2msguid` (`folder_id`, `uid`, `msguid`);
+ALTER TABLE `kolab_cache_task` ADD INDEX `task_uid2msguid` (`folder_id`, `uid`, `msguid`);
+ALTER TABLE `kolab_cache_journal` ADD INDEX `journal_uid2msguid` (`folder_id`, `uid`, `msguid`);
+ALTER TABLE `kolab_cache_note` ADD INDEX `note_uid2msguid` (`folder_id`, `uid`, `msguid`);
+ALTER TABLE `kolab_cache_file` ADD INDEX `file_uid2msguid` (`folder_id`, `uid`, `msguid`);
+ALTER TABLE `kolab_cache_freebusy` ADD INDEX `freebusy_uid2msguid` (`folder_id`, `uid`, `msguid`);
diff --git a/lib/plugins/libkolab/SQL/mysql/2014040900.sql b/lib/plugins/libkolab/SQL/mysql/2014040900.sql
new file mode 100644
index 0000000..cfcaa9d
--- /dev/null
+++ b/lib/plugins/libkolab/SQL/mysql/2014040900.sql
@@ -0,0 +1,16 @@
+ALTER TABLE `kolab_cache_contact` CHANGE `data` `data` LONGTEXT NOT NULL;
+ALTER TABLE `kolab_cache_event` CHANGE `data` `data` LONGTEXT NOT NULL;
+ALTER TABLE `kolab_cache_task` CHANGE `data` `data` LONGTEXT NOT NULL;
+ALTER TABLE `kolab_cache_journal` CHANGE `data` `data` LONGTEXT NOT NULL;
+ALTER TABLE `kolab_cache_note` CHANGE `data` `data` LONGTEXT NOT NULL;
+ALTER TABLE `kolab_cache_file` CHANGE `data` `data` LONGTEXT NOT NULL;
+ALTER TABLE `kolab_cache_configuration` CHANGE `data` `data` LONGTEXT NOT NULL;
+ALTER TABLE `kolab_cache_freebusy` CHANGE `data` `data` LONGTEXT NOT NULL;
+
+-- rebuild cache entries for xcal objects with alarms
+DELETE FROM `kolab_cache_event` WHERE tags LIKE '% x-has-alarms %';
+DELETE FROM `kolab_cache_task` WHERE tags LIKE '% x-has-alarms %';
+
+-- force cache synchronization
+UPDATE `kolab_folders` SET ctag='' WHERE `type` IN ('event','task');
+
diff --git a/lib/plugins/libkolab/bin/modcache.sh b/lib/plugins/libkolab/bin/modcache.sh
index da6e4f8..533fefd 100755
--- a/lib/plugins/libkolab/bin/modcache.sh
+++ b/lib/plugins/libkolab/bin/modcache.sh
@@ -1,4 +1,4 @@
-#!/usr/bin/env php -d enable_dl=On
+#!/usr/bin/env php
 <?php
 
 /**
@@ -7,7 +7,7 @@
  * @version 3.1
  * @author Thomas Bruederli <bruederli at kolabsys.com>
  *
- * Copyright (C) 2012, Kolab Systems AG <contact at kolabsys.com>
+ * Copyright (C) 2012-2014, Kolab Systems AG <contact at kolabsys.com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
@@ -65,6 +65,7 @@ $db->db_connect('w');
 if (!$db->is_connected() || $db->is_error())
     die("No DB connection\n");
 
+ini_set('display_errors', 1);
 
 /*
  * Script controller
@@ -109,7 +110,7 @@ case 'clear':
     }
 
     if ($sql_query) {
-        $db->query($sql_query . $sql_add, resource_prefix($opts).'%');
+        $db->query($sql_query, resource_prefix($opts).'%');
         echo $db->affected_rows() . " records deleted from 'kolab_folders'\n";
     }
     break;
@@ -142,6 +143,32 @@ case 'prewarm':
         die("Authentication failed for " . $opts['user']);
     break;
 
+/**
+ * Update the cache meta columns from the serialized/xml data
+ * (might be run after a schema update)
+ */
+case 'update':
+    // make sure libkolab classes are loaded
+    $rcmail->plugins->load_plugin('libkolab');
+
+    $folder_types = $opts['type'] ? explode(',', $opts['type']) : array('contact','configuration','event','file','task');
+    foreach ($folder_types as $type) {
+        $class = 'kolab_storage_cache_' . $type;
+        $sql_result = $db->query("SELECT folder_id FROM kolab_folders WHERE type=? AND synclock = 0", $type);
+        while ($sql_result && ($sql_arr = $db->fetch_assoc($sql_result))) {
+            $folder = new $class;
+            $folder->select_by_id($sql_arr['folder_id']);
+            echo "Updating " . $sql_arr['folder_id'] . " ($type) ";
+            foreach ($folder->select() as $object) {
+                $object['_formatobj']->to_array();  // load data
+                $folder->save($object['_msguid'], $object, $object['_msguid']);
+                echo ".";
+            }
+            echo "done.\n";
+        }
+    }
+    break;
+
 
 /*
  * Unknown action => show usage
@@ -194,7 +221,7 @@ function authenticate(&$opts)
             if ($opts['verbose'])
                 echo "IMAP login succeeded.\n";
             if (($user = rcube_user::query($opts['username'], $auth['host'])) && $user->ID)
-                $rcmail->set_user($user);
+                $rcmail->user = $user;
         }
         else
             die("Login to IMAP server failed!\n");
diff --git a/lib/plugins/libkolab/bin/randomcontacts.sh b/lib/plugins/libkolab/bin/randomcontacts.sh
new file mode 100755
index 0000000..e4a820c
--- /dev/null
+++ b/lib/plugins/libkolab/bin/randomcontacts.sh
@@ -0,0 +1,181 @@
+#!/usr/bin/env php
+<?php
+
+/**
+ * Generate a number contacts with random data
+ *
+ * @version 3.1
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2014, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+define('INSTALL_PATH', realpath('.') . '/' );
+ini_set('display_errors', 1);
+
+if (!file_exists(INSTALL_PATH . 'program/include/clisetup.php'))
+    die("Execute this from the Roundcube installation dir!\n\n");
+
+require_once INSTALL_PATH . 'program/include/clisetup.php';
+
+function print_usage()
+{
+    print "Usage:  randomcontacts.sh [OPTIONS] USERNAME FOLDER\n";
+    print "Create random contact that for then given user in the specified folder.\n";
+    print "-n, --num      Number of contacts to be created, defaults to 50\n";
+    print "-h, --host     IMAP host name\n";
+    print "-p, --password IMAP user password\n";
+}
+
+// read arguments
+$opts = get_opt(array(
+    'n' => 'num',
+    'h' => 'host',
+    'u' => 'user',
+    'p' => 'pass',
+    'v' => 'verbose',
+));
+
+$opts['username'] = !empty($opts[0]) ? $opts[0] : $opts['user'];
+$opts['folder'] = $opts[1];
+
+$rcmail = rcube::get_instance(rcube::INIT_WITH_DB | rcube::INIT_WITH_PLUGINS);
+$rcmail->plugins->load_plugins(array('libkolab'));
+ini_set('display_errors', 1);
+
+
+if (empty($opts['host'])) {
+    $opts['host'] = $rcmail->config->get('default_host');
+    if (is_array($opts['host']))  // not unique
+        $opts['host'] = null;
+}
+
+if (empty($opts['username']) || empty($opts['folder']) || empty($opts['host'])) {
+    print_usage();
+    exit;
+}
+
+// prompt for password
+if (empty($opts['pass'])) {
+    $opts['pass'] = rcube_utils::prompt_silent("Password: ");
+}
+
+// parse $host URL
+$a_host = parse_url($opts['host']);
+if ($a_host['host']) {
+    $host = $a_host['host'];
+    $imap_ssl = (isset($a_host['scheme']) && in_array($a_host['scheme'], array('ssl','imaps','tls'))) ? TRUE : FALSE;
+    $imap_port = isset($a_host['port']) ? $a_host['port'] : ($imap_ssl ? 993 : 143);
+}
+else {
+    $host = $opts['host'];
+    $imap_port = 143;
+}
+
+// instantiate IMAP class
+$IMAP = $rcmail->get_storage();
+
+// try to connect to IMAP server
+if ($IMAP->connect($host, $opts['username'], $opts['pass'], $imap_port, $imap_ssl)) {
+    print "IMAP login successful.\n";
+    $user = rcube_user::query($opts['username'], $host);
+    $rcmail->user = $user ?: new rcube_user(null, array('username' => $opts['username'], 'host' => $host));
+}
+else {
+    die("IMAP login failed for user " . $opts['username'] . " @ $host\n");
+}
+
+// get contacts folder
+$folder = kolab_storage::get_folder($opts['folder']);
+if (!$folder || empty($folder->type)) {
+    die("Invalid Address Book " . $opts['folder'] . "\n");
+}
+
+$format = new kolab_format_contact;
+
+$num = $opts['num'] ? intval($opts['num']) : 50;
+echo "Creating $num contacts in " . $folder->get_resource_uri() . "\n";
+
+for ($i=0; $i < $num; $i++) {
+    // generate random names
+    $contact = array(
+        'surname' => random_string(rand(1,2)),
+        'firstname' => random_string(rand(1,2)),
+        'organization' => random_string(rand(0,2)),
+        'profession' => random_string(rand(1,2)),
+        'email' => array(),
+        'phone' => array(),
+        'address' => array(),
+        'notes' => random_string(rand(10,200)),
+    );
+
+    // randomly add email addresses
+    $em = rand(1,3);
+    for ($e=0; $e < $em; $e++) {
+        $type = array_rand($format->emailtypes);
+        $contact['email'][] = array(
+            'address' => strtolower(random_string(1) . '@' . random_string(1) . '.tld'),
+            'type' => $type,
+        );
+    }
+
+    // randomly add phone numbers
+    $ph = rand(1,4);
+    for ($p=0; $p < $ph; $p++) {
+        $type = array_rand($format->phonetypes);
+        $contact['phone'][] = array(
+            'number' => '+'.rand(2,8).rand(1,9).rand(1,9).rand(0,9).rand(0,9).rand(0,9).rand(0,9).rand(0,9).rand(0,9).rand(0,9).rand(0,9),
+            'type' => $type,
+        );
+    }
+
+    // randomly add addresses
+    $ad = rand(0,2);
+    for ($a=0; $a < $ad; $a++) {
+        $type = array_rand($format->addresstypes);
+        $contact['address'][] = array(
+            'street' => random_string(rand(1,3)),
+            'locality' => random_string(rand(1,2)),
+            'code' => rand(1000, 89999),
+            'country' => random_string(1),
+            'type' => $type,
+        );
+    }
+
+    $contact['name'] = $contact['firstname'] . ' ' . $contact['surname'];
+
+    if ($folder->save($contact, 'contact')) {
+        echo ".";
+    }
+    else {
+        echo "x";
+        break;  // abort on error
+    }
+}
+
+echo " done.\n";
+
+
+
+function random_string($len)
+{
+    $words = explode(" ", "The Hough transform is named after Paul Hough who patented the method in 1962. It is a technique which can be used to isolate features of a particular shape within an image. Because it requires that the desired features be specified in some parametric form, the classical Hough transform is most commonly used for the de- tection of regular curves such as lines, circles, ellipses, etc. A generalized Hough transform can be employed in applications where a simple analytic description of a features is not possible. Due to the computational complexity of the generalized Hough algorithm, we restrict the main focus of this discussion to the classical Hough transform. Despite its domain restrictions, the classical Hough transform hereafter referred to without the classical prefix retains many applications, as most manufac- tured parts and many anatomical parts investigated in medical imagery contain feature boundaries which can be described by regular curve
 s. The main advantage of the Hough transform technique is that it is tolerant of gaps in feature boundary descriptions and is relatively unaffected by image noise.");
+    for ($i = 0; $i < $len; $i++) {
+        $str .= $words[rand(0,count($words)-1)] . " ";
+    }
+
+    return rtrim($str);
+}
diff --git a/lib/plugins/libkolab/composer.json b/lib/plugins/libkolab/composer.json
new file mode 100644
index 0000000..8926037
--- /dev/null
+++ b/lib/plugins/libkolab/composer.json
@@ -0,0 +1,30 @@
+{
+    "name": "kolab/libkolab",
+    "type": "roundcube-plugin",
+    "description": "Plugin to setup a basic environment for the interaction with a Kolab server.",
+    "homepage": "http://git.kolab.org/roundcubemail-plugins-kolab/",
+    "license": "AGPLv3",
+    "version": "1.1.0",
+    "authors": [
+        {
+            "name": "Thomas Bruederli",
+            "email": "bruederli at kolabsys.com",
+            "role": "Lead"
+        },
+        {
+            "name": "Alensader Machniak",
+            "email": "machniak at kolabsys.com",
+            "role": "Developer"
+        }
+    ],
+    "repositories": [
+        {
+            "type": "composer",
+            "url": "http://plugins.roundcube.net"
+        }
+    ],
+    "require": {
+        "php": ">=5.3.0",
+        "roundcube/plugin-installer": ">=0.1.3"
+    }
+}
diff --git a/lib/plugins/libkolab/config.inc.php.dist b/lib/plugins/libkolab/config.inc.php.dist
index 0c612a3..b043bb7 100644
--- a/lib/plugins/libkolab/config.inc.php.dist
+++ b/lib/plugins/libkolab/config.inc.php.dist
@@ -16,6 +16,10 @@ $rcmail_config['kolab_freebusy_server'] = 'https://<some-host>/<freebusy-path>';
 // folders in calendar view or available addressbooks
 $rcmail_config['kolab_use_subscriptions'] = false;
 
+// List any of 'personal','shared','other' namespaces to be excluded from groupware folder listing
+// example: array('other');
+$rcmail_config['kolab_skip_namespace'] = null;
+
 // Enables the use of displayname folder annotations as introduced in KEP:?
 // for displaying resource folder names (experimental!)
 $rcmail_config['kolab_custom_display_names'] = false;
@@ -30,3 +34,28 @@ $rcmail_config['kolab_http_request'] = array();
 // 2 - bypass messages/indexes cache completely
 // 1 - bypass only messages, but use index cache
 $rcmail_config['kolab_messages_cache_bypass'] = 0;
+
+// LDAP directory to find avilable users for folder sharing.
+// Either contains an array with LDAP addressbook configuration or refers to entry in $config['ldap_public'].
+// If not specified, the configuraton from 'kolab_auth_addressbook' will be used.
+$rcmail_config['kolab_users_directory'] = null;
+
+// Filter to be used for resolving user folders in LDAP.
+// Defaults to the 'kolab_auth_filter' configuration option.
+$rcmail_config['kolab_users_filter'] = '(&(objectclass=kolabInetOrgPerson)(|(uid=%u)(mail=%fu)))';
+
+// Which property of the LDAP user record to use for user folder mapping in IMAP.
+// Defaults to the 'kolab_auth_login' configuration option.
+$rcmail_config['kolab_users_id_attrib'] = null;
+
+// Use these attributes when searching users in LDAP
+$rcmail_config['kolab_users_search_attrib'] = array('cn','mail','alias');
+
+// JSON-RPC endpoint configuration of the Bonnie web service providing historic data for groupware objects
+$rcmail_config['kolab_bonnie_api'] = array(
+    'uri'    => 'https://<kolab-hostname>:8080/api/rpc',
+    'user'   => 'webclient',
+    'pass'   => 'Welcome2KolabSystems',
+    'secret' => '8431f191707fffffff00000000cccc',
+    'debug'  => true,   // logs requests/responses to <log-dir>/bonnie
+);
diff --git a/lib/plugins/libkolab/js/folderlist.js b/lib/plugins/libkolab/js/folderlist.js
new file mode 100644
index 0000000..1c8ce2f
--- /dev/null
+++ b/lib/plugins/libkolab/js/folderlist.js
@@ -0,0 +1,264 @@
+/**
+ * Kolab groupware folders treelist widget
+ *
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * @licstart  The following is the entire license notice for the
+ * JavaScript code in this file.
+ *
+ * Copyright (C) 2014, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * @licend  The above is the entire license notice
+ * for the JavaScript code in this file.
+ */
+
+function kolab_folderlist(node, p)
+{
+    // extends treelist.js
+    rcube_treelist_widget.call(this, node, p);
+
+    // private vars
+    var me = this;
+    var search_results;
+    var search_results_widget;
+    var search_results_container;
+    var listsearch_request;
+    var search_messagebox;
+
+    var Q = rcmail.quote_html;
+
+    // render the results for folderlist search
+    function render_search_results(results)
+    {
+        if (results.length) {
+          // create treelist widget to present the search results
+          if (!search_results_widget) {
+              var list_id = (me.container.attr('id') || p.id_prefix || '0')
+              search_results_container = $('<div class="searchresults"></div>')
+                  .html(p.search_title ? '<h2 class="boxtitle" id="st:' + list_id + '">' + p.search_title + '</h2>' : '')
+                  .insertAfter(me.container);
+
+              search_results_widget = new rcube_treelist_widget('<ul>', {
+                  id_prefix: p.id_prefix,
+                  id_encode: p.id_encode,
+                  id_decode: p.id_decode,
+                  selectable: false
+              });
+              // copy classes from main list
+              search_results_widget.container.addClass(me.container.attr('class')).attr('aria-labelledby', 'st:' + list_id);
+
+              // register click handler on search result's checkboxes to select the given item for listing
+              search_results_widget.container
+                  .appendTo(search_results_container)
+                  .on('click', 'input[type=checkbox], a.subscribed, span.subscribed', function(e) {
+                      var node, has_children, li = $(this).closest('li'),
+                          id = li.attr('id').replace(new RegExp('^'+p.id_prefix), '');
+                      if (p.id_decode)
+                          id = p.id_decode(id);
+                      node = search_results_widget.get_node(id),
+                      has_children = node.children && node.children.length;
+
+                      e.stopPropagation();
+                      e.bubbles = false;
+
+                      // activate + subscribe
+                      if ($(e.target).hasClass('subscribed')) {
+                          search_results[id].subscribed = true;
+                          $(e.target).attr('aria-checked', 'true');
+                          li.children().first()
+                              .toggleClass('subscribed')
+                              .find('input[type=checkbox]').get(0).checked = true;
+                      }
+                      else if (!this.checked) {
+                          return;
+                      }
+
+                      // copy item to the main list
+                      add_result2list(id, li, true);
+
+                      if (has_children) {
+                          li.find('input[type=checkbox]').first().prop('disabled', true).prop('checked', true);
+                          li.find('a.subscribed, span.subscribed').first().hide();
+                      }
+                      else {
+                          li.remove();
+                      }
+
+                      // set focus to cloned checkbox
+                      if (rcube_event.is_keyboard(e)) {
+                        $(me.get_item(id, true)).find('input[type=checkbox]').first().focus();
+                      }
+                  })
+                  .on('click', function(e) {
+                      var prop, id = String($(e.target).closest('li').attr('id')).replace(new RegExp('^'+p.id_prefix), '');
+                      if (p.id_decode)
+                          id = p.id_decode(id);
+
+                      // forward event
+                      if (prop = search_results[id]) {
+                        e.data = prop;
+                        if (me.triggerEvent('click-item', e) === false) {
+                          e.stopPropagation();
+                          return false;
+                        }
+                      }
+                  });
+          }
+
+          // add results to list
+          for (var prop, item, i=0; i < results.length; i++) {
+              prop = results[i];
+              item = $(prop.html);
+              search_results[prop.id] = prop;
+              search_results_widget.insert({
+                  id: prop.id,
+                  classes: [ prop.group || '' ],
+                  html: item,
+                  collapsed: true,
+                  virtual: prop.virtual
+              }, prop.parent);
+
+              // disable checkbox if item already exists in main list
+              if (me.get_node(prop.id) && !me.get_node(prop.id).virtual) {
+                  item.find('input[type=checkbox]').first().prop('disabled', true).prop('checked', true);
+                  item.find('a.subscribed, span.subscribed').hide();
+              }
+          }
+
+          search_results_container.show();
+        }
+    }
+
+    // helper method to (recursively) add a search result item to the main list widget
+    function add_result2list(id, li, active)
+    {
+        var node = search_results_widget.get_node(id),
+            prop = search_results[id],
+            parent_id = prop.parent || null,
+            has_children = node.children && node.children.length,
+            dom_node = has_children ? li.children().first().clone(true, true) : li.children().first();
+
+        // find parent node and insert at the right place
+        if (parent_id && me.get_node(parent_id)) {
+            dom_node.children('span,a').first().html(Q(prop.editname || prop.listname));
+        }
+        else if (parent_id && search_results[parent_id]) {
+            // copy parent tree from search results
+            add_result2list(parent_id, $(search_results_widget.get_item(parent_id)), false);
+        }
+        else if (parent_id) {
+            // use full name for list display
+            dom_node.children('span,a').first().html(Q(prop.name));
+        }
+
+        // replace virtual node with a real one
+        if (me.get_node(id)) {
+            $(me.get_item(id, true)).children().first()
+                .replaceWith(dom_node)
+                .removeClass('virtual');
+        }
+        else {
+            // move this result item to the main list widget
+            me.insert({
+                id: id,
+                classes: [ prop.group || '' ],
+                virtual: prop.virtual,
+                html: dom_node,
+            }, parent_id, prop.group);
+        }
+
+        delete prop.html;
+        prop.active = active;
+        me.triggerEvent('insert-item', { id: id, data: prop, item: li });
+    }
+
+    // do some magic when search is performed on the widget
+    this.addEventListener('search', function(search) {
+        // hide search results
+        if (search_results_widget) {
+            search_results_container.hide();
+            search_results_widget.reset();
+        }
+        search_results = {};
+
+        if (search_messagebox)
+            rcmail.hide_message(search_messagebox);
+
+        // send search request(s) to server
+        if (search.query && search.execute) {
+            // require a minimum length for the search string
+            if (rcmail.env.autocomplete_min_length && search.query.length < rcmail.env.autocomplete_min_length && search.query != '*') {
+                search_messagebox = rcmail.display_message(
+                    rcmail.get_label('autocompletechars').replace('$min', rcmail.env.autocomplete_min_length));
+                return;
+            }
+
+            if (listsearch_request) {
+                // ignore, let the currently running request finish
+                if (listsearch_request.query == search.query) {
+                    return;
+                }
+                else { // cancel previous search request
+                    rcmail.multi_thread_request_abort(listsearch_request.id);
+                    listsearch_request = null;
+                }
+            }
+
+            var sources = p.search_sources || [ 'folders' ];
+            var reqid = rcmail.multi_thread_http_request({
+                items: sources,
+                threads: rcmail.env.autocomplete_threads || 1,
+                action:  p.search_action || 'listsearch',
+                postdata: { action:'search', q:search.query, source:'%s' },
+                lock: rcmail.display_message(rcmail.get_label('searching'), 'loading'),
+                onresponse: render_search_results,
+                whendone: function(data){
+                  listsearch_request = null;
+                  me.triggerEvent('search-complete', data);
+                }
+            });
+
+            listsearch_request = { id:reqid, query:search.query };
+        }
+        else if (!search.query && listsearch_request) {
+            rcmail.multi_thread_request_abort(listsearch_request.id);
+            listsearch_request = null;
+        }
+    });
+
+    this.container.on('click', 'a.subscribed, span.subscribed', function(e){
+        var li = $(this).closest('li'),
+            id = li.attr('id').replace(new RegExp('^'+p.id_prefix), ''),
+            div = li.children().first();
+
+        if (me.is_search())
+          id = id.replace(/--xsR$/, '');
+
+        if (p.id_decode)
+            id = p.id_decode(id);
+
+        div.toggleClass('subscribed');
+        $(this).attr('aria-checked', div.hasClass('subscribed') ? 'true' : 'false');
+        me.triggerEvent('subscribe', { id: id, subscribed: div.hasClass('subscribed'), item: li });
+
+        e.stopPropagation();
+        return false;
+    })
+
+}
+
+// link prototype from base class
+kolab_folderlist.prototype = rcube_treelist_widget.prototype;
diff --git a/lib/plugins/libkolab/lib/kolab_bonnie_api.php b/lib/plugins/libkolab/lib/kolab_bonnie_api.php
new file mode 100644
index 0000000..23dafd8
--- /dev/null
+++ b/lib/plugins/libkolab/lib/kolab_bonnie_api.php
@@ -0,0 +1,82 @@
+<?php
+
+/**
+ * Provider class for accessing historic groupware object data through the Bonnie service
+ *
+ * API Specification at https://wiki.kolabsys.com/User:Bruederli/Draft:Bonnie_Client_API
+ *
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2014, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class kolab_bonnie_api
+{
+    public $ready = false;
+
+    private $config = array();
+    private $client = null;
+
+
+    /**
+     * Default constructor
+     */
+    public function __construct($config)
+    {
+        $this->config = $confg;
+
+        $this->client = new kolab_bonnie_api_client($config['uri'], $config['timeout'] ?: 5, (bool)$config['debug']);
+
+        $this->client->set_secret($config['secret']);
+        $this->client->set_authentication($config['user'], $config['pass']);
+        $this->client->set_request_user(rcube::get_instance()->get_user_name());
+
+        $this->ready = !empty($config['secret']) && !empty($config['user']) && !empty($config['pass']);
+    }
+
+    /**
+     * Wrapper function for <object>.changelog() API call
+     */
+    public function changelog($type, $uid, $mailbox=null)
+    {
+        return $this->client->execute($type.'.changelog', array('uid' => $uid, 'mailbox' => $mailbox));
+    }
+
+    /**
+     * Wrapper function for <object>.diff() API call
+     */
+    public function diff($type, $uid, $rev, $mailbox=null)
+    {
+        return $this->client->execute($type.'.diff', array('uid' => $uid, 'rev' => $rev, 'mailbox' => $mailbox));
+    }
+
+    /**
+     * Wrapper function for <object>.get() API call
+     */
+    public function get($type, $uid, $rev, $mailbox=null)
+    {
+      return $this->client->execute($type.'.get', array('uid' => $uid, 'rev' => intval($rev), 'mailbox' => $mailbox));
+    }
+
+    /**
+     * Generic wrapper for direct API calls
+     */
+    public function _execute($method, $params = array())
+    {
+        return $this->client->execute($method, $params);
+    }
+
+}
\ No newline at end of file
diff --git a/lib/plugins/libkolab/lib/kolab_bonnie_api_client.php b/lib/plugins/libkolab/lib/kolab_bonnie_api_client.php
new file mode 100644
index 0000000..bc209f4
--- /dev/null
+++ b/lib/plugins/libkolab/lib/kolab_bonnie_api_client.php
@@ -0,0 +1,239 @@
+<?php
+
+/**
+ * JSON-RPC client class with some extra features for communicating with the Bonnie API service.
+ *
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2014, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class kolab_bonnie_api_client
+{
+    /**
+     * URL of the RPC endpoint
+     * @var string
+     */
+    protected $url;
+
+    /**
+     * HTTP client timeout in seconds
+     * @var integer
+     */
+    protected $timeout;
+
+    /**
+     * Debug flag
+     * @var bool
+     */
+    protected $debug;
+
+    /**
+     * Username for authentication
+     * @var string
+     */
+    protected $username;
+
+    /**
+     * Password for authentication
+     * @var string
+     */
+    protected $password;
+
+    /**
+     * Secret key for request signing
+     * @var string
+     */
+    protected $secret;
+
+    /**
+     * Default HTTP headers to send to the server
+     * @var array
+     */
+    protected $headers = array(
+        'Connection' => 'close',
+        'Content-Type' => 'application/json',
+        'Accept' => 'application/json',
+    );
+
+    /**
+     * Constructor
+     *
+     * @param  string  $url      Server URL
+     * @param  integer $timeout  Request timeout
+     * @param  bool    $debug    Enabled debug logging
+     * @param  array   $headers  Custom HTTP headers
+     */
+    public function __construct($url, $timeout = 5, $debug = false, $headers = array())
+    {
+        $this->url = $url;
+        $this->timeout = $timeout;
+        $this->debug = $debug;
+        $this->headers = array_merge($this->headers, $headers);
+    }
+
+    /**
+     * Setter for secret key for request signing
+     */
+    public function set_secret($secret)
+    {
+        $this->secret = $secret;
+    }
+
+    /**
+     * Setter for the X-Request-User header
+     */
+    public function set_request_user($username)
+    {
+        $this->headers['X-Request-User'] = $username;
+    }
+
+    /**
+     * Set authentication parameters
+     *
+     * @param  string $username  Username
+     * @param  string $password  Password
+     */
+    public function set_authentication($username, $password)
+    {
+        $this->username = $username;
+        $this->password = $password;
+    }
+
+    /**
+     * Automatic mapping of procedures
+     *
+     * @param  string $method  Procedure name
+     * @param  array  $params  Procedure arguments
+     * @return mixed
+     */
+    public function __call($method, $params)
+    {
+        return $this->execute($method, $params);
+    }
+
+    /**
+     * Execute an RPC command
+     *
+     * @param  string $method  Procedure name
+     * @param  array  $params  Procedure arguments
+     * @return mixed
+     */
+    public function execute($method, array $params = array())
+    {
+        $id = mt_rand();
+
+        $payload = array(
+            'jsonrpc' => '2.0',
+            'method' => $method,
+            'id' => $id,
+        );
+
+        if (!empty($params)) {
+            $payload['params'] = $params;
+        }
+
+        $result = $this->send_request($payload, $method != 'system.keygen');
+
+        if (isset($result['id']) && $result['id'] == $id && array_key_exists('result', $result)) {
+            return $result['result'];
+        }
+        else if (isset($result['error'])) {
+            $this->_debug('ERROR', $result);
+        }
+
+        return null;
+    }
+
+    /**
+     * Do the HTTP request
+     *
+     * @param  string  $payload  Data to send
+     */
+    protected function send_request($payload, $sign = true)
+    {
+        try {
+            $payload_ = json_encode($payload);
+
+            // add request signature
+            if ($sign && !empty($this->secret)) {
+                $this->headers['X-Request-Sign'] = $this->request_signature($payload_);
+            }
+            else if ($this->headers['X-Request-Sign']) {
+                unset($this->headers['X-Request-Sign']);
+            }
+
+            $this->_debug('REQUEST', $payload, $this->headers);
+            $request = libkolab::http_request($this->url, 'POST', array('timeout' => $this->timeout));
+            $request->setHeader($this->headers);
+            $request->setAuth($this->username, $this->password);
+            $request->setBody($payload_);
+
+            $response = $request->send();
+
+            if ($response->getStatus() == 200) {
+                $result = json_decode($response->getBody(), true);
+                $this->_debug('RESPONSE', $result);
+            }
+            else {
+                throw new Exception(sprintf("HTTP %d %s", $response->getStatus(), $response->getReasonPhrase()));
+            }
+        }
+        catch (Exception $e) {
+            rcube::raise_error(array(
+                'code' => 500,
+                'type' => 'php',
+                'message' => "Bonnie API request failed: " . $e->getMessage(),
+            ), true);
+
+            return array('id' => $payload['id'], 'error' => $e->getMessage(), 'code' => -32000);
+        }
+
+        return is_array($result) ? $result : array();
+    }
+
+    /**
+     * Compute the hmac signature for the current event payload using
+     * the secret key configured for this API client
+     *
+     * @param string $data The request payload data
+     * @return string The request signature
+     */
+    protected function request_signature($data)
+    {
+        // TODO: get the session key with a system.keygen call
+        return hash_hmac('sha256', $this->headers['X-Request-User'] . ':' . $data, $this->secret);
+    }
+
+    /**
+     * Write debug log
+     */
+    protected function _debug(/* $message, $data1, data2, ...*/)
+    {
+        if (!$this->debug)
+            return;
+
+        $args = func_get_args();
+
+        $msg = array();
+        foreach ($args as $arg) {
+            $msg[] = !is_string($arg) ? var_export($arg, true) : $arg;
+        }
+
+        rcube::write_log('bonnie', join(";\n", $msg));
+    }
+
+}
\ No newline at end of file
diff --git a/lib/plugins/libkolab/lib/kolab_format.php b/lib/plugins/libkolab/lib/kolab_format.php
index 11a5c4f..ae7705c 100644
--- a/lib/plugins/libkolab/lib/kolab_format.php
+++ b/lib/plugins/libkolab/lib/kolab_format.php
@@ -45,7 +45,7 @@ abstract class kolab_format
     protected $version = '3.0';
 
     const KTYPE_PREFIX = 'application/x-vnd.kolab.';
-    const PRODUCT_ID   = 'Roundcube-libkolab-0.9';
+    const PRODUCT_ID   = 'Roundcube-libkolab-1.1';
 
     /**
      * Factory method to instantiate a kolab_format object of the given type and version
@@ -174,7 +174,7 @@ abstract class kolab_format
      * Convert a libkolabxml vector to a PHP array
      *
      * @param object vector Object
-     * @return array Indexed array contaning vector elements
+     * @return array Indexed array containing vector elements
      */
     public static function vector2array($vec, $max = PHP_INT_MAX)
     {
@@ -208,7 +208,11 @@ abstract class kolab_format
      */
     public static function mime2object_type($x_kolab_type)
     {
-        return preg_replace('/dictionary.[a-z.]+$/', 'dictionary', substr($x_kolab_type, strlen(self::KTYPE_PREFIX)));
+        return preg_replace(
+            array('/dictionary.[a-z.]+$/', '/contact.distlist$/'),
+            array( 'dictionary',            'distribution-list'),
+            substr($x_kolab_type, strlen(self::KTYPE_PREFIX))
+        );
     }
 
 
@@ -411,7 +415,7 @@ abstract class kolab_format
         $this->obj->setLastModified(self::get_datetime($object['changed']));
 
         // Save custom properties of the given object
-        if (isset($object['x-custom'])) {
+        if (isset($object['x-custom']) && method_exists($this->obj, 'setCustomProperties')) {
             $vcustom = new vectorcs;
             foreach ((array)$object['x-custom'] as $cp) {
                 if (is_array($cp))
@@ -419,7 +423,8 @@ abstract class kolab_format
             }
             $this->obj->setCustomProperties($vcustom);
         }
-        else {  // load custom properties from XML for caching (#2238)
+        // load custom properties from XML for caching (#2238) if method exists (#3125)
+        else if (method_exists($this->obj, 'customProperties')) {
             $object['x-custom'] = array();
             $vcustom = $this->obj->customProperties();
             for ($i=0; $i < $vcustom->size(); $i++) {
@@ -452,10 +457,12 @@ abstract class kolab_format
         }
 
         // read custom properties
-        $vcustom = $this->obj->customProperties();
-        for ($i=0; $i < $vcustom->size(); $i++) {
-            $cp = $vcustom->get($i);
-            $object['x-custom'][] = array($cp->identifier, $cp->value);
+        if (method_exists($this->obj, 'customProperties')) {
+            $vcustom = $this->obj->customProperties();
+            for ($i=0; $i < $vcustom->size(); $i++) {
+                $cp = $vcustom->get($i);
+                $object['x-custom'][] = array($cp->identifier, $cp->value);
+            }
         }
 
         // merge with additional data, e.g. attachments from the message
@@ -498,8 +505,15 @@ abstract class kolab_format
         return array();
     }
 
-    protected function get_attachments(&$object)
+    /**
+     * Utility function to extract object attachment data
+     *
+     * @param array Hash array reference to append attachment data into
+     */
+    public function get_attachments(&$object)
     {
+        $this->init();
+
         // handle attachments
         $vattach = $this->obj->attachments();
         for ($i=0; $i < $vattach->size(); $i++) {
@@ -508,21 +522,29 @@ abstract class kolab_format
             // skip cid: attachments which are mime message parts handled by kolab_storage_folder
             if (substr($attach->uri(), 0, 4) != 'cid:' && $attach->label()) {
                 $name    = $attach->label();
+                $key     = $name . (isset($object['_attachments'][$name]) ? '.'.$i : '');
                 $content = $attach->data();
-                $object['_attachments'][$name] = array(
+                $object['_attachments'][$key] = array(
+                    'id'       => 'i:'.$i,
                     'name'     => $name,
                     'mimetype' => $attach->mimetype(),
                     'size'     => strlen($content),
                     'content'  => $content,
                 );
             }
-            else if (substr($attach->uri(), 0, 4) == 'http') {
+            else if (in_array(substr($attach->uri(), 0, 4), array('http','imap'))) {
                 $object['links'][] = $attach->uri();
             }
         }
     }
 
-    protected function set_attachments($object)
+    /**
+     * Utility function to set attachment properties to the kolabformat object
+     *
+     * @param array  Object data as hash array
+     * @param boolean True to always overwrite attachment information
+     */
+    protected function set_attachments($object, $write = true)
     {
         // save attachments
         $vattach = new vectorattachment;
@@ -531,16 +553,31 @@ abstract class kolab_format
                 continue;
             $attach = new Attachment;
             $attach->setLabel((string)$attr['name']);
-            $attach->setUri('cid:' . $cid, $attr['mimetype']);
-            $vattach->push($attach);
+            $attach->setUri('cid:' . $cid, $attr['mimetype'] ?: 'application/octet-stream');
+            if ($attach->isValid()) {
+                $vattach->push($attach);
+                $write = true;
+            }
+            else {
+                rcube::raise_error(array(
+                    'code' => 660,
+                    'type' => 'php',
+                    'file' => __FILE__,
+                    'line' => __LINE__,
+                    'message' => "Invalid attributes for attachment $cid: " . var_export($attr, true),
+                ), true);
+            }
         }
 
         foreach ((array) $object['links'] as $link) {
             $attach = new Attachment;
             $attach->setUri($link, 'unknown');
             $vattach->push($attach);
+            $write = true;
         }
 
-        $this->obj->setAttachments($vattach);
+        if ($write) {
+            $this->obj->setAttachments($vattach);
+        }
     }
 }
diff --git a/lib/plugins/libkolab/lib/kolab_format_configuration.php b/lib/plugins/libkolab/lib/kolab_format_configuration.php
index 5a8d3ff..17b46a7 100644
--- a/lib/plugins/libkolab/lib/kolab_format_configuration.php
+++ b/lib/plugins/libkolab/lib/kolab_format_configuration.php
@@ -24,16 +24,18 @@
 
 class kolab_format_configuration extends kolab_format
 {
-    public $CTYPE = 'application/x-vnd.kolab.configuration';
+    public $CTYPE   = 'application/x-vnd.kolab.configuration';
     public $CTYPEv2 = 'application/x-vnd.kolab.configuration';
 
-    protected $objclass = 'Configuration';
-    protected $read_func = 'readConfiguration';
+    protected $objclass   = 'Configuration';
+    protected $read_func  = 'readConfiguration';
     protected $write_func = 'writeConfiguration';
 
     private $type_map = array(
+        'category'   => Configuration::TypeCategoryColor,
         'dictionary' => Configuration::TypeDictionary,
-        'category' => Configuration::TypeCategoryColor,
+        'relation'   => Configuration::TypeRelation,
+        'snippet'    => Configuration::TypeSnippet,
     );
 
 
@@ -60,6 +62,48 @@ class kolab_format_configuration extends kolab_format
             $categories = new vectorcategorycolor;
             $this->obj = new Configuration($categories);
             break;
+
+        case 'relation':
+            $relation = new Relation(strval($object['name']), strval($object['category']));
+
+            if ($object['color']) {
+                $relation->setColor($object['color']);
+            }
+            if ($object['parent']) {
+                $relation->setParent($object['parent']);
+            }
+            if ($object['iconName']) {
+                $relation->setIconName($object['iconName']);
+            }
+            if ($object['priority'] > 0) {
+                $relation->setPriority((int) $object['priority']);
+            }
+            if (!empty($object['members'])) {
+                $relation->setMembers(self::array2vector($object['members']));
+            }
+
+            $this->obj = new Configuration($relation);
+            break;
+
+        case 'snippet':
+            $collection = new SnippetCollection($object['name']);
+            $snippets   = new vectorsnippets;
+
+            foreach ((array) $object['snippets'] as $item) {
+                $snippet = new snippet($item['name'], $item['text']);
+                $snippet->setTextType(strtolower($item['type']) == 'html' ? Snippet::HTML : Snippet::Plain);
+                if ($item['shortcut']) {
+                    $snippet->setShortCut($item['shortcut']);
+                }
+
+                $snippets->push($snippet);
+            }
+
+            $collection->setSnippets($snippets);
+
+            $this->obj = new Configuration($collection);
+            break;
+
         default:
             return false;
         }
@@ -90,8 +134,9 @@ class kolab_format_configuration extends kolab_format
     public function to_array($data = array())
     {
         // return cached result
-        if (!empty($this->data))
+        if (!empty($this->data)) {
             return $this->data;
+        }
 
         // read common object props into local data object
         $object = parent::to_array($data);
@@ -111,11 +156,44 @@ class kolab_format_configuration extends kolab_format
         case 'category':
             // TODO: implement this
             break;
+
+        case 'relation':
+            $relation = $this->obj->relation();
+
+            $object['name']     = $relation->name();
+            $object['category'] = $relation->type();
+            $object['color']    = $relation->color();
+            $object['parent']   = $relation->parent();
+            $object['iconName'] = $relation->iconName();
+            $object['priority'] = $relation->priority();
+            $object['members']  = self::vector2array($relation->members());
+
+            break;
+
+        case 'snippet':
+            $collection = $this->obj->snippets();
+
+            $object['name']     = $collection->name();
+            $object['snippets'] = array();
+
+            $snippets = $collection->snippets();
+            for ($i=0; $i < $snippets->size(); $i++) {
+                $snippet = $snippets->get($i);
+                $object['snippets'][] = array(
+                    'name'     => $snippet->name(),
+                    'text'     => $snippet->text(),
+                    'type'     => $snippet->textType() == Snippet::HTML ? 'html' : 'plain',
+                    'shortcut' => $snippet->shortCut(),
+                );
+            }
+
+            break;
         }
 
         // adjust content-type string
-        if ($object['type'])
+        if ($object['type']) {
             $this->CTYPE = $this->CTYPEv2 = 'application/x-vnd.kolab.configuration.' . $object['type'];
+        }
 
         $this->data = $object;
         return $this->data;
@@ -130,10 +208,41 @@ class kolab_format_configuration extends kolab_format
     {
         $tags = array();
 
-        if ($this->data['type'] == 'dictionary')
+        switch ($this->data['type']) {
+        case 'dictionary':
             $tags = array($this->data['language']);
+            break;
+
+        case 'relation':
+            $tags = array('category:' . $this->data['category']);
+            break;
+        }
 
         return $tags;
     }
 
+    /**
+     * Callback for kolab_storage_cache to get words to index for fulltext search
+     *
+     * @return array List of words to save in cache
+     */
+    public function get_words()
+    {
+        $words = array();
+
+        foreach ((array)$this->data['members'] as $url) {
+            $member = kolab_storage_config::parse_member_url($url);
+
+            if (empty($member)) {
+                if (strpos($url, 'urn:uuid:') === 0) {
+                    $words[] = substr($url, 9);
+                }
+            }
+            else if (!empty($member['params']['message-id'])) {
+                $words[] = $member['params']['message-id'];
+            }
+        }
+
+        return $words;
+    }
 }
diff --git a/lib/plugins/libkolab/lib/kolab_format_contact.php b/lib/plugins/libkolab/lib/kolab_format_contact.php
index 63efe9a..806a819 100644
--- a/lib/plugins/libkolab/lib/kolab_format_contact.php
+++ b/lib/plugins/libkolab/lib/kolab_format_contact.php
@@ -203,6 +203,8 @@ class kolab_format_contact extends kolab_format
             $this->obj->setNote($object['notes']);
         if (isset($object['freebusyurl']))
             $this->obj->setFreeBusyUrl($object['freebusyurl']);
+        if (isset($object['lang']))
+            $this->obj->setLanguages(self::array2vector($object['lang']));
         if (isset($object['birthday']))
             $this->obj->setBDay(self::get_datetime($object['birthday'], false, true));
         if (isset($object['anniversary']))
@@ -227,6 +229,12 @@ class kolab_format_contact extends kolab_format
                 }
             }
         }
+        // add other relateds
+        if (is_array($object['related'])) {
+            foreach ($object['related'] as $value) {
+                $rels->push(new Related(Related::Text, $value));
+            }
+        }
         $this->obj->setRelateds($rels);
 
         // insert/replace crypto keys
@@ -346,6 +354,7 @@ class kolab_format_contact extends kolab_format
 
         $object['notes'] = $this->obj->note();
         $object['freebusyurl'] = $this->obj->freeBusyUrl();
+        $object['lang'] = self::vector2array($this->obj->languages());
 
         if ($bday = self::php_datetime($this->obj->bDay()))
             $object['birthday'] = $bday;
@@ -363,7 +372,7 @@ class kolab_format_contact extends kolab_format
             $object['photo'] = $photo_name;
 
         // relateds -> spouse, children
-        $this->read_relateds($this->obj->relateds(), $object);
+        $this->read_relateds($this->obj->relateds(), $object, 'related');
 
         // crypto settings: currently only key values are supported
         $keys = $this->obj->keys();
@@ -446,7 +455,7 @@ class kolab_format_contact extends kolab_format
     /**
      * Helper method to map contents of a Related vector to the contact data object
      */
-    private function read_relateds($rels, &$object)
+    private function read_relateds($rels, &$object, $catchall = null)
     {
         $typemap = array_flip($this->relatedmap);
 
@@ -455,13 +464,19 @@ class kolab_format_contact extends kolab_format
             if ($rel->type() != Related::Text)  // we can't handle UID relations yet
                 continue;
 
+            $known = false;
             $types = $rel->relationTypes();
             foreach ($typemap as $t => $field) {
                 if ($types & $t) {
                     $object[$field][] = $rel->text();
+                    $known = true;
                     break;
                 }
             }
+
+            if (!$known && $catchall) {
+                $object[$catchall][] = $rel->text();
+            }
         }
     }
 }
diff --git a/lib/plugins/libkolab/lib/kolab_format_event.php b/lib/plugins/libkolab/lib/kolab_format_event.php
index 6a8c3ae..c233f44 100644
--- a/lib/plugins/libkolab/lib/kolab_format_event.php
+++ b/lib/plugins/libkolab/lib/kolab_format_event.php
@@ -26,6 +26,8 @@ class kolab_format_event extends kolab_format_xcal
 {
     public $CTYPEv2 = 'application/x-vnd.kolab.event';
 
+    public $scheduling_properties = array('start', 'end', 'allday', 'location', 'status', 'cancelled');
+
     protected $objclass = 'Event';
     protected $read_func = 'readEvent';
     protected $write_func = 'writeEvent';
@@ -87,10 +89,12 @@ class kolab_format_event extends kolab_format_xcal
             $status = kolabformat::StatusTentative;
         if ($object['cancelled'])
             $status = kolabformat::StatusCancelled;
+        else if ($object['status'] && array_key_exists($object['status'], $this->status_map))
+            $status = $this->status_map[$object['status']];
         $this->obj->setStatus($status);
 
         // save recurrence exceptions
-        if ($object['recurrence']['EXCEPTIONS']) {
+        if (is_array($object['recurrence']) && $object['recurrence']['EXCEPTIONS']) {
             $vexceptions = new vectorevent;
             foreach((array)$object['recurrence']['EXCEPTIONS'] as $exception) {
                 $exevent = new kolab_format_event;
@@ -191,16 +195,12 @@ class kolab_format_event extends kolab_format_xcal
      */
     public function get_tags()
     {
-        $tags = array();
+        $tags = parent::get_tags();
 
         foreach ((array)$this->data['categories'] as $cat) {
             $tags[] = rcube_utils::normalize_string($cat);
         }
 
-        if (!empty($this->data['alarms'])) {
-            $tags[] = 'x-has-alarms';
-        }
-
         return $tags;
     }
 
diff --git a/lib/plugins/libkolab/lib/kolab_format_file.php b/lib/plugins/libkolab/lib/kolab_format_file.php
index 5f73bf1..34c0ca6 100644
--- a/lib/plugins/libkolab/lib/kolab_format_file.php
+++ b/lib/plugins/libkolab/lib/kolab_format_file.php
@@ -25,7 +25,7 @@
 
 class kolab_format_file extends kolab_format
 {
-    public $CTYPE = 'application/x-vnd.kolab.file';
+    public $CTYPE = 'application/vnd.kolab+xml';
 
     protected $objclass = 'File';
     protected $read_func = 'kolabformat::readKolabFile';
diff --git a/lib/plugins/libkolab/lib/kolab_format_note.php b/lib/plugins/libkolab/lib/kolab_format_note.php
index 1f49dee..bca5156 100644
--- a/lib/plugins/libkolab/lib/kolab_format_note.php
+++ b/lib/plugins/libkolab/lib/kolab_format_note.php
@@ -24,9 +24,11 @@
 
 class kolab_format_note extends kolab_format
 {
-    public $CTYPE = 'application/x-vnd.kolab.note';
+    public $CTYPE = 'application/vnd.kolab+xml';
     public $CTYPEv2 = 'application/x-vnd.kolab.note';
 
+    public static $fulltext_cols = array('title', 'description', 'categories');
+
     protected $objclass = 'Note';
     protected $read_func = 'readNote';
     protected $write_func = 'writeNote';
@@ -107,11 +109,45 @@ class kolab_format_note extends kolab_format
     {
         $tags = array();
 
-        foreach ((array) $this->data['categories'] as $cat) {
+        foreach ((array)$this->data['categories'] as $cat) {
             $tags[] = rcube_utils::normalize_string($cat);
         }
 
+        // add tag for message references
+        foreach ((array)$this->data['links'] as $link) {
+            $url = parse_url($link);
+            if ($url['scheme'] == 'imap') {
+                parse_str($url['query'], $param);
+                $tags[] = 'ref:' . trim($param['message-id'] ?: urldecode($url['fragment']), '<> ');
+            }
+        }
+
         return $tags;
     }
 
+    /**
+     * Callback for kolab_storage_cache to get words to index for fulltext search
+     *
+     * @return array List of words to save in cache
+     */
+    public function get_words()
+    {
+        $data = '';
+        foreach (self::$fulltext_cols as $col) {
+            // convert HTML content to plain text
+            if ($col == 'description' && preg_match('/<(html|body)(\s[a-z]|>)/', $this->data[$col], $m) && strpos($this->data[$col], '</'.$m[1].'>')) {
+                $converter = new rcube_html2text($this->data[$col], false, false, 0);
+                $val = $converter->get_text();
+            }
+            else {
+                $val = is_array($this->data[$col]) ? join(' ', $this->data[$col]) : $this->data[$col];
+            }
+
+            if (strlen($val))
+                $data .= $val . ' ';
+        }
+
+        return array_filter(array_unique(rcube_utils::normalize_string($data, true)));
+    }
+
 }
diff --git a/lib/plugins/libkolab/lib/kolab_format_task.php b/lib/plugins/libkolab/lib/kolab_format_task.php
index 465ba90..52744d4 100644
--- a/lib/plugins/libkolab/lib/kolab_format_task.php
+++ b/lib/plugins/libkolab/lib/kolab_format_task.php
@@ -26,6 +26,8 @@ class kolab_format_task extends kolab_format_xcal
 {
     public $CTYPEv2 = 'application/x-vnd.kolab.task';
 
+    public $scheduling_properties = array('start', 'due', 'summary', 'status');
+
     protected $objclass = 'Todo';
     protected $read_func = 'readTodo';
     protected $write_func = 'writeTodo';
@@ -44,7 +46,7 @@ class kolab_format_task extends kolab_format_xcal
         $this->obj->setPercentComplete(intval($object['complete']));
 
         $status = kolabformat::StatusUndefined;
-        if ($object['complete'] == 100)
+        if ($object['complete'] == 100 && !array_key_exists('status', $object))
             $status = kolabformat::StatusCompleted;
         else if ($object['status'] && array_key_exists($object['status'], $this->status_map))
             $status = $this->status_map[$object['status']];
@@ -111,17 +113,14 @@ class kolab_format_task extends kolab_format_xcal
      */
     public function get_tags()
     {
-        $tags = array();
+        $tags = parent::get_tags();
 
-        if ($this->data['status'] == 'COMPLETED' || $this->data['complete'] == 100)
+        if ($this->data['status'] == 'COMPLETED' || ($this->data['complete'] == 100 && empty($this->data['status'])))
             $tags[] = 'x-complete';
 
         if ($this->data['priority'] == 1)
             $tags[] = 'x-flagged';
 
-        if (!empty($this->data['valarms']))
-            $tags[] = 'x-has-alarms';
-
         if ($this->data['parent_id'])
             $tags[] = 'x-parent:' . $this->data['parent_id'];
 
diff --git a/lib/plugins/libkolab/lib/kolab_format_xcal.php b/lib/plugins/libkolab/lib/kolab_format_xcal.php
index a4ec8ea..421ee92 100644
--- a/lib/plugins/libkolab/lib/kolab_format_xcal.php
+++ b/lib/plugins/libkolab/lib/kolab_format_xcal.php
@@ -30,6 +30,8 @@ abstract class kolab_format_xcal extends kolab_format
 
     public static $fulltext_cols = array('title', 'description', 'location', 'attendees:name', 'attendees:email', 'categories');
 
+    public $scheduling_properties = array('start', 'end', 'location');
+
     protected $sensitivity_map = array(
         'public'       => kolabformat::ClassPublic,
         'private'      => kolabformat::ClassPrivate,
@@ -81,6 +83,10 @@ abstract class kolab_format_xcal extends kolab_format
         'IN-PROCESS'   => kolabformat::StatusInProcess,
         'COMPLETED'    => kolabformat::StatusCompleted,
         'CANCELLED'    => kolabformat::StatusCancelled,
+        'TENTATIVE'    => kolabformat::StatusTentative,
+        'CONFIRMED'    => kolabformat::StatusConfirmed,
+        'DRAFT'        => kolabformat::StatusDraft,
+        'FINAL'        => kolabformat::StatusFinal,
     );
 
     protected $part_status_map = array(
@@ -121,6 +127,10 @@ abstract class kolab_format_xcal extends kolab_format
             'start'       => self::php_datetime($this->obj->start()),
         );
 
+        if (method_exists($this->obj, 'comment')) {
+            $object['comment'] = $this->obj->comment();
+        }
+
         // read organizer and attendees
         if (($organizer = $this->obj->organizer()) && ($organizer->email() || $organizer->name())) {
             $object['organizer'] = array(
@@ -213,27 +223,68 @@ abstract class kolab_format_xcal extends kolab_format
         // read alarm
         $valarms = $this->obj->alarms();
         $alarm_types = array_flip($this->alarm_type_map);
+        $object['valarms'] = array();
         for ($i=0; $i < $valarms->size(); $i++) {
             $alarm = $valarms->get($i);
             $type = $alarm_types[$alarm->type()];
 
-            if ($type == 'DISPLAY' || $type == 'EMAIL') {  // only DISPLAY and EMAIL alarms are supported
+            if ($type == 'DISPLAY' || $type == 'EMAIL' || $type == 'AUDIO') {  // only some alarms are supported
+                $valarm = array(
+                    'action' => $type,
+                    'summary' => $alarm->summary(),
+                    'description' => $alarm->description(),
+                );
+
+                if ($type == 'EMAIL') {
+                    $valarm['attendees'] = array();
+                    $attvec = $this->obj->attendees();
+                    for ($j=0; $j < $attvec->size(); $j++) {
+                        $cr = $attvec->get($j);
+                        $valarm['attendees'][] = $cr->email();
+                    }
+                }
+                else if ($type == 'AUDIO') {
+                    $attach = $alarm->audioFile();
+                    $valarm['uri'] = $attach->uri();
+                }
+
                 if ($start = self::php_datetime($alarm->start())) {
                     $object['alarms'] = '@' . $start->format('U');
+                    $valarm['trigger'] = $start;
                 }
                 else if ($offset = $alarm->relativeStart()) {
-                    $value = $alarm->relativeTo() == kolabformat::End ? '+' : '-';
+                    $prefix = $alarm->relativeTo() == kolabformat::End ? '+' : '-';
+                    $value = $time = '';
                     if      ($w = $offset->weeks())     $value .= $w . 'W';
                     else if ($d = $offset->days())      $value .= $d . 'D';
-                    else if ($h = $offset->hours())     $value .= $h . 'H';
-                    else if ($m = $offset->minutes())   $value .= $m . 'M';
-                    else if ($s = $offset->seconds())   $value .= $s . 'S';
-                    else continue;
+                    else if ($h = $offset->hours())     $time  .= $h . 'H';
+                    else if ($m = $offset->minutes())   $time  .= $m . 'M';
+                    else if ($s = $offset->seconds())   $time  .= $s . 'S';
+
+                    // assume 'at event time'
+                    if (empty($value) && empty($time)) {
+                        $prefix = '';
+                        $time = '0S';
+                    }
+
+                    $object['alarms'] = $prefix . $value . $time;
+                    $valarm['trigger'] = $prefix . 'P' . $value . ($time ? 'T' . $time : '');
+                }
 
-                    $object['alarms'] = $value;
+                // read alarm duration and repeat properties
+                if (($duration = $alarm->duration()) && $duration->isValid()) {
+                    $value = $time = '';
+                    if      ($w = $duration->weeks())     $value .= $w . 'W';
+                    else if ($d = $duration->days())      $value .= $d . 'D';
+                    else if ($h = $duration->hours())     $time  .= $h . 'H';
+                    else if ($m = $duration->minutes())   $time  .= $m . 'M';
+                    else if ($s = $duration->seconds())   $time  .= $s . 'S';
+                    $valarm['duration'] = 'P' . $value . ($time ? 'T' . $time : '');
+                    $valarm['repeat'] = $alarm->numrepeat();
                 }
-                $object['alarms']  .= ':' . $type;
-                break;
+
+                $object['alarms']  .= ':' . $type;  // legacy property
+                $object['valarms'][] = array_filter($valarm);
             }
         }
 
@@ -253,14 +304,43 @@ abstract class kolab_format_xcal extends kolab_format
         $this->init();
 
         $is_new = !$this->obj->uid();
+        $old_sequence = $this->obj->sequence();
+        $reschedule = $is_new;
 
         // set common object properties
         parent::set($object);
 
-        // increment sequence on updates
-        if (empty($object['sequence']))
-            $object['sequence'] = !$is_new ? $this->obj->sequence()+1 : 0;
-        $this->obj->setSequence($object['sequence']);
+        // set sequence value
+        if (!isset($object['sequence'])) {
+            if ($is_new) {
+                $object['sequence'] = 0;
+            }
+            else {
+                $object['sequence'] = $old_sequence;
+                $old = $this->data['uid'] ? $this->data : $this->to_array();
+
+                // increment sequence when updating properties relevant for scheduling.
+                // RFC 5545: "It is incremented [...] each time the Organizer makes a significant revision to the calendar component."
+                // TODO: make the list of properties considered 'significant' for scheduling configurable
+                foreach ($this->scheduling_properties as $prop) {
+                    $a = $old[$prop];
+                    $b = $object[$prop];
+                    if ($object['allday'] && ($prop == 'start' || $prop == 'end') && $a instanceof DateTime && $b instanceof DateTime) {
+                        $a = $a->format('Y-m-d');
+                        $b = $b->format('Y-m-d');
+                    }
+                    if ($a != $b) {
+                        $object['sequence']++;
+                        break;
+                    }
+                }
+            }
+        }
+        $this->obj->setSequence(intval($object['sequence']));
+
+        if ($object['sequence'] > $old_sequence) {
+            $reschedule = true;
+        }
 
         $this->obj->setSummary($object['title']);
         $this->obj->setLocation($object['location']);
@@ -270,9 +350,13 @@ abstract class kolab_format_xcal extends kolab_format
         $this->obj->setCategories(self::array2vector($object['categories']));
         $this->obj->setUrl(strval($object['url']));
 
+        if (method_exists($this->obj, 'setComment')) {
+            $this->obj->setComment($object['comment']);
+        }
+
         // process event attendees
         $attendees = new vectorattendee;
-        foreach ((array)$object['attendees'] as $attendee) {
+        foreach ((array)$object['attendees'] as $i => $attendee) {
             if ($attendee['role'] == 'ORGANIZER') {
                 $object['organizer'] = $attendee;
             }
@@ -285,7 +369,9 @@ abstract class kolab_format_xcal extends kolab_format
                 $att->setPartStat($this->part_status_map[$attendee['status']]);
                 $att->setRole($this->role_map[$attendee['role']] ? $this->role_map[$attendee['role']] : kolabformat::Required);
                 $att->setCutype($this->cutype_map[$attendee['cutype']] ? $this->cutype_map[$attendee['cutype']] : kolabformat::CutypeIndividual);
-                $att->setRSVP((bool)$attendee['rsvp']);
+                $att->setRSVP((bool)$attendee['rsvp'] || $reschedule);
+
+                $object['attendees'][$i]['rsvp'] = $attendee['rsvp'] || $reschedule;
 
                 if (!empty($attendee['delegated-from'])) {
                     $vdelegators = new vectorcontactref;
@@ -393,7 +479,67 @@ abstract class kolab_format_xcal extends kolab_format
 
         // save alarm
         $valarms = new vectoralarm;
-        if ($object['alarms']) {
+        if ($object['valarms']) {
+            foreach ($object['valarms'] as $valarm) {
+                if (!array_key_exists($valarm['action'], $this->alarm_type_map)) {
+                    continue;  // skip unknown alarm types
+                }
+
+                if ($valarm['action'] == 'EMAIL') {
+                    $recipients = new vectorcontactref;
+                    foreach (($valarm['attendees'] ?: array($object['_owner'])) as $email) {
+                        $recipients->push(new ContactReference(ContactReference::EmailReference, $email));
+                    }
+                    $alarm = new Alarm(
+                        strval($valarm['summary'] ?: $object['title']),
+                        strval($valarm['description'] ?: $object['description']),
+                        $recipients
+                    );
+                }
+                else if ($valarm['action'] == 'AUDIO') {
+                    $attach = new Attachment;
+                    $attach->setUri($valarm['uri'] ?: 'null', 'unknown');
+                    $alarm = new Alarm($attach);
+                }
+                else {
+                    // action == DISPLAY
+                    $alarm = new Alarm(strval($valarm['summary'] ?: $object['title']));
+                }
+
+                if (is_object($valarm['trigger']) && $valarm['trigger'] instanceof DateTime) {
+                    $alarm->setStart(self::get_datetime($valarm['trigger'], new DateTimeZone('UTC')));
+                }
+                else {
+                    try {
+                        $prefix = $valarm['trigger'][0];
+                        $period = new DateInterval(preg_replace('/[^0-9PTWDHMS]/', '', $valarm['trigger']));
+                        $duration = new Duration($period->d, $period->h, $period->i, $period->s, $prefix == '-');
+                    }
+                    catch (Exception $e) {
+                        // skip alarm with invalid trigger values
+                        rcube::raise_error($e, true);
+                        continue;
+                    }
+
+                    $alarm->setRelativeStart($duration, $prefix == '-' ? kolabformat::Start : kolabformat::End);
+                }
+
+                if ($valarm['duration']) {
+                    try {
+                        $d = new DateInterval($valarm['duration']);
+                        $duration = new Duration($d->d, $d->h, $d->i, $d->s);
+                        $alarm->setDuration($duration, intval($valarm['repeat']));
+                    }
+                    catch (Exception $e) {
+                        // ignore
+                    }
+                }
+
+                $valarms->push($alarm);
+            }
+        }
+        // legacy support
+        else if ($object['alarms']) {
             list($offset, $type) = explode(":", $object['alarms']);
 
             if ($type == 'EMAIL' && !empty($object['_owner'])) {  // email alarms implicitly go to event owner
@@ -455,4 +601,27 @@ abstract class kolab_format_xcal extends kolab_format
         return array_unique(rcube_utils::normalize_string($data, true));
     }
 
+    /**
+     * Callback for kolab_storage_cache to get object specific tags to cache
+     *
+     * @return array List of tags to save in cache
+     */
+    public function get_tags()
+    {
+        $tags = array();
+
+        if (!empty($this->data['valarms'])) {
+            $tags[] = 'x-has-alarms';
+        }
+
+        // create tags reflecting participant status
+        if (is_array($this->data['attendees'])) {
+            foreach ($this->data['attendees'] as $attendee) {
+                if (!empty($attendee['email']) && !empty($attendee['status']))
+                    $tags[] = 'x-partstat:' . $attendee['email'] . ':' . strtolower($attendee['status']);
+            }
+        }
+
+        return $tags;
+    }
 }
\ No newline at end of file
diff --git a/lib/plugins/libkolab/lib/kolab_storage.php b/lib/plugins/libkolab/lib/kolab_storage.php
index 3d1dccb..7287fc2 100644
--- a/lib/plugins/libkolab/lib/kolab_storage.php
+++ b/lib/plugins/libkolab/lib/kolab_storage.php
@@ -7,7 +7,7 @@
  * @author Thomas Bruederli <bruederli at kolabsys.com>
  * @author Aleksander Machniak <machniak at kolabsys.com>
  *
- * Copyright (C) 2012, Kolab Systems AG <contact at kolabsys.com>
+ * Copyright (C) 2012-2014, Kolab Systems AG <contact at kolabsys.com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
@@ -37,12 +37,16 @@ class kolab_storage
 
     public static $version = '3.0';
     public static $last_error;
+    public static $encode_ids = false;
 
     private static $ready = false;
+    private static $with_tempsubs = true;
     private static $subscriptions;
+    private static $typedata = array();
     private static $states;
     private static $config;
     private static $imap;
+    private static $ldap;
 
     // Default folder names
     private static $default_folders = array(
@@ -83,7 +87,6 @@ class kolab_storage
                 'skip_deleted' => true,
                 'threading' => false,
             ));
-            self::$imap->set_pagesize(9999);
         }
         else if (!class_exists('kolabformat')) {
             rcube::raise_error(array(
@@ -101,6 +104,41 @@ class kolab_storage
         return self::$ready;
     }
 
+    /**
+     * Initializes LDAP object to resolve Kolab users
+     */
+    public static function ldap()
+    {
+        if (self::$ldap) {
+            return self::$ldap;
+        }
+
+        self::setup();
+
+        $config = self::$config->get('kolab_users_directory', self::$config->get('kolab_auth_addressbook'));
+
+        if (!is_array($config)) {
+            $ldap_config = (array)self::$config->get('ldap_public');
+            $config = $ldap_config[$config];
+        }
+
+        if (empty($config)) {
+            return null;
+        }
+
+        // overwrite filter option
+        if ($filter = self::$config->get('kolab_users_filter')) {
+            self::$config->set('kolab_auth_filter', $filter);
+        }
+
+        // re-use the LDAP wrapper class from kolab_auth plugin
+        require_once rtrim(RCUBE_PLUGINS_DIR, '/') . '/kolab_auth/kolab_auth_ldap.php';
+
+        self::$ldap = new kolab_auth_ldap($config);
+
+        return self::$ldap;
+    }
+
 
     /**
      * Get a list of storage folders for the given data type
@@ -178,6 +216,33 @@ class kolab_storage
         return false;
     }
 
+    /**
+     * Execute cross-folder searches with the given query.
+     *
+     * @param array  Pseudo-SQL query as list of filter parameter triplets
+     * @param string Object type (contact,event,task,journal,file,note,configuration)
+     * @return array List of Kolab data objects (each represented as hash array)
+     * @see kolab_storage_format::select()
+     */
+    public static function select($query, $type)
+    {
+        self::setup();
+        $folder = null;
+        $result = array();
+
+        foreach ((array)self::list_folders('', '*', $type) as $foldername) {
+            if (!$folder)
+                $folder = new kolab_storage_folder($foldername);
+            else
+                $folder->set_folder($foldername);
+
+            foreach ($folder->select($query, '*') as $object) {
+                $result[] = $object;
+            }
+        }
+
+        return $result;
+    }
 
     /**
      *
@@ -200,13 +265,57 @@ class kolab_storage
     /**
      * Creates folder ID from folder name
      *
-     * @param string $folder Folder name (UTF7-IMAP)
-     *
+     * @param string  $folder Folder name (UTF7-IMAP)
+     * @param boolean $enc    Use lossless encoding
      * @return string Folder ID string
      */
-    public static function folder_id($folder)
+    public static function folder_id($folder, $enc = null)
+    {
+        return $enc == true || ($enc === null && self::$encode_ids) ?
+            self::id_encode($folder) :
+            asciiwords(strtr($folder, '/.-', '___'));
+    }
+
+
+    /**
+     * Encode the given ID to a safe ascii representation
+     *
+     * @param string $id Arbitrary identifier string
+     *
+     * @return string Ascii representation
+     */
+    public static function id_encode($id)
+    {
+        return rtrim(strtr(base64_encode($id), '+/', '-_'), '=');
+    }
+
+    /**
+     * Convert the given identifier back to it's raw value
+     *
+     * @param string $id Ascii identifier
+     * @return string Raw identifier string
+     */
+    public static function id_decode($id)
     {
-        return asciiwords(strtr($folder, '/.-', '___'));
+      return base64_decode(str_pad(strtr($id, '-_', '+/'), strlen($id) % 4, '=', STR_PAD_RIGHT));
+    }
+
+
+    /**
+     * Return the (first) path of the requested IMAP namespace
+     *
+     * @param string  Namespace name (personal, shared, other)
+     * @return string IMAP root path for that namespace
+     */
+    public static function namespace_root($name)
+    {
+        foreach ((array)self::$imap->get_namespace($name) as $paths) {
+            if (strlen($paths[0]) > 1) {
+                return $paths[0];
+            }
+        }
+
+        return '';
     }
 
 
@@ -223,6 +332,9 @@ class kolab_storage
         if ($folder = self::get_folder($name))
             $folder->cache->purge();
 
+        $rcmail = rcube::get_instance();
+        $plugin = $rcmail->plugins->exec_hook('folder_delete', array('name' => $name));
+
         $success = self::$imap->delete_folder($name);
         self::$last_error = self::$imap->get_error_str();
 
@@ -243,6 +355,12 @@ class kolab_storage
     {
         self::setup();
 
+        $rcmail = rcube::get_instance();
+        $plugin = $rcmail->plugins->exec_hook('folder_create', array('record' => array(
+            'name' => $name,
+            'subscribe' => $subscribed,
+        )));
+
         if ($saved = self::$imap->create_folder($name, $subscribed)) {
             // set metadata for folder type
             if ($type) {
@@ -280,6 +398,10 @@ class kolab_storage
     {
         self::setup();
 
+        $rcmail = rcube::get_instance();
+        $plugin = $rcmail->plugins->exec_hook('folder_rename', array(
+            'oldname' => $oldname, 'newname' => $newname));
+
         $oldfolder = self::get_folder($oldname);
         $active = self::folder_is_active($oldname);
         $success = self::$imap->rename_folder($oldname, $newname);
@@ -437,7 +559,7 @@ class kolab_storage
                         $folder = substr($folder, $pos+1);
                     }
                     else {
-                        $prefix = $folder;
+                        $prefix = '('.$folder.')';
                         $folder = '';
                     }
 
@@ -563,8 +685,17 @@ class kolab_storage
             $name = $c_folder->name;
 
             // skip current folder and it's subfolders
-            if ($len && ($name == $current || strpos($name, $current.$delim) === 0)) {
-                continue;
+            if ($len) {
+                if ($name == $current) {
+                    // Make sure parent folder is listed (might be skipped e.g. if it's namespace root)
+                    if ($p_len && !isset($names[$parent])) {
+                        $names[$parent] = self::object_name($parent);
+                    }
+                    continue;
+                }
+                if (strpos($name, $current.$delim) === 0) {
+                    continue;
+                }
             }
 
             // always show the parent of current folder
@@ -578,11 +709,6 @@ class kolab_storage
                 }
             }
 
-            // Make sure parent folder is listed (might be skipped e.g. if it's namespace root)
-            if ($p_len && !isset($names[$parent]) && strpos($name, $parent.$delim) === 0) {
-                $names[$parent] = self::object_name($parent);
-            }
-
             $names[$name] = self::object_name($name);
         }
 
@@ -639,18 +765,29 @@ class kolab_storage
         if (!$filter) {
             // Get ALL folders list, standard way
             if ($subscribed) {
-                return self::$imap->list_folders_subscribed($root, $mbox);
+                $folders = self::$imap->list_folders_subscribed($root, $mbox);
+                // add temporarily subscribed folders
+                if (self::$with_tempsubs && is_array($_SESSION['kolab_subscribed_folders'])) {
+                    $folders = array_unique(array_merge($folders, $_SESSION['kolab_subscribed_folders']));
+                }
             }
             else {
-                return self::$imap->list_folders($root, $mbox);
+                $folders = self::_imap_list_folders($root, $mbox);
             }
-        }
 
+            return $folders;
+        }
         $prefix = $root . $mbox;
         $regexp = '/^' . preg_quote($filter, '/') . '(\..+)?$/';
 
-        // get folders types
-        $folderdata = self::folders_typedata($prefix);
+        // get folders types for all folders
+        if (!$subscribed || $prefix == '*' || !self::$config->get('kolab_skip_namespace')) {
+            $folderdata = self::folders_typedata($prefix);
+        }
+        else {
+            // fetch folder types for the effective list of (subscribed) folders when post-filtering
+            $folderdata = array();
+        }
 
         if (!is_array($folderdata)) {
             return array();
@@ -670,9 +807,14 @@ class kolab_storage
         // Get folders list
         if ($subscribed) {
             $folders = self::$imap->list_folders_subscribed($root, $mbox);
+
+            // add temporarily subscribed folders
+            if (self::$with_tempsubs && is_array($_SESSION['kolab_subscribed_folders'])) {
+                $folders = array_unique(array_merge($folders, $_SESSION['kolab_subscribed_folders']));
+            }
         }
         else {
-            $folders = self::$imap->list_folders($root, $mbox);
+            $folders = self::_imap_list_folders($root, $mbox);
         }
 
         // In case of an error, return empty list (?)
@@ -682,6 +824,11 @@ class kolab_storage
 
         // Filter folders list
         foreach ($folders as $idx => $folder) {
+            // lookup folder type
+            if (!array_key_exists($folder, $folderdata)) {
+                $folderdata[$folder] = self::folder_type($folder);
+            }
+
             $type = $folderdata[$folder];
 
             if ($filter == 'mail' && empty($type)) {
@@ -695,6 +842,72 @@ class kolab_storage
         return $folders;
     }
 
+    /**
+     * Wrapper for rcube_imap::list_folders() with optional post-filtering
+     */
+    protected static function _imap_list_folders($root, $mbox)
+    {
+        $postfilter = null;
+
+        // compose a post-filter expression for the excluded namespaces
+        if ($root . $mbox == '*' && ($skip_ns = self::$config->get('kolab_skip_namespace'))) {
+            $excludes = array();
+            foreach ((array)$skip_ns as $ns) {
+                if ($ns_root = self::namespace_root($ns)) {
+                    $excludes[] = $ns_root;
+                }
+            }
+
+            if (count($excludes)) {
+                $postfilter = '!^(' . join(')|(', array_map('preg_quote', $excludes)) . ')!';
+            }
+        }
+
+        // use normal LIST command to return all folders, it's fast enough
+        $folders = self::$imap->list_folders($root, $mbox, null, null, !empty($postfilter));
+
+        if (!empty($postfilter)) {
+            $folders = array_filter($folders, function($folder) use ($postfilter) { return !preg_match($postfilter, $folder); });
+            $folders = self::$imap->sort_folder_list($folders);
+        }
+
+        return $folders;
+    }
+
+
+    /**
+     * Search for shared or otherwise not listed groupware folders the user has access
+     *
+     * @param string Folder type of folders to search for
+     * @param string Search string
+     * @param array  Namespace(s) to exclude results from
+     *
+     * @return array List of matching kolab_storage_folder objects
+     */
+    public static function search_folders($type, $query, $exclude_ns = array())
+    {
+        if (!self::setup()) {
+            return array();
+        }
+
+        $folders = array();
+        $query = str_replace('*', '', $query);
+
+        // find unsubscribed IMAP folders of the given type
+        foreach ((array)self::list_folders('', '*', $type, false, $folderdata) as $foldername) {
+            // FIXME: only consider the last part of the folder path for searching?
+            $realname = strtolower(rcube_charset::convert($foldername, 'UTF7-IMAP'));
+            if (($query == '' || strpos($realname, $query) !== false) &&
+                !self::folder_is_subscribed($foldername, true) &&
+                !in_array(self::$imap->folder_namespace($foldername), (array)$exclude_ns)
+              ) {
+                $folders[] = new kolab_storage_folder($foldername, $folderdata[$foldername]);
+            }
+        }
+
+        return $folders;
+    }
+
 
     /**
      * Sort the given list of kolab folders by namespace/name
@@ -716,7 +929,7 @@ class kolab_storage
 
         // $folders is a result of get_folders() we can assume folders were already sorted
         foreach (array_keys($nsnames) as $ns) {
-            // asort($nsnames[$ns], SORT_LOCALE_STRING);
+            asort($nsnames[$ns], SORT_LOCALE_STRING);
             foreach (array_keys($nsnames[$ns]) as $utf7name) {
                 $out[] = $folders[$utf7name];
             }
@@ -730,43 +943,65 @@ class kolab_storage
      * Check the folder tree and add the missing parents as virtual folders
      *
      * @param array $folders Folders list
+     * @param object $tree   Reference to the root node of the folder tree
      *
-     * @return array Folders list
+     * @return array Flat folders list
      */
-    public static function folder_hierarchy($folders)
+    public static function folder_hierarchy($folders, &$tree = null)
     {
         $_folders = array();
-        $existing = array_map(function($folder){ return $folder->get_name(); }, $folders);
-        $delim    = rcube::get_instance()->get_storage()->get_hierarchy_delimiter();
+        $delim    = self::$imap->get_hierarchy_delimiter();
+        $other_ns = rtrim(self::namespace_root('other'), $delim);
+        $tree     = new kolab_storage_folder_virtual('', '<root>', '');  // create tree root
+        $refs     = array('' => $tree);
 
         foreach ($folders as $idx => $folder) {
             $path = explode($delim, $folder->name);
             array_pop($path);
+            $folder->parent = join($delim, $path);
+            $folder->children = array();  // reset list
 
             // skip top folders or ones with a custom displayname
-            if (count($path) <= 1 || kolab_storage::custom_displayname($folder->name)) {
+            if (count($path) < 1 || kolab_storage::custom_displayname($folder->name)) {
+                $tree->children[] = $folder;
             }
             else {
                 $parents = array();
+                $depth = $folder->get_namespace() == 'personal' ? 1 : 2;
 
-                while (count($path) > 1 && ($parent = join($delim, $path))) {
-                    $name = kolab_storage::object_name($parent, $folder->get_namespace());
-                    if (!in_array($name, $existing)) {
-                        $parents[$parent] = new virtual_kolab_storage_folder($parent, $name, $folder->get_namespace());
-                        $existing[] = $name;
-                    }
-
+                while (count($path) >= $depth && ($parent = join($delim, $path))) {
                     array_pop($path);
+                    $parent_parent = join($delim, $path);
+                    if (!$refs[$parent]) {
+                        if ($folder->type && self::folder_type($parent) == $folder->type) {
+                            $refs[$parent] = new kolab_storage_folder($parent, $folder->type);
+                            $refs[$parent]->parent = $parent_parent;
+                        }
+                        else if ($parent_parent == $other_ns) {
+                            $refs[$parent] = new kolab_storage_folder_user($parent, $parent_parent);
+                        }
+                        else {
+                            $name = kolab_storage::object_name($parent, $folder->get_namespace());
+                            $refs[$parent] = new kolab_storage_folder_virtual($parent, $name, $folder->get_namespace(), $parent_parent);
+                        }
+                        $parents[] = $refs[$parent];
+                    }
                 }
 
                 if (!empty($parents)) {
-                    $parents = array_reverse(array_values($parents));
+                    $parents = array_reverse($parents);
                     foreach ($parents as $parent) {
+                        $parent_node = $refs[$parent->parent] ?: $tree;
+                        $parent_node->children[] = $parent;
                         $_folders[] = $parent;
                     }
                 }
+
+                $parent_node = $refs[$folder->parent] ?: $tree;
+                $parent_node->children[] = $folder;
             }
 
+            $refs[$folder->name] = $folder;
             $_folders[] = $folder;
             unset($folders[$idx]);
         }
@@ -788,13 +1023,56 @@ class kolab_storage
             return false;
         }
 
-        $folderdata = self::$imap->get_metadata($prefix, array(self::CTYPE_KEY, self::CTYPE_KEY_PRIVATE));
+        // return cached result
+        if (is_array(self::$typedata[$prefix])) {
+            return self::$typedata[$prefix];
+        }
+
+        $type_keys = array(self::CTYPE_KEY, self::CTYPE_KEY_PRIVATE);
+
+        // fetch metadata from *some* folders only
+        if (($prefix == '*' || $prefix == '') && ($skip_ns = self::$config->get('kolab_skip_namespace'))) {
+            $delimiter = self::$imap->get_hierarchy_delimiter();
+            $folderdata = $blacklist = array();
+            foreach ((array)$skip_ns as $ns) {
+                if ($ns_root = rtrim(self::namespace_root($ns), $delimiter)) {
+                    $blacklist[] = $ns_root;
+                }
+            }
+            foreach (array('personal','other','shared') as $ns) {
+                if (!in_array($ns, (array)$skip_ns)) {
+                    $ns_root = rtrim(self::namespace_root($ns), $delimiter);
+
+                    // list top-level folders and their childs one by one
+                    // GETMETADATA "%" doesn't list shared or other namespace folders but "*" would
+                    if ($ns_root == '') {
+                        foreach ((array)self::$imap->get_metadata('%', $type_keys) as $folder => $metadata) {
+                            if (!in_array($folder, $blacklist)) {
+                                $folderdata[$folder] = $metadata;
+                                if ($data = self::$imap->get_metadata($folder.$delimiter.'*', $type_keys)) {
+                                    $folderdata += $data;
+                                }
+                            }
+                        }
+                    }
+                    else if ($data = self::$imap->get_metadata($ns_root.$delimiter.'*', $type_keys)) {
+                        $folderdata += $data;
+                    }
+                }
+            }
+        }
+        else {
+            $folderdata = self::$imap->get_metadata($prefix, $type_keys);
+        }
 
         if (!is_array($folderdata)) {
             return false;
         }
 
-        return array_map(array('kolab_storage', 'folder_select_metadata'), $folderdata);
+        // keep list in memory
+        self::$typedata[$prefix] = array_map(array('kolab_storage', 'folder_select_metadata'), $folderdata);
+
+        return self::$typedata[$prefix];
     }
 
 
@@ -825,6 +1103,11 @@ class kolab_storage
     {
         self::setup();
 
+        // return in-memory cached result
+        if (is_array(self::$typedata['*']) && array_key_exists($folder, self::$typedata['*'])) {
+            return self::$typedata['*'][$folder];
+        }
+
         $metadata = self::$imap->get_metadata($folder, array(self::CTYPE_KEY, self::CTYPE_KEY_PRIVATE));
 
         if (!is_array($metadata)) {
@@ -866,17 +1149,21 @@ class kolab_storage
      * Check subscription status of this folder
      *
      * @param string $folder Folder name
+     * @param boolean $temp  Include temporary/session subscriptions
      *
      * @return boolean True if subscribed, false if not
      */
-    public static function folder_is_subscribed($folder)
+    public static function folder_is_subscribed($folder, $temp = false)
     {
         if (self::$subscriptions === null) {
             self::setup();
+            self::$with_tempsubs = false;
             self::$subscriptions = self::$imap->list_folders_subscribed();
+            self::$with_tempsubs = true;
         }
 
-        return in_array($folder, self::$subscriptions);
+        return in_array($folder, self::$subscriptions) ||
+            ($temp && in_array($folder, (array)$_SESSION['kolab_subscribed_folders']));
     }
 
 
@@ -884,14 +1171,25 @@ class kolab_storage
      * Change subscription status of this folder
      *
      * @param string $folder Folder name
+     * @param boolean $temp  Only subscribe temporarily for the current session
      *
      * @return True on success, false on error
      */
-    public static function folder_subscribe($folder)
+    public static function folder_subscribe($folder, $temp = false)
     {
         self::setup();
 
-        if (self::$imap->subscribe($folder)) {
+        // temporary/session subscription
+        if ($temp) {
+            if (self::folder_is_subscribed($folder)) {
+                return true;
+            }
+            else if (!is_array($_SESSION['kolab_subscribed_folders']) || !in_array($folder, $_SESSION['kolab_subscribed_folders'])) {
+                $_SESSION['kolab_subscribed_folders'][] = $folder;
+                return true;
+            }
+        }
+        else if (self::$imap->subscribe($folder)) {
             self::$subscriptions === null;
             return true;
         }
@@ -904,14 +1202,22 @@ class kolab_storage
      * Change subscription status of this folder
      *
      * @param string $folder Folder name
+     * @param boolean $temp  Only remove temporary subscription
      *
      * @return True on success, false on error
      */
-    public static function folder_unsubscribe($folder)
+    public static function folder_unsubscribe($folder, $temp = false)
     {
         self::setup();
 
-        if (self::$imap->unsubscribe($folder)) {
+        // temporary/session subscription
+        if ($temp) {
+            if (is_array($_SESSION['kolab_subscribed_folders']) && ($i = array_search($folder, $_SESSION['kolab_subscribed_folders'])) !== false) {
+                unset($_SESSION['kolab_subscribed_folders'][$i]);
+            }
+            return true;
+        }
+        else if (self::$imap->unsubscribe($folder)) {
             self::$subscriptions === null;
             return true;
         }
@@ -944,6 +1250,8 @@ class kolab_storage
      */
     public static function folder_activate($folder)
     {
+        // activation implies temporary subscription
+        self::folder_subscribe($folder, true);
         return self::set_state($folder, true);
     }
 
@@ -957,6 +1265,9 @@ class kolab_storage
      */
     public static function folder_deactivate($folder)
     {
+        // remove from temp subscriptions, really?
+        self::folder_unsubscribe($folder, true);
+
         return self::set_state($folder, false);
     }
 
@@ -980,7 +1291,9 @@ class kolab_storage
         else {
             self::setup();
             if (self::$subscriptions === null) {
+                self::$with_tempsubs = false;
                 self::$subscriptions = self::$imap->list_folders_subscribed();
+                self::$with_tempsubs = true;
             }
             self::$states = self::$subscriptions;
             $folders = implode(self::$states, '**');
@@ -1037,7 +1350,7 @@ class kolab_storage
 
         // check if we have any folder in personal namespace
         // folder(s) may exist but not subscribed
-        foreach ($folders as $f => $data) {
+        foreach ((array)$folders as $f => $data) {
             if (strpos($data[self::CTYPE_KEY_PRIVATE], $type) === 0) {
                 $folder = $f;
                 break;
@@ -1115,34 +1428,129 @@ class kolab_storage
         }
     }
 
-}
 
-/**
- * Helper class that represents a virtual IMAP folder
- * with a subset of the kolab_storage_folder API.
- */
-class virtual_kolab_storage_folder
-{
-    public $id;
-    public $name;
-    public $namespace;
-    public $virtual = true;
+    /**
+     *
+     * @param mixed   $query    Search value (or array of field => value pairs)
+     * @param int     $mode     Matching mode: 0 - partial (*abc*), 1 - strict (=), 2 - prefix (abc*)
+     * @param array   $required List of fields that shall ot be empty
+     * @param int     $limit    Maximum number of records
+     * @param int     $count    Returns the number of records found
+     *
+     * @return array List or false on error
+     */
+    public static function search_users($query, $mode = 1, $required = array(), $limit = 0, &$count = 0)
+    {
+        $query = str_replace('*', '', $query);
+
+        // requires a working LDAP setup
+        if (!self::ldap() || strlen($query) == 0) {
+            return array();
+        }
 
-    public function __construct($realname, $name, $ns)
+        // search users using the configured attributes
+        $results = self::$ldap->dosearch(self::$config->get('kolab_users_search_attrib', array('cn','mail','alias')), $query, $mode, $required, $limit, $count);
+
+        // exclude myself
+        if ($_SESSION['kolab_dn']) {
+            unset($results[$_SESSION['kolab_dn']]);
+        }
+
+        // resolve to IMAP folder name
+        $root = self::namespace_root('other');
+        $user_attrib = self::$config->get('kolab_users_id_attrib', self::$config->get('kolab_auth_login', 'mail'));
+
+        array_walk($results, function(&$user, $dn) use ($root, $user_attrib) {
+            list($localpart, $domain) = explode('@', $user[$user_attrib]);
+            $user['kolabtargetfolder'] = $root . $localpart;
+        });
+
+        return $results;
+    }
+
+
+    /**
+     * Returns a list of IMAP folders shared by the given user
+     *
+     * @param array   User entry from LDAP
+     * @param string  Data type to list folders for (contact,event,task,journal,file,note,mail,configuration)
+     * @param boolean Return subscribed folders only (null to use configured subscription mode)
+     * @param array   Will be filled with folder-types data
+     *
+     * @return array List of folders
+     */
+    public static function list_user_folders($user, $type, $subscribed = null, &$folderdata = array())
     {
-        $this->id        = kolab_storage::folder_id($realname);
-        $this->name      = $name;
-        $this->namespace = $ns;
+        self::setup();
+
+        $folders = array();
+
+        // use localpart of user attribute as root for folder listing
+        $user_attrib = self::$config->get('kolab_users_id_attrib', self::$config->get('kolab_auth_login', 'mail'));
+        if (!empty($user[$user_attrib])) {
+            list($mbox) = explode('@', $user[$user_attrib]);
+
+            $delimiter = self::$imap->get_hierarchy_delimiter();
+            $other_ns = self::namespace_root('other');
+            $folders = self::list_folders($other_ns . $mbox . $delimiter, '*', $type, $subscribed, $folderdata);
+        }
+
+        return $folders;
     }
 
-    public function get_namespace()
+
+    /**
+     * Get a list of (virtual) top-level folders from the other users namespace
+     *
+     * @param string  Data type to list folders for (contact,event,task,journal,file,note,mail,configuration)
+     * @param boolean Enable to return subscribed folders only (null to use configured subscription mode)
+     *
+     * @return array List of kolab_storage_folder_user objects
+     */
+    public static function get_user_folders($type, $subscribed)
     {
-        return $this->namespace;
+        $folders = $folderdata = array();
+
+        if (self::setup()) {
+            $delimiter = self::$imap->get_hierarchy_delimiter();
+            $other_ns = rtrim(self::namespace_root('other'), $delimiter);
+            $path_len = count(explode($delimiter, $other_ns));
+
+            foreach ((array)self::list_folders($other_ns . $delimiter, '*', '', $subscribed) as $foldername) {
+                if ($foldername == 'INBOX')  // skip INBOX which is added by default
+                    continue;
+
+                $path = explode($delimiter, $foldername);
+
+                // compare folder type if a subfolder is listed
+                if ($type && count($path) > $path_len + 1 && $type != self::folder_type($foldername)) {
+                    continue;
+                }
+
+                // truncate folder path to top-level folders of the 'other' namespace
+                $foldername = join($delimiter, array_slice($path, 0, $path_len + 1));
+
+                if (!$folders[$foldername]) {
+                    $folders[$foldername] = new kolab_storage_folder_user($foldername, $other_ns);
+                }
+            }
+        }
+
+        return $folders;
     }
 
-    public function get_name()
+
+    /**
+     * Handler for user_delete plugin hooks
+     *
+     * Remove all cache data from the local database related to the given user.
+     */
+    public static function delete_user_folders($args)
     {
-        // this is already kolab_storage::object_name() result
-        return $this->name;
+        $db = rcmail::get_instance()->get_dbh();
+        $prefix = 'imap://' . urlencode($args['username']) . '@' . $args['host'] . '/%';
+        $db->query("DELETE FROM " . $db->table_name('kolab_folders') . " WHERE resource LIKE ?", $prefix);
     }
+
 }
+
diff --git a/lib/plugins/libkolab/lib/kolab_storage_cache.php b/lib/plugins/libkolab/lib/kolab_storage_cache.php
index 9c1368f..d56f04d 100644
--- a/lib/plugins/libkolab/lib/kolab_storage_cache.php
+++ b/lib/plugins/libkolab/lib/kolab_storage_cache.php
@@ -491,7 +491,7 @@ class kolab_storage_cache
                 else if ($fetchall && ($object = $this->_unserialize($sql_arr))) {
                     $result[] = $object;
                 }
-                else {
+                else if (!$fetchall) {
                     // only add msguid to dataset index
                     $result[] = $sql_arr;
                 }
@@ -633,7 +633,7 @@ class kolab_storage_cache
                 $qvalue = $this->db->quote('%'.preg_replace('/(^\^|\$$)/', ' ', $param[2]).'%');
             }
             else if ($param[0] == 'tags') {
-                $param[1] = 'LIKE';
+                $param[1] = ($param[1] == '!=' ? 'NOT ' : '' ) . 'LIKE';
                 $qvalue = $this->db->quote('% '.$param[2].' %');
             }
             else {
@@ -761,12 +761,15 @@ class kolab_storage_cache
             }
         }
 
+        $object_type = $sql_arr['type'] ?: $this->folder->type;
+        $format_type = $this->folder->type == 'configuration' ? 'configuration' : $object_type;
+
         // add meta data
-        $object['_type']      = $sql_arr['type'] ?: $this->folder->type;
+        $object['_type']      = $object_type;
         $object['_msguid']    = $sql_arr['msguid'];
         $object['_mailbox']   = $this->folder->name;
         $object['_size']      = strlen($sql_arr['xml']);
-        $object['_formatobj'] = kolab_format::factory($object['_type'], 3.0, $sql_arr['xml']);
+        $object['_formatobj'] = kolab_format::factory($format_type, 3.0, $sql_arr['xml']);
 
         return $object;
     }
@@ -911,12 +914,28 @@ class kolab_storage_cache
      */
     public function uid2msguid($uid, $deleted = false)
     {
+        // query local database if available
+        if (!isset($this->uid2msg[$uid]) && $this->ready) {
+            $this->_read_folder_data();
+
+            $sql_result = $this->db->query(
+                "SELECT msguid FROM $this->cache_table ".
+                "WHERE folder_id=? AND uid=? ORDER BY msguid DESC",
+                $this->folder_id,
+                $uid
+            );
+
+            if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
+                $this->uid2msg[$uid] = $sql_arr['msguid'];
+            }
+        }
+
         if (!isset($this->uid2msg[$uid])) {
             // use IMAP SEARCH to get the right message
             $index = $this->imap->search_once($this->folder->name, ($deleted ? '' : 'UNDELETED ') .
                 'HEADER SUBJECT ' . rcube_imap_generic::escape($uid));
             $results = $index->get();
-            $this->uid2msg[$uid] = $results[0];
+            $this->uid2msg[$uid] = end($results);
         }
 
         return $this->uid2msg[$uid];
diff --git a/lib/plugins/libkolab/lib/kolab_storage_cache_configuration.php b/lib/plugins/libkolab/lib/kolab_storage_cache_configuration.php
index 8380aa8..ec015dd 100644
--- a/lib/plugins/libkolab/lib/kolab_storage_cache_configuration.php
+++ b/lib/plugins/libkolab/lib/kolab_storage_cache_configuration.php
@@ -37,4 +37,30 @@ class kolab_storage_cache_configuration extends kolab_storage_cache
 
         return $sql_data;
     }
-}
\ No newline at end of file
+
+    /**
+     * Helper method to compose a valid SQL query from pseudo filter triplets
+     */
+    protected function _sql_where($query)
+    {
+        if (is_array($query)) {
+            foreach ($query as $idx => $param) {
+                // convert category filter
+                if ($param[0] == 'category') {
+                    $param[2] = array_map(function($n) { return 'category:' . $n; }, (array) $param[2]);
+
+                    $query[$idx][0] = 'tags';
+                    $query[$idx][2] = count($param[2]) > 1 ? $param[2] : $param[2][0];
+                }
+                // convert member filter (we support only = operator with single value)
+                else if ($param[0] == 'member') {
+                    $query[$idx][0] = 'words';
+                    $query[$idx][1] = '~';
+                    $query[$idx][2] = '^' . $param[2] . '$';
+                }
+            }
+        }
+
+        return parent::_sql_where($query);
+    }
+}
diff --git a/lib/plugins/libkolab/lib/kolab_storage_config.php b/lib/plugins/libkolab/lib/kolab_storage_config.php
new file mode 100644
index 0000000..9bc5d50
--- /dev/null
+++ b/lib/plugins/libkolab/lib/kolab_storage_config.php
@@ -0,0 +1,723 @@
+<?php
+
+/**
+ * Kolab storage class providing access to configuration objects on a Kolab server.
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ * @author Aleksander Machniak <machniak at kolabsys.com>
+ *
+ * Copyright (C) 2012-2014, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class kolab_storage_config
+{
+    const FOLDER_TYPE = 'configuration';
+
+
+    /**
+     * Singleton instace of kolab_storage_config
+     *
+     * @var kolab_storage_config
+     */
+    static protected $instance;
+
+    private $folders;
+    private $default;
+    private $enabled;
+
+
+    /**
+     * This implements the 'singleton' design pattern
+     *
+     * @return kolab_storage_config The one and only instance
+     */
+    static function get_instance()
+    {
+        if (!self::$instance) {
+            self::$instance = new kolab_storage_config();
+        }
+
+        return self::$instance;
+    }
+
+    /**
+     * Private constructor (finds default configuration folder as a config source)
+     */
+    private function __construct()
+    {
+        // get all configuration folders
+        $this->folders = kolab_storage::get_folders(self::FOLDER_TYPE, false);
+
+        foreach ($this->folders as $folder) {
+            if ($folder->default) {
+                $this->default = $folder;
+                break;
+            }
+        }
+
+        // if no folder is set as default, choose the first one
+        if (!$this->default) {
+            $this->default = reset($this->folders);
+        }
+
+        // attempt to create a default folder if it does not exist
+        if (!$this->default) {
+            $folder_name = 'Configuration';
+            $folder_type = self::FOLDER_TYPE . '.default';
+
+            if (kolab_storage::folder_create($folder_name, $folder_type, true)) {
+                $this->default = new kolab_storage_folder($folder_name, $folder_type);
+            }
+        }
+
+        // check if configuration folder exist
+        if ($this->default && $this->default->name) {
+            $this->enabled = true;
+        }
+    }
+
+    /**
+     * Check wether any configuration storage (folder) exists
+     *
+     * @return bool
+     */
+    public function is_enabled()
+    {
+        return $this->enabled;
+    }
+
+    /**
+     * Get configuration objects
+     *
+     * @param array $filter  Search filter
+     * @param bool  $default Enable to get objects only from default folder
+     * @param int   $limit   Max. number of records (per-folder)
+     *
+     * @return array List of objects
+     */
+    public function get_objects($filter = array(), $default = false, $limit = 0)
+    {
+        $list = array();
+
+        foreach ($this->folders as $folder) {
+            // we only want to read from default folder
+            if ($default && !$folder->default) {
+                continue;
+            }
+
+            // for better performance it's good to assume max. number of records
+            if ($limit) {
+                $folder->set_order_and_limit(null, $limit);
+            }
+
+            foreach ($folder->select($filter) as $object) {
+                $list[] = $object;
+            }
+        }
+
+        return $list;
+    }
+
+    /**
+     * Get configuration object
+     *
+     * @param string $uid     Object UID
+     * @param bool   $default Enable to get objects only from default folder
+     *
+     * @return array Object data
+     */
+    public function get_object($uid, $default = false)
+    {
+        foreach ($this->folders as $folder) {
+            // we only want to read from default folder
+            if ($default && !$folder->default) {
+                continue;
+            }
+
+            if ($object = $folder->get_object($uid)) {
+                return $object;
+            }
+        }
+    }
+
+    /**
+     * Create/update configuration object
+     *
+     * @param array  $object Object data
+     * @param string $type   Object type
+     *
+     * @return bool True on success, False on failure
+     */
+    public function save(&$object, $type)
+    {
+        if (!$this->enabled) {
+            return false;
+        }
+
+        $folder = $this->find_folder($object);
+
+        if ($type) {
+            $object['type'] = $type;
+        }
+
+        return $folder->save($object, self::FOLDER_TYPE . '.' . $object['type'], $object['uid']);
+    }
+
+    /**
+     * Remove configuration object
+     *
+     * @param string $uid Object UID
+     *
+     * @return bool True on success, False on failure
+     */
+    public function delete($uid)
+    {
+        if (!$this->enabled) {
+            return false;
+        }
+
+        // fetch the object to find folder
+        $object = $this->get_object($uid);
+
+        if (!$object) {
+            return false;
+        }
+
+        $folder = $this->find_folder($object);
+
+        return $folder->delete($uid);
+    }
+
+    /**
+     * Find folder
+     */
+    public function find_folder($object = array())
+    {
+        // find folder object
+        if ($object['_mailbox']) {
+            foreach ($this->folders as $folder) {
+                if ($folder->name == $object['_mailbox']) {
+                    break;
+                }
+            }
+        }
+        else {
+            $folder = $this->default;
+        }
+
+        return $folder;
+    }
+
+    /**
+     * Builds relation member URI
+     *
+     * @param string|array Object UUID or Message folder, UID, Search headers (Message-Id, Date)
+     *
+     * @return string $url Member URI
+     */
+    public static function build_member_url($params)
+    {
+        // param is object UUID
+        if (is_string($params) && !empty($params)) {
+            return 'urn:uuid:' . $params;
+        }
+
+        if (empty($params) || !strlen($params['folder'])) {
+            return null;
+        }
+
+        $rcube   = rcube::get_instance();
+        $storage = $rcube->get_storage();
+
+        // modify folder spec. according to namespace
+        $folder = $params['folder'];
+        $ns     = $storage->folder_namespace($folder);
+
+        if ($ns == 'shared') {
+            // Note: this assumes there's only one shared namespace root
+            if ($ns = $storage->get_namespace('shared')) {
+                if ($prefix = $ns[0][0]) {
+                    $folder = 'shared' . substr($folder, strlen($prefix));
+                }
+            }
+        }
+        else {
+            if ($ns == 'other') {
+                // Note: this assumes there's only one other users namespace root
+                if ($ns = $storage->get_namespace('shared')) {
+                    if ($prefix = $ns[0][0]) {
+                        $folder = 'user' . substr($folder, strlen($prefix));
+                    }
+                }
+            }
+            else {
+                $folder = 'user' . '/' . $rcube->get_user_name() . '/' . $folder;
+            }
+        }
+
+        $folder = implode('/', array_map('rawurlencode', explode('/', $folder)));
+
+        // build URI
+        $url = 'imap:///' . $folder;
+
+        // UID is optional here because sometimes we want
+        // to build just a member uri prefix
+        if ($params['uid']) {
+            $url .= '/' . $params['uid'];
+        }
+
+        unset($params['folder']);
+        unset($params['uid']);
+
+        if (!empty($params)) {
+            $url .= '?' . http_build_query($params, '', '&');
+        }
+
+        return $url;
+    }
+
+    /**
+     * Parses relation member string
+     *
+     * @param string $url Member URI
+     *
+     * @return array Message folder, UID, Search headers (Message-Id, Date)
+     */
+    public static function parse_member_url($url)
+    {
+        // Look for IMAP URI:
+        // imap:///(user/username@domain|shared)/<folder>/<UID>?<search_params>
+        if (strpos($url, 'imap:///') === 0) {
+            $rcube   = rcube::get_instance();
+            $storage = $rcube->get_storage();
+
+            // parse_url does not work with imap:/// prefix
+            $url   = parse_url(substr($url, 8));
+            $path  = explode('/', $url['path']);
+            parse_str($url['query'], $params);
+
+            $uid  = array_pop($path);
+            $ns   = array_shift($path);
+            $path = array_map('rawurldecode', $path);
+
+            // resolve folder name
+            if ($ns == 'shared') {
+                $folder = implode('/', $path);
+                // Note: this assumes there's only one shared namespace root
+                if ($ns = $storage->get_namespace('shared')) {
+                    if ($prefix = $ns[0][0]) {
+                        $folder = $prefix . '/' . $folder;
+                    }
+                }
+            }
+            else if ($ns == 'user') {
+                $username = array_shift($path);
+                $folder   = implode('/', $path);
+
+                if ($username != $rcube->get_user_name()) {
+                    // Note: this assumes there's only one other users namespace root
+                    if ($ns = $storage->get_namespace('other')) {
+                        if ($prefix = $ns[0][0]) {
+                            $folder = $prefix . '/' . $username . '/' . $folder;
+                        }
+                    }
+                }
+                else if (!strlen($folder)) {
+                    $folder = 'INBOX';
+                }
+            }
+            else {
+                return;
+            }
+
+            return array(
+                'folder' => $folder,
+                'uid'    => $uid,
+                'params' => $params,
+            );
+        }
+    }
+
+    /**
+     * Build array of member URIs from set of messages
+     *
+     * @param string $folder   Folder name
+     * @param array  $messages Array of rcube_message objects
+     *
+     * @return array List of members (IMAP URIs)
+     */
+    public static function build_members($folder, $messages)
+    {
+        $members = array();
+
+        foreach ((array) $messages as $msg) {
+            $params = array(
+                'folder' => $folder,
+                'uid'    => $msg->uid,
+            );
+
+            // add search parameters:
+            // we don't want to build "invalid" searches e.g. that
+            // will return false positives (more or wrong messages)
+            if (($messageid = $msg->get('message-id', false)) && ($date = $msg->get('date', false))) {
+                $params['message-id'] = $messageid;
+                $params['date']       = $date;
+
+                if ($subject = $msg->get('subject', false)) {
+                    $params['subject'] = substr($subject, 0, 256);
+                }
+            }
+
+            $members[] = self::build_member_url($params);
+        }
+
+        return $members;
+    }
+
+    /**
+     * Resolve/validate/update members (which are IMAP URIs) of relation object.
+     *
+     * @param array $tag   Tag object
+     * @param bool  $force Force members list update
+     *
+     * @return array Folder/UIDs list
+     */
+    public static function resolve_members(&$tag, $force = true)
+    {
+        $result = array();
+
+        foreach ((array) $tag['members'] as $member) {
+            // IMAP URI members
+            if ($url = self::parse_member_url($member)) {
+                $folder = $url['folder'];
+
+                if (!$force) {
+                    $result[$folder][] = $url['uid'];
+                }
+                else {
+                    $result[$folder]['uid'][]    = $url['uid'];
+                    $result[$folder]['params'][] = $url['params'];
+                    $result[$folder]['member'][] = $member;
+                }
+            }
+        }
+
+        if (empty($result) || !$force) {
+            return $result;
+        }
+
+        $rcube   = rcube::get_instance();
+        $storage = $rcube->get_storage();
+        $search  = array();
+        $missing = array();
+
+        // first we search messages by Folder+UID
+        foreach ($result as $folder => $data) {
+            // @FIXME: maybe better use index() which is cached?
+            // @TODO: consider skip_deleted option
+            $index = $storage->search_once($folder, 'UID ' . rcube_imap_generic::compressMessageSet($data['uid']));
+            $uids  = $index->get();
+
+            // messages that were not found need to be searched by search parameters
+            $not_found = array_diff($data['uid'], $uids);
+            if (!empty($not_found)) {
+                foreach ($not_found as $uid) {
+                    $idx = array_search($uid, $data['uid']);
+
+                    if ($p = $data['params'][$idx]) {
+                        $search[] = $p;
+                    }
+
+                    $missing[] = $result[$folder]['member'][$idx];
+
+                    unset($result[$folder]['uid'][$idx]);
+                    unset($result[$folder]['params'][$idx]);
+                    unset($result[$folder]['member'][$idx]);
+                }
+            }
+
+            $result[$folder] = $uids;
+        }
+
+        // search in all subscribed mail folders using search parameters
+        if (!empty($search)) {
+            // remove not found members from the members list
+            $tag['members'] = array_diff($tag['members'], $missing);
+
+            // get subscribed folders
+            $folders = $storage->list_folders_subscribed('', '*', 'mail', null, true);
+
+            // @TODO: do this search in chunks (for e.g. 10 messages)?
+            $search_str = '';
+
+            foreach ($search as $p) {
+                $search_params = array();
+                foreach ($p as $key => $val) {
+                    $key = strtoupper($key);
+                    // don't search by subject, we don't want false-positives
+                    if ($key != 'SUBJECT') {
+                        $search_params[] = 'HEADER ' . $key . ' ' . rcube_imap_generic::escape($val);
+                    }
+                }
+
+                $search_str .= ' (' . implode(' ', $search_params) . ')';
+            }
+
+            $search_str = trim(str_repeat(' OR', count($search)-1) . $search_str);
+
+            // search
+            $search = $storage->search_once($folders, $search_str);
+
+            // handle search result
+            $folders = (array) $search->get_parameters('MAILBOX');
+
+            foreach ($folders as $folder) {
+                $set  = $search->get_set($folder);
+                $uids = $set->get();
+
+                if (!empty($uids)) {
+                    $msgs    = $storage->fetch_headers($folder, $uids, false);
+                    $members = self::build_members($folder, $msgs);
+
+                    // merge new members into the tag members list
+                    $tag['members'] = array_merge($tag['members'], $members);
+
+                    // add UIDs into the result
+                    $result[$folder] = array_unique(array_merge((array)$result[$folder], $uids));
+                }
+            }
+
+            // update tag object with new members list
+            $tag['members'] = array_unique($tag['members']);
+            kolab_storage_config::get_instance()->save($tag, 'relation', false);
+        }
+
+        return $result;
+    }
+
+    /**
+     * Assign tags to kolab objects
+     *
+     * @param array $records List of kolab objects
+     *
+     * @return array List of tags
+     */
+    public function apply_tags(&$records)
+    {
+        // first convert categories into tags
+        foreach ($records as $i => $rec) {
+            if (!empty($rec['categories'])) {
+                $folder = new kolab_storage_folder($rec['_mailbox']);
+                if ($object = $folder->get_object($rec['uid'])) {
+                    $tags = $rec['categories'];
+
+                    unset($object['categories']);
+                    unset($records[$i]['categories']);
+
+                    $this->save_tags($rec['uid'], $tags);
+                    $folder->save($object, $rec['_type'], $rec['uid']);
+                }
+            }
+        }
+
+        $tags = array();
+
+        // assign tags to objects
+        foreach ($this->get_tags() as $tag) {
+            foreach ($records as $idx => $rec) {
+                $uid = self::build_member_url($rec['uid']);
+                if (in_array($uid, (array) $tag['members'])) {
+                    $records[$idx]['tags'][] = $tag['name'];
+                }
+            }
+
+            $tags[] = $tag['name'];
+        }
+
+        $tags = array_unique($tags);
+
+        return $tags;
+    }
+
+    /**
+     * Update object tags
+     *
+     * @param string $uid  Kolab object UID
+     * @param array  $tags List of tag names
+     */
+    public function save_tags($uid, $tags)
+    {
+        $url       = self::build_member_url($uid);
+        $relations = $this->get_tags();
+
+        foreach ($relations as $idx => $relation) {
+            $selected = !empty($tags) && in_array($relation['name'], $tags);
+            $found    = !empty($relation['members']) && in_array($url, $relation['members']);
+            $update   = false;
+
+            // remove member from the relation
+            if ($found && !$selected) {
+                $relation['members'] = array_diff($relation['members'], (array) $url);
+                $update = true;
+            }
+            // add member to the relation
+            else if (!$found && $selected) {
+                $relation['members'][] = $url;
+                $update = true;
+            }
+
+            if ($update) {
+                if ($this->save($relation, 'relation')) {
+                    $this->tags[$idx] = $relation; // update in-memory cache
+                }
+            }
+
+            if ($selected) {
+                $tags = array_diff($tags, (array)$relation['name']);
+            }
+        }
+
+        // create new relations
+        if (!empty($tags)) {
+            foreach ($tags as $tag) {
+                $relation = array(
+                    'name'     => $tag,
+                    'members'  => (array) $url,
+                    'category' => 'tag',
+                );
+
+                if ($this->save($relation, 'relation')) {
+                    $this->tags[] = $relation; // update in-memory cache
+                }
+            }
+        }
+    }
+
+    /**
+     * Get tags (all or referring to specified object)
+     *
+     * @param string $uid Optional object UID
+     *
+     * @return array List of Relation objects
+     */
+    public function get_tags($uid = '*')
+    {
+        if (!isset($this->tags)) {
+            $default = true;
+            $filter  = array(
+                array('type', '=', 'relation'),
+                array('category', '=', 'tag')
+            );
+
+            // use faster method
+            if ($uid && $uid != '*') {
+                $filter[] = array('member', '=', $uid);
+                return $this->get_objects($filter, $default);
+            }
+
+            $this->tags = $this->get_objects($filter, $default);
+        }
+
+        if ($uid === '*') {
+            return $this->tags;
+        }
+
+        $result = array();
+        $search = self::build_member_url($uid);
+
+        foreach ($this->tags as $tag) {
+            if (in_array($search, (array) $tag['members'])) {
+                $result[] = $tag;
+            }
+        }
+
+        return $result;
+    }
+
+    /**
+     * Find kolab objects assigned to specified e-mail message
+     *
+     * @param rcube_message $message E-mail message
+     * @param string        $folder  Folder name
+     * @param string        $type    Result objects type
+     *
+     * @return array List of kolab objects
+     */
+    public function get_message_relations($message, $folder, $type)
+    {
+        $result  = array();
+        $uids    = array();
+        $default = true;
+        $uri     = self::get_message_uri($message, $folder);
+        $filter  = array(
+            array('type', '=', 'relation'),
+            array('category', '=', 'generic'),
+            // @TODO: what if Message-Id (and Date) does not exist?
+            array('member', '=', $message->get('message-id', false)),
+        );
+
+        // get UIDs of assigned notes
+        foreach ($this->get_objects($filter, $default) as $relation) {
+            // we don't need to update members if the URI is found
+            if (in_array($uri, $relation['members'])) {
+                // update members...
+                $messages = kolab_storage_config::resolve_members($relation);
+                // ...and check again
+                if (empty($messages[$folder]) || !in_array($message->uid, $messages[$folder])) {
+                    continue;
+                }
+            }
+
+            // find note UID(s)
+            foreach ($relation['members'] as $member) {
+                if (strpos($member, 'urn:uuid:') === 0) {
+                    $uids[] = substr($member, 9);
+                }
+            }
+        }
+
+        // get kolab objects of specified type
+        if (!empty($uids)) {
+            $query  = array(array('uid', '=', array_unique($uids)));
+            $result = kolab_storage::select($query, $type);
+        }
+
+        return $result;
+    }
+
+    /**
+     * Build a URI representing the given message reference
+     */
+    public static function get_message_uri($headers, $folder)
+    {
+        $params = array(
+            'folder' => $headers->folder ?: $folder,
+            'uid'    => $headers->uid,
+        );
+
+        if (($messageid = $headers->get('message-id', false)) && ($date = $headers->get('date', false))) {
+            $params['message-id'] = $messageid;
+            $params['date']       = $date;
+
+            if ($subject = $headers->get('subject')) {
+                $params['subject'] = $subject;
+            }
+        }
+
+        return self::build_member_url($params);
+    }
+}
diff --git a/lib/plugins/libkolab/lib/kolab_storage_folder.php b/lib/plugins/libkolab/lib/kolab_storage_folder.php
index 1580314..ad6d5c0 100644
--- a/lib/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/lib/plugins/libkolab/lib/kolab_storage_folder.php
@@ -22,38 +22,15 @@
  * You should have received a copy of the GNU Affero General Public License
  * along with this program. If not, see <http://www.gnu.org/licenses/>.
  */
-class kolab_storage_folder
+class kolab_storage_folder extends kolab_storage_folder_api
 {
     /**
-     * The folder name.
-     * @var string
-     */
-    public $name;
-
-    /**
-     * The type of this folder.
-     * @var string
-     */
-    public $type;
-
-    /**
-     * Is this folder set to be the default for its type
-     * @var boolean
-     */
-    public $default = false;
-
-    /**
      * The kolab_storage_cache instance for caching operations
      * @var object
      */
     public $cache;
 
     private $type_annotation;
-    private $namespace;
-    private $imap;
-    private $info;
-    private $idata;
-    private $owner;
     private $resource_uri;
 
 
@@ -62,7 +39,7 @@ class kolab_storage_folder
      */
     function __construct($name, $type = null)
     {
-        $this->imap = rcube::get_instance()->get_storage();
+        parent::__construct($name);
         $this->imap->set_options(array('skip_deleted' => true));
         $this->set_folder($name, $type);
     }
@@ -82,7 +59,10 @@ class kolab_storage_folder
         list($this->type, $suffix) = explode('.', $this->type_annotation);
         $this->default      = $suffix == 'default';
         $this->name         = $name;
-        $this->resource_uri = null;
+        $this->id           = kolab_storage::folder_id($name);
+
+        // reset cached object properties
+        $this->owner = $this->namespace = $this->resource_uri = $this->info = $this->idata = null;
 
         // get a new cache instance of folder type changed
         if (!$this->cache || $type != $oldtype)
@@ -92,148 +72,6 @@ class kolab_storage_folder
         $this->cache->set_folder($this);
     }
 
-    /**
-     *
-     */
-    public function get_folder_info()
-    {
-        if (!isset($this->info))
-            $this->info = $this->imap->folder_info($this->name);
-
-        return $this->info;
-    }
-
-    /**
-     * Make IMAP folder data available for this folder
-     */
-    public function get_imap_data()
-    {
-        if (!isset($this->idata))
-            $this->idata = $this->imap->folder_data($this->name);
-
-        return $this->idata;
-    }
-
-    /**
-     * Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION)
-     *
-     * @param array List of metadata keys to read
-     * @return array Metadata entry-value hash array on success, NULL on error
-     */
-    public function get_metadata($keys)
-    {
-        $metadata = $this->imap->get_metadata($this->name, (array)$keys);
-        return $metadata[$this->name];
-    }
-
-
-    /**
-     * Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
-     *
-     * @param array  $entries Entry-value array (use NULL value as NIL)
-     * @return boolean True on success, False on failure
-     */
-    public function set_metadata($entries)
-    {
-        return $this->imap->set_metadata($this->name, $entries);
-    }
-
-
-    /**
-     * Returns the owner of the folder.
-     *
-     * @return string  The owner of this folder.
-     */
-    public function get_owner()
-    {
-        // return cached value
-        if (isset($this->owner))
-            return $this->owner;
-
-        $info = $this->get_folder_info();
-        $rcmail = rcube::get_instance();
-
-        switch ($info['namespace']) {
-        case 'personal':
-            $this->owner = $rcmail->get_user_name();
-            break;
-
-        case 'shared':
-            $this->owner = 'anonymous';
-            break;
-
-        default:
-            list($prefix, $user) = explode($this->imap->get_hierarchy_delimiter(), $info['name']);
-            if (strpos($user, '@') === false) {
-                $domain = strstr($rcmail->get_user_name(), '@');
-                if (!empty($domain))
-                    $user .= $domain;
-            }
-            $this->owner = $user;
-            break;
-        }
-
-        return $this->owner;
-    }
-
-
-    /**
-     * Getter for the name of the namespace to which the IMAP folder belongs
-     *
-     * @return string Name of the namespace (personal, other, shared)
-     */
-    public function get_namespace()
-    {
-        if (!isset($this->namespace))
-            $this->namespace = $this->imap->folder_namespace($this->name);
-        return $this->namespace;
-    }
-
-
-    /**
-     * Get IMAP ACL information for this folder
-     *
-     * @return string  Permissions as string
-     */
-    public function get_myrights()
-    {
-        $rights = $this->info['rights'];
-
-        if (!is_array($rights))
-            $rights = $this->imap->my_rights($this->name);
-
-        return join('', (array)$rights);
-    }
-
-
-    /**
-     * Get the display name value of this folder
-     *
-     * @return string Folder name
-     */
-    public function get_name()
-    {
-        return kolab_storage::object_name($this->name, $this->namespace);
-    }
-
-
-    /**
-     * Get the color value stored in metadata
-     *
-     * @param string Default color value to return if not set
-     * @return mixed Color value from IMAP metadata or $default is not set
-     */
-    public function get_color($default = null)
-    {
-        // color is defined in folder METADATA
-        $metadata = $this->get_metadata(array(kolab_storage::COLOR_KEY_PRIVATE, kolab_storage::COLOR_KEY_SHARED));
-        if (($color = $metadata[kolab_storage::COLOR_KEY_PRIVATE]) || ($color = $metadata[kolab_storage::COLOR_KEY_SHARED])) {
-            return $color;
-        }
-
-        return $default;
-    }
-
 
     /**
      * Compose a unique resource URI for this IMAP folder
@@ -508,7 +346,30 @@ class kolab_storage_folder
     {
         if ($msguid = ($mailbox ? $uid : $this->cache->uid2msguid($uid))) {
             $this->imap->set_folder($mailbox ? $mailbox : $this->name);
-            return $this->imap->get_message_part($msguid, $part, null, $print, $fp, $skip_charset_conv);
+
+            if (substr($part, 0, 2) == 'i:') {
+                // attachment data is stored in XML
+                if ($object = $this->cache->get($msguid)) {
+                    // load data from XML (attachment content is not stored in cache)
+                    if ($object['_formatobj'] && isset($object['_size'])) {
+                        $object['_attachments'] = array();
+                        $object['_formatobj']->get_attachments($object);
+                    }
+
+                    foreach ($object['_attachments'] as $k => $attach) {
+                        if ($attach['id'] == $part) {
+                            if ($print)   echo $attach['content'];
+                            else if ($fp) fwrite($fp, $attach['content']);
+                            else          return $attach['content'];
+                            return true;
+                        }
+                    }
+                }
+            }
+            else {
+                // return message part from IMAP directly
+                return $this->imap->get_message_part($msguid, $part, null, $print, $fp, $skip_charset_conv);
+            }
         }
 
         return null;
@@ -578,7 +439,7 @@ class kolab_storage_folder
 
         // get XML part
         foreach ((array)$message->attachments as $part) {
-            if (!$xml && ($part->mimetype == $content_type || preg_match('!application/([a-z]+\+)?xml!', $part->mimetype))) {
+            if (!$xml && ($part->mimetype == $content_type || preg_match('!application/([a-z.]+\+)?xml!', $part->mimetype))) {
                 $xml = $part->body ? $part->body : $message->get_part_content($part->mime_id);
             }
             else if ($part->filename || $part->content_id) {
diff --git a/lib/plugins/libkolab/lib/kolab_storage_folder_api.php b/lib/plugins/libkolab/lib/kolab_storage_folder_api.php
new file mode 100644
index 0000000..ef3309e
--- /dev/null
+++ b/lib/plugins/libkolab/lib/kolab_storage_folder_api.php
@@ -0,0 +1,330 @@
+<?php
+
+/**
+ * Abstract interface class for Kolab storage IMAP folder objects
+ *
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2014, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+abstract class kolab_storage_folder_api
+{
+    /**
+     * Folder identifier
+     * @var string
+     */
+    public $id;
+
+    /**
+     * The folder name.
+     * @var string
+     */
+    public $name;
+
+    /**
+     * The type of this folder.
+     * @var string
+     */
+    public $type;
+
+    /**
+     * Is this folder set to be the default for its type
+     * @var boolean
+     */
+    public $default = false;
+
+    /**
+     * List of direct child folders
+     * @var array
+     */
+    public $children = array();
+    
+    /**
+     * Name of the parent folder
+     * @var string
+     */
+    public $parent = '';
+
+    protected $imap;
+    protected $owner;
+    protected $info;
+    protected $idata;
+    protected $namespace;
+
+
+    /**
+     * Private constructor
+     */
+    protected function __construct($name)
+    {
+      $this->name = $name;
+      $this->id   = kolab_storage::folder_id($name);
+      $this->imap = rcube::get_instance()->get_storage();
+    }
+
+
+    /**
+     * Returns the owner of the folder.
+     *
+     * @return string  The owner of this folder.
+     */
+    public function get_owner()
+    {
+        // return cached value
+        if (isset($this->owner))
+            return $this->owner;
+
+        $info = $this->get_folder_info();
+        $rcmail = rcube::get_instance();
+
+        switch ($info['namespace']) {
+        case 'personal':
+            $this->owner = $rcmail->get_user_name();
+            break;
+
+        case 'shared':
+            $this->owner = 'anonymous';
+            break;
+
+        default:
+            list($prefix, $user) = explode($this->imap->get_hierarchy_delimiter(), $info['name']);
+            if (strpos($user, '@') === false) {
+                $domain = strstr($rcmail->get_user_name(), '@');
+                if (!empty($domain))
+                    $user .= $domain;
+            }
+            $this->owner = $user;
+            break;
+        }
+
+        return $this->owner;
+    }
+
+
+    /**
+     * Getter for the name of the namespace to which the IMAP folder belongs
+     *
+     * @return string Name of the namespace (personal, other, shared)
+     */
+    public function get_namespace()
+    {
+        if (!isset($this->namespace))
+            $this->namespace = $this->imap->folder_namespace($this->name);
+        return $this->namespace;
+    }
+
+
+    /**
+     * Get the display name value of this folder
+     *
+     * @return string Folder name
+     */
+    public function get_name()
+    {
+        return kolab_storage::object_name($this->name, $this->get_namespace());
+    }
+
+
+    /**
+     * Getter for the top-end folder name (not the entire path)
+     *
+     * @return string Name of this folder
+     */
+    public function get_foldername()
+    {
+        $parts = explode('/', $this->name);
+        return rcube_charset::convert(end($parts), 'UTF7-IMAP');
+    }
+
+    /**
+     * Getter for parent folder path
+     *
+     * @return string Full path to parent folder
+     */
+    public function get_parent()
+    {
+        $path = explode('/', $this->name);
+        array_pop($path);
+
+        // don't list top-level namespace folder
+        if (count($path) == 1 && in_array($this->get_namespace(), array('other', 'shared'))) {
+            $path = array();
+        }
+
+        return join('/', $path);
+    }
+
+    /**
+     * Getter for the Cyrus mailbox identifier corresponding to this folder
+     * (e.g. user/john.doe/Calendar/Personal at example.org)
+     *
+     * @return string Mailbox ID
+     */
+    public function get_mailbox_id()
+    {
+        $info = $this->get_folder_info();
+        $owner = $this->get_owner();
+        list($user, $domain) = explode('@', $owner);
+
+        switch ($info['namespace']) {
+        case 'personal':
+            return sprintf('user/%s/%s@%s', $user, $this->name, $domain);
+
+        case 'shared':
+            $ns = $this->imap->get_namespace('shared');
+            $prefix = is_array($ns) ? $ns[0][0] : '';
+            list(, $domain) = explode('@', rcube::get_instance()->get_user_name());
+            return substr($this->name, strlen($prefix)) . '@' . $domain;
+
+        default:
+            $ns = $this->imap->get_namespace('other');
+            $prefix = is_array($ns) ? $ns[0][0] : '';
+            list($user, $folder) = explode($this->imap->get_hierarchy_delimiter(), substr($info['name'], strlen($prefix)), 2);
+            if (strpos($user, '@')) {
+                list($user, $domain) = explode('@', $user);
+            }
+            return sprintf('user/%s/%s@%s', $user, $folder, $domain);
+        }
+    }
+
+    /**
+     * Get the color value stored in metadata
+     *
+     * @param string Default color value to return if not set
+     * @return mixed Color value from IMAP metadata or $default is not set
+     */
+    public function get_color($default = null)
+    {
+        // color is defined in folder METADATA
+        $metadata = $this->get_metadata(array(kolab_storage::COLOR_KEY_PRIVATE, kolab_storage::COLOR_KEY_SHARED));
+        if (($color = $metadata[kolab_storage::COLOR_KEY_PRIVATE]) || ($color = $metadata[kolab_storage::COLOR_KEY_SHARED])) {
+            return $color;
+        }
+
+        return $default;
+    }
+
+
+    /**
+     * Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION)
+     *
+     * @param array List of metadata keys to read
+     * @return array Metadata entry-value hash array on success, NULL on error
+     */
+    public function get_metadata($keys)
+    {
+        $metadata = rcube::get_instance()->get_storage()->get_metadata($this->name, (array)$keys);
+        return $metadata[$this->name];
+    }
+
+
+    /**
+     * Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
+     *
+     * @param array  $entries Entry-value array (use NULL value as NIL)
+     * @return boolean True on success, False on failure
+     */
+    public function set_metadata($entries)
+    {
+        return $this->imap->set_metadata($this->name, $entries);
+    }
+
+
+    /**
+     *
+     */
+    public function get_folder_info()
+    {
+        if (!isset($this->info))
+            $this->info = $this->imap->folder_info($this->name);
+
+        return $this->info;
+    }
+
+    /**
+     * Make IMAP folder data available for this folder
+     */
+    public function get_imap_data()
+    {
+        if (!isset($this->idata))
+            $this->idata = $this->imap->folder_data($this->name);
+
+        return $this->idata;
+    }
+
+
+    /**
+     * Get IMAP ACL information for this folder
+     *
+     * @return string  Permissions as string
+     */
+    public function get_myrights()
+    {
+        $rights = $this->info['rights'];
+
+        if (!is_array($rights))
+            $rights = $this->imap->my_rights($this->name);
+
+        return join('', (array)$rights);
+    }
+
+
+    /**
+     * Check activation status of this folder
+     *
+     * @return boolean True if enabled, false if not
+     */
+    public function is_active()
+    {
+        return kolab_storage::folder_is_active($this->name);
+    }
+
+    /**
+     * Change activation status of this folder
+     *
+     * @param boolean The desired subscription status: true = active, false = not active
+     *
+     * @return True on success, false on error
+     */
+    public function activate($active)
+    {
+        return $active ? kolab_storage::folder_activate($this->name) : kolab_storage::folder_deactivate($this->name);
+    }
+
+    /**
+     * Check subscription status of this folder
+     *
+     * @return boolean True if subscribed, false if not
+     */
+    public function is_subscribed()
+    {
+        return kolab_storage::folder_is_subscribed($this->name);
+    }
+
+    /**
+     * Change subscription status of this folder
+     *
+     * @param boolean The desired subscription status: true = subscribed, false = not subscribed
+     *
+     * @return True on success, false on error
+     */
+    public function subscribe($subscribed)
+    {
+        return $subscribed ? kolab_storage::folder_subscribe($this->name) : kolab_storage::folder_unsubscribe($this->name);
+    }
+
+}
+
diff --git a/lib/plugins/libkolab/lib/kolab_storage_folder_user.php b/lib/plugins/libkolab/lib/kolab_storage_folder_user.php
new file mode 100644
index 0000000..1c37da9
--- /dev/null
+++ b/lib/plugins/libkolab/lib/kolab_storage_folder_user.php
@@ -0,0 +1,111 @@
+<?php
+
+/**
+ * Class that represents a (virtual) folder in the 'other' namespace
+ * implementing a subset of the kolab_storage_folder API.
+ *
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2014, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+class kolab_storage_folder_user extends kolab_storage_folder_virtual
+{
+    protected static $ldapcache = array();
+
+    public $ldaprec;
+
+    /**
+     * Default constructor
+     */
+    public function __construct($name, $parent = '', $ldaprec = null)
+    {
+        parent::__construct($name, $name, 'other', $parent);
+
+        if (!empty($ldaprec)) {
+            self::$ldapcache[$name] = $this->ldaprec = $ldaprec;
+        }
+        // use value cached in memory for repeated lookups
+        else if (array_key_exists($name, self::$ldapcache)) {
+            $this->ldaprec = self::$ldapcache[$name];
+        }
+        // lookup user in LDAP and set $this->ldaprec
+        else if ($ldap = kolab_storage::ldap()) {
+            // get domain from current user
+            list(,$domain) = explode('@', rcube::get_instance()->get_user_name());
+            $this->ldaprec = $ldap->get_user_record(parent::get_foldername($this->name) . '@' . $domain, $_SESSION['imap_host']);
+            if (!empty($this->ldaprec)) {
+                $this->ldaprec['kolabtargetfolder'] = $name;
+            }
+            self::$ldapcache[$name] = $this->ldaprec;
+        }
+    }
+
+    /**
+     * Getter for the top-end folder name to be displayed
+     *
+     * @return string Name of this folder
+     */
+    public function get_foldername()
+    {
+        return $this->ldaprec ? ($this->ldaprec['displayname'] ?: $this->ldaprec['name']) :
+            parent::get_foldername();
+    }
+
+    /**
+     * Getter for a more informative title of this user folder
+     *
+     * @return string Title for the given user record
+     */
+    public function get_title()
+    {
+      return trim($this->ldaprec['displayname'] . '; ' . $this->ldaprec['mail'], '; ');
+    }
+
+    /**
+     * Returns the owner of the folder.
+     *
+     * @return string  The owner of this folder.
+     */
+    public function get_owner()
+    {
+        return $this->ldaprec['mail'];
+    }
+
+    /**
+     * Check subscription status of this folder
+     *
+     * @return boolean True if subscribed, false if not
+     */
+    public function is_subscribed()
+    {
+        return kolab_storage::folder_is_subscribed($this->name, true);
+    }
+
+    /**
+     * Change subscription status of this folder
+     *
+     * @param boolean The desired subscription status: true = subscribed, false = not subscribed
+     *
+     * @return True on success, false on error
+     */
+    public function subscribe($subscribed)
+    {
+        return $subscribed ?
+            kolab_storage::folder_subscribe($this->name, true) :
+            kolab_storage::folder_unsubscribe($this->name, true);
+    }
+
+}
\ No newline at end of file
diff --git a/lib/plugins/libkolab/lib/kolab_storage_folder_virtual.php b/lib/plugins/libkolab/lib/kolab_storage_folder_virtual.php
new file mode 100644
index 0000000..e419ced
--- /dev/null
+++ b/lib/plugins/libkolab/lib/kolab_storage_folder_virtual.php
@@ -0,0 +1,59 @@
+<?php
+
+/**
+ * Helper class that represents a virtual IMAP folder
+ * with a subset of the kolab_storage_folder API.
+ *
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2014, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+class kolab_storage_folder_virtual extends kolab_storage_folder_api
+{
+    public $virtual = true;
+
+    protected $displayname;
+
+    public function __construct($name, $dispname, $ns, $parent = '')
+    {
+        parent::__construct($name);
+
+        $this->namespace = $ns;
+        $this->parent    = $parent;
+        $this->displayname = $dispname;
+    }
+
+    /**
+     * Get the display name value of this folder
+     *
+     * @return string Folder name
+     */
+    public function get_name()
+    {
+        return $this->displayname ?: parent::get_name();
+    }
+
+    /**
+     * Get the color value stored in metadata
+     *
+     * @param string Default color value to return if not set
+     * @return mixed Color value from IMAP metadata or $default is not set
+     */
+    public function get_color($default = null)
+    {
+        return $default;
+    }
+}
\ No newline at end of file
diff --git a/lib/plugins/libkolab/libkolab.php b/lib/plugins/libkolab/libkolab.php
index 48a5033..052724c 100644
--- a/lib/plugins/libkolab/libkolab.php
+++ b/lib/plugins/libkolab/libkolab.php
@@ -37,12 +37,13 @@ class libkolab extends rcube_plugin
         // load local config
         $this->load_config();
 
-        $this->add_hook('storage_init', array($this, 'storage_init'));
-
         // extend include path to load bundled lib classes
         $include_path = $this->home . '/lib' . PATH_SEPARATOR . ini_get('include_path');
         set_include_path($include_path);
 
+        $this->add_hook('storage_init', array($this, 'storage_init'));
+        $this->add_hook('user_delete', array('kolab_storage', 'delete_user_folders'));
+
         $rcmail = rcube::get_instance();
         try {
             kolab_format::$timezone = new DateTimeZone($rcmail->config->get('timezone', 'GMT'));
@@ -123,4 +124,15 @@ class libkolab extends rcube_plugin
 
         return $request;
     }
+
+    /**
+     * Wrapper function for generating a html diff using the FineDiff class by Raymond Hill
+     */
+    public static function html_diff($from, $to)
+    {
+      include_once __dir__ . '/vendor/finediff.php';
+
+      $diff = new FineDiff($from, $to, FineDiff::$wordGranularity);
+      return $diff->renderDiffToHTML();
+    }
 }
diff --git a/lib/plugins/libkolab/vendor/finediff.php b/lib/plugins/libkolab/vendor/finediff.php
new file mode 100644
index 0000000..b3c416c
--- /dev/null
+++ b/lib/plugins/libkolab/vendor/finediff.php
@@ -0,0 +1,688 @@
+<?php
+/**
+* FINE granularity DIFF
+*
+* Computes a set of instructions to convert the content of
+* one string into another.
+*
+* Copyright (c) 2011 Raymond Hill (http://raymondhill.net/blog/?p=441)
+*
+* Licensed under The MIT License
+* 
+* Permission is hereby granted, free of charge, to any person obtaining a copy
+* of this software and associated documentation files (the "Software"), to deal
+* in the Software without restriction, including without limitation the rights
+* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+* copies of the Software, and to permit persons to whom the Software is
+* furnished to do so, subject to the following conditions:
+*
+* The above copyright notice and this permission notice shall be included in
+* all copies or substantial portions of the Software.
+*
+* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+* THE SOFTWARE.
+*
+* @copyright Copyright 2011 (c) Raymond Hill (http://raymondhill.net/blog/?p=441)
+* @link http://www.raymondhill.net/finediff/
+* @version 0.6
+* @license MIT License (http://www.opensource.org/licenses/mit-license.php)
+*/
+
+/**
+* Usage (simplest):
+*
+*   include 'finediff.php';
+*
+*   // for the stock stack, granularity values are:
+*   // FineDiff::$paragraphGranularity = paragraph/line level
+*   // FineDiff::$sentenceGranularity = sentence level
+*   // FineDiff::$wordGranularity = word level
+*   // FineDiff::$characterGranularity = character level [default]
+*
+*   $opcodes = FineDiff::getDiffOpcodes($from_text, $to_text [, $granularityStack = null] );
+*   // store opcodes for later use...
+*
+*   ...
+*
+*   // restore $to_text from $from_text + $opcodes
+*   include 'finediff.php';
+*   $to_text = FineDiff::renderToTextFromOpcodes($from_text, $opcodes);
+*
+*   ...
+*/
+
+/**
+* Persisted opcodes (string) are a sequence of atomic opcode.
+* A single opcode can be one of the following:
+*   c | c{n} | d | d{n} | i:{c} | i{length}:{s}
+*   'c'        = copy one character from source
+*   'c{n}'     = copy n characters from source
+*   'd'        = skip one character from source
+*   'd{n}'     = skip n characters from source
+*   'i:{c}     = insert character 'c'
+*   'i{n}:{s}' = insert string s, which is of length n
+*
+* Do not exist as of now, under consideration:
+*   'm{n}:{o}  = move n characters from source o characters ahead.
+*   It would be essentially a shortcut for a delete->copy->insert
+*   command (swap) for when the inserted segment is exactly the same
+*   as the deleted one, and with only a copy operation in between.
+*   TODO: How often this case occurs? Is it worth it? Can only
+*   be done as a postprocessing method (->optimize()?)
+*/
+abstract class FineDiffOp {
+	abstract public function getFromLen();
+	abstract public function getToLen();
+	abstract public function getOpcode();
+	}
+
+class FineDiffDeleteOp extends FineDiffOp {
+	public function __construct($len) {
+		$this->fromLen = $len;
+		}
+	public function getFromLen() {
+		return $this->fromLen;
+		}
+	public function getToLen() {
+		return 0;
+		}
+	public function getOpcode() {
+		if ( $this->fromLen === 1 ) {
+			return 'd';
+			}
+		return "d{$this->fromLen}";
+		}
+	}
+
+class FineDiffInsertOp extends FineDiffOp {
+	public function __construct($text) {
+		$this->text = $text;
+		}
+	public function getFromLen() {
+		return 0;
+		}
+	public function getToLen() {
+		return strlen($this->text);
+		}
+	public function getText() {
+		return $this->text;
+		}
+	public function getOpcode() {
+		$to_len = strlen($this->text);
+		if ( $to_len === 1 ) {
+			return "i:{$this->text}";
+			}
+		return "i{$to_len}:{$this->text}";
+		}
+	}
+
+class FineDiffReplaceOp extends FineDiffOp {
+	public function __construct($fromLen, $text) {
+		$this->fromLen = $fromLen;
+		$this->text = $text;
+		}
+	public function getFromLen() {
+		return $this->fromLen;
+		}
+	public function getToLen() {
+		return strlen($this->text);
+		}
+	public function getText() {
+		return $this->text;
+		}
+	public function getOpcode() {
+		if ( $this->fromLen === 1 ) {
+			$del_opcode = 'd';
+			}
+		else {
+			$del_opcode = "d{$this->fromLen}";
+			}
+		$to_len = strlen($this->text);
+		if ( $to_len === 1 ) {
+			return "{$del_opcode}i:{$this->text}";
+			}
+		return "{$del_opcode}i{$to_len}:{$this->text}";
+		}
+	}
+
+class FineDiffCopyOp extends FineDiffOp {
+	public function __construct($len) {
+		$this->len = $len;
+		}
+	public function getFromLen() {
+		return $this->len;
+		}
+	public function getToLen() {
+		return $this->len;
+		}
+	public function getOpcode() {
+		if ( $this->len === 1 ) {
+			return 'c';
+			}
+		return "c{$this->len}";
+		}
+	public function increase($size) {
+		return $this->len += $size;
+		}
+	}
+
+/**
+* FineDiff ops
+*
+* Collection of ops
+*/
+class FineDiffOps {
+	public function appendOpcode($opcode, $from, $from_offset, $from_len) {
+		if ( $opcode === 'c' ) {
+			$edits[] = new FineDiffCopyOp($from_len);
+			}
+		else if ( $opcode === 'd' ) {
+			$edits[] = new FineDiffDeleteOp($from_len);
+			}
+		else /* if ( $opcode === 'i' ) */ {
+			$edits[] = new FineDiffInsertOp(substr($from, $from_offset, $from_len));
+			}
+		}
+	public $edits = array();
+	}
+
+/**
+* FineDiff class
+*
+* TODO: Document
+*
+*/
+class FineDiff {
+
+	/**------------------------------------------------------------------------
+	*
+	* Public section
+	*
+	*/
+
+	/**
+	* Constructor
+	* ...
+	* The $granularityStack allows FineDiff to be configurable so that
+	* a particular stack tailored to the specific content of a document can
+	* be passed.
+	*/
+	public function __construct($from_text = '', $to_text = '', $granularityStack = null) {
+		// setup stack for generic text documents by default
+		$this->granularityStack = $granularityStack ? $granularityStack : FineDiff::$characterGranularity;
+		$this->edits = array();
+		$this->from_text = $from_text;
+		$this->doDiff($from_text, $to_text);
+		}
+
+	public function getOps() {
+		return $this->edits;
+		}
+
+	public function getOpcodes() {
+		$opcodes = array();
+		foreach ( $this->edits as $edit ) {
+			$opcodes[] = $edit->getOpcode();
+			}
+		return implode('', $opcodes);
+		}
+
+	public function renderDiffToHTML() {
+		$in_offset = 0;
+		$html = '';
+		foreach ( $this->edits as $edit ) {
+			$n = $edit->getFromLen();
+			if ( $edit instanceof FineDiffCopyOp ) {
+				$html .= FineDiff::renderDiffToHTMLFromOpcode('c', $this->from_text, $in_offset, $n);
+				}
+			else if ( $edit instanceof FineDiffDeleteOp ) {
+				$html .= FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n);
+				}
+			else if ( $edit instanceof FineDiffInsertOp ) {
+				$html .= FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen());
+				}
+			else /* if ( $edit instanceof FineDiffReplaceOp ) */ {
+				$html .= FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n);
+				$html .= FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen());
+				}
+			$in_offset += $n;
+			}
+		return $html;
+		}
+
+	/**------------------------------------------------------------------------
+	* Return an opcodes string describing the diff between a "From" and a
+	* "To" string
+	*/
+	public static function getDiffOpcodes($from, $to, $granularities = null) {
+		$diff = new FineDiff($from, $to, $granularities);
+		return $diff->getOpcodes();
+		}
+
+	/**------------------------------------------------------------------------
+	* Return an iterable collection of diff ops from an opcodes string
+	*/
+	public static function getDiffOpsFromOpcodes($opcodes) {
+		$diffops = new FineDiffOps();
+		FineDiff::renderFromOpcodes(null, $opcodes, array($diffops,'appendOpcode'));
+		return $diffops->edits;
+		}
+
+	/**------------------------------------------------------------------------
+	* Re-create the "To" string from the "From" string and an "Opcodes" string
+	*/
+	public static function renderToTextFromOpcodes($from, $opcodes) {
+		return FineDiff::renderFromOpcodes($from, $opcodes, array('FineDiff','renderToTextFromOpcode'));
+		}
+
+	/**------------------------------------------------------------------------
+	* Render the diff to an HTML string
+	*/
+	public static function renderDiffToHTMLFromOpcodes($from, $opcodes) {
+		return FineDiff::renderFromOpcodes($from, $opcodes, array('FineDiff','renderDiffToHTMLFromOpcode'));
+		}
+
+	/**------------------------------------------------------------------------
+	* Generic opcodes parser, user must supply callback for handling
+	* single opcode
+	*/
+	public static function renderFromOpcodes($from, $opcodes, $callback) {
+		if ( !is_callable($callback) ) {
+			return '';
+			}
+		$out = '';
+		$opcodes_len = strlen($opcodes);
+		$from_offset = $opcodes_offset = 0;
+		while ( $opcodes_offset <  $opcodes_len ) {
+			$opcode = substr($opcodes, $opcodes_offset, 1);
+			$opcodes_offset++;
+			$n = intval(substr($opcodes, $opcodes_offset));
+			if ( $n ) {
+				$opcodes_offset += strlen(strval($n));
+				}
+			else {
+				$n = 1;
+				}
+			if ( $opcode === 'c' ) { // copy n characters from source
+				$out .= call_user_func($callback, 'c', $from, $from_offset, $n, '');
+				$from_offset += $n;
+				}
+			else if ( $opcode === 'd' ) { // delete n characters from source
+				$out .= call_user_func($callback, 'd', $from, $from_offset, $n, '');
+				$from_offset += $n;
+				}
+			else /* if ( $opcode === 'i' ) */ { // insert n characters from opcodes
+				$out .= call_user_func($callback, 'i', $opcodes, $opcodes_offset + 1, $n);
+				$opcodes_offset += 1 + $n;
+				}
+			}
+		return $out;
+		}
+
+	/**
+	* Stock granularity stacks and delimiters
+	*/
+
+	const paragraphDelimiters = "\n\r";
+	public static $paragraphGranularity = array(
+		FineDiff::paragraphDelimiters
+		);
+	const sentenceDelimiters = ".\n\r";
+	public static $sentenceGranularity = array(
+		FineDiff::paragraphDelimiters,
+		FineDiff::sentenceDelimiters
+		);
+	const wordDelimiters = " \t.\n\r";
+	public static $wordGranularity = array(
+		FineDiff::paragraphDelimiters,
+		FineDiff::sentenceDelimiters,
+		FineDiff::wordDelimiters
+		);
+	const characterDelimiters = "";
+	public static $characterGranularity = array(
+		FineDiff::paragraphDelimiters,
+		FineDiff::sentenceDelimiters,
+		FineDiff::wordDelimiters,
+		FineDiff::characterDelimiters
+		);
+
+	public static $textStack = array(
+		".",
+		" \t.\n\r",
+		""
+		);
+
+	/**------------------------------------------------------------------------
+	*
+	* Private section
+	*
+	*/
+
+	/**
+	* Entry point to compute the diff.
+	*/
+	private function doDiff($from_text, $to_text) {
+		$this->last_edit = false;
+		$this->stackpointer = 0;
+		$this->from_text = $from_text;
+		$this->from_offset = 0;
+		// can't diff without at least one granularity specifier
+		if ( empty($this->granularityStack) ) {
+			return;
+			}
+		$this->_processGranularity($from_text, $to_text);
+		}
+
+	/**
+	* This is the recursive function which is responsible for
+	* handling/increasing granularity.
+	*
+	* Incrementally increasing the granularity is key to compute the
+	* overall diff in a very efficient way.
+	*/
+	private function _processGranularity($from_segment, $to_segment) {
+		$delimiters = $this->granularityStack[$this->stackpointer++];
+		$has_next_stage = $this->stackpointer < count($this->granularityStack);
+		foreach ( FineDiff::doFragmentDiff($from_segment, $to_segment, $delimiters) as $fragment_edit ) {
+			// increase granularity
+			if ( $fragment_edit instanceof FineDiffReplaceOp && $has_next_stage ) {
+				$this->_processGranularity(
+					substr($this->from_text, $this->from_offset, $fragment_edit->getFromLen()),
+					$fragment_edit->getText()
+					);
+				}
+			// fuse copy ops whenever possible
+			else if ( $fragment_edit instanceof FineDiffCopyOp && $this->last_edit instanceof FineDiffCopyOp ) {
+				$this->edits[count($this->edits)-1]->increase($fragment_edit->getFromLen());
+				$this->from_offset += $fragment_edit->getFromLen();
+				}
+			else {
+				/* $fragment_edit instanceof FineDiffCopyOp */
+				/* $fragment_edit instanceof FineDiffDeleteOp */
+				/* $fragment_edit instanceof FineDiffInsertOp */
+				$this->edits[] = $this->last_edit = $fragment_edit;
+				$this->from_offset += $fragment_edit->getFromLen();
+				}
+			}
+		$this->stackpointer--;
+		}
+
+	/**
+	* This is the core algorithm which actually perform the diff itself,
+	* fragmenting the strings as per specified delimiters.
+	*
+	* This function is naturally recursive, however for performance purpose
+	* a local job queue is used instead of outright recursivity.
+	*/
+	private static function doFragmentDiff($from_text, $to_text, $delimiters) {
+		// Empty delimiter means character-level diffing.
+		// In such case, use code path optimized for character-level
+		// diffing.
+		if ( empty($delimiters) ) {
+			return FineDiff::doCharDiff($from_text, $to_text);
+			}
+
+		$result = array();
+
+		// fragment-level diffing
+		$from_text_len = strlen($from_text);
+		$to_text_len = strlen($to_text);
+		$from_fragments = FineDiff::extractFragments($from_text, $delimiters);
+		$to_fragments = FineDiff::extractFragments($to_text, $delimiters);
+
+		$jobs = array(array(0, $from_text_len, 0, $to_text_len));
+
+		$cached_array_keys = array();
+
+		while ( $job = array_pop($jobs) ) {
+
+			// get the segments which must be diff'ed
+			list($from_segment_start, $from_segment_end, $to_segment_start, $to_segment_end) = $job;
+
+			// catch easy cases first
+			$from_segment_length = $from_segment_end - $from_segment_start;
+			$to_segment_length = $to_segment_end - $to_segment_start;
+			if ( !$from_segment_length || !$to_segment_length ) {
+				if ( $from_segment_length ) {
+					$result[$from_segment_start * 4] = new FineDiffDeleteOp($from_segment_length);
+					}
+				else if ( $to_segment_length ) {
+					$result[$from_segment_start * 4 + 1] = new FineDiffInsertOp(substr($to_text, $to_segment_start, $to_segment_length));
+					}
+				continue;
+				}
+
+			// find longest copy operation for the current segments
+			$best_copy_length = 0;
+
+			$from_base_fragment_index = $from_segment_start;
+
+			$cached_array_keys_for_current_segment = array();
+
+			while ( $from_base_fragment_index < $from_segment_end ) {
+				$from_base_fragment = $from_fragments[$from_base_fragment_index];
+				$from_base_fragment_length = strlen($from_base_fragment);
+				// performance boost: cache array keys
+				if ( !isset($cached_array_keys_for_current_segment[$from_base_fragment]) ) {
+					if ( !isset($cached_array_keys[$from_base_fragment]) ) {
+						$to_all_fragment_indices = $cached_array_keys[$from_base_fragment] = array_keys($to_fragments, $from_base_fragment, true);
+						}
+					else {
+						$to_all_fragment_indices = $cached_array_keys[$from_base_fragment];
+						}
+					// get only indices which falls within current segment
+					if ( $to_segment_start > 0 || $to_segment_end < $to_text_len ) {
+						$to_fragment_indices = array();
+						foreach ( $to_all_fragment_indices as $to_fragment_index ) {
+							if ( $to_fragment_index < $to_segment_start ) { continue; }
+							if ( $to_fragment_index >= $to_segment_end ) { break; }
+							$to_fragment_indices[] = $to_fragment_index;
+							}
+						$cached_array_keys_for_current_segment[$from_base_fragment] = $to_fragment_indices;
+						}
+					else {
+						$to_fragment_indices = $to_all_fragment_indices;
+						}
+					}
+				else {
+					$to_fragment_indices = $cached_array_keys_for_current_segment[$from_base_fragment];
+					}
+				// iterate through collected indices
+				foreach ( $to_fragment_indices as $to_base_fragment_index ) {
+					$fragment_index_offset = $from_base_fragment_length;
+					// iterate until no more match
+					for (;;) {
+						$fragment_from_index = $from_base_fragment_index + $fragment_index_offset;
+						if ( $fragment_from_index >= $from_segment_end ) {
+							break;
+							}
+						$fragment_to_index = $to_base_fragment_index + $fragment_index_offset;
+						if ( $fragment_to_index >= $to_segment_end ) {
+							break;
+							}
+						if ( $from_fragments[$fragment_from_index] !== $to_fragments[$fragment_to_index] ) {
+							break;
+							}
+						$fragment_length = strlen($from_fragments[$fragment_from_index]);
+						$fragment_index_offset += $fragment_length;
+						}
+					if ( $fragment_index_offset > $best_copy_length ) {
+						$best_copy_length = $fragment_index_offset;
+						$best_from_start = $from_base_fragment_index;
+						$best_to_start = $to_base_fragment_index;
+						}
+					}
+				$from_base_fragment_index += strlen($from_base_fragment);
+				// If match is larger than half segment size, no point trying to find better
+				// TODO: Really?
+				if ( $best_copy_length >= $from_segment_length / 2) {
+					break;
+					}
+				// no point to keep looking if what is left is less than
+				// current best match
+				if ( $from_base_fragment_index + $best_copy_length >= $from_segment_end ) {
+					break;
+					}
+				}
+
+			if ( $best_copy_length ) {
+				$jobs[] = array($from_segment_start, $best_from_start, $to_segment_start, $best_to_start);
+				$result[$best_from_start * 4 + 2] = new FineDiffCopyOp($best_copy_length);
+				$jobs[] = array($best_from_start + $best_copy_length, $from_segment_end, $best_to_start + $best_copy_length, $to_segment_end);
+				}
+			else {
+				$result[$from_segment_start * 4 ] = new FineDiffReplaceOp($from_segment_length, substr($to_text, $to_segment_start, $to_segment_length));
+				}
+			}
+
+		ksort($result, SORT_NUMERIC);
+		return array_values($result);
+		}
+
+	/**
+	* Perform a character-level diff.
+	*
+	* The algorithm is quite similar to doFragmentDiff(), except that
+	* the code path is optimized for character-level diff -- strpos() is
+	* used to find out the longest common subequence of characters.
+	*
+	* We try to find a match using the longest possible subsequence, which
+	* is at most the length of the shortest of the two strings, then incrementally
+	* reduce the size until a match is found.
+	*
+	* I still need to study more the performance of this function. It
+	* appears that for long strings, the generic doFragmentDiff() is more
+	* performant. For word-sized strings, doCharDiff() is somewhat more
+	* performant.
+	*/
+	private static function doCharDiff($from_text, $to_text) {
+		$result = array();
+		$jobs = array(array(0, strlen($from_text), 0, strlen($to_text)));
+		while ( $job = array_pop($jobs) ) {
+			// get the segments which must be diff'ed
+			list($from_segment_start, $from_segment_end, $to_segment_start, $to_segment_end) = $job;
+			$from_segment_len = $from_segment_end - $from_segment_start;
+			$to_segment_len = $to_segment_end - $to_segment_start;
+
+			// catch easy cases first
+			if ( !$from_segment_len || !$to_segment_len ) {
+				if ( $from_segment_len ) {
+					$result[$from_segment_start * 4 + 0] = new FineDiffDeleteOp($from_segment_len);
+					}
+				else if ( $to_segment_len ) {
+					$result[$from_segment_start * 4 + 1] = new FineDiffInsertOp(substr($to_text, $to_segment_start, $to_segment_len));
+					}
+				continue;
+				}
+			if ( $from_segment_len >= $to_segment_len ) {
+				$copy_len = $to_segment_len;
+				while ( $copy_len ) {
+					$to_copy_start = $to_segment_start;
+					$to_copy_start_max = $to_segment_end - $copy_len;
+					while ( $to_copy_start <= $to_copy_start_max ) {
+						$from_copy_start = strpos(substr($from_text, $from_segment_start, $from_segment_len), substr($to_text, $to_copy_start, $copy_len));
+						if ( $from_copy_start !== false ) {
+							$from_copy_start += $from_segment_start;
+							break 2;
+							}
+						$to_copy_start++;
+						}
+					$copy_len--;
+					}
+				}
+			else {
+				$copy_len = $from_segment_len;
+				while ( $copy_len ) {
+					$from_copy_start = $from_segment_start;
+					$from_copy_start_max = $from_segment_end - $copy_len;
+					while ( $from_copy_start <= $from_copy_start_max ) {
+						$to_copy_start = strpos(substr($to_text, $to_segment_start, $to_segment_len), substr($from_text, $from_copy_start, $copy_len));
+						if ( $to_copy_start !== false ) {
+							$to_copy_start += $to_segment_start;
+							break 2;
+							}
+						$from_copy_start++;
+						}
+					$copy_len--;
+					}
+				}
+			// match found
+			if ( $copy_len ) {
+				$jobs[] = array($from_segment_start, $from_copy_start, $to_segment_start, $to_copy_start);
+				$result[$from_copy_start * 4 + 2] = new FineDiffCopyOp($copy_len);
+				$jobs[] = array($from_copy_start + $copy_len, $from_segment_end, $to_copy_start + $copy_len, $to_segment_end);
+				}
+			// no match,  so delete all, insert all
+			else {
+				$result[$from_segment_start * 4] = new FineDiffReplaceOp($from_segment_len, substr($to_text, $to_segment_start, $to_segment_len));
+				}
+			}
+		ksort($result, SORT_NUMERIC);
+		return array_values($result);
+		}
+
+	/**
+	* Efficiently fragment the text into an array according to
+	* specified delimiters.
+	* No delimiters means fragment into single character.
+	* The array indices are the offset of the fragments into
+	* the input string.
+	* A sentinel empty fragment is always added at the end.
+	* Careful: No check is performed as to the validity of the
+	* delimiters.
+	*/
+	private static function extractFragments($text, $delimiters) {
+		// special case: split into characters
+		if ( empty($delimiters) ) {
+			$chars = str_split($text, 1);
+			$chars[strlen($text)] = '';
+			return $chars;
+			}
+		$fragments = array();
+		$start = $end = 0;
+		for (;;) {
+			$end += strcspn($text, $delimiters, $end);
+			$end += strspn($text, $delimiters, $end);
+			if ( $end === $start ) {
+				break;
+				}
+			$fragments[$start] = substr($text, $start, $end - $start);
+			$start = $end;
+			}
+		$fragments[$start] = '';
+		return $fragments;
+		}
+
+	/**
+	* Stock opcode renderers
+	*/
+	private static function renderToTextFromOpcode($opcode, $from, $from_offset, $from_len) {
+		if ( $opcode === 'c' || $opcode === 'i' ) {
+			return substr($from, $from_offset, $from_len);
+			}
+		return '';
+		}
+
+	private static function renderDiffToHTMLFromOpcode($opcode, $from, $from_offset, $from_len) {
+		if ( $opcode === 'c' ) {
+			return htmlentities(substr($from, $from_offset, $from_len));
+			}
+		else if ( $opcode === 'd' ) {
+			$deletion = substr($from, $from_offset, $from_len);
+			if ( strcspn($deletion, " \n\r") === 0 ) {
+				$deletion = str_replace(array("\n","\r"), array('\n','\r'), $deletion);
+				}
+			return '<del>' . htmlentities($deletion) . '</del>';
+			}
+		else /* if ( $opcode === 'i' ) */ {
+ 			return '<ins>' . htmlentities(substr($from, $from_offset, $from_len)) . '</ins>';
+			}
+		return '';
+		}
+	}
+
diff --git a/lib/plugins/libkolab/vendor/finediff_modifications.diff b/lib/plugins/libkolab/vendor/finediff_modifications.diff
new file mode 100644
index 0000000..3a9ad5c
--- /dev/null
+++ b/lib/plugins/libkolab/vendor/finediff_modifications.diff
@@ -0,0 +1,121 @@
+--- finediff.php.orig	2014-07-29 14:24:10.000000000 +0200
++++ finediff.php	2014-07-29 14:30:38.000000000 +0200
+@@ -234,25 +234,25 @@
+ 
+ 	public function renderDiffToHTML() {
+ 		$in_offset = 0;
+-		ob_start();
++		$html = '';
+ 		foreach ( $this->edits as $edit ) {
+ 			$n = $edit->getFromLen();
+ 			if ( $edit instanceof FineDiffCopyOp ) {
+-				FineDiff::renderDiffToHTMLFromOpcode('c', $this->from_text, $in_offset, $n);
++				$html .= FineDiff::renderDiffToHTMLFromOpcode('c', $this->from_text, $in_offset, $n);
+ 				}
+ 			else if ( $edit instanceof FineDiffDeleteOp ) {
+-				FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n);
++				$html .= FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n);
+ 				}
+ 			else if ( $edit instanceof FineDiffInsertOp ) {
+-				FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen());
++				$html .= FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen());
+ 				}
+ 			else /* if ( $edit instanceof FineDiffReplaceOp ) */ {
+-				FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n);
+-				FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen());
++				$html .= FineDiff::renderDiffToHTMLFromOpcode('d', $this->from_text, $in_offset, $n);
++				$html .= FineDiff::renderDiffToHTMLFromOpcode('i', $edit->getText(), 0, $edit->getToLen());
+ 				}
+ 			$in_offset += $n;
+ 			}
+-		return ob_get_clean();
++		return $html;
+ 		}
+ 
+ 	/**------------------------------------------------------------------------
+@@ -277,18 +277,14 @@
+ 	* Re-create the "To" string from the "From" string and an "Opcodes" string
+ 	*/
+ 	public static function renderToTextFromOpcodes($from, $opcodes) {
+-		ob_start();
+-		FineDiff::renderFromOpcodes($from, $opcodes, array('FineDiff','renderToTextFromOpcode'));
+-		return ob_get_clean();
++		return FineDiff::renderFromOpcodes($from, $opcodes, array('FineDiff','renderToTextFromOpcode'));
+ 		}
+ 
+ 	/**------------------------------------------------------------------------
+ 	* Render the diff to an HTML string
+ 	*/
+ 	public static function renderDiffToHTMLFromOpcodes($from, $opcodes) {
+-		ob_start();
+-		FineDiff::renderFromOpcodes($from, $opcodes, array('FineDiff','renderDiffToHTMLFromOpcode'));
+-		return ob_get_clean();
++		return FineDiff::renderFromOpcodes($from, $opcodes, array('FineDiff','renderDiffToHTMLFromOpcode'));
+ 		}
+ 
+ 	/**------------------------------------------------------------------------
+@@ -297,8 +293,9 @@
+ 	*/
+ 	public static function renderFromOpcodes($from, $opcodes, $callback) {
+ 		if ( !is_callable($callback) ) {
+-			return;
++			return '';
+ 			}
++		$out = '';
+ 		$opcodes_len = strlen($opcodes);
+ 		$from_offset = $opcodes_offset = 0;
+ 		while ( $opcodes_offset <  $opcodes_len ) {
+@@ -312,18 +309,19 @@
+ 				$n = 1;
+ 				}
+ 			if ( $opcode === 'c' ) { // copy n characters from source
+-				call_user_func($callback, 'c', $from, $from_offset, $n, '');
++				$out .= call_user_func($callback, 'c', $from, $from_offset, $n, '');
+ 				$from_offset += $n;
+ 				}
+ 			else if ( $opcode === 'd' ) { // delete n characters from source
+-				call_user_func($callback, 'd', $from, $from_offset, $n, '');
++				$out .= call_user_func($callback, 'd', $from, $from_offset, $n, '');
+ 				$from_offset += $n;
+ 				}
+ 			else /* if ( $opcode === 'i' ) */ { // insert n characters from opcodes
+-				call_user_func($callback, 'i', $opcodes, $opcodes_offset + 1, $n);
++				$out .= call_user_func($callback, 'i', $opcodes, $opcodes_offset + 1, $n);
+ 				$opcodes_offset += 1 + $n;
+ 				}
+ 			}
++		return $out;
+ 		}
+ 
+ 	/**
+@@ -665,24 +663,26 @@
+ 	*/
+ 	private static function renderToTextFromOpcode($opcode, $from, $from_offset, $from_len) {
+ 		if ( $opcode === 'c' || $opcode === 'i' ) {
+-			echo substr($from, $from_offset, $from_len);
++			return substr($from, $from_offset, $from_len);
+ 			}
++		return '';
+ 		}
+ 
+ 	private static function renderDiffToHTMLFromOpcode($opcode, $from, $from_offset, $from_len) {
+ 		if ( $opcode === 'c' ) {
+-			echo htmlentities(substr($from, $from_offset, $from_len));
++			return htmlentities(substr($from, $from_offset, $from_len));
+ 			}
+ 		else if ( $opcode === 'd' ) {
+ 			$deletion = substr($from, $from_offset, $from_len);
+ 			if ( strcspn($deletion, " \n\r") === 0 ) {
+ 				$deletion = str_replace(array("\n","\r"), array('\n','\r'), $deletion);
+ 				}
+-			echo '<del>', htmlentities($deletion), '</del>';
++			return '<del>' . htmlentities($deletion) . '</del>';
+ 			}
+ 		else /* if ( $opcode === 'i' ) */ {
+- 			echo '<ins>', htmlentities(substr($from, $from_offset, $from_len)), '</ins>';
++ 			return '<ins>' . htmlentities(substr($from, $from_offset, $from_len)) . '</ins>';
+ 			}
++		return '';
+ 		}
+ 	}
+ 




More information about the commits mailing list