Branch 'kolab-syncroton-2.2' - 5 commits - lib/ext lib/kolab_sync.php

Jeroen van Meeuwen vanmeeuwen at kolabsys.com
Tue Nov 12 14:06:17 CET 2013


 lib/ext/Roundcube/html.php |    9 +++++----
 lib/kolab_sync.php         |    2 +-
 2 files changed, 6 insertions(+), 5 deletions(-)

New commits:
commit b2bf9c32dc68ad427f083fd592011df86d21d517
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Tue Nov 12 14:05:45 2013 +0100

    Bump version to 2.2.3

diff --git a/lib/kolab_sync.php b/lib/kolab_sync.php
index d768ed0..4f19cb0 100644
--- a/lib/kolab_sync.php
+++ b/lib/kolab_sync.php
@@ -43,7 +43,7 @@ class kolab_sync extends rcube
     public $user;
 
     const CHARSET = 'UTF-8';
-    const VERSION = "2.2.2";
+    const VERSION = "2.2.3";
 
 
     /**


commit 4a58eb4c5d8041fea1bd6b5c1ed8c584640fd6a3
Merge: 7601b25 24f6cf4
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Tue Nov 12 14:05:23 2013 +0100

    Merge branch 'kolab-syncroton-2.2' of ssh://git.kolab.org/git/kolab-syncroton into kolab-syncroton-2.2
    
    Conflicts:
    	lib/plugins/libkolab/SQL/mysql.initial.sql
    	lib/plugins/libkolab/lib/kolab_format.php
    	lib/plugins/libkolab/lib/kolab_storage_cache.php



commit 7601b25834eab797d2b29a32f5b4b03f103ddd7b
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Thu Oct 17 18:20:27 2013 +0200

    Set version to 2.2.2

diff --git a/lib/kolab_sync.php b/lib/kolab_sync.php
index 52c8b8a..d768ed0 100644
--- a/lib/kolab_sync.php
+++ b/lib/kolab_sync.php
@@ -43,7 +43,7 @@ class kolab_sync extends rcube
     public $user;
 
     const CHARSET = 'UTF-8';
-    const VERSION = "2.2.1";
+    const VERSION = "2.2.2";
 
 
     /**


commit ba11518280a7d6c667f72bd5a28fcde767e5b143
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Thu Oct 17 18:19:46 2013 +0200

    Rebase lib/ext/Roundcube with cache refactoring

diff --git a/lib/ext/Roundcube/bootstrap.php b/lib/ext/Roundcube/bootstrap.php
index 182ea12..6e51433 100644
--- a/lib/ext/Roundcube/bootstrap.php
+++ b/lib/ext/Roundcube/bootstrap.php
@@ -26,25 +26,25 @@
  */
 
 $config = array(
-    'error_reporting'         => E_ALL &~ (E_NOTICE | E_STRICT),
+    'error_reporting'         => E_ALL & ~E_NOTICE & ~E_STRICT,
     // Some users are not using Installer, so we'll check some
     // critical PHP settings here. Only these, which doesn't provide
     // an error/warning in the logs later. See (#1486307).
     'mbstring.func_overload'  => 0,
-    'magic_quotes_runtime'    => 0,
-    'magic_quotes_sybase'     => 0, // #1488506
+    'magic_quotes_runtime'    => false,
+    'magic_quotes_sybase'     => false, // #1488506
 );
 
 // check these additional ini settings if not called via CLI
 if (php_sapi_name() != 'cli') {
     $config += array(
-        'suhosin.session.encrypt' => 0,
-        'file_uploads'            => 1,
+        'suhosin.session.encrypt' => false,
+        'file_uploads'            => true,
     );
 }
 
 foreach ($config as $optname => $optval) {
-    $ini_optval = filter_var(ini_get($optname), FILTER_VALIDATE_BOOLEAN);
+    $ini_optval = filter_var(ini_get($optname), is_bool($optval) ? FILTER_VALIDATE_BOOLEAN : FILTER_VALIDATE_INT);
     if ($optval != $ini_optval && @ini_set($optname, $optval) === false) {
         $error = "ERROR: Wrong '$optname' option value and it wasn't possible to set it to required value ($optval).\n"
             . "Check your PHP configuration (including php_admin_flag).";
@@ -58,7 +58,7 @@ define('RCUBE_VERSION', '1.0-git');
 define('RCUBE_CHARSET', 'UTF-8');
 
 if (!defined('RCUBE_LIB_DIR')) {
-    define('RCUBE_LIB_DIR', dirname(__FILE__).'/');
+    define('RCUBE_LIB_DIR', dirname(__FILE__).DIRECTORY_SEPARATOR);
 }
 
 if (!defined('RCUBE_INSTALL_PATH')) {
@@ -83,6 +83,16 @@ if (extension_loaded('mbstring')) {
     @mb_regex_encoding(RCUBE_CHARSET);
 }
 
+// make sure the Roundcube lib directory is in the include_path
+$rcube_path = realpath(RCUBE_LIB_DIR . '..');
+$sep        = PATH_SEPARATOR;
+$regexp     = "!(^|$sep)" . preg_quote($rcube_path, '!') . "($sep|\$)!";
+$path       = ini_get('include_path');
+
+if (!preg_match($regexp, $path)) {
+    set_include_path($path . PATH_SEPARATOR . $rcube_path);
+}
+
 // Register autoloader
 spl_autoload_register('rcube_autoload');
 
diff --git a/lib/ext/Roundcube/html.php b/lib/ext/Roundcube/html.php
index a367112..4f87d25 100644
--- a/lib/ext/Roundcube/html.php
+++ b/lib/ext/Roundcube/html.php
@@ -604,16 +604,17 @@ class html_select extends html
      *
      * @param mixed $names  Option name or array with option names
      * @param mixed $values Option value or array with option values
+     * @param array $attrib Additional attributes for the option entry
      */
-    public function add($names, $values = null)
+    public function add($names, $values = null, $attrib = array())
     {
         if (is_array($names)) {
             foreach ($names as $i => $text) {
-                $this->options[] = array('text' => $text, 'value' => $values[$i]);
+                $this->options[] = array('text' => $text, 'value' => $values[$i]) + $attrib;
             }
         }
         else {
-            $this->options[] = array('text' => $names, 'value' => $values);
+            $this->options[] = array('text' => $names, 'value' => $values) + $attrib;
         }
     }
 
@@ -644,7 +645,7 @@ class html_select extends html
                 $option_content = self::quote($option_content);
             }
 
-            $this->content .= self::tag('option', $attr, $option_content);
+            $this->content .= self::tag('option', $attr + $option, $option_content, array('value','label','class','style','title','disabled','selected'));
         }
 
         return parent::show();
diff --git a/lib/ext/Roundcube/rcube.php b/lib/ext/Roundcube/rcube.php
index e0f889a..399f84f 100644
--- a/lib/ext/Roundcube/rcube.php
+++ b/lib/ext/Roundcube/rcube.php
@@ -467,6 +467,10 @@ class rcube
         $this->session->set_secret($this->config->get('des_key') . dirname($_SERVER['SCRIPT_NAME']));
         $this->session->set_ip_check($this->config->get('ip_check'));
 
+        if ($this->config->get('session_auth_name')) {
+            $this->session->set_cookiename($this->config->get('session_auth_name'));
+        }
+
         // start PHP session (if not in CLI mode)
         if ($_SERVER['REMOTE_ADDR']) {
             $this->session->start();
@@ -494,7 +498,14 @@ class rcube
     public function gc_temp()
     {
         $tmp = unslashify($this->config->get('temp_dir'));
-        $expire = time() - 172800;  // expire in 48 hours
+
+        // expire in 48 hours by default
+        $temp_dir_ttl = $this->config->get('temp_dir_ttl', '48h');
+        $temp_dir_ttl = get_offset_sec($temp_dir_ttl);
+        if ($temp_dir_ttl < 6*3600)
+            $temp_dir_ttl = 6*3600;   // 6 hours sensible lower bound.
+
+        $expire = time() - $temp_dir_ttl;
 
         if ($tmp && ($dir = opendir($tmp))) {
             while (($fname = readdir($dir)) !== false) {
@@ -691,7 +702,11 @@ class rcube
         // user HTTP_ACCEPT_LANGUAGE if no language is specified
         if (empty($lang) || $lang == 'auto') {
             $accept_langs = explode(',', $_SERVER['HTTP_ACCEPT_LANGUAGE']);
-            $lang         = str_replace('-', '_', $accept_langs[0]);
+            $lang         = $accept_langs[0];
+
+            if (preg_match('/^([a-z]+)[_-]([a-z]+)$/i', $lang, $m)) {
+                $lang = $m[1] . '_' . strtoupper($m[2]);
+            }
         }
 
         if (empty($rcube_languages)) {
@@ -1121,8 +1136,8 @@ class rcube
      *      - code:    Error code (required)
      *      - type:    Error type [php|db|imap|javascript] (required)
      *      - message: Error message
-     *      - file:    File where error occured
-     *      - line:    Line where error occured
+     *      - file:    File where error occurred
+     *      - line:    Line where error occurred
      * @param boolean True to log the error
      * @param boolean Terminate script execution
      */
@@ -1393,6 +1408,10 @@ class rcube
             'options' => $options,
         ));
 
+        if ($plugin['abort']) {
+            return isset($plugin['result']) ? $plugin['result'] : false;
+        }
+
         $from    = $plugin['from'];
         $mailto  = $plugin['mailto'];
         $options = $plugin['options'];
diff --git a/lib/ext/Roundcube/rcube_addressbook.php b/lib/ext/Roundcube/rcube_addressbook.php
index 4ed139c..6e2b439 100644
--- a/lib/ext/Roundcube/rcube_addressbook.php
+++ b/lib/ext/Roundcube/rcube_addressbook.php
@@ -3,7 +3,7 @@
 /*
  +-----------------------------------------------------------------------+
  | This file is part of the Roundcube Webmail client                     |
- | Copyright (C) 2006-2012, The Roundcube Dev Team                       |
+ | Copyright (C) 2006-2013, The Roundcube Dev Team                       |
  |                                                                       |
  | Licensed under the GNU General Public License version 3 or            |
  | any later version with exceptions for skins & plugins.                |
@@ -35,6 +35,7 @@ abstract class rcube_addressbook
     /** public properties (mandatory) */
     public $primary_key;
     public $groups = false;
+    public $export_groups = true;
     public $readonly = true;
     public $searchonly = false;
     public $undelete = false;
@@ -133,7 +134,7 @@ abstract class rcube_addressbook
     abstract function get_record($id, $assoc=false);
 
     /**
-     * Returns the last error occured (e.g. when updating/inserting failed)
+     * Returns the last error occurred (e.g. when updating/inserting failed)
      *
      * @return array Hash array with the following fields: type, message
      */
@@ -423,7 +424,7 @@ abstract class rcube_addressbook
      * @param boolean True to return one array with all values, False for hash array with values grouped by type
      * @return array List of column values
      */
-    function get_col_values($col, $data, $flat = false)
+    public static function get_col_values($col, $data, $flat = false)
     {
         $out = array();
         foreach ((array)$data as $c => $values) {
@@ -476,7 +477,8 @@ abstract class rcube_addressbook
             $fn = trim(join(' ', array_filter(array($contact['prefix'], $contact['firstname'], $contact['middlename'], $contact['surname'], $contact['suffix']))));
 
         // use email address part for name
-        $email = is_array($contact['email']) ? $contact['email'][0] : $contact['email'];
+        $email = self::get_col_values('email', $contact, true);
+        $email = $email[0];
 
         if ($email && (empty($fn) || $fn == $email)) {
             // return full email
@@ -523,9 +525,9 @@ abstract class rcube_addressbook
             $fn = $contact['name'];
 
         // fallback to email address
-        $email = is_array($contact['email']) ? $contact['email'][0] : $contact['email'];
-        if (empty($fn) && $email)
-            return $email;
+        if (empty($fn) && ($email = self::get_col_values('email', $contact, true)) && !empty($email)) {
+            return $email[0];
+        }
 
         return $fn;
     }
@@ -538,8 +540,8 @@ abstract class rcube_addressbook
         $key = $contact[$sort_col] . ':' . $contact['sourceid'];
 
         // add email to a key to not skip contacts with the same name (#1488375)
-        if (!empty($contact['email'])) {
-            $key .= ':' . implode(':', (array)$contact['email']);
+        if (($email = self::get_col_values('email', $contact, true)) && !empty($email)) {
+            $key .= ':' . implode(':', (array)$email);
         }
 
         return $key;
@@ -561,9 +563,9 @@ abstract class rcube_addressbook
         // use only strict comparison (mode = 1)
         // @TODO: partial search, e.g. match only day and month
         if (in_array($colname, $this->date_cols)) {
-            return (($value = rcube_utils::strtotime($value))
-                && ($search = rcube_utils::strtotime($search))
-                && date('Ymd', $value) == date('Ymd', $search));
+            return (($value = rcube_utils::anytodatetime($value))
+                && ($search = rcube_utils::anytodatetime($search))
+                && $value->format('Ymd') == $search->format('Ymd'));
         }
 
         // composite field, e.g. address
diff --git a/lib/ext/Roundcube/rcube_base_replacer.php b/lib/ext/Roundcube/rcube_base_replacer.php
index a59bba9..fa67647 100644
--- a/lib/ext/Roundcube/rcube_base_replacer.php
+++ b/lib/ext/Roundcube/rcube_base_replacer.php
@@ -90,8 +90,8 @@ class rcube_base_replacer
 
             if (preg_match_all('/\.\.\//', $path, $matches, PREG_SET_ORDER)) {
                 foreach ($matches as $a_match) {
-                    if (strrpos($base_url, '/')) {
-                        $base_url = substr($base_url, 0, strrpos($base_url, '/'));
+                    if ($pos = strrpos($base_url, '/')) {
+                        $base_url = substr($base_url, 0, $pos);
                     }
                     $path = substr($path, 3);
                 }
diff --git a/lib/ext/Roundcube/rcube_config.php b/lib/ext/Roundcube/rcube_config.php
index 3cf4da8..04b914c 100644
--- a/lib/ext/Roundcube/rcube_config.php
+++ b/lib/ext/Roundcube/rcube_config.php
@@ -3,7 +3,7 @@
 /*
  +-----------------------------------------------------------------------+
  | This file is part of the Roundcube Webmail client                     |
- | Copyright (C) 2008-2012, The Roundcube Dev Team                       |
+ | Copyright (C) 2008-2013, The Roundcube Dev Team                       |
  |                                                                       |
  | Licensed under the GNU General Public License version 3 or            |
  | any later version with exceptions for skins & plugins.                |
@@ -27,7 +27,7 @@ class rcube_config
     const DEFAULT_SKIN = 'larry';
 
     private $env = '';
-    private $basedir = 'config/';
+    private $paths = array();
     private $prop = array();
     private $errors = array();
     private $userprefs = array();
@@ -58,7 +58,32 @@ class rcube_config
     public function __construct($env = '')
     {
         $this->env = $env;
-        $this->basedir = RCUBE_CONFIG_DIR;
+
+        if ($paths = getenv('RCUBE_CONFIG_PATH')) {
+            $this->paths = explode(PATH_SEPARATOR, $paths);
+            // make all paths absolute
+            foreach ($this->paths as $i => $path) {
+                if (!$this->_is_absolute($path)) {
+                    if ($realpath = realpath(RCUBE_INSTALL_PATH . $path)) {
+                        $this->paths[$i] = unslashify($realpath) . '/';
+                    }
+                    else {
+                        unset($this->paths[$i]);
+                    }
+                }
+                else {
+                    $this->paths[$i] = unslashify($path) . '/';
+                }
+            }
+        }
+
+        if (defined('RCUBE_CONFIG_DIR') && !in_array(RCUBE_CONFIG_DIR, $this->paths)) {
+            $this->paths[] = RCUBE_CONFIG_DIR;
+        }
+
+        if (empty($this->paths)) {
+            $this->paths[] = RCUBE_INSTALL_PATH . 'config/';
+        }
 
         $this->load();
 
@@ -94,8 +119,7 @@ class rcube_config
         }
 
         // load host-specific configuration
-        if (!empty($_SERVER['HTTP_HOST']))
-            $this->load_host_config();
+        $this->load_host_config();
 
         // set skin (with fallback to old 'skin_path' property)
         if (empty($this->prop['skin'])) {
@@ -138,17 +162,6 @@ class rcube_config
         // enable display_errors in 'show' level, but not for ajax requests
         ini_set('display_errors', intval(empty($_REQUEST['_remote']) && ($this->prop['debug_level'] & 4)));
 
-        // set timezone auto settings values
-        if ($this->prop['timezone'] == 'auto') {
-          $this->prop['_timezone_value'] = $this->client_timezone();
-        }
-        else if (is_numeric($this->prop['timezone']) && ($tz = timezone_name_from_abbr("", $this->prop['timezone'] * 3600, 0))) {
-          $this->prop['timezone'] = $tz;
-        }
-        else if (empty($this->prop['timezone'])) {
-          $this->prop['timezone'] = 'UTC';
-        }
-
         // remove deprecated properties
         unset($this->prop['dst_active']);
 
@@ -162,21 +175,31 @@ class rcube_config
      */
     private function load_host_config()
     {
-        $fname = null;
-
-        if (is_array($this->prop['include_host_config'])) {
-            $fname = $this->prop['include_host_config'][$_SERVER['HTTP_HOST']];
-        }
-        else if (!empty($this->prop['include_host_config'])) {
-            $fname = preg_replace('/[^a-z0-9\.\-_]/i', '', $_SERVER['HTTP_HOST']) . '.inc.php';
+        if (empty($this->prop['include_host_config'])) {
+            return;
         }
 
-        if ($fname) {
-            $this->load_from_file($fname);
+        foreach (array('HTTP_HOST', 'SERVER_NAME', 'SERVER_ADDR') as $key) {
+            $fname = null;
+            $name  = $_SERVER[$key];
+
+            if (!$name) {
+                continue;
+            }
+
+            if (is_array($this->prop['include_host_config'])) {
+                $fname = $this->prop['include_host_config'][$name];
+            }
+            else {
+                $fname = preg_replace('/[^a-z0-9\.\-_]/i', '', $name) . '.inc.php';
+            }
+
+            if ($fname && $this->load_from_file($fname)) {
+                return;
+            }
         }
     }
 
-
     /**
      * Read configuration from a file
      * and merge with the already stored config values
@@ -186,51 +209,73 @@ class rcube_config
      */
     public function load_from_file($file)
     {
-        $fpath = $this->resolve_path($file);
-        if ($fpath && (is_file($fpath) || file_exists($fpath)) && is_readable($fpath)) {
-            // use output buffering, we don't need any output here 
-            ob_start();
-            include($fpath);
-            ob_end_clean();
-
-            if (is_array($config)) {
-                $this->merge($config);
-                return true;
-            }
-            // deprecated name of config variable
-            else if (is_array($rcmail_config)) {
-                $this->merge($rcmail_config);
-                return true;
+        $success = false;
+
+        foreach ($this->resolve_paths($file) as $fpath) {
+            if ($fpath && is_file($fpath) && is_readable($fpath)) {
+                // use output buffering, we don't need any output here 
+                ob_start();
+                include($fpath);
+                ob_end_clean();
+
+                if (is_array($config)) {
+                    $this->merge($config);
+                    $success = true;
+                }
+                // deprecated name of config variable
+                if (is_array($rcmail_config)) {
+                    $this->merge($rcmail_config);
+                    $success = true;
+                }
             }
         }
 
-        return false;
+        return $success;
     }
 
     /**
-     * Helper method to resolve the absolute path to the given config file.
+     * Helper method to resolve absolute paths to the given config file.
      * This also takes the 'env' property into account.
+     *
+     * @param string  Filename or absolute file path
+     * @param boolean Return -$env file path if exists
+     * @return array  List of candidates in config dir path(s)
      */
-    public function resolve_path($file, $use_env = true)
+    public function resolve_paths($file, $use_env = true)
     {
-        if (strpos($file, '/') === false) {
-            $file = rtrim($this->basedir, '/') . '/' . $file;
+        $files = array();
+        $abs_path = $this->_is_absolute($file);
 
-            if (!realpath($file) === false) {
-                $file = realpath($file);
+        foreach ($this->paths as $basepath) {
+            $realpath = $abs_path ? $file : realpath($basepath . '/' . $file);
+
+            // check if <file>-env.ini exists
+            if ($realpath && $use_env && !empty($this->env)) {
+                $envfile = preg_replace('/\.(inc.php)$/', '-' . $this->env . '.\\1', $realpath);
+                if (is_file($envfile))
+                    $realpath = $envfile;
             }
-        }
 
-        // check if <file>-env.ini exists
-        if ($file && $use_env && !empty($this->env)) {
-            $envfile = preg_replace('/\.(inc.php)$/', '-' . $this->env . '.\\1', $file);
-            if (is_file($envfile))
-                return $envfile;
+            if ($realpath) {
+                $files[] = $realpath;
+
+                // no need to continue the loop if an absolute file path is given
+                if ($abs_path) {
+                    break;
+                }
+            }
         }
 
-        return $file;
+        return $files;
     }
 
+    /**
+     * 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
@@ -250,8 +295,10 @@ class rcube_config
 
         $rcube = rcube::get_instance();
 
-        if ($name == 'timezone' && isset($this->prop['_timezone_value'])) {
-            $result = $this->prop['_timezone_value'];
+        if ($name == 'timezone') {
+            if (empty($result) || $result == 'auto') {
+                $result = $this->client_timezone();
+            }
         }
         else if ($name == 'client_mimetypes') {
             if ($result == null && $def == null)
@@ -286,8 +333,8 @@ class rcube_config
      */
     public function merge($prefs)
     {
+        $prefs = $this->fix_legacy_props($prefs);
         $this->prop = array_merge($this->prop, $prefs, $this->userprefs);
-        $this->fix_legacy_props();
     }
 
 
@@ -299,6 +346,8 @@ class rcube_config
      */
     public function set_user_prefs($prefs)
     {
+        $prefs = $this->fix_legacy_props($prefs);
+
         // Honor the dont_override setting for any existing user preferences
         $dont_override = $this->get('dont_override');
         if (is_array($dont_override) && !empty($dont_override)) {
@@ -307,11 +356,6 @@ class rcube_config
             }
         }
 
-        // convert user's timezone into the new format
-        if (is_numeric($prefs['timezone']) && ($tz = timezone_name_from_abbr('', $prefs['timezone'] * 3600, 0))) {
-            $prefs['timezone'] = $tz;
-        }
-
         // larry is the new default skin :-)
         if ($prefs['skin'] == 'default') {
             $prefs['skin'] = self::DEFAULT_SKIN;
@@ -319,22 +363,13 @@ class rcube_config
 
         $this->userprefs = $prefs;
         $this->prop      = array_merge($this->prop, $prefs);
-
-        $this->fix_legacy_props();
-
-        // override timezone settings with client values
-        if ($this->prop['timezone'] == 'auto') {
-            $this->prop['_timezone_value'] = isset($_SESSION['timezone']) ? $this->client_timezone() : $this->prop['_timezone_value'];
-        }
-        else if (isset($this->prop['_timezone_value']))
-           unset($this->prop['_timezone_value']);
     }
 
 
     /**
      * Getter for all config options
      *
-     * @return array  Hash array containg all config properties
+     * @return array  Hash array containing all config properties
      */
     public function all()
     {
@@ -468,13 +503,12 @@ class rcube_config
      */
     private function client_timezone()
     {
-        if (isset($_SESSION['timezone']) && is_numeric($_SESSION['timezone'])
-              && ($ctz = timezone_name_from_abbr("", $_SESSION['timezone'] * 3600, 0))) {
-            return $ctz;
-        }
-        else if (!empty($_SESSION['timezone'])) {
+        // @TODO: remove this legacy timezone handling in the future
+        $props = $this->fix_legacy_props(array('timezone' => $_SESSION['timezone']));
+
+        if (!empty($props['timezone'])) {
             try {
-                $tz = timezone_open($_SESSION['timezone']);
+                $tz = new DateTimeZone($props['timezone']);
                 return $tz->getName();
             }
             catch (Exception $e) { /* gracefully ignore */ }
@@ -486,16 +520,93 @@ class rcube_config
 
     /**
      * Convert legacy options into new ones
+     *
+     * @param array $props Hash array with config props
+     *
+     * @return array Converted config props
      */
-    private function fix_legacy_props()
+    private function fix_legacy_props($props)
     {
         foreach ($this->legacy_props as $new => $old) {
-            if (isset($this->prop[$old])) {
-                if (!isset($this->prop[$new])) {
-                    $this->prop[$new] = $this->prop[$old];
+            if (isset($props[$old])) {
+                if (!isset($props[$new])) {
+                    $props[$new] = $props[$old];
                 }
-                unset($this->prop[$old]);
+                unset($props[$old]);
+            }
+        }
+
+        // convert deprecated numeric timezone value
+        if (isset($props['timezone']) && is_numeric($props['timezone'])) {
+            if ($tz = self::timezone_name_from_abbr($props['timezone'])) {
+                $props['timezone'] = $tz;
+            }
+            else {
+                unset($props['timezone']);
             }
         }
+
+        return $props;
+    }
+
+    /**
+     * timezone_name_from_abbr() replacement. Converts timezone offset
+     * into timezone name abbreviation.
+     *
+     * @param float $offset Timezone offset (in hours)
+     *
+     * @return string Timezone abbreviation
+     */
+    static public function timezone_name_from_abbr($offset)
+    {
+        // List of timezones here is not complete - https://bugs.php.net/bug.php?id=44780
+        if ($tz = timezone_name_from_abbr('', $offset * 3600, 0)) {
+            return $tz;
+        }
+
+        // try with more complete list (#1489261)
+        $timezones = array(
+            '-660' => "Pacific/Apia",
+            '-600' => "Pacific/Honolulu",
+            '-570' => "Pacific/Marquesas",
+            '-540' => "America/Anchorage",
+            '-480' => "America/Los_Angeles",
+            '-420' => "America/Denver",
+            '-360' => "America/Chicago",
+            '-300' => "America/New_York",
+            '-270' => "America/Caracas",
+            '-240' => "America/Halifax",
+            '-210' => "Canada/Newfoundland",
+            '-180' => "America/Sao_Paulo",
+             '-60' => "Atlantic/Azores",
+               '0' => "Europe/London",
+              '60' => "Europe/Paris",
+             '120' => "Europe/Helsinki",
+             '180' => "Europe/Moscow",
+             '210' => "Asia/Tehran",
+             '240' => "Asia/Dubai",
+             '300' => "Asia/Karachi",
+             '270' => "Asia/Kabul",
+             '300' => "Asia/Karachi",
+             '330' => "Asia/Kolkata",
+             '345' => "Asia/Katmandu",
+             '360' => "Asia/Yekaterinburg",
+             '390' => "Asia/Rangoon",
+             '420' => "Asia/Krasnoyarsk",
+             '480' => "Asia/Shanghai",
+             '525' => "Australia/Eucla",
+             '540' => "Asia/Tokyo",
+             '570' => "Australia/Adelaide",
+             '600' => "Australia/Melbourne",
+             '630' => "Australia/Lord_Howe",
+             '660' => "Asia/Vladivostok",
+             '690' => "Pacific/Norfolk",
+             '720' => "Pacific/Auckland",
+             '765' => "Pacific/Chatham",
+             '780' => "Pacific/Enderbury",
+             '840' => "Pacific/Kiritimati",
+        );
+
+        return $timezones[(string) intval($offset * 60)];
     }
 }
diff --git a/lib/ext/Roundcube/rcube_contacts.php b/lib/ext/Roundcube/rcube_contacts.php
index 3919cdc..6d01368 100644
--- a/lib/ext/Roundcube/rcube_contacts.php
+++ b/lib/ext/Roundcube/rcube_contacts.php
@@ -718,6 +718,10 @@ class rcube_contacts extends rcube_addressbook
         foreach ($save_data as $key => $values) {
             list($field, $section) = explode(':', $key);
             $fulltext = in_array($field, $this->fulltext_cols);
+            // avoid casting DateTime objects to array
+            if (is_object($values) && is_a($values, 'DateTime')) {
+                $values = array(0 => $values);
+            }
             foreach ((array)$values as $value) {
                 if (isset($value))
                     $vcard->set($field, $value, $section);
diff --git a/lib/ext/Roundcube/rcube_csv2vcard.php b/lib/ext/Roundcube/rcube_csv2vcard.php
index fb8d8f1..00e6d4e 100644
--- a/lib/ext/Roundcube/rcube_csv2vcard.php
+++ b/lib/ext/Roundcube/rcube_csv2vcard.php
@@ -145,6 +145,7 @@ class rcube_csv2vcard
         'work_mobile'           => 'phone:work,cell',
         'work_title'            => 'jobtitle',
         'work_zip'              => 'zipcode:work',
+        'group'                 => 'groups',
     );
 
     /**
@@ -268,6 +269,7 @@ class rcube_csv2vcard
         'work_mobile'       => "Work Mobile",
         'work_title'        => "Work Title",
         'work_zip'          => "Work Zip",
+        'groups'            => "Group",
     );
 
     protected $local_label_map = array();
diff --git a/lib/ext/Roundcube/rcube_db.php b/lib/ext/Roundcube/rcube_db.php
index 8520700..aaba281 100644
--- a/lib/ext/Roundcube/rcube_db.php
+++ b/lib/ext/Roundcube/rcube_db.php
@@ -31,7 +31,10 @@ 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();
 
     protected $db_error     = false;
     protected $db_error_msg = '';
@@ -97,9 +100,12 @@ 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()));
     }
 
     /**
@@ -113,6 +119,13 @@ class rcube_db
         $this->db_error     = false;
         $this->db_error_msg = null;
 
+        // return existing handle
+        if ($this->dbhs[$mode]) {
+            $this->dbh = $this->dbhs[$mode];
+            $this->db_mode = $mode;
+            return $this->dbh;
+        }
+
         // Get database specific connection options
         $dsn_string  = $this->dsn_string($dsn);
         $dsn_options = $this->dsn_options($dsn);
@@ -147,6 +160,7 @@ class rcube_db
         }
 
         $this->dbh          = $dbh;
+        $this->dbhs[$mode]  = $dbh;
         $this->db_mode      = $mode;
         $this->db_connected = true;
         $this->conn_configure($dsn, $dbh);
@@ -175,8 +189,9 @@ class rcube_db
      * Connect to appropriate database depending on the operation
      *
      * @param string $mode Connection mode (r|w)
+     * @param boolean $force Enforce using the given mode
      */
-    public function db_connect($mode)
+    public function db_connect($mode, $force = false)
     {
         // previous connection failed, don't attempt to connect again
         if ($this->conn_failure) {
@@ -190,14 +205,13 @@ class rcube_db
 
         // Already connected
         if ($this->db_connected) {
-            // connected to db with the same or "higher" mode
-            if ($this->db_mode == 'w' || $this->db_mode == $mode) {
+            // 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) {
                 return;
             }
         }
 
         $dsn = ($mode == 'r') ? $this->db_dsnr_array : $this->db_dsnw_array;
-
         $this->dsn_connect($dsn, $mode);
 
         // use write-master when read-only fails
@@ -209,6 +223,46 @@ class rcube_db
     }
 
     /**
+     * Analyze the given SQL statement and select the appropriate connection to use
+     */
+    protected function dsn_select($query)
+    {
+        // no replication
+        if ($this->db_dsnw == $this->db_dsnr) {
+            return 'w';
+        }
+
+        // Read or write ?
+        $mode = preg_match('/^(select|show|set)/i', $query) ? 'r' : 'w';
+
+        // find tables involved in this query
+        if (preg_match_all('/(?:^|\s)(from|update|into|join)\s+'.$this->options['identifier_start'].'?([a-z0-9._]+)'.$this->options['identifier_end'].'?\s+/i', $query, $matches, PREG_SET_ORDER)) {
+            foreach ($matches as $m) {
+                $table = $m[2];
+
+                // always use direct mapping
+                if ($this->db_table_dsn_map[$table]) {
+                    $mode = $this->db_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) {
+                        $mode = $db_mode;
+                    }
+                }
+            }
+
+            // remember mode chosen (for primary table)
+            $table = $matches[0][2];
+            $this->table_connections[$table] = $mode;
+        }
+
+        return $mode;
+    }
+
+    /**
      * Activate/deactivate debug mode
      *
      * @param boolean $dbg True if SQL queries should be logged
@@ -340,10 +394,7 @@ class rcube_db
     {
         $query = trim($query);
 
-        // Read or write ?
-        $mode = preg_match('/^(select|show|set)/i', $query) ? 'r' : 'w';
-
-        $this->db_connect($mode);
+        $this->db_connect($this->dsn_select($query), true);
 
         // check connection before proceeding
         if (!$this->is_connected()) {
@@ -386,17 +437,7 @@ class rcube_db
         $result = $this->dbh->query($query);
 
         if ($result === false) {
-            $error = $this->dbh->errorInfo();
-
-            if (empty($this->options['ignore_key_errors']) || $error[0] != '23000') {
-                $this->db_error = true;
-                $this->db_error_msg = sprintf('[%s] %s', $error[1], $error[2]);
-
-                rcube::raise_error(array('code' => 500, 'type' => 'db',
-                    'line' => __LINE__, 'file' => __FILE__,
-                    'message' => $this->db_error_msg . " (SQL Query: $query)"
-                    ), true, false);
-            }
+            $result = $this->handle_error($query);
         }
 
         $this->last_result = $result;
@@ -405,6 +446,30 @@ class rcube_db
     }
 
     /**
+     * Helper method to handle DB errors.
+     * This by default logs the error but could be overriden by a driver implementation
+     *
+     * @param string Query that triggered the error
+     * @return mixed Result to be stored and returned
+     */
+    protected function handle_error($query)
+    {
+        $error = $this->dbh->errorInfo();
+
+        if (empty($this->options['ignore_key_errors']) || !in_array($error[0], array('23000', '23505'))) {
+            $this->db_error = true;
+            $this->db_error_msg = sprintf('[%s] %s', $error[1], $error[2]);
+
+            rcube::raise_error(array('code' => 500, 'type' => 'db',
+                'line' => __LINE__, 'file' => __FILE__,
+                'message' => $this->db_error_msg . " (SQL Query: $query)"
+                ), true, false);
+        }
+
+        return false;
+    }
+
+    /**
      * Get number of affected rows for the last query
      *
      * @param mixed $result Optional query handle
@@ -854,10 +919,14 @@ class rcube_db
      */
     public function table_name($table)
     {
-        $rcube = rcube::get_instance();
+        static $rcube;
+
+        if (!$rcube) {
+            $rcube = rcube::get_instance();
+        }
 
         // add prefix to the table name if configured
-        if ($prefix = $rcube->config->get('db_prefix')) {
+        if (($prefix = $rcube->config->get('db_prefix')) && strpos($table, $prefix) !== 0) {
             return $prefix . $table;
         }
 
@@ -876,6 +945,17 @@ class rcube_db
     }
 
     /**
+     * Set DSN connection to be used for the given table
+     *
+     * @param string Table name
+     * @param string DSN connection ('r' or 'w') to be used
+     */
+    public function set_table_dsn($table, $mode)
+    {
+        $this->db_table_dsn_map[$this->table_name($table)] = $mode;
+    }
+
+    /**
      * MDB2 DSN string parser
      *
      * @param string $sequence Secuence name
diff --git a/lib/ext/Roundcube/rcube_db_mssql.php b/lib/ext/Roundcube/rcube_db_mssql.php
index 3c1b9d7..726e4b4 100644
--- a/lib/ext/Roundcube/rcube_db_mssql.php
+++ b/lib/ext/Roundcube/rcube_db_mssql.php
@@ -52,7 +52,7 @@ class rcube_db_mssql extends rcube_db
     protected function conn_configure($dsn, $dbh)
     {
         // Set date format in case of non-default language (#1488918)
-        $this->query("SET DATEFORMAT ymd");
+        $dbh->query("SET DATEFORMAT ymd");
     }
 
     /**
diff --git a/lib/ext/Roundcube/rcube_db_mysql.php b/lib/ext/Roundcube/rcube_db_mysql.php
index 6fa5ad7..d3d0ac5 100644
--- a/lib/ext/Roundcube/rcube_db_mysql.php
+++ b/lib/ext/Roundcube/rcube_db_mysql.php
@@ -60,7 +60,7 @@ class rcube_db_mysql extends rcube_db
      */
     protected function conn_configure($dsn, $dbh)
     {
-        $this->query("SET NAMES 'utf8'");
+        $dbh->query("SET NAMES 'utf8'");
     }
 
     /**
@@ -179,4 +179,29 @@ class rcube_db_mysql extends rcube_db
         return isset($this->variables[$varname]) ? $this->variables[$varname] : $default;
     }
 
+    /**
+     * Handle DB errors, re-issue the query on deadlock errors from InnoDB row-level locking
+     *
+     * @param string Query that triggered the error
+     * @return mixed Result to be stored and returned
+     */
+    protected function handle_error($query)
+    {
+        $error = $this->dbh->errorInfo();
+
+        // retry after "Deadlock found when trying to get lock" errors
+        $retries = 2;
+        while ($error[1] == 1213 && $retries >= 0) {
+            usleep(50000);  // wait 50 ms
+            $result = $this->dbh->query($query);
+            if ($result !== false) {
+                return $result;
+            }
+            $error = $this->dbh->errorInfo();
+            $retries--;
+        }
+
+        return parent::handle_error($query);
+    }
+
 }
diff --git a/lib/ext/Roundcube/rcube_db_pgsql.php b/lib/ext/Roundcube/rcube_db_pgsql.php
index d72c9d6..68bf6d8 100644
--- a/lib/ext/Roundcube/rcube_db_pgsql.php
+++ b/lib/ext/Roundcube/rcube_db_pgsql.php
@@ -36,7 +36,7 @@ class rcube_db_pgsql extends rcube_db
      */
     protected function conn_configure($dsn, $dbh)
     {
-        $this->query("SET NAMES 'utf8'");
+        $dbh->query("SET NAMES 'utf8'");
     }
 
     /**
diff --git a/lib/ext/Roundcube/rcube_db_sqlsrv.php b/lib/ext/Roundcube/rcube_db_sqlsrv.php
index 45c41cd..4339f3d 100644
--- a/lib/ext/Roundcube/rcube_db_sqlsrv.php
+++ b/lib/ext/Roundcube/rcube_db_sqlsrv.php
@@ -52,7 +52,7 @@ class rcube_db_sqlsrv extends rcube_db
     protected function conn_configure($dsn, $dbh)
     {
         // Set date format in case of non-default language (#1488918)
-        $this->query("SET DATEFORMAT ymd");
+        $dbh->query("SET DATEFORMAT ymd");
     }
 
     /**
diff --git a/lib/ext/Roundcube/rcube_html2text.php b/lib/ext/Roundcube/rcube_html2text.php
index 9b248a3..6f79e2f 100644
--- a/lib/ext/Roundcube/rcube_html2text.php
+++ b/lib/ext/Roundcube/rcube_html2text.php
@@ -611,11 +611,13 @@ class rcube_html2text
                     $body = preg_replace_callback('/((?:^|\n)>*)([^\n]*)/', array($this, 'blockquote_citation_ballback'), trim($body));
                     $body = '<pre>' . htmlspecialchars($body) . '</pre>';
 
-                    $text = substr($text, 0, $start) . $body . "\n" . substr($text, $end + 13);
+                    $text = substr_replace($text, $body . "\n", $start, $end + 13 - $start);
                     $offset = 0;
+
                     break;
                 }
-            } while ($end || $next);
+            }
+            while ($end || $next);
         }
     }
 
@@ -624,8 +626,9 @@ class rcube_html2text
      */
     public function blockquote_citation_ballback($m)
     {
-        $line = ltrim($m[2]);
+        $line  = ltrim($m[2]);
         $space = $line[0] == '>' ? '' : ' ';
+
         return $m[1] . '>' . $space . $line;
     }
 
diff --git a/lib/ext/Roundcube/rcube_imap.php b/lib/ext/Roundcube/rcube_imap.php
index c5346c8..9faf1bb 100644
--- a/lib/ext/Roundcube/rcube_imap.php
+++ b/lib/ext/Roundcube/rcube_imap.php
@@ -70,7 +70,7 @@ class rcube_imap extends rcube_storage
     protected $search_sort_field = '';
     protected $search_threads = false;
     protected $search_sorted = false;
-    protected $options = array('auth_method' => 'check');
+    protected $options = array('auth_type' => 'check');
     protected $caching = false;
     protected $messages_caching = false;
     protected $threading = false;
@@ -391,10 +391,10 @@ class rcube_imap extends rcube_storage
     public function check_permflag($flag)
     {
         $flag       = strtoupper($flag);
-        $imap_flag  = $this->conn->flags[$flag];
         $perm_flags = $this->get_permflags($this->folder);
+        $imap_flag  = $this->conn->flags[$flag];
 
-        return in_array_nocase($imap_flag, $perm_flags);
+        return $imap_flag && !empty($perm_flags) && in_array_nocase($imap_flag, $perm_flags);
     }
 
 
@@ -410,17 +410,7 @@ class rcube_imap extends rcube_storage
         if (!strlen($folder)) {
             return array();
         }
-/*
-        Checking PERMANENTFLAGS is rather rare, so we disable caching of it
-        Re-think when we'll use it for more than only MDNSENT flag
 
-        $cache_key = 'mailboxes.permanentflags.' . $folder;
-        $permflags = $this->get_cache($cache_key);
-
-        if ($permflags !== null) {
-            return explode(' ', $permflags);
-        }
-*/
         if (!$this->check_connection()) {
             return array();
         }
@@ -435,10 +425,7 @@ class rcube_imap extends rcube_storage
         if (!is_array($permflags)) {
             $permflags = array();
         }
-/*
-        // Store permflags as string to limit cached object size
-        $this->update_cache($cache_key, implode(' ', $permflags));
-*/
+
         return $permflags;
     }
 
@@ -3773,12 +3760,17 @@ class rcube_imap extends rcube_storage
     /**
      * Enable or disable messages caching
      *
-     * @param boolean $set Flag
+     * @param boolean $set  Flag
+     * @param int     $mode Cache mode
      */
-    public function set_messages_caching($set)
+    public function set_messages_caching($set, $mode = null)
     {
         if ($set) {
             $this->messages_caching = true;
+
+            if ($mode && ($cache = $this->get_mcache_engine())) {
+                $cache->set_mode($mode);
+            }
         }
         else {
             if ($this->mcache) {
@@ -3798,9 +3790,10 @@ class rcube_imap extends rcube_storage
         if ($this->messages_caching && !$this->mcache) {
             $rcube = rcube::get_instance();
             if (($dbh = $rcube->get_dbh()) && ($userid = $rcube->get_user_id())) {
-                $ttl = $rcube->config->get('messages_cache_ttl', '10d');
+                $ttl       = $rcube->config->get('messages_cache_ttl', '10d');
+                $threshold = $rcube->config->get('messages_cache_threshold', 50);
                 $this->mcache = new rcube_imap_cache(
-                    $dbh, $this, $userid, $this->options['skip_deleted'], $ttl);
+                    $dbh, $this, $userid, $this->options['skip_deleted'], $ttl, $threshold);
             }
         }
 
@@ -3812,7 +3805,7 @@ class rcube_imap extends rcube_storage
      * Clears the messages cache.
      *
      * @param string $folder Folder name
-     * @param array  $uids    Optional message UIDs to remove from cache
+     * @param array  $uids   Optional message UIDs to remove from cache
      */
     protected function clear_message_cache($folder = null, $uids = null)
     {
diff --git a/lib/ext/Roundcube/rcube_imap_cache.php b/lib/ext/Roundcube/rcube_imap_cache.php
index 061ac54..a816654 100644
--- a/lib/ext/Roundcube/rcube_imap_cache.php
+++ b/lib/ext/Roundcube/rcube_imap_cache.php
@@ -27,6 +27,9 @@
  */
 class rcube_imap_cache
 {
+    const MODE_INDEX   = 1;
+    const MODE_MESSAGE = 2;
+
     /**
      * Instance of rcube_imap
      *
@@ -56,6 +59,13 @@ class rcube_imap_cache
     private $ttl;
 
     /**
+     * Maximum cached message size
+     *
+     * @var int
+     */
+    private $threshold;
+
+    /**
      * Internal (in-memory) cache
      *
      * @var array
@@ -63,6 +73,7 @@ class rcube_imap_cache
     private $icache = array();
 
     private $skip_deleted = false;
+    private $mode;
 
     /**
      * List of known flags. Thanks to this we can handle flag changes
@@ -88,6 +99,7 @@ class rcube_imap_cache
     );
 
 
+
     /**
      * Object constructor.
      *
@@ -96,9 +108,9 @@ class rcube_imap_cache
      * @param int        $userid       User identifier
      * @param bool       $skip_deleted skip_deleted flag
      * @param string     $ttl          Expiration time of memcache/apc items
-     *
+     * @param int        $threshold    Maximum cached message size
      */
-    function __construct($db, $imap, $userid, $skip_deleted, $ttl=0)
+    function __construct($db, $imap, $userid, $skip_deleted, $ttl=0, $threshold=0)
     {
         // convert ttl string to seconds
         $ttl = get_offset_sec($ttl);
@@ -109,6 +121,10 @@ class rcube_imap_cache
         $this->userid       = $userid;
         $this->skip_deleted = $skip_deleted;
         $this->ttl          = $ttl;
+        $this->threshold    = $threshold;
+
+        // cache all possible information by default
+        $this->mode = self::MODE_INDEX | self::MODE_MESSAGE;
     }
 
 
@@ -123,6 +139,17 @@ class rcube_imap_cache
 
 
     /**
+     * Set cache mode
+     *
+     * @param int $mode Cache mode
+     */
+    public function set_mode($mode)
+    {
+        $this->mode = $mode;
+    }
+
+
+    /**
      * Return (sorted) messages index (UIDs).
      * If index doesn't exist or is invalid, will be updated.
      *
@@ -300,38 +327,46 @@ class rcube_imap_cache
             return array();
         }
 
-        // Fetch messages from cache
-        $sql_result = $this->db->query(
-            "SELECT uid, data, flags"
-            ." FROM ".$this->db->table_name('cache_messages')
-            ." WHERE user_id = ?"
-                ." AND mailbox = ?"
-                ." AND uid IN (".$this->db->array2list($msgs, 'integer').")",
-            $this->userid, $mailbox);
-
-        $msgs   = array_flip($msgs);
         $result = array();
 
-        while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
-            $uid          = intval($sql_arr['uid']);
-            $result[$uid] = $this->build_message($sql_arr);
+        if ($this->mode & self::MODE_MESSAGE) {
+            // Fetch messages from cache
+            $sql_result = $this->db->query(
+                "SELECT uid, data, flags"
+                ." FROM ".$this->db->table_name('cache_messages')
+                ." WHERE user_id = ?"
+                    ." AND mailbox = ?"
+                    ." AND uid IN (".$this->db->array2list($msgs, 'integer').")",
+                $this->userid, $mailbox);
+
+            $msgs = array_flip($msgs);
+
+            while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
+                $uid          = intval($sql_arr['uid']);
+                $result[$uid] = $this->build_message($sql_arr);
 
-            if (!empty($result[$uid])) {
-                // save memory, we don't need message body here (?)
-                $result[$uid]->body = null;
+                if (!empty($result[$uid])) {
+                    // save memory, we don't need message body here (?)
+                    $result[$uid]->body = null;
 
-                unset($msgs[$uid]);
+                    unset($msgs[$uid]);
+                }
             }
+
+            $msgs = array_flip($msgs);
         }
 
         // Fetch not found messages from IMAP server
         if (!empty($msgs)) {
-            $messages = $this->imap->fetch_headers($mailbox, array_keys($msgs), false, true);
+            $messages = $this->imap->fetch_headers($mailbox, $msgs, false, true);
 
             // Insert to DB and add to result list
             if (!empty($messages)) {
                 foreach ($messages as $msg) {
-                    $this->add_message($mailbox, $msg, !array_key_exists($msg->uid, $result));
+                    if ($this->mode & self::MODE_MESSAGE) {
+                        $this->add_message($mailbox, $msg, !array_key_exists($msg->uid, $result));
+                    }
+
                     $result[$msg->uid] = $msg;
                 }
             }
@@ -362,17 +397,19 @@ class rcube_imap_cache
             return $this->icache['__message']['object'];
         }
 
-        $sql_result = $this->db->query(
-            "SELECT flags, data"
-            ." FROM ".$this->db->table_name('cache_messages')
-            ." WHERE user_id = ?"
-                ." AND mailbox = ?"
-                ." AND uid = ?",
-                $this->userid, $mailbox, (int)$uid);
+        if ($this->mode & self::MODE_MESSAGE) {
+            $sql_result = $this->db->query(
+                "SELECT flags, data"
+                ." FROM ".$this->db->table_name('cache_messages')
+                ." WHERE user_id = ?"
+                    ." AND mailbox = ?"
+                    ." AND uid = ?",
+                    $this->userid, $mailbox, (int)$uid);
 
-        if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
-            $message = $this->build_message($sql_arr);
-            $found   = true;
+            if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
+                $message = $this->build_message($sql_arr);
+                $found   = true;
+            }
         }
 
         // Get the message from IMAP server
@@ -381,6 +418,10 @@ class rcube_imap_cache
             // cache will be updated in close(), see below
         }
 
+        if (!($this->mode & self::MODE_MESSAGE)) {
+            return $message;
+        }
+
         // Save the message in internal cache, will be written to DB in close()
         // Common scenario: user opens unseen message
         // - get message (SELECT)
@@ -416,6 +457,10 @@ class rcube_imap_cache
             return;
         }
 
+        if (!($this->mode & self::MODE_MESSAGE)) {
+            return;
+        }
+
         $flags = 0;
         $msg   = clone $message;
 
@@ -487,6 +532,10 @@ class rcube_imap_cache
             return;
         }
 
+        if (!($this->mode & self::MODE_MESSAGE)) {
+            return;
+        }
+
         $flag = strtoupper($flag);
         $idx  = (int) array_search($flag, $this->flags);
         $uids = (array) $uids;
@@ -527,6 +576,10 @@ class rcube_imap_cache
      */
     function remove_message($mailbox = null, $uids = null)
     {
+        if (!($this->mode & self::MODE_MESSAGE)) {
+            return;
+        }
+
         if (!strlen($mailbox)) {
             $this->db->query(
                 "DELETE FROM ".$this->db->table_name('cache_messages')
@@ -1028,15 +1081,17 @@ class rcube_imap_cache
         $removed = array();
 
         // Get known UIDs
-        $sql_result = $this->db->query(
-            "SELECT uid"
-            ." FROM ".$this->db->table_name('cache_messages')
-            ." WHERE user_id = ?"
-                ." AND mailbox = ?",
-            $this->userid, $mailbox);
+        if ($this->mode & self::MODE_MESSAGE) {
+            $sql_result = $this->db->query(
+                "SELECT uid"
+                ." FROM ".$this->db->table_name('cache_messages')
+                ." WHERE user_id = ?"
+                    ." AND mailbox = ?",
+                $this->userid, $mailbox);
 
-        while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
-            $uids[] = $sql_arr['uid'];
+            while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
+                $uids[] = $sql_arr['uid'];
+            }
         }
 
         // Synchronize messages data
@@ -1155,13 +1210,13 @@ class rcube_imap_cache
         // Save current message from internal cache
         if ($message = $this->icache['__message']) {
             // clean up some object's data
-            $object = $this->message_object_prepare($message['object']);
+            $this->message_object_prepare($message['object']);
 
             // calculate current md5 sum
-            $md5sum = md5(serialize($object));
+            $md5sum = md5(serialize($message['object']));
 
             if ($message['md5sum'] != $md5sum) {
-                $this->add_message($message['mailbox'], $object, !$message['exists']);
+                $this->add_message($message['mailbox'], $message['object'], !$message['exists']);
             }
 
             $this->icache['__message']['md5sum'] = $md5sum;
@@ -1171,12 +1226,19 @@ class rcube_imap_cache
 
     /**
      * Prepares message object to be stored in database.
+     *
+     * @param rcube_message_header|rcube_message_part
      */
-    private function message_object_prepare($msg)
+    private function message_object_prepare(&$msg, &$size = 0)
     {
-        // Remove body too big (>25kB)
-        if ($msg->body && strlen($msg->body) > 25 * 1024) {
-            unset($msg->body);
+        // Remove body too big
+        if ($msg->body && ($length = strlen($msg->body))) {
+            $size += $length;
+
+            if ($size > $this->threshold * 1024) {
+                $size -= $length;
+                unset($msg->body);
+            }
         }
 
         // Fix mimetype which might be broken by some code when message is displayed
@@ -1186,13 +1248,19 @@ class rcube_imap_cache
             list($msg->ctype_primary, $msg->ctype_secondary) = explode('/', $msg->mimetype);
         }
 
+        unset($msg->replaces);
+
         if (is_array($msg->structure->parts)) {
-            foreach ($msg->structure->parts as $idx => $part) {
-                $msg->structure->parts[$idx] = $this->message_object_prepare($part);
+            foreach ($msg->structure->parts as $part) {
+                $this->message_object_prepare($part, $size);
             }
         }
 
-        return $msg;
+        if (is_array($msg->parts)) {
+            foreach ($msg->parts as $part) {
+                $this->message_object_prepare($part, $size);
+            }
+        }
     }
 
 
diff --git a/lib/ext/Roundcube/rcube_imap_generic.php b/lib/ext/Roundcube/rcube_imap_generic.php
index 3138465..f9a62f0 100644
--- a/lib/ext/Roundcube/rcube_imap_generic.php
+++ b/lib/ext/Roundcube/rcube_imap_generic.php
@@ -706,22 +706,11 @@ class rcube_imap_generic
      */
     function connect($host, $user, $password, $options=null)
     {
-        // set options
-        if (is_array($options)) {
-            $this->prefs = $options;
-        }
-        // set auth method
-        if (!empty($this->prefs['auth_type'])) {
-            $auth_method = strtoupper($this->prefs['auth_type']);
-        } else {
-            $auth_method = 'CHECK';
-        }
+        // configure
+        $this->set_prefs($options);
 
-        if (!empty($this->prefs['disabled_caps'])) {
-            $this->prefs['disabled_caps'] = array_map('strtoupper', (array)$this->prefs['disabled_caps']);
-        }
-
-        $result = false;
+        $auth_method = $this->prefs['auth_type'];
+        $result      = false;
 
         // initialize connection
         $this->error    = '';
@@ -898,6 +887,36 @@ class rcube_imap_generic
     }
 
     /**
+     * Initializes environment
+     */
+    protected function set_prefs($prefs)
+    {
+        // set preferences
+        if (is_array($prefs)) {
+            $this->prefs = $prefs;
+        }
+
+        // set auth method
+        if (!empty($this->prefs['auth_type'])) {
+            $this->prefs['auth_type'] = strtoupper($this->prefs['auth_type']);
+        }
+        else {
+            $this->prefs['auth_type'] = 'CHECK';
+        }
+
+        // disabled capabilities
+        if (!empty($this->prefs['disabled_caps'])) {
+            $this->prefs['disabled_caps'] = array_map('strtoupper', (array)$this->prefs['disabled_caps']);
+        }
+
+        // additional message flags
+        if (!empty($this->prefs['message_flags'])) {
+            $this->flags = array_merge($this->flags, $this->prefs['message_flags']);
+            unset($this->prefs['message_flags']);
+        }
+    }
+
+    /**
      * Checks connection status
      *
      * @return bool True if connection is active and user is logged in, False otherwise.
@@ -3139,8 +3158,7 @@ class rcube_imap_generic
         }
 
         foreach ($data as $entry) {
-            // If we are running in a murder topology, the entry[2] string needs
-            // to be escaped.
+            // Workaround cyrus-murder bug, the entry[2] string needs to be escaped
             if (self::$mupdate) {
                 $entry[2] = addcslashes($entry[2], '\\"');
             }
diff --git a/lib/ext/Roundcube/rcube_ldap.php b/lib/ext/Roundcube/rcube_ldap.php
index cb7fa84..64288f9 100644
--- a/lib/ext/Roundcube/rcube_ldap.php
+++ b/lib/ext/Roundcube/rcube_ldap.php
@@ -34,6 +34,7 @@ class rcube_ldap extends rcube_addressbook
     public $ready       = false;
     public $group_id    = 0;
     public $coltypes    = array();
+    public $export_groups = false;
 
     // private properties
     protected $ldap;
@@ -288,7 +289,9 @@ class rcube_ldap extends rcube_addressbook
                 $replaces = array('%dn' => '', '%dc' => $dc, '%d' => $d, '%fu' => $fu, '%u' => $u);
 
                 // Search for the dn to use to authenticate
-                if ($this->prop['search_base_dn'] && $this->prop['search_filter']) {
+                if ($this->prop['search_base_dn'] && $this->prop['search_filter']
+                    && (strstr($bind_dn, '%dn') || strstr($this->base_dn, '%dn') || strstr($this->groups_base_dn, '%dn'))
+                ) {
                     $search_bind_dn = strtr($this->prop['search_bind_dn'], $replaces);
                     $search_base_dn = strtr($this->prop['search_base_dn'], $replaces);
                     $search_filter  = strtr($this->prop['search_filter'], $replaces);
diff --git a/lib/ext/Roundcube/rcube_ldap_generic.php b/lib/ext/Roundcube/rcube_ldap_generic.php
index 88378dc..923a12a 100644
--- a/lib/ext/Roundcube/rcube_ldap_generic.php
+++ b/lib/ext/Roundcube/rcube_ldap_generic.php
@@ -696,11 +696,17 @@ class rcube_ldap_generic
      * Turn an LDAP entry into a regular PHP array with attributes as keys.
      *
      * @param array $entry Attributes array as retrieved from ldap_get_attributes() or ldap_get_entries()
+     *
      * @return array       Hash array with attributes as keys
      */
     public static function normalize_entry($entry)
     {
+        if (!isset($entry['count'])) {
+            return $entry;
+        }
+
         $rec = array();
+
         for ($i=0; $i < $entry['count']; $i++) {
             $attr = $entry[$i];
             if ($entry[$attr]['count'] == 1) {
diff --git a/lib/ext/Roundcube/rcube_message.php b/lib/ext/Roundcube/rcube_message.php
index 0d33ea4..9b662a2 100644
--- a/lib/ext/Roundcube/rcube_message.php
+++ b/lib/ext/Roundcube/rcube_message.php
@@ -195,8 +195,6 @@ class rcube_message
     /**
      * Determine if the message contains a HTML part. This must to be
      * a real part not an attachment (or its part)
-     * This must to be
-     * a real part not an attachment (or its part)
      *
      * @param bool $enriched Enables checking for text/enriched parts too
      *
@@ -214,14 +212,15 @@ class rcube_message
 
                 $level = explode('.', $part->mime_id);
 
-                // Check if the part belongs to higher-level's alternative/related
+                // Check if the part belongs to higher-level's multipart part
+                // this can be alternative/related/signed/encrypted, but not mixed
                 while (array_pop($level) !== null) {
                     if (!count($level)) {
                         return true;
                     }
 
                     $parent = $this->mime_parts[join('.', $level)];
-                    if ($parent->mimetype != 'multipart/alternative' && $parent->mimetype != 'multipart/related') {
+                    if (!preg_match('/^multipart\/(alternative|related|signed|encrypted)$/', $parent->mimetype)) {
                         continue 2;
                     }
                 }
@@ -435,17 +434,24 @@ class rcube_message
                     continue;
                 }
 
+                // We've encountered (malformed) messages with more than
+                // one text/plain or text/html part here. There's no way to choose
+                // which one is better, so we'll display first of them and add
+                // others as attachments (#1489358)
+
                 // check if sub part is
                 if ($is_multipart)
                     $related_part = $p;
-                else if ($sub_mimetype == 'text/plain')
+                else if ($sub_mimetype == 'text/plain' && !$plain_part)
                     $plain_part = $p;
-                else if ($sub_mimetype == 'text/html')
+                else if ($sub_mimetype == 'text/html' && !$html_part)
                     $html_part = $p;
-                else if ($sub_mimetype == 'text/enriched')
+                else if ($sub_mimetype == 'text/enriched' && !$enriched_part)
                     $enriched_part = $p;
-                else
-                    $attach_part = $p;
+                else {
+                    // add unsupported/unrecognized parts to attachments list
+                    $this->attachments[] = $sub_part;
+                }
             }
 
             // parse related part (alternative part could be in here)
@@ -486,11 +492,6 @@ class rcube_message
 
                 $this->parts[] = $c;
             }
-
-            // add unsupported/unrecognized parts to attachments list
-            if ($attach_part) {
-                $this->attachments[] = $structure->parts[$attach_part];
-            }
         }
         // this is an ecrypted message -> create a plaintext body with the according message
         else if ($mimetype == 'multipart/encrypted') {
diff --git a/lib/ext/Roundcube/rcube_mime.php b/lib/ext/Roundcube/rcube_mime.php
index 572540f..9c22203 100644
--- a/lib/ext/Roundcube/rcube_mime.php
+++ b/lib/ext/Roundcube/rcube_mime.php
@@ -637,7 +637,8 @@ class rcube_mime
                     if ($nextChar === ' ' || $nextChar === $separator) {
                         $afterNextChar = mb_substr($string, $width + 1, 1);
 
-                        if ($afterNextChar === false) {
+                        // Note: mb_substr() does never return False
+                        if ($afterNextChar === false || $afterNextChar === '') {
                             $subString .= $nextChar;
                         }
 
@@ -650,24 +651,23 @@ class rcube_mime
                             $subString = mb_substr($subString, 0, $spacePos);
                             $cutLength = $spacePos + 1;
                         }
-                        else if ($cut === false && $breakPos === false) {
-                            $subString = $string;
-                            $cutLength = null;
-                        }
                         else if ($cut === false) {
                             $spacePos = mb_strpos($string, ' ', 0);
 
-                            if ($spacePos !== false && $spacePos < $breakPos) {
+                            if ($spacePos !== false && ($breakPos === false || $spacePos < $breakPos)) {
                                 $subString = mb_substr($string, 0, $spacePos);
                                 $cutLength = $spacePos + 1;
                             }
+                            else if ($breakPos === false) {
+                                $subString = $string;
+                                $cutLength = null;
+                            }
                             else {
                                 $subString = mb_substr($string, 0, $breakPos);
                                 $cutLength = $breakPos + 1;
                             }
                         }
                         else {
-                            $subString = mb_substr($subString, 0, $width);
                             $cutLength = $width;
                         }
                     }
@@ -708,12 +708,20 @@ class rcube_mime
      */
     public static function file_content_type($path, $name, $failover = 'application/octet-stream', $is_stream = false, $skip_suffix = false)
     {
+        static $mime_ext = array();
+
         $mime_type = null;
-        $mime_magic = rcube::get_instance()->config->get('mime_magic');
-        $mime_ext = $skip_suffix ? null : @include(RCUBE_CONFIG_DIR . '/mimetypes.php');
+        $config = rcube::get_instance()->config;
+        $mime_magic = $config->get('mime_magic');
+
+        if (!$skip_suffix && empty($mime_ext)) {
+            foreach ($config->resolve_paths('mimetypes.php') as $fpath) {
+                $mime_ext = array_merge($mime_ext, (array) @include($fpath));
+            }
+        }
 
         // use file name suffix with hard-coded mime-type map
-        if (is_array($mime_ext) && $name) {
+        if (!$skip_suffix && is_array($mime_ext) && $name) {
             if ($suffix = substr($name, strrpos($name, '.')+1)) {
                 $mime_type = $mime_ext[strtolower($suffix)];
             }
@@ -818,7 +826,9 @@ class rcube_mime
 
         // fallback to some well-known types most important for daily emails
         if (empty($mime_types)) {
-            $mime_extensions = (array) @include(RCUBE_CONFIG_DIR . '/mimetypes.php');
+            foreach (rcube::get_instance()->config->resolve_paths('mimetypes.php') as $fpath) {
+                $mime_extensions = array_merge($mime_extensions, (array) @include($fpath));
+            }
 
             foreach ($mime_extensions as $ext => $mime) {
                 $mime_types[$mime][] = $ext;
diff --git a/lib/ext/Roundcube/rcube_plugin_api.php b/lib/ext/Roundcube/rcube_plugin_api.php
index 33f04ea..5a25ada 100644
--- a/lib/ext/Roundcube/rcube_plugin_api.php
+++ b/lib/ext/Roundcube/rcube_plugin_api.php
@@ -403,7 +403,7 @@ class rcube_plugin_api
                 $args = $ret + $args;
             }
 
-            if ($args['abort']) {
+            if ($args['break']) {
                 break;
             }
         }
diff --git a/lib/ext/Roundcube/rcube_spellchecker.php b/lib/ext/Roundcube/rcube_spellchecker.php
index df43652..31835db 100644
--- a/lib/ext/Roundcube/rcube_spellchecker.php
+++ b/lib/ext/Roundcube/rcube_spellchecker.php
@@ -3,8 +3,8 @@
 /*
  +-----------------------------------------------------------------------+
  | This file is part of the Roundcube Webmail client                     |
- | Copyright (C) 2011, Kolab Systems AG                                  |
- | Copyright (C) 2008-2011, The Roundcube Dev Team                       |
+ | Copyright (C) 2011-2013, Kolab Systems AG                             |
+ | Copyright (C) 2008-2013, The Roundcube Dev Team                       |
  |                                                                       |
  | Licensed under the GNU General Public License version 3 or            |
  | any later version with exceptions for skins & plugins.                |
@@ -28,21 +28,15 @@ class rcube_spellchecker
 {
     private $matches = array();
     private $engine;
+    private $backend;
     private $lang;
     private $rc;
     private $error;
-    private $separator = '/[\s\r\n\t\(\)\/\[\]{}<>\\"]+|[:;?!,\.](?=\W|$)/';
     private $options = array();
     private $dict;
     private $have_dict;
 
 
-    // default settings
-    const GOOGLE_HOST = 'ssl://www.google.com';
-    const GOOGLE_PORT = 443;
-    const MAX_SUGGESTIONS = 10;
-
-
     /**
      * Constructor
      *
@@ -60,6 +54,15 @@ class rcube_spellchecker
             'ignore_caps' => $this->rc->config->get('spellcheck_ignore_caps'),
             'dictionary'  => $this->rc->config->get('spellcheck_dictionary'),
         );
+
+        $cls = 'rcube_spellcheck_' . $this->engine;
+        if (class_exists($cls)) {
+            $this->backend = new $cls($this, $this->lang);
+            $this->backend->options = $this->options;
+        }
+        else {
+            $this->error = "Unknown spellcheck engine '$this->engine'";
+        }
     }
 
 
@@ -81,14 +84,8 @@ class rcube_spellchecker
             $this->content = $text;
         }
 
-        if ($this->engine == 'pspell') {
-            $this->matches = $this->_pspell_check($this->content);
-        }
-        else if ($this->engine == 'enchant') {
-            $this->matches = $this->_enchant_check($this->content);
-        }
-        else {
-            $this->matches = $this->_googie_check($this->content);
+        if ($this->backend) {
+            $this->matches = $this->backend->check($this->content);
         }
 
         return $this->found() == 0;
@@ -115,14 +112,11 @@ class rcube_spellchecker
      */
     function get_suggestions($word)
     {
-        if ($this->engine == 'pspell') {
-            return $this->_pspell_suggestions($word);
-        }
-        else if ($this->engine == 'enchant') {
-            return $this->_enchant_suggestions($word);
+        if ($this->backend) {
+            return $this->backend->get_suggestions($word);
         }
 
-        return $this->_googie_suggestions($word);
+        return array();
     }
 
 
@@ -136,14 +130,15 @@ class rcube_spellchecker
      */
     function get_words($text = null, $is_html=false)
     {
-        if ($this->engine == 'pspell') {
-            return $this->_pspell_words($text, $is_html);
+        if ($is_html) {
+            $text = $this->html2text($text);
         }
-        else if ($this->engine == 'enchant') {
-            return $this->_enchant_words($text, $is_html);
+
+        if ($this->backend) {
+            return $this->backend->get_words($text);
         }
 
-        return $this->_googie_words($text, $is_html);
+        return array();
     }
 
 
@@ -199,394 +194,7 @@ class rcube_spellchecker
      */
     function error()
     {
-        return $this->error;
-    }
-
-
-    /**
-     * Checks the text using pspell
-     *
-     * @param string $text Text content for spellchecking
-     */
-    private function _pspell_check($text)
-    {
-        // init spellchecker
-        $this->_pspell_init();
-
-        if (!$this->plink) {
-            return array();
-        }
-
-        // tokenize
-        $text = preg_split($this->separator, $text, NULL, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE);
-
-        $diff       = 0;
-        $matches    = array();
-
-        foreach ($text as $w) {
-            $word = trim($w[0]);
-            $pos  = $w[1] - $diff;
-            $len  = mb_strlen($word);
-
-            // skip exceptions
-            if ($this->is_exception($word)) {
-            }
-            else if (!pspell_check($this->plink, $word)) {
-                $suggestions = pspell_suggest($this->plink, $word);
-
-                if (sizeof($suggestions) > self::MAX_SUGGESTIONS) {
-                    $suggestions = array_slice($suggestions, 0, self::MAX_SUGGESTIONS);
-                }
-
-                $matches[] = array($word, $pos, $len, null, $suggestions);
-            }
-
-            $diff += (strlen($word) - $len);
-        }
-
-        return $matches;
-    }
-
-
-    /**
-     * Returns the misspelled words
-     */
-    private function _pspell_words($text = null, $is_html=false)
-    {
-        $result = array();
-
-        if ($text) {
-            // init spellchecker
-            $this->_pspell_init();
-
-            if (!$this->plink) {
-                return array();
-            }
-
-            // With PSpell we don't need to get suggestions to return misspelled words
-            if ($is_html) {
-                $text = $this->html2text($text);
-            }
-
-            $text = preg_split($this->separator, $text, NULL, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE);
-
-            foreach ($text as $w) {
-                $word = trim($w[0]);
-
-                // skip exceptions
-                if ($this->is_exception($word)) {
-                    continue;
-                }
-
-                if (!pspell_check($this->plink, $word)) {
-                    $result[] = $word;
-                }
-            }
-
-            return $result;
-        }
-
-        foreach ($this->matches as $m) {
-            $result[] = $m[0];
-        }
-
-        return $result;
-    }
-
-
-    /**
-     * Returns suggestions for misspelled word
-     */
-    private function _pspell_suggestions($word)
-    {
-        // init spellchecker
-        $this->_pspell_init();
-
-        if (!$this->plink) {
-            return array();
-        }
-
-        $suggestions = pspell_suggest($this->plink, $word);
-
-        if (sizeof($suggestions) > self::MAX_SUGGESTIONS)
-            $suggestions = array_slice($suggestions, 0, self::MAX_SUGGESTIONS);
-
-        return is_array($suggestions) ? $suggestions : array();
-    }
-
-
-    /**
-     * Initializes PSpell dictionary
-     */
-    private function _pspell_init()
-    {
-        if (!$this->plink) {
-            if (!extension_loaded('pspell')) {
-                $this->error = "Pspell extension not available";
-                return;
-            }
-
-            $this->plink = pspell_new($this->lang, null, null, RCUBE_CHARSET, PSPELL_FAST);
-        }
-
-        if (!$this->plink) {
-            $this->error = "Unable to load Pspell engine for selected language";
-        }
-    }
-
-
-    /**
-     * Checks the text using enchant
-     *
-     * @param string $text Text content for spellchecking
-     */
-    private function _enchant_check($text)
-    {
-        // init spellchecker
-        $this->_enchant_init();
-
-        if (!$this->enchant_dictionary) {
-            return array();
-        }
-
-        // tokenize
-        $text = preg_split($this->separator, $text, NULL, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE);
-
-        $diff       = 0;
-        $matches    = array();
-
-        foreach ($text as $w) {
-            $word = trim($w[0]);
-            $pos  = $w[1] - $diff;
-            $len  = mb_strlen($word);
-
-            // skip exceptions
-            if ($this->is_exception($word)) {
-            }
-            else if (!enchant_dict_check($this->enchant_dictionary, $word)) {
-                $suggestions = enchant_dict_suggest($this->enchant_dictionary, $word);
-
-                if (sizeof($suggestions) > self::MAX_SUGGESTIONS) {
-                    $suggestions = array_slice($suggestions, 0, self::MAX_SUGGESTIONS);
-                }
-
-                $matches[] = array($word, $pos, $len, null, $suggestions);
-            }
-
-            $diff += (strlen($word) - $len);
-        }
-
-        return $matches;
-    }
-
-
-    /**
-     * Returns the misspelled words
-     */
-    private function _enchant_words($text = null, $is_html=false)
-    {
-        $result = array();
-
-        if ($text) {
-            // init spellchecker
-            $this->_enchant_init();
-
-            if (!$this->enchant_dictionary) {
-                return array();
-            }
-
-            // With Enchant we don't need to get suggestions to return misspelled words
-            if ($is_html) {
-                $text = $this->html2text($text);
-            }
-
-            $text = preg_split($this->separator, $text, NULL, PREG_SPLIT_NO_EMPTY | PREG_SPLIT_OFFSET_CAPTURE);
-
-            foreach ($text as $w) {
-                $word = trim($w[0]);
-
-                // skip exceptions
-                if ($this->is_exception($word)) {
-                    continue;
-                }
-
-                if (!enchant_dict_check($this->enchant_dictionary, $word)) {
-                    $result[] = $word;
-                }
-            }
-
-            return $result;
-        }
-
-        foreach ($this->matches as $m) {
-            $result[] = $m[0];
-        }
-
-        return $result;
-    }
-
-
-    /**
-     * Returns suggestions for misspelled word
-     */
-    private function _enchant_suggestions($word)
-    {
-        // init spellchecker
-        $this->_enchant_init();
-
-        if (!$this->enchant_dictionary) {
-            return array();
-        }
-
-        $suggestions = enchant_dict_suggest($this->enchant_dictionary, $word);
-
-        if (sizeof($suggestions) > self::MAX_SUGGESTIONS)
-            $suggestions = array_slice($suggestions, 0, self::MAX_SUGGESTIONS);
-
-        return is_array($suggestions) ? $suggestions : array();
-    }
-
-
-    /**
-     * Initializes PSpell dictionary
-     */
-    private function _enchant_init()
-    {
-        if (!$this->enchant_broker) {
-            if (!extension_loaded('enchant')) {
-                $this->error = "Enchant extension not available";
-                return;
-            }
-
-            $this->enchant_broker = enchant_broker_init();
-        }
-
-        if (!enchant_broker_dict_exists($this->enchant_broker, $this->lang)) {
-            $this->error = "Unable to load dictionary for selected language using Enchant";
-            return;
-        }
-
-        $this->enchant_dictionary = enchant_broker_request_dict($this->enchant_broker, $this->lang);
-    }
-
-
-    private function _googie_check($text)
-    {
-        // spell check uri is configured
-        $url = $this->rc->config->get('spellcheck_uri');
-
-        if ($url) {
-            $a_uri = parse_url($url);
-            $ssl   = ($a_uri['scheme'] == 'https' || $a_uri['scheme'] == 'ssl');
-            $port  = $a_uri['port'] ? $a_uri['port'] : ($ssl ? 443 : 80);
-            $host  = ($ssl ? 'ssl://' : '') . $a_uri['host'];
-            $path  = $a_uri['path'] . ($a_uri['query'] ? '?'.$a_uri['query'] : '') . $this->lang;
-        }
-        else {
-            $host = self::GOOGLE_HOST;
-            $port = self::GOOGLE_PORT;
-            $path = '/tbproxy/spell?lang=' . $this->lang;
-        }
-
-        // Google has some problem with spaces, use \n instead
-        $gtext = str_replace(' ', "\n", $text);
-
-        $gtext = '<?xml version="1.0" encoding="utf-8" ?>'
-            .'<spellrequest textalreadyclipped="0" ignoredups="0" ignoredigits="1" ignoreallcaps="1">'
-            .'<text>' . $gtext . '</text>'
-            .'</spellrequest>';
-
-        $store = '';
-        if ($fp = fsockopen($host, $port, $errno, $errstr, 30)) {
-            $out = "POST $path HTTP/1.0\r\n";
-            $out .= "Host: " . str_replace('ssl://', '', $host) . "\r\n";
-            $out .= "Content-Length: " . strlen($gtext) . "\r\n";
-            $out .= "Content-Type: application/x-www-form-urlencoded\r\n";
-            $out .= "Connection: Close\r\n\r\n";
-            $out .= $gtext;
-            fwrite($fp, $out);
-
-            while (!feof($fp))
-                $store .= fgets($fp, 128);
-            fclose($fp);
-        }
-
-        // parse HTTP response
-        if (preg_match('!^HTTP/1.\d (\d+)(.+)!', $store, $m)) {
-            $http_status = $m[1];
-            if ($http_status != '200')
-                $this->error = 'HTTP ' . $m[1] . $m[2];
-        }
-
-        if (!$store) {
-            $this->error = "Empty result from spelling engine";
-        }
-        else if (preg_match('/<spellresult error="([^"]+)"/', $store, $m) && $m[1]) {
-            $this->error = "Error code $m[1] returned";
-        }
-
-        preg_match_all('/<c o="([^"]*)" l="([^"]*)" s="([^"]*)">([^<]*)<\/c>/', $store, $matches, PREG_SET_ORDER);
-
-        // skip exceptions (if appropriate options are enabled)
-        if (!empty($this->options['ignore_syms']) || !empty($this->options['ignore_nums'])
-            || !empty($this->options['ignore_caps']) || !empty($this->options['dictionary'])
-        ) {
-            foreach ($matches as $idx => $m) {
-                $word = mb_substr($text, $m[1], $m[2], RCUBE_CHARSET);
-                // skip  exceptions
-                if ($this->is_exception($word)) {
-                    unset($matches[$idx]);
-                }
-            }
-        }
-
-        return $matches;
-    }
-
-
-    private function _googie_words($text = null, $is_html=false)
-    {
-        if ($text) {
-            if ($is_html) {
-                $text = $this->html2text($text);
-            }
-
-            $matches = $this->_googie_check($text);
-        }
-        else {
-            $matches = $this->matches;
-            $text    = $this->content;
-        }
-
-        $result = array();
-
-        foreach ($matches as $m) {
-            $result[] = mb_substr($text, $m[1], $m[2], RCUBE_CHARSET);
-        }
-
-        return $result;
-    }
-
-
-    private function _googie_suggestions($word)
-    {
-        if ($word) {
-            $matches = $this->_googie_check($word);
-        }
-        else {
-            $matches = $this->matches;
-        }
-
-        if ($matches[0][4]) {
-            $suggestions = explode("\t", $matches[0][4]);
-            if (sizeof($suggestions) > self::MAX_SUGGESTIONS) {
-                $suggestions = array_slice($suggestions, 0, MAX_SUGGESTIONS);
-            }
-
-            return $suggestions;
-        }
-
-        return array();
+        return $this->error ? $this->error : ($this->backend ? $this->backend->error() : false);
     }
 
 
diff --git a/lib/ext/Roundcube/rcube_storage.php b/lib/ext/Roundcube/rcube_storage.php
index de83345..e697b2c 100644
--- a/lib/ext/Roundcube/rcube_storage.php
+++ b/lib/ext/Roundcube/rcube_storage.php
@@ -39,7 +39,7 @@ abstract class rcube_storage
     protected $default_charset = 'ISO-8859-1';
     protected $default_folders = array('INBOX');
     protected $search_set;
-    protected $options = array('auth_method' => 'check');
+    protected $options = array('auth_type' => 'check');
     protected $page_size = 10;
     protected $threading = false;
 
diff --git a/lib/ext/Roundcube/rcube_string_replacer.php b/lib/ext/Roundcube/rcube_string_replacer.php
index 354b459..77b91d1 100644
--- a/lib/ext/Roundcube/rcube_string_replacer.php
+++ b/lib/ext/Roundcube/rcube_string_replacer.php
@@ -24,11 +24,16 @@
  */
 class rcube_string_replacer
 {
-    public static $pattern = '/##str_replacement\[([0-9]+)\]##/';
+    public static $pattern = '/##str_replacement_(\d+)##/';
     public $mailto_pattern;
     public $link_pattern;
+    public $linkref_index;
+    public $linkref_pattern;
+
     private $values = array();
     private $options = array();
+    private $linkrefs = array();
+    private $urls = array();
 
 
     function __construct($options = array())
@@ -45,6 +50,8 @@ class rcube_string_replacer
             ."@$utf_domain"                                                 // domain-part
             ."(\?[$url1$url2]+)?"                                           // e.g. ?subject=test...
             .")/";
+        $this->linkref_index = '/\[([^\]#]+)\](:?\s*##str_replacement_(\d+)##)/';
+        $this->linkref_pattern = '/\[([^\]#]+)\]/';
 
         $this->options = $options;
     }
@@ -67,7 +74,7 @@ class rcube_string_replacer
      */
     public function get_replacement($i)
     {
-        return '##str_replacement['.$i.']##';
+        return '##str_replacement_' . $i . '##';
     }
 
     /**
@@ -96,6 +103,7 @@ class rcube_string_replacer
             $attrib['href'] = $url_prefix . $url;
 
             $i = $this->add(html::a($attrib, rcube::Q($url)) . $suffix);
+            $this->urls[$i] = $attrib['href'];
         }
 
         // Return valid link for recognized schemes, otherwise
@@ -104,6 +112,32 @@ class rcube_string_replacer
     }
 
     /**
+     * Callback to add an entry to the link index
+     */
+    public function linkref_addindex($matches)
+    {
+        $key = $matches[1];
+        $this->linkrefs[$key] = $this->urls[$matches[3]];
+
+        return $this->get_replacement($this->add('['.$key.']')) . $matches[2];
+    }
+
+    /**
+     * Callback to replace link references with real links
+     */
+    public function linkref_callback($matches)
+    {
+        $i = 0;
+        if ($url = $this->linkrefs[$matches[1]]) {
+            $attrib = (array)$this->options['link_attribs'];
+            $attrib['href'] = $url;
+            $i = $this->add(html::a($attrib, rcube::Q($matches[1])));
+        }
+
+        return $i > 0 ? '['.$this->get_replacement($i).']' : $matches[0];
+    }
+
+    /**
      * Callback function used to build mailto: links around e-mail strings
      *
      * @param array Matches result from preg_replace_callback
@@ -142,6 +176,9 @@ class rcube_string_replacer
         // search for patterns like links and e-mail addresses
         $str = preg_replace_callback($this->link_pattern, array($this, 'link_callback'), $str);
         $str = preg_replace_callback($this->mailto_pattern, array($this, 'mailto_callback'), $str);
+        // resolve link references
+        $str = preg_replace_callback($this->linkref_index, array($this, 'linkref_addindex'), $str);
+        $str = preg_replace_callback($this->linkref_pattern, array($this, 'linkref_callback'), $str);
 
         return $str;
     }
diff --git a/lib/ext/Roundcube/rcube_utils.php b/lib/ext/Roundcube/rcube_utils.php
index cf87ded..b73bc08 100644
--- a/lib/ext/Roundcube/rcube_utils.php
+++ b/lib/ext/Roundcube/rcube_utils.php
@@ -390,12 +390,13 @@ class rcube_utils
      * Convert array of request parameters (prefixed with _)
      * to a regular array with non-prefixed keys.
      *
-     * @param int    $mode   Source to get value from (GPC)
-     * @param string $ignore PCRE expression to skip parameters by name
+     * @param int     $mode       Source to get value from (GPC)
+     * @param string  $ignore     PCRE expression to skip parameters by name
+     * @param boolean $allow_html Allow HTML tags in field value
      *
      * @return array Hash array with all request parameters
      */
-    public static function request2param($mode = null, $ignore = 'task|action')
+    public static function request2param($mode = null, $ignore = 'task|action', $allow_html = false)
     {
         $out = array();
         $src = $mode == self::INPUT_GET ? $_GET : ($mode == self::INPUT_POST ? $_POST : $_REQUEST);
@@ -403,7 +404,7 @@ class rcube_utils
         foreach (array_keys($src) as $key) {
             $fname = $key[0] == '_' ? substr($key, 1) : $key;
             if ($ignore && !preg_match('/^(' . $ignore . ')$/', $fname)) {
-                $out[$fname] = self::get_input_value($key, $mode);
+                $out[$fname] = self::get_input_value($key, $mode, $allow_html);
             }
         }
 
@@ -444,41 +445,45 @@ class rcube_utils
         $source   = self::xss_entity_decode($source);
         $stripped = preg_replace('/[^a-z\(:;]/i', '', $source);
         $evilexpr = 'expression|behavior|javascript:|import[^a]' . (!$allow_remote ? '|url\(' : '');
+
         if (preg_match("/$evilexpr/i", $stripped)) {
             return '/* evil! */';
         }
 
+        $strict_url_regexp = '!url\s*\([ "\'](https?:)//[a-z0-9/._+-]+["\' ]\)!Uims';
+
         // cut out all contents between { and }
         while (($pos = strpos($source, '{', $last_pos)) && ($pos2 = strpos($source, '}', $pos))) {
-            $styles = substr($source, $pos+1, $pos2-($pos+1));
+            $length = $pos2 - $pos - 1;
+            $styles = substr($source, $pos+1, $length);
 
             // check every line of a style block...
             if ($allow_remote) {
                 $a_styles = preg_split('/;[\r\n]*/', $styles, -1, PREG_SPLIT_NO_EMPTY);
+
                 foreach ($a_styles as $line) {
                     $stripped = preg_replace('/[^a-z\(:;]/i', '', $line);
                     // ... and only allow strict url() values
-                    $regexp = '!url\s*\([ "\'](https?:)//[a-z0-9/._+-]+["\' ]\)!Uims';
-                    if (stripos($stripped, 'url(') && !preg_match($regexp, $line)) {
+                    if (stripos($stripped, 'url(') && !preg_match($strict_url_regexp, $line)) {
                         $a_styles = array('/* evil! */');
                         break;
                     }
                 }
+
                 $styles = join(";\n", $a_styles);
             }
 
-            $key = $replacements->add($styles);
-            $source = substr($source, 0, $pos+1)
-                . $replacements->get_replacement($key)
-                . substr($source, $pos2, strlen($source)-$pos2);
-            $last_pos = $pos+2;
+            $key      = $replacements->add($styles);
+            $repl     = $replacements->get_replacement($key);
+            $source   = substr_replace($source, $repl, $pos+1, $length);
+            $last_pos = $pos2 - ($length - strlen($repl));
         }
 
         // remove html comments and add #container to each tag selector.
         // also replace body definition because we also stripped off the <body> tag
-        $styles = preg_replace(
+        $source = preg_replace(
             array(
-                '/(^\s*<!--)|(-->\s*$)/',
+                '/(^\s*<\!--)|(-->\s*$)/m',
                 '/(^\s*|,\s*|\}\s*)([a-z0-9\._#\*][a-z0-9\.\-_]*)/im',
                 '/'.preg_quote($container_id, '/').'\s+body/i',
             ),
@@ -490,9 +495,9 @@ class rcube_utils
             $source);
 
         // put block contents back in
-        $styles = $replacements->resolve($styles);
+        $source = $replacements->resolve($source);
 
-        return $styles;
+        return $source;
     }
 
 
@@ -739,11 +744,22 @@ class rcube_utils
      */
     public static function strtotime($date)
     {
+        $date = trim($date);
+
         // check for MS Outlook vCard date format YYYYMMDD
-        if (preg_match('/^([12][90]\d\d)([01]\d)(\d\d)$/', trim($date), $matches)) {
-            return mktime(0,0,0, intval($matches[2]), intval($matches[3]), intval($matches[1]));
+        if (preg_match('/^([12][90]\d\d)([01]\d)([0123]\d)$/', $date, $m)) {
+            return mktime(0,0,0, intval($m[2]), intval($m[3]), intval($m[1]));
+        }
+
+        // common little-endian formats, e.g. dd/mm/yyyy (not all are supported by strtotime)
+        if (preg_match('/^(\d{1,2})[.\/-](\d{1,2})[.\/-](\d{4})$/', $date, $m)
+            && $m[1] > 0 && $m[1] <= 31 && $m[2] > 0 && $m[2] <= 12 && $m[3] >= 1970
+        ) {
+            return mktime(0,0,0, intval($m[2]), intval($m[1]), intval($m[3]));
         }
-        else if (is_numeric($date)) {
+
+        // unix timestamp
+        if (is_numeric($date)) {
             return (int) $date;
         }
 
@@ -776,6 +792,44 @@ class rcube_utils
         return (int) $ts;
     }
 
+    /**
+     * Date parsing function that turns the given value into a DateTime object
+     *
+     * @param string $date  Date string
+     *
+     * @return object DateTime instance or false on failure
+     */
+    public static function anytodatetime($date)
+    {
+        if (is_object($date) && is_a($date, 'DateTime')) {
+            return $date;
+        }
+
+        $dt = false;
+        $date = trim($date);
+
+        // try to parse string with DateTime first
+        if (!empty($date)) {
+            try {
+                $dt = new DateTime($date);
+            }
+            catch (Exception $e) {
+                // ignore
+            }
+        }
+
+        // try our advanced strtotime() method
+        if (!$dt && ($timestamp = self::strtotime($date))) {
+            try {
+                $dt = new DateTime("@".$timestamp);
+            }
+            catch (Exception $e) {
+                // ignore
+            }
+        }
+
+        return $dt;
+    }
 
     /*
      * Idn_to_ascii wrapper.
diff --git a/lib/ext/Roundcube/rcube_vcard.php b/lib/ext/Roundcube/rcube_vcard.php
index a71305c..d54dc56 100644
--- a/lib/ext/Roundcube/rcube_vcard.php
+++ b/lib/ext/Roundcube/rcube_vcard.php
@@ -47,6 +47,7 @@ class rcube_vcard
         'manager'     => 'X-MANAGER',
         'spouse'      => 'X-SPOUSE',
         'edit'        => 'X-AB-EDIT',
+        'groups'      => 'CATEGORIES',
     );
     private $typemap = array(
         'IPHONE'   => 'mobile',
@@ -357,8 +358,8 @@ class rcube_vcard
 
         case 'birthday':
         case 'anniversary':
-            if (($val = rcube_utils::strtotime($value)) && ($fn = self::$fieldmap[$field])) {
-                $this->raw[$fn][] = array(0 => date('Y-m-d', $val), 'value' => array('date'));
+            if (($val = rcube_utils::anytodatetime($value)) && ($fn = self::$fieldmap[$field])) {
+                $this->raw[$fn][] = array(0 => $val->format('Y-m-d'), 'value' => array('date'));
             }
             break;
 
@@ -756,7 +757,7 @@ class rcube_vcard
      *
      * @return string Joined and quoted string
      */
-    private static function vcard_quote($s, $sep = ';')
+    public static function vcard_quote($s, $sep = ';')
     {
         if (is_array($s)) {
             foreach($s as $part) {
@@ -765,7 +766,7 @@ class rcube_vcard
             return(implode($sep, (array)$r));
         }
 
-        return strtr($s, array('\\' => '\\\\', "\r" => '', "\n" => '\n', ',' => '\,', ';' => '\;'));
+        return strtr($s, array('\\' => '\\\\', "\r" => '', "\n" => '\n', $sep => '\\'.$sep));
     }
 
     /**
diff --git a/lib/ext/Roundcube/rcube_washtml.php b/lib/ext/Roundcube/rcube_washtml.php
index 8f7fe97..e746754 100644
--- a/lib/ext/Roundcube/rcube_washtml.php
+++ b/lib/ext/Roundcube/rcube_washtml.php
@@ -377,7 +377,14 @@ class rcube_washtml
         // Detect max nesting level (for dumpHTML) (#1489110)
         $this->max_nesting_level = (int) @ini_get('xdebug.max_nesting_level');
 
-        @$node->loadHTML($html);
+        // Use optimizations if supported
+        if (version_compare(PHP_VERSION, '5.4.0', '>=')) {
+            @$node->loadHTML($html, LIBXML_PARSEHUGE | LIBXML_COMPACT);
+        }
+        else {
+            @$node->loadHTML($html);
+        }
+
         return $this->dumpHtml($node);
     }
 
@@ -448,7 +455,7 @@ class rcube_washtml
         }
 
         // fix (unknown/malformed) HTML tags before "wash"
-        $html = preg_replace_callback('/(<[\/]*)([^\s>]+)/', array($this, 'html_tag_callback'), $html);
+        $html = preg_replace_callback('/(<(?!\!)[\/]*)([^\s>]+)/', array($this, 'html_tag_callback'), $html);
 
         // Remove invalid HTML comments (#1487759)
         // Don't remove valid conditional comments


commit f59e95cc68a08dde997a37ab3b9c7b9d3a1b763c
Author: Jeroen van Meeuwen (Kolab Systems) <vanmeeuwen at kolabsys.com>
Date:   Thu Oct 17 18:19:33 2013 +0200

    Rebase libkolab with cache refactoring

diff --git a/lib/plugins/libkolab/SQL/mysql.initial.sql b/lib/plugins/libkolab/SQL/mysql.initial.sql
index 764da2a..4f23a52 100644
--- a/lib/plugins/libkolab/SQL/mysql.initial.sql
+++ b/lib/plugins/libkolab/SQL/mysql.initial.sql
@@ -1,29 +1,175 @@
 /**
  * libkolab database schema
  *
- * @version @package_version@
+ * @version 1.0
  * @author Thomas Bruederli
  * @licence GNU AGPL
  **/
 
+
+DROP TABLE IF EXISTS `kolab_folders`;
+
+CREATE TABLE `kolab_folders` (
+  `folder_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
+  `resource` VARCHAR(255) NOT NULL,
+  `type` VARCHAR(32) NOT NULL,
+  `synclock` INT(10) NOT NULL DEFAULT '0',
+  `ctag` VARCHAR(40) DEFAULT NULL,
+  PRIMARY KEY(`folder_id`),
+  INDEX `resource_type` (`resource`, `type`)
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
 DROP TABLE IF EXISTS `kolab_cache`;
 
-CREATE TABLE `kolab_cache` (
-  `resource` VARCHAR(255) CHARACTER SET ascii NOT NULL,
+DROP TABLE IF EXISTS `kolab_cache_contact`;
+
+CREATE TABLE `kolab_cache_contact` (
+  `folder_id` BIGINT UNSIGNED NOT NULL,
+  `msguid` BIGINT UNSIGNED NOT NULL,
+  `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
+  `created` DATETIME DEFAULT NULL,
+  `changed` DATETIME DEFAULT NULL,
+  `data` TEXT NOT NULL,
+  `xml` TEXT NOT NULL,
+  `tags` VARCHAR(255) NOT NULL,
+  `words` TEXT NOT NULL,
   `type` VARCHAR(32) CHARACTER SET ascii 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`)
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+DROP TABLE IF EXISTS `kolab_cache_event`;
+
+CREATE TABLE `kolab_cache_event` (
+  `folder_id` BIGINT UNSIGNED NOT NULL,
   `msguid` BIGINT UNSIGNED NOT NULL,
   `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
   `created` DATETIME DEFAULT NULL,
   `changed` DATETIME DEFAULT NULL,
   `data` TEXT NOT NULL,
   `xml` TEXT NOT NULL,
+  `tags` VARCHAR(255) NOT NULL,
+  `words` TEXT NOT NULL,
   `dtstart` DATETIME,
   `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`)
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+DROP TABLE IF EXISTS `kolab_cache_task`;
+
+CREATE TABLE `kolab_cache_task` (
+  `folder_id` BIGINT UNSIGNED NOT NULL,
+  `msguid` BIGINT UNSIGNED NOT NULL,
+  `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
+  `created` DATETIME DEFAULT NULL,
+  `changed` DATETIME DEFAULT NULL,
+  `data` TEXT NOT NULL,
+  `xml` TEXT NOT NULL,
+  `tags` VARCHAR(255) NOT NULL,
+  `words` TEXT NOT NULL,
+  `dtstart` DATETIME,
+  `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`)
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+DROP TABLE IF EXISTS `kolab_cache_journal`;
+
+CREATE TABLE `kolab_cache_journal` (
+  `folder_id` BIGINT UNSIGNED NOT NULL,
+  `msguid` BIGINT UNSIGNED NOT NULL,
+  `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
+  `created` DATETIME DEFAULT NULL,
+  `changed` DATETIME DEFAULT NULL,
+  `data` TEXT NOT NULL,
+  `xml` TEXT NOT NULL,
+  `tags` VARCHAR(255) NOT NULL,
+  `words` TEXT NOT NULL,
+  `dtstart` DATETIME,
+  `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`)
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+DROP TABLE IF EXISTS `kolab_cache_note`;
+
+CREATE TABLE `kolab_cache_note` (
+  `folder_id` BIGINT UNSIGNED NOT NULL,
+  `msguid` BIGINT UNSIGNED NOT NULL,
+  `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
+  `created` DATETIME DEFAULT NULL,
+  `changed` DATETIME DEFAULT NULL,
+  `data` TEXT NOT NULL,
+  `xml` TEXT 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`)
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+DROP TABLE IF EXISTS `kolab_cache_file`;
+
+CREATE TABLE `kolab_cache_file` (
+  `folder_id` BIGINT UNSIGNED NOT NULL,
+  `msguid` BIGINT UNSIGNED NOT NULL,
+  `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
+  `created` DATETIME DEFAULT NULL,
+  `changed` DATETIME DEFAULT NULL,
+  `data` TEXT NOT NULL,
+  `xml` TEXT NOT NULL,
   `tags` VARCHAR(255) NOT NULL,
   `words` TEXT NOT NULL,
   `filename` varchar(255) DEFAULT NULL,
-  PRIMARY KEY(`resource`,`type`,`msguid`),
-  INDEX `resource_filename` (`resource`, `filename`)
+  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`)
 ) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
 
-INSERT INTO `system` (`name`, `value`) VALUES ('libkolab-version', '2013041900');
+DROP TABLE IF EXISTS `kolab_cache_configuration`;
+
+CREATE TABLE `kolab_cache_configuration` (
+  `folder_id` BIGINT UNSIGNED NOT NULL,
+  `msguid` BIGINT UNSIGNED NOT NULL,
+  `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
+  `created` DATETIME DEFAULT NULL,
+  `changed` DATETIME DEFAULT NULL,
+  `data` TEXT NOT NULL,
+  `xml` TEXT NOT NULL,
+  `tags` VARCHAR(255) NOT NULL,
+  `words` TEXT NOT NULL,
+  `type` VARCHAR(32) CHARACTER SET ascii NOT NULL,
+  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`)
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+DROP TABLE IF EXISTS `kolab_cache_freebusy`;
+
+CREATE TABLE `kolab_cache_freebusy` (
+  `folder_id` BIGINT UNSIGNED NOT NULL,
+  `msguid` BIGINT UNSIGNED NOT NULL,
+  `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
+  `created` DATETIME DEFAULT NULL,
+  `changed` DATETIME DEFAULT NULL,
+  `data` TEXT NOT NULL,
+  `xml` TEXT NOT NULL,
+  `tags` VARCHAR(255) NOT NULL,
+  `words` TEXT NOT NULL,
+  `dtstart` DATETIME,
+  `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`)
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+
+INSERT INTO `system` (`name`, `value`) VALUES ('libkolab-version', '2013100400');
diff --git a/lib/plugins/libkolab/bin/modcache.sh b/lib/plugins/libkolab/bin/modcache.sh
index 5ac9a21..da6e4f8 100755
--- a/lib/plugins/libkolab/bin/modcache.sh
+++ b/lib/plugins/libkolab/bin/modcache.sh
@@ -4,7 +4,7 @@
 /**
  * Kolab storage cache modification script
  *
- * @version 3.0
+ * @version 3.1
  * @author Thomas Bruederli <bruederli at kolabsys.com>
  *
  * Copyright (C) 2012, Kolab Systems AG <contact at kolabsys.com>
@@ -56,7 +56,14 @@ $opts = get_opt(array(
 $opts['username'] = !empty($opts[1]) ? $opts[1] : $opts['user'];
 $action = $opts[0];
 
-$rcmail = rcube::get_instance();
+$rcmail = rcube::get_instance(rcube::INIT_WITH_DB | rcube::INIT_WITH_PLUGINS);
+
+
+// connect to database
+$db = $rcmail->get_dbh();
+$db->db_connect('w');
+if (!$db->is_connected() || $db->is_error())
+    die("No DB connection\n");
 
 
 /*
@@ -68,32 +75,42 @@ switch (strtolower($action)) {
  * Clear/expunge all cache records
  */
 case 'expunge':
+    $folder_types = $opts['type'] ? explode(',', $opts['type']) : array('contact','configuration','event','file','journal','note','task');
+    $folder_types_db = array_map(array($db, 'quote'), $folder_types);
     $expire = strtotime(!empty($opts[2]) ? $opts[2] : 'now - 10 days');
-    $sql_add = " AND created <= '" . date('Y-m-d 00:00:00', $expire) . "'";
+    $sql_where = "type IN (" . join(',', $folder_types_db) . ")";
+
+    if ($opts['username']) {
+        $sql_where .= ' AND resource LIKE ?';
+    }
+
+    $sql_query = "DELETE FROM %s WHERE folder_id IN (SELECT folder_id FROM kolab_folders WHERE $sql_where) AND created <= " . $db->quote(date('Y-m-d 00:00:00', $expire));
     if ($opts['limit']) {
-        $sql_add .= ' LIMIT ' . intval($opts['limit']);
+        $sql_query = ' LIMIT ' . intval($opts['limit']);
+    }
+    foreach ($folder_types as $type) {
+        $table_name = 'kolab_cache_' . $type;
+        $db->query(sprintf($sql_query, $table_name), resource_prefix($opts).'%');
+        echo $db->affected_rows() . " records deleted from '$table_name'\n";
     }
 
-case 'clear':
-    // connect to database
-    $db = $rcmail->get_dbh();
-    $db->db_connect('w');
-    if (!$db->is_connected() || $db->is_error())
-        die("No DB connection\n");
+    $db->query("UPDATE kolab_folders SET ctag='' WHERE $sql_where", resource_prefix($opts).'%');
+    break;
 
-    $folder_types = $opts['type'] ? explode(',', $opts['type']) : array('contact','distribution-list','event','task','configuration','file');
+case 'clear':
+    $folder_types = $opts['type'] ? explode(',', $opts['type']) : array('contact','configuration','event','file','journal','note','task');
     $folder_types_db = array_map(array($db, 'quote'), $folder_types);
 
     if ($opts['all']) {
-        $sql_query = "DELETE FROM kolab_cache WHERE type IN (" . join(',', $folder_types_db) . ")";
+        $sql_query = "DELETE FROM kolab_folders WHERE 1";
     }
     else if ($opts['username']) {
-        $sql_query = "DELETE FROM kolab_cache WHERE type IN (" . join(',', $folder_types_db) . ") AND resource LIKE ?";
+        $sql_query = "DELETE FROM kolab_folders WHERE type IN (" . join(',', $folder_types_db) . ") AND resource LIKE ?";
     }
 
     if ($sql_query) {
         $db->query($sql_query . $sql_add, resource_prefix($opts).'%');
-        echo $db->affected_rows() . " records deleted from 'kolab_cache'\n";
+        echo $db->affected_rows() . " records deleted from 'kolab_folders'\n";
     }
     break;
 
@@ -106,7 +123,7 @@ case 'prewarm':
     $rcmail->plugins->load_plugin('libkolab');
 
     if (authenticate($opts)) {
-        $folder_types = $opts['type'] ? explode(',', $opts['type']) : array('contact','event','task','configuration','file');
+        $folder_types = $opts['type'] ? explode(',', $opts['type']) : array('contact','configuration','event','file','task');
         foreach ($folder_types as $type) {
             // sync every folder of the given type
             foreach (kolab_storage::get_folders($type) as $folder) {
@@ -140,7 +157,7 @@ default:
  */
 function resource_prefix($opts)
 {
-    return 'imap://' . urlencode($opts['username']) . '@' . $opts['host'] . '/';
+    return 'imap://' . str_replace('%', '\\%', urlencode($opts['username'])) . '@' . $opts['host'] . '/';
 }
 
 
diff --git a/lib/plugins/libkolab/config.inc.php.dist b/lib/plugins/libkolab/config.inc.php.dist
index aa0c8d0..0c612a3 100644
--- a/lib/plugins/libkolab/config.inc.php.dist
+++ b/lib/plugins/libkolab/config.inc.php.dist
@@ -12,9 +12,6 @@ $rcmail_config['kolab_format_version']  = '3.0';
 // Defaults to https://<imap-server->/freebusy
 $rcmail_config['kolab_freebusy_server'] = 'https://<some-host>/<freebusy-path>';
 
-// Set this option to disable SSL certificate checks when triggering Free/Busy (enabled by default)
-$rcmail_config['kolab_ssl_verify_peer'] = false;
-
 // Enables listing of only subscribed folders. This e.g. will limit
 // folders in calendar view or available addressbooks
 $rcmail_config['kolab_use_subscriptions'] = false;
@@ -23,4 +20,13 @@ $rcmail_config['kolab_use_subscriptions'] = false;
 // for displaying resource folder names (experimental!)
 $rcmail_config['kolab_custom_display_names'] = false;
 
-?>
+// Configuration of HTTP requests.
+// See http://pear.php.net/manual/en/package.http.http-request2.config.php
+// for list of supported configuration options (array keys)
+$rcmail_config['kolab_http_request'] = array();
+
+// When kolab_cache is enabled Roundcube's messages cache will be redundant
+// when working on kolab folders. Here we can:
+// 2 - bypass messages/indexes cache completely
+// 1 - bypass only messages, but use index cache
+$rcmail_config['kolab_messages_cache_bypass'] = 0;
diff --git a/lib/plugins/libkolab/lib/kolab_format.php b/lib/plugins/libkolab/lib/kolab_format.php
index 66ba380..5bcc57a 100644
--- a/lib/plugins/libkolab/lib/kolab_format.php
+++ b/lib/plugins/libkolab/lib/kolab_format.php
@@ -40,6 +40,7 @@ abstract class kolab_format
     protected $data;
     protected $xmldata;
     protected $xmlobject;
+    protected $formaterror;
     protected $loaded = false;
     protected $version = '3.0';
 
@@ -104,13 +105,17 @@ abstract class kolab_format
         }
         $result = new cDateTime();
 
-        // got a unix timestamp (in UTC)
-        if (is_numeric($datetime)) {
-            $datetime = new DateTime('@'.$datetime, new DateTimeZone('UTC'));
-            if ($tz) $datetime->setTimezone($tz);
+        try {
+            // got a unix timestamp (in UTC)
+            if (is_numeric($datetime)) {
+                $datetime = new DateTime('@'.$datetime, new DateTimeZone('UTC'));
+                if ($tz) $datetime->setTimezone($tz);
+            }
+            else if (is_string($datetime) && strlen($datetime)) {
+                $datetime = new DateTime($datetime, $tz ?: null);
+            }
         }
-        else if (is_string($datetime) && strlen($datetime))
-            $datetime = new DateTime($datetime, $tz ?: null);
+        catch (Exception $e) {}
 
         if ($datetime instanceof DateTime) {
             $result->setDate($datetime->format('Y'), $datetime->format('n'), $datetime->format('j'));
@@ -244,7 +249,7 @@ abstract class kolab_format
                 $log = "Error";
         }
 
-        if ($log) {
+        if ($log && !isset($this->formaterror)) {
             rcube::raise_error(array(
                 'code' => 660,
                 'type' => 'php',
@@ -252,6 +257,8 @@ abstract class kolab_format
                 'line' => __LINE__,
                 'message' => "kolabformat $log: " . kolabformat::errorMessage(),
             ), true);
+
+            $this->formaterror = $ret;
         }
 
         return $ret;
@@ -338,6 +345,7 @@ abstract class kolab_format
      */
     public function load($xml)
     {
+        $this->formaterror = null;
         $read_func = $this->libfunc($this->read_func);
 
         if (is_array($read_func))
@@ -361,6 +369,8 @@ abstract class kolab_format
      */
     public function write($version = null)
     {
+        $this->formaterror = null;
+
         $this->init();
         $write_func = $this->libfunc($this->write_func);
         if (is_array($write_func))
@@ -389,24 +399,33 @@ abstract class kolab_format
             $this->obj->setUid($object['uid']);
 
         // set some automatic values if missing
-        if (method_exists($this->obj, 'setCreated') && !$this->obj->created()) {
-            if (empty($object['created']))
-                $object['created'] = new DateTime('now', self::$timezone);
-            $this->obj->setCreated(self::get_datetime($object['created']));
+        if (empty($object['created']) && method_exists($this->obj, 'setCreated')) {
+            $cdt = $this->obj->created();
+            $object['created'] = $cdt && $cdt->isValid() ? self::php_datetime($cdt) : new DateTime('now', self::$timezone);
+            if (!$cdt || !$cdt->isValid())
+                $this->obj->setCreated(self::get_datetime($object['created']));
         }
 
         $object['changed'] = new DateTime('now', self::$timezone);
         $this->obj->setLastModified(self::get_datetime($object['changed'], new DateTimeZone('UTC')));
 
         // Save custom properties of the given object
-        if (!empty($object['x-custom'])) {
+        if (isset($object['x-custom'])) {
             $vcustom = new vectorcs;
-            foreach ($object['x-custom'] as $cp) {
+            foreach ((array)$object['x-custom'] as $cp) {
                 if (is_array($cp))
                     $vcustom->push(new CustomProperty($cp[0], $cp[1]));
             }
             $this->obj->setCustomProperties($vcustom);
         }
+        else {  // load custom properties from XML for caching (#2238)
+            $object['x-custom'] = array();
+            $vcustom = $this->obj->customProperties();
+            for ($i=0; $i < $vcustom->size(); $i++) {
+                $cp = $vcustom->get($i);
+                $object['x-custom'][] = array($cp->identifier, $cp->value);
+            }
+        }
     }
 
     /**
diff --git a/lib/plugins/libkolab/lib/kolab_format_contact.php b/lib/plugins/libkolab/lib/kolab_format_contact.php
index 72867fc..0d0bc75 100644
--- a/lib/plugins/libkolab/lib/kolab_format_contact.php
+++ b/lib/plugins/libkolab/lib/kolab_format_contact.php
@@ -268,7 +268,7 @@ class kolab_format_contact extends kolab_format
      */
     public function is_valid()
     {
-        return $this->data || (is_object($this->obj) && $this->obj->uid() /*$this->obj->isValid()*/);
+        return !$this->formaterror && ($this->data || (is_object($this->obj) && $this->obj->uid() /*$this->obj->isValid()*/));
     }
 
     /**
diff --git a/lib/plugins/libkolab/lib/kolab_format_distributionlist.php b/lib/plugins/libkolab/lib/kolab_format_distributionlist.php
index 304cdc4..46dda01 100644
--- a/lib/plugins/libkolab/lib/kolab_format_distributionlist.php
+++ b/lib/plugins/libkolab/lib/kolab_format_distributionlist.php
@@ -69,7 +69,7 @@ class kolab_format_distributionlist extends kolab_format
 
     public function is_valid()
     {
-        return $this->data || (is_object($this->obj) && $this->obj->isValid());
+        return !$this->formaterror && ($this->data || (is_object($this->obj) && $this->obj->isValid()));
     }
 
     /**
diff --git a/lib/plugins/libkolab/lib/kolab_format_event.php b/lib/plugins/libkolab/lib/kolab_format_event.php
index f3d0470..9be9bdf 100644
--- a/lib/plugins/libkolab/lib/kolab_format_event.php
+++ b/lib/plugins/libkolab/lib/kolab_format_event.php
@@ -111,7 +111,8 @@ class kolab_format_event extends kolab_format_xcal
      */
     public function is_valid()
     {
-        return $this->data || (is_object($this->obj) && $this->obj->isValid() && $this->obj->uid());
+        return !$this->formaterror && (($this->data && !empty($this->data['start']) && !empty($this->data['end'])) ||
+            (is_object($this->obj) && $this->obj->isValid() && $this->obj->uid()));
     }
 
     /**
@@ -138,6 +139,17 @@ class kolab_format_event extends kolab_format_xcal
             'attendees'   => array(),
         );
 
+        // derive event end from duration (#1916)
+        if (!$object['end'] && $object['start'] && ($duration = $this->obj->duration()) && $duration->isValid()) {
+            $interval = new DateInterval('PT0S');
+            $interval->d = $duration->weeks() * 7 + $duration->days();
+            $interval->h = $duration->hours();
+            $interval->i = $duration->minutes();
+            $interval->s = $duration->seconds();
+            $object['end'] = clone $object['start'];
+            $object['end']->add($interval);
+        }
+
         // organizer is part of the attendees list in Roundcube
         if ($object['organizer']) {
             $object['organizer']['role'] = 'ORGANIZER';
diff --git a/lib/plugins/libkolab/lib/kolab_format_file.php b/lib/plugins/libkolab/lib/kolab_format_file.php
index f5b153b..5f73bf1 100644
--- a/lib/plugins/libkolab/lib/kolab_format_file.php
+++ b/lib/plugins/libkolab/lib/kolab_format_file.php
@@ -95,7 +95,7 @@ class kolab_format_file extends kolab_format
      */
     public function is_valid()
     {
-        return $this->data || (is_object($this->obj) && $this->obj->isValid());
+        return !$this->formaterror && ($this->data || (is_object($this->obj) && $this->obj->isValid()));
     }
 
     /**
diff --git a/lib/plugins/libkolab/lib/kolab_format_journal.php b/lib/plugins/libkolab/lib/kolab_format_journal.php
index b9a1b4f..f7ccd31 100644
--- a/lib/plugins/libkolab/lib/kolab_format_journal.php
+++ b/lib/plugins/libkolab/lib/kolab_format_journal.php
@@ -54,7 +54,7 @@ class kolab_format_journal extends kolab_format
      */
     public function is_valid()
     {
-        return $this->data || (is_object($this->obj) && $this->obj->isValid());
+        return !$this->formaterror && ($this->data || (is_object($this->obj) && $this->obj->isValid()));
     }
 
     /**
diff --git a/lib/plugins/libkolab/lib/kolab_format_note.php b/lib/plugins/libkolab/lib/kolab_format_note.php
index 466c536..04a8421 100644
--- a/lib/plugins/libkolab/lib/kolab_format_note.php
+++ b/lib/plugins/libkolab/lib/kolab_format_note.php
@@ -54,7 +54,7 @@ class kolab_format_note extends kolab_format
      */
     public function is_valid()
     {
-        return $this->data || (is_object($this->obj) && $this->obj->isValid());
+        return !$this->formaterror && ($this->data || (is_object($this->obj) && $this->obj->isValid()));
     }
 
     /**
diff --git a/lib/plugins/libkolab/lib/kolab_format_task.php b/lib/plugins/libkolab/lib/kolab_format_task.php
index 56f22dc..a15cb0b 100644
--- a/lib/plugins/libkolab/lib/kolab_format_task.php
+++ b/lib/plugins/libkolab/lib/kolab_format_task.php
@@ -63,7 +63,7 @@ class kolab_format_task extends kolab_format_xcal
      */
     public function is_valid()
     {
-        return $this->data || (is_object($this->obj) && $this->obj->isValid());
+        return !$this->formaterror && ($this->data || (is_object($this->obj) && $this->obj->isValid()));
     }
 
     /**
diff --git a/lib/plugins/libkolab/lib/kolab_format_xcal.php b/lib/plugins/libkolab/lib/kolab_format_xcal.php
index 085e577..284a068 100644
--- a/lib/plugins/libkolab/lib/kolab_format_xcal.php
+++ b/lib/plugins/libkolab/lib/kolab_format_xcal.php
@@ -385,7 +385,7 @@ abstract class kolab_format_xcal extends kolab_format
             if (preg_match('/^@(\d+)/', $offset, $d)) {
                 $alarm->setStart(self::get_datetime($d[1], new DateTimeZone('UTC')));
             }
-            else if (preg_match('/^([-+]?)(\d+)([SMHDW])/', $offset, $d)) {
+            else if (preg_match('/^([-+]?)P?T?(\d+)([SMHDW])/', $offset, $d)) {
                 $days = $hours = $minutes = $seconds = 0;
                 switch ($d[3]) {
                     case 'W': $days  = 7*intval($d[2]); break;
diff --git a/lib/plugins/libkolab/lib/kolab_storage.php b/lib/plugins/libkolab/lib/kolab_storage.php
index ee6ede0..5f8b9c6 100644
--- a/lib/plugins/libkolab/lib/kolab_storage.php
+++ b/lib/plugins/libkolab/lib/kolab_storage.php
@@ -31,6 +31,9 @@ class kolab_storage
     const COLOR_KEY_PRIVATE = '/private/vendor/kolab/color';
     const NAME_KEY_SHARED   = '/shared/vendor/kolab/displayname';
     const NAME_KEY_PRIVATE  = '/private/vendor/kolab/displayname';
+    const UID_KEY_SHARED    = '/shared/vendor/kolab/uniqueid';
+    const UID_KEY_PRIVATE   = '/private/vendor/kolab/uniqueid';
+    const UID_KEY_CYRUS     = '/shared/vendor/cmu/cyrus-imapd/uniqueid';
 
     public static $version = '3.0';
     public static $last_error;
@@ -103,15 +106,16 @@ class kolab_storage
      * Get a list of storage folders for the given data type
      *
      * @param string Data type to list folders for (contact,distribution-list,event,task,note)
+     * @param boolean Enable to return subscribed folders only (null to use configured subscription mode)
      *
      * @return array List of Kolab_Folder objects (folder names in UTF7-IMAP)
      */
-    public static function get_folders($type)
+    public static function get_folders($type, $subscribed = null)
     {
         $folders = $folderdata = array();
 
         if (self::setup()) {
-            foreach ((array)self::list_folders('', '*', $type, null, $folderdata) as $foldername) {
+            foreach ((array)self::list_folders('', '*', $type, $subscribed, $folderdata) as $foldername) {
                 $folders[$foldername] = new kolab_storage_folder($foldername, $folderdata[$foldername]);
             }
         }
@@ -154,7 +158,7 @@ class kolab_storage
      * This will search all folders storing objects of the given type.
      *
      * @param string Object UID
-     * @param string Object type (contact,distribution-list,event,task,note)
+     * @param string Object type (contact,event,task,journal,file,note,configuration)
      * @return array The Kolab object represented as hash array or false if not found
      */
     public static function get_object($uid, $type)
@@ -167,7 +171,7 @@ class kolab_storage
             else
                 $folder->set_folder($foldername);
 
-            if ($object = $folder->get_object($uid))
+            if ($object = $folder->get_object($uid, '*'))
                 return $object;
         }
 
@@ -276,9 +280,22 @@ class kolab_storage
     {
         self::setup();
 
+        $oldfolder = self::get_folder($oldname);
+        $active = self::folder_is_active($oldname);
         $success = self::$imap->rename_folder($oldname, $newname);
         self::$last_error = self::$imap->get_error_str();
 
+        // pass active state to new folder name
+        if ($success && $active) {
+            self::set_state($oldnam, false);
+            self::set_state($newname, true);
+        }
+
+        // assign existing cache entries to new resource uri
+        if ($success && $oldfolder) {
+            $oldfolder->cache->rename($newname);
+        }
+
         return $success;
     }
 
@@ -388,11 +405,8 @@ class kolab_storage
         self::setup();
 
         // find custom display name in folder METADATA
-        if (self::$config->get('kolab_custom_display_names', true)) {
-            $metadata = self::$imap->get_metadata($folder, array(self::NAME_KEY_PRIVATE, self::NAME_KEY_SHARED));
-            if (($name = $metadata[$folder][self::NAME_KEY_PRIVATE]) || ($name = $metadata[$folder][self::NAME_KEY_SHARED])) {
-                return $name;
-            }
+        if ($name = self::custom_displayname($folder)) {
+            return $name;
         }
 
         $found     = false;
@@ -461,6 +475,21 @@ class kolab_storage
         return $folder;
     }
 
+    /**
+     * Get custom display name (saved in metadata) for the given folder
+     */
+    public static function custom_displayname($folder)
+    {
+      // find custom display name in folder METADATA
+      if (self::$config->get('kolab_custom_display_names', true)) {
+          $metadata = self::$imap->get_metadata($folder, array(self::NAME_KEY_PRIVATE, self::NAME_KEY_SHARED));
+          if (($name = $metadata[$folder][self::NAME_KEY_PRIVATE]) || ($name = $metadata[$folder][self::NAME_KEY_SHARED])) {
+              return $name;
+          }
+      }
+
+      return false;
+    }
 
     /**
      * Helper method to generate a truncated folder name to display
@@ -475,7 +504,7 @@ class kolab_storage
                 $length = strlen($names[$i] . ' » ');
                 $prefix = substr($name, 0, $length);
                 $count  = count(explode(' » ', $prefix));
-                $name   = str_repeat('  ', $count-1) . '» ' . substr($name, $length);
+                $name   = str_repeat('   ', $count-1) . '» ' . substr($name, $length);
                 break;
             }
         }
@@ -497,7 +526,7 @@ class kolab_storage
     public static function folder_selector($type, $attrs, $current = '')
     {
         // get all folders of specified type
-        $folders = self::get_folders($type);
+        $folders = self::get_folders($type, false);
 
         $delim = self::$imap->get_hierarchy_delimiter();
         $names = array();
@@ -570,7 +599,7 @@ class kolab_storage
      *
      * @param string  Optional root folder
      * @param string  Optional name pattern
-     * @param string  Data type to list folders for (contact,distribution-list,event,task,note,mail)
+     * @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)
      * @param array   Will be filled with folder-types data
      *
@@ -654,11 +683,12 @@ class kolab_storage
      */
     public static function sort_folders($folders)
     {
+        $pad = '  ';
         $nsnames = array('personal' => array(), 'shared' => array(), 'other' => array());
         foreach ($folders as $folder) {
             $folders[$folder->name] = $folder;
             $ns = $folder->get_namespace();
-            $nsnames[$ns][$folder->name] = strtolower(html_entity_decode(self::object_name($folder->name, $ns), ENT_COMPAT, RCUBE_CHARSET));  // decode »
+            $nsnames[$ns][$folder->name] = strtolower(html_entity_decode(self::object_name($folder->name, $ns), ENT_COMPAT, RCUBE_CHARSET)) . $pad;  // decode »
         }
 
         $names = array();
@@ -963,7 +993,7 @@ class kolab_storage
             }
 
             if (!self::$imap->folder_exists($folder)) {
-                if (!self::$imap->folder_create($folder)) {
+                if (!self::$imap->create_folder($folder)) {
                     return;
                 }
             }
diff --git a/lib/plugins/libkolab/lib/kolab_storage_cache.php b/lib/plugins/libkolab/lib/kolab_storage_cache.php
index ba6c106..a4fd34c 100644
--- a/lib/plugins/libkolab/lib/kolab_storage_cache.php
+++ b/lib/plugins/libkolab/lib/kolab_storage_cache.php
@@ -6,7 +6,7 @@
  * @version @package_version@
  * @author Thomas Bruederli <bruederli at kolabsys.com>
  *
- * Copyright (C) 2012, Kolab Systems AG <contact at kolabsys.com>
+ * Copyright (C) 2012-2013, 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
@@ -24,24 +24,44 @@
 
 class kolab_storage_cache
 {
-    private $db;
-    private $imap;
-    private $folder;
-    private $uid2msg;
-    private $objects;
-    private $index = array();
-    private $resource_uri;
-    private $enabled = true;
-    private $synched = false;
-    private $synclock = false;
-    private $ready = false;
-    private $max_sql_packet;
-    private $max_sync_lock_time = 600;
-    private $binary_items = array(
-        'photo'          => '|<photo><uri>[^;]+;base64,([^<]+)</uri></photo>|i',
-        'pgppublickey'   => '|<key><uri>date:application/pgp-keys;base64,([^<]+)</uri></photo>|i',
-        'pkcs7publickey' => '|<key><uri>date:application/pkcs7-mime;base64,([^<]+)</uri></photo>|i',
-    );
+    protected $db;
+    protected $imap;
+    protected $folder;
+    protected $uid2msg;
+    protected $objects;
+    protected $index = array();
+    protected $metadata = array();
+    protected $folder_id;
+    protected $resource_uri;
+    protected $enabled = true;
+    protected $synched = false;
+    protected $synclock = false;
+    protected $ready = false;
+    protected $cache_table;
+    protected $folders_table;
+    protected $max_sql_packet;
+    protected $max_sync_lock_time = 600;
+    protected $binary_items = array();
+    protected $extra_cols = array();
+
+
+    /**
+     * Factory constructor
+     */
+    public static function factory(kolab_storage_folder $storage_folder)
+    {
+        $subclass = 'kolab_storage_cache_' . $storage_folder->type;
+        if (class_exists($subclass)) {
+            return new $subclass($storage_folder);
+        }
+        else {
+            rcube::raise_error(array(
+                'code' => 900,
+                'type' => 'php',
+                'message' => "No kolab_storage_cache class found for folder of type " . $storage_folder->type
+            ), true);
+        }
+    }
 
 
     /**
@@ -55,6 +75,8 @@ class kolab_storage_cache
         $this->enabled = $rcmail->config->get('kolab_cache', false);
 
         if ($this->enabled) {
+            // always read folder cache and lock state from DB master
+            $this->db->set_table_dsn('kolab_folders', 'w');
             // remove sync-lock on script termination
             $rcmail->add_shutdown_function(array($this, '_sync_unlock'));
         }
@@ -80,9 +102,19 @@ class kolab_storage_cache
 
         // compose fully qualified ressource uri for this instance
         $this->resource_uri = $this->folder->get_resource_uri();
+        $this->folders_table = $this->db->table_name('kolab_folders');
+        $this->cache_table = $this->db->table_name('kolab_cache_' . $this->folder->type);
         $this->ready = $this->enabled;
+        $this->folder_id = null;
     }
 
+    /**
+     * Returns true if this cache supports query by type
+     */
+    public function has_type_col()
+    {
+        return in_array('type', $this->extra_cols);
+    }
 
     /**
      * Synchronize local cache data with remote
@@ -96,52 +128,65 @@ class kolab_storage_cache
         // increase time limit
         @set_time_limit($this->max_sync_lock_time);
 
-        // lock synchronization for this folder or wait if locked
-        $this->_sync_lock();
+        // read cached folder metadata
+        $this->_read_folder_data();
 
-        // synchronize IMAP mailbox cache
-        $this->imap->folder_sync($this->folder->name);
+        // check cache status hash first ($this->metadata is set in _read_folder_data())
+        if ($this->metadata['ctag'] != $this->folder->get_ctag()) {
+            // lock synchronization for this folder or wait if locked
+            $this->_sync_lock();
 
-        // compare IMAP index with object cache index
-        $imap_index = $this->imap->index($this->folder->name);
-        $this->index = $imap_index->get();
+            // disable messages cache if configured to do so
+            $this->bypass(true);
 
-        // determine objects to fetch or to invalidate
-        if ($this->ready) {
-            // read cache index
-            $sql_result = $this->db->query(
-                "SELECT msguid, uid FROM kolab_cache WHERE resource=? AND type<>?",
-                $this->resource_uri,
-                'lock'
-            );
+            // synchronize IMAP mailbox cache
+            $this->imap->folder_sync($this->folder->name);
 
-            $old_index = array();
-            while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
-                $old_index[] = $sql_arr['msguid'];
-                $this->uid2msg[$sql_arr['uid']] = $sql_arr['msguid'];
-            }
+            // compare IMAP index with object cache index
+            $imap_index = $this->imap->index($this->folder->name, null, null, true, true);
+            $this->index = $imap_index->get();
 
-            // fetch new objects from imap
-            foreach (array_diff($this->index, $old_index) as $msguid) {
-                if ($object = $this->folder->read_object($msguid, '*')) {
-                    $this->_extended_insert($msguid, $object);
-                }
-            }
-            $this->_extended_insert(0, null);
-
-            // delete invalid entries from local DB
-            $del_index = array_diff($old_index, $this->index);
-            if (!empty($del_index)) {
-                $quoted_ids = join(',', array_map(array($this->db, 'quote'), $del_index));
-                $this->db->query(
-                    "DELETE FROM kolab_cache WHERE resource=? AND msguid IN ($quoted_ids)",
-                    $this->resource_uri
+            // determine objects to fetch or to invalidate
+            if ($this->ready) {
+                // read cache index
+                $sql_result = $this->db->query(
+                    "SELECT msguid, uid FROM $this->cache_table WHERE folder_id=?",
+                    $this->folder_id
                 );
+
+                $old_index = array();
+                while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
+                    $old_index[] = $sql_arr['msguid'];
+                    $this->uid2msg[$sql_arr['uid']] = $sql_arr['msguid'];
+                }
+
+                // fetch new objects from imap
+                foreach (array_diff($this->index, $old_index) as $msguid) {
+                    if ($object = $this->folder->read_object($msguid, '*')) {
+                        $this->_extended_insert($msguid, $object);
+                    }
+                }
+                $this->_extended_insert(0, null);
+
+                // delete invalid entries from local DB
+                $del_index = array_diff($old_index, $this->index);
+                if (!empty($del_index)) {
+                    $quoted_ids = join(',', array_map(array($this->db, 'quote'), $del_index));
+                    $this->db->query(
+                        "DELETE FROM $this->cache_table WHERE folder_id=? AND msguid IN ($quoted_ids)",
+                        $this->folder_id
+                    );
+                }
+
+                // update ctag value (will be written to database in _sync_unlock())
+                $this->metadata['ctag'] = $this->folder->get_ctag();
             }
-        }
 
-        // remove lock
-        $this->_sync_unlock();
+            $this->bypass(false);
+
+            // remove lock
+            $this->_sync_unlock();
+        }
 
         $this->synched = time();
     }
@@ -165,11 +210,12 @@ class kolab_storage_cache
         // load object if not in memory
         if (!isset($this->objects[$msguid])) {
             if ($this->ready) {
+                $this->_read_folder_data();
+
                 $sql_result = $this->db->query(
-                    "SELECT * FROM kolab_cache ".
-                    "WHERE resource=? AND type=? AND msguid=?",
-                    $this->resource_uri,
-                    $type ?: $this->folder->type,
+                    "SELECT * FROM $this->cache_table ".
+                    "WHERE folder_id=? AND msguid=?",
+                    $this->folder_id,
                     $msguid
                 );
 
@@ -210,8 +256,9 @@ class kolab_storage_cache
 
         // remove old entry
         if ($this->ready) {
-            $this->db->query("DELETE FROM kolab_cache WHERE resource=? AND msguid=? AND type<>?",
-                $this->resource_uri, $msguid, 'lock');
+            $this->_read_folder_data();
+            $this->db->query("DELETE FROM $this->cache_table WHERE folder_id=? AND msguid=?",
+                $this->folder_id, $msguid);
         }
 
         if ($object) {
@@ -235,27 +282,33 @@ class kolab_storage_cache
     {
         // write to cache
         if ($this->ready) {
+            $this->_read_folder_data();
+
             $sql_data = $this->_serialize($object);
-            $objtype = $object['_type'] ? $object['_type'] : $this->folder->type;
 
-            $result = $this->db->query(
-                "INSERT INTO kolab_cache ".
-                " (resource, type, msguid, uid, created, changed, data, xml, dtstart, dtend, tags, words, filename)".
-                " VALUES (?, ?, ?, ?, " . $this->db->now() . ", ?, ?, ?, ?, ?, ?, ?, ?)",
-                $this->resource_uri,
-                $objtype,
+            $extra_cols   = $this->extra_cols ? ', ' . join(', ', $this->extra_cols) : '';
+            $extra_fields = $this->extra_cols ? str_repeat(', ?', count($this->extra_cols)) : '';
+
+            $args = array(
+                "INSERT INTO $this->cache_table ".
+                " (folder_id, msguid, uid, created, changed, data, xml, tags, words $extra_cols)".
+                " VALUES (?, ?, ?, " . $this->db->now() . ", ?, ?, ?, ?, ? $extra_fields)",
+                $this->folder_id,
                 $msguid,
                 $object['uid'],
                 $sql_data['changed'],
                 $sql_data['data'],
                 $sql_data['xml'],
-                $sql_data['dtstart'],
-                $sql_data['dtend'],
                 $sql_data['tags'],
                 $sql_data['words'],
-                $sql_data['filename']
             );
 
+            foreach ($this->extra_cols as $col) {
+                $args[] = $sql_data[$col];
+            }
+
+            $result = call_user_func_array(array($this->db, 'query'), $args);
+
             if (!$this->db->affected_rows($result)) {
                 rcube::raise_error(array(
                     'code' => 900, 'type' => 'php',
@@ -283,14 +336,15 @@ class kolab_storage_cache
 
         // resolve new message UID in target folder
         if ($new_msguid = $target->cache->uid2msguid($uid)) {
+            $this->_read_folder_data();
+
             $this->db->query(
-                "UPDATE kolab_cache SET resource=?, msguid=? ".
-                "WHERE resource=? AND msguid=? AND type<>?",
-                $target->get_resource_uri(),
+                "UPDATE $this->cache_table SET folder_id=?, msguid=? ".
+                "WHERE folder_id=? AND msguid=?",
+                $target->folder_id,
                 $new_msguid,
-                $this->resource_uri,
-                $msguid,
-                'lock'
+                $this->folder_id,
+                $msguid
             );
         }
         else {
@@ -307,15 +361,32 @@ class kolab_storage_cache
      */
     public function purge($type = null)
     {
+        $this->_read_folder_data();
+
         $result = $this->db->query(
-            "DELETE FROM kolab_cache WHERE resource=?".
-            ($type ? ' AND type=?' : ''),
-            $this->resource_uri,
-            $type
+            "DELETE FROM $this->cache_table WHERE folder_id=?".
+            $this->folder_id
         );
         return $this->db->affected_rows($result);
     }
 
+    /**
+     * Update resource URI for existing cache entries
+     *
+     * @param string Target IMAP folder to move it to
+     */
+    public function rename($new_folder)
+    {
+        $target = kolab_storage::get_folder($new_folder);
+
+        // resolve new message UID in target folder
+        $this->db->query(
+            "UPDATE $this->folders_table SET resource=? ".
+            "WHERE resource=?",
+            $target->get_resource_uri(),
+            $this->resource_uri
+        );
+    }
 
     /**
      * Select Kolab objects filtered by the given query
@@ -331,10 +402,12 @@ class kolab_storage_cache
 
         // read from local cache DB (assume it to be synchronized)
         if ($this->ready) {
+            $this->_read_folder_data();
+
             $sql_result = $this->db->query(
-                "SELECT " . ($uids ? 'msguid, uid' : '*') . " FROM kolab_cache ".
-                "WHERE resource=? " . $this->_sql_where($query),
-                $this->resource_uri
+                "SELECT " . ($uids ? 'msguid, uid' : '*') . " FROM $this->cache_table ".
+                "WHERE folder_id=? " . $this->_sql_where($query),
+                $this->folder_id
             );
 
             while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
@@ -391,10 +464,12 @@ class kolab_storage_cache
 
         // cache is in sync, we can count records in local DB
         if ($this->synched) {
+            $this->_read_folder_data();
+
             $sql_result = $this->db->query(
-                "SELECT COUNT(*) AS numrows FROM kolab_cache ".
-                "WHERE resource=? " . $this->_sql_where($query),
-                $this->resource_uri
+                "SELECT COUNT(*) AS numrows FROM $this->cache_table ".
+                "WHERE folder_id=? " . $this->_sql_where($query),
+                $this->folder_id
             );
 
             $sql_arr = $this->db->fetch_assoc($sql_result);
@@ -415,10 +490,10 @@ class kolab_storage_cache
     /**
      * Helper method to compose a valid SQL query from pseudo filter triplets
      */
-    private function _sql_where($query)
+    protected function _sql_where($query)
     {
         $sql_where = '';
-        foreach ($query as $param) {
+        foreach ((array) $query as $param) {
             if (is_array($param[0])) {
                 $subq = array();
                 foreach ($param[0] as $q) {
@@ -460,7 +535,7 @@ class kolab_storage_cache
      * Helper method to convert the given pseudo-query triplets into
      * an associative filter array with 'equals' values only
      */
-    private function _query2assoc($query)
+    protected function _query2assoc($query)
     {
         // extract object type from query parameter
         $filter = array();
@@ -479,7 +554,7 @@ class kolab_storage_cache
      * @param string IMAP folder to read from
      * @return array List of parsed Kolab objects
      */
-    private function _fetch($index, $type = null, $folder = null)
+    protected function _fetch($index, $type = null, $folder = null)
     {
         $results = array();
         foreach ((array)$index as $msguid) {
@@ -501,13 +576,19 @@ class kolab_storage_cache
      * @param string IMAP folder to read from
      * @return array List of parsed Kolab objects
      */
-    private function _fetch_uids($index, $type = null)
+    protected function _fetch_uids($index, $type = null)
     {
         if (!$type)
             $type = $this->folder->type;
 
+        $this->bypass(true);
+
         $results = array();
-        foreach ((array)$this->imap->fetch_headers($this->folder->name, $index, false) as $msguid => $headers) {
+        $headers = $this->imap->fetch_headers($this->folder->name, $index, false);
+
+        $this->bypass(false);
+
+        foreach ((array)$headers as $msguid => $headers) {
             $object_type = kolab_format::mime2object_type($headers->others['x-kolab-type']);
 
             // check object type header and abort on mismatch
@@ -526,35 +607,9 @@ class kolab_storage_cache
     /**
      * Helper method to convert the given Kolab object into a dataset to be written to cache
      */
-    private function _serialize($object)
+    protected function _serialize($object)
     {
-        $sql_data = array('changed' => null, 'dtstart' => null, 'dtend' => null, 'xml' => '', 'tags' => '', 'words' => '');
-        $objtype  = $object['_type'] ? $object['_type'] : $this->folder->type;
-
-        // set type specific values
-        if ($objtype == 'event') {
-            // database runs in server's timezone so using date() is what we want
-            $sql_data['dtstart'] = date('Y-m-d H:i:s', is_object($object['start']) ? $object['start']->format('U') : $object['start']);
-            $sql_data['dtend']   = date('Y-m-d H:i:s', is_object($object['end'])   ? $object['end']->format('U')   : $object['end']);
-
-            // extend date range for recurring events
-            if ($object['recurrence'] && $object['_formatobj']) {
-                $recurrence = new kolab_date_recurrence($object['_formatobj']);
-                $sql_data['dtend'] = date('Y-m-d 23:59:59', $recurrence->end() ?: strtotime('now +1 year'));
-            }
-        }
-        else if ($objtype == 'task') {
-            if ($object['start'])
-                $sql_data['dtstart'] = date('Y-m-d H:i:s', is_object($object['start']) ? $object['start']->format('U') : $object['start']);
-            if ($object['due'])
-                $sql_data['dtend']   = date('Y-m-d H:i:s', is_object($object['due'])   ? $object['due']->format('U')   : $object['due']);
-        }
-        else if ($objtype == 'file') {
-            if (!empty($object['_attachments'])) {
-                reset($object['_attachments']);
-                $sql_data['filename'] = $object['_attachments'][key($object['_attachments'])]['name'];
-            }
-        }
+        $sql_data = array('changed' => null, 'xml' => '', 'tags' => '', 'words' => '');
 
         if ($object['changed']) {
             $sql_data['changed'] = date('Y-m-d H:i:s', is_object($object['changed']) ? $object['changed']->format('U') : $object['changed']);
@@ -596,7 +651,7 @@ class kolab_storage_cache
     /**
      * Helper method to turn stored cache data into a valid storage object
      */
-    private function _unserialize($sql_arr)
+    protected function _unserialize($sql_arr)
     {
         $object = unserialize($sql_arr['data']);
 
@@ -608,11 +663,11 @@ class kolab_storage_cache
         }
 
         // add meta data
-        $object['_type'] = $sql_arr['type'];
+        $object['_type'] = $sql_arr['type'] ?: $this->folder->type;
         $object['_msguid'] = $sql_arr['msguid'];
         $object['_mailbox'] = $this->folder->name;
         $object['_size'] = strlen($sql_arr['xml']);
-        $object['_formatobj'] = kolab_format::factory($sql_arr['type'], 3.0, $sql_arr['xml']);
+        $object['_formatobj'] = kolab_format::factory($object['_type'], 3.0, $sql_arr['xml']);
 
         return $object;
     }
@@ -623,37 +678,35 @@ class kolab_storage_cache
      * @param int  Message UID. Set 0 to commit buffered inserts
      * @param array Kolab object to cache
      */
-    private function _extended_insert($msguid, $object)
+    protected function _extended_insert($msguid, $object)
     {
         static $buffer = '';
 
         $line = '';
         if ($object) {
             $sql_data = $this->_serialize($object);
-            $objtype = $object['_type'] ? $object['_type'] : $this->folder->type;
-
             $values = array(
-                $this->db->quote($this->resource_uri),
-                $this->db->quote($objtype),
+                $this->db->quote($this->folder_id),
                 $this->db->quote($msguid),
                 $this->db->quote($object['uid']),
                 $this->db->now(),
                 $this->db->quote($sql_data['changed']),
                 $this->db->quote($sql_data['data']),
                 $this->db->quote($sql_data['xml']),
-                $this->db->quote($sql_data['dtstart']),
-                $this->db->quote($sql_data['dtend']),
                 $this->db->quote($sql_data['tags']),
                 $this->db->quote($sql_data['words']),
-                $this->db->quote($sql_data['filename']),
             );
+            foreach ($this->extra_cols as $col) {
+                $values[] = $this->db->quote($sql_data[$col]);
+            }
             $line = '(' . join(',', $values) . ')';
         }
 
         if ($buffer && (!$msguid || (strlen($buffer) + strlen($line) > $this->max_sql_packet()))) {
+            $extra_cols = $this->extra_cols ? ', ' . join(', ', $this->extra_cols) : '';
             $result = $this->db->query(
-                "INSERT INTO kolab_cache ".
-                " (resource, type, msguid, uid, created, changed, data, xml, dtstart, dtend, tags, words, filename)".
+                "INSERT INTO $this->cache_table ".
+                " (folder_id, msguid, uid, created, changed, data, xml, tags, words $extra_cols)".
                 " VALUES $buffer"
             );
             if (!$this->db->affected_rows($result)) {
@@ -672,7 +725,7 @@ class kolab_storage_cache
     /**
      * Returns max_allowed_packet from mysql config
      */
-    private function max_sql_packet()
+    protected function max_sql_packet()
     {
         if (!$this->max_sql_packet) {
             // mysql limit or max 4 MB
@@ -684,16 +737,36 @@ class kolab_storage_cache
     }
 
     /**
+     * Read this folder's ID and cache metadata
+     */
+    protected function _read_folder_data()
+    {
+        // already done
+        if (!empty($this->folder_id))
+            return;
+
+        $sql_arr = $this->db->fetch_assoc($this->db->query("SELECT folder_id, synclock, ctag FROM $this->folders_table WHERE resource=?", $this->resource_uri));
+        if ($sql_arr) {
+            $this->metadata = $sql_arr;
+            $this->folder_id = $sql_arr['folder_id'];
+        }
+        else {
+            $this->db->query("INSERT INTO $this->folders_table (resource, type) VALUES (?, ?)", $this->resource_uri, $this->folder->type);
+            $this->folder_id = $this->db->insert_id('kolab_folders');
+            $this->metadata = array();
+        }
+    }
+
+    /**
      * Check lock record for this folder and wait if locked or set lock
      */
-    private function _sync_lock()
+    protected function _sync_lock()
     {
         if (!$this->ready)
             return;
 
-        $sql_query = "SELECT msguid AS locked, ".$this->db->unixtimestamp('created')." AS created FROM kolab_cache ".
-            "WHERE resource=? AND type=?";
-        $sql_arr = $this->db->fetch_assoc($this->db->query($sql_query, $this->resource_uri, 'lock'));
+        $this->_read_folder_data();
+        $sql_query = "SELECT synclock, ctag FROM $this->folders_table WHERE folder_id=?";
 
         // abort if database is not set-up
         if ($this->db->is_error()) {
@@ -704,28 +777,13 @@ class kolab_storage_cache
         $this->synclock = true;
 
         // wait if locked (expire locks after 10 minutes)
-        while ($sql_arr && intval($sql_arr['locked']) > 0 && $sql_arr['created'] + $this->max_sync_lock_time > time()) {
+        while ($this->metadata && intval($this->metadata['synclock']) > 0 && $this->metadata['synclock'] + $this->max_sync_lock_time > time()) {
             usleep(500000);
-            $sql_arr = $this->db->fetch_assoc($this->db->query($sql_query, $this->resource_uri, 'lock'));
+            $this->metadata = $this->db->fetch_assoc($this->db->query($sql_query, $this->folder_id));
         }
 
-        // create lock record if not exists
-        if (!$sql_arr) {
-            $this->db->query(
-                "INSERT INTO kolab_cache (resource, type, msguid, created, uid, data, xml, tags, words)".
-                " VALUES (?, ?, 1, " . $this->db->now() . ", '', '', '', '', '')",
-                $this->resource_uri,
-                'lock'
-            );
-        }
-        else {
-            $this->db->query(
-                "UPDATE kolab_cache SET msguid = 1, created = " . $this->db->now() .
-                " WHERE resource = ? AND type = ?",
-                $this->resource_uri,
-                'lock'
-            );
-        }
+        // set lock
+        $this->db->query("UPDATE $this->folders_table SET synclock = ? WHERE folder_id = ?", time(), $this->folder_id);
     }
 
     /**
@@ -737,9 +795,9 @@ class kolab_storage_cache
             return;
 
         $this->db->query(
-            "UPDATE kolab_cache SET msguid = 0 WHERE resource = ? AND type = ?",
-            $this->resource_uri,
-            'lock'
+            "UPDATE $this->folders_table SET synclock = 0, ctag = ? WHERE folder_id = ?",
+            $this->metadata['ctag'],
+            $this->folder_id
         );
 
         $this->synclock = false;
@@ -765,4 +823,72 @@ class kolab_storage_cache
         return $this->uid2msg[$uid];
     }
 
+    /**
+     * Getter for protected member variables
+     */
+    public function __get($name)
+    {
+        if ($name == 'folder_id') {
+            $this->_read_folder_data();
+        }
+
+        return $this->$name;
+    }
+
+    /**
+     * Bypass Roundcube messages cache.
+     * Roundcube cache duplicates information already stored in kolab_cache.
+     *
+     * @param bool $disable True disables, False enables messages cache
+     */
+    public function bypass($disable = false)
+    {
+        // if kolab cache is disabled do nothing
+        if (!$this->enabled) {
+            return;
+        }
+
+        static $messages_cache, $cache_bypass;
+
+        if ($messages_cache === null) {
+            $rcmail = rcube::get_instance();
+            $messages_cache = (bool) $rcmail->config->get('messages_cache');
+            $cache_bypass   = (int) $rcmail->config->get('kolab_messages_cache_bypass');
+        }
+
+        if ($messages_cache) {
+            // handle recurrent (multilevel) bypass() calls
+            if ($disable) {
+                $this->cache_bypassed += 1;
+                if ($this->cache_bypassed > 1) {
+                    return;
+                }
+            }
+            else {
+                $this->cache_bypassed -= 1;
+                if ($this->cache_bypassed > 0) {
+                    return;
+                }
+            }
+
+            switch ($cache_bypass) {
+                case 2:
+                    // Disable messages cache completely
+                    $this->imap->set_messages_caching(!$disable);
+                    break;
+
+                case 1:
+                    // We'll disable messages cache, but keep index cache.
+                    // Default mode is both (MODE_INDEX | MODE_MESSAGE)
+                    $mode = rcube_imap_cache::MODE_INDEX;
+
+                    if (!$disable) {
+                        $mode |= rcube_imap_cache::MODE_MESSAGE;
+                    }
+
+                    $this->imap->set_messages_caching(true, $mode);
+            }
+        }
+    }
+
 }
diff --git a/lib/plugins/libkolab/lib/kolab_storage_folder.php b/lib/plugins/libkolab/lib/kolab_storage_folder.php
index 303ed99..80f13fc 100644
--- a/lib/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/lib/plugins/libkolab/lib/kolab_storage_folder.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-2013, 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
@@ -43,8 +43,8 @@ class kolab_storage_folder
     public $default = false;
 
     /**
-     * Is this folder set to be default
-     * @var boolean
+     * The kolab_storage_cache instance for caching operations
+     * @var object
      */
     public $cache;
 
@@ -64,7 +64,6 @@ class kolab_storage_folder
     {
         $this->imap = rcube::get_instance()->get_storage();
         $this->imap->set_options(array('skip_deleted' => true));
-        $this->cache = new kolab_storage_cache($this);
         $this->set_folder($name, $type);
     }
 
@@ -79,11 +78,16 @@ class kolab_storage_folder
     {
         $this->type_annotation = $ftype ? $ftype : kolab_storage::folder_type($name);
 
+        $oldtype = $this->type;
         list($this->type, $suffix) = explode('.', $this->type_annotation);
         $this->default      = $suffix == 'default';
         $this->name         = $name;
         $this->resource_uri = null;
 
+        // get a new cache instance of folder type changed
+        if (!$this->cache || $type != $oldtype)
+            $this->cache = kolab_storage_cache::factory($this);
+
         $this->imap->set_folder($this->name);
         $this->cache->set_folder($this);
     }
@@ -92,7 +96,7 @@ class kolab_storage_folder
     /**
      *
      */
-    private function get_folder_info()
+    public function get_folder_info()
     {
         if (!isset($this->info))
             $this->info = $this->imap->folder_info($this->name);
@@ -260,6 +264,52 @@ class kolab_storage_folder
     }
 
     /**
+     * Helper method to extract folder UID metadata
+     *
+     * @return string Folder's UID
+     */
+    public function get_uid()
+    {
+        // UID is defined in folder METADATA
+        $metakeys = array(kolab_storage::UID_KEY_SHARED, kolab_storage::UID_KEY_PRIVATE, kolab_storage::UID_KEY_CYRUS);
+        $metadata = $this->get_metadata($metakeys);
+        foreach ($metakeys as $key) {
+            if (($uid = $metadata[$key])) {
+                return $uid;
+            }
+        }
+
+        // generate a folder UID and set it to IMAP
+        $uid = rtrim(chunk_split(md5($this->name . $this->get_owner()), 12, '-'), '-');
+        $this->set_uid($uid);
+
+        return $uid;
+    }
+
+    /**
+     * Helper method to set an UID value to the given IMAP folder instance
+     *
+     * @param string Folder's UID
+     * @return boolean True on succes, False on failure
+     */
+    public function set_uid($uid)
+    {
+        if (!($success = $this->set_metadata(array(kolab_storage::UID_KEY_SHARED => $uid)))) {
+            $success = $this->set_metadata(array(kolab_storage::UID_KEY_PRIVATE => $uid));
+        }
+        return $success;
+    }
+
+    /**
+     * Compose a folder Etag identifier
+     */
+    public function get_ctag()
+    {
+        $fdata = $this->get_imap_data();
+        return sprintf('%d-%d-%d', $fdata['UIDVALIDITY'], $fdata['HIGHESTMODSEQ'], $fdata['UIDNEXT']);
+    }
+
+    /**
      * Check activation status of this folder
      *
      * @return boolean True if enabled, false if not
@@ -303,7 +353,6 @@ class kolab_storage_folder
         return $subscribed ? kolab_storage::folder_subscribe($this->name) : kolab_storage::folder_unsubscribe($this->name);
     }
 
-
     /**
      * Get number of objects stored in this folder
      *
@@ -312,19 +361,12 @@ class kolab_storage_folder
      * @return integer The number of objects of the given type
      * @see self::select()
      */
-    public function count($type_or_query = null)
+    public function count($query = null)
     {
-        if (!$type_or_query)
-            $query = array(array('type','=',$this->type));
-        else if (is_string($type_or_query))
-            $query = array(array('type','=',$type_or_query));
-        else
-            $query = $this->_prepare_query((array)$type_or_query);
-
         // synchronize cache first
         $this->cache->synchronize();
 
-        return $this->cache->count($query);
+        return $this->cache->count($this->_prepare_query($query));
     }
 
 
@@ -342,7 +384,7 @@ class kolab_storage_folder
         $this->cache->synchronize();
 
         // fetch objects from cache
-        return $this->cache->select(array(array('type','=',$type)));
+        return $this->cache->select($this->_prepare_query($type));
     }
 
 
@@ -388,10 +430,15 @@ class kolab_storage_folder
      */
     private function _prepare_query($query)
     {
-        $type = null;
-        foreach ($query as $i => $param) {
-            if ($param[0] == 'type') {
-                $type = $param[2];
+        // string equals type query
+        // FIXME: should not be called this way!
+        if (is_string($query)) {
+            return $this->cache->has_type_col() && !empty($query) ? array(array('type','=',$query)) : array();
+        }
+
+        foreach ((array)$query as $i => $param) {
+            if ($param[0] == 'type' && !$this->cache->has_type_col()) {
+                unset($query[$i]);
             }
             else if (($param[0] == 'dtstart' || $param[0] == 'dtend' || $param[0] == 'changed')) {
                 if (is_object($param[2]) && is_a($param[2], 'DateTime'))
@@ -401,10 +448,6 @@ class kolab_storage_folder
             }
         }
 
-        // add type selector if not in $query
-        if (!$type)
-            $query[] = array('type','=',$this->type);
-
         return $query;
     }
 
@@ -465,6 +508,7 @@ class kolab_storage_folder
      * @param string The IMAP message UID to fetch
      * @param string The object type expected (use wildcard '*' to accept all types)
      * @param string The folder name where the message is stored
+     *
      * @return mixed Hash array representing the Kolab object, a kolab_format instance or false if not found
      */
     public function read_object($msguid, $type = null, $folder = null)
@@ -474,31 +518,31 @@ class kolab_storage_folder
 
         $this->imap->set_folder($folder);
 
-        $headers = $this->imap->get_message_headers($msguid);
-        $message = null;
+        $this->cache->bypass(true);
+        $message = new rcube_message($msguid);
+        $this->cache->bypass(false);
 
         // Message doesn't exist?
-        if (empty($headers)) {
+        if (empty($message->headers)) {
             return false;
         }
 
         // extract the X-Kolab-Type header from the XML attachment part if missing
-        if (empty($headers->others['x-kolab-type'])) {
-            $message = new rcube_message($msguid);
+        if (empty($message->headers->others['x-kolab-type'])) {
             foreach ((array)$message->attachments as $part) {
                 if (strpos($part->mimetype, kolab_format::KTYPE_PREFIX) === 0) {
-                    $headers->others['x-kolab-type'] = $part->mimetype;
+                    $message->headers->others['x-kolab-type'] = $part->mimetype;
                     break;
                 }
             }
         }
         // fix buggy messages stating the X-Kolab-Type header twice
-        else if (is_array($headers->others['x-kolab-type'])) {
-            $headers->others['x-kolab-type'] = reset($headers->others['x-kolab-type']);
+        else if (is_array($message->headers->others['x-kolab-type'])) {
+            $message->headers->others['x-kolab-type'] = reset($message->headers->others['x-kolab-type']);
         }
 
         // no object type header found: abort
-        if (empty($headers->others['x-kolab-type'])) {
+        if (empty($message->headers->others['x-kolab-type'])) {
             rcube::raise_error(array(
                 'code' => 600,
                 'type' => 'php',
@@ -509,14 +553,13 @@ class kolab_storage_folder
             return false;
         }
 
-        $object_type = kolab_format::mime2object_type($headers->others['x-kolab-type']);
-        $content_type  = kolab_format::KTYPE_PREFIX . $object_type;
+        $object_type  = kolab_format::mime2object_type($message->headers->others['x-kolab-type']);
+        $content_type = kolab_format::KTYPE_PREFIX . $object_type;
 
         // check object type header and abort on mismatch
         if ($type != '*' && $object_type != $type)
             return false;
 
-        if (!$message) $message = new rcube_message($msguid);
         $attachments = array();
 
         // get XML part
@@ -558,7 +601,7 @@ class kolab_storage_folder
         }
 
         // check kolab format version
-        $format_version = $headers->others['x-kolab-mime-version'];
+        $format_version = $message->headers->others['x-kolab-mime-version'];
         if (empty($format_version)) {
             list($xmltype, $subtype) = explode('.', $object_type);
             $xmlhead = substr($xml, 0, 512);
@@ -651,8 +694,10 @@ class kolab_storage_folder
             $numatt = count($object['_attachments']);
             foreach ($object['_attachments'] as $key => $attachment) {
                 // FIXME: kolab_storage and Roundcube attachment hooks use different fields!
-                if (empty($attachment['content']) && !empty($attachment['data']))
+                if (empty($attachment['content']) && !empty($attachment['data'])) {
                     $attachment['content'] = $attachment['data'];
+                    unset($attachment['data'], $object['_attachments'][$key]['data']);
+                }
 
                 // make sure size is set, so object saved in cache contains this info
                 if (!isset($attachment['size'])) {
@@ -710,7 +755,9 @@ class kolab_storage_folder
 
             // delete old message
             if ($result && !empty($object['_msguid']) && !empty($object['_mailbox'])) {
+                $this->cache->bypass(true);
                 $this->imap->delete_message($object['_msguid'], $object['_mailbox']);
+                $this->cache->bypass(false);
                 $this->cache->set($object['_msguid'], false, $object['_mailbox']);
             }
 
@@ -805,6 +852,8 @@ class kolab_storage_folder
         $msguid = is_array($object) ? $object['_msguid'] : $this->cache->uid2msguid($object);
         $success = false;
 
+        $this->cache->bypass(true);
+
         if ($msguid && $expunge) {
             $success = $this->imap->delete_message($msguid, $this->name);
         }
@@ -812,6 +861,8 @@ class kolab_storage_folder
             $success = $this->imap->set_flag($msguid, 'DELETED', $this->name);
         }
 
+        $this->cache->bypass(false);
+
         if ($success) {
             $this->cache->set($msguid, false);
         }
@@ -826,7 +877,11 @@ class kolab_storage_folder
     public function delete_all()
     {
         $this->cache->purge();
-        return $this->imap->clear_folder($this->name);
+        $this->cache->bypass(true);
+        $result = $this->imap->clear_folder($this->name);
+        $this->cache->bypass(false);
+
+        return $result;
     }
 
 
@@ -839,7 +894,11 @@ class kolab_storage_folder
     public function undelete($uid)
     {
         if ($msguid = $this->cache->uid2msguid($uid, true)) {
-            if ($this->imap->set_flag($msguid, 'UNDELETED', $this->name)) {
+            $this->cache->bypass(true);
+            $result = $this->imap->set_flag($msguid, 'UNDELETED', $this->name);
+            $this->cache->bypass(false);
+
+            if ($result) {
                 return $msguid;
             }
         }
@@ -858,7 +917,11 @@ class kolab_storage_folder
     public function move($uid, $target_folder)
     {
         if ($msguid = $this->cache->uid2msguid($uid)) {
-            if ($this->imap->move_message($msguid, $target_folder, $this->name)) {
+            $this->cache->bypass(true);
+            $result = $this->imap->move_message($msguid, $target_folder, $this->name);
+            $this->cache->bypass(false);
+
+            if ($result) {
                 $this->cache->move($msguid, $uid, $target_folder);
                 return true;
             }
@@ -1108,9 +1171,7 @@ class kolab_storage_folder
         require_once('HTTP/Request2.php');
 
         try {
-            $rcmail = rcube::get_instance();
-            $request = new HTTP_Request2($url);
-            $request->setConfig(array('ssl_verify_peer' => $rcmail->config->get('kolab_ssl_verify_peer', true)));
+            $request = libkolab::http_request($url);
 
             // set authentication credentials
             if ($auth_user && $auth_passwd)
diff --git a/lib/plugins/libkolab/libkolab.php b/lib/plugins/libkolab/libkolab.php
index b5ff968..48a5033 100644
--- a/lib/plugins/libkolab/libkolab.php
+++ b/lib/plugins/libkolab/libkolab.php
@@ -27,6 +27,8 @@
 
 class libkolab extends rcube_plugin
 {
+    static $http_requests = array();
+
     /**
      * Required startup method of a Roundcube plugin
      */
@@ -59,4 +61,66 @@ class libkolab extends rcube_plugin
         $p['fetch_headers'] = trim($p['fetch_headers'] .' X-KOLAB-TYPE X-KOLAB-MIME-VERSION');
         return $p;
     }
+
+    /**
+     * Wrapper function to load and initalize the HTTP_Request2 Object
+     *
+     * @param string|Net_Url2 Request URL
+     * @param string          Request method ('OPTIONS','GET','HEAD','POST','PUT','DELETE','TRACE','CONNECT')
+     * @param array           Configuration for this Request instance, that will be merged
+     *                        with default configuration
+     *
+     * @return HTTP_Request2 Request object
+     */
+    public static function http_request($url = '', $method = 'GET', $config = array())
+    {
+        $rcube       = rcube::get_instance();
+        $http_config = (array) $rcube->config->get('kolab_http_request');
+
+        // deprecated configuration options
+        if (empty($http_config)) {
+            foreach (array('ssl_verify_peer', 'ssl_verify_host') as $option) {
+                $value = $rcube->config->get('kolab_' . $option, true);
+                if (is_bool($value)) {
+                    $http_config[$option] = $value;
+                }
+            }
+        }
+
+        if (!empty($config)) {
+            $http_config = array_merge($http_config, $config);
+        }
+
+        $key = md5(serialize($http_config));
+
+        if (!($request = self::$http_requests[$key])) {
+            // load HTTP_Request2
+            require_once 'HTTP/Request2.php';
+
+            try {
+                $request = new HTTP_Request2();
+                $request->setConfig($http_config);
+            }
+            catch (Exception $e) {
+                rcube::raise_error($e, true, true);
+            }
+
+            // proxy User-Agent string
+            $request->setHeader('user-agent', $_SERVER['HTTP_USER_AGENT']);
+
+            self::$http_requests[$key] = $request;
+        }
+
+        // cleanup
+        try {
+            $request->setBody('');
+            $request->setUrl($url);
+            $request->setMethod($method);
+        }
+        catch (Exception $e) {
+            rcube::raise_error($e, true, true);
+        }
+
+        return $request;
+    }
 }




More information about the commits mailing list