diff --git a/plugins/libkolab/SQL/postgres.initial.sql b/plugins/libkolab/SQL/postgres.initial.sql
new file mode 100644
index 00000000..df396883
--- /dev/null
+++ b/plugins/libkolab/SQL/postgres.initial.sql
@@ -0,0 +1,207 @@
+CREATE SEQUENCE kolab_folders_seq
+    INCREMENT BY 1
+    NO MAXVALUE
+    NO MINVALUE
+    CACHE 1;
+
+CREATE TABLE kolab_folders (
+    folder_id integer DEFAULT nextval('kolab_folders_seq'::text) PRIMARY KEY,
+    resource varchar(255) NOT NULL,
+    "type" varchar(32) NOT NULL,
+    synclock integer NOT NULL DEFAULT 0,
+    ctag varchar(40) DEFAULT NULL,
+    changed timestamp with time zone DEFAULT NULL,
+    objectcount integer DEFAULT NULL
+);
+
+CREATE INDEX kolab_folders_resource_type_idx ON kolab_folders(resource, "type");
+
+CREATE TABLE kolab_cache_contact (
+    folder_id integer NOT NULL
+        REFERENCES kolab_folders (folder_id) ON DELETE CASCADE ON UPDATE CASCADE,
+    msguid integer NOT NULL,
+    uid varchar(512) NOT NULL,
+    created timestamp with time zone DEFAULT NULL,
+    changed timestamp with time zone DEFAULT NULL,
+    data text NOT NULL,
+    tags text NOT NULL,
+    words text NOT NULL,
+    "type" varchar(32) NOT NULL,
+    name varchar(255) NOT NULL,
+    firstname varchar(255) NOT NULL,
+    surname varchar(255) NOT NULL,
+    email varchar(255) NOT NULL,
+    PRIMARY KEY(folder_id, msguid)
+);
+
+CREATE INDEX kolab_cache_contact_type_idx ON kolab_cache_contact(folder_id, "type");
+CREATE INDEX kolab_cache_contact_uid2msguid_idx ON kolab_cache_contact(folder_id, uid, msguid);
+
+CREATE TABLE kolab_cache_event (
+    folder_id integer NOT NULL
+        REFERENCES kolab_folders (folder_id) ON DELETE CASCADE ON UPDATE CASCADE,
+    msguid integer NOT NULL,
+    uid varchar(512) NOT NULL,
+    created timestamp with time zone DEFAULT NULL,
+    changed timestamp with time zone DEFAULT NULL,
+    data text NOT NULL,
+    tags text NOT NULL,
+    words text NOT NULL,
+    dtstart timestamp with time zone,
+    dtend timestamp with time zone,
+    PRIMARY KEY(folder_id, msguid)
+);
+
+CREATE INDEX kolab_cache_event_uid2msguid_idx ON kolab_cache_event(folder_id, uid, msguid);
+
+CREATE TABLE kolab_cache_task (
+    folder_id integer NOT NULL
+        REFERENCES kolab_folders (folder_id) ON DELETE CASCADE ON UPDATE CASCADE,
+    msguid integer NOT NULL,
+    uid varchar(512) NOT NULL,
+    created timestamp with time zone DEFAULT NULL,
+    changed timestamp with time zone DEFAULT NULL,
+    data text NOT NULL,
+    tags text NOT NULL,
+    words text NOT NULL,
+    dtstart timestamp with time zone,
+    dtend timestamp with time zone,
+    PRIMARY KEY(folder_id, msguid)
+);
+
+CREATE INDEX kolab_cache_task_uid2msguid_idx ON kolab_cache_task(folder_id, uid, msguid);
+
+CREATE TABLE kolab_cache_journal (
+    folder_id integer NOT NULL
+        REFERENCES kolab_folders (folder_id) ON DELETE CASCADE ON UPDATE CASCADE,
+    msguid integer NOT NULL,
+    uid varchar(512) NOT NULL,
+    created timestamp with time zone DEFAULT NULL,
+    changed timestamp with time zone DEFAULT NULL,
+    data text NOT NULL,
+    tags text NOT NULL,
+    words text NOT NULL,
+    dtstart timestamp with time zone,
+    dtend timestamp with time zone,
+    PRIMARY KEY(folder_id, msguid)
+);
+
+CREATE INDEX kolab_cache_journal_uid2msguid_idx ON kolab_cache_journal(folder_id, uid, msguid);
+
+CREATE TABLE kolab_cache_note (
+    folder_id integer NOT NULL
+        REFERENCES kolab_folders (folder_id) ON DELETE CASCADE ON UPDATE CASCADE,
+    msguid integer NOT NULL,
+    uid varchar(512) NOT NULL,
+    created timestamp with time zone DEFAULT NULL,
+    changed timestamp with time zone DEFAULT NULL,
+    data text NOT NULL,
+    tags text NOT NULL,
+    words text NOT NULL,
+    PRIMARY KEY(folder_id, msguid)
+);
+
+CREATE INDEX kolab_cache_note_uid2msguid_idx ON kolab_cache_note(folder_id, uid, msguid);
+
+CREATE TABLE kolab_cache_file (
+    folder_id integer NOT NULL
+        REFERENCES kolab_folders (folder_id) ON DELETE CASCADE ON UPDATE CASCADE,
+    msguid integer NOT NULL,
+    uid varchar(512) NOT NULL,
+    created timestamp with time zone DEFAULT NULL,
+    changed timestamp with time zone DEFAULT NULL,
+    data text NOT NULL,
+    tags text NOT NULL,
+    words text NOT NULL,
+    filename varchar(255) DEFAULT NULL,
+    PRIMARY KEY(folder_id, msguid)
+);
+
+CREATE INDEX kolab_cache_file_filename_idx ON kolab_cache_file(folder_id, filename);
+CREATE INDEX kolab_cache_file_uid2msguid_idx ON kolab_cache_file(folder_id, uid, msguid);
+
+CREATE TABLE kolab_cache_configuration (
+    folder_id integer NOT NULL
+        REFERENCES kolab_folders (folder_id) ON DELETE CASCADE ON UPDATE CASCADE,
+    msguid integer NOT NULL,
+    uid varchar(512) NOT NULL,
+    created timestamp with time zone DEFAULT NULL,
+    changed timestamp with time zone DEFAULT NULL,
+    data text NOT NULL,
+    tags text NOT NULL,
+    words text NOT NULL,
+    "type" varchar(32) NOT NULL,
+    PRIMARY KEY(folder_id, msguid)
+);
+
+CREATE INDEX kolab_cache_configuration_type_idx ON kolab_cache_configuration(folder_id, "type");
+CREATE INDEX kolab_cache_configuration_uid2msguid_idx ON kolab_cache_configuration(folder_id, uid, msguid);
+
+CREATE TABLE kolab_cache_freebusy (
+    folder_id integer NOT NULL
+        REFERENCES kolab_folders (folder_id) ON DELETE CASCADE ON UPDATE CASCADE,
+    msguid integer NOT NULL,
+    uid varchar(512) NOT NULL,
+    created timestamp with time zone DEFAULT NULL,
+    changed timestamp with time zone DEFAULT NULL,
+    data text NOT NULL,
+    tags text NOT NULL,
+    words text NOT NULL,
+    dtstart timestamp with time zone,
+    dtend timestamp with time zone,
+    PRIMARY KEY(folder_id, msguid)
+);
+
+CREATE INDEX kolab_cache_freebusy_uid2msguid_idx ON kolab_cache_freebusy(folder_id, uid, msguid);
+
+CREATE TABLE kolab_cache_dav_contact (
+    folder_id integer NOT NULL
+        REFERENCES kolab_folders (folder_id) ON DELETE CASCADE ON UPDATE CASCADE,
+    uid varchar(512) NOT NULL,
+    etag varchar(128) NOT NULL,
+    created timestamp with time zone DEFAULT NULL,
+    changed timestamp with time zone DEFAULT NULL,
+    data text NOT NULL,
+    tags text NOT NULL,
+    words text NOT NULL,
+    "type" varchar(32) NOT NULL,
+    name varchar(255) NOT NULL,
+    firstname varchar(255) NOT NULL,
+    surname varchar(255) NOT NULL,
+    email varchar(255) NOT NULL,
+    PRIMARY KEY(folder_id, uid)
+);
+
+CREATE INDEX kolab_cache_dav_contact_type_idx ON kolab_cache_dav_contact(folder_id, "type");
+
+CREATE TABLE kolab_cache_dav_event (
+    folder_id integer NOT NULL
+        REFERENCES kolab_folders (folder_id) ON DELETE CASCADE ON UPDATE CASCADE,
+    uid varchar(512) NOT NULL,
+    etag varchar(128) NOT NULL,
+    created timestamp with time zone DEFAULT NULL,
+    changed timestamp with time zone DEFAULT NULL,
+    data text NOT NULL,
+    tags text NOT NULL,
+    words text NOT NULL,
+    dtstart timestamp with time zone,
+    dtend timestamp with time zone,
+    PRIMARY KEY(folder_id, uid)
+);
+
+CREATE TABLE kolab_cache_dav_task (
+    folder_id integer NOT NULL
+        REFERENCES kolab_folders (folder_id) ON DELETE CASCADE ON UPDATE CASCADE,
+    uid varchar(512) NOT NULL,
+    etag varchar(128) NOT NULL,
+    created timestamp with time zone DEFAULT NULL,
+    changed timestamp with time zone DEFAULT NULL,
+    data text NOT NULL,
+    tags text NOT NULL,
+    words text NOT NULL,
+    dtstart timestamp with time zone,
+    dtend timestamp with time zone,
+    PRIMARY KEY(folder_id, uid)
+);
+
+INSERT INTO "system" (name, "value") VALUES ('libkolab-version', '2022122800');
diff --git a/plugins/libkolab/lib/kolab_storage_dav_cache.php b/plugins/libkolab/lib/kolab_storage_dav_cache.php
index 68a97708..2bc4b317 100644
--- a/plugins/libkolab/lib/kolab_storage_dav_cache.php
+++ b/plugins/libkolab/lib/kolab_storage_dav_cache.php
@@ -1,745 +1,747 @@
 <?php
 
 /**
  * Kolab storage cache class providing a local caching layer for Kolab groupware objects.
  *
  * @author Aleksander Machniak <machniak@apheleia-it.ch>
  *
  * Copyright (C) 2012-2022, Apheleia IT AG <contact@apheleia-it.ch>
  *
  * 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_dav_cache extends kolab_storage_cache
 {
     /**
      * Factory constructor
      */
     public static function factory(kolab_storage_folder $storage_folder)
     {
         $subclass = 'kolab_storage_dav_cache_' . $storage_folder->type;
         if (class_exists($subclass)) {
             return new $subclass($storage_folder);
         }
 
         rcube::raise_error(
             ['code' => 900, 'message' => "No {$subclass} class found for folder '{$storage_folder->name}'"],
             true
         );
 
         return new kolab_storage_dav_cache($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 (!$this->folder->valid) {
             $this->ready = false;
             return;
         }
 
         // compose fully qualified ressource uri for this instance
         $this->resource_uri = $this->folder->get_resource_uri();
         $this->cache_table = $this->db->table_name('kolab_cache_dav_' . $this->folder->type);
         $this->ready = true;
     }
 
     /**
      * Synchronize local cache data with remote
      */
     public function synchronize()
     {
         // only sync once per request cycle
         if ($this->synched) {
             return;
         }
 
         $this->sync_start = time();
 
         // read cached folder metadata
         $this->_read_folder_data();
 
         $ctag = $this->folder->get_ctag();
 
         // check cache status ($this->metadata is set in _read_folder_data())
         if (
             empty($this->metadata['ctag'])
             || empty($this->metadata['changed'])
             || $this->metadata['ctag'] !== $ctag
         ) {
             // lock synchronization for this folder and wait if already locked
             $this->_sync_lock();
 
             $result = $this->synchronize_worker();
 
             // update ctag value (will be written to database in _sync_unlock())
             if ($result) {
                 $this->metadata['ctag']    = $ctag;
                 $this->metadata['changed'] = date(self::DB_DATE_FORMAT, time());
             }
 
             // remove lock
             $this->_sync_unlock();
         }
 
         $this->synched = time();
     }
 
     /**
      * Perform cache synchronization
      */
     protected function synchronize_worker()
     {
         // get effective time limit we have for synchronization (~70% of the execution time)
         $time_limit = $this->_max_sync_lock_time() * 0.7;
 
         if (time() - $this->sync_start > $time_limit) {
             return false;
         }
 
         // TODO: Implement synchronization with use of WebDAV-Sync (RFC 6578)
 
         // Get the objects from the DAV server
         $dav_index = $this->folder->dav->getIndex($this->folder->href, $this->folder->get_dav_type());
 
         if (!is_array($dav_index)) {
             rcube::raise_error([
                     'code' => 900,
                     'message' => "Failed to sync the kolab cache for {$this->folder->href}"
                 ], true);
             return false;
         }
 
         // WARNING: For now we assume object's href is <calendar-href>/<uid>.ics,
         //          which would mean there are no duplicates (objects with the same uid).
         //          With DAV protocol we can't get UID without fetching the whole object.
         //          Also the folder_id + uid is a unique index in the database.
         //          In the future we maybe should store the href in database.
 
         // Determine objects to fetch or delete
         $new_index    = [];
         $update_index = [];
         $old_index    = $this->folder_index(); // uid -> etag
         $chunk_size   = 20; // max numer of objects per DAV request
 
         foreach ($dav_index as $object) {
             $uid = $object['uid'];
             if (isset($old_index[$uid])) {
                 $old_etag = $old_index[$uid];
                 $old_index[$uid] = null;
 
                 if ($old_etag === $object['etag']) {
                     // the object didn't change
                     continue;
                 }
 
                 $update_index[$uid] = $object['href'];
             }
             else {
                 $new_index[$uid] = $object['href'];
             }
         }
 
         $i = 0;
 
         // Fetch new objects and store in DB
         if (!empty($new_index)) {
             foreach (array_chunk($new_index, $chunk_size, true) as $chunk) {
                 $objects = $this->folder->dav->getData($this->folder->href, $this->folder->get_dav_type(), $chunk);
 
                 if (!is_array($objects)) {
                     rcube::raise_error([
                             'code' => 900,
                             'message' => "Failed to sync the kolab cache for {$this->folder->href}"
                         ], true);
                     return false;
                 }
 
                 foreach ($objects as $dav_object) {
                     if ($object = $this->folder->from_dav($dav_object)) {
                         $object['_raw'] = $dav_object['data'];
                         $this->_extended_insert(false, $object);
                         unset($object['_raw']);
                     }
                 }
 
                 $this->_extended_insert(true, null);
 
                 // check time limit and abort sync if running too long
                 if (++$i % 25 == 0 && time() - $this->sync_start > $time_limit) {
                     return false;
                 }
             }
         }
 
         // Fetch updated objects and store in DB
         if (!empty($update_index)) {
             foreach (array_chunk($update_index, $chunk_size, true) as $chunk) {
                 $objects = $this->folder->dav->getData($this->folder->href, $this->folder->get_dav_type(), $chunk);
 
                 if (!is_array($objects)) {
                     rcube::raise_error([
                             'code' => 900,
                             'message' => "Failed to sync the kolab cache for {$this->folder->href}"
                         ], true);
                     return false;
                 }
 
                 foreach ($objects as $dav_object) {
                     if ($object = $this->folder->from_dav($dav_object)) {
                         $object['_raw'] = $dav_object['data'];
                         $this->save($object, $object['uid']);
                         unset($object['_raw']);
                     }
                 }
 
                 // check time limit and abort sync if running too long
                 if (++$i % 25 == 0 && time() - $this->sync_start > $time_limit) {
                     return false;
                 }
             }
         }
 
         // Remove deleted objects
         $old_index = array_filter($old_index);
         if (!empty($old_index)) {
             $quoted_uids = join(',', array_map(array($this->db, 'quote'), $old_index));
             $this->db->query(
                 "DELETE FROM `{$this->cache_table}` WHERE `folder_id` = ? AND `uid` IN ($quoted_uids)",
                 $this->folder_id
             );
         }
 
         return true;
     }
 
     /**
      * Return current folder index (uid -> etag)
      */
     public function folder_index()
     {
         $this->_read_folder_data();
 
         // read cache index
         $sql_result = $this->db->query(
             "SELECT `uid`, `etag` FROM `{$this->cache_table}` WHERE `folder_id` = ?",
             $this->folder_id
         );
 
         $index = [];
 
         while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
             $index[$sql_arr['uid']] = $sql_arr['etag'];
         }
 
         return $index;
     }
 
     /**
      * Read a single entry from cache or from server directly
      *
      * @param string Object UID
      * @param string Object type to read
      * @param string Unused (kept for compat. with the parent class)
      *
      * @return null|array An array of objects, NULL if not found
      */
     public function get($uid, $type = null, $unused = null)
     {
         if ($this->ready) {
             $this->_read_folder_data();
 
             $sql_result = $this->db->query(
                 "SELECT * FROM `{$this->cache_table}` WHERE `folder_id` = ? AND `uid` = ?",
                 $this->folder_id,
                 $uid
             );
 
             if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
                 $object = $this->_unserialize($sql_arr);
             }
         }
 
         // fetch from DAV if not present in cache
         if (empty($object)) {
             if ($object = $this->folder->read_object($uid, $type ?: '*')) {
                 $this->save($object);
             }
         }
 
         return $object ?: null;
     }
 
     /**
      * Read multiple entries from the server directly
      *
      * @param array Object UIDs
      *
      * @return false|array An array of objects, False on error
      */
     public function multiget($uids)
     {
         return $this->folder->read_objects($uids);
     }
 
     /**
      * Insert/Update a cache entry
      *
      * @param string      Object UID
      * @param array|false Hash array with object properties to save or false to delete the cache entry
      * @param string      Unused (kept for compat. with the parent class)
      */
     public function set($uid, $object, $unused = null)
     {
         // remove old entry
         if ($this->ready) {
             $this->_read_folder_data();
 
             $this->db->query(
                 "DELETE FROM `{$this->cache_table}` WHERE `folder_id` = ? AND `uid` = ?",
                 $this->folder_id,
                 $uid
             );
         }
 
         if ($object) {
             $this->save($object);
         }
     }
 
     /**
      * Insert (or update) a cache entry
      *
      * @param mixed  Hash array with object properties to save or false to delete the cache entry
      * @param string Optional old message UID (for update)
      * @param string Unused (kept for compat. with the parent class)
      */
     public function save($object, $olduid = null, $unused = null)
     {
         // write to cache
         if ($this->ready) {
             $this->_read_folder_data();
 
             $sql_data              = $this->_serialize($object);
             $sql_data['folder_id'] = $this->folder_id;
             $sql_data['uid']       = rcube_charset::clean($object['uid']);
             $sql_data['etag']      = rcube_charset::clean($object['etag']);
 
             $args = [];
             $cols = ['folder_id', 'uid', 'etag', 'changed', 'data', 'tags', 'words'];
             $cols = array_merge($cols, $this->extra_cols);
 
             foreach ($cols as $idx => $col) {
                 $cols[$idx] = $this->db->quote_identifier($col);
                 $args[]     = $sql_data[$col];
             }
 
             if ($olduid) {
                 foreach ($cols as $idx => $col) {
                     $cols[$idx] = "$col = ?";
                 }
 
                 $query = "UPDATE `{$this->cache_table}` SET " . implode(', ', $cols)
                     . " WHERE `folder_id` = ? AND `uid` = ?";
                 $args[] = $this->folder_id;
                 $args[] = $olduid;
             }
             else {
                 $query = "INSERT INTO `{$this->cache_table}` (`created`, " . implode(', ', $cols)
                     . ") VALUES (" . $this->db->now() . str_repeat(', ?', count($cols)) . ")";
             }
 
             $result = $this->db->query($query, $args);
 
             if (!$this->db->affected_rows($result)) {
                 rcube::raise_error([
                     'code' => 900,
                     'message' => "Failed to write to kolab cache"
                 ], true);
             }
         }
     }
 
     /**
      * Move an existing cache entry to a new resource
      *
      * @param string               Entry's  UID
      * @param kolab_storage_folder Target storage folder instance
      * @param string Unused (kept for compat. with the parent class)
      * @param string Unused (kept for compat. with the parent class)
      */
     public function move($uid, $target, $unused1 = null, $unused2 = null)
     {
         // TODO
     }
 
     /**
      * Update resource URI for existing folder
      *
      * @param string Target DAV folder to move it to
      */
     public function rename($new_folder)
     {
         // TODO
     }
 
     /**
      * Select Kolab objects filtered by the given query
      *
      * @param array Pseudo-SQL query as list of filter parameter triplets
      *   triplet: ['<colname>', '<comparator>', '<value>']
      * @param bool  Set true to only return UIDs instead of complete objects
      * @param bool  Use fast mode to fetch only minimal set of information
      *              (no xml fetching and parsing, etc.)
      *
      * @return array|null|kolab_storage_dataset List of Kolab data objects (each represented as hash array) or UIDs
      */
     public function select($query = [], $uids = false, $fast = false)
     {
         $result = $uids ? [] : new kolab_storage_dataset($this);
         $count  = null;
 
         $this->_read_folder_data();
 
         // fetch full object data on one query if a small result set is expected
         $fetchall = !$uids && ($this->limit ? $this->limit[0] : ($count = $this->count($query))) < self::MAX_RECORDS;
 
         // skip SELECT if we know it will return nothing
         if ($count === 0) {
             return $result;
         }
 
         $sql_query = "SELECT " . ($fetchall ? '*' : "`uid`")
             . " FROM `{$this->cache_table}` WHERE `folder_id` = ?"
             . $this->_sql_where($query)
             . (!empty($this->order_by) ? " ORDER BY " . $this->order_by : '');
 
         $sql_result = $this->limit ?
             $this->db->limitquery($sql_query, $this->limit[1], $this->limit[0], $this->folder_id) :
             $this->db->query($sql_query, $this->folder_id);
 
         if ($this->db->is_error($sql_result)) {
             if ($uids) {
                 return null;
             }
 
             $result->set_error(true);
             return $result;
         }
 
         while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
             if ($uids) {
                 $result[] = $sql_arr['uid'];
             }
             else if (!$fetchall) {
                 $result[] = $sql_arr;
             }
             else if (($object = $this->_unserialize($sql_arr, true, $fast))) {
                 $result[] = $object;
             }
             else {
                 $result[] = $sql_arr['uid'];
             }
         }
 
         return $result;
     }
 
     /**
      * Get number of objects mathing the given query
      *
      * @param array $query Pseudo-SQL query as list of filter parameter triplets
      *
      * @return int The number of objects of the given type
      */
     public function count($query = [])
     {
         // read from local cache DB (assume it to be synchronized)
         $this->_read_folder_data();
 
         $sql_result = $this->db->query(
             "SELECT COUNT(*) AS `numrows` FROM `{$this->cache_table}` ".
             "WHERE `folder_id` = ?" . $this->_sql_where($query),
             $this->folder_id
         );
 
         if ($this->db->is_error($sql_result)) {
             return null;
         }
 
         $sql_arr = $this->db->fetch_assoc($sql_result);
         $count   = intval($sql_arr['numrows']);
 
         return $count;
     }
 
     /**
      * Getter for a single Kolab object identified by its UID
      *
      * @param string $uid Object UID
      *
      * @return array|null The Kolab object represented as hash array
      */
     public function get_by_uid($uid)
     {
         $old_limit = $this->limit;
 
         // set limit to skip count query
         $this->limit = [1, 0];
 
         $list = $this->select([['uid', '=', $uid]]);
 
         // set the limit back to defined value
         $this->limit = $old_limit;
 
         if (!empty($list) && !empty($list[0])) {
             return $list[0];
         }
     }
 
     /**
      * Write records into cache using extended inserts to reduce the number of queries to be executed
      *
      * @param bool  Set to false to commit buffered insert, true to force an insert
      * @param array Kolab object to cache
      */
     protected function _extended_insert($force, $object)
     {
         static $buffer = '';
 
         $line = '';
         $cols = ['folder_id', 'uid', 'etag', 'created', 'changed', 'data', 'tags', 'words'];
         if ($this->extra_cols) {
             $cols = array_merge($cols, $this->extra_cols);
         }
 
         if ($object) {
             $sql_data = $this->_serialize($object);
 
-            // Skip multi-folder insert for all databases but MySQL
-            // In Oracle we can't put long data inline, others we don't support yet
-            if (strpos($this->db->db_provider, 'mysql') !== 0) {
+            // Skip multi-folder insert for all databases but MySQL and Postgres
+            if (!preg_match('/^(mysql|postgres)/', $this->db->db_provider)) {
                 $extra_args = [];
                 $params = [
                     $this->folder_id,
                     rcube_charset::clean($object['uid']),
                     rcube_charset::clean($object['etag']),
                     $sql_data['changed'],
                     $sql_data['data'],
                     $sql_data['tags'],
                     $sql_data['words']
                 ];
 
                 foreach ($this->extra_cols as $col) {
                     $params[] = $sql_data[$col];
                     $extra_args[] = '?';
                 }
 
                 $cols = implode(', ', array_map(function($n) { return "`{$n}`"; }, $cols));
                 $extra_args = count($extra_args) ? ', ' . implode(', ', $extra_args) : '';
 
                 $result = $this->db->query(
                     "INSERT INTO `{$this->cache_table}` ($cols)"
                     . " VALUES (?, ?, ?, " . $this->db->now() . ", ?, ?, ?, ?$extra_args)",
                     $params
                 );
 
                 if (!$this->db->affected_rows($result)) {
                     rcube::raise_error(array(
                         'code' => 900, 'message' => "Failed to write to kolab cache"
                     ), true);
                 }
 
                 return;
             }
 
             $values = array(
                 $this->db->quote($this->folder_id),
                 $this->db->quote(rcube_charset::clean($object['uid'])),
                 $this->db->quote(rcube_charset::clean($object['etag'])),
                 !empty($sql_data['created']) ? $this->db->quote($sql_data['created']) : $this->db->now(),
                 $this->db->quote($sql_data['changed']),
                 $this->db->quote($sql_data['data']),
                 $this->db->quote($sql_data['tags']),
                 $this->db->quote($sql_data['words']),
             );
             foreach ($this->extra_cols as $col) {
                 $values[] = $this->db->quote($sql_data[$col]);
             }
             $line = '(' . join(',', $values) . ')';
         }
 
         if ($buffer && ($force || (strlen($buffer) + strlen($line) > $this->max_sql_packet()))) {
             $columns = implode(', ', array_map(function($n) { return "`{$n}`"; }, $cols));
-            $update  = implode(', ', array_map(function($i) { return "`{$i}` = VALUES(`{$i}`)"; }, array_slice($cols, 2)));
 
-            $result = $this->db->query(
-                "INSERT INTO `{$this->cache_table}` ($columns) VALUES $buffer"
-                . " ON DUPLICATE KEY UPDATE $update"
-            );
+            if ($this->db->db_provider == 'postgres') {
+                $update = "ON CONFLICT (folder_id, uid) DO UPDATE SET "
+                    . implode(', ', array_map(function($i) { return "`{$i}` = EXCLUDED.`{$i}`"; }, array_slice($cols, 2)));
+            }
+            else {
+                $update = "ON DUPLICATE KEY UPDATE "
+                    . implode(', ', array_map(function($i) { return "`{$i}` = VALUES(`{$i}`)"; }, array_slice($cols, 2)));
+            }
+
+            $result = $this->db->query("INSERT INTO `{$this->cache_table}` ($columns) VALUES $buffer $update");
 
             if (!$this->db->affected_rows($result)) {
-                rcube::raise_error(array(
-                    'code' => 900, 'message' => "Failed to write to kolab cache"
-                ), true);
+                rcube::raise_error(['code' => 900, 'message' => "Failed to write to kolab cache"], true);
             }
 
             $buffer = '';
         }
 
         $buffer .= ($buffer ? ',' : '') . $line;
     }
 
     /**
      * Helper method to convert the given Kolab object into a dataset to be written to cache
      */
     protected function _serialize($object)
     {
         static $threshold;
 
         if ($threshold === null) {
             $rcube     = rcube::get_instance();
             $threshold = parse_bytes(rcube::get_instance()->config->get('dav_cache_threshold', 0));
         }
 
         $data     = [];
         $sql_data = ['created' => date(self::DB_DATE_FORMAT), 'changed' => null, 'tags' => '', 'words' => ''];
 
         if (!empty($object['changed'])) {
             $sql_data['changed'] = self::_convert_datetime($object['changed']);
         }
         if (!empty($object['created'])) {
             $sql_data['created'] = self::_convert_datetime($object['created']);
         }
 
         // Store only minimal set of object properties
         foreach ($this->data_props as $prop) {
             if (isset($object[$prop])) {
                 $data[$prop] = $object[$prop];
                 if ($data[$prop] instanceof DateTimeInterface) {
                     $data[$prop] = array(
                         'cl' => 'DateTime',
                         'dt' => $data[$prop]->format('Y-m-d H:i:s'),
                         'tz' => $data[$prop]->getTimezone()->getName(),
                     );
                 }
             }
         }
 
         if (!empty($object['_raw']) && $threshold > 0 && strlen($object['_raw']) <= $threshold) {
             $data['_raw'] = $object['_raw'];
         }
 
         $sql_data['data'] = json_encode(rcube_charset::clean($data));
 
         return $sql_data;
     }
 
     /**
      * Helper method to turn stored cache data into a valid storage object
      */
     protected function _unserialize($sql_arr, $noread = false, $fast_mode = false)
     {
         $init = function(&$object) use ($sql_arr) {
             if ($sql_arr['created'] && empty($object['created'])) {
                 $object['created'] = new DateTime($sql_arr['created'], $this->server_timezone);
             }
 
             if ($sql_arr['changed'] && empty($object['changed'])) {
                 $object['changed'] = new DateTime($sql_arr['changed'], $this->server_timezone);
             }
 
             $object['_type'] = !empty($sql_arr['type']) ? $sql_arr['type'] : $this->folder->type;
             $object['uid']   = $sql_arr['uid'];
             $object['etag']  = $sql_arr['etag'];
         };
 
         if (!empty($sql_arr['data']) && ($object = json_decode($sql_arr['data'], true))) {
             foreach ($this->data_props as $prop) {
                 if (isset($object[$prop]) && is_array($object[$prop])
                     && isset($object[$prop]['cl']) && $object[$prop]['cl'] == 'DateTime'
                 ) {
                     $object[$prop] = new DateTime($object[$prop]['dt'], new DateTimeZone($object[$prop]['tz']));
                 }
                 else if (!isset($object[$prop]) && isset($sql_arr[$prop])) {
                     $object[$prop] = $sql_arr[$prop];
                 }
             }
 
             $init($object);
         }
 
         if (!empty($fast_mode) && !empty($object)) {
             unset($object['_raw']);
         }
         else if ($noread) {
             // We have the raw content already, parse it
             if (!empty($object['_raw'])) {
                 $object['data'] = $object['_raw'];
                 if ($object = $this->folder->from_dav($object)) {
                     $init($object);
                     return $object;
                 }
             }
 
             return null;
         }
         else {
             // Fetch a complete object from the server
             $object = $this->folder->read_object($sql_arr['uid'], '*');
         }
 
         return $object;
     }
 
     /**
      * Read this folder's ID and cache metadata
      */
     protected function _read_folder_data()
     {
         // already done
         if (!empty($this->folder_id) || !$this->ready) {
             return;
         }
 
         // Different than in Kolab XML-based storage, in *DAV folders can
         // contain different types of data, e.g. Calendar can store events and tasks.
         // Therefore we both `resource` and `type` in WHERE.
 
         $sql_arr = $this->db->fetch_assoc($this->db->query(
                 "SELECT `folder_id`, `synclock`, `ctag`, `changed` FROM `{$this->folders_table}`"
                 . " WHERE `resource` = ? AND `type` = ?",
                 $this->resource_uri,
                 $this->folder->type
         ));
 
         if ($sql_arr) {
             $this->folder_id = $sql_arr['folder_id'];
             $this->metadata  = $sql_arr;
         }
         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  = [];
         }
     }
 }