2 commits - lib/ext lib/kolab_sync_data_calendar.php lib/kolab_sync_data_contacts.php lib/kolab_sync_data_tasks.php lib/plugins

Aleksander Machniak machniak at kolabsys.com
Fri Oct 18 12:07:31 CEST 2013


 lib/ext/Mail/mime.php                                          |  145 +-
 lib/ext/Mail/mimeDecode.php                                    |    4 
 lib/ext/Mail/mimePart.php                                      |   65 -
 lib/ext/Roundcube/bootstrap.php                                |   24 
 lib/ext/Roundcube/rcube.php                                    |   27 
 lib/ext/Roundcube/rcube_addressbook.php                        |   26 
 lib/ext/Roundcube/rcube_base_replacer.php                      |    4 
 lib/ext/Roundcube/rcube_config.php                             |  279 +++-
 lib/ext/Roundcube/rcube_contacts.php                           |    4 
 lib/ext/Roundcube/rcube_csv2vcard.php                          |    2 
 lib/ext/Roundcube/rcube_db.php                                 |  122 +-
 lib/ext/Roundcube/rcube_db_mssql.php                           |    2 
 lib/ext/Roundcube/rcube_db_mysql.php                           |   27 
 lib/ext/Roundcube/rcube_db_pgsql.php                           |    2 
 lib/ext/Roundcube/rcube_db_sqlsrv.php                          |    2 
 lib/ext/Roundcube/rcube_html2text.php                          |    9 
 lib/ext/Roundcube/rcube_imap.php                               |   37 
 lib/ext/Roundcube/rcube_imap_cache.php                         |  166 ++
 lib/ext/Roundcube/rcube_imap_generic.php                       |   52 
 lib/ext/Roundcube/rcube_ldap.php                               |    5 
 lib/ext/Roundcube/rcube_ldap_generic.php                       |    6 
 lib/ext/Roundcube/rcube_message.php                            |   29 
 lib/ext/Roundcube/rcube_mime.php                               |   32 
 lib/ext/Roundcube/rcube_plugin_api.php                         |    2 
 lib/ext/Roundcube/rcube_spellcheck_atd.php                     |  192 +++
 lib/ext/Roundcube/rcube_spellcheck_enchant.php                 |  164 ++
 lib/ext/Roundcube/rcube_spellcheck_engine.php                  |   84 +
 lib/ext/Roundcube/rcube_spellcheck_googie.php                  |  158 ++
 lib/ext/Roundcube/rcube_spellcheck_pspell.php                  |  160 ++
 lib/ext/Roundcube/rcube_spellchecker.php                       |  440 -------
 lib/ext/Roundcube/rcube_storage.php                            |    2 
 lib/ext/Roundcube/rcube_string_replacer.php                    |   41 
 lib/ext/Roundcube/rcube_utils.php                              |   92 +
 lib/ext/Roundcube/rcube_vcard.php                              |    9 
 lib/ext/Roundcube/rcube_washtml.php                            |   11 
 lib/kolab_sync_data_calendar.php                               |    2 
 lib/kolab_sync_data_contacts.php                               |   14 
 lib/kolab_sync_data_tasks.php                                  |    2 
 lib/plugins/kolab_auth/config.inc.php.dist                     |    4 
 lib/plugins/kolab_auth/kolab_auth.php                          |  287 +++--
 lib/plugins/kolab_auth/kolab_auth_ldap.php                     |   18 
 lib/plugins/kolab_auth/localization/de_CH.inc                  |    2 
 lib/plugins/kolab_auth/localization/de_DE.inc                  |    2 
 lib/plugins/kolab_auth/localization/es_ES.inc                  |    3 
 lib/plugins/kolab_auth/localization/et_EE.inc                  |    3 
 lib/plugins/kolab_auth/localization/fr_FR.inc                  |    2 
 lib/plugins/kolab_auth/localization/ja_JP.inc                  |    4 
 lib/plugins/kolab_auth/localization/nl_NL.inc                  |    2 
 lib/plugins/kolab_auth/localization/pl_PL.inc                  |    2 
 lib/plugins/kolab_auth/localization/ru_RU.inc                  |    2 
 lib/plugins/kolab_auth/package.xml                             |    6 
 lib/plugins/kolab_folders/kolab_folders.php                    |   52 
 lib/plugins/kolab_folders/localization/de_CH.inc               |   10 
 lib/plugins/kolab_folders/localization/de_DE.inc               |   14 
 lib/plugins/kolab_folders/localization/es_ES.inc               |   21 
 lib/plugins/kolab_folders/localization/et_EE.inc               |   25 
 lib/plugins/kolab_folders/localization/fr_FR.inc               |    6 
 lib/plugins/kolab_folders/localization/ja_JP.inc               |   40 
 lib/plugins/kolab_folders/localization/nl_NL.inc               |    6 
 lib/plugins/kolab_folders/localization/pl_PL.inc               |   15 
 lib/plugins/kolab_folders/localization/ru_RU.inc               |    6 
 lib/plugins/libkolab/SQL/mysql.initial.sql                     |  158 ++
 lib/plugins/libkolab/SQL/mysql/2013100400.sql                  |  174 +++
 lib/plugins/libkolab/bin/modcache.sh                           |   49 
 lib/plugins/libkolab/config.inc.php.dist                       |   14 
 lib/plugins/libkolab/lib/kolab_format.php                      |   45 
 lib/plugins/libkolab/lib/kolab_format_contact.php              |    2 
 lib/plugins/libkolab/lib/kolab_format_distributionlist.php     |    2 
 lib/plugins/libkolab/lib/kolab_format_event.php                |   14 
 lib/plugins/libkolab/lib/kolab_format_file.php                 |    2 
 lib/plugins/libkolab/lib/kolab_format_journal.php              |    2 
 lib/plugins/libkolab/lib/kolab_format_note.php                 |    2 
 lib/plugins/libkolab/lib/kolab_format_task.php                 |    2 
 lib/plugins/libkolab/lib/kolab_format_xcal.php                 |    4 
 lib/plugins/libkolab/lib/kolab_storage.php                     |   58 -
 lib/plugins/libkolab/lib/kolab_storage_cache.php               |  457 +++++---
 lib/plugins/libkolab/lib/kolab_storage_cache_configuration.php |   40 
 lib/plugins/libkolab/lib/kolab_storage_cache_contact.php       |   45 
 lib/plugins/libkolab/lib/kolab_storage_cache_event.php         |   49 
 lib/plugins/libkolab/lib/kolab_storage_cache_file.php          |   44 
 lib/plugins/libkolab/lib/kolab_storage_cache_freebusy.php      |   27 
 lib/plugins/libkolab/lib/kolab_storage_cache_journal.php       |   28 
 lib/plugins/libkolab/lib/kolab_storage_cache_mongodb.php       |  561 ++++++++++
 lib/plugins/libkolab/lib/kolab_storage_cache_note.php          |   27 
 lib/plugins/libkolab/lib/kolab_storage_cache_task.php          |   44 
 lib/plugins/libkolab/lib/kolab_storage_folder.php              |  149 +-
 lib/plugins/libkolab/libkolab.php                              |   64 +
 87 files changed, 3775 insertions(+), 1264 deletions(-)

New commits:
commit 2081508fb76b4d246ffcf205457996b9f69ba196
Author: Aleksander Machniak <alec at alec.pl>
Date:   Fri Oct 18 12:05:16 2013 +0200

    Specify object type directly in libkolab queries, do not depend
    on kolab_cache behaviour that was changed in latest cache refactoring (Bug #2385)

diff --git a/lib/kolab_sync_data_calendar.php b/lib/kolab_sync_data_calendar.php
index d305dde..fc2da1e 100644
--- a/lib/kolab_sync_data_calendar.php
+++ b/lib/kolab_sync_data_calendar.php
@@ -501,7 +501,7 @@ class kolab_sync_data_calendar extends kolab_sync_data implements Syncroton_Data
      */
     protected function filter($filter_type = 0)
     {
-        $filter = array();
+        $filter = array(array('type', '=', $this->modelName));
 
         switch ($filter_type) {
         case Syncroton_Command_Sync::FILTER_2_WEEKS_BACK:
diff --git a/lib/kolab_sync_data_contacts.php b/lib/kolab_sync_data_contacts.php
index 94dc2f6..7d703ef 100644
--- a/lib/kolab_sync_data_contacts.php
+++ b/lib/kolab_sync_data_contacts.php
@@ -254,4 +254,18 @@ class kolab_sync_data_contacts extends kolab_sync_data
         return $contact;
     }
 
+    /**
+     * Returns filter query array according to specified ActiveSync FilterType
+     *
+     * @param int $filter_type Filter type
+     *
+     * @param array Filter query
+     */
+    protected function filter($filter_type = 0)
+    {
+        // specify object type, contact folders in Kolab might
+        // contain also ditribution-list objects, we'll skip them
+        return array(array('type', '=', $this->modelName));
+    }
+
 }
diff --git a/lib/kolab_sync_data_tasks.php b/lib/kolab_sync_data_tasks.php
index bade8a6..86ee985 100644
--- a/lib/kolab_sync_data_tasks.php
+++ b/lib/kolab_sync_data_tasks.php
@@ -220,7 +220,7 @@ class kolab_sync_data_tasks extends kolab_sync_data
      */
     protected function filter($filter_type = 0)
     {
-        $filter = array();
+        $filter = array(array('type', '=', $this->modelName));
 
         if ($filter_type == Syncroton_Command_Sync::FILTER_INCOMPLETE) {
             $filter[] = array('tags', '!~', 'x-complete');


commit d5e0153b825fc35f9332c74a094a40c311d880c7
Author: Aleksander Machniak <alec at alec.pl>
Date:   Fri Oct 18 11:44:26 2013 +0200

    Rebase Roundcube and kolab plugins

diff --git a/lib/ext/Mail/mime.php b/lib/ext/Mail/mime.php
index c459b91..e079af7 100644
--- a/lib/ext/Mail/mime.php
+++ b/lib/ext/Mail/mime.php
@@ -48,7 +48,7 @@
  * @author    Aleksander Machniak <alec at php.net>
  * @copyright 2003-2006 PEAR <pear-group at php.net>
  * @license   http://www.opensource.org/licenses/bsd-license.php BSD License
- * @version   1.8.5
+ * @version   CVS: $Id$
  * @link      http://pear.php.net/package/Mail_mime
  *
  *            This class is based on HTML Mime Mail class from
@@ -89,7 +89,7 @@ require_once 'Mail/mimePart.php';
  * @author    Sean Coates <sean at php.net>
  * @copyright 2003-2006 PEAR <pear-group at php.net>
  * @license   http://www.opensource.org/licenses/bsd-license.php BSD License
- * @version   Release: 1.8.5
+ * @version   Release: @package_version@
  * @link      http://pear.php.net/package/Mail_mime
  */
 class Mail_mime
@@ -245,7 +245,7 @@ class Mail_mime
             }
         } else {
             $cont = $this->_file2str($data);
-            if (PEAR::isError($cont)) {
+            if ($this->_isError($cont)) {
                 return $cont;
             }
             if (!$append) {
@@ -254,6 +254,7 @@ class Mail_mime
                 $this->_txtbody .= $cont;
             }
         }
+
         return true;
     }
 
@@ -286,7 +287,7 @@ class Mail_mime
             $this->_htmlbody = $data;
         } else {
             $cont = $this->_file2str($data);
-            if (PEAR::isError($cont)) {
+            if ($this->_isError($cont)) {
                 return $cont;
             }
             $this->_htmlbody = $cont;
@@ -336,7 +337,7 @@ class Mail_mime
                 $filedata = null;
                 $bodyfile = $file;
             } else {
-                if (PEAR::isError($filedata = $this->_file2str($file))) {
+                if ($this->_isError($filedata = $this->_file2str($file))) {
                     return $filedata;
                 }
             }
@@ -347,7 +348,7 @@ class Mail_mime
         }
 
         if (!$content_id) {
-            $content_id = md5(uniqid(time()));
+            $content_id = preg_replace('/[^0-9a-zA-Z]/', '', uniqid(time(), true));
         }
 
         $this->_html_images[] = array(
@@ -416,12 +417,12 @@ class Mail_mime
                 $filedata = null;
                 $bodyfile = $file;
             } else {
-                if (PEAR::isError($filedata = $this->_file2str($file))) {
+                if ($this->_isError($filedata = $this->_file2str($file))) {
                     return $filedata;
                 }
             }
             // Force the name the user supplied, otherwise use $file
-            $filename = ($name ? $name : $file);
+            $filename = ($name ? $name : $this->_basename($file));
         } else {
             $filedata = $file;
             $filename = $name;
@@ -429,10 +430,8 @@ class Mail_mime
 
         if (!strlen($filename)) {
             $msg = "The supplied filename for the attachment can't be empty";
-            $err = PEAR::raiseError($msg);
-            return $err;
+            return $this->_raiseError($msg);
         }
-        $filename = $this->_basename($filename);
 
         $this->_parts[] = array(
             'body'        => $filedata,
@@ -462,20 +461,17 @@ class Mail_mime
      * @return string           Contents of $file_name
      * @access private
      */
-    function &_file2str($file_name)
+    function _file2str($file_name)
     {
         // Check state of file and raise an error properly
         if (!file_exists($file_name)) {
-            $err = PEAR::raiseError('File not found: ' . $file_name);
-            return $err;
+            return $this->_raiseError('File not found: ' . $file_name);
         }
         if (!is_file($file_name)) {
-            $err = PEAR::raiseError('Not a regular file: ' . $file_name);
-            return $err;
+            return $this->_raiseError('Not a regular file: ' . $file_name);
         }
         if (!is_readable($file_name)) {
-            $err = PEAR::raiseError('File is not readable: ' . $file_name);
-            return $err;
+            return $this->_raiseError('File is not readable: ' . $file_name);
         }
 
         // Temporarily reset magic_quotes_runtime and read file contents
@@ -501,7 +497,7 @@ class Mail_mime
      * @return object      The text mimePart object
      * @access private
      */
-    function &_addTextPart(&$obj, $text)
+    function &_addTextPart(&$obj = null, $text = '')
     {
         $params['content_type'] = 'text/plain';
         $params['encoding']     = $this->_build_params['text_encoding'];
@@ -510,11 +506,11 @@ class Mail_mime
 
         if (is_object($obj)) {
             $ret = $obj->addSubpart($text, $params);
-            return $ret;
         } else {
             $ret = new Mail_mimePart($text, $params);
-            return $ret;
         }
+
+        return $ret;
     }
 
     /**
@@ -527,7 +523,7 @@ class Mail_mime
      * @return object     The html mimePart object
      * @access private
      */
-    function &_addHtmlPart(&$obj)
+    function &_addHtmlPart(&$obj = null)
     {
         $params['content_type'] = 'text/html';
         $params['encoding']     = $this->_build_params['html_encoding'];
@@ -536,11 +532,11 @@ class Mail_mime
 
         if (is_object($obj)) {
             $ret = $obj->addSubpart($this->_htmlbody, $params);
-            return $ret;
         } else {
             $ret = new Mail_mimePart($this->_htmlbody, $params);
-            return $ret;
         }
+
+        return $ret;
     }
 
     /**
@@ -553,7 +549,6 @@ class Mail_mime
      */
     function &_addMixedPart()
     {
-        $params                 = array();
         $params['content_type'] = 'multipart/mixed';
         $params['eol']          = $this->_build_params['eol'];
 
@@ -573,17 +568,18 @@ class Mail_mime
      * @return object     The multipart/mixed mimePart object
      * @access private
      */
-    function &_addAlternativePart(&$obj)
+    function &_addAlternativePart(&$obj = null)
     {
         $params['content_type'] = 'multipart/alternative';
         $params['eol']          = $this->_build_params['eol'];
 
         if (is_object($obj)) {
-            return $obj->addSubpart('', $params);
+            $ret = $obj->addSubpart('', $params);
         } else {
             $ret = new Mail_mimePart('', $params);
-            return $ret;
         }
+
+        return $ret;
     }
 
     /**
@@ -597,17 +593,18 @@ class Mail_mime
      * @return object     The multipart/mixed mimePart object
      * @access private
      */
-    function &_addRelatedPart(&$obj)
+    function &_addRelatedPart(&$obj = null)
     {
         $params['content_type'] = 'multipart/related';
         $params['eol']          = $this->_build_params['eol'];
 
         if (is_object($obj)) {
-            return $obj->addSubpart('', $params);
+            $ret = $obj->addSubpart('', $params);
         } else {
             $ret = new Mail_mimePart('', $params);
-            return $ret;
         }
+
+        return $ret;
     }
 
     /**
@@ -701,9 +698,9 @@ class Mail_mime
      * 
      * @param string $separation The separation between these two parts.
      * @param array  $params     The Build parameters passed to the
-     *                           &get() function. See &get for more info.
+     *                           get() function. See get() for more info.
      * @param array  $headers    The extra headers that should be passed
-     *                           to the &headers() function.
+     *                           to the headers() method.
      *                           See that function for more info.
      * @param bool   $overwrite  Overwrite the existing headers with new.
      *
@@ -719,13 +716,11 @@ class Mail_mime
 
         $body = $this->get($params);
 
-        if (PEAR::isError($body)) {
+        if ($this->_isError($body)) {
             return $body;
         }
 
-        $head = $this->txtHeaders($headers, $overwrite);
-        $mail = $head . $separation . $body;
-        return $mail;
+        return $this->txtHeaders($headers, $overwrite) . $separation . $body;
     }
 
     /**
@@ -733,7 +728,7 @@ class Mail_mime
      * mail delivery method.
      * 
      * @param array $params The Build parameters passed to the
-     *                      &get() function. See &get for more info.
+     *                      get() method. See get() for more info.
      *
      * @return mixed The e-mail body or PEAR error object
      * @access public
@@ -749,9 +744,9 @@ class Mail_mime
      * 
      * @param string $filename  Output file location
      * @param array  $params    The Build parameters passed to the
-     *                          &get() function. See &get for more info.
+     *                          get() method. See get() for more info.
      * @param array  $headers   The extra headers that should be passed
-     *                          to the &headers() function.
+     *                          to the headers() function.
      *                          See that function for more info.
      * @param bool   $overwrite Overwrite the existing headers with new.
      *
@@ -763,8 +758,7 @@ class Mail_mime
     {
         // Check state of file and raise an error properly
         if (file_exists($filename) && !is_writable($filename)) {
-            $err = PEAR::raiseError('File is not writable: ' . $filename);
-            return $err;
+            return $this->_raiseError('File is not writable: ' . $filename);
         }
 
         // Temporarily reset magic_quotes_runtime and read file contents
@@ -773,15 +767,13 @@ class Mail_mime
         }
 
         if (!($fh = fopen($filename, 'ab'))) {
-            $err = PEAR::raiseError('Unable to open file: ' . $filename);
-            return $err;
+            return $this->_raiseError('Unable to open file: ' . $filename);
         }
 
         // Write message headers into file (skipping Content-* headers)
         $head = $this->txtHeaders($headers, $overwrite, true);
         if (fwrite($fh, $head) === false) {
-            $err = PEAR::raiseError('Error writing to file: ' . $filename);
-            return $err;
+            return $this->_raiseError('Error writing to file: ' . $filename);
         }
 
         fclose($fh);
@@ -798,10 +790,10 @@ class Mail_mime
 
     /**
      * Writes (appends) the complete e-mail body into file.
-     * 
+     *
      * @param string $filename Output file location
      * @param array  $params   The Build parameters passed to the
-     *                         &get() function. See &get for more info.
+     *                         get() method. See get() for more info.
      *
      * @return mixed True or PEAR error object
      * @access public
@@ -811,8 +803,7 @@ class Mail_mime
     {
         // Check state of file and raise an error properly
         if (file_exists($filename) && !is_writable($filename)) {
-            $err = PEAR::raiseError('File is not writable: ' . $filename);
-            return $err;
+            return $this->_raiseError('File is not writable: ' . $filename);
         }
 
         // Temporarily reset magic_quotes_runtime and read file contents
@@ -821,8 +812,7 @@ class Mail_mime
         }
 
         if (!($fh = fopen($filename, 'ab'))) {
-            $err = PEAR::raiseError('Unable to open file: ' . $filename);
-            return $err;
+            return $this->_raiseError('Unable to open file: ' . $filename);
         }
 
         // Write the rest of the message into file
@@ -845,7 +835,7 @@ class Mail_mime
      * @return mixed The MIME message content string, null or PEAR error object
      * @access public
      */
-    function &get($params = null, $filename = null, $skip_head = false)
+    function get($params = null, $filename = null, $skip_head = false)
     {
         if (isset($params)) {
             while (list($key, $value) = each($params)) {
@@ -1005,8 +995,7 @@ class Mail_mime
         }
 
         if (!isset($message)) {
-            $ret = null;
-            return $ret;
+            return null;
         }
 
         // Use saved boundary
@@ -1020,20 +1009,18 @@ class Mail_mime
         if ($filename) {
             // Append mimePart message headers and body into file
             $headers = $message->encodeToFile($filename, $boundary, $skip_head);
-            if (PEAR::isError($headers)) {
+            if ($this->_isError($headers)) {
                 return $headers;
             }
             $this->_headers = array_merge($this->_headers, $headers);
-            $ret = null;
-            return $ret;
+            return null;
         } else {
             $output = $message->encode($boundary, $skip_head);
-            if (PEAR::isError($output)) {
+            if ($this->_isError($output)) {
                 return $output;
             }
             $this->_headers = array_merge($this->_headers, $output['headers']);
-            $body = $output['body'];
-            return $body;
+            return $output['body'];
         }
     }
 
@@ -1051,7 +1038,7 @@ class Mail_mime
      * @return array              Assoc array with the mime headers
      * @access public
      */
-    function &headers($xtra_headers = null, $overwrite = false, $skip_content = false)
+    function headers($xtra_headers = null, $overwrite = false, $skip_content = false)
     {
         // Add mime version header
         $headers['MIME-Version'] = '1.0';
@@ -1090,7 +1077,7 @@ class Mail_mime
 
     /**
      * Get the text version of the headers
-     * (useful if you want to use the PHP mail() function)
+     * (usefull if you want to use the PHP mail() function)
      *
      * @param array $xtra_headers Assoc array with any extra headers (optional)
      *                            (Don't set Content-Type for multipart messages here!)
@@ -1473,4 +1460,36 @@ class Mail_mime
         }
     }
 
+    /**
+     * PEAR::isError implementation
+     *
+     * @param mixed $data Object
+     *
+     * @return bool True if object is an instance of PEAR_Error
+     * @access private
+     */
+    function _isError($data)
+    {
+        // PEAR::isError() is not PHP 5.4 compatible (see Bug #19473)
+        if (is_object($data) && is_a($data, 'PEAR_Error')) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * PEAR::raiseError implementation
+     *
+     * @param $message A text error message
+     *
+     * @return PEAR_Error Instance of PEAR_Error
+     * @access private
+     */
+    function _raiseError($message)
+    {
+        // PEAR::raiseError() is not PHP 5.4 compatible
+        return new PEAR_Error($message);
+    }
+
 } // End of class
diff --git a/lib/ext/Mail/mimeDecode.php b/lib/ext/Mail/mimeDecode.php
index 677d245..9f45894 100644
--- a/lib/ext/Mail/mimeDecode.php
+++ b/lib/ext/Mail/mimeDecode.php
@@ -52,7 +52,7 @@
  * @author     Sean Coates <sean at php.net>
  * @copyright  2003-2006 PEAR <pear-group at php.net>
  * @license    http://www.opensource.org/licenses/bsd-license.php BSD License
- * @version    CVS: $Id$
+ * @version    CVS: $Id: mimeDecode.php 305875 2010-12-01 07:17:10Z alan_k $
  * @link       http://pear.php.net/package/Mail_mime
  */
 
@@ -85,7 +85,7 @@ require_once 'PEAR.php';
  * @author     Sean Coates <sean at php.net>
  * @copyright  2003-2006 PEAR <pear-group at php.net>
  * @license    http://www.opensource.org/licenses/bsd-license.php BSD License
- * @version    Release: @package_version@
+ * @version    Release: 1.5.5
  * @link       http://pear.php.net/package/Mail_mime
  */
 class Mail_mimeDecode extends PEAR
diff --git a/lib/ext/Mail/mimePart.php b/lib/ext/Mail/mimePart.php
index 292227f..c6e9f4a 100644
--- a/lib/ext/Mail/mimePart.php
+++ b/lib/ext/Mail/mimePart.php
@@ -48,7 +48,7 @@
  * @author    Aleksander Machniak <alec at php.net>
  * @copyright 2003-2006 PEAR <pear-group at php.net>
  * @license   http://www.opensource.org/licenses/bsd-license.php BSD License
- * @version   1.8.5
+ * @version   CVS: $Id$
  * @link      http://pear.php.net/package/Mail_mime
  */
 
@@ -70,7 +70,7 @@
  * @author    Aleksander Machniak <alec at php.net>
  * @copyright 2003-2006 PEAR <pear-group at php.net>
  * @license   http://www.opensource.org/licenses/bsd-license.php BSD License
- * @version   Release: 1.8.5
+ * @version   Release: @package_version@
  * @link      http://pear.php.net/package/Mail_mime
  */
 class Mail_mimePart
@@ -315,7 +315,7 @@ class Mail_mimePart
             for ($i = 0; $i < count($this->_subparts); $i++) {
                 $encoded['body'] .= '--' . $boundary . $eol;
                 $tmp = $this->_subparts[$i]->encode();
-                if (PEAR::isError($tmp)) {
+                if ($this->_isError($tmp)) {
                     return $tmp;
                 }
                 foreach ($tmp['headers'] as $key => $value) {
@@ -338,7 +338,7 @@ class Mail_mimePart
                 @ini_set('magic_quotes_runtime', $magic_quote_setting);
             }
 
-            if (PEAR::isError($body)) {
+            if ($this->_isError($body)) {
                 return $body;
             }
             $encoded['body'] = $body;
@@ -368,12 +368,12 @@ class Mail_mimePart
     function encodeToFile($filename, $boundary=null, $skip_head=false)
     {
         if (file_exists($filename) && !is_writable($filename)) {
-            $err = PEAR::raiseError('File is not writeable: ' . $filename);
+            $err = $this->_raiseError('File is not writeable: ' . $filename);
             return $err;
         }
 
         if (!($fh = fopen($filename, 'ab'))) {
-            $err = PEAR::raiseError('Unable to open file: ' . $filename);
+            $err = $this->_raiseError('Unable to open file: ' . $filename);
             return $err;
         }
 
@@ -390,7 +390,7 @@ class Mail_mimePart
             @ini_set('magic_quotes_runtime', $magic_quote_setting);
         }
 
-        return PEAR::isError($res) ? $res : $this->_headers;
+        return $this->_isError($res) ? $res : $this->_headers;
     }
 
     /**
@@ -425,7 +425,7 @@ class Mail_mimePart
             for ($i = 0; $i < count($this->_subparts); $i++) {
                 fwrite($fh, $f_eol . '--' . $boundary . $eol);
                 $res = $this->_subparts[$i]->_encodePartToFile($fh);
-                if (PEAR::isError($res)) {
+                if ($this->_isError($res)) {
                     return $res;
                 }
                 $f_eol = $eol;
@@ -440,7 +440,7 @@ class Mail_mimePart
             $res = $this->_getEncodedDataFromFile(
                 $this->_body_file, $this->_encoding, $fh
             );
-            if (PEAR::isError($res)) {
+            if ($this->_isError($res)) {
                 return $res;
             }
         }
@@ -456,7 +456,7 @@ class Mail_mimePart
      * @param array  $params The parameters for the subpart, same
      *                       as the $params argument for constructor.
      *
-     * @return Mail_mimePart A reference to the part you just added. It is
+     * @return Mail_mimePart A reference to the part you just added. In PHP4, it is
      *                       crucial if using multipart/* in your subparts that
      *                       you use =& in your script when calling this function,
      *                       otherwise you will not be able to add further subparts.
@@ -464,8 +464,8 @@ class Mail_mimePart
      */
     function &addSubpart($body, $params)
     {
-        $this->_subparts[] = new Mail_mimePart($body, $params);
-        return $this->_subparts[count($this->_subparts) - 1];
+        $this->_subparts[] = $part = new Mail_mimePart($body, $params);
+        return $part;
     }
 
     /**
@@ -511,12 +511,12 @@ class Mail_mimePart
     function _getEncodedDataFromFile($filename, $encoding, $fh=null)
     {
         if (!is_readable($filename)) {
-            $err = PEAR::raiseError('Unable to read file: ' . $filename);
+            $err = $this->_raiseError('Unable to read file: ' . $filename);
             return $err;
         }
 
         if (!($fd = fopen($filename, 'rb'))) {
-            $err = PEAR::raiseError('Could not open file: ' . $filename);
+            $err = $this->_raiseError('Could not open file: ' . $filename);
             return $err;
         }
 
@@ -815,6 +815,7 @@ class Mail_mimePart
             'from', 'to', 'cc', 'bcc', 'sender', 'reply-to',
             'resent-from', 'resent-to', 'resent-cc', 'resent-bcc',
             'resent-sender', 'resent-reply-to',
+            'mail-reply-to', 'mail-followup-to',
             'return-receipt-to', 'disposition-notification-to',
         );
         $other_headers = array(
@@ -1012,7 +1013,7 @@ class Mail_mimePart
                 $value = substr($value, $cutpoint);
                 $cutpoint = $maxLength;
                 // RFC 2047 specifies that any split header should
-                // be seperated by a CRLF SPACE.
+                // be separated by a CRLF SPACE.
                 if ($output) {
                     $output .= $eol . ' ';
                 }
@@ -1054,7 +1055,7 @@ class Mail_mimePart
                     }
 
                     // RFC 2047 specifies that any split header should
-                    // be seperated by a CRLF SPACE
+                    // be separated by a CRLF SPACE
                     if ($output) {
                         $output .= $eol . ' ';
                     }
@@ -1225,4 +1226,36 @@ class Mail_mimePart
         return sprintf('%%%02X', ord($matches[1]));
     }
 
+    /**
+     * PEAR::isError implementation
+     *
+     * @param mixed $data Object
+     *
+     * @return bool True if object is an instance of PEAR_Error
+     * @access private
+     */
+    function _isError($data)
+    {
+        // PEAR::isError() is not PHP 5.4 compatible (see Bug #19473)
+        if (is_object($data) && is_a($data, 'PEAR_Error')) {
+            return true;
+        }
+
+        return false;
+    }
+
+    /**
+     * PEAR::raiseError implementation
+     *
+     * @param $message A text error message
+     *
+     * @return PEAR_Error Instance of PEAR_Error
+     * @access private
+     */
+    function _raiseError($message)
+    {
+        // PEAR::raiseError() is not PHP 5.4 compatible
+        return new PEAR_Error($message);
+    }
+
 } // End of class
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/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_spellcheck_atd.php b/lib/ext/Roundcube/rcube_spellcheck_atd.php
new file mode 100644
index 0000000..68e8b7c
--- /dev/null
+++ b/lib/ext/Roundcube/rcube_spellcheck_atd.php
@@ -0,0 +1,192 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | This file is part of the Roundcube Webmail client                     |
+ |                                                                       |
+ | Copyright (C) 2013, The Roundcube Dev Team                            |
+ |                                                                       |
+ | Licensed under the GNU General Public License version 3 or            |
+ | any later version with exceptions for skins & plugins.                |
+ | See the README file for a full license statement.                     |
+ |                                                                       |
+ | PURPOSE:                                                              |
+ |   Spellchecking backend implementation for afterthedeadline services  |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube at gmail.com>                        |
+ +-----------------------------------------------------------------------+
+*/
+
+/**
+ * Spellchecking backend implementation to work with an After the Deadline service
+ * See http://www.afterthedeadline.com/ for more information
+ *
+ * @package    Framework
+ * @subpackage Utils
+ */
+class rcube_spellcheck_atd extends rcube_spellcheck_engine
+{
+    const SERVICE_HOST = 'service.afterthedeadline.com';
+    const SERVICE_PORT = 80;
+
+    private $matches = array();
+    private $content;
+    private $langhosts = array(
+        'fr' => 'fr.',
+        'de' => 'de.',
+        'pt' => 'pt.',
+        'es' => 'es.',
+    );
+
+    /**
+     * Set content and check spelling
+     *
+     * @see rcube_spellcheck_engine::check()
+     */
+    function check($text)
+    {
+        $this->content = $text;
+
+        // spell check uri is configured
+        $rcube = rcube::get_instance();
+        $url = $rcube->config->get('spellcheck_uri');
+        $key = $rcube->config->get('spellcheck_atd_key');
+
+        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::SERVICE_HOST;
+            $port = self::SERVICE_PORT;
+            $path = '/checkDocument';
+
+            // prefix host for other languages than 'en'
+            $lang = substr($this->lang, 0, 2);
+            if ($this->langhosts[$lang])
+                $host = $this->langhosts[$lang] . $host;
+        }
+
+        $postdata = 'data=' . urlencode($text);
+
+        if (!empty($key))
+            $postdata .= '&key=' . urlencode($key);
+
+        $response = $headers = '';
+        $in_header = true;
+        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($postdata) . "\r\n";
+            $out .= "Content-Type: application/x-www-form-urlencoded\r\n";
+            $out .= "Connection: Close\r\n\r\n";
+            $out .= $postdata;
+            fwrite($fp, $out);
+
+            while (!feof($fp)) {
+                if ($in_header) {
+                    $line = fgets($fp, 512);
+                    $headers .= $line;
+                    if (trim($line) == '')
+                        $in_header = false;
+                }
+                else {
+                    $response .= fgets($fp, 1024);
+                }
+            }
+            fclose($fp);
+        }
+
+        // parse HTTP response headers
+        if (preg_match('!^HTTP/1.\d (\d+)(.+)!', $headers, $m)) {
+            $http_status = $m[1];
+            if ($http_status != '200')
+                $this->error = 'HTTP ' . $m[1] . $m[2];
+        }
+
+        if (!$response) {
+            $this->error = "Empty result from spelling engine";
+        }
+
+        try {
+            $result = new SimpleXMLElement($response);
+        }
+        catch (Exception $e) {
+            $thid->error = "Unexpected response from server: " . $store;
+            return array();
+        }
+
+        foreach ($result->error as $error) {
+            if (strval($error->type) == 'spelling') {
+                $word = strval($error->string);
+
+                // skip exceptions
+                if ($this->dictionary->is_exception($word)) {
+                    continue;
+                }
+
+                $prefix = strval($error->precontext);
+                $start = $prefix ? mb_strpos($text, $prefix) : 0;
+                $pos = mb_strpos($text, $word, $start);
+                $len = mb_strlen($word);
+                $num = 0;
+
+                $match = array($word, $pos, $len, null, array());
+                foreach ($error->suggestions->option as $option) {
+                    $match[4][] = strval($option);
+                    if (++$num == self::MAX_SUGGESTIONS)
+                        break;
+                }
+                $matches[] = $match;
+            }
+        }
+
+        $this->matches = $matches;
+        return $matches;
+    }
+
+    /**
+     * Returns suggestions for the specified word
+     *
+     * @see rcube_spellcheck_engine::get_words()
+     */
+    function get_suggestions($word)
+    {
+        $matches = $word ? $this->check($word) : $this->matches;
+
+        if ($matches[0][4]) {
+            return $matches[0][4];
+        }
+
+        return array();
+    }
+
+    /**
+     * Returns misspelled words
+     *
+     * @see rcube_spellcheck_engine::get_suggestions()
+     */
+    function get_words($text = null)
+    {
+        if ($text) {
+            $matches = $this->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;
+    }
+
+}
+
diff --git a/lib/ext/Roundcube/rcube_spellcheck_enchant.php b/lib/ext/Roundcube/rcube_spellcheck_enchant.php
new file mode 100644
index 0000000..a22251e
--- /dev/null
+++ b/lib/ext/Roundcube/rcube_spellcheck_enchant.php
@@ -0,0 +1,164 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | This file is part of the Roundcube Webmail client                     |
+ |                                                                       |
+ | Copyright (C) 2011-2013, Kolab Systems AG                             |
+ | Copyright (C) 20011-2013, The Roundcube Dev Team                      |
+ |                                                                       |
+ | Licensed under the GNU General Public License version 3 or            |
+ | any later version with exceptions for skins & plugins.                |
+ | See the README file for a full license statement.                     |
+ |                                                                       |
+ | PURPOSE:                                                              |
+ |   Spellchecking backend implementation to work with Enchant           |
+ +-----------------------------------------------------------------------+
+ | Author: Aleksander Machniak <machniak at kolabsys.com>                   |
+ +-----------------------------------------------------------------------+
+*/
+
+/**
+ * Spellchecking backend implementation to work with Pspell
+ *
+ * @package    Framework
+ * @subpackage Utils
+ */
+class rcube_spellcheck_enchant extends rcube_spellcheck_engine
+{
+    private $enchant_broker;
+    private $enchant_dictionary;
+    private $matches = array();
+
+    /**
+     * Initializes Enchant dictionary
+     */
+    private function 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);
+    }
+
+    /**
+     * Set content and check spelling
+     *
+     * @see rcube_spellcheck_engine::check()
+     */
+    function check($text)
+    {
+        $this->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->dictionary->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);
+        }
+
+        $this->matches = $matches;
+        return $matches;
+    }
+
+    /**
+     * Returns suggestions for the specified word
+     *
+     * @see rcube_spellcheck_engine::get_words()
+     */
+    function get_suggestions($word)
+    {
+        $this->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();
+    }
+
+    /**
+     * Returns misspelled words
+     *
+     * @see rcube_spellcheck_engine::get_suggestions()
+     */
+    function get_words($text = null)
+    {
+        $result = array();
+
+        if ($text) {
+            // init spellchecker
+            $this->init();
+
+            if (!$this->enchant_dictionary) {
+                return array();
+            }
+
+            // With Enchant we don't need to get suggestions to return misspelled words
+            $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->dictionary->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;
+    }
+
+}
+
diff --git a/lib/ext/Roundcube/rcube_spellcheck_engine.php b/lib/ext/Roundcube/rcube_spellcheck_engine.php
new file mode 100644
index 0000000..88e10ac
--- /dev/null
+++ b/lib/ext/Roundcube/rcube_spellcheck_engine.php
@@ -0,0 +1,84 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | This file is part of the Roundcube Webmail client                     |
+ |                                                                       |
+ | 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.                |
+ | See the README file for a full license statement.                     |
+ |                                                                       |
+ | PURPOSE:                                                              |
+ |   Interface class for a spell-checking backend                        |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube at gmail.com>                        |
+ +-----------------------------------------------------------------------+
+*/
+
+/**
+ * Interface class for a spell-checking backend
+ *
+ * @package    Framework
+ * @subpackage Utils
+ */
+abstract class rcube_spellcheck_engine
+{
+    const MAX_SUGGESTIONS = 10;
+
+    protected $lang;
+    protected $error;
+    protected $dictionary;
+    protected $separator = '/[\s\r\n\t\(\)\/\[\]{}<>\\"]+|[:;?!,\.](?=\W|$)/';
+
+    /**
+     * Default constructor
+     */
+    public function __construct($dict, $lang)
+    {
+        $this->dictionary = $dict;
+        $this->lang = $lang;
+    }
+
+    /**
+     * Set content and check spelling
+     *
+     * @param string $text    Text content for spellchecking
+     *
+     * @return bool True when no mispelling found, otherwise false
+     */
+    abstract function check($text);
+
+    /**
+     * Returns suggestions for the specified word
+     *
+     * @param string $word The word
+     *
+     * @return array Suggestions list
+     */
+    abstract function get_suggestions($word);
+
+    /**
+     * Returns misspelled words
+     *
+     * @param string $text The content for spellchecking. If empty content
+     *                     used for check() method will be used.
+     *
+     * @return array List of misspelled words
+     */
+    abstract function get_words($text = null);
+
+    /**
+     * Returns error message
+     *
+     * @return string Error message
+     */
+    public function error()
+    {
+        return $this->error;
+    }
+
+}
+
diff --git a/lib/ext/Roundcube/rcube_spellcheck_googie.php b/lib/ext/Roundcube/rcube_spellcheck_googie.php
new file mode 100644
index 0000000..70507dc
--- /dev/null
+++ b/lib/ext/Roundcube/rcube_spellcheck_googie.php
@@ -0,0 +1,158 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | This file is part of the Roundcube Webmail client                     |
+ |                                                                       |
+ | 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.                |
+ | See the README file for a full license statement.                     |
+ |                                                                       |
+ | PURPOSE:                                                              |
+ |   Spellchecking backend implementation to work with Googiespell       |
+ +-----------------------------------------------------------------------+
+ | Author: Aleksander Machniak <machniak at kolabsys.com>                   |
+ | Author: Thomas Bruederli <roundcube at gmail.com>                        |
+ +-----------------------------------------------------------------------+
+*/
+
+/**
+ * Spellchecking backend implementation to work with a Googiespell service
+ *
+ * @package    Framework
+ * @subpackage Utils
+ */
+class rcube_spellcheck_googie extends rcube_spellcheck_engine
+{
+    const GOOGLE_HOST = 'ssl://www.google.com';
+    const GOOGLE_PORT = 443;
+
+    private $matches = array();
+    private $content;
+
+    /**
+     * Set content and check spelling
+     *
+     * @see rcube_spellcheck_engine::check()
+     */
+    function check($text)
+    {
+        $this->content = $text;
+
+        // spell check uri is configured
+        $url = rcube::get_instance()->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)
+        foreach ($matches as $idx => $m) {
+            $word = mb_substr($text, $m[1], $m[2], RCUBE_CHARSET);
+            // skip  exceptions
+            if ($this->dictionary->is_exception($word)) {
+                unset($matches[$idx]);
+            }
+        }
+
+        $this->matches = $matches;
+        return $matches;
+    }
+
+    /**
+     * Returns suggestions for the specified word
+     *
+     * @see rcube_spellcheck_engine::get_words()
+     */
+    function get_suggestions($word)
+    {
+        $matches = $word ? $this->check($word) : $this->matches;
+
+        if ($matches[0][4]) {
+            $suggestions = explode("\t", $matches[0][4]);
+            if (sizeof($suggestions) > self::MAX_SUGGESTIONS) {
+                $suggestions = array_slice($suggestions, 0, self::MAX_SUGGESTIONS);
+            }
+
+            return $suggestions;
+        }
+
+        return array();
+    }
+
+    /**
+     * Returns misspelled words
+     *
+     * @see rcube_spellcheck_engine::get_suggestions()
+     */
+    function get_words($text = null)
+    {
+        if ($text) {
+            $matches = $this->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;
+    }
+
+}
+
diff --git a/lib/ext/Roundcube/rcube_spellcheck_pspell.php b/lib/ext/Roundcube/rcube_spellcheck_pspell.php
new file mode 100644
index 0000000..ce089ed
--- /dev/null
+++ b/lib/ext/Roundcube/rcube_spellcheck_pspell.php
@@ -0,0 +1,160 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | This file is part of the Roundcube Webmail client                     |
+ |                                                                       |
+ | 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.                |
+ | See the README file for a full license statement.                     |
+ |                                                                       |
+ | PURPOSE:                                                              |
+ |   Spellchecking backend implementation to work with Pspell            |
+ +-----------------------------------------------------------------------+
+ | Author: Aleksander Machniak <machniak at kolabsys.com>                   |
+ | Author: Thomas Bruederli <roundcube at gmail.com>                        |
+ +-----------------------------------------------------------------------+
+*/
+
+/**
+ * Spellchecking backend implementation to work with Pspell
+ *
+ * @package    Framework
+ * @subpackage Utils
+ */
+class rcube_spellcheck_pspell extends rcube_spellcheck_engine
+{
+    private $plink;
+    private $matches = array();
+
+    /**
+     * Initializes PSpell dictionary
+     */
+    private function 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";
+        }
+    }
+
+    /**
+     * Set content and check spelling
+     *
+     * @see rcube_spellcheck_engine::check()
+     */
+    function check($text)
+    {
+        $this->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->dictionary->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);
+        }
+
+        $this->matches = $matches;
+        return $matches;
+    }
+
+    /**
+     * Returns suggestions for the specified word
+     *
+     * @see rcube_spellcheck_engine::get_words()
+     */
+    function get_suggestions($word)
+    {
+        $this->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();
+    }
+
+    /**
+     * Returns misspelled words
+     *
+     * @see rcube_spellcheck_engine::get_suggestions()
+     */
+    function get_words($text = null)
+    {
+        $result = array();
+
+        if ($text) {
+            // init spellchecker
+            $this->init();
+
+            if (!$this->plink) {
+                return array();
+            }
+
+            // With PSpell we don't need to get suggestions to return misspelled words
+            $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->dictionary->is_exception($word)) {
+                    continue;
+                }
+
+                if (!pspell_check($this->plink, $word)) {
+                    $result[] = $word;
+                }
+            }
+
+            return $result;
+        }
+
+        foreach ($this->matches as $m) {
+            $result[] = $m[0];
+        }
+
+        return $result;
+    }
+
+}
+
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
diff --git a/lib/plugins/kolab_auth/config.inc.php.dist b/lib/plugins/kolab_auth/config.inc.php.dist
index 3f77ea2..e7b9d15 100644
--- a/lib/plugins/kolab_auth/config.inc.php.dist
+++ b/lib/plugins/kolab_auth/config.inc.php.dist
@@ -72,5 +72,9 @@ $rcmail_config['kolab_auth_role_settings'] = Array(
             ),
     );
 
+// List of LDAP addressbooks (keys of ldap_public configuration array)
+// for which base_dn variables (%dc, etc.) will be replaced according to authenticated user DN
+// Note: special name '*' for all LDAP addressbooks
+$rcmail_config['kolab_auth_ldap_addressbooks'] = array('*');
 
 ?>
diff --git a/lib/plugins/kolab_auth/kolab_auth.php b/lib/plugins/kolab_auth/kolab_auth.php
index b139e32..7ff5761 100644
--- a/lib/plugins/kolab_auth/kolab_auth.php
+++ b/lib/plugins/kolab_auth/kolab_auth.php
@@ -12,7 +12,7 @@
  * @version @package_version@
  * @author Aleksander Machniak <machniak at kolabsys.com>
  *
- * Copyright (C) 2011-2012, Kolab Systems AG <contact at kolabsys.com>
+ * Copyright (C) 2011-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
@@ -37,50 +37,95 @@ class kolab_auth extends rcube_plugin
     {
         $rcmail = rcube::get_instance();
 
+        $this->load_config();
+
         $this->add_hook('authenticate', array($this, 'authenticate'));
         $this->add_hook('startup', array($this, 'startup'));
         $this->add_hook('user_create', array($this, 'user_create'));
 
+        // Hook for password change
+        $this->add_hook('password_ldap_bind', array($this, 'password_ldap_bind'));
+
         // Hooks related to "Login As" feature
         $this->add_hook('template_object_loginform', array($this, 'login_form'));
         $this->add_hook('storage_connect', array($this, 'imap_connect'));
         $this->add_hook('managesieve_connect', array($this, 'imap_connect'));
         $this->add_hook('smtp_connect', array($this, 'smtp_connect'));
+        $this->add_hook('identity_form', array($this, 'identity_form'));
+
+        // Hook to modify some configuration, e.g. ldap
+        $this->add_hook('config_get', array($this, 'config_get'));
 
-        $this->add_hook('write_log', array($this, 'write_log'));
+        // Enable debug logs per-user, this enables logging only after
+        // user has logged in
+        if (!empty($_SESSION['username']) && $rcmail->config->get('kolab_auth_auditlog')) {
+            $this->add_hook('write_log', array($this, 'write_log'));
 
-        // TODO: This section does not actually seem to work
-        if ($rcmail->config->get('kolab_auth_auditlog', false)) {
             $rcmail->config->set('debug_level', 1);
             $rcmail->config->set('devel_mode', true);
             $rcmail->config->set('smtp_log', true);
             $rcmail->config->set('log_logins', true);
             $rcmail->config->set('log_session', true);
-            $rcmail->config->set('sql_debug', true);
             $rcmail->config->set('memcache_debug', true);
             $rcmail->config->set('imap_debug', true);
             $rcmail->config->set('ldap_debug', true);
             $rcmail->config->set('smtp_debug', true);
-        }
+            $rcmail->config->set('sql_debug', true);
 
+            // SQL debug need to be set directly on DB object
+            // setting config variable will not work here because
+            // the object is already initialized/configured
+            if ($db = $rcmail->get_dbh()) {
+                $db->set_debug(true);
+            }
+        }
     }
 
     public function startup($args)
     {
-        // Arguments are task / action, not interested
-        if (!empty($_SESSION['user_roledns'])) {
-            $this->load_user_role_plugins_and_settings($_SESSION['user_roledns']);
+        $this->load_user_role_plugins_and_settings();
+
+        return $args;
+    }
+
+    /**
+     * Modify some configuration according to LDAP user record
+     */
+    public function config_get($args)
+    {
+        // Replaces ldap_vars (%dc, etc) in public kolab ldap addressbooks
+        // config based on the users base_dn. (for multi domain support)
+        if ($args['name'] == 'ldap_public' && !empty($args['result'])) {
+            $rcmail      = rcube::get_instance();
+            $kolab_books = (array) $rcmail->config->get('kolab_auth_ldap_addressbooks');
+
+            foreach ($args['result'] as $name => $config) {
+                if (in_array($name, $kolab_books) || in_array('*', $kolab_books)) {
+                    $args['result'][$name]['base_dn']        = self::parse_ldap_vars($config['base_dn']);
+                    $args['result'][$name]['search_base_dn'] = self::parse_ldap_vars($config['search_base_dn']);
+                    $args['result'][$name]['bind_dn']        = str_replace('%dn', $_SESSION['kolab_dn'], $config['bind_dn']);
+
+                    if (!empty($config['groups'])) {
+                        $args['result'][$name]['groups']['base_dn'] = self::parse_ldap_vars($config['groups']['base_dn']);
+                    }
+                }
+            }
         }
 
         return $args;
     }
 
-    public function load_user_role_plugins_and_settings($role_dns)
+    /**
+     * Modifies list of plugins and settings according to
+     * specified LDAP roles
+     */
+    public function load_user_role_plugins_and_settings()
     {
-        $rcmail = rcube::get_instance();
-        $this->load_config();
+        if (empty($_SESSION['user_roledns'])) {
+            return;
+        }
 
-        // Check role dependent plugins to enable and settings to modify
+        $rcmail = rcube::get_instance();
 
         // Example 'kolab_auth_role_plugins' =
         //
@@ -108,25 +153,19 @@ class kolab_auth extends rcube_plugin
 
         $role_settings = $rcmail->config->get('kolab_auth_role_settings');
 
-        $ldap = self::ldap();
-        if (!$ldap || !$ldap->ready) {
-            $args['abort'] = true;
-            return $args;
-        }
-
         if (!empty($role_plugins)) {
             foreach ($role_plugins as $role_dn => $plugins) {
-                $role_plugins[$ldap->parse_vars($role_dn)] = $plugins;
+                $role_plugins[self::parse_ldap_vars($role_dn)] = $plugins;
             }
         }
 
         if (!empty($role_settings)) {
             foreach ($role_settings as $role_dn => $settings) {
-                $role_settings[$ldap->parse_vars($role_dn)] = $settings;
+                $role_settings[self::parse_ldap_vars($role_dn)] = $settings;
             }
         }
 
-        foreach ($role_dns as $role_dn) {
+        foreach ($_SESSION['user_roledns'] as $role_dn) {
             if (isset($role_plugins[$role_dn]) && is_array($role_plugins[$role_dn])) {
                 foreach ($role_plugins[$role_dn] as $plugin) {
                     $this->require_plugin($plugin);
@@ -174,52 +213,48 @@ class kolab_auth extends rcube_plugin
         }
     }
 
+    /**
+     * Logging method replacement to print debug/errors into
+     * a separate (sub)folder for each user
+     */
     public function write_log($args)
     {
         $rcmail = rcube::get_instance();
 
-        if (!$rcmail->config->get('kolab_auth_auditlog', false)) {
-            return $args;
-        }
-
-        $args['abort'] = true;
-
         if ($rcmail->config->get('log_driver') == 'syslog') {
-            $prio = $args['name'] == 'errors' ? LOG_ERR : LOG_INFO;
-            syslog($prio, $args['line']);
             return $args;
         }
-        else {
-            $line = sprintf("[%s]: %s\n", $args['date'], $args['line']);
 
-            // log_driver == 'file' is assumed here
-            $log_dir  = $rcmail->config->get('log_dir', INSTALL_PATH . 'logs');
-            $log_path = $log_dir.'/'.strtolower($_SESSION['kolab_auth_admin']).'/'.strtolower($_SESSION['username']);
+        $line = sprintf("[%s]: %s\n", $args['date'], $args['line']);
 
-            // Append original username + target username
-            if (!is_dir($log_path)) {
-                // Attempt to create the directory
-                if (@mkdir($log_path, 0750, true)) {
-                    $log_dir = $log_path;
-                }
-            }
-            else {
+        // log_driver == 'file' is assumed here
+        $log_dir  = $rcmail->config->get('log_dir', RCUBE_INSTALL_PATH . 'logs');
+        $log_path = $log_dir.'/'.strtolower($_SESSION['kolab_auth_admin']).'/'.strtolower($_SESSION['username']);
+
+        // Append original username + target username
+        if (!is_dir($log_path)) {
+            // Attempt to create the directory
+            if (@mkdir($log_path, 0750, true)) {
                 $log_dir = $log_path;
             }
+        }
+        else {
+            $log_dir = $log_path;
+        }
 
-            // try to open specific log file for writing
-            $logfile = $log_dir.'/'.$args['name'];
+        // try to open specific log file for writing
+        $logfile = $log_dir.'/'.$args['name'];
 
-            if ($fp = fopen($logfile, 'a')) {
-                fwrite($fp, $line);
-                fflush($fp);
-                fclose($fp);
-                return $args;
-            }
-            else {
-                trigger_error("Error writing to log file $logfile; Please check permissions", E_USER_WARNING);
-            }
+        if ($fp = fopen($logfile, 'a')) {
+            fwrite($fp, $line);
+            fflush($fp);
+            fclose($fp);
         }
+        else {
+            trigger_error("Error writing to log file $logfile; Please check permissions", E_USER_WARNING);
+        }
+
+        $args['abort'] = true;
 
         return $args;
     }
@@ -263,7 +298,6 @@ class kolab_auth extends rcube_plugin
      */
     public function login_form($args)
     {
-        $this->load_config();
         $this->add_texts('localization/');
 
         $rcmail      = rcube::get_instance();
@@ -306,6 +340,16 @@ class kolab_auth extends rcube_plugin
         $ldap = self::ldap();
         if (!$ldap || !$ldap->ready) {
             $args['abort'] = true;
+            $message = sprintf(
+                    'Login failure for user %s from %s in session %s (error %s)',
+                    $user,
+                    rcube_utils::remote_ip(),
+                    session_id(),
+                    "LDAP not ready"
+                );
+
+            rcube::write_log('userlogins', $message);
+
             return $args;
         }
 
@@ -314,6 +358,16 @@ class kolab_auth extends rcube_plugin
 
         if (empty($record)) {
             $args['abort'] = true;
+            $message = sprintf(
+                    'Login failure for user %s from %s in session %s (error %s)',
+                    $user,
+                    rcube_utils::remote_ip(),
+                    session_id(),
+                    "No user record found"
+                );
+
+            rcube::write_log('userlogins', $message);
+
             return $args;
         }
 
@@ -325,11 +379,21 @@ class kolab_auth extends rcube_plugin
         $email_attr  = $rcmail->config->get('kolab_auth_email');
         $org_attr    = $rcmail->config->get('kolab_auth_organization');
         $role_attr   = $rcmail->config->get('kolab_auth_role');
+        $imap_attr   = $rcmail->config->get('kolab_auth_mailhost');
 
         if (!empty($role_attr) && !empty($record[$role_attr])) {
             $_SESSION['user_roledns'] = (array)($record[$role_attr]);
         }
 
+        if (!empty($imap_attr) && !empty($record[$imap_attr])) {
+            $default_host = $rcmail->config->get('default_host');
+            if (!empty($default_host)) {
+                rcube::write_log("errors", "Both default host and kolab_auth_mailhost set. Incompatible.");
+            } else {
+                $args['host'] = "tls://" . $record[$imap_attr];
+            }
+        }
+
         // Login As...
         if (!empty($loginas) && $admin_login) {
             // Authenticate to LDAP
@@ -337,6 +401,16 @@ class kolab_auth extends rcube_plugin
 
             if (!$result) {
                 $args['abort'] = true;
+                $message = sprintf(
+                        'Login failure for user %s from %s in session %s (error %s)',
+                        $user,
+                        rcube_utils::remote_ip(),
+                        session_id(),
+                        "Unable to bind with '" . $record['dn'] . "'"
+                    );
+
+                rcube::write_log('userlogins', $message);
+
                 return $args;
             }
 
@@ -378,6 +452,17 @@ class kolab_auth extends rcube_plugin
 
             if (empty($record)) {
                 $args['abort'] = true;
+                $message = sprintf(
+                        'Login failure for user %s (as user %s) from %s in session %s (error %s)',
+                        $user,
+                        $loginas,
+                        rcube_utils::remote_ip(),
+                        session_id(),
+                        "No user record found for '" . $loginas . "'"
+                    );
+
+                rcube::write_log('userlogins', $message);
+
                 return $args;
             }
 
@@ -393,6 +478,12 @@ class kolab_auth extends rcube_plugin
         $_SESSION['kolab_uid'] = is_array($record['uid']) ? $record['uid'][0] : $record['uid'];
         $_SESSION['kolab_dn']  = $record['dn'];
 
+        // Store LDAP replacement variables used for current user
+        // This improves performance of load_user_role_plugins_and_settings()
+        // which is executed on every request (via startup hook) and where
+        // we don't like to use LDAP (connection + bind + search)
+        $_SESSION['kolab_auth_vars'] = $ldap->get_parse_vars();
+
         // Set user login
         if ($login_attr) {
             $this->data['user_login'] = is_array($record[$login_attr]) ? $record[$login_attr][0] : $record[$login_attr];
@@ -435,6 +526,20 @@ class kolab_auth extends rcube_plugin
     }
 
     /**
+     * Set user DN for password change (password plugin with ldap_simple driver)
+     */
+    public function password_ldap_bind($args)
+    {
+        $args['user_dn'] = $_SESSION['kolab_dn'];
+
+        $rcmail = rcube::get_instance();
+
+        $rcmail->config->set('password_ldap_method', 'user');
+
+        return $args;
+    }
+
+    /**
      * Sets SASL Proxy login/password for IMAP and Managesieve auth
      */
     public function imap_connect($args)
@@ -469,6 +574,48 @@ class kolab_auth extends rcube_plugin
     }
 
     /**
+     * Hook to replace the plain text input field for email address by a drop-down list
+     * with all email addresses (including aliases) from this user's LDAP record.
+     */
+    public function identity_form($args)
+    {
+        $rcmail      = rcube::get_instance();
+        $ident_level = intval($rcmail->config->get('identities_level', 0));
+
+        // do nothing if email address modification is disabled
+        if ($ident_level == 1 || $ident_level == 3) {
+            return $args;
+        }
+
+        $ldap = self::ldap();
+        if (!$ldap || !$ldap->ready || empty($_SESSION['kolab_dn'])) {
+            return $args;
+        }
+
+        $emails      = array();
+        $user_record = $ldap->get_record($_SESSION['kolab_dn']);
+
+        foreach ((array)$rcmail->config->get('kolab_auth_email', array()) as $col) {
+            $values = rcube_addressbook::get_col_values($col, $user_record, true);
+            if (!empty($values))
+                $emails = array_merge($emails, array_filter($values));
+        }
+
+        // kolab_delegation might want to modify this addresses list
+        $plugin = $rcmail->plugins->exec_hook('kolab_auth_emails', array('emails' => $emails));
+        $emails = $plugin['emails'];
+
+        if (!empty($emails)) {
+            $args['form']['addressing']['content']['email'] = array(
+                'type' => 'select',
+                'options' => array_combine($emails, $emails),
+            );
+        }
+
+        return $args;
+    }
+
+    /**
      * Initializes LDAP object and connects to LDAP server
      */
     public static function ldap()
@@ -477,18 +624,7 @@ class kolab_auth extends rcube_plugin
             return self::$ldap;
         }
 
-        $rcmail = rcube::get_instance();
-
-        // $this->load_config();
-        // we're in static method, load config manually
-        $fpath = $rcmail->plugins->dir . '/kolab_auth/config.inc.php';
-        if (is_file($fpath) && !$rcmail->config->load_from_file($fpath)) {
-            rcube::raise_error(array(
-                'code' => 527, 'type' => 'php',
-                'file' => __FILE__, 'line' => __LINE__,
-                'message' => "Failed to load config from $fpath"), true, false);
-        }
-
+        $rcmail      = rcube::get_instance();
         $addressbook = $rcmail->config->get('kolab_auth_addressbook');
 
         if (!is_array($addressbook)) {
@@ -506,4 +642,21 @@ class kolab_auth extends rcube_plugin
 
         return self::$ldap;
     }
+
+    /**
+     * Parses LDAP DN string with replacing supported variables.
+     * See kolab_auth_ldap::parse_vars()
+     *
+     * @param string $str LDAP DN string
+     *
+     * @return string Parsed DN string
+     */
+    public static function parse_ldap_vars($str)
+    {
+        if (!empty($_SESSION['kolab_auth_vars'])) {
+            $str = strtr($str, $_SESSION['kolab_auth_vars']);
+        }
+
+        return $str;
+    }
 }
diff --git a/lib/plugins/kolab_auth/kolab_auth_ldap.php b/lib/plugins/kolab_auth/kolab_auth_ldap.php
index 4584c60..b9b3e4a 100644
--- a/lib/plugins/kolab_auth/kolab_auth_ldap.php
+++ b/lib/plugins/kolab_auth/kolab_auth_ldap.php
@@ -287,7 +287,8 @@ class kolab_auth_ldap extends rcube_ldap_generic
                 if ($limit && $limit <= $i) {
                     break;
                 }
-                $dn = $result->get_dn();
+                $dn        = $result->get_dn();
+                $entry     = rcube_ldap_generic::normalize_entry($entry);
                 $list[$dn] = $this->field_mapping($dn, $entry);
                 $i++;
             }
@@ -369,7 +370,8 @@ class kolab_auth_ldap extends rcube_ldap_generic
         if (!$user) {
             $user = $_SESSION['username'];
         }
-        else if (isset($this->icache[$user])) {
+
+        if (isset($this->icache[$user])) {
             list($user, $dc) = $this->icache[$user];
         }
         else {
@@ -412,6 +414,8 @@ class kolab_auth_ldap extends rcube_ldap_generic
 
         $replaces = array('%dc' => $dc, '%d' => $d, '%fu' => $user, '%u' => $u);
 
+        $this->parse_replaces = $replaces;
+
         return strtr($str, $replaces);
     }
 
@@ -459,6 +463,16 @@ class kolab_auth_ldap extends rcube_ldap_generic
     }
 
     /**
+     * Returns variables used for replacement in (last) parse_vars() call
+     *
+     * @return array Variable-value hash array
+     */
+    public function get_parse_vars()
+    {
+        return $this->parse_replaces;
+    }
+
+    /**
      * HTML-safe DN string encoding
      *
      * @param string $str DN string
diff --git a/lib/plugins/kolab_auth/localization/de_CH.inc b/lib/plugins/kolab_auth/localization/de_CH.inc
index 9cdad33..5e85a01 100644
--- a/lib/plugins/kolab_auth/localization/de_CH.inc
+++ b/lib/plugins/kolab_auth/localization/de_CH.inc
@@ -1,5 +1,3 @@
 <?php
-
 $labels['loginas'] = 'Anmelden als';
-
 ?>
diff --git a/lib/plugins/kolab_auth/localization/de_DE.inc b/lib/plugins/kolab_auth/localization/de_DE.inc
index 9cdad33..5e85a01 100644
--- a/lib/plugins/kolab_auth/localization/de_DE.inc
+++ b/lib/plugins/kolab_auth/localization/de_DE.inc
@@ -1,5 +1,3 @@
 <?php
-
 $labels['loginas'] = 'Anmelden als';
-
 ?>
diff --git a/lib/plugins/kolab_auth/localization/es_ES.inc b/lib/plugins/kolab_auth/localization/es_ES.inc
index e1adb3f..acb6c35 100644
--- a/lib/plugins/kolab_auth/localization/es_ES.inc
+++ b/lib/plugins/kolab_auth/localization/es_ES.inc
@@ -1,5 +1,2 @@
 <?php
-
-$labels['loginas'] = 'Login As';
-
 ?>
diff --git a/lib/plugins/kolab_auth/localization/et_EE.inc b/lib/plugins/kolab_auth/localization/et_EE.inc
index e1adb3f..acb6c35 100644
--- a/lib/plugins/kolab_auth/localization/et_EE.inc
+++ b/lib/plugins/kolab_auth/localization/et_EE.inc
@@ -1,5 +1,2 @@
 <?php
-
-$labels['loginas'] = 'Login As';
-
 ?>
diff --git a/lib/plugins/kolab_auth/localization/fr_FR.inc b/lib/plugins/kolab_auth/localization/fr_FR.inc
index a25707f..6f72695 100644
--- a/lib/plugins/kolab_auth/localization/fr_FR.inc
+++ b/lib/plugins/kolab_auth/localization/fr_FR.inc
@@ -1,5 +1,3 @@
 <?php
-
 $labels['loginas'] = 'Se connecter en tant que';
-
 ?>
diff --git a/lib/plugins/kolab_auth/localization/ja_JP.inc b/lib/plugins/kolab_auth/localization/ja_JP.inc
index e1adb3f..ed0358a 100644
--- a/lib/plugins/kolab_auth/localization/ja_JP.inc
+++ b/lib/plugins/kolab_auth/localization/ja_JP.inc
@@ -1,5 +1,3 @@
 <?php
-
-$labels['loginas'] = 'Login As';
-
+$labels['loginas'] = 'ログイン';
 ?>
diff --git a/lib/plugins/kolab_auth/localization/nl_NL.inc b/lib/plugins/kolab_auth/localization/nl_NL.inc
index 935b1cf..a98283f 100644
--- a/lib/plugins/kolab_auth/localization/nl_NL.inc
+++ b/lib/plugins/kolab_auth/localization/nl_NL.inc
@@ -1,5 +1,3 @@
 <?php
-
 $labels['loginas'] = 'Log in als';
-
 ?>
diff --git a/lib/plugins/kolab_auth/localization/pl_PL.inc b/lib/plugins/kolab_auth/localization/pl_PL.inc
index 785f200..124c373 100644
--- a/lib/plugins/kolab_auth/localization/pl_PL.inc
+++ b/lib/plugins/kolab_auth/localization/pl_PL.inc
@@ -1,5 +1,3 @@
 <?php
-
 $labels['loginas'] = 'Zaloguj jako';
-
 ?>
diff --git a/lib/plugins/kolab_auth/localization/ru_RU.inc b/lib/plugins/kolab_auth/localization/ru_RU.inc
index 61ebc59..9e28c12 100644
--- a/lib/plugins/kolab_auth/localization/ru_RU.inc
+++ b/lib/plugins/kolab_auth/localization/ru_RU.inc
@@ -1,5 +1,3 @@
 <?php
-
 $labels['loginas'] = 'Войти как';
-
 ?>
diff --git a/lib/plugins/kolab_auth/package.xml b/lib/plugins/kolab_auth/package.xml
index 00bc969..5a2093b 100644
--- a/lib/plugins/kolab_auth/package.xml
+++ b/lib/plugins/kolab_auth/package.xml
@@ -18,10 +18,10 @@
 		<email>machniak at kolabsys.com</email>
 		<active>yes</active>
 	</lead>
-	<date>2013-06-25</date>
+	<date>2013-10-04</date>
 	<version>
-		<release>0.7</release>
-		<api>0.2</api>
+		<release>1.0</release>
+		<api>1.0</api>
 	</version>
 	<stability>
 		<release>stable</release>
diff --git a/lib/plugins/kolab_folders/kolab_folders.php b/lib/plugins/kolab_folders/kolab_folders.php
index cc43390..6de4ed3 100644
--- a/lib/plugins/kolab_folders/kolab_folders.php
+++ b/lib/plugins/kolab_folders/kolab_folders.php
@@ -26,8 +26,9 @@ class kolab_folders extends rcube_plugin
 {
     public $task = '?(?!login).*';
 
-    public $types = array('mail', 'event', 'journal', 'task', 'note', 'contact', 'configuration', 'file', 'freebusy');
+    public $types      = array('mail', 'event', 'journal', 'task', 'note', 'contact', 'configuration', 'file', 'freebusy');
     public $mail_types = array('inbox', 'drafts', 'sentitems', 'outbox', 'wastebasket', 'junkemail');
+    public $act_types  = array('event', 'task');
 
     private $rc;
     private static $instance;
@@ -86,7 +87,7 @@ class kolab_folders extends rcube_plugin
 
         // Create default folders
         if ($args['root'] == '' && $args['name'] = '*') {
-            $this->create_default_folders($folders, $args['filter'], $folderdata);
+            $this->create_default_folders($folders, $args['filter'], $folderdata, $args['mode'] == 'LSUB');
         }
 
         $args['folders'] = $folders;
@@ -476,7 +477,7 @@ class kolab_folders extends rcube_plugin
     /**
      * Creates default folders if they doesn't exist
      */
-    private function create_default_folders(&$folders, $filter, $folderdata = null)
+    private function create_default_folders(&$folders, $filter, $folderdata = null, $lsub = false)
     {
         $storage     = $this->rc->get_storage();
         $namespace   = $storage->get_namespace();
@@ -530,25 +531,54 @@ class kolab_folders extends rcube_plugin
             }
 
             list($type1, $type2) = explode('.', $type);
-            $exists = !empty($folderdata[$foldername]) || $foldername == 'INBOX';
+
+            $activate = in_array($type1, $this->act_types);
+            $exists   = false;
+            $result   = false;
+
+            // check if folder exists
+            if (!empty($folderdata[$foldername]) || $foldername == 'INBOX') {
+                $exists = true;
+            }
+            else if ((!$filter || $filter == $type1) && in_array($foldername, $folders)) {
+                // this assumes also that subscribed folder exists
+                $exists = true;
+            }
+            else {
+                $exists = $storage->folder_exists($foldername);
+            }
 
             // create folder
-            if (!$exists && !$storage->folder_exists($foldername)) {
-                $storage->create_folder($foldername, $type1 == 'mail');
-                $storage->subscribe($foldername);
+            if (!$exists) {
+                $exists = $storage->create_folder($foldername);
             }
 
-            // set type
-            $result = $this->set_folder_type($foldername, $type);
+            // set type + subscribe + activate
+            if ($exists) {
+                if ($result = kolab_storage::set_folder_type($foldername, $type)) {
+                    // check if folder is subscribed
+                    if ((!$filter || $filter == $type1) && $lsub && in_array($foldername, $folders)) {
+                        // already subscribed
+                        $subscribed = true;
+                    }
+                    else {
+                        $subscribed = $storage->subscribe($foldername);
+                    }
+
+                    // activate folder
+                    if ($activate) {
+                        kolab_storage::set_state($foldername, true);
+                    }
+                }
+            }
 
             // add new folder to the result
-            if ($result && (!$filter || $filter == $type1)) {
+            if ($result && (!$filter || $filter == $type1) && (!$lsub || $subscribed)) {
                 $folders[] = $foldername;
             }
         }
     }
 
-
     /**
      * Static getter for default folder of the given type
      *
diff --git a/lib/plugins/kolab_folders/localization/de_CH.inc b/lib/plugins/kolab_folders/localization/de_CH.inc
index f9f6e16..b53eea0 100644
--- a/lib/plugins/kolab_folders/localization/de_CH.inc
+++ b/lib/plugins/kolab_folders/localization/de_CH.inc
@@ -1,16 +1,14 @@
 <?php
-
-$labels = array();
-
 $labels['folderctype'] = 'Ordnerinhalt';
 $labels['foldertypemail'] = 'E-Mail';
-$labels['foldertypeevent'] = 'Kalender'; // Termine?
+$labels['foldertypeevent'] = 'Kalender'; // Events?
 $labels['foldertypejournal'] = 'Journal';
 $labels['foldertypetask'] = 'Aufgaben';
 $labels['foldertypenote'] = 'Notizen';
 $labels['foldertypecontact'] = 'Kontakte';
 $labels['foldertypeconfiguration'] = 'Konfiguration';
-
+$labels['foldertypefile'] = 'Dateien';
+$labels['foldertypefreebusy'] = 'Frei-Besetzt';
 $labels['default'] = 'Standard';
 $labels['inbox'] = 'Posteingang';
 $labels['drafts'] = 'Entwürfe';
@@ -18,7 +16,5 @@ $labels['sentitems'] = 'Gesendet';
 $labels['outbox'] = 'Postausgang';
 $labels['wastebasket'] = 'Gelöscht';
 $labels['junkemail'] = 'Spam';
-
 $messages['defaultfolderexists'] = 'Es existiert bereits ein Standardordner für den angegebenen Typ';
-
 ?>
diff --git a/lib/plugins/kolab_folders/localization/de_DE.inc b/lib/plugins/kolab_folders/localization/de_DE.inc
index f9f6e16..60c6741 100644
--- a/lib/plugins/kolab_folders/localization/de_DE.inc
+++ b/lib/plugins/kolab_folders/localization/de_DE.inc
@@ -1,24 +1,20 @@
 <?php
-
-$labels = array();
-
 $labels['folderctype'] = 'Ordnerinhalt';
 $labels['foldertypemail'] = 'E-Mail';
-$labels['foldertypeevent'] = 'Kalender'; // Termine?
+$labels['foldertypeevent'] = 'Kalender'; // Events?
 $labels['foldertypejournal'] = 'Journal';
 $labels['foldertypetask'] = 'Aufgaben';
 $labels['foldertypenote'] = 'Notizen';
 $labels['foldertypecontact'] = 'Kontakte';
 $labels['foldertypeconfiguration'] = 'Konfiguration';
-
+$labels['foldertypefile'] = 'Dateien';
+$labels['foldertypefreebusy'] = 'Frei/Belegt';
 $labels['default'] = 'Standard';
 $labels['inbox'] = 'Posteingang';
 $labels['drafts'] = 'Entwürfe';
 $labels['sentitems'] = 'Gesendet';
 $labels['outbox'] = 'Postausgang';
-$labels['wastebasket'] = 'Gelöscht';
+$labels['wastebasket'] = 'Mülleimer';
 $labels['junkemail'] = 'Spam';
-
-$messages['defaultfolderexists'] = 'Es existiert bereits ein Standardordner für den angegebenen Typ';
-
+$messages['defaultfolderexists'] = 'Es gibt bereits einen Standardordner dieses Typs';
 ?>
diff --git a/lib/plugins/kolab_folders/localization/es_ES.inc b/lib/plugins/kolab_folders/localization/es_ES.inc
index 0481d09..91fe5b4 100644
--- a/lib/plugins/kolab_folders/localization/es_ES.inc
+++ b/lib/plugins/kolab_folders/localization/es_ES.inc
@@ -1,26 +1,7 @@
 <?php
-
-$labels = array();
-
-$labels['folderctype'] = 'Content type';
-$labels['foldertypemail'] = 'Mail';
-$labels['foldertypeevent'] = 'Calendar'; // Events?
-$labels['foldertypejournal'] = 'Journal';
+$labels['foldertypeevent'] = ''; // Events?
 $labels['foldertypetask'] = 'Tareas';
 $labels['foldertypenote'] = 'Notas';
 $labels['foldertypecontact'] = 'Contactos';
 $labels['foldertypeconfiguration'] = 'Configuración';
-$labels['foldertypefile'] = 'Files';
-$labels['foldertypefreebusy'] = 'Free-Busy';
-
-$labels['default'] = 'Default';
-$labels['inbox'] = 'Inbox';
-$labels['drafts'] = 'Drafts';
-$labels['sentitems'] = 'Sent';
-$labels['outbox'] = 'Outbox';
-$labels['wastebasket'] = 'Trash';
-$labels['junkemail'] = 'Junk';
-
-$messages['defaultfolderexists'] = 'There is already default folder of specified type';
-
 ?>
diff --git a/lib/plugins/kolab_folders/localization/et_EE.inc b/lib/plugins/kolab_folders/localization/et_EE.inc
index 856f59d..e39fe4a 100644
--- a/lib/plugins/kolab_folders/localization/et_EE.inc
+++ b/lib/plugins/kolab_folders/localization/et_EE.inc
@@ -1,26 +1,3 @@
 <?php
-
-$labels = array();
-
-$labels['folderctype'] = 'Content type';
-$labels['foldertypemail'] = 'Mail';
-$labels['foldertypeevent'] = 'Calendar'; // Events?
-$labels['foldertypejournal'] = 'Journal';
-$labels['foldertypetask'] = 'Tasks';
-$labels['foldertypenote'] = 'Notes';
-$labels['foldertypecontact'] = 'Contacts';
-$labels['foldertypeconfiguration'] = 'Configuration';
-$labels['foldertypefile'] = 'Files';
-$labels['foldertypefreebusy'] = 'Free-Busy';
-
-$labels['default'] = 'Default';
-$labels['inbox'] = 'Inbox';
-$labels['drafts'] = 'Drafts';
-$labels['sentitems'] = 'Sent';
-$labels['outbox'] = 'Outbox';
-$labels['wastebasket'] = 'Trash';
-$labels['junkemail'] = 'Junk';
-
-$messages['defaultfolderexists'] = 'There is already default folder of specified type';
-
+$labels['foldertypeevent'] = ''; // Events?
 ?>
diff --git a/lib/plugins/kolab_folders/localization/fr_FR.inc b/lib/plugins/kolab_folders/localization/fr_FR.inc
index 19e03e7..9ddece1 100644
--- a/lib/plugins/kolab_folders/localization/fr_FR.inc
+++ b/lib/plugins/kolab_folders/localization/fr_FR.inc
@@ -1,7 +1,4 @@
 <?php
-
-$labels = array();
-
 $labels['folderctype'] = 'Type de contenu';
 $labels['foldertypemail'] = 'Courriel';
 $labels['foldertypeevent'] = 'Calendrier'; // Events?
@@ -12,7 +9,6 @@ $labels['foldertypecontact'] = 'Contacts';
 $labels['foldertypeconfiguration'] = 'Configuration';
 $labels['foldertypefile'] = 'Fichiers';
 $labels['foldertypefreebusy'] = 'Disponible/Occupé';
-
 $labels['default'] = 'Par Défaut';
 $labels['inbox'] = 'Courrier entrant';
 $labels['drafts'] = 'Brouillons';
@@ -20,7 +16,5 @@ $labels['sentitems'] = 'Envoyés';
 $labels['outbox'] = 'Courrier sortant';
 $labels['wastebasket'] = 'Corbeille';
 $labels['junkemail'] = 'Indésirables';
-
 $messages['defaultfolderexists'] = 'Il existe déjà un répertoire par défaut pour le type spécifié';
-
 ?>
diff --git a/lib/plugins/kolab_folders/localization/ja_JP.inc b/lib/plugins/kolab_folders/localization/ja_JP.inc
index 3bba3ed..14f3692 100644
--- a/lib/plugins/kolab_folders/localization/ja_JP.inc
+++ b/lib/plugins/kolab_folders/localization/ja_JP.inc
@@ -1,26 +1,20 @@
 <?php
-
-$labels = array();
-
-$labels['folderctype'] = 'Content type';
-$labels['foldertypemail'] = 'Mail';
-$labels['foldertypeevent'] = 'Calendar'; // Events?
-$labels['foldertypejournal'] = 'Journal';
-$labels['foldertypetask'] = 'Tasks';
-$labels['foldertypenote'] = 'Notes';
-$labels['foldertypecontact'] = 'Contacts';
+$labels['folderctype'] = 'コンテンツタイプ';
+$labels['foldertypemail'] = 'メール';
+$labels['foldertypeevent'] = 'カレンダー'; // Events?
+$labels['foldertypejournal'] = 'ジャーナル';
+$labels['foldertypetask'] = 'タスク';
+$labels['foldertypenote'] = 'ノート';
+$labels['foldertypecontact'] = 'コンタクト';
 $labels['foldertypeconfiguration'] = '設定';
-$labels['foldertypefile'] = 'Files';
-$labels['foldertypefreebusy'] = 'Free-Busy';
-
-$labels['default'] = 'Default';
-$labels['inbox'] = 'Inbox';
-$labels['drafts'] = 'Drafts';
-$labels['sentitems'] = 'Sent';
-$labels['outbox'] = 'Outbox';
-$labels['wastebasket'] = 'Trash';
-$labels['junkemail'] = 'Junk';
-
-$messages['defaultfolderexists'] = 'There is already default folder of specified type';
-
+$labels['foldertypefile'] = 'ファイル';
+$labels['foldertypefreebusy'] = '空状況';
+$labels['default'] = 'デフォルト';
+$labels['inbox'] = '受信箱';
+$labels['drafts'] = '下書き';
+$labels['sentitems'] = '送信済';
+$labels['outbox'] = '送信箱';
+$labels['wastebasket'] = 'ごみ箱';
+$labels['junkemail'] = '迷惑メール';
+$messages['defaultfolderexists'] = '指定したタイプの初期フォルダは既にあります。';
 ?>
diff --git a/lib/plugins/kolab_folders/localization/nl_NL.inc b/lib/plugins/kolab_folders/localization/nl_NL.inc
index 3011279..e0dffcd 100644
--- a/lib/plugins/kolab_folders/localization/nl_NL.inc
+++ b/lib/plugins/kolab_folders/localization/nl_NL.inc
@@ -1,7 +1,4 @@
 <?php
-
-$labels = array();
-
 $labels['folderctype'] = 'Inhoudstype';
 $labels['foldertypemail'] = 'Mail';
 $labels['foldertypeevent'] = 'Agenda'; // Events?
@@ -12,7 +9,6 @@ $labels['foldertypecontact'] = 'Adresboek';
 $labels['foldertypeconfiguration'] = 'Configuratie';
 $labels['foldertypefile'] = 'Bestanden';
 $labels['foldertypefreebusy'] = 'Free/Busy';
-
 $labels['default'] = 'Standaard';
 $labels['inbox'] = 'Inbox';
 $labels['drafts'] = 'Concepten';
@@ -20,7 +16,5 @@ $labels['sentitems'] = 'Verzonden';
 $labels['outbox'] = 'Te versturen';
 $labels['wastebasket'] = 'Prullenbak';
 $labels['junkemail'] = 'Ongewenst';
-
 $messages['defaultfolderexists'] = 'Er is reeds een standaard map voor dit type inhoud';
-
 ?>
diff --git a/lib/plugins/kolab_folders/localization/pl_PL.inc b/lib/plugins/kolab_folders/localization/pl_PL.inc
index 4520dac..f6d98a5 100644
--- a/lib/plugins/kolab_folders/localization/pl_PL.inc
+++ b/lib/plugins/kolab_folders/localization/pl_PL.inc
@@ -1,23 +1,20 @@
 <?php
-
-$labels = array();
-$labels['folderctype'] = 'Zawartość';
+$labels['folderctype'] = 'Typ treści';
 $labels['foldertypemail'] = 'Poczta';
-$labels['foldertypeevent'] = 'Kalendarz';
+$labels['foldertypeevent'] = 'Kalendarz'; // Events?
 $labels['foldertypejournal'] = 'Dziennik';
 $labels['foldertypetask'] = 'Zadania';
 $labels['foldertypenote'] = 'Notatki';
 $labels['foldertypecontact'] = 'Kontakty';
 $labels['foldertypeconfiguration'] = 'Konfiguracja';
 $labels['foldertypefile'] = 'Pliki';
-$labels['foldertypefreebusy'] = 'Free-Busy';
+$labels['foldertypefreebusy'] = 'Wolny-Zajęty';
 $labels['default'] = 'Domyślny';
 $labels['inbox'] = 'Odebrane';
-$labels['drafts'] = 'Szkice';
+$labels['drafts'] = 'Kopie robocze';
 $labels['sentitems'] = 'Wysłane';
-$labels['outbox'] = 'WychodzÄ…ce';
+$labels['outbox'] = 'Poczta wychodzÄ…ca';
 $labels['wastebasket'] = 'Kosz';
 $labels['junkemail'] = 'Spam';
-$messages['defaultfolderexists'] = 'Folder domyślny dla podanego typu już istnieje';
-
+$messages['defaultfolderexists'] = 'Folder domyślny wybranego typu już istnieje';
 ?>
diff --git a/lib/plugins/kolab_folders/localization/ru_RU.inc b/lib/plugins/kolab_folders/localization/ru_RU.inc
index e9878ba..3e55480 100644
--- a/lib/plugins/kolab_folders/localization/ru_RU.inc
+++ b/lib/plugins/kolab_folders/localization/ru_RU.inc
@@ -1,7 +1,4 @@
 <?php
-
-$labels = array();
-
 $labels['folderctype'] = 'Тип ящика';
 $labels['foldertypemail'] = 'Почта';
 $labels['foldertypeevent'] = 'Календарь'; // Events?
@@ -12,7 +9,6 @@ $labels['foldertypecontact'] = 'Контакты';
 $labels['foldertypeconfiguration'] = 'Настройки';
 $labels['foldertypefile'] = 'Файлы';
 $labels['foldertypefreebusy'] = 'Занят/Свободен';
-
 $labels['default'] = 'По умолчанию';
 $labels['inbox'] = 'Входящие';
 $labels['drafts'] = 'Черновики';
@@ -20,7 +16,5 @@ $labels['sentitems'] = 'Отправленные';
 $labels['outbox'] = 'Исходящие';
 $labels['wastebasket'] = 'Корзина';
 $labels['junkemail'] = 'Спам';
-
 $messages['defaultfolderexists'] = 'Уже назначен ящик по умолчанию для указанного типа';
-
 ?>
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/SQL/mysql/2013100400.sql b/lib/plugins/libkolab/SQL/mysql/2013100400.sql
new file mode 100644
index 0000000..d41d0e1
--- /dev/null
+++ b/lib/plugins/libkolab/SQL/mysql/2013100400.sql
@@ -0,0 +1,174 @@
+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 */;
+
+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 */;
+
+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 */;
+
+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 */;
+
+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 */;
+
+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 */;
+
+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,
+  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 */;
+
+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 */;
+
+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 */;
+
+
+-- Migrate data from old kolab_cache table
+
+INSERT INTO kolab_folders (resource, type)
+  SELECT DISTINCT resource, type
+  FROM  kolab_cache WHERE type IN ('event','contact','task','file');
+
+INSERT INTO kolab_cache_event (folder_id, msguid, uid, created, changed, data, xml, tags, words, dtstart, dtend)
+  SELECT kolab_folders.folder_id, msguid, uid, created, changed, data, xml, tags, words, dtstart, dtend
+  FROM kolab_cache LEFT JOIN kolab_folders ON (kolab_folders.resource = kolab_cache.resource)
+  WHERE kolab_cache.type = 'event' AND kolab_folders.folder_id IS NOT NULL;
+
+INSERT INTO kolab_cache_task (folder_id, msguid, uid, created, changed, data, xml, tags, words, dtstart, dtend)
+  SELECT kolab_folders.folder_id, msguid, uid, created, changed, data, xml, tags, words, dtstart, dtend
+  FROM kolab_cache LEFT JOIN kolab_folders ON (kolab_folders.resource = kolab_cache.resource)
+  WHERE kolab_cache.type = 'task' AND kolab_folders.folder_id IS NOT NULL;
+
+INSERT INTO kolab_cache_contact (folder_id, msguid, uid, created, changed, data, xml, tags, words, type)
+  SELECT kolab_folders.folder_id, msguid, uid, created, changed, data, xml, tags, words, kolab_cache.type
+  FROM kolab_cache LEFT JOIN kolab_folders ON (kolab_folders.resource = kolab_cache.resource)
+  WHERE kolab_cache.type IN ('contact','distribution-list') AND kolab_folders.folder_id IS NOT NULL;
+
+INSERT INTO kolab_cache_file (folder_id, msguid, uid, created, changed, data, xml, tags, words, filename)
+  SELECT kolab_folders.folder_id, msguid, uid, created, changed, data, xml, tags, words, filename
+  FROM kolab_cache LEFT JOIN kolab_folders ON (kolab_folders.resource = kolab_cache.resource)
+  WHERE kolab_cache.type = 'file' AND kolab_folders.folder_id IS NOT NULL;
+
+
+DROP TABLE IF EXISTS `kolab_cache`;
+
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..500dfa2 100644
--- a/lib/plugins/libkolab/lib/kolab_format_xcal.php
+++ b/lib/plugins/libkolab/lib/kolab_format_xcal.php
@@ -373,7 +373,7 @@ abstract class kolab_format_xcal extends kolab_format
         if ($object['alarms']) {
             list($offset, $type) = explode(":", $object['alarms']);
 
-            if ($type == 'EMAIL') {  // email alarms implicitly go to event owner
+            if ($type == 'EMAIL' && !empty($object['_owner'])) {  // email alarms implicitly go to event owner
                 $recipients = new vectorcontactref;
                 $recipients->push(new ContactReference(ContactReference::EmailReference, $object['_owner']));
                 $alarm = new Alarm($object['title'], strval($object['description']), $recipients);
@@ -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..651dc18 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,66 @@ 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()) {
 
-        // compare IMAP index with object cache index
-        $imap_index = $this->imap->index($this->folder->name);
-        $this->index = $imap_index->get();
+            // lock synchronization for this folder or wait if locked
+            $this->_sync_lock();
 
-        // 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'
-            );
+            // disable messages cache if configured to do so
+            $this->bypass(true);
 
-            $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'];
-            }
+            // synchronize IMAP mailbox cache
+            $this->imap->folder_sync($this->folder->name);
 
-            // 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
+            // compare IMAP index with object cache index
+            $imap_index = $this->imap->index($this->folder->name);
+            $this->index = $imap_index->get();
+
+            // 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 +211,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 +257,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 +283,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',
@@ -265,8 +319,8 @@ class kolab_storage_cache
         }
 
         // keep a copy in memory for fast access
-        $this->objects[$msguid] = $object;
-        $this->uid2msg[$object['uid']] = $msguid;
+        $this->objects = array($msguid => $object);
+        $this->uid2msg = array($object['uid'] => $msguid);
     }
 
 
@@ -283,14 +337,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 +362,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 +403,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 +465,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 +491,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 +536,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 +555,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 +577,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 +608,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 +652,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 +664,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 +679,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 +726,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 +738,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 +778,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 +796,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 +824,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_cache_configuration.php b/lib/plugins/libkolab/lib/kolab_storage_cache_configuration.php
new file mode 100644
index 0000000..8380aa8
--- /dev/null
+++ b/lib/plugins/libkolab/lib/kolab_storage_cache_configuration.php
@@ -0,0 +1,40 @@
+<?php
+
+/**
+ * Kolab storage cache class for configuration objects
+ *
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 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
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class kolab_storage_cache_configuration extends kolab_storage_cache
+{
+    protected $extra_cols = array('type');
+
+    /**
+     * Helper method to convert the given Kolab object into a dataset to be written to cache
+     *
+     * @override
+     */
+    protected function _serialize($object)
+    {
+        $sql_data = parent::_serialize($object);
+        $sql_data['type'] = $object['type'];
+
+        return $sql_data;
+    }
+}
\ No newline at end of file
diff --git a/lib/plugins/libkolab/lib/kolab_storage_cache_contact.php b/lib/plugins/libkolab/lib/kolab_storage_cache_contact.php
new file mode 100644
index 0000000..e17923d
--- /dev/null
+++ b/lib/plugins/libkolab/lib/kolab_storage_cache_contact.php
@@ -0,0 +1,45 @@
+<?php
+
+/**
+ * Kolab storage cache class for contact objects
+ *
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 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
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class kolab_storage_cache_contact extends kolab_storage_cache
+{
+    protected $extra_cols = array('type');
+    protected $binary_items = array(
+        'photo'          => '|<photo><uri>[^;]+;base64,([^<]+)</uri></photo>|i',
+        'pgppublickey'   => '|<key><uri>date:application/pgp-keys;base64,([^<]+)</uri></key>|i',
+        'pkcs7publickey' => '|<key><uri>date:application/pkcs7-mime;base64,([^<]+)</uri></key>|i',
+    );
+
+    /**
+     * Helper method to convert the given Kolab object into a dataset to be written to cache
+     *
+     * @override
+     */
+    protected function _serialize($object)
+    {
+        $sql_data = parent::_serialize($object);
+        $sql_data['type'] = $object['_type'];
+
+        return $sql_data;
+    }
+}
\ No newline at end of file
diff --git a/lib/plugins/libkolab/lib/kolab_storage_cache_event.php b/lib/plugins/libkolab/lib/kolab_storage_cache_event.php
new file mode 100644
index 0000000..69134e7
--- /dev/null
+++ b/lib/plugins/libkolab/lib/kolab_storage_cache_event.php
@@ -0,0 +1,49 @@
+<?php
+
+/**
+ * Kolab storage cache class for calendar event objects
+ *
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 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
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class kolab_storage_cache_event extends kolab_storage_cache
+{
+    protected $extra_cols = array('dtstart','dtend');
+
+    /**
+     * Helper method to convert the given Kolab object into a dataset to be written to cache
+     *
+     * @override
+     */
+    protected function _serialize($object)
+    {
+        $sql_data = parent::_serialize($object);
+
+        // 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'));
+        }
+
+        return $sql_data;
+    }
+}
\ No newline at end of file
diff --git a/lib/plugins/libkolab/lib/kolab_storage_cache_file.php b/lib/plugins/libkolab/lib/kolab_storage_cache_file.php
new file mode 100644
index 0000000..ea1823d
--- /dev/null
+++ b/lib/plugins/libkolab/lib/kolab_storage_cache_file.php
@@ -0,0 +1,44 @@
+<?php
+
+/**
+ * Kolab storage cache class for file objects
+ *
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 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
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class kolab_storage_cache_file extends kolab_storage_cache
+{
+    protected $extra_cols = array('filename');
+
+    /**
+     * Helper method to convert the given Kolab object into a dataset to be written to cache
+     *
+     * @override
+     */
+    protected function _serialize($object)
+    {
+        $sql_data = parent::_serialize($object);
+
+        if (!empty($object['_attachments'])) {
+            reset($object['_attachments']);
+            $sql_data['filename'] = $object['_attachments'][key($object['_attachments'])]['name'];
+        }
+
+        return $sql_data;
+    }
+}
\ No newline at end of file
diff --git a/lib/plugins/libkolab/lib/kolab_storage_cache_freebusy.php b/lib/plugins/libkolab/lib/kolab_storage_cache_freebusy.php
new file mode 100644
index 0000000..d8ab554
--- /dev/null
+++ b/lib/plugins/libkolab/lib/kolab_storage_cache_freebusy.php
@@ -0,0 +1,27 @@
+<?php
+
+/**
+ * Kolab storage cache class for freebusy objects
+ *
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 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
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class kolab_storage_cache_freebusy extends kolab_storage_cache
+{
+        protected $extra_cols = array('dtstart','dtend');
+}
\ No newline at end of file
diff --git a/lib/plugins/libkolab/lib/kolab_storage_cache_journal.php b/lib/plugins/libkolab/lib/kolab_storage_cache_journal.php
new file mode 100644
index 0000000..a63577b
--- /dev/null
+++ b/lib/plugins/libkolab/lib/kolab_storage_cache_journal.php
@@ -0,0 +1,28 @@
+<?php
+
+/**
+ * Kolab storage cache class for journal objects
+ *
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 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
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class kolab_storage_cache_journal extends kolab_storage_cache
+{
+    protected $extra_cols = array('dtstart','dtend');
+    
+}
\ No newline at end of file
diff --git a/lib/plugins/libkolab/lib/kolab_storage_cache_mongodb.php b/lib/plugins/libkolab/lib/kolab_storage_cache_mongodb.php
new file mode 100644
index 0000000..8ae95e4
--- /dev/null
+++ b/lib/plugins/libkolab/lib/kolab_storage_cache_mongodb.php
@@ -0,0 +1,561 @@
+<?php
+
+/**
+ * Kolab storage cache class providing a local caching layer for Kolab groupware objects.
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2012, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class kolab_storage_cache_mongodb
+{
+    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 = 1046576;  // 1 MB - 2000 bytes
+    private $binary_cols = array('photo','pgppublickey','pkcs7publickey');
+
+
+    /**
+     * Default constructor
+     */
+    public function __construct(kolab_storage_folder $storage_folder = null)
+    {
+        $rcmail = rcube::get_instance();
+        $mongo = new Mongo();
+        $this->db = $mongo->kolab_cache;
+        $this->imap = $rcmail->get_storage();
+        $this->enabled = $rcmail->config->get('kolab_cache', false);
+
+        if ($this->enabled) {
+            // remove sync-lock on script termination
+            $rcmail->add_shutdown_function(array($this, '_sync_unlock'));
+        }
+
+        if ($storage_folder)
+            $this->set_folder($storage_folder);
+    }
+
+
+    /**
+     * Connect cache with a storage folder
+     *
+     * @param kolab_storage_folder The storage folder instance to connect with
+     */
+    public function set_folder(kolab_storage_folder $storage_folder)
+    {
+        $this->folder = $storage_folder;
+
+        if (empty($this->folder->name)) {
+            $this->ready = false;
+            return;
+        }
+
+        // compose fully qualified ressource uri for this instance
+        $this->resource_uri = $this->folder->get_resource_uri();
+        $this->ready = $this->enabled;
+    }
+
+
+    /**
+     * Synchronize local cache data with remote
+     */
+    public function synchronize()
+    {
+        // only sync once per request cycle
+        if ($this->synched)
+            return;
+
+        // increase time limit
+        @set_time_limit(500);
+
+        // lock synchronization for this folder or wait if locked
+        $this->_sync_lock();
+
+        // synchronize IMAP mailbox cache
+        $this->imap->folder_sync($this->folder->name);
+
+        // compare IMAP index with object cache index
+        $imap_index = $this->imap->index($this->folder->name);
+        $this->index = $imap_index->get();
+
+        // determine objects to fetch or to invalidate
+        if ($this->ready) {
+            // read cache index
+            $old_index = array();
+            $cursor = $this->db->cache->find(array('resource' => $this->resource_uri), array('msguid' => 1, 'uid' => 1));
+            foreach ($cursor as $doc) {
+                $old_index[] = $doc['msguid'];
+                $this->uid2msg[$doc['uid']] = $doc['msguid'];
+            }
+
+            // fetch new objects from imap
+            foreach (array_diff($this->index, $old_index) as $msguid) {
+                if ($object = $this->folder->read_object($msguid, '*')) {
+                    try {
+                        $this->db->cache->insert($this->_serialize($object, $msguid));
+                    }
+                    catch (Exception $e) {
+                        rcmail::raise_error(array(
+                            'code' => 900, 'type' => 'php',
+                            'message' => "Failed to write to mongodb cache: " . $e->getMessage(),
+                        ), true);
+                    }
+                }
+            }
+
+            // delete invalid entries from local DB
+            $del_index = array_diff($old_index, $this->index);
+            if (!empty($del_index)) {
+                $this->db->cache->remove(array('resource' => $this->resource_uri, 'msguid' => array('$in' => $del_index)));
+            }
+        }
+
+        // remove lock
+        $this->_sync_unlock();
+
+        $this->synched = time();
+    }
+
+
+    /**
+     * Read a single entry from cache or from IMAP directly
+     *
+     * @param string Related IMAP message UID
+     * @param string Object type to read
+     * @param string IMAP folder name the entry relates to
+     * @param array  Hash array with object properties or null if not found
+     */
+    public function get($msguid, $type = null, $foldername = null)
+    {
+        // delegate to another cache instance
+        if ($foldername && $foldername != $this->folder->name) {
+            return kolab_storage::get_folder($foldername)->cache->get($msguid, $object);
+        }
+
+        // load object if not in memory
+        if (!isset($this->objects[$msguid])) {
+            if ($this->ready && ($doc = $this->db->cache->findOne(array('resource' => $this->resource_uri, 'msguid' => $msguid))))
+                $this->objects[$msguid] = $this->_unserialize($doc);
+
+            // fetch from IMAP if not present in cache
+            if (empty($this->objects[$msguid])) {
+                $result = $this->_fetch(array($msguid), $type, $foldername);
+                $this->objects[$msguid] = $result[0];
+            }
+        }
+
+        return $this->objects[$msguid];
+    }
+
+
+    /**
+     * Insert/Update a cache entry
+     *
+     * @param string Related IMAP message UID
+     * @param mixed  Hash array with object properties to save or false to delete the cache entry
+     * @param string IMAP folder name the entry relates to
+     */
+    public function set($msguid, $object, $foldername = null)
+    {
+        // delegate to another cache instance
+        if ($foldername && $foldername != $this->folder->name) {
+            kolab_storage::get_folder($foldername)->cache->set($msguid, $object);
+            return;
+        }
+
+        // write to cache
+        if ($this->ready) {
+            // remove old entry
+            $this->db->cache->remove(array('resource' => $this->resource_uri, 'msguid' => $msguid));
+
+            // write new object data if not false (wich means deleted)
+            if ($object) {
+                try {
+                    $this->db->cache->insert($this->_serialize($object, $msguid));
+                }
+                catch (Exception $e) {
+                    rcmail::raise_error(array(
+                        'code' => 900, 'type' => 'php',
+                        'message' => "Failed to write to mongodb cache: " . $e->getMessage(),
+                    ), true);
+                }
+            }
+        }
+
+        // keep a copy in memory for fast access
+        $this->objects[$msguid] = $object;
+
+        if ($object)
+            $this->uid2msg[$object['uid']] = $msguid;
+    }
+
+    /**
+     * Move an existing cache entry to a new resource
+     *
+     * @param string Entry's IMAP message UID
+     * @param string Entry's Object UID
+     * @param string Target IMAP folder to move it to
+     */
+    public function move($msguid, $objuid, $target_folder)
+    {
+        $target = kolab_storage::get_folder($target_folder);
+
+        // resolve new message UID in target folder
+        if ($new_msguid = $target->cache->uid2msguid($objuid)) {
+/*
+            $this->db->query(
+                "UPDATE kolab_cache SET resource=?, msguid=? ".
+                "WHERE resource=? AND msguid=? AND type<>?",
+                $target->get_resource_uri(),
+                $new_msguid,
+                $this->resource_uri,
+                $msguid,
+                'lock'
+            );
+*/
+        }
+        else {
+            // just clear cache entry
+            $this->set($msguid, false);
+        }
+
+        unset($this->uid2msg[$uid]);
+    }
+
+
+    /**
+     * Remove all objects from local cache
+     */
+    public function purge($type = null)
+    {
+        return $this->db->cache->remove(array(), array('safe' => true));
+    }
+
+
+    /**
+     * Select Kolab objects filtered by the given query
+     *
+     * @param array Pseudo-SQL query as list of filter parameter triplets
+     *   triplet: array('<colname>', '<comparator>', '<value>')
+     * @return array List of Kolab data objects (each represented as hash array)
+     */
+    public function select($query = array())
+    {
+        $result = array();
+
+        // read from local cache DB (assume it to be synchronized)
+        if ($this->ready) {
+            $cursor = $this->db->cache->find(array('resource' => $this->resource_uri) + $this->_mongo_filter($query));
+            foreach ($cursor as $doc) {
+                if ($object = $this->_unserialize($doc))
+                    $result[] = $object;
+            }
+        }
+        else {
+            // extract object type from query parameter
+            $filter = $this->_query2assoc($query);
+
+            // use 'list' for folder's default objects
+            if ($filter['type'] == $this->type) {
+                $index = $this->index;
+            }
+            else {  // search by object type
+                $search = 'UNDELETED HEADER X-Kolab-Type ' . kolab_storage_folder::KTYPE_PREFIX . $filter['type'];
+                $index = $this->imap->search_once($this->folder->name, $search)->get();
+            }
+
+            // fetch all messages in $index from IMAP
+            $result = $this->_fetch($index, $filter['type']);
+
+            // TODO: post-filter result according to query
+        }
+
+        return $result;
+    }
+
+
+    /**
+     * Get number of objects mathing the given query
+     *
+     * @param array  $query Pseudo-SQL query as list of filter parameter triplets
+     * @return integer The number of objects of the given type
+     */
+    public function count($query = array())
+    {
+        $count = 0;
+
+        // cache is in sync, we can count records in local DB
+        if ($this->synched) {
+            $cursor = $this->db->cache->find(array('resource' => $this->resource_uri) + $this->_mongo_filter($query));
+            $count = $cursor->valid() ? $cursor->count() : 0;
+        }
+        else {
+            // search IMAP by object type
+            $filter = $this->_query2assoc($query);
+            $ctype  = kolab_storage_folder::KTYPE_PREFIX . $filter['type'];
+            $index = $this->imap->search_once($this->folder->name, 'UNDELETED HEADER X-Kolab-Type ' . $ctype);
+            $count = $index->count();
+        }
+
+        return $count;
+    }
+
+    /**
+     * Helper method to convert the pseudo-SQL query into a valid mongodb filter
+     */
+    private function _mongo_filter($query)
+    {
+        $filters = array();
+        foreach ($query as $param) {
+            $filter = array();
+            if ($param[1] == '=' && is_array($param[2])) {
+                $filter[$param[0]] = array('$in' => $param[2]);
+                $filters[] = $filter;
+            }
+            else if ($param[1] == '=') {
+                $filters[] = array($param[0] => $param[2]);
+            }
+            else if ($param[1] == 'LIKE' || $param[1] == '~') {
+                $filter[$param[0]] = array('$regex' => preg_quote($param[2]), '$options' => 'i');
+                $filters[] = $filter;
+            }
+            else if ($param[1] == '!~' || $param[1] == '!LIKE') {
+                $filter[$param[0]] = array('$not' => '/' . preg_quote($param[2]) . '/i');
+                $filters[] = $filter;
+            }
+            else {
+                $op = '';
+                switch ($param[1]) {
+                    case '>':  $op = '$gt';  break;
+                    case '>=': $op = '$gte'; break;
+                    case '<':  $op = '$lt';  break;
+                    case '<=': $op = '$lte'; break;
+                    case '!=':
+                    case '<>': $op = '$gte'; break;
+                }
+                if ($op) {
+                    $filter[$param[0]] = array($op => $param[2]);
+                    $filters[] = $filter;
+                }
+            }
+        }
+
+        return array('$and' => $filters);
+    }
+
+    /**
+     * Helper method to convert the given pseudo-query triplets into
+     * an associative filter array with 'equals' values only
+     */
+    private function _query2assoc($query)
+    {
+        // extract object type from query parameter
+        $filter = array();
+        foreach ($query as $param) {
+            if ($param[1] == '=')
+                $filter[$param[0]] = $param[2];
+        }
+        return $filter;
+    }
+
+    /**
+     * Fetch messages from IMAP
+     *
+     * @param array List of message UIDs to fetch
+     * @return array List of parsed Kolab objects
+     */
+    private function _fetch($index, $type = null, $folder = null)
+    {
+        $results = array();
+        foreach ((array)$index as $msguid) {
+            if ($object = $this->folder->read_object($msguid, $type, $folder)) {
+                $results[] = $object;
+                $this->set($msguid, $object);
+            }
+        }
+
+        return $results;
+    }
+
+
+    /**
+     * Helper method to convert the given Kolab object into a dataset to be written to cache
+     */
+    private function _serialize($object, $msguid)
+    {
+        $bincols = array_flip($this->binary_cols);
+        $doc = array(
+            'resource' => $this->resource_uri,
+            'type'     => $object['_type'] ? $object['_type'] : $this->folder->type,
+            'msguid'   => $msguid,
+            'uid'      => $object['uid'],
+            'xml'      => '',
+            'tags'     => array(),
+            'words'    => array(),
+            'objcols'  => array(),
+        );
+
+        // set type specific values
+        if ($this->folder->type == 'event') {
+            // database runs in server's timezone so using date() is what we want
+            $doc['dtstart'] = date('Y-m-d H:i:s', is_object($object['start']) ? $object['start']->format('U') : $object['start']);
+            $doc['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']) {
+                $doc['dtend'] = date('Y-m-d H:i:s', $object['recurrence']['UNTIL'] ?: strtotime('now + 2 years'));
+            }
+        }
+
+        if ($object['_formatobj']) {
+            $doc['xml'] = preg_replace('!(</?[a-z0-9:-]+>)[\n\r\t\s]+!ms', '$1', (string)$object['_formatobj']->write());
+            $doc['tags'] = $object['_formatobj']->get_tags();
+            $doc['words'] = $object['_formatobj']->get_words();
+        }
+
+        // extract object data
+        $data = array();
+        foreach ($object as $key => $val) {
+            if ($val === "" || $val === null) {
+                // skip empty properties
+                continue;
+            }
+            if (isset($bincols[$key])) {
+                $data[$key] = base64_encode($val);
+            }
+            else if (is_object($val)) {
+                if (is_a($val, 'DateTime')) {
+                    $data[$key] = array('_class' => 'DateTime', 'date' => $val->format('Y-m-d H:i:s'), 'timezone' => $val->getTimezone()->getName());
+                    $doc['objcols'][] = $key;
+                }
+            }
+            else if ($key[0] != '_') {
+                $data[$key] = $val;
+            }
+            else if ($key == '_attachments') {
+                foreach ($val as $k => $att) {
+                    unset($att['content'], $att['path']);
+                    if ($att['id'])
+                        $data[$key][$k] = $att;
+                }
+            }
+        }
+
+        $doc['data'] = $data;
+        return $doc;
+    }
+
+    /**
+     * Helper method to turn stored cache data into a valid storage object
+     */
+    private function _unserialize($doc)
+    {
+        $object = $doc['data'];
+
+        // decode binary properties
+        foreach ($this->binary_cols as $key) {
+            if (!empty($object[$key]))
+                $object[$key] = base64_decode($object[$key]);
+        }
+
+        // restore serialized objects
+        foreach ((array)$doc['objcols'] as $key) {
+            switch ($object[$key]['_class']) {
+                case 'DateTime':
+                    $val = new DateTime($object[$key]['date'], new DateTimeZone($object[$key]['timezone']));
+                    $object[$key] = $val;
+                    break;
+            }
+        }
+
+        // add meta data
+        $object['_type'] = $doc['type'];
+        $object['_msguid'] = $doc['msguid'];
+        $object['_mailbox'] = $this->folder->name;
+        $object['_formatobj'] = kolab_format::factory($doc['type'], $doc['xml']);
+
+        return $object;
+    }
+
+    /**
+     * Check lock record for this folder and wait if locked or set lock
+     */
+    private function _sync_lock()
+    {
+        if (!$this->ready)
+            return;
+
+        $this->synclock = true;
+        $lock = $this->db->locks->findOne(array('resource' => $this->resource_uri));
+
+        // create lock record if not exists
+        if (!$lock) {
+            $this->db->locks->insert(array('resource' => $this->resource_uri, 'created' => time()));
+        }
+        // wait if locked (expire locks after 10 minutes)
+        else if ((time() - $lock['created']) < 600) {
+            usleep(500000);
+            return $this->_sync_lock();
+        }
+        // set lock
+        else {
+            $lock['created'] = time();
+            $this->db->locks->update(array('_id' => $lock['_id']), $lock, array('safe' => true));
+        }
+    }
+
+    /**
+     * Remove lock for this folder
+     */
+    public function _sync_unlock()
+    {
+        if (!$this->ready || !$this->synclock)
+            return;
+
+        $this->db->locks->remove(array('resource' => $this->resource_uri));
+    }
+
+    /**
+     * Resolve an object UID into an IMAP message UID
+     *
+     * @param string  Kolab object UID
+     * @param boolean Include deleted objects
+     * @return int The resolved IMAP message UID
+     */
+    public function uid2msguid($uid, $deleted = false)
+    {
+        if (!isset($this->uid2msg[$uid])) {
+            // use IMAP SEARCH to get the right message
+            $index = $this->imap->search_once($this->folder->name, ($deleted ? '' : 'UNDELETED ') . 'HEADER SUBJECT ' . $uid);
+            $results = $index->get();
+            $this->uid2msg[$uid] = $results[0];
+        }
+
+        return $this->uid2msg[$uid];
+    }
+
+}
diff --git a/lib/plugins/libkolab/lib/kolab_storage_cache_note.php b/lib/plugins/libkolab/lib/kolab_storage_cache_note.php
new file mode 100644
index 0000000..8546927
--- /dev/null
+++ b/lib/plugins/libkolab/lib/kolab_storage_cache_note.php
@@ -0,0 +1,27 @@
+<?php
+
+/**
+ * Kolab storage cache class for note objects
+ *
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 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
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class kolab_storage_cache_note extends kolab_storage_cache
+{
+    
+}
\ No newline at end of file
diff --git a/lib/plugins/libkolab/lib/kolab_storage_cache_task.php b/lib/plugins/libkolab/lib/kolab_storage_cache_task.php
new file mode 100644
index 0000000..a1953f6
--- /dev/null
+++ b/lib/plugins/libkolab/lib/kolab_storage_cache_task.php
@@ -0,0 +1,44 @@
+<?php
+
+/**
+ * Kolab storage cache class for task objects
+ *
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 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
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class kolab_storage_cache_task extends kolab_storage_cache
+{
+    protected $extra_cols = array('dtstart','dtend');
+
+    /**
+     * Helper method to convert the given Kolab object into a dataset to be written to cache
+     *
+     * @override
+     */
+    protected function _serialize($object)
+    {
+        $sql_data = parent::_serialize($object) + array('dtstart' => null, 'dtend' => null);
+
+        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']);
+
+        return $sql_data;
+    }
+}
\ No newline at end of file
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