diff --git a/lib/drivers/kolabfiles/kolabfiles_file_storage.php b/lib/drivers/kolabfiles/kolabfiles_file_storage.php
new file mode 100644
--- /dev/null
+++ b/lib/drivers/kolabfiles/kolabfiles_file_storage.php
@@ -0,0 +1,1179 @@
+<?php
+/*
+ +--------------------------------------------------------------------------+
+ | This file is part of the Kolab File API                                  |
+ |                                                                          |
+ | Copyright (C) 2024, Apheleia It                                          |
+ |                                                                          |
+ | 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/>      |
+ +--------------------------------------------------------------------------+
+ | Author: Christian Mollekopf <mollekopf@apheleia-it.ch>                      |
+ +--------------------------------------------------------------------------+
+*/
+
+// FIXME
+// * Support non-toplevel folders for collection creation/moves/...
+// * Paging (we don't display beyond the first page (100 items))
+// * Searching is not implemented
+class kolabfiles_file_storage implements file_storage
+{
+    /**
+     * @var rcube
+     */
+    protected $rc;
+
+    protected $client = null;
+
+    /**
+     * @var array
+     */
+    protected $config = array();
+
+    /**
+     * List of Kolabfiles collections
+     *
+     * @var array
+     */
+    protected $collections;
+
+    /**
+     * Instance title (mount point)
+     *
+     * @var string
+     */
+    protected $title;
+
+
+    /**
+     * Class constructor
+     */
+    public function __construct()
+    {
+        $this->rc = rcube::get_instance();
+        $this->config = [
+            'baseuri' => $this->rc->config->get('fileapi_kolabfiles_baseuri', 'https://' . $_SERVER["HTTP_HOST"] . '/api/'),
+            'host' => $this->rc->config->get('fileapi_kolabfiles_host', $_SERVER["HTTP_HOST"]),
+        ];
+    }
+
+
+    protected function init()
+    {
+        if ($this->client !== null) {
+            return true;
+        }
+
+        $stack = new \GuzzleHttp\HandlerStack();
+        $stack->setHandler(\GuzzleHttp\choose_handler());
+        $stack->push(\GuzzleHttp\Middleware::retry(
+            function (
+                $retries,
+                \GuzzleHttp\Psr7\Request $request,
+                \GuzzleHttp\Psr7\Response $response = null,
+                \GuzzleHttp\Exception\RequestException $exception = null
+            ) {
+                $maxRetries = 2;
+
+                if ($retries >= $maxRetries) {
+                    return false;
+                }
+
+                if ($response && $response->getStatusCode() === 401) {
+                    $this->refreshClientAccessToken();
+                    return true;
+                }
+
+                return false;
+            }
+        ));
+
+        $stack->push(\GuzzleHttp\Middleware::mapRequest(function (\GuzzleHttp\Psr7\Request $request) {
+            //TODO Just forward the bearer token, once we manage to make sure it get's sent to roundcube
+            // 'Authorization' => rcube_utils::request_header('Authorization')
+            if ($accessToken = $this->getClientAccessToken()) {
+                return $request->withHeader('Authorization', 'Bearer ' . $accessToken);
+            }
+            return $request;
+        }));
+
+        $this->client = new \GuzzleHttp\Client(
+            [
+                'http_errors' => false, // No exceptions from Guzzle
+                'base_uri' => rtrim($this->config['baseuri'], '/') . '/',
+                'handler' => $stack,
+                'verify' => false,
+                'connect_timeout' => 10,
+                'timeout' => 10,
+                // 'on_stats' => function (\GuzzleHttp\TransferStats $stats) {
+                //     $threshold = \config('logging.slow_log');
+                //     if ($threshold && ($sec = $stats->getTransferTime()) > $threshold) {
+                //         $url = $stats->getEffectiveUri();
+                //         $method = $stats->getRequest()->getMethod();
+                //         \Log::warning(sprintf("[STATS] %s %s: %.4f sec.", $method, $url, $sec));
+                //     }
+                // },
+            ]
+        );
+    }
+
+    private function getClientAccessToken()
+    {
+        if (!empty($_SESSION['access_token'])) {
+            return $this->rc->decrypt($_SESSION['access_token']);
+        }
+        return null;
+    }
+
+    private function refreshClientAccessToken()
+    {
+        //TODO use the refresh token if available instead of refreshing from scratch always.
+        rcube::write_log('kolabfiles', "Refreshing the access token");
+        $username = $_SESSION['username'];
+        $password = $this->rc->decrypt($_SESSION['password']);
+
+        $client = new \GuzzleHttp\Client([
+            'http_errors' => false, // No exceptions from Guzzle
+            'base_uri' => rtrim($this->config['baseuri'], '/') . '/',
+            'verify' => false,
+            'connect_timeout' => 10,
+            'timeout' => 10,
+        ]);
+
+        $response = $client->request('POST', "auth/login?email=$username&password=$password");
+        if ($response->getStatusCode() != 200) {
+            throw new Exception("Failed to authenticate $username:$password");
+        }
+        $json = json_decode($response->getBody(), true);
+        $accessToken = $json['access_token'];
+        $_SESSION['access_token'] = $this->rc->encrypt($accessToken);
+    }
+
+    protected function client()
+    {
+        $this->init();
+        return $this->client;
+    }
+
+    /**
+     * Authenticates a user
+     *
+     * @param string $username User name
+     * @param string $password User password
+     *
+     * @param bool True on success, False on failure
+     */
+    public function authenticate($username, $password)
+    {
+        $_SESSION['username'] = $username;
+        $_SESSION['password'] = $this->rc->encrypt($password);
+
+
+        $this->init();
+
+        return true;
+    }
+
+    /**
+     * Get password and name of authenticated user
+     *
+     * @return array Authenticated user data
+     */
+    public function auth_info()
+    {
+        return array(
+            'username' => $_SESSION['username'],
+            'password' => $this->rc->decrypt($_SESSION['password']),
+        );
+    }
+
+    /**
+     * Configures environment
+     *
+     * @param array  $config Configuration
+     * @param string $title  Source identifier
+     */
+    public function configure($config, $title = null)
+    {
+        $this->config = array_merge($this->config, $config);
+        $this->title  = $title;
+    }
+
+    /**
+     * Returns current instance title
+     *
+     * @return string Instance title (mount point)
+     */
+    public function title()
+    {
+        return $this->title;
+    }
+
+    /**
+     * Storage driver capabilities
+     *
+     * @return array List of capabilities
+     */
+    public function capabilities()
+    {
+        // find max filesize value
+        $max_filesize = parse_bytes(ini_get('upload_max_filesize'));
+        $max_postsize = parse_bytes(ini_get('post_max_size'));
+        if ($max_postsize && $max_postsize < $max_filesize) {
+            $max_filesize = $max_postsize;
+        }
+
+        return array(
+            file_storage::CAPS_MAX_UPLOAD => $max_filesize,
+            file_storage::CAPS_QUOTA      => false,
+            file_storage::CAPS_LOCKS      => true,
+            file_storage::CAPS_ACL        => true,
+        );
+    }
+
+    /**
+     * Save configuration of external driver (mount point)
+     *
+     * @param array $driver Driver data
+     *
+     * @throws Exception
+     */
+    public function driver_create($driver)
+    {
+        throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED);
+    }
+
+    /**
+     * Delete configuration of external driver (mount point)
+     *
+     * @param string $title Driver instance name
+     *
+     * @throws Exception
+     */
+    public function driver_delete($title)
+    {
+        throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED);
+    }
+
+    /**
+     * Return list of registered drivers (mount points)
+     *
+     * @return array List of drivers data
+     * @throws Exception
+     */
+    public function driver_list()
+    {
+        return [];
+    }
+
+    /**
+     * Update configuration of external driver (mount point)
+     *
+     * @param string $title  Driver instance name
+     * @param array  $driver Driver data
+     *
+     * @throws Exception
+     */
+    public function driver_update($title, $driver)
+    {
+        throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED);
+    }
+
+    /**
+     * Returns metadata of the driver
+     *
+     * @return array Driver meta data (image, name, form)
+     */
+    public function driver_metadata()
+    {
+        $image_content = file_get_contents(__DIR__ . '/kolab.png');
+
+        $metadata = array(
+            'image' => 'data:image/png;base64,' . base64_encode($image_content),
+            'name'  => 'Kolab Files',
+            'ref'   => 'http://kolab.org',
+            'description' => 'Kolab File Storage',
+            'form'  => array(
+                'host'     => 'hostname',
+                'username' => 'username',
+                'password' => 'password',
+            ),
+        );
+
+        return $metadata;
+    }
+
+    /**
+     * Validate metadata (config) of the driver
+     *
+     * @param array $metadata Driver metadata
+     *
+     * @return array Driver meta data to be stored in configuration
+     * @throws Exception
+     */
+    public function driver_validate($metadata)
+    {
+        throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED);
+    }
+
+    /**
+     * Create a file.
+     *
+     * @param string $file_name Name of a file (with folder path)
+     * @param array  $file      File data (path, type)
+     *
+     * @throws Exception
+     */
+    public function file_create($file_name, $file)
+    {
+        list($fn, $repo_id) = $this->find_collection($file_name);
+
+        if (empty($repo_id)) {
+            throw new Exception("Storage error. Folder not found.", file_storage::ERROR);
+        }
+
+        $data = null;
+        $fp = null;
+        if ($file['path']) {
+            $fp = fopen($file['path'], 'r');
+            $data = $fp;
+        } else {
+            $data = $file['content'];
+        }
+
+        $response = $this->client->request("POST", "v4/fs?name=$fn&parent=$repo_id", ['body' => $data]);
+        $created = $response->getStatusCode() == 200;
+
+        if (!$created) {
+            rcube::raise_error(array(
+                'code' => 600, 'type' => 'php',
+                'file' => __FILE__, 'line' => __LINE__,
+                'message' => "Error saving file to Kolab server"),
+                true, false);
+
+            throw new Exception("Storage error. Saving file failed.", file_storage::ERROR);
+        }
+    }
+
+    /**
+     * Update a file.
+     *
+     * @param string $file_name Name of a file (with folder path)
+     * @param array  $file      File data (path, type)
+     *
+     * @throws Exception
+     */
+    public function file_update($file_name, $file)
+    {
+        list($fn, $repo_id) = $this->find_collection($file_name);
+        $file_id = $this->find_file_id($fn, $repo_id);
+
+        if (empty($repo_id)) {
+            throw new Exception("Storage error. Folder not found.", file_storage::ERROR);
+        }
+
+        $data = null;
+        $fp = null;
+        if ($file['path']) {
+            $fp = fopen($file['path'], 'r');
+            $data = $fp;
+        } else {
+            $data = $file['content'];
+        }
+
+        $response = $this->client->request("PATCH", "v4/fs/$file_id?media=content", ['body' => $data]);
+
+        if ($response->getStatusCode() != 200) {
+            rcube::raise_error(array(
+                'code' => 600, 'type' => 'php',
+                'file' => __FILE__, 'line' => __LINE__,
+                'message' => "Error saving file to Kolab server"),
+                true, false);
+
+            throw new Exception("Storage error. Saving file failed.", file_storage::ERROR);
+        }
+    }
+
+    /**
+     * Delete a file.
+     *
+     * @param string $file_name Name of a file (with folder path)
+     *
+     * @throws Exception
+     */
+    public function file_delete($file_name)
+    {
+        list($file_name, $repo_id) = $this->find_collection($file_name);
+
+        if ($repo_id && $file_name != '/') {
+            $file_id = $this->find_file_id($file_name, $repo_id);
+            $response = $this->client->request("DELETE", "v4/fs/$file_id");
+            $deleted = $response->getStatusCode() == 200;
+        }
+
+        if (!$deleted) {
+            rcube::raise_error(array(
+                'code' => 600, 'type' => 'php',
+                'file' => __FILE__, 'line' => __LINE__,
+                'message' => "Error deleting object from Kolab server"),
+                true, false);
+
+            throw new Exception("Storage error. Deleting file failed.", file_storage::ERROR);
+        }
+    }
+
+    /**
+     * Return file body.
+     *
+     * @param string   $file_name Name of a file (with folder path)
+     * @param array    $params    Parameters (force-download, force-type, head)
+     * @param resource $fp        Print to file pointer instead (send no headers)
+     *
+     * @throws Exception
+     */
+    public function file_get($file_name, $params = array(), $fp = null)
+    {
+        list($fn, $repo_id) = $this->find_collection($file_name);
+        $file_id = $this->find_file_id($fn, $repo_id);
+
+        // $file = $this->from_file_object($file);
+        $response = $this->client->request("GET", "v4/fs/{$file_id}");
+        $file = json_decode($response->getBody(), true);
+
+        // write to file pointer, send no headers
+        if ($fp) {
+            $response = $this->client->request("GET", "v4/fs/{$file_id}?download=1");
+            fwrite(fp, $request->getBody());
+
+            return;
+        }
+
+        if (!empty($params['force-download'])) {
+            $disposition = 'attachment';
+            header("Content-Type: application/octet-stream");
+// @TODO
+//            if ($browser->ie)
+//                header("Content-Type: application/force-download");
+        }
+        else {
+            $mimetype    = file_utils::real_mimetype($params['force-type'] ? $params['force-type'] : $file['mimetype']);
+            $disposition = 'inline';
+
+            header("Content-Transfer-Encoding: binary");
+            header("Content-Type: $mimetype");
+        }
+
+        $filename = addcslashes($file['name'], '"');
+
+        // Workaround for nasty IE bug (#1488844)
+        // If Content-Disposition header contains string "attachment" e.g. in filename
+        // IE handles data as attachment not inline
+/*
+@TODO
+        if ($disposition == 'inline' && $browser->ie && $browser->ver < 9) {
+            $filename = str_ireplace('attachment', 'attach', $filename);
+        }
+*/
+        header("Content-Length: " . $file['size']);
+        header("Content-Disposition: $disposition; filename=\"$filename\"");
+
+        // just send redirect to Kolab server
+        if ($file['size'] && empty($params['head'])) {
+            $allow_redirects = $this->rc->config->get('fileapi_kolabfiles_allow_redirects');
+            // In view-mode we can't redirect to Kolab server because:
+            // - it responds with Content-Disposition: attachment, which causes that
+            //   e.g. previewing images is not possible
+            // - pdf/odf viewers can't follow redirects for some reason (#4590)
+            if ($allow_redirects && !empty($params['force-download'])) {
+                $response = $this->client->request("GET", "v4/fs/{$file_id}?downloadUrl=1");
+                $json = json_decode($response->getBody(), true);
+                $link = $json['downloadUrl'];
+                header("Location: $link");
+            }
+            else if ($fp = fopen('php://output', 'wb')) {
+                $response = $this->client->request("GET", "v4/fs/{$file_id}?download=1");
+                fwrite($fp, $response->getBody());
+                fclose($fp);
+            }
+        }
+    }
+
+    /**
+     * Returns file metadata.
+     *
+     * @param string $file_name Name of a file (with folder path)
+     *
+     * @throws Exception
+     */
+    public function file_info($file_name)
+    {
+        list($file, $repo_id) = $this->find_collection($file_name);
+        $file_id = $this->find_file_id($file, $repo_id);
+
+        $response = $this->client->request("GET", "v4/fs/{$file_id}");
+        $json = json_decode($response->getBody(), true);
+
+        if (empty($json)) {
+            throw new Exception("Storage error. File not found.", file_storage::ERROR);
+        }
+
+        $file = $this->from_file_object($json);
+
+        return array(
+            'name'     => $file['name'],
+            'size'     => (int) $file['size'],
+            'type'     => (string) $file['type'],
+            'mtime'    => file_utils::date_format($file['changed'], $this->config['date_format'], $this->config['timezone']),
+            'ctime'    => file_utils::date_format($file['created'], $this->config['date_format'], $this->config['timezone']),
+            'modified' => $file['changed'] ? $file['changed']->format('U') : 0,
+            'created'  => $file['created'] ? $file['created']->format('U') : 0,
+        );
+    }
+
+    /**
+     * List files in a folder.
+     *
+     * @param string $folder_name Name of a folder with full path
+     * @param array  $params      List parameters ('sort', 'reverse', 'search', 'prefix')
+     *
+     * @return array List of files (file properties array indexed by filename)
+     * @throws Exception
+     */
+    public function file_list($folder_name, $params = array())
+    {
+        // mount point contains only folders
+        if (!is_string($folder_name) || $folder_name === '') {
+            return array();
+        }
+
+        list($folder, $repo_id) = $this->find_collection($folder_name);
+
+        // prepare search filter
+        if (!empty($params['search'])) {
+            foreach ($params['search'] as $idx => $value) {
+                if ($idx == 'name') {
+                    $params['search'][$idx] = mb_strtoupper($value);
+                }
+                else if ($idx == 'class') {
+                    $params['search'][$idx] = file_utils::class2mimetypes($value);
+                }
+            }
+        }
+
+        $response = $this->client()->request('GET', "v4/fs?parent={$repo_id}&type=file");
+        $json = json_decode($response->getBody(), true);
+        $entries = $json['list'];
+
+        $result  = array();
+
+        foreach ((array) $entries as $idx => $file) {
+            //TODO file['name']
+            //TODO file['type'] = 'file'
+            //TODO file['type']
+
+            if ($file['type'] != 'file') {
+                continue;
+            }
+
+            $file_id = $file['id'];
+            $file = $this->from_file_object($file);
+
+            //FIXME slightly wasteful to just get the size....
+            $response = $this->client->request("GET", "v4/fs/{$file_id}");
+            $json = json_decode($response->getBody(), true);
+            $file['size'] = $json['size'];
+
+            // search filter
+            if (!empty($params['search'])) {
+                foreach ($params['search'] as $idx => $value) {
+                    if ($idx == 'name') {
+                        if (strpos(mb_strtoupper($file['name']), $value) === false) {
+                            continue 2;
+                        }
+                    }
+                    else if ($idx == 'class') {
+                        foreach ($value as $v) {
+                            if (stripos($file['type'], $v) !== false) {
+                                continue 2;
+                            }
+                        }
+
+                        continue 2;
+                    }
+                }
+            }
+
+            $filename = $params['prefix'] . $folder_name . file_storage::SEPARATOR . $file['name'];
+
+            $result[$filename] = array(
+                'name'     => $file['name'],
+                'size'     => (int) $file['size'],
+                'type'     => (string) $file['type'],
+                'mtime'    => file_utils::date_format($file['changed'], $this->config['date_format'], $this->config['timezone']),
+                'ctime'    => file_utils::date_format($file['created'], $this->config['date_format'], $this->config['timezone']),
+                'modified' => $file['changed'] ? $file['changed']->format('U') : 0,
+                'created'  => $file['created'] ? $file['created']->format('U') : 0,
+            );
+
+            unset($entries[$idx]);
+        }
+
+        // @TODO: pagination, search (by filename, mimetype)
+
+        // Sorting
+        $sort  = !empty($params['sort']) ? $params['sort'] : 'name';
+        $index = array();
+
+        if ($sort == 'mtime') {
+            $sort = 'modified';
+        }
+
+        if (in_array($sort, array('name', 'size', 'modified'))) {
+            foreach ($result as $key => $val) {
+                $index[$key] = $val[$sort];
+            }
+            array_multisort($index, SORT_ASC, SORT_NUMERIC, $result);
+        }
+
+        if ($params['reverse']) {
+            $result = array_reverse($result, true);
+        }
+
+        return $result;
+    }
+
+    /**
+     * Copy a file.
+     *
+     * @param string $file_name Name of a file (with folder path)
+     * @param string $new_name  New name of a file (with folder path)
+     *
+     * @throws Exception
+     */
+    public function file_copy($file_name, $new_name)
+    {
+        throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED);
+    }
+
+    /**
+     * Move (or rename) a file.
+     *
+     * @param string $file_name Name of a file (with folder path)
+     * @param string $new_name  New name of a file (with folder path)
+     *
+     * @throws Exception
+     */
+    public function file_move($file_name, $new_name)
+    {
+        list($src_name, $repo_id)     = $this->find_collection($file_name);
+        list($dst_name, $dst_repo_id) = $this->find_collection($new_name);
+        $file_id = $this->find_file_id($src_name, $repo_id);
+
+        $response = $this->client->request("PUT", "v4/fs/$file_id?name=$dst_name", ['headers' => ["X-Kolab-Parents" => $dst_repo_id]]);
+        $success = $response->getStatusCode() == 200;
+
+        if (!$success) {
+            rcube::raise_error(array(
+                'code' => 600, 'type' => 'php',
+                'file' => __FILE__, 'line' => __LINE__,
+                'message' => "Error moving file on Kolab server"),
+                true, false);
+
+            throw new Exception("Storage error. File rename failed.", file_storage::ERROR);
+        }
+    }
+
+    /**
+     * Create a folder.
+     *
+     * @param string $folder_name Name of a folder with full path
+     *
+     * @throws Exception on error
+     */
+    public function folder_create($folder_name)
+    {
+        list($folder, $repo_id) = $this->find_collection($folder_name, true);
+
+        if (empty($repo_id)) {
+            $response = $this->client->request("POST", "v4/fs?name=$folder_name&type=collection");
+        }
+        else {
+            $response = $this->client->request("POST", "v4/fs?name=$folder&type=collection&parent={$repo_id}");
+        }
+
+        $success = $response->getStatusCode() == 200;
+
+        if (!$success) {
+            throw new Exception("Storage error. Unable to create folder", file_storage::ERROR);
+        }
+
+        // clear the cache
+        if (empty($repo_id)) {
+            $this->collections = null;
+        }
+    }
+
+    /**
+     * Delete a folder.
+     *
+     * @param string $folder_name Name of a folder with full path
+     *
+     * @throws Exception on error
+     */
+    public function folder_delete($folder_name)
+    {
+        list($folder, $repo_id) = $this->find_collection($folder_name, true);
+
+        $response = $this->client->request("DELETE", "v4/fs/$repo_id");
+        $success = $response->getStatusCode() == 200;
+
+        if (!$success) {
+            throw new Exception("Storage error. Unable to delete folder.", file_storage::ERROR);
+        }
+    }
+
+    /**
+     * Move/Rename a folder.
+     *
+     * @param string $folder_name Name of a folder with full path
+     * @param string $new_name    New name of a folder with full path
+     *
+     * @throws Exception on error
+     */
+    public function folder_move($folder_name, $new_name)
+    {
+        list($folder_name, $repo_id, $collection) = $this->find_collection($folder_name, true);
+        list($dest_folder_name, $dest_repo_id) = $this->find_collection($new_name, true);
+
+        $response = $this->client->request("PUT", "v4/fs/$repo_id?name=$dest_folder_name", ['headers' => ["X-Kolab-Parents" => $dest_repo_id]]);
+        $success = $response->getStatusCode() == 200;
+
+        if (!$success) {
+            throw new Exception("Storage error. Unable to rename/move folder", file_storage::ERROR);
+        }
+    }
+
+    /**
+     * Subscribe a folder.
+     *
+     * @param string $folder_name Name of a folder with full path
+     *
+     * @throws Exception
+     */
+    public function folder_subscribe($folder_name)
+    {
+        throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED);
+    }
+
+    /**
+     * Unsubscribe a folder.
+     *
+     * @param string $folder_name Name of a folder with full path
+     *
+     * @throws Exception
+     */
+    public function folder_unsubscribe($folder_name)
+    {
+        throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED);
+    }
+
+    /**
+     * Returns list of folders.
+     *
+     * @param array $params List parameters ('type', 'search', 'path', 'level')
+     *
+     * @return array List of folders
+     * @throws Exception
+     */
+    public function folder_list($params = array())
+    {
+        // $collections  = $this->collections();
+        // $writable   = ($params['type'] & file_storage::FILTER_WRITABLE) ? true : false;
+        // $prefix     = (string) $params['path'];
+        // $prefix_len = strlen($prefix);
+        $folders    = array();
+        $collections = $this->collections();
+        foreach ($collections as $folder) {
+            $item = array('folder' => $folder['name']);
+            $item['readonly'] = false;
+
+            //TODO?
+            // 'mtime'      => $collection['mtime'],
+            // 'permission' => $collection['permission'],
+
+            $folders[$folder['name']] = $item;
+        }
+
+
+        if (empty($params['extended'])) {
+            $folders = array_keys($folders);
+        }
+
+        // sort folders
+        usort($folders, array('file_utils', 'sort_folder_comparator'));
+
+        return $folders;
+    }
+
+    /**
+     * Check folder rights.
+     *
+     * @param string $folder_name Name of a folder with full path
+     *
+     * @return int Folder rights (sum of file_storage::ACL_*)
+     */
+    public function folder_rights($folder_name)
+    {
+        list($folder, $repo_id, $collection) = $this->find_collection($folder_name);
+
+        $response = $this->client()->request('GET', "v4/fs/$repo_id");
+        $json = json_decode($response->getBody(), true);
+        if ($json['canUpdate']) {
+            return file_storage::ACL_READ | file_storage::ACL_WRITE;
+        }
+        return file_storage::ACL_READ;
+    }
+
+    /**
+     * Returns a list of locks
+     *
+     * This method should return all the locks for a particular URI, including
+     * locks that might be set on a parent URI.
+     *
+     * If child_locks is set to true, this method should also look for
+     * any locks in the subtree of the URI for locks.
+     *
+     * @param string $path        File/folder path
+     * @param bool   $child_locks Enables subtree checks
+     *
+     * @return array List of locks
+     * @throws Exception
+     */
+    public function lock_list($path, $child_locks = false)
+    {
+        $this->init_lock_db();
+
+        // convert URI to global resource string
+        $uri = $this->path2uri($path);
+
+        // get locks list
+        $list = $this->lock_db->lock_list($uri, $child_locks);
+
+        // convert back resource string into URIs
+        foreach ($list as $idx => $lock) {
+            $list[$idx]['uri'] = $this->uri2path($lock['uri']);
+        }
+
+        return $list;
+    }
+
+    /**
+     * Locks a URI
+     *
+     * @param string $path File/folder path
+     * @param array  $lock Lock data
+     *                     - depth: 0/'infinite'
+     *                     - scope: 'shared'/'exclusive'
+     *                     - owner: string
+     *                     - token: string
+     *                     - timeout: int
+     *
+     * @throws Exception
+     */
+    public function lock($path, $lock)
+    {
+        $this->init_lock_db();
+
+        // convert URI to global resource string
+        $uri = $this->path2uri($path);
+
+        if (!$this->lock_db->lock($uri, $lock)) {
+            throw new Exception("Database error. Unable to create a lock.", file_storage::ERROR);
+        }
+    }
+
+    /**
+     * Removes a lock from a URI
+     *
+     * @param string $path File/folder path
+     * @param array  $lock Lock data
+     *
+     * @throws Exception
+     */
+    public function unlock($path, $lock)
+    {
+        $this->init_lock_db();
+
+        // convert URI to global resource string
+        $uri = $this->path2uri($path);
+
+        if (!$this->lock_db->unlock($uri, $lock)) {
+            throw new Exception("Database error. Unable to remove a lock.", file_storage::ERROR);
+        }
+    }
+
+    /**
+     * Return disk quota information for specified folder.
+     *
+     * @param string $folder_name Name of a folder with full path
+     *
+     * @return array Quota
+     * @throws Exception
+     */
+    public function quota($folder)
+    {
+        // throw new Exception("Not implemented", file_storage::ERROR_UNSUPPORTED);
+        return [];
+    }
+
+    /**
+     * Sharing interface
+     *
+     * @param string $folder_name Name of a folder with full path
+     * @param int    $mode        Sharing action mode
+     * @param array  $args        POST/GET parameters
+     *
+     * @return mixed Sharing response
+     * @throws Exception
+     */
+    public function sharing($folder, $mode, $args = array())
+    {
+        throw new Exception("Search not implemented", file_storage::ERROR_UNSUPPORTED);
+    }
+
+    /**
+     * User/group search (autocompletion)
+     *
+     * @param string $search Search string
+     * @param int    $mode   Search mode
+     *
+     * @return array Users/Groups list
+     * @throws Exception
+     */
+    public function autocomplete($search, $mode)
+    {
+        throw new Exception("Autocomplete not implemented", file_storage::ERROR_UNSUPPORTED);
+    }
+
+    /**
+     * Convert file/folder path into a global URI.
+     *
+     * @param string $path File/folder path
+     *
+     * @return string URI
+     * @throws Exception
+     */
+    public function path2uri($path)
+    {
+        // Remove protocol prefix and path, we work with host only
+        $host = preg_replace('#(^https?://|/.*$)#i', '', $this->config['host']);
+
+        if (!is_string($path) || !strlen($path)) {
+            $user = $_SESSION[$this->title . 'user'];
+            return 'kolabfiles://' . rawurlencode($user) . '@' . $host . '/';
+        }
+
+        list($file, $repo_id, $collection) = $this->find_collection($path);
+
+        //FIXME we don't currently have an owner
+        return 'kolabfiles://' . rawurlencode($collection['owner']) . '@' . $host . '/' . file_utils::encode_path($path);
+    }
+
+    /**
+     * Convert global URI into file/folder path.
+     *
+     * @param string $uri URI
+     *
+     * @return string File/folder path
+     * @throws Exception
+     */
+    public function uri2path($uri)
+    {
+        if (!preg_match('|^kolabfiles://([^@]+)@([^/]+)/(.*)$|', $uri, $matches)) {
+            throw new Exception("Internal storage error. Unexpected data format. $uri", file_storage::ERROR);
+        }
+
+        $user   = rawurldecode($matches[1]);
+        $host   = $matches[2];
+        $path   = file_utils::decode_path($matches[3]);
+        $c_host = preg_replace('#(^https?://|/.*$)#i', '', $this->config['host']);
+
+        if (strlen($path)) {
+            list($file, $repo_id, $collection) = $this->find_collection($path, true);
+
+            if (empty($collection) || $host != $c_host || $user != $collection['owner']) {
+                throw new Exception("Internal storage error. Unresolvable URI.", file_storage::ERROR);
+            }
+        }
+
+        return $path;
+    }
+
+    /**
+     * Get list of collections
+     */
+    private function collections($parent = null, $path = null)
+    {
+        if ($this->collections) {
+            return $this->collections;
+        }
+        $folders = array();
+
+        //FIXME If we could just fetch all collections, we could assemble the tree after a single fetch.
+        if ($parent) {
+            $parentId = $parent['id'];
+            $response = $this->client()->request('GET', "v4/fs?parent=$parentId&type=collection");
+        } else {
+            $response = $this->client()->request('GET', "v4/fs?type=collection");
+        }
+        //FIXME should we just always throw on request errors? Probably?
+        if ($response->getStatusCode() != 200) {
+            rcube::write_log('kolabfiles', "The request failed: " . $response->getStatusCode());
+            throw new Exception("Get request was unsuccessful");
+        }
+        $json = json_decode($response->getBody(), true);
+        // rcube::write_log('console', var_export($json, true));
+        $collections = $json['list'];
+
+        if (!$collections) {
+            return [];
+        }
+
+        $collections = array_map(function ($entry) use ($path) {
+            //FIXME: retrieve the actual owner from the api. (Do we need the owner though?), not sure it matters
+            $entry['owner'] = $_SESSION[$this->title . 'user'];
+            if ($path) {
+                $entry['name'] = $path . "/" . $entry['name'];
+            }
+            return $entry;
+        }, $collections);
+
+        $tmp = $collections;
+        foreach ($tmp as $lib) {
+            $subfolders = $this->collections($lib, $lib['name']);
+            $collections = array_merge($collections, $subfolders);
+        }
+        if (!$parent) {
+            $this->collections = $collections;
+        }
+        return $collections;
+    }
+
+
+    protected function find_file_id($file_name, $repo_id)
+    {
+        $response = $this->client()->request('GET', "v4/fs?parent={$repo_id}&type=file");
+        $json = json_decode($response->getBody(), true);
+        foreach ($json['list'] as $idx => $file) {
+            if ($file['name'] == $file_name) {
+                return $file['id'];
+            }
+        }
+        rcube::write_log('console', "Failed to find the file $file_name in $repo_id");
+        throw new Exception("Failed to find the file $file_name in $repo_id");
+    }
+
+    /**
+     * Find collection ID from folder name
+     */
+    protected function find_collection($folder_name, $no_exception = false)
+    {
+        $collections = $this->collections();
+
+        foreach ($collections as $lib) {
+            $path = $lib['name'] . '/';
+
+            if ($folder_name == $lib['name'] || strpos($folder_name, $path) === 0) {
+                if (empty($collection) || strlen($collection['name']) < strlen($lib['name'])) {
+                    $collection = $lib;
+                }
+            }
+        }
+
+        if (empty($collection)) {
+            if (!$no_exception) {
+                throw new Exception("Storage error. Collection not found.", file_storage::ERROR);
+            }
+            return array(null, null);
+        }
+        else {
+            $folder = substr($folder_name, strlen($collection['name']) + 1);
+        }
+
+        return array(
+            // '/' . ($folder ? $folder : ''),
+            ($folder ? $folder : ''),
+            $collection['id'],
+            $collection
+        );
+    }
+
+    /**
+     * Simplify internal structure of the file object
+     */
+    protected function from_file_object($file)
+    {
+        // if ($file['type'] != 'file') {
+        //     return null;
+        // }
+        //
+        if ($file['created_at']) {
+            try {
+                $file['created'] = new DateTime('@' . $file['created_at']);
+            }
+            catch (Exception $e) { }
+        }
+        unset($file['created_at']);
+
+        if ($file['updated_at']) {
+            try {
+                $file['changed'] = new DateTime('@' . $file['updated_at']);
+            }
+            catch (Exception $e) { }
+        }
+        unset($file['updated_at']);
+
+        // We're not taking the servers filetype. The server might have octet/stream stored for a file,
+        // which will break the editor which needs some odt mimetype (it will silently fall back to downloading the file instead).
+        $file['type'] = file_utils::ext_to_type($file['name']);
+
+        unset($file['id']);
+
+        return $file;
+    }
+
+    /**
+     * Initializes file_locks object
+     */
+    protected function init_lock_db()
+    {
+        if (!$this->lock_db) {
+            $this->lock_db = new file_locks;
+        }
+    }
+
+    /**
+     * Create display-username-with-email string
+     */
+    protected function user_label($name, $email)
+    {
+        if ($name && $name != $email) {
+            $label = "{$name} ({$email})";
+        }
+        else {
+            $label = $email;
+        }
+
+        return $label;
+    }
+}