diff --git a/lib/kolab_sync_data.php b/lib/kolab_sync_data.php
index 199702b..f2a6d47 100644
--- a/lib/kolab_sync_data.php
+++ b/lib/kolab_sync_data.php
@@ -1,1567 +1,1567 @@
 <?php
 
 /*
  +--------------------------------------------------------------------------+
  | Kolab Sync (ActiveSync for Kolab)                                        |
  |                                                                          |
  | Copyright (C) 2011-2012, Kolab Systems AG <contact@kolabsys.com>         |
  |                                                                          |
  | This program is free software: you can redistribute it and/or modify     |
  | it under the terms of the GNU Affero General Public License as published |
  | by the Free Software Foundation, either version 3 of the License, or     |
  | (at your option) any later version.                                      |
  |                                                                          |
  | This program is distributed in the hope that it will be useful,          |
  | but WITHOUT ANY WARRANTY; without even the implied warranty of           |
  | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the             |
  | GNU Affero General Public License for more details.                      |
  |                                                                          |
  | You should have received a copy of the GNU Affero General Public License |
  | along with this program. If not, see <http://www.gnu.org/licenses/>      |
  +--------------------------------------------------------------------------+
  | Author: Aleksander Machniak <machniak@kolabsys.com>                      |
  +--------------------------------------------------------------------------+
 */
 
 /**
  * Base class for Syncroton data backends
  */
 abstract class kolab_sync_data implements Syncroton_Data_IData
 {
     /**
      * ActiveSync protocol version
      *
      * @var float
      */
     protected $asversion = 0;
 
     /**
      * The storage backend
      *
      * @var kolab_sync_storage
      */
     protected $backend;
 
     /**
      * information about the current device
      *
      * @var Syncroton_Model_IDevice
      */
     protected $device;
 
     /**
      * timestamp to use for all sync requests
      *
      * @var DateTime
      */
     protected $syncTimeStamp;
 
     /**
      * name of model to use
      *
      * @var string
      */
     protected $modelName;
 
     /**
      * type of the default folder
      *
      * @var int
      */
     protected $defaultFolderType;
 
     /**
      * default container for new entries
      *
      * @var string
      */
     protected $defaultFolder;
 
     /**
      * default root folder
      *
      * @var string
      */
     protected $defaultRootFolder;
 
     /**
      * type of user created folders
      *
      * @var int
      */
     protected $folderType;
 
     /**
      * Internal cache for storage folders list
      *
      * @var array
      */
     protected $folders = [];
 
     /**
      * Logger instance.
      *
      * @var kolab_sync_logger
      */
     protected $logger;
 
     /**
      * Timezone
      *
      * @var string
      */
     protected $timezone;
 
     /**
      * List of device types with multiple folders support
      *
      * @var array
      */
     protected $ext_devices = [
         'iphone',
         'ipad',
         'thundertine',
         'windowsphone',
         'wp',
         'wp8',
         'playbook',
     ];
 
     protected $lastsync_folder = null;
     protected $lastsync_time = null;
 
     public const RESULT_OBJECT = 0;
     public const RESULT_UID    = 1;
     public const RESULT_COUNT  = 2;
 
     /**
      * Recurrence types
      */
     public const RECUR_TYPE_DAILY          = 0;     // Recurs daily.
     public const RECUR_TYPE_WEEKLY         = 1;     // Recurs weekly
     public const RECUR_TYPE_MONTHLY        = 2;     // Recurs monthly
     public const RECUR_TYPE_MONTHLY_DAYN   = 3;     // Recurs monthly on the nth day
     public const RECUR_TYPE_YEARLY         = 5;     // Recurs yearly
     public const RECUR_TYPE_YEARLY_DAYN    = 6;     // Recurs yearly on the nth day
 
     /**
      * Day of week constants
      */
     public const RECUR_DOW_SUNDAY      = 1;
     public const RECUR_DOW_MONDAY      = 2;
     public const RECUR_DOW_TUESDAY     = 4;
     public const RECUR_DOW_WEDNESDAY   = 8;
     public const RECUR_DOW_THURSDAY    = 16;
     public const RECUR_DOW_FRIDAY      = 32;
     public const RECUR_DOW_SATURDAY    = 64;
     public const RECUR_DOW_LAST        = 127;      //  The last day of the month. Used as a special value in monthly or yearly recurrences.
 
     /**
      * Mapping of recurrence types
      *
      * @var array
      */
     protected $recurTypeMap = [
         self::RECUR_TYPE_DAILY        => 'DAILY',
         self::RECUR_TYPE_WEEKLY       => 'WEEKLY',
         self::RECUR_TYPE_MONTHLY      => 'MONTHLY',
         self::RECUR_TYPE_MONTHLY_DAYN => 'MONTHLY',
         self::RECUR_TYPE_YEARLY       => 'YEARLY',
         self::RECUR_TYPE_YEARLY_DAYN  => 'YEARLY',
     ];
 
     /**
      * Mapping of weekdays
      * NOTE: ActiveSync uses a bitmask
      *
      * @var array
      */
     protected $recurDayMap = [
         'SU'  => self::RECUR_DOW_SUNDAY,
         'MO'  => self::RECUR_DOW_MONDAY,
         'TU'  => self::RECUR_DOW_TUESDAY,
         'WE'  => self::RECUR_DOW_WEDNESDAY,
         'TH'  => self::RECUR_DOW_THURSDAY,
         'FR'  => self::RECUR_DOW_FRIDAY,
         'SA'  => self::RECUR_DOW_SATURDAY,
     ];
 
 
     /**
      * the constructor
      *
      * @param Syncroton_Model_IDevice $device
      * @param DateTime                $syncTimeStamp
      */
     public function __construct(Syncroton_Model_IDevice $device, DateTime $syncTimeStamp)
     {
         $this->backend       = kolab_sync::storage();
         $this->device        = $device;
         $this->asversion     = floatval($device->acsversion);
         $this->syncTimeStamp = $this->backend->syncTimeStamp = $syncTimeStamp;
         $this->logger        = Syncroton_Registry::get(Syncroton_Registry::LOGGERBACKEND);
 
         $this->defaultRootFolder = $this->defaultFolder . '::Syncroton';
 
         // set internal timezone of kolab_format to user timezone
         try {
             $this->timezone = rcube::get_instance()->config->get('timezone', 'GMT');
             kolab_format::$timezone = new DateTimeZone($this->timezone);
         } catch (Exception $e) {
             //rcube::raise_error($e, true);
             $this->timezone = 'GMT';
             kolab_format::$timezone = new DateTimeZone('GMT');
         }
     }
 
     /**
      * return list of supported folders for this backend
      *
      * @return array
      */
     public function getAllFolders()
     {
         $list = [];
 
         // device supports multiple folders ?
         if ($this->isMultiFolder()) {
             // get the folders the user has access to
             $list = $this->listFolders();
         } elseif ($default = $this->getDefaultFolder()) {
             $list = [$default['serverId'] => $default];
         }
 
         // getAllFolders() is called only in FolderSync
         // throw Syncroton_Exception_Status_FolderSync exception
         if (!is_array($list)) {
             throw new Syncroton_Exception_Status_FolderSync(Syncroton_Exception_Status_FolderSync::FOLDER_SERVER_ERROR);
         }
 
         foreach ($list as $idx => $folder) {
             $list[$idx] = new Syncroton_Model_Folder($folder);
         }
 
         return $list;
     }
 
     /**
      * Retrieve folders which were modified since last sync
      *
      * @param DateTime $startTimeStamp
      * @param DateTime $endTimeStamp
      *
      * @return array List of folders
      */
     public function getChangedFolders(DateTime $startTimeStamp, DateTime $endTimeStamp)
     {
         // FIXME/TODO: Can we get mtime of a DAV folder?
         // Without this, we have a problem if folder ID does not change on rename
         return [];
     }
 
     /**
      * Returns true if the device supports multiple folders or it was configured so
      */
     protected function isMultiFolder()
     {
         $config    = rcube::get_instance()->config;
         $blacklist = $config->get('activesync_multifolder_blacklist_' . $this->modelName);
 
         if (!is_array($blacklist)) {
             $blacklist = $config->get('activesync_multifolder_blacklist');
         }
 
         if (is_array($blacklist)) {
             return !$this->deviceTypeFilter($blacklist);
         }
 
         return in_array_nocase($this->device->devicetype, $this->ext_devices);
     }
 
     /**
      * Returns default folder for current class type.
      */
     protected function getDefaultFolder()
     {
         // Check if there's any folder configured for sync
         $folders = $this->listFolders();
 
         if (empty($folders)) {
             return $folders;
         }
 
         foreach ($folders as $folder) {
             if ($folder['type'] == $this->defaultFolderType) {
                 $default = $folder;
                 break;
             }
         }
 
         // Return first on the list if there's no default
         if (empty($default)) {
             $default = array_first($folders);
             // make sure the type is default here
             $default['type'] = $this->defaultFolderType;
         }
 
         // Remember real folder ID and set ID/name to root folder
         $default['realid']      = $default['serverId'];
         $default['serverId']    = $this->defaultRootFolder;
         $default['displayName'] = $this->defaultFolder;
 
         return $default;
     }
 
     /**
      * Creates a folder
      */
     public function createFolder(Syncroton_Model_IFolder $folder)
     {
         $result = $this->backend->folder_create($folder->displayName, $folder->type, $this->device->deviceid, $folder->parentId);
 
         if ($result) {
             $folder->serverId = $result;
             return $folder;
         }
 
         // Note: Looks like Outlook 2013 ignores any errors on FolderCreate command
 
         throw new Syncroton_Exception_Status_FolderCreate(Syncroton_Exception_Status_FolderCreate::UNKNOWN_ERROR);
     }
 
     /**
      * Updates a folder
      */
     public function updateFolder(Syncroton_Model_IFolder $folder)
     {
         $result = $this->backend->folder_rename($folder->serverId, $this->device->deviceid, $folder->displayName, $folder->parentId);
 
         if ($result) {
             return $folder;
         }
 
         // @TODO: throw exception
     }
 
     /**
      * Deletes a folder
      */
     public function deleteFolder($folder)
     {
         if ($folder instanceof Syncroton_Model_IFolder) {
             $folder = $folder->serverId;
         }
 
         // @TODO: throw exception
         return $this->backend->folder_delete($folder, $this->device->deviceid);
     }
 
     /**
      * Empty folder (remove all entries and optionally subfolders)
      *
      * @param string $folderid Folder identifier
      * @param array  $options  Options
      */
     public function emptyFolderContents($folderid, $options)
     {
         // ActiveSync spec.: Clients use EmptyFolderContents to empty the Deleted Items folder.
         // The client can clear out all items in the Deleted Items folder when the user runs out of storage quota
         // (indicated by the return of an MailboxQuotaExceeded (113) status code from the server.
         // FIXME: Does that mean we don't need this to work on any other folder?
         // TODO: Respond with MailboxQuotaExceeded status. Where exactly?
 
         foreach ($this->extractFolders($folderid) as $folderid) {
             if (!$this->backend->folder_empty($folderid, $this->device->deviceid, !empty($options['deleteSubFolders']))) {
                 throw new Syncroton_Exception_Status_ItemOperations(Syncroton_Exception_Status_ItemOperations::ITEM_SERVER_ERROR);
             }
         }
     }
 
     /**
      * Moves object into another location (folder)
      *
      * @param string $srcFolderId Source folder identifier
      * @param string $serverId    Object identifier
      * @param string $dstFolderId Destination folder identifier
      *
      * @throws Syncroton_Exception_Status
      * @return string New object identifier
      */
     public function moveItem($srcFolderId, $serverId, $dstFolderId)
     {
         // TODO: Optimize, we just need to find the folder ID and UID, we do not need to "fetch" it.
         $item = $this->getObject($srcFolderId, $serverId);
 
         if (!$item) {
             throw new Syncroton_Exception_Status_MoveItems(Syncroton_Exception_Status_MoveItems::INVALID_SOURCE);
         }
 
         $uid = $this->backend->moveItem($item['folderId'], $this->device->deviceid, $this->modelName, $item['uid'], $dstFolderId);
 
         return $this->serverId($uid, $dstFolderId);
     }
 
     /**
      * Add entry
      *
      * @param string                 $folderId Folder identifier
      * @param Syncroton_Model_IEntry $entry    Entry object
      *
      * @return string ID of the created entry
      */
     public function createEntry($folderId, Syncroton_Model_IEntry $entry)
     {
         $entry = $this->toKolab($entry, $folderId);
 
         if ($folderId == $this->defaultRootFolder) {
             $default = $this->getDefaultFolder();
 
             if (!is_array($default)) {
                 throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::SYNC_SERVER_ERROR);
             }
 
             $folderId = $default['realid'] ?? $default['serverId'];
         }
 
         $uid = $this->backend->createItem($folderId, $this->device->deviceid, $this->modelName, $entry);
 
         if (empty($uid)) {
             throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::SYNC_SERVER_ERROR);
         }
 
         return $this->serverId($uid, $folderId);
     }
 
     /**
      * update existing entry
      *
      * @param string                 $folderId
      * @param string                 $serverId
      * @param Syncroton_Model_IEntry $entry
      *
      * @return string ID of the updated entry
      */
     public function updateEntry($folderId, $serverId, Syncroton_Model_IEntry $entry)
     {
         $oldEntry = $this->getObject($folderId, $serverId);
 
         if (empty($oldEntry)) {
             throw new Syncroton_Exception_NotFound('entry not found');
         }
 
         $entry = $this->toKolab($entry, $folderId, $oldEntry);
         $uid = $this->backend->updateItem($oldEntry['folderId'], $this->device->deviceid, $this->modelName, $oldEntry['uid'], $entry);
 
         if (empty($uid)) {
             throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::SYNC_SERVER_ERROR);
         }
 
         return $this->serverId($uid, $oldEntry['folderId']);
     }
 
     /**
      * Delete entry
      *
      * @param string                          $folderId
      * @param string                          $serverId
      * @param ?Syncroton_Model_SyncCollection $collectionData
      */
     public function deleteEntry($folderId, $serverId, $collectionData = null)
     {
         // TODO: Optimize, we just need to find the folder ID and UID, we do not need to "fetch" it.
         $object = $this->getObject($folderId, $serverId);
 
         if ($object) {
             $deleted = $this->backend->deleteItem($object['folderId'], $this->device->deviceid, $this->modelName, $object['uid']);
 
             if (!$deleted) {
                 throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::SYNC_SERVER_ERROR);
             }
         }
     }
 
     /**
      * Get attachment data from the server.
      *
      * @param string $fileReference
      *
      * @return Syncroton_Model_FileReference
      */
     public function getFileReference($fileReference)
     {
         // to be implemented by Email data class
         throw new Syncroton_Exception_NotFound('File references not supported');
     }
 
     /**
      * Search for existing entries
      *
      * @param string $folderid    Folder identifier
      * @param array  $filter      Search filter
      * @param int    $result_type Type of the result (see RESULT_* constants)
      *
      * @return array|int Search result as count or array of uids/objects
      */
     protected function searchEntries($folderid, $filter = [], $result_type = self::RESULT_UID)
     {
         $result = $result_type == self::RESULT_COUNT ? 0 : [];
         $ts     = time();
         $force  = $this->lastsync_folder != $folderid || $this->lastsync_time <= $ts - Syncroton_Registry::getPingTimeout();
         $found  = false;
 
         foreach ($this->extractFolders($folderid) as $fid) {
-            $search = $this->backend->searchEntries($fid, $this->device->deviceid, $this->modelName, $filter, $result_type, $force);
+            $search = $this->backend->searchEntries($fid, $this->device->deviceid, $this->device->id, $this->modelName, $filter, $result_type, $force);
             $found = true;
 
             switch ($result_type) {
                 case self::RESULT_COUNT:
                     $result += $search;
                     break;
 
                 case self::RESULT_UID:
                     foreach ($search as $idx => $uid) {
                         $search[$idx] = $this->serverId($uid, $fid);
                     }
 
                     $result = array_unique(array_merge($result, $search));
                     break;
             }
         }
 
         if (!$found) {
             throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
         }
 
         $this->lastsync_folder = $folderid;
         $this->lastsync_time   = $ts;
 
         return $result;
     }
 
     /**
      * Returns filter query array according to specified ActiveSync FilterType
      *
      * @param int $filter_type  Filter type
      *
      * @return array Filter query
      */
     protected function filter($filter_type = 0)
     {
         // overwrite by child class according to specified type
         return [];
     }
 
     /**
      * get all entries changed between two dates
      *
      * @param string   $folderId
      * @param DateTime $start
      * @param DateTime $end
      * @param int      $filter_type
      *
      * @return array
      */
     public function getChangedEntries($folderId, DateTime $start, DateTime $end = null, $filter_type = null)
     {
         $filter   = $this->filter($filter_type);
         $filter[] = ['changed', '>', $start];
 
         if ($end) {
             $filter[] = ['changed', '<=', $end];
         }
 
         return $this->searchEntries($folderId, $filter, self::RESULT_UID);
     }
 
     /**
      * Get count of entries changed between two dates
      *
      * @param string   $folderId
      * @param DateTime $start
      * @param DateTime $end
      * @param int      $filter_type
      *
      * @return int
      */
     public function getChangedEntriesCount($folderId, DateTime $start, DateTime $end = null, $filter_type = null)
     {
         $filter   = $this->filter($filter_type);
         $filter[] = ['changed', '>', $start];
 
         if ($end) {
             $filter[] = ['changed', '<=', $end];
         }
 
         return $this->searchEntries($folderId, $filter, self::RESULT_COUNT);
     }
 
     /**
      * get id's of all entries available on the server
      *
      * @param string $folder_id
      * @param string $filter_type
      *
      * @return array
      */
     public function getServerEntries($folder_id, $filter_type)
     {
         $filter = $this->filter($filter_type);
         $result = $this->searchEntries($folder_id, $filter, self::RESULT_UID);
 
         return $result;
     }
 
     /**
      * get count of all entries available on the server
      *
      * @param string $folder_id
      * @param string $filter_type
      *
      * @return int
      */
     public function getServerEntriesCount($folder_id, $filter_type)
     {
         $filter = $this->filter($filter_type);
         $result = $this->searchEntries($folder_id, $filter, self::RESULT_COUNT);
 
         return $result;
     }
 
     /**
      * Returns number of changed objects in the backend folder
      *
      * @param Syncroton_Backend_IContent $contentBackend
      * @param Syncroton_Model_IFolder    $folder
      * @param Syncroton_Model_ISyncState $syncState
      *
      * @return int
      */
     public function getCountOfChanges(Syncroton_Backend_IContent $contentBackend, Syncroton_Model_IFolder $folder, Syncroton_Model_ISyncState $syncState)
     {
         // @phpstan-ignore-next-line
         $allClientEntries = $contentBackend->getFolderState($this->device, $folder, $syncState->counter);
         $allServerEntries = $this->getServerEntries($folder->serverId, $folder->lastfiltertype);
         $changedEntries   = $this->getChangedEntriesCount($folder->serverId, $syncState->lastsync, null, $folder->lastfiltertype);
         $addedEntries     = array_diff($allServerEntries, $allClientEntries);
         $deletedEntries   = array_diff($allClientEntries, $allServerEntries);
 
         return count($addedEntries) + count($deletedEntries) + $changedEntries;
     }
 
     /**
      * Returns true if any data got modified in the backend folder
      *
      * @param Syncroton_Backend_IContent $contentBackend
      * @param Syncroton_Model_IFolder    $folder
      * @param Syncroton_Model_ISyncState $syncState
      *
      * @return bool
      */
     public function hasChanges(Syncroton_Backend_IContent $contentBackend, Syncroton_Model_IFolder $folder, Syncroton_Model_ISyncState $syncState)
     {
         try {
             if ($this->getChangedEntriesCount($folder->serverId, $syncState->lastsync, null, $folder->lastfiltertype)) {
                 return true;
             }
 
             // @phpstan-ignore-next-line
             $allClientEntries = $contentBackend->getFolderState($this->device, $folder, $syncState->counter);
 
             // @TODO: Consider looping over all folders here, not in getServerEntries() and
             // getChangedEntriesCount(). This way we could break the loop and not check all folders
             // or at least skip redundant cache sync of the same folder
             $allServerEntries = $this->getServerEntries($folder->serverId, $folder->lastfiltertype);
 
             $addedEntries   = array_diff($allServerEntries, $allClientEntries);
             $deletedEntries = array_diff($allClientEntries, $allServerEntries);
 
             return count($addedEntries) > 0 || count($deletedEntries) > 0;
         } catch (Exception $e) {
             // return "no changes" if something failed
             return false;
         }
     }
 
     /**
      * Fetches the entry from the backend
      */
     protected function getObject($folderid, $entryid)
     {
         foreach ($this->extractFolders($folderid) as $fid) {
             $crc = null;
             $uid = $entryid;
 
             // See self::serverId() for full explanation
             // Use (slower) UID prefix matching...
             if (preg_match('/^CRC([0-9A-Fa-f]{8})(.+)$/', $uid, $matches)) {
                 $crc = $matches[1];
                 $uid = $matches[2];
 
                 if (strlen($entryid) >= 64) {
                     $objects = $this->backend->getItemsByUidPrefix($fid, $this->device->deviceid, $this->modelName, $uid);
 
                     foreach ($objects as $object) {
                         if (($object['uid'] === $uid || strpos($object['uid'], $uid) === 0)
                             && $crc == $this->objectCRC($object['uid'], $fid)
                         ) {
                             $object['folderId'] = $fid;
                             return $object;
                         }
                     }
 
                     continue;
                 }
             }
 
             // Or (faster) strict UID matching...
             $object = $this->backend->getItem($fid, $this->device->deviceid, $this->modelName, $uid);
 
             if (!empty($object) && ($crc === null || $crc == $this->objectCRC($object['uid'], $fid))) {
                 $object['folderId'] = $fid;
                 return $object;
             }
         }
     }
 
     /**
      * Returns internal folder IDs
      *
      * @param string $folderid Folder identifier
      *
      * @return array List of folder identifiers
      */
     protected function extractFolders($folderid)
     {
         if ($folderid instanceof Syncroton_Model_IFolder) {
             $folderid = $folderid->serverId;
         }
 
         if ($folderid === $this->defaultRootFolder) {
             $folders = $this->listFolders();
 
             if (!is_array($folders)) {
                 throw new Syncroton_Exception_NotFound('Folder not found');
             }
 
             $folders = array_keys($folders);
         } else {
             $folders = [$folderid];
         }
 
         return $folders;
     }
 
     /**
      * List of all IMAP folders (or subtree)
      *
      * @param string $parentid Parent folder identifier
      *
      * @return array List of folder identifiers
      */
     protected function listFolders($parentid = null)
     {
         if (empty($this->folders)) {
             $this->folders = $this->backend->folders_list(
                 $this->device->deviceid,
                 $this->modelName,
                 $this->isMultiFolder()
             );
         }
 
         if ($parentid === null || !is_array($this->folders)) {
             return $this->folders;
         }
 
         $folders = [];
         $parents = [$parentid];
 
         foreach ($this->folders as $folder_id => $folder) {
             if ($folder['parentId'] && in_array($folder['parentId'], $parents)) {
                 $folders[$folder_id] = $folder;
                 $parents[] = $folder_id;
             }
         }
 
         return $folders;
     }
 
     /**
      * Returns ActiveSync settings of specified folder
      *
      * @param string $folderid Folder identifier
      *
      * @return array Folder settings
      */
     protected function getFolderConfig($folderid)
     {
         if ($folderid == $this->defaultRootFolder) {
             $default = $this->getDefaultFolder();
 
             if (!is_array($default)) {
                 return [];
             }
 
             $folderid = $default['realid'] ?? $default['serverId'];
         }
 
         return $this->backend->getFolderConfig($folderid, $this->device->deviceid, $this->modelName);
     }
 
     /**
      * Convert contact from xml to kolab format
      *
      * @param mixed  $data     Contact data
      * @param string $folderId Folder identifier
      * @param array  $entry    Old Contact data for merge
      *
      * @return array
      */
     abstract public function toKolab($data, $folderId, $entry = null);
 
     /**
      * Extracts data from kolab data array
      */
     protected function getKolabDataItem($data, $name)
     {
         $name_items = explode('.', $name);
         $count      = count($name_items);
 
         // multi-level array (e.g. address, phone)
         if ($count == 3) {
             $name     = $name_items[0];
             $type     = $name_items[1];
             $key_name = $name_items[2];
 
             if (!empty($data[$name]) && is_array($data[$name])) {
                 foreach ($data[$name] as $element) {
                     if ($element['type'] == $type) {
                         return $element[$key_name];
                     }
                 }
             }
 
             return null;
         }
 
         // custom properties
         if ($count == 2 && $name_items[0] == 'x-custom') {
             $value = null;
 
             if (!empty($data['x-custom']) && is_array($data['x-custom'])) {
                 foreach ($data['x-custom'] as $val) {
                     if (is_array($val) && $val[0] == $name_items[1]) {
                         $value = $val[1];
                         break;
                     }
                 }
             }
 
             return $value;
         }
 
         $name_items = explode(':', $name);
         $name       = $name_items[0];
 
         if (empty($data[$name])) {
             return null;
         }
 
         // simple array (e.g. email)
         if (count($name_items) == 2) {
             return $data[$name][$name_items[1]];
         }
 
         return $data[$name];
     }
 
     /**
      * Saves data in kolab data array
      */
     protected function setKolabDataItem(&$data, $name, $value)
     {
         if (empty($value)) {
             return $this->unsetKolabDataItem($data, $name);
         }
 
         $name_items = explode('.', $name);
         $count      = count($name_items);
 
         // multi-level array (e.g. address, phone)
         if ($count == 3) {
             $name     = $name_items[0];
             $type     = $name_items[1];
             $key_name = $name_items[2];
 
             if (!isset($data[$name])) {
                 $data[$name] = [];
             }
 
             foreach ($data[$name] as $idx => $element) {
                 if ($element['type'] == $type) {
                     $found = $idx;
                     break;
                 }
             }
 
             if (!isset($found)) {
                 $data[$name] = array_values($data[$name]);
                 $found = count($data[$name]);
                 $data[$name][$found] = ['type' => $type];
             }
 
             $data[$name][$found][$key_name] = $value;
 
             return;
         }
 
         // custom properties
         if ($count == 2 && $name_items[0] == 'x-custom') {
             $data['x-custom'] = isset($data['x-custom']) ? ((array) $data['x-custom']) : [];
             foreach ($data['x-custom'] as $idx => $val) {
                 if (is_array($val) && $val[0] == $name_items[1]) {
                     $data['x-custom'][$idx][1] = $value;
                     return;
                 }
             }
 
             $data['x-custom'][] = [$name_items[1], $value];
             return;
         }
 
 
         $name_items = explode(':', $name);
         $name       = $name_items[0];
 
         // simple array (e.g. email)
         if (count($name_items) == 2) {
             $data[$name][$name_items[1]] = $value;
             return;
         }
 
         $data[$name] = $value;
     }
 
     /**
      * Unsets data item in kolab data array
      */
     protected function unsetKolabDataItem(&$data, $name)
     {
         $name_items = explode('.', $name);
         $count      = count($name_items);
 
         // multi-level array (e.g. address, phone)
         if ($count == 3) {
             $name     = $name_items[0];
             $type     = $name_items[1];
             $key_name = $name_items[2];
 
             if (!isset($data[$name])) {
                 return;
             }
 
             foreach ($data[$name] as $idx => $element) {
                 if ($element['type'] == $type) {
                     $found = $idx;
                     break;
                 }
             }
 
             if (!isset($found)) {
                 return;
             }
 
             unset($data[$name][$found][$key_name]);
 
             // if there's only one element and it's 'type', remove it
             if (count($data[$name][$found]) == 1 && isset($data[$name][$found]['type'])) {
                 unset($data[$name][$found]['type']);
             }
             if (empty($data[$name][$found])) {
                 unset($data[$name][$found]);
             }
             if (empty($data[$name])) {
                 unset($data[$name]);
             }
 
             return;
         }
 
         // custom properties
         if ($count == 2 && $name_items[0] == 'x-custom') {
             foreach ((array) $data['x-custom'] as $idx => $val) {
                 if (is_array($val) && $val[0] == $name_items[1]) {
                     unset($data['x-custom'][$idx]);
                 }
             }
         }
 
         $name_items = explode(':', $name);
         $name       = $name_items[0];
 
         // simple array (e.g. email)
         if (count($name_items) == 2) {
             unset($data[$name][$name_items[1]]);
             if (empty($data[$name])) {
                 unset($data[$name]);
             }
             return;
         }
 
         unset($data[$name]);
     }
 
     /**
      * Setter for Body attribute according to client version
      *
      * @param string $value  Body
      * @param array  $params Body parameters
      *
      * @reurn Syncroton_Model_EmailBody Body element
      */
     protected function setBody($value, $params = [])
     {
         if (empty($value) && empty($params)) {
             return;
         }
 
         // Old protocol version doesn't support AirSyncBase:Body, it's eg. WindowsCE
         if ($this->asversion < 12) {
             return;
         }
 
         if (!empty($value)) {
             // cast to string to workaround issue described in Bug #1635
             $params['data'] = (string) $value;
         }
 
         if (!isset($params['type'])) {
             $params['type'] = Syncroton_Model_EmailBody::TYPE_PLAINTEXT;
         }
 
         return new Syncroton_Model_EmailBody($params);
     }
 
     /**
      * Getter for Body attribute value according to client version
      *
      * @param mixed $body Body element
      * @param int   $type Result data type (to which the body will be converted, if specified).
      *                    One or array of Syncroton_Model_EmailBody constants.
      *
      * @return string|null Body value
      */
     protected function getBody($body, $type = null)
     {
         $data = null;
         if ($body && $body->data) {
             $data = $body->data;
         }
 
         if (!$data || empty($type)) {
             return null;
         }
 
         $type = (array) $type;
 
         // Convert to specified type
         if (!in_array($body->type, $type)) {
             $converter = new kolab_sync_body_converter($data, $body->type);
             $data      = $converter->convert($type[0]);
         }
 
         return $data;
     }
 
     /**
      * Converts text (plain or html) into ActiveSync Body element.
      * Takes bodyPreferences into account and detects if the text is plain or html.
      */
     protected function body_from_kolab($body, $collection)
     {
         if (empty($body)) {
             return;
         }
 
         $opts      = $collection->options;
         $prefs     = $opts['bodyPreferences'];
         $html_type = Syncroton_Command_Sync::BODY_TYPE_HTML;
         $type      = Syncroton_Command_Sync::BODY_TYPE_PLAIN_TEXT;
         $params    = [];
 
         // HTML? check for opening and closing <html> or <body> tags
         $is_html = preg_match('/<(html|body)(\s+[a-z]|>)/', $body, $m) && strpos($body, '</' . $m[1] . '>') > 0;
 
         // here we assume that all devices support plain text
         if ($is_html) {
             // device supports HTML...
             if (!empty($prefs[$html_type])) {
                 $type = $html_type;
             }
             // ...else convert to plain text
             else {
                 $txt  = new rcube_html2text($body, false, true);
                 $body = $txt->get_text();
             }
         }
 
         // strip out any non utf-8 characters
         $body        = rcube_charset::clean($body);
         $real_length = $body_length = strlen($body);
 
         // truncate the body if needed
         if (isset($prefs[$type]['truncationSize']) && ($truncateAt = $prefs[$type]['truncationSize']) && $body_length > $truncateAt) {
             $body        = mb_strcut($body, 0, $truncateAt);
             $body_length = strlen($body);
 
             $params['truncated']         = 1;
             $params['estimatedDataSize'] = $real_length;
         }
 
         $params['type'] = $type;
 
         return $this->setBody($body, $params);
     }
 
 
     /**
      * Converts PHP DateTime, date (YYYY-MM-DD) or unixtimestamp into PHP DateTime in UTC
      *
      * @param DateTime|int|string $date Unix timestamp, date (YYYY-MM-DD) or PHP DateTime object
      *
      * @return DateTime|null Datetime object
      */
     protected static function date_from_kolab($date)
     {
         if (!empty($date)) {
             if (is_numeric($date)) {
                 $date = new DateTime('@' . $date);
             } elseif (is_string($date)) {
                 $date = new DateTime($date, new DateTimeZone('UTC'));
             } elseif ($date instanceof DateTime) {
                 $date    = clone $date;
                 $tz      = $date->getTimezone();
                 $tz_name = $tz->getName();
 
                 // convert to UTC if needed
                 if ($tz_name != 'UTC') {
                     $utc = new DateTimeZone('UTC');
                     // safe dateonly object conversion to UTC
                     // note: _dateonly flag is set by libkolab e.g. for birthdays
                     if (!empty($date->_dateonly)) {
                         // avoid time change
                         $date = new DateTime($date->format('Y-m-d'), $utc);
                         // set time to noon to avoid timezone troubles
                         $date->setTime(12, 0, 0);
                     } else {
                         $date->setTimezone($utc);
                     }
                 }
             } else {
                 return null; // invalid input
             }
 
             return $date;
         }
 
         return null;
     }
 
     /**
      * Convert Kolab event/task recurrence into ActiveSync
      */
     protected function recurrence_from_kolab($collection, $data, &$result, $type = 'Event')
     {
         if (empty($data['recurrence']) || !empty($data['recurrence_date']) || empty($data['recurrence']['FREQ'])) {
             return;
         }
 
         $recurrence = [];
         $r          = $data['recurrence'];
 
         // required fields
         switch($r['FREQ']) {
             case 'DAILY':
                 $recurrence['type'] = self::RECUR_TYPE_DAILY;
                 break;
 
             case 'WEEKLY':
                 $day = $r['BYDAY'] ?? 0;
                 if (!$day && (!empty($data['_start']) || !empty($data['start']))) {
                     $days = ['', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA','SU'];
                     $start = $data['_start'] ?? $data['start'];
                     $day = $days[$start->format('N')];
                 }
 
                 $recurrence['type'] = self::RECUR_TYPE_WEEKLY;
                 $recurrence['dayOfWeek'] = $this->day2bitmask($day);
                 break;
 
             case 'MONTHLY':
                 if (!empty($r['BYMONTHDAY'])) {
                     // @TODO: ActiveSync doesn't support multi-valued month days,
                     // should we replicate the recurrence element for each day of month?
                     [$month_day, ] = explode(',', $r['BYMONTHDAY']);
                     $recurrence['type'] = self::RECUR_TYPE_MONTHLY;
                     $recurrence['dayOfMonth'] = $month_day;
                 } elseif (!empty($r['BYDAY'])) {
                     $week = (int) substr($r['BYDAY'], 0, -2);
                     $week = ($week == -1) ? 5 : $week;
                     $day  = substr($r['BYDAY'], -2);
                     $recurrence['type'] = self::RECUR_TYPE_MONTHLY_DAYN;
                     $recurrence['weekOfMonth'] = $week;
                     $recurrence['dayOfWeek'] = $this->day2bitmask($day);
                 } else {
                     return;
                 }
                 break;
 
             case 'YEARLY':
                 // @TODO: ActiveSync doesn't support multi-valued months,
                 // should we replicate the recurrence element for each month?
                 [$month, ] = explode(',', $r['BYMONTH']);
 
                 if (!empty($r['BYDAY'])) {
                     $week = (int) substr($r['BYDAY'], 0, -2);
                     $week = ($week == -1) ? 5 : $week;
                     $day  = substr($r['BYDAY'], -2);
                     $recurrence['type'] = self::RECUR_TYPE_YEARLY_DAYN;
                     $recurrence['weekOfMonth'] = $week;
                     $recurrence['dayOfWeek'] = $this->day2bitmask($day);
                     $recurrence['monthOfYear'] = $month;
                 } elseif (!empty($r['BYMONTHDAY'])) {
                     // @TODO: ActiveSync doesn't support multi-valued month days,
                     // should we replicate the recurrence element for each day of month?
                     [$month_day, ] = explode(',', $r['BYMONTHDAY']);
                     $recurrence['type'] = self::RECUR_TYPE_YEARLY;
                     $recurrence['dayOfMonth'] = $month_day;
                     $recurrence['monthOfYear'] = $month;
                 } else {
                     $recurrence['type'] = self::RECUR_TYPE_YEARLY;
                     $recurrence['monthOfYear'] = $month;
                 }
                 break;
         }
 
         // Skip all empty values (T2519)
         if ($recurrence['type'] != self::RECUR_TYPE_DAILY) {
             $recurrence = array_filter($recurrence);
         }
 
         // required field
         $recurrence['interval'] = $r['INTERVAL'] ?: 1;
 
         if (!empty($r['UNTIL'])) {
             $recurrence['until'] = self::date_from_kolab($r['UNTIL']);
         } elseif (!empty($r['COUNT'])) {
             $recurrence['occurrences'] = $r['COUNT'];
         }
 
         $class = 'Syncroton_Model_' . $type . 'Recurrence';
 
         $result['recurrence'] = new $class($recurrence);
 
         // Tasks do not support exceptions
         if ($type == 'Event') {
             $result['exceptions'] = $this->exceptions_from_kolab($collection, $data);
         }
     }
 
     /**
      * Convert ActiveSync event/task recurrence into Kolab
      */
     protected function recurrence_to_kolab($data, $folderid, $timezone = null)
     {
         if (!($data->recurrence instanceof Syncroton_Model_EventRecurrence)
             && !($data->recurrence instanceof Syncroton_Model_TaskRecurrence)
         ) {
             return;
         }
 
         if (!isset($data->recurrence->type)) {
             return;
         }
 
         $recurrence = $data->recurrence;
         $type       = $recurrence->type;
 
         switch ($type) {
             case self::RECUR_TYPE_DAILY:
                 break;
 
             case self::RECUR_TYPE_WEEKLY:
                 $rrule['BYDAY'] = $this->bitmask2day($recurrence->dayOfWeek);
                 break;
 
             case self::RECUR_TYPE_MONTHLY:
                 $rrule['BYMONTHDAY'] = $recurrence->dayOfMonth;
                 break;
 
             case self::RECUR_TYPE_MONTHLY_DAYN:
                 $week = $recurrence->weekOfMonth;
                 $day  = $recurrence->dayOfWeek;
                 $byDay  = $week == 5 ? -1 : $week;
                 $byDay .= $this->bitmask2day($day);
 
                 $rrule['BYDAY'] = $byDay;
                 break;
 
             case self::RECUR_TYPE_YEARLY:
                 $rrule['BYMONTH']    = $recurrence->monthOfYear;
                 $rrule['BYMONTHDAY'] = $recurrence->dayOfMonth;
                 break;
 
             case self::RECUR_TYPE_YEARLY_DAYN:
                 $rrule['BYMONTH'] = $recurrence->monthOfYear;
 
                 $week = $recurrence->weekOfMonth;
                 $day  = $recurrence->dayOfWeek;
                 $byDay  = $week == 5 ? -1 : $week;
                 $byDay .= $this->bitmask2day($day);
 
                 $rrule['BYDAY'] = $byDay;
                 break;
         }
 
         $rrule['FREQ']     = $this->recurTypeMap[$type];
         $rrule['INTERVAL'] = $recurrence->interval ?? 1;
 
         if (isset($recurrence->until)) {
             if ($timezone) {
                 $recurrence->until->setTimezone($timezone);
             }
             $rrule['UNTIL'] = $recurrence->until;
         } elseif (!empty($recurrence->occurrences)) {
             $rrule['COUNT'] = $recurrence->occurrences;
         }
 
         // recurrence exceptions (not supported by Tasks)
         if ($data instanceof Syncroton_Model_Event) {
             $this->exceptions_to_kolab($data, $rrule, $folderid, $timezone);
         }
 
         return $rrule;
     }
 
     /**
      * Convert Kolab event recurrence exceptions into ActiveSync
      */
     protected function exceptions_from_kolab($collection, $data)
     {
         if (empty($data['recurrence']['EXCEPTIONS']) && empty($data['recurrence']['EXDATE'])) {
             return null;
         }
 
         $ex_list = [];
 
         // exceptions (modified occurences)
         if (!empty($data['recurrence']['EXCEPTIONS'])) {
             foreach ((array)$data['recurrence']['EXCEPTIONS'] as $exception) {
                 $exception['_mailbox'] = $data['_mailbox'];
 
                 $ex   = $this->getEntry($collection, $exception, true); // @phpstan-ignore-line
                 $date = clone ($exception['recurrence_date'] ?: $ex['startTime']);
 
                 $ex['exceptionStartTime'] = self::set_exception_time($date, $data['_start'] ?? null);
 
                 // remove fields not supported by Syncroton_Model_EventException
                 unset($ex['uID']);
 
                 // @TODO: 'thisandfuture=true' is not supported in Activesync
                 // we'd need to slit the event into two separate events
 
                 $ex_list[] = new Syncroton_Model_EventException($ex);
             }
         }
 
         // exdate (deleted occurences)
         if (!empty($data['recurrence']['EXDATE'])) {
             foreach ((array)$data['recurrence']['EXDATE'] as $exception) {
                 if (!($exception instanceof DateTime)) {
                     continue;
                 }
 
                 $ex = [
                     'deleted'            => 1,
                     'exceptionStartTime' => self::set_exception_time($exception, $data['_start'] ?? null),
                 ];
 
                 $ex_list[] = new Syncroton_Model_EventException($ex);
             }
         }
 
         return $ex_list;
     }
 
     /**
      * Convert ActiveSync event recurrence exceptions into Kolab
      */
     protected function exceptions_to_kolab($data, &$rrule, $folderid, $timezone = null)
     {
         $rrule['EXDATE']     = [];
         $rrule['EXCEPTIONS'] = [];
 
         // handle exceptions from recurrence
         if (!empty($data->exceptions)) {
             foreach ($data->exceptions as $exception) {
                 $date = clone $exception->exceptionStartTime;
                 if ($timezone) {
                     $date->setTimezone($timezone);
                 }
 
                 if ($exception->deleted) {
                     $date->setTime(0, 0, 0);
                     $rrule['EXDATE'][] = $date;
                 } else {
                     $ex = $this->toKolab($exception, $folderid, null, $timezone); // @phpstan-ignore-line
 
                     $ex['recurrence_date'] = $date;
 
                     if (!empty($data->allDayEvent)) {
                         $ex['allday'] = 1;
                     }
 
                     $rrule['EXCEPTIONS'][] = $ex;
                 }
             }
         }
 
         if (empty($rrule['EXDATE'])) {
             unset($rrule['EXDATE']);
         }
         if (empty($rrule['EXCEPTIONS'])) {
             unset($rrule['EXCEPTIONS']);
         }
     }
 
     /**
      * Sets ExceptionStartTime according to occurrence date and event start time
      */
     protected static function set_exception_time($exception_date, $event_start)
     {
         if ($exception_date && $event_start) {
             $hour   = $event_start->format('H');
             $minute = $event_start->format('i');
             $second = $event_start->format('s');
 
             $exception_date->setTime($hour, $minute, $second);
             $exception_date->_dateonly = false;
 
             return self::date_from_kolab($exception_date);
         }
     }
 
     /**
      * Converts string of days (TU,TH) to bitmask used by ActiveSync
      *
      * @param string $days
      *
      * @return int
      */
     protected function day2bitmask($days)
     {
         $days   = explode(',', $days);
         $result = 0;
 
         foreach ($days as $day) {
             if ($day) {
                 $result = $result + ($this->recurDayMap[$day] ?? 0);
             }
         }
 
         return $result;
     }
 
     /**
      * Convert bitmask used by ActiveSync to string of days (TU,TH)
      *
      * @param int $days
      *
      * @return string
      */
     protected function bitmask2day($days)
     {
         $days_arr = [];
 
         for ($bitmask = 1; $bitmask <= self::RECUR_DOW_SATURDAY; $bitmask = $bitmask << 1) {
             $dayMatch = $days & $bitmask;
             if ($dayMatch === $bitmask) {
                 $days_arr[] = array_search($bitmask, $this->recurDayMap);
             }
         }
         $result = implode(',', $days_arr);
 
         return $result;
     }
 
     /**
      * Check if current device type string matches any of options
      */
     protected function deviceTypeFilter($options)
     {
         foreach ($options as $option) {
             if ($option[0] == '/') {
                 if (preg_match($option, $this->device->devicetype)) {
                     return true;
                 }
             } elseif (stripos($this->device->devicetype, $option) !== false) {
                 return true;
             }
         }
 
         return false;
     }
 
     /**
      * Returns all email addresses of the current user
      */
     protected function user_emails()
     {
         $user_emails = kolab_sync::get_instance()->user->list_emails();
         $user_emails = array_map(function ($v) { return $v['email']; }, $user_emails);
 
         return $user_emails;
     }
 
     /**
      * Generate CRC-based ServerId from object UID
      */
     protected function serverId($uid, $folder)
     {
         // When ActiveSync communicates with the client, it refers to objects with a ServerId
         // We can't use object UID for ServerId because:
         //     - ServerId is limited to 64 chars,
         //     - there can be multiple calendars with a copy of the same event.
         //
         // The solution is to; Take the original UID, and regardless of its length, execute the following:
         //     - Hash the UID concatenated with the Folder ID using CRC32b,
         //     - Prefix the UID with 'CRC' and the hash string,
         //     - Tryncate the result to 64 characters.
         //
         // Searching for the server-side copy of the object now follows the logic;
         //     - If the ServerId is prefixed with 'CRC', strip off the first 11 characters
         //       and we search for the UID using the remainder;
         //     - if the UID is shorter than 53 characters, it'll be the complete UID,
         //     - if the UID is longer than 53 characters, it'll be the truncated UID,
         //       and we search for a wildcard match of <uid>*
         // When multiple copies of the same event are found, the same CRC32b hash can be used
         // on the events metadata (i.e. the copy's UID and Folder ID), and compared with the CRC from the ServerId.
 
         // ServerId is max. 64 characters, below we generate a string of max. 64 chars
         // Note: crc32b is always 8 characters
         return 'CRC' . $this->objectCRC($uid, $folder) . substr($uid, 0, 53);
     }
 
     /**
      * Calculate checksum on object UID and folder UID
      */
     protected function objectCRC($uid, $folder)
     {
         if (!is_object($folder)) {
             $folder = $this->backend->getFolder($folder, $this->device->deviceid, $this->modelName);
         }
 
         $folder_uid = $folder->get_uid();
 
         return strtoupper(hash('crc32b', $folder_uid . $uid)); // always 8 chars
     }
 }
diff --git a/lib/kolab_sync_storage.php b/lib/kolab_sync_storage.php
index fd0d019..cebb8fe 100644
--- a/lib/kolab_sync_storage.php
+++ b/lib/kolab_sync_storage.php
@@ -1,2064 +1,2065 @@
 <?php
 
 /*
  +--------------------------------------------------------------------------+
  | Kolab Sync (ActiveSync for Kolab)                                        |
  |                                                                          |
  | Copyright (C) 2011-2012, Kolab Systems AG <contact@kolabsys.com>         |
  |                                                                          |
  | This program is free software: you can redistribute it and/or modify     |
  | it under the terms of the GNU Affero General Public License as published |
  | by the Free Software Foundation, either version 3 of the License, or     |
  | (at your option) any later version.                                      |
  |                                                                          |
  | This program is distributed in the hope that it will be useful,          |
  | but WITHOUT ANY WARRANTY; without even the implied warranty of           |
  | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the             |
  | GNU Affero General Public License for more details.                      |
  |                                                                          |
  | You should have received a copy of the GNU Affero General Public License |
  | along with this program. If not, see <http://www.gnu.org/licenses/>      |
  +--------------------------------------------------------------------------+
  | Author: Aleksander Machniak <machniak@kolabsys.com>                      |
  +--------------------------------------------------------------------------+
 */
 
 /**
  * Storage handling class with basic Kolab support (everything stored in IMAP)
  */
 class kolab_sync_storage
 {
     public const INIT_SUB_PERSONAL = 1; // all subscribed folders in personal namespace
     public const INIT_ALL_PERSONAL = 2; // all folders in personal namespace
     public const INIT_SUB_OTHER    = 4; // all subscribed folders in other users namespace
     public const INIT_ALL_OTHER    = 8; // all folders in other users namespace
     public const INIT_SUB_SHARED   = 16; // all subscribed folders in shared namespace
     public const INIT_ALL_SHARED   = 32; // all folders in shared namespace
 
     public const MODEL_CALENDAR = 'event';
     public const MODEL_CONTACTS = 'contact';
     public const MODEL_EMAIL    = 'mail';
     public const MODEL_NOTES    = 'note';
     public const MODEL_TASKS    = 'task';
 
     public const ROOT_MAILBOX  = 'INBOX';
     public const ASYNC_KEY     = '/private/vendor/kolab/activesync';
     public const UID_KEY       = '/shared/vendor/cmu/cyrus-imapd/uniqueid';
 
     public const CTYPE_KEY         = '/shared/vendor/kolab/folder-type';
     public const CTYPE_KEY_PRIVATE = '/private/vendor/kolab/folder-type';
 
     public $syncTimeStamp;
 
     protected $storage;
     protected $folder_meta;
     protected $folder_uids;
     protected $folders = [];
     protected $modseq = [];
     protected $root_meta;
     protected $relations = [];
     protected $relationSupport = true;
     protected $tag_rts = [];
 
     protected static $instance;
 
     protected static $types = [
         1  => '',
         2  => 'mail.inbox',
         3  => 'mail.drafts',
         4  => 'mail.wastebasket',
         5  => 'mail.sentitems',
         6  => 'mail.outbox',
         7  => 'task.default',
         8  => 'event.default',
         9  => 'contact.default',
         10 => 'note.default',
         11 => 'journal.default',
         12 => 'mail',
         13 => 'event',
         14 => 'contact',
         15 => 'task',
         16 => 'journal',
         17 => 'note',
     ];
 
 
     /**
      * This implements the 'singleton' design pattern
      *
      * @return kolab_sync_storage The one and only instance
      */
     public static function get_instance()
     {
         if (!self::$instance) {
             self::$instance = new kolab_sync_storage();
             self::$instance->startup();  // init AFTER object was linked with self::$instance
         }
 
         return self::$instance;
     }
 
     /**
      * Class initialization
      */
     public function startup()
     {
         $this->storage = kolab_sync::get_instance()->get_storage();
 
         // set additional header used by libkolab
         $this->storage->set_options([
             // @TODO: there can be Roundcube plugins defining additional headers,
             // we maybe would need to add them here
             'fetch_headers' => 'X-KOLAB-TYPE X-KOLAB-MIME-VERSION',
             'skip_deleted'  => true,
             'threading'     => false,
         ]);
 
         // Disable paging
         $this->storage->set_pagesize(999999);
     }
 
     /**
      * Clear internal cache state
      */
     public function reset()
     {
         $this->folders = [];
     }
 
     /**
      * List known devices
      *
      * @return array Device list as hash array
      */
     public function devices_list()
     {
         if ($this->root_meta === null) {
             // @TODO: consider server annotation instead of INBOX
             if ($meta = $this->storage->get_metadata(self::ROOT_MAILBOX, self::ASYNC_KEY)) {
                 $this->root_meta = $this->unserialize_metadata($meta[self::ROOT_MAILBOX][self::ASYNC_KEY]);
             } else {
                 $this->root_meta = [];
             }
         }
 
         if (!empty($this->root_meta['DEVICE']) && is_array($this->root_meta['DEVICE'])) {
             return $this->root_meta['DEVICE'];
         }
 
         return [];
     }
 
     /**
      * Get list of folders available for sync
      *
      * @param string $deviceid  Device identifier
      * @param string $type      Folder type
      * @param bool   $flat_mode Enables flat-list mode
      *
      * @return array|bool List of mailbox folders, False on backend failure
      */
     public function folders_list($deviceid, $type, $flat_mode = false)
     {
         // get all folders of specified type
         $folders = kolab_storage::list_folders('', '*', $type, false, $typedata);
 
         // get folders activesync config
         $folderdata = $this->folder_meta();
 
         if (!is_array($folders) || !is_array($folderdata)) {
             return false;
         }
 
         $folders_list = [];
 
         // check if folders are "subscribed" for activesync
         foreach ($folderdata as $folder => $meta) {
             if (empty($meta['FOLDER']) || empty($meta['FOLDER'][$deviceid])
                 || empty($meta['FOLDER'][$deviceid]['S'])
             ) {
                 continue;
             }
 
             // force numeric folder name to be a string (T1283)
             $folder = (string) $folder;
 
             if (!empty($type) && !in_array($folder, $folders)) {
                 continue;
             }
 
             // Activesync folder identifier (serverId)
             $folder_type = !empty($typedata[$folder]) ? $typedata[$folder] : 'mail';
             $folder_id   = $this->folder_id($folder, $folder_type);
 
             $folders_list[$folder_id] = $this->folder_data($folder, $folder_type);
         }
 
         if ($flat_mode) {
             $folders_list = $this->folders_list_flat($folders_list, $type, $typedata);
         }
 
         return $folders_list;
     }
 
     /**
      * Converts list of folders to a "flat" list
      */
     protected function folders_list_flat($folders, $type, $typedata)
     {
         $delim = $this->storage->get_hierarchy_delimiter();
 
         foreach ($folders as $idx => $folder) {
             if ($folder['parentId']) {
                 // for non-mail folders we make the list completely flat
                 if ($type != self::MODEL_EMAIL) {
                     $display_name = kolab_storage::object_name($folder['imap_name']);
                     $display_name = html_entity_decode($display_name, ENT_COMPAT, RCUBE_CHARSET);
 
                     $folders[$idx]['parentId']    = 0;
                     $folders[$idx]['displayName'] = $display_name;
                 }
                 // for mail folders we modify only folders with non-existing parents
                 elseif (!isset($folders[$folder['parentId']])) {
                     $items  = explode($delim, $folder['imap_name']);
                     $parent = 0;
 
                     // find existing parent
                     while (count($items) > 0) {
                         array_pop($items);
 
                         $parent_name = implode($delim, $items);
                         $parent_type = !empty($typedata[$parent_name]) ? $typedata[$parent_name] : 'mail';
                         $parent_id   = $this->folder_id($parent_name, $parent_type);
 
                         if (isset($folders[$parent_id])) {
                             $parent = $parent_id;
                             break;
                         }
                     }
 
                     if (!$parent) {
                         $display_name = kolab_storage::object_name($folder['imap_name']);
                         $display_name = html_entity_decode($display_name, ENT_COMPAT, RCUBE_CHARSET);
                     } else {
                         $parent_name  = isset($parent_id) ? $folders[$parent_id]['imap_name'] : '';
                         $display_name = substr($folder['imap_name'], strlen($parent_name) + 1);
                         $display_name = rcube_charset::convert($display_name, 'UTF7-IMAP');
                         $display_name = str_replace($delim, ' ยป ', $display_name);
                     }
 
                     $folders[$idx]['parentId']    = $parent;
                     $folders[$idx]['displayName'] = $display_name;
                 }
             }
         }
 
         return $folders;
     }
 
     /**
      * Getter for folder metadata
      *
      * @return array|bool Hash array with meta data for each folder, False on backend failure
      */
     protected function folder_meta()
     {
         if (!isset($this->folder_meta)) {
             // get folders activesync config
             $folderdata = $this->storage->get_metadata("*", self::ASYNC_KEY);
 
             if (!is_array($folderdata)) {
                 return $this->folder_meta = false;
             }
 
             $this->folder_meta = [];
 
             foreach ($folderdata as $folder => $meta) {
                 if (isset($meta[self::ASYNC_KEY])) {
                     if ($metadata = $this->unserialize_metadata($meta[self::ASYNC_KEY])) {
                         $this->folder_meta[$folder] = $metadata;
                     }
                 }
             }
         }
 
         return $this->folder_meta;
     }
 
     /**
      * Creates folder and subscribes to the device
      *
      * @param string  $name      Folder name (UTF8)
      * @param int     $type      Folder (ActiveSync) type
      * @param string  $deviceid  Device identifier
      * @param ?string $parentid  Parent folder id identifier
      *
      * @return string|false New folder identifier on success, False on failure
      */
     public function folder_create($name, $type, $deviceid, $parentid = null)
     {
         $parent = null;
         $name = rcube_charset::convert($name, kolab_sync::CHARSET, 'UTF7-IMAP');
 
         if ($parentid) {
             $parent = $this->folder_id2name($parentid, $deviceid);
 
             if ($parent === null) {
                 throw new Syncroton_Exception_Status_FolderCreate(Syncroton_Exception_Status_FolderCreate::PARENT_NOT_FOUND);
             }
         }
 
         if ($parent !== null) {
             $delim = $this->storage->get_hierarchy_delimiter();
             $name  = $parent . $delim . $name;
         }
 
         if ($this->storage->folder_exists($name)) {
             throw new Syncroton_Exception_Status_FolderCreate(Syncroton_Exception_Status_FolderCreate::FOLDER_EXISTS);
         }
 
         $type    = self::type_activesync2kolab($type);
         $created = kolab_storage::folder_create($name, $type, true);
 
         if ($created) {
             // Set ActiveSync subscription flag
             $this->folder_set($name, $deviceid, 1);
 
             return $this->folder_id($name, $type);
         }
 
         // Special case when client tries to create a subfolder of INBOX
         // which is not possible on Cyrus-IMAP (T2223)
         if ($parent === 'INBOX' && stripos($this->last_error(), 'invalid') !== false) {
             throw new Syncroton_Exception_Status_FolderCreate(Syncroton_Exception_Status_FolderCreate::SPECIAL_FOLDER);
         }
 
         return false;
     }
 
     /**
      * Renames a folder
      *
      * @param string  $folderid Folder identifier
      * @param string  $deviceid Device identifier
      * @param string  $new_name New folder name (UTF8)
      * @param ?string $parentid Folder parent identifier
      *
      * @return bool True on success, False on failure
      */
     public function folder_rename($folderid, $deviceid, $new_name, $parentid)
     {
         $old_name = $this->folder_id2name($folderid, $deviceid);
 
         if ($parentid) {
             $parent = $this->folder_id2name($parentid, $deviceid);
         }
 
         $name = rcube_charset::convert($new_name, kolab_sync::CHARSET, 'UTF7-IMAP');
 
         if (isset($parent)) {
             $delim   = $this->storage->get_hierarchy_delimiter();
             $name    = $parent . $delim . $name;
         }
 
         // Rename/move IMAP folder
         if ($name === $old_name) {
             return true;
         }
 
         $this->folder_meta = null;
 
         // TODO: folder type change?
 
         $type = kolab_storage::folder_type($old_name);
 
         // don't use kolab_storage for moving mail folders
         if (preg_match('/^mail/', $type)) {
             return $this->storage->rename_folder($old_name, $name);
         } else {
             return kolab_storage::folder_rename($old_name, $name);
         }
     }
 
     /**
      * Deletes folder
      *
      * @param string $folderid Folder identifier
      * @param string $deviceid Device identifier
      *
      * @return bool True on success, False otherwise
      */
     public function folder_delete($folderid, $deviceid)
     {
         $name = $this->folder_id2name($folderid, $deviceid);
         $type = kolab_storage::folder_type($name);
 
         unset($this->folder_meta[$name]);
 
         // don't use kolab_storage for deleting mail folders
         if (preg_match('/^mail/', $type)) {
             return $this->storage->delete_folder($name);
         }
 
         return kolab_storage::folder_delete($name);
     }
 
     /**
      * Deletes contents of a folder
      *
      * @param string $folderid  Folder identifier
      * @param string $deviceid  Device identifier
      * @param bool   $recursive Apply to the folder and its subfolders
      *
      * @return bool True on success, False otherwise
      */
     public function folder_empty($folderid, $deviceid, $recursive = false)
     {
         $foldername = $this->folder_id2name($folderid, $deviceid);
 
         // Remove all entries
         if (!$this->storage->clear_folder($foldername)) {
             return false;
         }
 
         // Remove subfolders
         if ($recursive) {
             $delim = $this->storage->get_hierarchy_delimiter();
             $folderdata = $this->folder_meta();
 
             if (!is_array($folderdata)) {
                 return false;
             }
 
             foreach ($folderdata as $subfolder => $meta) {
                 if (!empty($meta['FOLDER'][$deviceid]['S']) && strpos((string) $subfolder, $foldername . $delim)) {
                     if (!$this->storage->clear_folder((string) $subfolder)) {
                         return false;
                     }
                 }
             }
         }
 
         return true;
     }
 
     /**
      * Sets ActiveSync subscription flag on a folder
      *
      * @param string $name      Folder name (UTF7-IMAP)
      * @param string $deviceid  Device identifier
      * @param int    $flag      Flag value (0|1|2)
      *
      * @return bool True on success, False on failure
      */
     protected function folder_set($name, $deviceid, $flag)
     {
         if (empty($deviceid)) {
             return false;
         }
 
         // get folders activesync config
         $metadata = $this->folder_meta();
 
         if (!is_array($metadata)) {
             return false;
         }
 
         $metadata = $metadata[$name] ?? [];
 
         if ($flag) {
             if (empty($metadata)) {
                 $metadata = [];
             }
 
             if (empty($metadata['FOLDER'])) {
                 $metadata['FOLDER'] = [];
             }
 
             if (empty($metadata['FOLDER'][$deviceid])) {
                 $metadata['FOLDER'][$deviceid] = [];
             }
 
             // Z-Push uses:
             //  1 - synchronize, no alarms
             //  2 - synchronize with alarms
             $metadata['FOLDER'][$deviceid]['S'] = $flag;
         } else {
             unset($metadata['FOLDER'][$deviceid]['S']);
 
             if (empty($metadata['FOLDER'][$deviceid])) {
                 unset($metadata['FOLDER'][$deviceid]);
             }
 
             if (empty($metadata['FOLDER'])) {
                 unset($metadata['FOLDER']);
             }
 
             if (empty($metadata)) {
                 $metadata = null;
             }
         }
 
         // Return if nothing's been changed
         if (!self::data_array_diff($this->folder_meta[$name] ?? null, $metadata)) {
             return true;
         }
 
         $this->folder_meta[$name] = $metadata;
 
         return $this->storage->set_metadata($name, [self::ASYNC_KEY => $this->serialize_metadata($metadata)]);
     }
 
     /**
      * Returns device metadata
      *
      * @param string $id Device ID
      *
      * @return array|null Device metadata
      */
     public function device_get($id)
     {
         $devices_list = $this->devices_list();
         return $devices_list[$id] ?? null;
     }
 
     /**
      * Registers new device on server
      *
      * @param array  $device  Device data
      * @param string $id      Device ID
      *
      * @return bool True on success, False on failure
      */
     public function device_create($device, $id)
     {
         // Fill local cache
         $this->devices_list();
 
         // Some devices create dummy devices with name "validate" (#1109)
         // This device entry is used in two initial requests, but later
         // the device registers a real name. We can remove this dummy entry
         // on new device creation
         $this->device_delete('validate');
 
         // Old Kolab_ZPush device parameters
         // MODE:  -1 | 0 | 1  (not set | flatmode | foldermode)
         // TYPE:  device type string
         // ALIAS: user-friendly device name
 
         // Syncroton (kolab_sync_backend_device) uses
         // ID:    internal identifier in syncroton database
         // TYPE:  device type string
         // ALIAS: user-friendly device name
 
         $metadata = $this->root_meta;
         $metadata['DEVICE'][$id] = $device;
         $metadata = [self::ASYNC_KEY => $this->serialize_metadata($metadata)];
 
         $result = $this->storage->set_metadata(self::ROOT_MAILBOX, $metadata);
 
         if ($result) {
             // Update local cache
             $this->root_meta['DEVICE'][$id] = $device;
 
             // subscribe default set of folders
             $this->device_init_subscriptions($id);
         }
 
         return $result;
     }
 
     /**
      * Device update.
      *
      * @param array  $device  Device data
      * @param string $id      Device ID
      *
      * @return bool True on success, False on failure
      */
     public function device_update($device, $id)
     {
         $devices_list = $this->devices_list();
         $old_device   = $devices_list[$id];
 
         if (!$old_device) {
             return false;
         }
 
         // Do nothing if nothing is changed
         if (!self::data_array_diff($old_device, $device)) {
             return true;
         }
 
         $device = array_merge($old_device, $device);
 
         $metadata = $this->root_meta;
         $metadata['DEVICE'][$id] = $device;
         $metadata = [self::ASYNC_KEY => $this->serialize_metadata($metadata)];
 
         $result = $this->storage->set_metadata(self::ROOT_MAILBOX, $metadata);
 
         if ($result) {
             // Update local cache
             $this->root_meta['DEVICE'][$id] = $device;
         }
 
         return $result;
     }
 
     /**
      * Device delete.
      *
      * @param string $id Device ID
      *
      * @return bool True on success, False on failure
      */
     public function device_delete($id)
     {
         $device = $this->device_get($id);
 
         if (!$device) {
             return false;
         }
 
         unset($this->root_meta['DEVICE'][$id], $this->root_meta['FOLDER'][$id]);
 
         if (empty($this->root_meta['DEVICE'])) {
             unset($this->root_meta['DEVICE']);
         }
         if (empty($this->root_meta['FOLDER'])) {
             unset($this->root_meta['FOLDER']);
         }
 
         $metadata = $this->serialize_metadata($this->root_meta);
         $metadata = [self::ASYNC_KEY => $metadata];
 
         // update meta data
         $result = $this->storage->set_metadata(self::ROOT_MAILBOX, $metadata);
 
         if ($result) {
             // remove device annotation for every folder
             foreach ($this->folder_meta() as $folder => $meta) {
                 // skip root folder (already handled above)
                 if ($folder == self::ROOT_MAILBOX) {
                     continue;
                 }
 
                 if (!empty($meta['FOLDER']) && isset($meta['FOLDER'][$id])) {
                     unset($meta['FOLDER'][$id]);
 
                     if (empty($meta['FOLDER'])) {
                         unset($this->folder_meta[$folder]['FOLDER']);
                         unset($meta['FOLDER']);
                     }
                     if (empty($meta)) {
                         unset($this->folder_meta[$folder]);
                         $meta = null;
                     }
 
                     $metadata = [self::ASYNC_KEY => $this->serialize_metadata($meta)];
                     $res = $this->storage->set_metadata($folder, $metadata);
 
                     if ($res && $meta) {
                         $this->folder_meta[$folder] = $meta;
                     }
                 }
             }
         }
 
         return $result;
     }
 
     /**
      * Creates an item in a folder.
      *
      * @param string       $folderid Folder identifier
      * @param string       $deviceid Device identifier
      * @param string       $type     Activesync model name (folder type)
      * @param string|array $data     Object data (string for email, array for other types)
      * @param array        $params   Additional parameters (e.g. mail flags)
      *
      * @return string|null Item UID on success or null on failure
      */
     public function createItem($folderid, $deviceid, $type, $data, $params = [])
     {
         if ($type == self::MODEL_EMAIL) {
             $foldername = $this->folder_id2name($folderid, $deviceid);
 
             $uid = $this->storage->save_message($foldername, $data, '', false, $params['flags'] ?? []);
 
             if (!$uid) {
                 // $this->logger->error("Error while storing the message " . $this->storage->get_error_str());
             }
 
             return $uid;
         }
 
         $useTags = $this->relationSupport && ($type == self::MODEL_TASKS || $type == self::MODEL_NOTES);
 
         // convert categories into tags, save them after creating an object
         if ($useTags && !empty($data['categories'])) {
             $tags = $data['categories'];
             unset($data['categories']);
         }
 
         $folder = $this->getFolder($folderid, $deviceid, $type);
 
         // Set User-Agent for saved objects
         $app = kolab_sync::get_instance();
         $app->config->set('useragent', $app->app_name . ' ' . kolab_sync::VERSION);
 
         if ($folder && $folder->valid && $folder->save($data)) {
             if (!empty($tags) && ($type == self::MODEL_TASKS || $type == self::MODEL_NOTES)) {
                 $this->setCategories($data['uid'], $tags);
             }
 
             return $data['uid'];
         }
 
         return null;
     }
 
     /**
      * Deletes an item from a folder by UID.
      *
      * @param string $folderid    Folder identifier
      * @param string $deviceid    Device identifier
      * @param string $type        Activesync model name (folder type)
      * @param string $uid         Requested object UID
      * @param bool   $moveToTrash Move to trash, instead of delete (for mail messages only)
      *
      * @return bool True on success, False on failure
      */
     public function deleteItem($folderid, $deviceid, $type, $uid, $moveToTrash = false)
     {
         if ($type == self::MODEL_EMAIL) {
             $foldername = $this->folder_id2name($folderid, $deviceid);
             $trash = kolab_sync::get_instance()->config->get('trash_mbox');
 
             // move message to the Trash folder
             if ($moveToTrash && strlen($trash) && $trash != $foldername && $this->storage->folder_exists($trash)) {
                 return $this->storage->move_message($uid, $trash, $foldername);
             }
 
             // delete the message
             // According to the ActiveSync spec. "If the DeletesAsMoves element is set to false,
             // the deletion is PERMANENT.", therefore we delete the message, and not flag as deleted.
 
             // FIXME: We could consider acting according to the 'flag_for_deletion' setting.
             //        Don't forget about 'read_when_deleted' setting then.
             // $this->storage->set_flag($uid, 'DELETED', $foldername);
             // $this->storage->set_flag($uid, 'SEEN', $foldername);
             return $this->storage->delete_message($uid, $foldername);
         }
 
         $useTags = $this->relationSupport && ($type == self::MODEL_TASKS || $type == self::MODEL_NOTES);
 
         $folder = $this->getFolder($folderid, $deviceid, $type);
 
         if (!$folder || !$folder->valid) {
             throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
         }
 
         if ($folder->delete($uid)) {
             if ($useTags) {
                 $this->setCategories($uid, []);
             }
 
             return true;
         }
 
         return false;
     }
 
     /**
      * Updates an item in a folder.
      *
      * @param string       $folderid Folder identifier
      * @param string       $deviceid Device identifier
      * @param string       $type     Activesync model name (folder type)
      * @param string       $uid      Object UID
      * @param string|array $data     Object data (string for email, array for other types)
      * @param array        $params   Additional parameters (e.g. mail flags)
      *
      * @return string|null Item UID on success or null on failure
      */
     public function updateItem($folderid, $deviceid, $type, $uid, $data, $params = [])
     {
         if ($type == self::MODEL_EMAIL) {
             $foldername = $this->folder_id2name($folderid, $deviceid);
 
             // Note: We do not support a message body update, as it's not needed
 
             foreach (($params['flags'] ?? []) as $flag) {
                 $this->storage->set_flag($uid, $flag, $foldername);
             }
 
             // Categories (Tags) change
             if (isset($params['categories']) && $this->relationSupport) {
                 $message = new rcube_message($uid, $foldername);
 
                 if (empty($message->headers)) {
                     throw new Syncroton_Exception_Status_Sync(Syncroton_Exception_Status_Sync::SYNC_SERVER_ERROR);
                 }
 
                 $this->setCategories($message, $params['categories']);
             }
 
             return $uid;
         }
 
         $folder = $this->getFolder($folderid, $deviceid, $type);
 
         $useTags = $this->relationSupport && ($type == self::MODEL_TASKS || $type == self::MODEL_NOTES);
 
         // convert categories into tags, save them after updating an object
         if ($useTags && array_key_exists('categories', $data)) {
             $tags = (array) $data['categories'];
             unset($data['categories']);
         }
 
         // Set User-Agent for saved objects
         $app = kolab_sync::get_instance();
         $app->config->set('useragent', $app->app_name . ' ' . kolab_sync::VERSION);
 
         if ($folder && $folder->valid && $folder->save($data, $type, $uid)) {
             if (isset($tags)) {
                 $this->setCategories($uid, $tags);
             }
 
             return $uid;
         }
 
         return null;
     }
 
     /**
      * Returns list of categories assigned to an object
      *
      * @param object|string $object     UID or rcube_message object
      * @param array         $categories Addition tag names to merge with
      *
      * @return array List of categories
      */
     public function getCategories($object, $categories = [])
     {
         if (is_object($object)) {
             // support only messages with message-id
             if (!($msg_id = $object->headers->get('message-id', false))) {
                 return [];
             }
 
             $config = kolab_storage_config::get_instance();
             $delta  = Syncroton_Registry::getPingTimeout();
             $folder = $object->folder;
             $uid    = $object->uid;
 
             // get tag objects raleted to specified message-id
             $tags = $config->get_tags($msg_id);
 
             foreach ($tags as $idx => $tag) {
                 // resolve members if it wasn't done recently
                 $force   = empty($this->tag_rts[$tag['uid']]) || $this->tag_rts[$tag['uid']] <= time() - $delta;
                 $members = $config->resolve_members($tag, $force);
 
                 if (empty($members[$folder]) || !in_array($uid, $members[$folder])) {
                     unset($tags[$idx]);
                 }
 
                 if ($force) {
                     $this->tag_rts[$tag['uid']] = time();
                 }
             }
 
             // make sure current folder is set correctly again
             $this->storage->set_folder($folder);
         } else {
             $config = kolab_storage_config::get_instance();
             $tags   = $config->get_tags($object);
         }
 
         $tags = array_filter(array_map(function ($v) { return $v['name']; }, $tags));
 
         // merge result with old categories
         if (!empty($categories)) {
             $tags = array_unique(array_merge($tags, (array) $categories));
         }
 
         return $tags;
     }
 
     /**
      * Gets kolab_storage_folder object from Activesync folder ID.
      *
      * @param string $folderid Folder identifier
      * @param string $deviceid Device identifier
      * @param string $type     Activesync model name (folder type)
      *
      * @return ?kolab_storage_folder
      */
     public function getFolder($folderid, $deviceid, $type)
     {
         $unique_key = "$folderid:$deviceid:$type";
 
         if (array_key_exists($unique_key, $this->folders)) {
             return $this->folders[$unique_key];
         }
 
         $foldername = $this->folder_id2name($folderid, $deviceid);
 
         return $this->folders[$unique_key] = kolab_storage::get_folder($foldername, $type);
     }
 
     /**
      * Gets Activesync preferences for a folder.
      *
      * @param string $folderid Folder identifier
      * @param string $deviceid Device identifier
      * @param string $type     Activesync model name (folder type)
      *
      * @return array Folder preferences
      */
     public function getFolderConfig($folderid, $deviceid, $type)
     {
         $foldername = $this->folder_id2name($folderid, $deviceid);
 
         $metadata = $this->folder_meta();
         $config = [];
 
         if (!empty($metadata[$foldername]['FOLDER'][$deviceid])) {
             $config = $metadata[$foldername]['FOLDER'][$deviceid];
         }
 
         return [
             'ALARMS' => ($config['S'] ?? 0) == 2,
         ];
     }
 
     /**
      * Gets an item from a folder by UID.
      *
      * @param string $folderid Folder identifier
      * @param string $deviceid Device identifier
      * @param string $type     Activesync model name (folder type)
      * @param string $uid      Requested object UID
      *
      * @return array|rcube_message|null Object properties
      */
     public function getItem($folderid, $deviceid, $type, $uid)
     {
         if ($type == self::MODEL_EMAIL) {
             $foldername = $this->folder_id2name($folderid, $deviceid);
             $message = new rcube_message($uid, $foldername);
 
             if (!empty($message->headers)) {
                 if ($this->relationSupport) {
                     $message->headers->others['categories'] = $this->getCategories($message);
                 }
 
                 return $message;
             }
 
             return null;
         }
 
         $folder = $this->getFolder($folderid, $deviceid, $type);
 
         if (!$folder || !$folder->valid) {
             throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
         }
 
         $result = $folder->get_object($uid);
 
         if ($result === false) {
             throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
         }
 
         $useTags = $this->relationSupport && ($type == self::MODEL_TASKS || $type == self::MODEL_NOTES);
 
         if ($useTags) {
             $result['categories'] = $this->getCategories($uid, $result['categories'] ?? []);
         }
 
         return $result;
     }
 
     /**
      * Gets items matching UID by prefix.
      *
      * @param string $folderid Folder identifier
      * @param string $deviceid Device identifier
      * @param string $type     Activesync model name (folder type)
      * @param string $uid      Requested object UID prefix
      *
      * @return array|iterable List of objects
      */
     public function getItemsByUidPrefix($folderid, $deviceid, $type, $uid)
     {
         $folder = $this->getFolder($folderid, $deviceid, $type);
 
         if (!$folder || !$folder->valid) {
             throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
         }
 
         $result = $folder->select([['uid', '~*', $uid]]);
 
         if ($result === null) {
             throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
         }
 
         return $result;
     }
 
     /**
      * Move an item from one folder to another.
      *
      * @param string $srcFolderId Source folder identifier
      * @param string $deviceid    Device identifier
      * @param string $type        Activesync model name (folder type)
      * @param string $uid         Object UID
      * @param string $dstFolderId Destination folder identifier
      *
      * @return string New object UID
      * @throws Syncroton_Exception_Status
      */
     public function moveItem($srcFolderId, $deviceid, $type, $uid, $dstFolderId)
     {
         if ($type === self::MODEL_EMAIL) {
             $src_name = $this->folder_id2name($srcFolderId, $deviceid);
             $dst_name = $this->folder_id2name($dstFolderId, $deviceid);
 
             if ($dst_name === null) {
                 throw new Syncroton_Exception_Status_MoveItems(Syncroton_Exception_Status_MoveItems::INVALID_DESTINATION);
             }
 
             if ($src_name === null) {
                 throw new Syncroton_Exception_Status_MoveItems(Syncroton_Exception_Status_MoveItems::INVALID_SOURCE);
             }
 
             if (!$this->storage->move_message($uid, $dst_name, $src_name)) {
                 throw new Syncroton_Exception_Status_MoveItems(Syncroton_Exception_Status_MoveItems::INVALID_DESTINATION);
             }
 
             // Use COPYUID feature (RFC2359) to get the new UID of the copied message
             if (empty($this->storage->conn->data['COPYUID'])) {
                 throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
             }
 
             return $this->storage->conn->data['COPYUID'][1];
         }
 
         $srcFolder = $this->getFolder($srcFolderId, $deviceid, $type);
         $dstFolder = $this->getFolder($dstFolderId, $deviceid, $type);
 
         if (!$srcFolder || !$dstFolder) {
             throw new Syncroton_Exception_Status_MoveItems(Syncroton_Exception_Status_MoveItems::INVALID_DESTINATION);
         }
 
         if (!$srcFolder->move($uid, $dstFolder)) {
             throw new Syncroton_Exception_Status_MoveItems(Syncroton_Exception_Status_MoveItems::INVALID_SOURCE);
         }
 
         return $uid;
     }
 
     /**
      * Set categories to an object
      *
      * @param object|string $object     UID or rcube_message object
      * @param array         $categories List of Category names
      */
     public function setCategories($object, $categories)
     {
         if (!is_object($object)) {
             $config = kolab_storage_config::get_instance();
             $config->save_tags($object, $categories);
             return;
         }
 
         $config = kolab_storage_config::get_instance();
         $delta  = Syncroton_Registry::getPingTimeout();
         $uri    = kolab_storage_config::get_message_uri($object->headers, $object->folder);
 
         // for all tag objects...
         foreach ($config->get_tags() as $relation) {
             // resolve members if it wasn't done recently
             $uid     = $relation['uid'];
             $force   = empty($this->tag_rts[$uid]) || $this->tag_rts[$uid] <= time() - $delta;
 
             if ($force) {
                 $config->resolve_members($relation, $force);
                 $this->tag_rts[$relation['uid']] = time();
             }
 
             $selected = !empty($categories) && in_array($relation['name'], $categories);
             $found    = !empty($relation['members']) && in_array($uri, $relation['members']);
             $update   = false;
 
             // remove member from the relation
             if ($found && !$selected) {
                 $relation['members'] = array_diff($relation['members'], (array) $uri);
                 $update = true;
             }
             // add member to the relation
             elseif (!$found && $selected) {
                 $relation['members'][] = $uri;
                 $update = true;
             }
 
             if ($update) {
                 $config->save($relation, 'relation');
             }
 
             $categories = array_diff($categories, (array) $relation['name']);
         }
 
         // create new relations
         if (!empty($categories)) {
             foreach ($categories as $tag) {
                 $relation = [
                     'name'     => $tag,
                     'members'  => (array) $uri,
                     'category' => 'tag',
                 ];
 
                 $config->save($relation, 'relation');
             }
         }
 
         // make sure current folder is set correctly again
         $this->storage->set_folder($object->folder);
     }
 
     /**
      * Search for existing objects in a folder
      *
      * @param string $folderid    Folder identifier
      * @param string $deviceid    Device identifier
+     * @param string $deviceid_key    Device table primary key
      * @param string $type        Activesync model name (folder type)
      * @param array  $filter      Filter
      * @param int    $result_type Type of the result (see kolab_sync_data::RESULT_* constants)
      * @param bool   $force       Force IMAP folder cache synchronization
      *
      * @return array|int Search result as count or array of uids
      */
-    public function searchEntries($folderid, $deviceid, $type, $filter, $result_type, $force)
+    public function searchEntries($folderid, $deviceid, $deviceid_key, $type, $filter, $result_type, $force)
     {
         if ($type != self::MODEL_EMAIL) {
             return $this->searchKolabEntries($folderid, $deviceid, $type, $filter, $result_type, $force);
         }
 
         $filter_str = 'ALL UNDELETED';
 
         // convert filter into one IMAP search string
         foreach ($filter as $idx => $filter_item) {
             if (is_array($filter_item)) {
                 // This is a request for changes since last time
                 // we'll use HIGHESTMODSEQ value from the last Sync
                 if ($filter_item[0] == 'changed' && $filter_item[1] == '>') {
                     $modseq_lasttime = $filter_item[2];
                     $modseq_data     = [];
-                    $modseq = (array) $this->modseq_get($deviceid, $folderid, $modseq_lasttime);
+                    $modseq = (array) $this->modseq_get($deviceid_key, $folderid, $modseq_lasttime);
                 }
             } else {
                 $filter_str .= ' ' . $filter_item;
             }
         }
 
         // get members of modified relations
         if ($this->relationSupport) {
             $changed_msgs = $this->getChangesByRelations($folderid, $deviceid, $type, $filter);
         }
 
         $result = $result_type == kolab_sync_data::RESULT_COUNT ? 0 : [];
 
         $foldername = $this->folder_id2name($folderid, $deviceid);
 
         if ($foldername === null) {
             return $result;
         }
 
         $this->storage->set_folder($foldername);
 
         // Synchronize folder (if it wasn't synced in this request already)
         if ($force) {
             $this->storage->folder_sync($foldername);
         }
 
         // We're in "get changes" mode
         if (isset($modseq_data)) {
             $folder_data = $this->storage->folder_data($foldername);
             $modified    = false;
 
             // If previous HIGHESTMODSEQ doesn't exist we can't get changes
             // We can only get folder's HIGHESTMODSEQ value and store it for the next try
             // Skip search if HIGHESTMODSEQ didn't change
             if (!empty($folder_data['HIGHESTMODSEQ'])) {
                 $modseq_data[$foldername] = $folder_data['HIGHESTMODSEQ'];
                 $modseq_old = $modseq[$foldername] ?? null;
                 if ($modseq_data[$foldername] != $modseq_old) {
                     $modseq_update = true;
                     if (!empty($modseq) && $modseq_old) {
                         $modified    = true;
                         $filter_str .= " MODSEQ " . ($modseq_old + 1);
                     }
                 }
             }
         } else {
             $modified = true;
         }
 
         // We could use messages cache by replacing search() with index()
         // in some cases. This however is possible only if user has skip_deleted=true,
         // in his Roundcube preferences, otherwise we'd make often cache re-initialization,
         // because Roundcube message cache can work only with one skip_deleted
         // setting at a time. We'd also need to make sure folder_sync() was called
         // before (see above).
         //
         // if ($filter_str == 'ALL UNDELETED')
         //     $search = $this->storage->index($foldername, null, null, true, true);
         // else
 
         if ($modified) {
             $search = $this->storage->search_once($foldername, $filter_str);
 
             if (!($search instanceof rcube_result_index) || $search->is_error()) {
                 throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
             }
 
             switch ($result_type) {
                 case kolab_sync_data::RESULT_COUNT:
                     $result = $search->count();
                     break;
 
                 case kolab_sync_data::RESULT_UID:
                     $result = $search->get();
                     break;
             }
         }
 
         // handle relation changes
         if (!empty($changed_msgs)) {
             $members = $this->findRelationMembersInFolder($foldername, $changed_msgs, $filter);
 
             switch ($result_type) {
                 case kolab_sync_data::RESULT_COUNT:
                     $result += count($members);
                     break;
 
                 case kolab_sync_data::RESULT_UID:
                     $result = array_values(array_unique(array_merge($result, $members)));
                     break;
             }
         }
 
         if (!empty($modseq_update) && !empty($modseq_data)) {
-            $this->modseq_set($deviceid, $folderid, $this->syncTimeStamp, $modseq_data);
+            $this->modseq_set($deviceid_key, $folderid, $this->syncTimeStamp, $modseq_data);
 
             // if previous modseq information does not exist save current set as it,
             // we would at least be able to detect changes since now
             if (empty($result) && empty($modseq)) {
-                $this->modseq_set($deviceid, $folderid, $modseq_lasttime ?? 0, $modseq_data);
+                $this->modseq_set($deviceid_key, $folderid, $modseq_lasttime ?? 0, $modseq_data);
             }
         }
 
         return $result;
     }
 
     /**
      * Search for existing objects in a folder
      *
      * @param string $folderid    Folder identifier
      * @param string $deviceid    Device identifier
      * @param string $type        Activesync model name (folder type)
      * @param array  $filter      Filter
      * @param int    $result_type Type of the result (see kolab_sync_data::RESULT_* constants)
      * @param bool   $force       Force IMAP folder cache synchronization
      *
      * @return array|int Search result as count or array of uids
      */
     protected function searchKolabEntries($folderid, $deviceid, $type, $filter, $result_type, $force)
     {
         // there's a PHP Warning from kolab_storage if $filter isn't an array
         if (empty($filter)) {
             $filter = [];
         } elseif ($this->relationSupport && ($type == self::MODEL_TASKS || $type == self::MODEL_NOTES)) {
             $changed_objects = $this->getChangesByRelations($folderid, $deviceid, $type, $filter);
         }
 
         $folder = $this->getFolder($folderid, $deviceid, $type);
 
         if (!$folder || !$folder->valid) {
             throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
         }
 
         $error = false;
 
         switch ($result_type) {
             case kolab_sync_data::RESULT_COUNT:
                 $count = $folder->count($filter);
 
                 if ($count === null) {
                     throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
                 }
 
                 $result = (int) $count;
                 break;
 
             case kolab_sync_data::RESULT_UID:
             default:
                 $uids = $folder->get_uids($filter);
 
                 if (!is_array($uids)) {
                     throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
                 }
 
                 $result = $uids;
                 break;
         }
 
         // handle tag modifications
         if (!empty($changed_objects)) {
             // build new filter
             // search objects mathing current filter,
             // relations may contain members of many types, we need to
             // search them by UID in all requested folders to get
             // only these with requested type (and that really exist
             // in specified folders)
             $tag_filter = [['uid', '=', $changed_objects]];
             foreach ($filter as $f) {
                 if ($f[0] != 'changed') {
                     $tag_filter[] = $f;
                 }
             }
 
             switch ($result_type) {
                 case kolab_sync_data::RESULT_COUNT:
                     // Note: this way we're potentally counting the same objects twice
                     // I'm not sure if this is a problem, we most likely do not
                     // need a precise result here
                     $count = $folder->count($tag_filter);
                     if ($count !== null) {
                         $result += (int) $count;
                     }
 
                     break;
 
                 case kolab_sync_data::RESULT_UID:
                 default:
                     $uids = $folder->get_uids($tag_filter);
                     if (is_array($uids) && !empty($uids)) {
                         $result = array_unique(array_merge($result, $uids));
                     }
 
                     break;
             }
         }
 
         return $result;
     }
 
     /**
      * Find members (messages) in specified folder
      */
     protected function findRelationMembersInFolder($foldername, $members, $filter)
     {
         foreach ($members as $member) {
             // IMAP URI members
             if ($url = kolab_storage_config::parse_member_url($member)) {
                 $result[$url['folder']][$url['uid']] = $url['params'];
             }
         }
 
         // convert filter into one IMAP search string
         $filter_str = 'ALL UNDELETED';
         foreach ($filter as $filter_item) {
             if (is_string($filter_item)) {
                 $filter_str .= ' ' . $filter_item;
             }
         }
 
         $found = [];
 
         // first find messages by UID
         if (!empty($result[$foldername])) {
             $index = $this->storage->search_once($foldername, 'UID '
                 . rcube_imap_generic::compressMessageSet(array_keys($result[$foldername])));
             $found = $index->get();
 
             // remove found messages from the $result
             if (!empty($found)) {
                 $result[$foldername] = array_diff_key($result[$foldername], array_flip($found));
 
                 if (empty($result[$foldername])) {
                     unset($result[$foldername]);
                 }
 
                 // now apply the current filter to the found messages
                 $index = $this->storage->search_once($foldername, $filter_str . ' UID '
                     . rcube_imap_generic::compressMessageSet($found));
                 $found = $index->get();
             }
         }
 
         // search by message parameters
         if (!empty($result)) {
             // @TODO: do this search in chunks (for e.g. 25 messages)?
             $search       = '';
             $search_count = 0;
 
             foreach ($result as $data) {
                 foreach ($data as $p) {
                     $search_params = [];
                     $search_count++;
 
                     foreach ($p as $key => $val) {
                         $key = strtoupper($key);
                         // don't search by subject, we don't want false-positives
                         if ($key != 'SUBJECT') {
                             $search_params[] = 'HEADER ' . $key . ' ' . rcube_imap_generic::escape($val);
                         }
                     }
 
                     $search .= ' (' . implode(' ', $search_params) . ')';
                 }
             }
 
             $search_str = str_repeat(' OR', $search_count - 1) . $search;
 
             // search messages in current folder
             $search = $this->storage->search_once($foldername, $search_str);
             $uids   = $search->get();
 
             if (!empty($uids)) {
                 // add UIDs into the result
                 $found = array_unique(array_merge($found, $uids));
             }
         }
 
         return $found;
     }
 
     /**
      * Detect changes of relation (tag) objects data and assigned objects
      * Returns relation member identifiers
      */
     protected function getChangesByRelations($folderid, $deviceid, $type, $filter)
     {
         // get period filter, create new objects filter
         foreach ($filter as $f) {
             if ($f[0] == 'changed' && $f[1] == '>') {
                 $since = $f[2];
             }
         }
 
         // this is not search for changes, do nothing
         if (empty($since)) {
             return;
         }
 
         // get relations state from the last sync
         $last_state = (array) $this->relations_state_get($deviceid, $folderid, $since);
 
         // get current relations state
         $config  = kolab_storage_config::get_instance();
         $default = true;
         $filter  = [
             ['type', '=', 'relation'],
             ['category', '=', 'tag'],
         ];
 
         $relations = $config->get_objects($filter, $default, 100);
 
         $result  = [];
         $changed = false;
 
         // compare states, get members of changed relations
         foreach ($relations as $relation) {
             $rel_id = $relation['uid'];
 
             if ($relation['changed']) {
                 $relation['changed']->setTimezone(new DateTimeZone('UTC'));
             }
 
             // last state unknown...
             if (empty($last_state[$rel_id])) {
                 // ...get all members
                 if (!empty($relation['members'])) {
                     $changed = true;
                     $result  = array_merge($result, $relation['members']);
                 }
             }
             // last state known, changed tag name...
             elseif ($last_state[$rel_id]['name'] != $relation['name']) {
                 // ...get all (old and new) members
                 $members_old = explode("\n", $last_state[$rel_id]['members']);
                 $changed = true;
                 $members = array_unique(array_merge($relation['members'], $members_old));
                 $result  = array_merge($result, $members);
             }
             // last state known, any other change change...
             elseif ($last_state[$rel_id]['changed'] < $relation['changed']->format('U')) {
                 // ...find new and removed members
                 $members_old = explode("\n", $last_state[$rel_id]['members']);
                 $new     = array_diff($relation['members'], $members_old);
                 $removed = array_diff($members_old, $relation['members']);
 
                 if (!empty($new) || !empty($removed)) {
                     $changed = true;
                     $result  = array_merge($result, $new, $removed);
                 }
             }
 
             unset($last_state[$rel_id]);
         }
 
         // get members of deleted relations
         if (!empty($last_state)) {
             $changed = true;
             foreach ($last_state as $relation) {
                 $members = explode("\n", $relation['members']);
                 $result  = array_merge($result, $members);
             }
         }
 
         // save current state
         if ($changed) {
             $data = [];
             foreach ($relations as $relation) {
                 $data[$relation['uid']] = [
                     'name'    => $relation['name'],
                     'changed' => $relation['changed']->format('U'),
                     'members' => implode("\n", (array)$relation['members']),
                 ];
             }
 
             $now = new DateTime('now', new DateTimeZone('UTC'));
 
             $this->relations_state_set($deviceid, $folderid, $now, $data);
         }
 
         // in mail mode return only message URIs
         if ($type == self::MODEL_EMAIL) {
             // lambda function to skip email members
             $filter_func = function ($value) {
                 return strpos($value, 'imap://') === 0;
             };
 
             $result = array_filter(array_unique($result), $filter_func);
         }
         // otherwise return only object UIDs
         else {
             // lambda function to skip email members
             $filter_func = function ($value) {
                 return strpos($value, 'urn:uuid:') === 0;
             };
 
             // lambda function to parse member URI
             $member_func = function ($value) {
                 if (strpos($value, 'urn:uuid:') === 0) {
                     $value = substr($value, 9);
                 }
                 return $value;
             };
 
             $result = array_map($member_func, array_filter(array_unique($result), $filter_func));
         }
 
         return $result;
     }
 
     /**
      * Subscribe default set of folders on device registration
      */
     protected function device_init_subscriptions($deviceid)
     {
         // INBOX always exists
         $this->folder_set('INBOX', $deviceid, 1);
 
         $supported_types = [
             'mail.drafts',
             'mail.wastebasket',
             'mail.sentitems',
             'mail.outbox',
             'event.default',
             'contact.default',
             'note.default',
             'task.default',
             'event',
             'contact',
             'note',
             'task',
             'event.confidential',
             'event.private',
             'task.confidential',
             'task.private',
         ];
 
         $rcube   = rcube::get_instance();
         $config  = $rcube->config;
         $mode    = (int) $config->get('activesync_init_subscriptions');
         $folders = [];
 
         // Subscribe to default folders
         $foldertypes = kolab_storage::folders_typedata();
 
         if (!empty($foldertypes)) {
             $_foldertypes = array_intersect($foldertypes, $supported_types);
 
             // get default folders
             foreach ($_foldertypes as $folder => $type) {
                 // only personal folders
                 if ($this->storage->folder_namespace($folder) == 'personal') {
                     $flag = preg_match('/^(event|task)/', $type) ? 2 : 1;
                     $this->folder_set($folder, $deviceid, $flag);
                     $folders[] = $folder;
                 }
             }
         }
 
         // we're in default mode, exit
         if (!$mode) {
             return;
         }
 
         // below we support additionally all mail folders
         $supported_types[] = 'mail';
         $supported_types[] = 'mail.junkemail';
 
         // get configured special folders
         $special_folders = [];
         $map             = [
             'drafts' => 'mail.drafts',
             'junk'   => 'mail.junkemail',
             'sent'   => 'mail.sentitems',
             'trash'  => 'mail.wastebasket',
         ];
 
         foreach ($map as $folder => $type) {
             if ($folder = $config->get($folder . '_mbox')) {
                 $special_folders[$folder] = $type;
             }
         }
 
         // get folders list(s)
         if (($mode & self::INIT_ALL_PERSONAL) || ($mode & self::INIT_ALL_OTHER) || ($mode & self::INIT_ALL_SHARED)) {
             $all_folders = $this->storage->list_folders();
             if (($mode & self::INIT_SUB_PERSONAL) || ($mode & self::INIT_SUB_OTHER) || ($mode & self::INIT_SUB_SHARED)) {
                 $subscribed_folders = $this->storage->list_folders_subscribed();
             }
         } else {
             $all_folders = $this->storage->list_folders_subscribed();
         }
 
         foreach ($all_folders as $folder) {
             // folder already subscribed
             if (in_array($folder, $folders)) {
                 continue;
             }
 
             $type = ($foldertypes[$folder] ?? null) ?: 'mail';
             if ($type == 'mail' && isset($special_folders[$folder])) {
                 $type = $special_folders[$folder];
             }
 
             if (!in_array($type, $supported_types)) {
                 continue;
             }
 
             $ns = strtoupper($this->storage->folder_namespace($folder));
 
             // subscribe the folder according to configured mode
             // and folder namespace/subscription status
             if (($mode & constant("self::INIT_ALL_{$ns}"))
                 || (($mode & constant("self::INIT_SUB_{$ns}"))
                     && (!isset($subscribed_folders) || in_array($folder, $subscribed_folders)))
             ) {
                 $flag = preg_match('/^(event|task)/', $type) ? 2 : 1;
                 $this->folder_set($folder, $deviceid, $flag);
             }
         }
     }
 
     /**
      * Helper method to decode saved IMAP metadata
      */
     protected function unserialize_metadata($str)
     {
         if (!empty($str)) {
             $data = json_decode($str, true);
             return $data;
         }
 
         return null;
     }
 
     /**
      * Helper method to encode IMAP metadata for saving
      */
     protected function serialize_metadata($data)
     {
         if (!empty($data) && is_array($data)) {
             $data = json_encode($data);
             return $data;
         }
 
         return null;
     }
 
     /**
      * Returns Kolab folder type for specified ActiveSync type ID
      */
     protected static function type_activesync2kolab($type)
     {
         if (!empty(self::$types[$type])) {
             return self::$types[$type];
         }
 
         return '';
     }
 
     /**
      * Returns ActiveSync folder type for specified Kolab type
      */
     protected static function type_kolab2activesync($type)
     {
         $type = preg_replace('/\.(confidential|private)$/i', '', $type);
 
         if ($key = array_search($type, self::$types)) {
             return $key;
         }
 
         return key(self::$types);
     }
 
     /**
      * Returns folder data in Syncroton format
      */
     protected function folder_data($folder, $type)
     {
         // Folder name parameters
         $delim = $this->storage->get_hierarchy_delimiter();
         $items = explode($delim, $folder);
         $name  = array_pop($items);
 
         // Folder UID
         $folder_id = $this->folder_id($folder, $type);
 
         // Folder type
         if (strcasecmp($folder, 'INBOX') === 0) {
             // INBOX is always inbox, prevent from issues related with a change of
             // folder type annotation (it can be initially unset).
             $as_type = 2;
         } else {
             $as_type = self::type_kolab2activesync($type);
 
             // fix type, if there's no type annotation it's detected as UNKNOWN we'll use 'mail' (12)
             if ($as_type == 1) {
                 $as_type = 12;
             }
         }
 
         // Syncroton folder data array
         return [
             'serverId'    => $folder_id,
             'parentId'    => count($items) ? $this->folder_id(implode($delim, $items), $type) : 0,
             'displayName' => rcube_charset::convert($name, 'UTF7-IMAP', kolab_sync::CHARSET),
             'type'        => $as_type,
             // for internal use
             'imap_name'   => $folder,
         ];
     }
 
     /**
      * Builds folder ID based on folder name
      */
     protected function folder_id($name, $type = null)
     {
         // ActiveSync expects folder identifiers to be max.64 characters
         // So we can't use just folder name
 
         $name = (string) $name;
 
         if ($name === '') {
             return null;
         }
 
         if (isset($this->folder_uids[$name])) {
             return $this->folder_uids[$name];
         }
 
         /*
                 @TODO: For now uniqueid annotation doesn't work, we will create UIDs by ourselves.
                        There's one inconvenience of this solution: folder name/type change
                        would be handled in ActiveSync as delete + create.
 
                 // get folders unique identifier
                 $folderdata = $this->storage->get_metadata($name, self::UID_KEY);
 
                 if ($folderdata && !empty($folderdata[$name])) {
                     $uid = $folderdata[$name][self::UID_KEY];
                     return $this->folder_uids[$name] = $uid;
                 }
         */
         if (strcasecmp($name, 'INBOX') === 0) {
             // INBOX is always inbox, prevent from issues related with a change of
             // folder type annotation (it can be initially unset).
             $type = 'mail.inbox';
         } else {
             if ($type === null) {
                 $type = kolab_storage::folder_type($name);
             }
 
             if ($type != null) {
                 $type = preg_replace('/\.(confidential|private)$/i', '', $type);
             }
         }
 
         // Add type to folder UID hash, so type change can be detected by Syncroton
         $uid = $name . '!!' . $type;
         $uid = md5($uid);
 
         return $this->folder_uids[$name] = $uid;
     }
 
     /**
      * Returns IMAP folder name
      *
      * @param string $id        Folder identifier
      * @param string $deviceid  Device dentifier
      *
      * @return string|null Folder name (UTF7-IMAP)
      */
     public function folder_id2name($id, $deviceid)
     {
         // check in cache first
         if (!empty($this->folder_uids)) {
             if (($name = array_search($id, $this->folder_uids)) !== false) {
                 return $name;
             }
         }
         /*
                 @TODO: see folder_id()
 
                 // get folders unique identifier
                 $folderdata = $this->storage->get_metadata('*', self::UID_KEY);
 
                 foreach ((array)$folderdata as $folder => $data) {
                     if (!empty($data[self::UID_KEY])) {
                         $uid = $data[self::UID_KEY];
                         $this->folder_uids[$folder] = $uid;
                         if ($uid == $id) {
                             $name = $folder;
                         }
                     }
                 }
         */
         // get all folders of specified type
         $folderdata = $this->folder_meta();
 
         if (!is_array($folderdata) || empty($id)) {
             return null;
         }
 
         $name = null;
 
         // check if folders are "subscribed" for activesync
         foreach ($folderdata as $folder => $meta) {
             if (empty($meta['FOLDER']) || empty($meta['FOLDER'][$deviceid])
                 || empty($meta['FOLDER'][$deviceid]['S'])
             ) {
                 continue;
             }
 
             if ($uid = $this->folder_id($folder)) {
                 $this->folder_uids[$folder] = $uid;
             }
 
             if ($uid === $id) {
                 $name = $folder;
             }
         }
 
         return $name;
     }
 
     /**
      * Save MODSEQ value for a folder
      */
     protected function modseq_set($deviceid, $folderid, $synctime, $data)
     {
         $synctime = $synctime->format('Y-m-d H:i:s');
         $rcube    = rcube::get_instance();
         $db       = $rcube->get_dbh();
         $old_data = $this->modseq[$folderid][$synctime] ?? null;
 
         if (empty($old_data)) {
             $this->modseq[$folderid][$synctime] = $data;
             $data = json_encode($data);
 
             $db->set_option('ignore_key_errors', true);
             $db->query(
                 "INSERT INTO `syncroton_modseq` (`device_id`, `folder_id`, `synctime`, `data`)"
                 . " VALUES (?, ?, ?, ?)",
                 $deviceid,
                 $folderid,
                 $synctime,
                 $data
             );
             $db->set_option('ignore_key_errors', false);
         }
     }
 
     /**
      * Get stored MODSEQ value for a folder
      */
     protected function modseq_get($deviceid, $folderid, $synctime)
     {
         $synctime = $synctime->format('Y-m-d H:i:s');
 
         if (empty($this->modseq[$folderid][$synctime])) {
             $this->modseq[$folderid] = [];
 
             $rcube = rcube::get_instance();
             $db    = $rcube->get_dbh();
 
             $db->limitquery(
                 "SELECT `data`, `synctime` FROM `syncroton_modseq`"
                 . " WHERE `device_id` = ? AND `folder_id` = ? AND `synctime` <= ?"
                 . " ORDER BY `synctime` DESC",
                 0,
                 1,
                 $deviceid,
                 $folderid,
                 $synctime
             );
 
             if ($row = $db->fetch_assoc()) {
                 $synctime = $row['synctime'];
                 // @TODO: make sure synctime from sql is in "Y-m-d H:i:s" format
                 $this->modseq[$folderid][$synctime] = json_decode($row['data'], true);
             }
 
             // Cleanup: remove all records except the current one
             $db->query(
                 "DELETE FROM `syncroton_modseq`"
                 . " WHERE `device_id` = ? AND `folder_id` = ? AND `synctime` <> ?",
                 $deviceid,
                 $folderid,
                 $synctime
             );
         }
 
         return $this->modseq[$folderid][$synctime] ?? null;
     }
 
     /**
      * Set state of relation objects at specified point in time
      */
     public function relations_state_set($deviceid, $folderid, $synctime, $relations)
     {
         $synctime = $synctime->format('Y-m-d H:i:s');
         $rcube    = rcube::get_instance();
         $db       = $rcube->get_dbh();
         $old_data = $this->relations[$folderid][$synctime] ?? null;
 
         if (empty($old_data)) {
             $this->relations[$folderid][$synctime] = $relations;
             $data = rcube_charset::clean(json_encode($relations));
 
             $db->set_option('ignore_key_errors', true);
             $db->query(
                 "INSERT INTO `syncroton_relations_state`"
                 . " (`device_id`, `folder_id`, `synctime`, `data`)"
                 . " VALUES (?, ?, ?, ?)",
                 $deviceid,
                 $folderid,
                 $synctime,
                 $data
             );
             $db->set_option('ignore_key_errors', false);
         }
     }
 
     /**
      * Get state of relation objects at specified point in time
      */
     protected function relations_state_get($deviceid, $folderid, $synctime)
     {
         $synctime = $synctime->format('Y-m-d H:i:s');
 
         if (empty($this->relations[$folderid][$synctime])) {
             $this->relations[$folderid] = [];
 
             $rcube = rcube::get_instance();
             $db    = $rcube->get_dbh();
 
             $db->limitquery(
                 "SELECT `data`, `synctime` FROM `syncroton_relations_state`"
                 . " WHERE `device_id` = ? AND `folder_id` = ? AND `synctime` <= ?"
                 . " ORDER BY `synctime` DESC",
                 0,
                 1,
                 $deviceid,
                 $folderid,
                 $synctime
             );
 
             if ($row = $db->fetch_assoc()) {
                 $synctime = $row['synctime'];
                 // @TODO: make sure synctime from sql is in "Y-m-d H:i:s" format
                 $this->relations[$folderid][$synctime] = json_decode($row['data'], true);
             }
 
             // Cleanup: remove all records except the current one
             $db->query(
                 "DELETE FROM `syncroton_relations_state`"
                 . " WHERE `device_id` = ? AND `folder_id` = ? AND `synctime` <> ?",
                 $deviceid,
                 $folderid,
                 $synctime
             );
         }
 
         return $this->relations[$folderid][$synctime] ?? null;
     }
 
     /**
      * Return last storage error
      */
     public static function last_error()
     {
         return kolab_storage::$last_error;
     }
 
     /**
      * Compares two arrays
      *
      * @param array $array1
      * @param array $array2
      *
      * @return bool True if arrays differs, False otherwise
      */
     protected static function data_array_diff($array1, $array2)
     {
         if (!is_array($array1) || !is_array($array2)) {
             return $array1 != $array2;
         }
 
         if (count($array1) != count($array2)) {
             return true;
         }
 
         foreach ($array1 as $key => $val) {
             if (!array_key_exists($key, $array2)) {
                 return true;
             }
             if ($val !== $array2[$key]) {
                 return true;
             }
         }
 
         return false;
     }
 }