diff --git a/plugins/kolab_tags/README b/plugins/kolab_tags/README
new file mode 100644
--- /dev/null
+++ b/plugins/kolab_tags/README
@@ -0,0 +1,69 @@
+A mail tags module for Roundcube
+--------------------------------------
+
+This plugin currently supports a local database or a Kolab groupware
+server as backends for tags storage.
+
+
+REQUIREMENTS
+------------
+
+Some functions are shared with other plugins and therefore being moved to
+library plugins. Thus in order to run the kolab_tags plugin, you also need the
+following plugins installed:
+
+* kolab/libkolab [1]
+
+
+INSTALLATION
+------------
+
+For a manual installation of the plugin (and its dependencies),
+execute the following steps. This will set it up with the database backend
+driver.
+
+1. Get the source from git
+
+  $ cd /tmp
+  $ git clone https://git.kolab.org/diffusion/RPK/roundcubemail-plugins-kolab.git
+  $ cd /<path-to-roundcube>/plugins
+  $ cp -r /tmp/roundcubemail-plugins-kolab/plugins/kolab_tags .
+  $ cp -r /tmp/roundcubemail-plugins-kolab/plugins/libkolab .
+
+2. Create kolab_tags plugin configuration
+
+  $ cd kolab_tags/
+  $ cp config.inc.php.dist config.inc.php
+  $ edit config.inc.php
+
+3. Initialize the plugin database tables
+
+  $ cd ../../
+  $ bin/initdb.sh --dir=plugins/kolab_tags/drivers/database/SQL
+
+4. Build css styles for the Elastic skin (if needed)
+
+  $ lessc --relative-urls -x plugins/libkolab/skins/elastic/libkolab.less > plugins/libkolab/skins/elastic/libkolab.min.css
+
+5. Enable the plugin
+
+  $ edit config/config.inc.php
+
+Add 'kolab_tags' to the list of active plugins:
+
+  $config['plugins'] = [
+    (...)
+    'kolab_tags',
+  ];
+
+
+IMPORTANT
+---------
+
+This plugin doesn't work with the Classic skin of Roundcube because no
+templates are available for that skin.
+
+Use Roundcube `skins_allowed` option to limit skins available to the user
+or remove incompatible skins from the skins folder.
+
+[1] https://git.kolab.org/diffusion/RPK/
diff --git a/plugins/kolab_tags/composer.json b/plugins/kolab_tags/composer.json
--- a/plugins/kolab_tags/composer.json
+++ b/plugins/kolab_tags/composer.json
@@ -4,7 +4,7 @@
     "description": "Email tags plugin",
     "homepage": "https://git.kolab.org/diffusion/RPK/",
     "license": "AGPLv3",
-    "version": "3.5.2",
+    "version": "3.5.3",
     "authors": [
         {
             "name": "Aleksander Machniak",
diff --git a/plugins/kolab_tags/config.inc.php.dist b/plugins/kolab_tags/config.inc.php.dist
new file mode 100644
--- /dev/null
+++ b/plugins/kolab_tags/config.inc.php.dist
@@ -0,0 +1,4 @@
+<?php
+
+// Storage backend type (database, kolab)
+$config['kolab_tags_driver'] = 'kolab';
diff --git a/plugins/kolab_tags/drivers/DriverInterface.php b/plugins/kolab_tags/drivers/DriverInterface.php
new file mode 100644
--- /dev/null
+++ b/plugins/kolab_tags/drivers/DriverInterface.php
@@ -0,0 +1,73 @@
+<?php
+
+/**
+ * Kolab Tags backend driver interface
+ *
+ * @author Aleksander Machniak <machniak@kolabsys.com>
+ *
+ * Copyright (C) 2024, 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/>.
+ */
+
+namespace KolabTags\Drivers;
+
+interface DriverInterface
+{
+    /**
+     * Tags list
+     *
+     * @param array $filter Search filter
+     *
+     * @return array List of tags
+     */
+    public function list_tags($filter = []);
+
+    /**
+     * Create tag object
+     *
+     * @param array $tag Tag data
+     *
+     * @return false|array Tag data on success, False on failure
+     */
+    public function create($tag);
+
+    /**
+     * Update tag object
+     *
+     * @param array $tag Tag data
+     *
+     * @return false|array Tag data on success, False on failure
+     */
+    public function update($tag);
+
+    /**
+     * Remove tag object
+     *
+     * @param string $uid Object unique identifier
+     *
+     * @return bool True on success, False on failure
+     */
+    public function remove($uid);
+
+    /**
+     * Resolve members to folder/UID
+     *
+     * @param array $tag   Tag object
+     * @param bool  $force Force members list update
+     *
+     * @return array Folder/UID list
+     */
+    public function get_tag_messages(&$tag, $force = true);
+}
diff --git a/plugins/kolab_tags/drivers/database/Driver.php b/plugins/kolab_tags/drivers/database/Driver.php
new file mode 100644
--- /dev/null
+++ b/plugins/kolab_tags/drivers/database/Driver.php
@@ -0,0 +1,254 @@
+<?php
+
+/**
+ * Kolab Tags backend driver for SQL database.
+ *
+ * @author Aleksander Machniak <machniak@apheleia-it.ch>
+ *
+ * Copyright (C) 2024, 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/>.
+ */
+
+namespace KolabTags\Drivers\Database;
+
+use KolabTags\Drivers\DriverInterface;
+use kolab_storage_cache;
+use kolab_storage_config;
+use rcube;
+
+class Driver implements DriverInterface
+{
+    private $members_table = 'kolab_tag_members';
+    private $tags_table = 'kolab_tags';
+    private $tag_cols = ['name', 'color'];
+
+
+    /**
+     * Tags list
+     *
+     * @param array $filter Search filter
+     *
+     * @return array List of tags
+     */
+    public function list_tags($filter = [])
+    {
+        $rcube = rcube::get_instance();
+        $db = $rcube->get_dbh();
+        $user_id = $rcube->get_user_id();
+
+        // Parse filter, convert 'uid' into 'id'
+        foreach ($filter as $idx => $record) {
+            if ($record[0] == 'uid') {
+                $filter[$idx][0] = 'id';
+            }
+        }
+
+        // TODO: Support 'members' filter
+
+        $where = kolab_storage_cache::sql_where($filter);
+
+        $query = $db->query("SELECT * FROM `{$this->tags_table}` WHERE `user_id` = {$user_id}" . $where);
+
+        $result = [];
+        while ($tag = $db->fetch_assoc($query)) {
+            $tag['uid'] = $tag['id']; // the API expects 'uid' property
+            $tag['members'] = [];
+            unset($tag['id']);
+            $result[$tag['uid']] = $tag;
+        }
+
+        // Get the tag's members
+        if (!empty($result)) {
+            $ids = $db->array2list(array_keys($result), 'int');
+            $query = $db->query("SELECT `tag_id`, `url` FROM `{$this->members_table}` WHERE `tag_id` IN ({$ids})");
+
+            while ($member = $db->fetch_assoc($query)) {
+                $result[$member['tag_id']]['members'][] = $member['url'];
+            }
+        }
+
+        return array_values($result);
+    }
+
+    /**
+     * Create tag object
+     *
+     * @param array $tag Tag data
+     *
+     * @return false|array Tag data on success, False on failure
+     */
+    public function create($tag)
+    {
+        $rcube = rcube::get_instance();
+        $db = $rcube->get_dbh();
+        $user_id = $rcube->get_user_id();
+        $insert = [];
+
+        foreach ($this->tag_cols as $col) {
+            if (isset($tag[$col])) {
+                $insert[$db->quoteIdentifier($col)] = $db->quote($tag[$col]);
+            }
+        }
+
+        if (empty($insert)) {
+            return false;
+        }
+
+        $now = new \DateTime('now', new \DateTimeZone('UTC'));
+
+        $insert['user_id'] = $user_id;
+        $insert['created'] = $insert['updated'] = $now->format("'Y-m-d H:i:s'");
+
+        $result = $db->query("INSERT INTO `{$this->tags_table}`"
+            . " (" . implode(', ', array_keys($insert)) . ")"
+            . " VALUES(" . implode(', ', array_values($insert)) . ")"
+        );
+
+        $tag['uid'] = $db->insert_id($this->tags_table);
+
+        if (empty($tag['uid'])) {
+            return false;
+        }
+
+        $this->save_members($tag, false);
+
+        return $tag;
+    }
+
+    /**
+     * Update tag object
+     *
+     * @param array $tag Tag data
+     *
+     * @return false|array Tag data on success, False on failure
+     */
+    public function update($tag)
+    {
+        $rcube = rcube::get_instance();
+        $db = $rcube->get_dbh();
+        $user_id = $rcube->get_user_id();
+        $update = [];
+
+        foreach ($this->tag_cols as $col) {
+            if (isset($tag[$col])) {
+                $update[] = $db->quoteIdentifier($col) . ' = ' . $db->quote($tag[$col]);
+            }
+        }
+
+        if (!empty($update)) {
+            $now = new \DateTime('now', new \DateTimeZone('UTC'));
+            $update[] = '`updated` = ' . $db->quote($now->format('Y-m-d H:i:s'));
+
+            $result = $db->query("UPDATE `{$this->tags_table}` SET " . implode(', ', $update)
+                . " WHERE `id` = ? AND `user_id` = ?", $tag['uid'], $user_id);
+
+            if ($result === false) {
+                return false;
+            }
+        }
+
+        // Update members
+        $this->save_members($tag);
+
+        return $tag;
+    }
+
+    /**
+     * Remove tag object
+     *
+     * @param string $uid Object unique identifier
+     *
+     * @return bool True on success, False on failure
+     */
+    public function remove($uid)
+    {
+        $rcube = rcube::get_instance();
+        $db = $rcube->get_dbh();
+        $user_id = $rcube->get_user_id();
+
+        $result = $db->query("DELETE FROM `{$this->tags_table}` WHERE `id` = ? AND `user_id` = ?", $uid, $user_id);
+
+        return $db->affected_rows($result) > 0;
+    }
+
+    /**
+     * Resolve members to folder/UID
+     *
+     * @param array $tag   Tag object
+     * @param bool  $force Force members list update
+     *
+     * @return array Folder/UID list
+     */
+    public function get_tag_messages(&$tag, $force = true)
+    {
+        $result = kolab_storage_config::resolve_members($tag, $force, false);
+
+        if ($force) {
+            // Update tag members
+            $this->save_members($tag);
+        }
+
+        return $result;
+    }
+
+    protected function save_members($tag, $update = true)
+    {
+        if (empty($tag['uid']) || !isset($tag['members'])) {
+            return;
+        }
+
+        $rcube = rcube::get_instance();
+        $db = $rcube->get_dbh();
+        $existing = [];
+
+        if ($update) {
+            $query = $db->query("SELECT `url` FROM `{$this->members_table}` WHERE `tag_id` = ?", $tag['uid']);
+
+            while ($member = $db->fetch_assoc($query)) {
+                $existing[] = $member['url'];
+            }
+        }
+
+        if (!empty($existing)) {
+            $insert = array_diff($tag['members'], $existing);
+            $delete = array_diff($existing, $tag['members']);
+        } else {
+            $insert = $tag['members'];
+            $delete = [];
+        }
+
+        if (!empty($delete)) {
+            foreach (array_chunk($delete, 100) as $chunk) {
+                $query = $db->query("DELETE FROM `{$this->members_table}` WHERE `tag_id` = ?"
+                    . " AND `url` IN (" . $db->array2list($chunk) . ")",
+                    $tag['uid']
+                );
+            }
+        }
+
+        if (!empty($insert)) {
+            $ts = (new \DateTime('now', new \DateTimeZone('UTC')))->format('Y-m-d H:i:s');
+
+            foreach (array_chunk($insert, 100) as $chunk) {
+                $query = "INSERT INTO `{$this->members_table}` (`tag_id`, `url`, `created`) VALUES ";
+                foreach ($chunk as $idx => $url) {
+                    $chunk[$idx] = sprintf("(%d, %s, %s)", $tag['uid'], $db->quote($url), $db->quote($ts));
+                }
+
+                $query = $db->query($query . implode(', ', $chunk));
+            }
+        }
+    }
+}
diff --git a/plugins/kolab_tags/drivers/database/SQL/mysql.initial.sql b/plugins/kolab_tags/drivers/database/SQL/mysql.initial.sql
new file mode 100644
--- /dev/null
+++ b/plugins/kolab_tags/drivers/database/SQL/mysql.initial.sql
@@ -0,0 +1,26 @@
+
+CREATE TABLE IF NOT EXISTS `kolab_tags` (
+  `id` int UNSIGNED NOT NULL AUTO_INCREMENT,
+  `user_id` int(10) UNSIGNED NOT NULL,
+  `name` varchar(255) NOT NULL,
+  `color` varchar(8) DEFAULT NULL,
+  `created` datetime DEFAULT NULL,
+  `updated` datetime DEFAULT NULL,
+  PRIMARY KEY(`id`),
+  UNIQUE KEY `user_id_name_idx` (`user_id`, `name`),
+  INDEX (`updated`),
+  CONSTRAINT `fk_kolab_tags_user_id` FOREIGN KEY (`user_id`)
+    REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE
+) ROW_FORMAT=DYNAMIC ENGINE=INNODB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+
+CREATE TABLE IF NOT EXISTS `kolab_tag_members` (
+  `tag_id` int UNSIGNED NOT NULL,
+  `url` varchar(2048) BINARY NOT NULL,
+  `created` datetime DEFAULT NULL,
+  PRIMARY KEY(`tag_id`, `url`),
+  INDEX (`created`),
+  CONSTRAINT `fk_kolab_tag_members_tag_id` FOREIGN KEY (`tag_id`)
+    REFERENCES `kolab_tags`(`id`) ON DELETE CASCADE ON UPDATE CASCADE
+) ROW_FORMAT=DYNAMIC ENGINE=INNODB CHARACTER SET ascii;
+
+REPLACE INTO `system` (`name`, `value`) VALUES ('kolab-tags-database-version', '2024112000');
diff --git a/plugins/kolab_tags/lib/kolab_tags_backend.php b/plugins/kolab_tags/drivers/kolab/Driver.php
rename from plugins/kolab_tags/lib/kolab_tags_backend.php
rename to plugins/kolab_tags/drivers/kolab/Driver.php
--- a/plugins/kolab_tags/lib/kolab_tags_backend.php
+++ b/plugins/kolab_tags/drivers/kolab/Driver.php
@@ -1,11 +1,11 @@
 <?php
 
 /**
- * Kolab Tags backend
+ * Kolab Tags backend driver for Kolab v3
  *
- * @author Aleksander Machniak <machniak@kolabsys.com>
+ * @author Aleksander Machniak <machniak@apheleia-it.ch>
  *
- * Copyright (C) 2014, Kolab Systems AG <contact@kolabsys.com>
+ * Copyright (C) 2024, 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
@@ -21,7 +21,12 @@
  * along with this program. If not, see <http://www.gnu.org/licenses/>.
  */
 
-class kolab_tags_backend
+namespace KolabTags\Drivers\Kolab;
+
+use KolabTags\Drivers\DriverInterface;
+use kolab_storage_config;
+
+class Driver implements DriverInterface
 {
     private $tag_cols = ['name', 'category', 'color', 'parent', 'iconName', 'priority', 'members'];
 
@@ -53,7 +58,7 @@
      *
      * @param array $tag Tag data
      *
-     * @return boolean|array Tag data on success, False on failure
+     * @return false|array Tag data on success, False on failure
      */
     public function create($tag)
     {
@@ -72,7 +77,7 @@
      *
      * @param array $tag Tag data
      *
-     * @return boolean|array Tag data on success, False on failure
+     * @return false|array Tag data on success, False on failure
      */
     public function update($tag)
     {
@@ -99,7 +104,7 @@
      *
      * @param string $uid Object unique identifier
      *
-     * @return boolean True on success, False on failure
+     * @return bool True on success, False on failure
      */
     public function remove($uid)
     {
@@ -107,4 +112,17 @@
 
         return $config->delete($uid);
     }
+
+    /**
+     * Resolve members to folder/UID
+     *
+     * @param array $tag   Tag object
+     * @param bool  $force Force members list update
+     *
+     * @return array Folder/UID list
+     */
+    public function get_tag_messages(&$tag, $force = true)
+    {
+        return kolab_storage_config::resolve_members($tag, $force);
+    }
 }
diff --git a/plugins/kolab_tags/kolab_tags.js b/plugins/kolab_tags/kolab_tags.js
--- a/plugins/kolab_tags/kolab_tags.js
+++ b/plugins/kolab_tags/kolab_tags.js
@@ -776,7 +776,6 @@
 {
     var selection = list.get_selection(),
         has_tags = selection.length && rcmail.env.tags.length;
-
     if (has_tags && !rcmail.select_all_mode) {
         has_tags = false;
         $.each(selection, function() {
diff --git a/plugins/kolab_tags/kolab_tags.php b/plugins/kolab_tags/kolab_tags.php
--- a/plugins/kolab_tags/kolab_tags.php
+++ b/plugins/kolab_tags/kolab_tags.php
@@ -57,12 +57,12 @@
     private function engine()
     {
         if ($this->engine === null) {
-            // the files module can be enabled/disabled by the kolab_auth plugin
+            // the plugin can be enabled/disabled by the kolab_auth plugin
             if ($this->rc->config->get('kolab_tags_disabled') || !$this->rc->config->get('kolab_tags_enabled', true)) {
                 return $this->engine = false;
             }
 
-            //            $this->load_config();
+            $this->load_config();
 
             require_once $this->home . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'kolab_tags_engine.php';
 
diff --git a/plugins/kolab_tags/lib/kolab_tags_engine.php b/plugins/kolab_tags/lib/kolab_tags_engine.php
--- a/plugins/kolab_tags/lib/kolab_tags_engine.php
+++ b/plugins/kolab_tags/lib/kolab_tags_engine.php
@@ -35,11 +35,16 @@
     {
         $plugin->require_plugin('libkolab');
 
-        require_once $plugin->home . DIRECTORY_SEPARATOR . 'lib' . DIRECTORY_SEPARATOR . 'kolab_tags_backend.php';
-
-        $this->backend = new kolab_tags_backend();
         $this->plugin  = $plugin;
         $this->rc      = $plugin->rc;
+
+        $driver = $this->rc->config->get('kolab_tags_driver') ?: 'database';
+        $class = "\\KolabTags\\Drivers\\" . ucfirst($driver) . "\\Driver";
+
+        require_once "{$plugin->home}/drivers/DriverInterface.php";
+        require_once "{$plugin->home}/drivers/{$driver}/Driver.php";
+
+        $this->backend = new $class();
     }
 
     /**
@@ -497,24 +502,12 @@
         ];
 
         if ($list) {
-            $result['uids'] = $this->get_tag_messages($tag, $force);
+            $result['uids'] = $this->backend->get_tag_messages($tag, $force);
         }
 
         return $result;
     }
 
-    /**
-     * Resolve members to folder/UID
-     *
-     * @param array $tag Tag object
-     *
-     * @return array Folder/UID list
-     */
-    protected function get_tag_messages(&$tag, $force = true)
-    {
-        return kolab_storage_config::resolve_members($tag, $force);
-    }
-
     /**
      * Build array of member URIs from set of messages
      */
diff --git a/plugins/kolab_tags/skins/elastic/templates/ui.html b/plugins/kolab_tags/skins/elastic/templates/ui.html
--- a/plugins/kolab_tags/skins/elastic/templates/ui.html
+++ b/plugins/kolab_tags/skins/elastic/templates/ui.html
@@ -17,8 +17,8 @@
 <div id="tagmessagemenu" class="popupmenu" aria-hidden="true">
 	<ul class="menu iconized">
 		<li class="separator"><label><roundcube:label name="kolab_tags.tags" /></label></li>
-		<roundcube:button type="link-menuitem" command="tag-add" label="kolab_tags.tagadd" classAct="tag add active" class="tag add disabled" />
-		<roundcube:button type="link-menuitem" command="tag-remove" label="kolab_tags.tagremove" classAct="tag remove active" class="tag remove disabled" />
+		<roundcube:button type="link-menuitem" command="tag-add" label="kolab_tags.tagadd" classAct="tag add active" class="tag add disabled" innerclass="inner" aria-haspopup="true" />
+		<roundcube:button type="link-menuitem" command="tag-remove" label="kolab_tags.tagremove" classAct="tag remove active" class="tag remove disabled" innerclass="inner" aria-haspopup="true" />
 		<roundcube:button type="link-menuitem" command="tag-remove-all" label="kolab_tags.tagremoveall" classAct="tag remove all active" class="tag remove all disabled" />
 	</ul>
 </div>
diff --git a/plugins/libkolab/lib/kolab_storage_cache.php b/plugins/libkolab/lib/kolab_storage_cache.php
--- a/plugins/libkolab/lib/kolab_storage_cache.php
+++ b/plugins/libkolab/lib/kolab_storage_cache.php
@@ -777,7 +777,7 @@
 
             $sql_query = "SELECT " . ($fetchall ? '*' : "`msguid` AS `_msguid`, `uid`")
                 . " FROM `{$this->cache_table}` WHERE `folder_id` = ?"
-                . $this->_sql_where($query)
+                . self::sql_where($query)
                 . (!empty($this->order_by) ? " ORDER BY " . $this->order_by : '');
 
             $sql_result = $this->limit ?
@@ -866,7 +866,7 @@
 
             $sql_result = $this->db->query(
                 "SELECT COUNT(*) AS `numrows` FROM `{$this->cache_table}` " .
-                "WHERE `folder_id` = ?" . $this->_sql_where($query),
+                "WHERE `folder_id` = ?" . self::sql_where($query),
                 $this->folder_id
             );
 
@@ -946,40 +946,42 @@
     /**
      * Helper method to compose a valid SQL query from pseudo filter triplets
      */
-    protected function _sql_where($query)
+    public static function sql_where($query)
     {
+        $db = rcube::get_instance()->get_dbh();
         $sql_where = '';
+
         foreach ((array) $query as $param) {
             if (is_array($param[0])) {
                 $subq = [];
                 foreach ($param[0] as $q) {
-                    $subq[] = preg_replace('/^\s*AND\s+/i', '', $this->_sql_where([$q]));
+                    $subq[] = preg_replace('/^\s*AND\s+/i', '', self::sql_where([$q]));
                 }
                 if (!empty($subq)) {
                     $sql_where .= ' AND (' . implode($param[1] == 'OR' ? ' OR ' : ' AND ', $subq) . ')';
                 }
                 continue;
             } elseif ($param[1] == '=' && is_array($param[2])) {
-                $qvalue = '(' . implode(',', array_map([$this->db, 'quote'], $param[2])) . ')';
+                $qvalue = '(' . implode(',', array_map([$db, 'quote'], $param[2])) . ')';
                 $param[1] = 'IN';
             } elseif ($param[1] == '~' || $param[1] == 'LIKE' || $param[1] == '!~' || $param[1] == '!LIKE') {
                 $not = ($param[1] == '!~' || $param[1] == '!LIKE') ? 'NOT ' : '';
                 $param[1] = $not . 'LIKE';
-                $qvalue = $this->db->quote('%' . preg_replace('/(^\^|\$$)/', ' ', $param[2]) . '%');
+                $qvalue = $db->quote('%' . preg_replace('/(^\^|\$$)/', ' ', $param[2]) . '%');
             } elseif ($param[1] == '~*' || $param[1] == '!~*') {
                 $not = $param[1][1] == '!' ? 'NOT ' : '';
                 $param[1] = $not . 'LIKE';
-                $qvalue = $this->db->quote(preg_replace('/(^\^|\$$)/', ' ', $param[2]) . '%');
+                $qvalue = $db->quote(preg_replace('/(^\^|\$$)/', ' ', $param[2]) . '%');
             } elseif ($param[0] == 'tags') {
                 $param[1] = ($param[1] == '!=' ? 'NOT ' : '') . 'LIKE';
-                $qvalue = $this->db->quote('% ' . $param[2] . ' %');
+                $qvalue = $db->quote('% ' . $param[2] . ' %');
             } else {
-                $qvalue = $this->db->quote($param[2]);
+                $qvalue = $db->quote($param[2]);
             }
 
             $sql_where .= sprintf(
                 ' AND %s %s %s',
-                $this->db->quote_identifier($param[0]),
+                $db->quote_identifier($param[0]),
                 $param[1],
                 $qvalue
             );
diff --git a/plugins/libkolab/lib/kolab_storage_cache_configuration.php b/plugins/libkolab/lib/kolab_storage_cache_configuration.php
--- a/plugins/libkolab/lib/kolab_storage_cache_configuration.php
+++ b/plugins/libkolab/lib/kolab_storage_cache_configuration.php
@@ -80,7 +80,7 @@
     /**
      * Helper method to compose a valid SQL query from pseudo filter triplets
      */
-    protected function _sql_where($query)
+    public static function sql_where($query)
     {
         if (is_array($query)) {
             foreach ($query as $idx => $param) {
@@ -100,7 +100,7 @@
             }
         }
 
-        return parent::_sql_where($query);
+        return parent::sql_where($query);
     }
 
     /**
diff --git a/plugins/libkolab/lib/kolab_storage_config.php b/plugins/libkolab/lib/kolab_storage_config.php
--- a/plugins/libkolab/lib/kolab_storage_config.php
+++ b/plugins/libkolab/lib/kolab_storage_config.php
@@ -448,7 +448,7 @@
      *
      * @return array Folder/UIDs list
      */
-    public static function resolve_members(&$tag, $force = true)
+    public static function resolve_members(&$tag, $force = true, $update = true)
     {
         $result = [];
 
@@ -558,7 +558,9 @@
 
             // update tag object with new members list
             $tag['members'] = array_unique($tag['members']);
-            kolab_storage_config::get_instance()->save($tag, 'relation');
+            if ($update) {
+                kolab_storage_config::get_instance()->save($tag, 'relation');
+            }
         }
 
         return $result;
diff --git a/plugins/libkolab/lib/kolab_storage_dav_cache.php b/plugins/libkolab/lib/kolab_storage_dav_cache.php
--- a/plugins/libkolab/lib/kolab_storage_dav_cache.php
+++ b/plugins/libkolab/lib/kolab_storage_dav_cache.php
@@ -413,7 +413,7 @@
 
         $sql_query = "SELECT " . ($uids ? "`uid`" : '*')
             . " FROM `{$this->cache_table}` WHERE `folder_id` = ?"
-            . $this->_sql_where($query)
+            . self::sql_where($query)
             . (!empty($this->order_by) ? " ORDER BY " . $this->order_by : '');
 
         $sql_result = $this->limit ?
@@ -454,7 +454,7 @@
 
         $sql_result = $this->db->query(
             "SELECT COUNT(*) AS `numrows` FROM `{$this->cache_table}` " .
-            "WHERE `folder_id` = ?" . $this->_sql_where($query),
+            "WHERE `folder_id` = ?" . self::sql_where($query),
             $this->folder_id
         );