Branch 'kolab-syncroton-2.2' - 2 commits - lib/kolab_sync_backend.php lib/kolab_sync_data.php lib/plugins

Aleksander Machniak machniak at kolabsys.com
Thu Feb 5 11:56:27 CET 2015


 lib/kolab_sync_backend.php                                     |    9 
 lib/kolab_sync_data.php                                        |   48 
 lib/plugins/libkolab/SQL/mysql.initial.sql                     |   38 
 lib/plugins/libkolab/SQL/mysql/2013121100.sql                  |   13 
 lib/plugins/libkolab/SQL/mysql/2014021000.sql                  |    9 
 lib/plugins/libkolab/SQL/mysql/2014040900.sql                  |    8 
 lib/plugins/libkolab/SQL/mysql/2014112700.sql                  |    2 
 lib/plugins/libkolab/bin/modcache.sh                           |   29 
 lib/plugins/libkolab/lib/kolab_date_recurrence.php             |    2 
 lib/plugins/libkolab/lib/kolab_format.php                      |  145 +
 lib/plugins/libkolab/lib/kolab_format_configuration.php        |   59 
 lib/plugins/libkolab/lib/kolab_format_contact.php              |   43 
 lib/plugins/libkolab/lib/kolab_format_distributionlist.php     |   26 
 lib/plugins/libkolab/lib/kolab_format_event.php                |    2 
 lib/plugins/libkolab/lib/kolab_format_task.php                 |    7 
 lib/plugins/libkolab/lib/kolab_format_xcal.php                 |  163 +
 lib/plugins/libkolab/lib/kolab_storage.php                     |  189 +-
 lib/plugins/libkolab/lib/kolab_storage_cache.php               |  229 ++
 lib/plugins/libkolab/lib/kolab_storage_cache_configuration.php |   50 
 lib/plugins/libkolab/lib/kolab_storage_cache_contact.php       |   16 
 lib/plugins/libkolab/lib/kolab_storage_config.php              |  840 ++++++++++
 lib/plugins/libkolab/lib/kolab_storage_dataset.php             |  154 +
 lib/plugins/libkolab/lib/kolab_storage_folder.php              |  143 +
 23 files changed, 1998 insertions(+), 226 deletions(-)

New commits:
commit 7b5e7fbbb86c13b54ab12cde3fd162d8f35a2d13
Author: Aleksander Machniak <machniak at kolabsys.com>
Date:   Thu Feb 5 05:56:07 2015 -0500

    Handle kolab_storage errors correctly (#4418)
    
    As we already found out (in #4378) kolab_storage did't prevent from sql errors
    if some imap connection error happened. This is already fixed, however
    syncroton need to use 'valid' flag implemented in kolab_storage_folder
    to pass correct error codes to activesync client and to prevent from more issues.
    
    Conflicts:
    
    	lib/kolab_sync_data.php

diff --git a/lib/kolab_sync_backend.php b/lib/kolab_sync_backend.php
index b4bd3ca..f11f631 100644
--- a/lib/kolab_sync_backend.php
+++ b/lib/kolab_sync_backend.php
@@ -789,7 +789,7 @@ class kolab_sync_backend
         // get all folders of specified type
         $folderdata = $this->folder_meta();
 
-        if (!is_array($folderdata)) {
+        if (!is_array($folderdata) || $id === null) {
             return null;
         }
 
@@ -801,10 +801,11 @@ class kolab_sync_backend
                 continue;
             }
 
-            $uid = self::folder_id($folder);
-            $this->folder_uids[$folder] = $uid;
+            if ($uid = self::folder_id($folder)) {
+                $this->folder_uids[$folder] = $uid;
+            }
 
-            if ($uid == $id) {
+            if ($uid === $id) {
                 $name = $folder;
             }
         }
diff --git a/lib/kolab_sync_data.php b/lib/kolab_sync_data.php
index 3579a12..2e6a1e2 100644
--- a/lib/kolab_sync_data.php
+++ b/lib/kolab_sync_data.php
@@ -375,13 +375,12 @@ abstract class kolab_sync_data implements Syncroton_Data_IData
 
         foreach ($folders as $folderid) {
             $foldername = $this->backend->folder_id2name($folderid, $this->device->deviceid);
+            $folder     = $this->getFolderObject($foldername);
 
-            if ($foldername === null) {
-                continue;
+            if (!$folder || !$folder->valid) {
+                throw new Syncroton_Exception_Status_ItemOperations(Syncroton_Exception_Status_ItemOperations::ITEM_SERVER_ERROR);
             }
 
-            $folder = $this->getFolderObject($foldername);
-
             // Remove all entries
             $folder->delete_all();
 
@@ -390,13 +389,12 @@ abstract class kolab_sync_data implements Syncroton_Data_IData
                 $list = $this->listFolders($folderid);
                 foreach ($list as $folderid => $folder) {
                     $foldername = $this->backend->folder_id2name($folderid, $this->device->deviceid);
+                    $folder     = $this->getFolderObject($foldername);
 
-                    if ($foldername === null) {
-                        continue;
+                    if (!$folder || !$folder->valid) {
+                        throw new Syncroton_Exception_Status_ItemOperations(Syncroton_Exception_Status_ItemOperations::ITEM_SERVER_ERROR);
                     }
 
-                    $folder = $this->getFolderObject($foldername);
-
                     // Remove all entries
                     $folder->delete_all();
                 }
@@ -538,17 +536,12 @@ abstract class kolab_sync_data implements Syncroton_Data_IData
         $result = $result_type == self::RESULT_COUNT ? 0 : array();
         $found  = 0;
 
-        foreach ($folders as $folderid) {
-            $foldername = $this->backend->folder_id2name($folderid, $this->device->deviceid);
-
-            if ($foldername === null) {
-                continue;
-            }
+        foreach ($folders as $folder_id) {
+            $foldername = $this->backend->folder_id2name($folder_id, $this->device->deviceid);
+            $folder     = $this->getFolderObject($foldername);
 
-            $folder = $this->getFolderObject($foldername);
-
-            if (!$folder) {
-                continue;
+            if (!$folder || !$folder->valid) {
+                throw new Syncroton_Exception_Status(Syncroton_Exception_Status::SERVER_ERROR);
             }
 
             $found++;
@@ -753,14 +746,9 @@ abstract class kolab_sync_data implements Syncroton_Data_IData
 
         foreach ($folders as $folderid) {
             $foldername = $this->backend->folder_id2name($folderid, $this->device->deviceid);
+            $folder     = $this->getFolderObject($foldername);
 
-            if ($foldername === null) {
-                continue;
-            }
-
-            $folder = $this->getFolderObject($foldername);
-
-            if ($folder && ($object = $folder->get_object($entryid))) {
+            if ($folder && $folder->valid && ($object = $folder->get_object($entryid))) {
                 $object['_folderid'] = $folderid;
 
                 return $object;
@@ -786,7 +774,7 @@ abstract class kolab_sync_data implements Syncroton_Data_IData
         $foldername = $this->backend->folder_id2name($folderid, $this->device->deviceid);
         $folder     = $this->getFolderObject($foldername);
 
-        if ($folder && $folder->save($data)) {
+        if ($folder && $folder->valid && $folder->save($data)) {
             return $data;
         }
     }
@@ -801,7 +789,7 @@ abstract class kolab_sync_data implements Syncroton_Data_IData
         if ($object) {
             $folder = $this->getFolderObject($object['_mailbox']);
 
-            if ($folder && $folder->save($data)) {
+            if ($folder && $folder->valid && $folder->save($data)) {
                 return $data;
             }
         }
@@ -816,7 +804,7 @@ abstract class kolab_sync_data implements Syncroton_Data_IData
 
         if ($object) {
             $folder = $this->getFolderObject($object['_mailbox']);
-            return $folder && $folder->delete($entryid);
+            return $folder && $folder->valid && $folder->delete($entryid);
         }
 
         // object doesn't exist, confirm deletion
@@ -891,12 +879,12 @@ abstract class kolab_sync_data implements Syncroton_Data_IData
      */
     protected function getFolderObject($name)
     {
-        if ($name === null) {
+        if ($name === null || $name === '') {
             return null;
         }
 
         if (!isset($this->folders[$name])) {
-            $this->folders[$name] = kolab_storage::get_folder($name);
+            $this->folders[$name] = kolab_storage::get_folder($name, $this->modelName);
         }
 
         return $this->folders[$name];


commit 354fd67871e30c554f609515a6755dc6236d164c
Author: Aleksander Machniak <machniak at kolabsys.com>
Date:   Thu Feb 5 05:51:56 2015 -0500

    Update libkolab plugin (from roundcubemail-plugins-kolab-3.1 branch)

diff --git a/lib/plugins/libkolab/SQL/mysql.initial.sql b/lib/plugins/libkolab/SQL/mysql.initial.sql
index fcb51b0..cc83ad3 100644
--- a/lib/plugins/libkolab/SQL/mysql.initial.sql
+++ b/lib/plugins/libkolab/SQL/mysql.initial.sql
@@ -29,11 +29,15 @@ CREATE TABLE `kolab_cache_contact` (
   `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
   `created` DATETIME DEFAULT NULL,
   `changed` DATETIME DEFAULT NULL,
-  `data` TEXT NOT NULL,
-  `xml` LONGTEXT NOT NULL,
+  `data` LONGTEXT NOT NULL,
+  `xml` LONGBLOB NOT NULL,
   `tags` VARCHAR(255) NOT NULL,
   `words` TEXT NOT NULL,
   `type` VARCHAR(32) CHARACTER SET ascii NOT NULL,
+  `name` VARCHAR(255) NOT NULL,
+  `firstname` VARCHAR(255) NOT NULL,
+  `surname` VARCHAR(255) NOT NULL,
+  `email` VARCHAR(255) NOT NULL,
   CONSTRAINT `fk_kolab_cache_contact_folder` FOREIGN KEY (`folder_id`)
     REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
   PRIMARY KEY(`folder_id`,`msguid`),
@@ -48,8 +52,8 @@ CREATE TABLE `kolab_cache_event` (
   `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
   `created` DATETIME DEFAULT NULL,
   `changed` DATETIME DEFAULT NULL,
-  `data` TEXT NOT NULL,
-  `xml` TEXT NOT NULL,
+  `data` LONGTEXT NOT NULL,
+  `xml` LONGBLOB NOT NULL,
   `tags` VARCHAR(255) NOT NULL,
   `words` TEXT NOT NULL,
   `dtstart` DATETIME,
@@ -67,8 +71,8 @@ CREATE TABLE `kolab_cache_task` (
   `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
   `created` DATETIME DEFAULT NULL,
   `changed` DATETIME DEFAULT NULL,
-  `data` TEXT NOT NULL,
-  `xml` TEXT NOT NULL,
+  `data` LONGTEXT NOT NULL,
+  `xml` LONGBLOB NOT NULL,
   `tags` VARCHAR(255) NOT NULL,
   `words` TEXT NOT NULL,
   `dtstart` DATETIME,
@@ -86,8 +90,8 @@ CREATE TABLE `kolab_cache_journal` (
   `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
   `created` DATETIME DEFAULT NULL,
   `changed` DATETIME DEFAULT NULL,
-  `data` TEXT NOT NULL,
-  `xml` TEXT NOT NULL,
+  `data` LONGTEXT NOT NULL,
+  `xml` LONGBLOB NOT NULL,
   `tags` VARCHAR(255) NOT NULL,
   `words` TEXT NOT NULL,
   `dtstart` DATETIME,
@@ -105,8 +109,8 @@ CREATE TABLE `kolab_cache_note` (
   `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
   `created` DATETIME DEFAULT NULL,
   `changed` DATETIME DEFAULT NULL,
-  `data` TEXT NOT NULL,
-  `xml` TEXT NOT NULL,
+  `data` LONGTEXT NOT NULL,
+  `xml` LONGBLOB NOT NULL,
   `tags` VARCHAR(255) NOT NULL,
   `words` TEXT NOT NULL,
   CONSTRAINT `fk_kolab_cache_note_folder` FOREIGN KEY (`folder_id`)
@@ -122,8 +126,8 @@ CREATE TABLE `kolab_cache_file` (
   `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
   `created` DATETIME DEFAULT NULL,
   `changed` DATETIME DEFAULT NULL,
-  `data` TEXT NOT NULL,
-  `xml` TEXT NOT NULL,
+  `data` LONGTEXT NOT NULL,
+  `xml` LONGBLOB NOT NULL,
   `tags` VARCHAR(255) NOT NULL,
   `words` TEXT NOT NULL,
   `filename` varchar(255) DEFAULT NULL,
@@ -141,8 +145,8 @@ CREATE TABLE `kolab_cache_configuration` (
   `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
   `created` DATETIME DEFAULT NULL,
   `changed` DATETIME DEFAULT NULL,
-  `data` TEXT NOT NULL,
-  `xml` TEXT NOT NULL,
+  `data` LONGTEXT NOT NULL,
+  `xml` LONGBLOB NOT NULL,
   `tags` VARCHAR(255) NOT NULL,
   `words` TEXT NOT NULL,
   `type` VARCHAR(32) CHARACTER SET ascii NOT NULL,
@@ -160,8 +164,8 @@ CREATE TABLE `kolab_cache_freebusy` (
   `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
   `created` DATETIME DEFAULT NULL,
   `changed` DATETIME DEFAULT NULL,
-  `data` TEXT NOT NULL,
-  `xml` TEXT NOT NULL,
+  `data` LONGTEXT NOT NULL,
+  `xml` LONGBLOB NOT NULL,
   `tags` VARCHAR(255) NOT NULL,
   `words` TEXT NOT NULL,
   `dtstart` DATETIME,
@@ -172,4 +176,4 @@ CREATE TABLE `kolab_cache_freebusy` (
 ) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
 
 
-INSERT INTO `system` (`name`, `value`) VALUES ('libkolab-version', '2013110400');
+INSERT INTO `system` (`name`, `value`) VALUES ('libkolab-version', '2014021000');
diff --git a/lib/plugins/libkolab/SQL/mysql/2013121100.sql b/lib/plugins/libkolab/SQL/mysql/2013121100.sql
new file mode 100644
index 0000000..8cab5ef
--- /dev/null
+++ b/lib/plugins/libkolab/SQL/mysql/2013121100.sql
@@ -0,0 +1,13 @@
+-- well, these deletes are really optional
+-- we can clear all caches or only contacts/events/tasks
+-- the issue we're fixing here was about contacts (Bug #2662)
+DELETE FROM `kolab_folders` WHERE `type` IN ('contact', 'event', 'task');
+
+ALTER TABLE `kolab_cache_contact` CHANGE `xml` `xml` LONGBLOB NOT NULL;
+ALTER TABLE `kolab_cache_event` CHANGE `xml` `xml` LONGBLOB NOT NULL;
+ALTER TABLE `kolab_cache_task` CHANGE `xml` `xml` LONGBLOB NOT NULL;
+ALTER TABLE `kolab_cache_journal` CHANGE `xml` `xml` LONGBLOB NOT NULL;
+ALTER TABLE `kolab_cache_note` CHANGE `xml` `xml` LONGBLOB NOT NULL;
+ALTER TABLE `kolab_cache_file` CHANGE `xml` `xml` LONGBLOB NOT NULL;
+ALTER TABLE `kolab_cache_configuration` CHANGE `xml` `xml` LONGBLOB NOT NULL;
+ALTER TABLE `kolab_cache_freebusy` CHANGE `xml` `xml` LONGBLOB NOT NULL;
diff --git a/lib/plugins/libkolab/SQL/mysql/2014021000.sql b/lib/plugins/libkolab/SQL/mysql/2014021000.sql
new file mode 100644
index 0000000..31ce699
--- /dev/null
+++ b/lib/plugins/libkolab/SQL/mysql/2014021000.sql
@@ -0,0 +1,9 @@
+ALTER TABLE `kolab_cache_contact` ADD `name` VARCHAR(255) NOT NULL,
+  ADD `firstname` VARCHAR(255) NOT NULL,
+  ADD `surname` VARCHAR(255) NOT NULL,
+  ADD `email` VARCHAR(255) NOT NULL;
+
+-- updating or clearing all contacts caches is required.
+-- either run `bin/modcache.sh update --type=contact` or execute the following query:
+--   DELETE FROM `kolab_folders` WHERE `type`='contact';
+
diff --git a/lib/plugins/libkolab/SQL/mysql/2014040900.sql b/lib/plugins/libkolab/SQL/mysql/2014040900.sql
new file mode 100644
index 0000000..61649c1
--- /dev/null
+++ b/lib/plugins/libkolab/SQL/mysql/2014040900.sql
@@ -0,0 +1,8 @@
+ALTER TABLE `kolab_cache_contact` CHANGE `data` `data` LONGTEXT NOT NULL;
+ALTER TABLE `kolab_cache_event` CHANGE `data` `data` LONGTEXT NOT NULL;
+ALTER TABLE `kolab_cache_task` CHANGE `data` `data` LONGTEXT NOT NULL;
+ALTER TABLE `kolab_cache_journal` CHANGE `data` `data` LONGTEXT NOT NULL;
+ALTER TABLE `kolab_cache_note` CHANGE `data` `data` LONGTEXT NOT NULL;
+ALTER TABLE `kolab_cache_file` CHANGE `data` `data` LONGTEXT NOT NULL;
+ALTER TABLE `kolab_cache_configuration` CHANGE `data` `data` LONGTEXT NOT NULL;
+ALTER TABLE `kolab_cache_freebusy` CHANGE `data` `data` LONGTEXT NOT NULL;
diff --git a/lib/plugins/libkolab/SQL/mysql/2014112700.sql b/lib/plugins/libkolab/SQL/mysql/2014112700.sql
new file mode 100644
index 0000000..90c77b8
--- /dev/null
+++ b/lib/plugins/libkolab/SQL/mysql/2014112700.sql
@@ -0,0 +1,2 @@
+-- delete cache entries for old folder identifiers
+DELETE FROM `kolab_folders` WHERE `resource` LIKE 'imap://anonymous@%';
diff --git a/lib/plugins/libkolab/bin/modcache.sh b/lib/plugins/libkolab/bin/modcache.sh
index da6e4f8..550a7d6 100755
--- a/lib/plugins/libkolab/bin/modcache.sh
+++ b/lib/plugins/libkolab/bin/modcache.sh
@@ -7,7 +7,7 @@
  * @version 3.1
  * @author Thomas Bruederli <bruederli at kolabsys.com>
  *
- * Copyright (C) 2012, Kolab Systems AG <contact at kolabsys.com>
+ * Copyright (C) 2012-2014, Kolab Systems AG <contact at kolabsys.com>
  *
  * This program is free software: you can redistribute it and/or modify
  * it under the terms of the GNU Affero General Public License as
@@ -65,6 +65,7 @@ $db->db_connect('w');
 if (!$db->is_connected() || $db->is_error())
     die("No DB connection\n");
 
+ini_set('display_errors', 1);
 
 /*
  * Script controller
@@ -142,6 +143,32 @@ case 'prewarm':
         die("Authentication failed for " . $opts['user']);
     break;
 
+/**
+ * Update the cache meta columns from the serialized/xml data
+ * (might be run after a schema update)
+ */
+case 'update':
+    // make sure libkolab classes are loaded
+    $rcmail->plugins->load_plugin('libkolab');
+
+    $folder_types = $opts['type'] ? explode(',', $opts['type']) : array('contact','configuration','event','file','task');
+    foreach ($folder_types as $type) {
+        $class = 'kolab_storage_cache_' . $type;
+        $sql_result = $db->query("SELECT folder_id FROM kolab_folders WHERE type=? AND synclock = 0", $type);
+        while ($sql_result && ($sql_arr = $db->fetch_assoc($sql_result))) {
+            $folder = new $class;
+            $folder->select_by_id($sql_arr['folder_id']);
+            echo "Updating " . $sql_arr['folder_id'] . " ($type) ";
+            foreach ($folder->select() as $object) {
+                $object['_formatobj']->to_array();  // load data
+                $folder->save($object['_msguid'], $object, $object['_msguid']);
+                echo ".";
+            }
+            echo "done.\n";
+        }
+    }
+    break;
+
 
 /*
  * Unknown action => show usage
diff --git a/lib/plugins/libkolab/lib/kolab_date_recurrence.php b/lib/plugins/libkolab/lib/kolab_date_recurrence.php
index 85ffd91..1dc63be 100644
--- a/lib/plugins/libkolab/lib/kolab_date_recurrence.php
+++ b/lib/plugins/libkolab/lib/kolab_date_recurrence.php
@@ -118,7 +118,7 @@ class kolab_date_recurrence
         }
 
         // determine a reasonable end date if none given
-        if (!$event['recurrence']['COUNT']) {
+        if (!$event['recurrence']['COUNT'] && $event['start'] instanceof DateTime) {
           switch ($event['recurrence']['FREQ']) {
             case 'YEARLY':  $intvl = 'P100Y'; break;
             case 'MONTHLY': $intvl = 'P20Y';  break;
diff --git a/lib/plugins/libkolab/lib/kolab_format.php b/lib/plugins/libkolab/lib/kolab_format.php
index 9dc8a31..fa85eec 100644
--- a/lib/plugins/libkolab/lib/kolab_format.php
+++ b/lib/plugins/libkolab/lib/kolab_format.php
@@ -47,6 +47,117 @@ abstract class kolab_format
     const KTYPE_PREFIX = 'application/x-vnd.kolab.';
     const PRODUCT_ID = 'Roundcube-libkolab-0.9';
 
+    // mapping table for valid PHP timezones not supported by libkolabxml
+    // basically the entire list of ftp://ftp.iana.org/tz/data/backward
+    protected static $timezone_map = array(
+        'Africa/Asmera' => 'Africa/Asmara',
+        'Africa/Timbuktu' => 'Africa/Abidjan',
+        'America/Argentina/ComodRivadavia' => 'America/Argentina/Catamarca',
+        'America/Atka' => 'America/Adak',
+        'America/Buenos_Aires' => 'America/Argentina/Buenos_Aires',
+        'America/Catamarca' => 'America/Argentina/Catamarca',
+        'America/Coral_Harbour' => 'America/Atikokan',
+        'America/Cordoba' => 'America/Argentina/Cordoba',
+        'America/Ensenada' => 'America/Tijuana',
+        'America/Fort_Wayne' => 'America/Indiana/Indianapolis',
+        'America/Indianapolis' => 'America/Indiana/Indianapolis',
+        'America/Jujuy' => 'America/Argentina/Jujuy',
+        'America/Knox_IN' => 'America/Indiana/Knox',
+        'America/Louisville' => 'America/Kentucky/Louisville',
+        'America/Mendoza' => 'America/Argentina/Mendoza',
+        'America/Porto_Acre' => 'America/Rio_Branco',
+        'America/Rosario' => 'America/Argentina/Cordoba',
+        'America/Virgin' => 'America/Port_of_Spain',
+        'Asia/Ashkhabad' => 'Asia/Ashgabat',
+        'Asia/Calcutta' => 'Asia/Kolkata',
+        'Asia/Chungking' => 'Asia/Shanghai',
+        'Asia/Dacca' => 'Asia/Dhaka',
+        'Asia/Katmandu' => 'Asia/Kathmandu',
+        'Asia/Macao' => 'Asia/Macau',
+        'Asia/Saigon' => 'Asia/Ho_Chi_Minh',
+        'Asia/Tel_Aviv' => 'Asia/Jerusalem',
+        'Asia/Thimbu' => 'Asia/Thimphu',
+        'Asia/Ujung_Pandang' => 'Asia/Makassar',
+        'Asia/Ulan_Bator' => 'Asia/Ulaanbaatar',
+        'Atlantic/Faeroe' => 'Atlantic/Faroe',
+        'Atlantic/Jan_Mayen' => 'Europe/Oslo',
+        'Australia/ACT' => 'Australia/Sydney',
+        'Australia/Canberra' => 'Australia/Sydney',
+        'Australia/LHI' => 'Australia/Lord_Howe',
+        'Australia/NSW' => 'Australia/Sydney',
+        'Australia/North' => 'Australia/Darwin',
+        'Australia/Queensland' => 'Australia/Brisbane',
+        'Australia/South' => 'Australia/Adelaide',
+        'Australia/Tasmania' => 'Australia/Hobart',
+        'Australia/Victoria' => 'Australia/Melbourne',
+        'Australia/West' => 'Australia/Perth',
+        'Australia/Yancowinna' => 'Australia/Broken_Hill',
+        'Brazil/Acre' => 'America/Rio_Branco',
+        'Brazil/DeNoronha' => 'America/Noronha',
+        'Brazil/East' => 'America/Sao_Paulo',
+        'Brazil/West' => 'America/Manaus',
+        'Canada/Atlantic' => 'America/Halifax',
+        'Canada/Central' => 'America/Winnipeg',
+        'Canada/East-Saskatchewan' => 'America/Regina',
+        'Canada/Eastern' => 'America/Toronto',
+        'Canada/Mountain' => 'America/Edmonton',
+        'Canada/Newfoundland' => 'America/St_Johns',
+        'Canada/Pacific' => 'America/Vancouver',
+        'Canada/Saskatchewan' => 'America/Regina',
+        'Canada/Yukon' => 'America/Whitehorse',
+        'Chile/Continental' => 'America/Santiago',
+        'Chile/EasterIsland' => 'Pacific/Easter',
+        'Cuba' => 'America/Havana',
+        'Egypt' => 'Africa/Cairo',
+        'Eire' => 'Europe/Dublin',
+        'Europe/Belfast' => 'Europe/London',
+        'Europe/Tiraspol' => 'Europe/Chisinau',
+        'GB' => 'Europe/London',
+        'GB-Eire' => 'Europe/London',
+        'Greenwich' => 'Etc/GMT',
+        'Hongkong' => 'Asia/Hong_Kong',
+        'Iceland' => 'Atlantic/Reykjavik',
+        'Iran' => 'Asia/Tehran',
+        'Israel' => 'Asia/Jerusalem',
+        'Jamaica' => 'America/Jamaica',
+        'Japan' => 'Asia/Tokyo',
+        'Kwajalein' => 'Pacific/Kwajalein',
+        'Libya' => 'Africa/Tripoli',
+        'Mexico/BajaNorte' => 'America/Tijuana',
+        'Mexico/BajaSur' => 'America/Mazatlan',
+        'Mexico/General' => 'America/Mexico_City',
+        'NZ' => 'Pacific/Auckland',
+        'NZ-CHAT' => 'Pacific/Chatham',
+        'Navajo' => 'America/Denver',
+        'PRC' => 'Asia/Shanghai',
+        'Pacific/Ponape' => 'Pacific/Pohnpei',
+        'Pacific/Samoa' => 'Pacific/Pago_Pago',
+        'Pacific/Truk' => 'Pacific/Chuuk',
+        'Pacific/Yap' => 'Pacific/Chuuk',
+        'Poland' => 'Europe/Warsaw',
+        'Portugal' => 'Europe/Lisbon',
+        'ROC' => 'Asia/Taipei',
+        'ROK' => 'Asia/Seoul',
+        'Singapore' => 'Asia/Singapore',
+        'Turkey' => 'Europe/Istanbul',
+        'UCT' => 'Etc/UCT',
+        'US/Alaska' => 'America/Anchorage',
+        'US/Aleutian' => 'America/Adak',
+        'US/Arizona' => 'America/Phoenix',
+        'US/Central' => 'America/Chicago',
+        'US/East-Indiana' => 'America/Indiana/Indianapolis',
+        'US/Eastern' => 'America/New_York',
+        'US/Hawaii' => 'Pacific/Honolulu',
+        'US/Indiana-Starke' => 'America/Indiana/Knox',
+        'US/Michigan' => 'America/Detroit',
+        'US/Mountain' => 'America/Denver',
+        'US/Pacific' => 'America/Los_Angeles',
+        'US/Samoa' => 'Pacific/Pago_Pago',
+        'Universal' => 'Etc/UTC',
+        'W-SU' => 'Europe/Moscow',
+        'Zulu' => 'Etc/UTC',
+    );
+
     /**
      * Factory method to instantiate a kolab_format object of the given type and version
      *
@@ -63,7 +174,7 @@ abstract class kolab_format
         if (!self::supports($version))
             return PEAR::raiseError("No support for Kolab format version " . $version);
 
-        $type = preg_replace('/configuration\.[a-z.]+$/', 'configuration', $type);
+        $type = preg_replace('/configuration\.[a-z._]+$/', 'configuration', $type);
         $suffix = preg_replace('/[^a-z]+/', '', $type);
         $classname = 'kolab_format_' . $suffix;
         if (class_exists($classname))
@@ -123,10 +234,15 @@ abstract class kolab_format
             if (!$dateonly)
                 $result->setTime($datetime->format('G'), $datetime->format('i'), $datetime->format('s'));
 
-            if ($tz && in_array($tz->getName(), array('UTC','GMT','+00:00')))
+            if ($tz && in_array($tz->getName(), array('UTC', 'GMT', '+00:00', 'Z'))) {
                 $result->setUTC(true);
-            else if ($tz !== false)
-                $result->setTimezone($tz->getName());
+            }
+            else if ($tz !== false) {
+                $tzid = $tz->getName();
+                if (array_key_exists($tzid, self::$timezone_map))
+                    $tzid = self::$timezone_map[$tzid];
+                $result->setTimezone($tzid);
+            }
         }
 
         return $result;
@@ -208,7 +324,11 @@ abstract class kolab_format
      */
     public static function mime2object_type($x_kolab_type)
     {
-        return preg_replace('/dictionary.[a-z.]+$/', 'dictionary', substr($x_kolab_type, strlen(self::KTYPE_PREFIX)));
+        return preg_replace(
+            array('/dictionary.[a-z.]+$/', '/contact.distlist$/'),
+            array( 'dictionary',            'distribution-list'),
+            substr($x_kolab_type, strlen(self::KTYPE_PREFIX))
+        );
     }
 
 
@@ -410,7 +530,7 @@ abstract class kolab_format
         $this->obj->setLastModified(self::get_datetime($object['changed']));
 
         // Save custom properties of the given object
-        if (isset($object['x-custom'])) {
+        if (isset($object['x-custom']) && method_exists($this->obj, 'setCustomProperties')) {
             $vcustom = new vectorcs;
             foreach ((array)$object['x-custom'] as $cp) {
                 if (is_array($cp))
@@ -418,7 +538,8 @@ abstract class kolab_format
             }
             $this->obj->setCustomProperties($vcustom);
         }
-        else {  // load custom properties from XML for caching (#2238)
+        // load custom properties from XML for caching (#2238) if method exists (#3125)
+        else if (method_exists($this->obj, 'customProperties')) {
             $object['x-custom'] = array();
             $vcustom = $this->obj->customProperties();
             for ($i=0; $i < $vcustom->size(); $i++) {
@@ -451,10 +572,12 @@ abstract class kolab_format
         }
 
         // read custom properties
-        $vcustom = $this->obj->customProperties();
-        for ($i=0; $i < $vcustom->size(); $i++) {
-            $cp = $vcustom->get($i);
-            $object['x-custom'][] = array($cp->identifier, $cp->value);
+        if (method_exists($this->obj, 'customProperties')) {
+            $vcustom = $this->obj->customProperties();
+            for ($i=0; $i < $vcustom->size(); $i++) {
+                $cp = $vcustom->get($i);
+                $object['x-custom'][] = array($cp->identifier, $cp->value);
+            }
         }
 
         // merge with additional data, e.g. attachments from the message
diff --git a/lib/plugins/libkolab/lib/kolab_format_configuration.php b/lib/plugins/libkolab/lib/kolab_format_configuration.php
index 5a8d3ff..b555484 100644
--- a/lib/plugins/libkolab/lib/kolab_format_configuration.php
+++ b/lib/plugins/libkolab/lib/kolab_format_configuration.php
@@ -32,10 +32,12 @@ class kolab_format_configuration extends kolab_format
     protected $write_func = 'writeConfiguration';
 
     private $type_map = array(
-        'dictionary' => Configuration::TypeDictionary,
-        'category' => Configuration::TypeCategoryColor,
+        'category'    => Configuration::TypeCategoryColor,
+        'dictionary'  => Configuration::TypeDictionary,
+        'file_driver' => Configuration::TypeFileDriver,
     );
 
+    private $driver_settings_fields = array('host', 'port', 'username', 'password');
 
     /**
      * Set properties to the kolabformat object
@@ -60,6 +62,22 @@ class kolab_format_configuration extends kolab_format
             $categories = new vectorcategorycolor;
             $this->obj = new Configuration($categories);
             break;
+
+        case 'file_driver':
+            $driver = new FileDriver($object['driver'], $object['title']);
+
+            $driver->setEnabled((bool) $object['enabled']);
+
+            foreach ($this->driver_settings_fields as $field) {
+                $value = $object[$field];
+                if ($value !== null) {
+                    $driver->{'set' . ucfirst($field)}($value);
+                }
+            }
+
+            $this->obj = new Configuration($driver);
+            break;
+
         default:
             return false;
         }
@@ -111,6 +129,19 @@ class kolab_format_configuration extends kolab_format
         case 'category':
             // TODO: implement this
             break;
+
+        case 'file_driver':
+            $driver = $this->obj->fileDriver();
+
+            $object['driver']  = $driver->driver();
+            $object['title']   = $driver->title();
+            $object['enabled'] = $driver->enabled();
+
+            foreach ($this->driver_settings_fields as $field) {
+                $object[$field] = $driver->{$field}();
+            }
+
+            break;
         }
 
         // adjust content-type string
@@ -136,4 +167,28 @@ class kolab_format_configuration extends kolab_format
         return $tags;
     }
 
+    /**
+     * Callback for kolab_storage_cache to get words to index for fulltext search
+     *
+     * @return array List of words to save in cache
+     */
+    public function get_words()
+    {
+        $words = array();
+
+        foreach ((array)$this->data['members'] as $url) {
+            $member = kolab_storage_config::parse_member_url($url);
+
+            if (empty($member)) {
+                if (strpos($url, 'urn:uuid:') === 0) {
+                    $words[] = substr($url, 9);
+                }
+            }
+            else if (!empty($member['params']['message-id'])) {
+                $words[] = $member['params']['message-id'];
+            }
+        }
+
+        return $words;
+    }
 }
diff --git a/lib/plugins/libkolab/lib/kolab_format_contact.php b/lib/plugins/libkolab/lib/kolab_format_contact.php
index 0d0bc75..a96312a 100644
--- a/lib/plugins/libkolab/lib/kolab_format_contact.php
+++ b/lib/plugins/libkolab/lib/kolab_format_contact.php
@@ -107,8 +107,8 @@ class kolab_format_contact extends kolab_format
 
         if (isset($object['nickname']))
             $this->obj->setNickNames(self::array2vector($object['nickname']));
-        if (isset($object['profession']))
-            $this->obj->setTitles(self::array2vector($object['profession']));
+        if (isset($object['jobtitle']))
+            $this->obj->setTitles(self::array2vector($object['jobtitle']));
 
         // organisation related properties (affiliation)
         $org = new Affiliation;
@@ -117,17 +117,17 @@ class kolab_format_contact extends kolab_format
             $org->setOrganisation($object['organization']);
         if ($object['department'])
             $org->setOrganisationalUnits(self::array2vector($object['department']));
-        if ($object['jobtitle'])
-            $org->setRoles(self::array2vector($object['jobtitle']));
+        if ($object['profession'])
+            $org->setRoles(self::array2vector($object['profession']));
 
         $rels = new vectorrelated;
-        if ($object['manager']) {
-            foreach ((array)$object['manager'] as $manager)
-                $rels->push(new Related(Related::Text, $manager, Related::Manager));
-        }
-        if ($object['assistant']) {
-            foreach ((array)$object['assistant'] as $assistant)
-                $rels->push(new Related(Related::Text, $assistant, Related::Assistant));
+        foreach (array('manager','assistant') as $field) {
+            if (!empty($object[$field])) {
+                $reltype = $this->relatedmap[$field];
+                foreach ((array)$object[$field] as $value) {
+                    $rels->push(new Related(Related::Text, $value, $reltype));
+                }
+            }
         }
         $org->setRelateds($rels);
 
@@ -219,12 +219,13 @@ class kolab_format_contact extends kolab_format
 
         // spouse and children are relateds
         $rels = new vectorrelated;
-        if ($object['spouse']) {
-            $rels->push(new Related(Related::Text, $object['spouse'], Related::Spouse));
-        }
-        if ($object['children']) {
-            foreach ((array)$object['children'] as $child)
-                $rels->push(new Related(Related::Text, $child, Related::Child));
+        foreach (array('spouse','children') as $field) {
+            if (!empty($object[$field])) {
+                $reltype = $this->relatedmap[$field];
+                foreach ((array)$object[$field] as $value) {
+                    $rels->push(new Related(Related::Text, $value, $reltype));
+                }
+            }
         }
         $this->obj->setRelateds($rels);
 
@@ -296,7 +297,7 @@ class kolab_format_contact extends kolab_format
         $object['prefix']     = join(' ', self::vector2array($nc->prefixes()));
         $object['suffix']     = join(' ', self::vector2array($nc->suffixes()));
         $object['nickname']   = join(' ', self::vector2array($this->obj->nickNames()));
-        $object['profession'] = join(' ', self::vector2array($this->obj->titles()));
+        $object['jobtitle']   = join(' ', self::vector2array($this->obj->titles()));
         $object['categories'] = self::vector2array($this->obj->categories());
 
         // organisation related properties (affiliation)
@@ -304,7 +305,7 @@ class kolab_format_contact extends kolab_format
         if ($orgs->size()) {
             $org = $orgs->get(0);
             $object['organization']   = $org->organisation();
-            $object['jobtitle']       = join(' ', self::vector2array($org->roles()));
+            $object['profession']     = join(' ', self::vector2array($org->roles()));
             $object['department']     = join(' ', self::vector2array($org->organisationalUnits()));
             $this->read_relateds($org->relateds(), $object);
         }
@@ -347,10 +348,10 @@ class kolab_format_contact extends kolab_format
         $object['freebusyurl'] = $this->obj->freeBusyUrl();
 
         if ($bday = self::php_datetime($this->obj->bDay()))
-            $object['birthday'] = $bday->format('c');
+            $object['birthday'] = $bday;
 
         if ($anniversary = self::php_datetime($this->obj->anniversary()))
-            $object['anniversary'] = $anniversary->format('c');
+            $object['anniversary'] = $anniversary;
 
         $gendermap = array_flip($this->gendermap);
         if (($g = $this->obj->gender()) && $gendermap[$g])
diff --git a/lib/plugins/libkolab/lib/kolab_format_distributionlist.php b/lib/plugins/libkolab/lib/kolab_format_distributionlist.php
index 46dda01..88c6f7b 100644
--- a/lib/plugins/libkolab/lib/kolab_format_distributionlist.php
+++ b/lib/plugins/libkolab/lib/kolab_format_distributionlist.php
@@ -44,17 +44,29 @@ class kolab_format_distributionlist extends kolab_format
 
         $this->obj->setName($object['name']);
 
+        $seen = array();
         $members = new vectorcontactref;
-        foreach ((array)$object['member'] as $member) {
-            if ($member['uid'])
+        foreach ((array)$object['member'] as $i => $member) {
+            if ($member['uid']) {
+                $key = 'uid:' . $member['uid'];
                 $m = new ContactReference(ContactReference::UidReference, $member['uid']);
-            else if ($member['email'])
+            }
+            else if ($member['email']) {
+                $key = 'mailto:' . $member['email'];
                 $m = new ContactReference(ContactReference::EmailReference, $member['email']);
-            else
+                $m->setName($member['name']);
+            }
+            else {
                 continue;
-
-            $m->setName($member['name']);
-            $members->push($m);
+            }
+
+            if (!$seen[$key]++) {
+                $members->push($m);
+            }
+            else {
+                // remove dupes for caching
+                unset($object['member'][$i]);
+            }
         }
 
         $this->obj->setMembers($members);
diff --git a/lib/plugins/libkolab/lib/kolab_format_event.php b/lib/plugins/libkolab/lib/kolab_format_event.php
index 6a8c3ae..7d1d53c 100644
--- a/lib/plugins/libkolab/lib/kolab_format_event.php
+++ b/lib/plugins/libkolab/lib/kolab_format_event.php
@@ -87,6 +87,8 @@ class kolab_format_event extends kolab_format_xcal
             $status = kolabformat::StatusTentative;
         if ($object['cancelled'])
             $status = kolabformat::StatusCancelled;
+        else if ($object['status'] && array_key_exists($object['status'], $this->status_map))
+            $status = $this->status_map[$object['status']];
         $this->obj->setStatus($status);
 
         // save recurrence exceptions
diff --git a/lib/plugins/libkolab/lib/kolab_format_task.php b/lib/plugins/libkolab/lib/kolab_format_task.php
index 465ba90..555e77f 100644
--- a/lib/plugins/libkolab/lib/kolab_format_task.php
+++ b/lib/plugins/libkolab/lib/kolab_format_task.php
@@ -42,6 +42,8 @@ class kolab_format_task extends kolab_format_xcal
         parent::set($object);
 
         $this->obj->setPercentComplete(intval($object['complete']));
+        $this->obj->setStart(self::get_datetime($object['start'], null, $object['start']->_dateonly));
+        $this->obj->setDue(self::get_datetime($object['due'], null, $object['due']->_dateonly));
 
         $status = kolabformat::StatusUndefined;
         if ($object['complete'] == 100)
@@ -50,9 +52,6 @@ class kolab_format_task extends kolab_format_xcal
             $status = $this->status_map[$object['status']];
         $this->obj->setStatus($status);
 
-        $this->obj->setStart(self::get_datetime($object['start'], null, $object['start']->_dateonly));
-        $this->obj->setDue(self::get_datetime($object['due'], null, $object['due']->_dateonly));
-
         $related = new vectors;
         if (!empty($object['parent_id']))
             $related->push($object['parent_id']);
@@ -119,7 +118,7 @@ class kolab_format_task extends kolab_format_xcal
         if ($this->data['priority'] == 1)
             $tags[] = 'x-flagged';
 
-        if (!empty($this->data['valarms']))
+        if (!empty($this->data['alarms']))
             $tags[] = 'x-has-alarms';
 
         if ($this->data['parent_id'])
diff --git a/lib/plugins/libkolab/lib/kolab_format_xcal.php b/lib/plugins/libkolab/lib/kolab_format_xcal.php
index ef423aa..76b7b3c 100644
--- a/lib/plugins/libkolab/lib/kolab_format_xcal.php
+++ b/lib/plugins/libkolab/lib/kolab_format_xcal.php
@@ -81,6 +81,10 @@ abstract class kolab_format_xcal extends kolab_format
         'IN-PROCESS'   => kolabformat::StatusInProcess,
         'COMPLETED'    => kolabformat::StatusCompleted,
         'CANCELLED'    => kolabformat::StatusCancelled,
+        'TENTATIVE'    => kolabformat::StatusTentative,
+        'CONFIRMED'    => kolabformat::StatusConfirmed,
+        'DRAFT'        => kolabformat::StatusDraft,
+        'FINAL'        => kolabformat::StatusFinal,
     );
 
     protected $part_status_map = array(
@@ -191,30 +195,77 @@ abstract class kolab_format_xcal extends kolab_format
             }
         }
 
+        if ($rdates = $this->obj->recurrenceDates()) {
+            for ($i=0; $i < $rdates->size(); $i++) {
+                if ($rdate = self::php_datetime($rdates->get($i)))
+                    $object['recurrence']['RDATE'][] = $rdate;
+            }
+        }
+
         // read alarm
         $valarms = $this->obj->alarms();
         $alarm_types = array_flip($this->alarm_type_map);
+        $object['valarms'] = array();
         for ($i=0; $i < $valarms->size(); $i++) {
             $alarm = $valarms->get($i);
             $type = $alarm_types[$alarm->type()];
 
             if ($type == 'DISPLAY' || $type == 'EMAIL') {  // only DISPLAY and EMAIL alarms are supported
+                $valarm = array(
+                    'action' => $type,
+                    'summary' => $alarm->summary(),
+                    'description' => $alarm->description(),
+                );
+
+                if ($type == 'EMAIL') {
+                    $valarm['attendees'] = array();
+                    $attvec = $this->obj->attendees();
+                    for ($j=0; $j < $attvec->size(); $j++) {
+                        $cr = $attvec->get($j);
+                        $valarm['attendees'][] = $cr->email();
+                    }
+                }
+
                 if ($start = self::php_datetime($alarm->start())) {
-                    $object['alarms'] = '@' . $start->format('U');
+                    $trigger = '@' . $start->format('U');
+                    $valarm['trigger'] = $start;
                 }
                 else if ($offset = $alarm->relativeStart()) {
-                    $value = $alarm->relativeTo() == kolabformat::End ? '+' : '-';
+                    $prefix = $alarm->relativeTo() == kolabformat::End ? '+' : '-';
+                    $value = $time = '';
                     if      ($w = $offset->weeks())     $value .= $w . 'W';
                     else if ($d = $offset->days())      $value .= $d . 'D';
-                    else if ($h = $offset->hours())     $value .= $h . 'H';
-                    else if ($m = $offset->minutes())   $value .= $m . 'M';
-                    else if ($s = $offset->seconds())   $value .= $s . 'S';
-                    else continue;
+                    else if ($h = $offset->hours())     $time  .= $h . 'H';
+                    else if ($m = $offset->minutes())   $time  .= $m . 'M';
+                    else if ($s = $offset->seconds())   $time  .= $s . 'S';
+
+                    // assume 'at event time'
+                    if (empty($value) && empty($time)) {
+                        $prefix = '';
+                        $time = '0S';
+                    }
+
+                    $trigger = $prefix . $value . $time;
+                    $valarm['trigger'] = $prefix . 'P' . $value . ($time ? 'T' . $time : '');
+                }
 
-                    $object['alarms'] = $value;
+                // read alarm duration and repeat properties
+                if (($duration = $alarm->duration()) && $duration->isValid()) {
+                    $value = $time = '';
+                    if      ($w = $duration->weeks())     $value .= $w . 'W';
+                    else if ($d = $duration->days())      $value .= $d . 'D';
+                    else if ($h = $duration->hours())     $time  .= $h . 'H';
+                    else if ($m = $duration->minutes())   $time  .= $m . 'M';
+                    else if ($s = $duration->seconds())   $time  .= $s . 'S';
+                    $valarm['duration'] = 'P' . $value . ($time ? 'T' . $time : '');
+                    $valarm['repeat'] = $alarm->numrepeat();
+                }
+
+                $object['valarms'][] = array_filter($valarm);
+
+                if (!$object['alarms']) {
+                    $object['alarms'] = $trigger . ':' . $type;  // legacy property
                 }
-                $object['alarms']  .= ':' . $type;
-                break;
             }
         }
 
@@ -311,7 +362,7 @@ abstract class kolab_format_xcal extends kolab_format
         $rr = new RecurrenceRule;
         $rr->setFrequency(RecurrenceRule::FreqNone);
 
-        if ($object['recurrence']) {
+        if ($object['recurrence'] && !empty($object['recurrence']['FREQ'])) {
             $rr->setFrequency($this->rrule_type_map[$object['recurrence']['FREQ']]);
 
             if ($object['recurrence']['INTERVAL'])
@@ -368,9 +419,71 @@ abstract class kolab_format_xcal extends kolab_format
 
         $this->obj->setRecurrenceRule($rr);
 
+        // save recurrence dates (aka RDATE)
+        if (!empty($object['recurrence']['RDATE'])) {
+            $rdates = new vectordatetime;
+            foreach ((array)$object['recurrence']['RDATE'] as $rdate)
+                $rdates->push(self::get_datetime($rdate, null, true));
+            $this->obj->setRecurrenceDates($rdates);
+        }
+
         // save alarm
         $valarms = new vectoralarm;
-        if ($object['alarms']) {
+        if ($object['valarms']) {
+            foreach ($object['valarms'] as $valarm) {
+                if (!array_key_exists($valarm['action'], $this->alarm_type_map)) {
+                    continue;  // skip unknown alarm types
+                }
+
+                if ($valarm['action'] == 'EMAIL') {
+                    $recipients = new vectorcontactref;
+                    foreach (($valarm['attendees'] ?: array($object['_owner'])) as $email) {
+                        $recipients->push(new ContactReference(ContactReference::EmailReference, $email));
+                    }
+                    $alarm = new Alarm(
+                        strval($valarm['summary'] ?: $object['title']),
+                        strval($valarm['description'] ?: $object['description']),
+                        $recipients
+                    );
+                }
+                else {
+                    $alarm = new Alarm(strval($valarm['summary'] ?: $object['title']));
+                }
+
+                if (is_object($valarm['trigger']) && $valarm['trigger'] instanceof DateTime) {
+                    $alarm->setStart(self::get_datetime($valarm['trigger'], new DateTimeZone('UTC')));
+                }
+                else {
+                    try {
+                        $prefix = $valarm['trigger'][0];
+                        $period = new DateInterval(preg_replace('/[^0-9PTWDHMS]/', '', $valarm['trigger']));
+                        $duration = new Duration($period->d, $period->h, $period->i, $period->s, $prefix == '-');
+                    }
+                    catch (Exception $e) {
+                        // skip alarm with invalid trigger values
+                        rcube::raise_error($e, true);
+                        continue;
+                    }
+
+                    $alarm->setRelativeStart($duration, $prefix == '-' ? kolabformat::Start : kolabformat::End);
+                }
+
+                if ($valarm['duration']) {
+                    try {
+                        $d = new DateInterval($valarm['duration']);
+                        $duration = new Duration($d->d, $d->h, $d->i, $d->s);
+                        $alarm->setDuration($duration, intval($valarm['repeat']));
+                    }
+                    catch (Exception $e) {
+                        // ignore
+                    }
+                }
+
+                $valarms->push($alarm);
+            }
+        }
+        // legacy support
+        else if ($object['alarms']) {
             list($offset, $type) = explode(":", $object['alarms']);
 
             if ($type == 'EMAIL' && !empty($object['_owner'])) {  // email alarms implicitly go to event owner
@@ -398,6 +511,19 @@ abstract class kolab_format_xcal extends kolab_format
             }
 
             $valarms->push($alarm);
+
+            // preserve additional alarm entries
+            $oldvalarms = $this->obj->alarms();
+            for ($i=1; $i < $oldvalarms->size(); $i++) {
+                $valarms->push($oldvalarms->get($i));
+            }
+
+            // HACK: set and read back alarms to store the correct 'valarms' value in cache
+            if ($i > 1) {
+                $this->obj->setAlarms($valarms);
+                $update = $this->to_array();
+                $object['valarms'] = $update['valarms'];
+            }
         }
         $this->obj->setAlarms($valarms);
 
@@ -408,8 +534,19 @@ abstract class kolab_format_xcal extends kolab_format
                 continue;
             $attach = new Attachment;
             $attach->setLabel((string)$attr['name']);
-            $attach->setUri('cid:' . $cid, $attr['mimetype']);
-            $vattach->push($attach);
+            $attach->setUri('cid:' . $cid, $attr['mimetype'] ?: 'application/octet-stream');
+            if ($attach->isValid()) {
+              $vattach->push($attach);
+            }
+            else {
+              rcube::raise_error(array(
+                  'code' => 660,
+                  'type' => 'php',
+                  'file' => __FILE__,
+                  'line' => __LINE__,
+                  'message' => "Invalid attributes for attachment $cid: " . var_export($attr, true),
+              ), true);
+            }
         }
 
         foreach ((array)$object['links'] as $link) {
diff --git a/lib/plugins/libkolab/lib/kolab_storage.php b/lib/plugins/libkolab/lib/kolab_storage.php
index 5f8b9c6..9ef3057 100644
--- a/lib/plugins/libkolab/lib/kolab_storage.php
+++ b/lib/plugins/libkolab/lib/kolab_storage.php
@@ -35,6 +35,11 @@ class kolab_storage
     const UID_KEY_PRIVATE   = '/private/vendor/kolab/uniqueid';
     const UID_KEY_CYRUS     = '/shared/vendor/cmu/cyrus-imapd/uniqueid';
 
+    const ERROR_IMAP_CONN      = 1;
+    const ERROR_CACHE_DB       = 2;
+    const ERROR_NO_PERMISSION  = 3;
+    const ERROR_INVALID_FOLDER = 4;
+
     public static $version = '3.0';
     public static $last_error;
 
@@ -101,7 +106,6 @@ class kolab_storage
         return self::$ready;
     }
 
-
     /**
      * Get a list of storage folders for the given data type
      *
@@ -116,7 +120,7 @@ class kolab_storage
 
         if (self::setup()) {
             foreach ((array)self::list_folders('', '*', $type, $subscribed, $folderdata) as $foldername) {
-                $folders[$foldername] = new kolab_storage_folder($foldername, $folderdata[$foldername]);
+                $folders[$foldername] = new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
             }
         }
 
@@ -133,26 +137,26 @@ class kolab_storage
     {
         if (self::setup()) {
             foreach ((array)self::list_folders('', '*', $type . '.default', false, $folderdata) as $foldername) {
-                return new kolab_storage_folder($foldername, $folderdata[$foldername]);
+                return new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
             }
         }
 
         return null;
     }
 
-
     /**
      * Getter for a specific storage folder
      *
-     * @param string  IMAP folder to access (UTF7-IMAP)
+     * @param string IMAP folder to access (UTF7-IMAP)
+     * @param string Expected folder type
+     *
      * @return object kolab_storage_folder  The folder object
      */
-    public static function get_folder($folder)
+    public static function get_folder($folder, $type = null)
     {
-        return self::setup() ? new kolab_storage_folder($folder) : null;
+        return self::setup() ? new kolab_storage_folder($folder, $type) : null;
     }
 
-
     /**
      * Getter for a single Kolab object, identified by its UID.
      * This will search all folders storing objects of the given type.
@@ -165,11 +169,11 @@ class kolab_storage
     {
         self::setup();
         $folder = null;
-        foreach ((array)self::list_folders('', '*', $type) as $foldername) {
+        foreach ((array)self::list_folders('', '*', $type, null, $folderdata) as $foldername) {
             if (!$folder)
-                $folder = new kolab_storage_folder($foldername);
+                $folder = new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
             else
-                $folder->set_folder($foldername);
+                $folder->set_folder($foldername, $type, $folderdata[$foldername]);
 
             if ($object = $folder->get_object($uid, '*'))
                 return $object;
@@ -433,13 +437,14 @@ class kolab_storage
                     // get username
                     $pos    = strpos($folder, $delim);
                     if ($pos) {
-                        $prefix = '('.substr($folder, 0, $pos).') ';
+                        $prefix = '('.substr($folder, 0, $pos).')';
                         $folder = substr($folder, $pos+1);
                     }
                     else {
-                        $prefix = '('.$folder.')';
+                        $prefix = $folder;
                         $folder = '';
                     }
+
                     $found  = true;
                     $folder_ns = 'other';
                     break;
@@ -467,7 +472,7 @@ class kolab_storage
         $folder = str_replace(html::quote($delim), ' » ', $folder);
 
         if ($prefix)
-            $folder = html::quote($prefix) . ' ' . $folder;
+            $folder = html::quote($prefix) . ($folder !== '' ? ' ' . $folder : '');
 
         if (!$folder_ns)
             $folder_ns = 'personal';
@@ -492,7 +497,8 @@ class kolab_storage
     }
 
     /**
-     * Helper method to generate a truncated folder name to display
+     * Helper method to generate a truncated folder name to display.
+     * Note: $origname is a string returned by self::object_name()
      */
     public static function folder_displayname($origname, &$names)
     {
@@ -504,10 +510,29 @@ class kolab_storage
                 $length = strlen($names[$i] . ' » ');
                 $prefix = substr($name, 0, $length);
                 $count  = count(explode(' » ', $prefix));
-                $name   = str_repeat('   ', $count-1) . '» ' . substr($name, $length);
+                $diff   = 1;
+
+                // check if prefix folder is in other users namespace
+                for ($n = count($names)-1; $n >= 0; $n--) {
+                    if (strpos($prefix, '(' . $names[$n] . ') ') === 0) {
+                        $diff = 0;
+                        break;
+                    }
+                }
+
+                $name = str_repeat('   ', $count - $diff) . '» ' . substr($name, $length);
+                break;
+            }
+            // other users namespace and parent folder exists
+            else if (strpos($name, '(' . $names[$i] . ') ') === 0) {
+                $length = strlen('(' . $names[$i] . ') ');
+                $prefix = substr($name, 0, $length);
+                $count  = count(explode(' » ', $prefix));
+                $name   = str_repeat('   ', $count) . '» ' . substr($name, $length);
                 break;
             }
         }
+
         $names[] = $origname;
 
         return $name;
@@ -525,8 +550,8 @@ class kolab_storage
      */
     public static function folder_selector($type, $attrs, $current = '')
     {
-        // get all folders of specified type
-        $folders = self::get_folders($type, false);
+        // get all folders of specified type (sorted)
+        $folders = self::get_folders($type, true);
 
         $delim = self::$imap->get_hierarchy_delimiter();
         $names = array();
@@ -540,13 +565,24 @@ class kolab_storage
         // Filter folders list
         foreach ($folders as $c_folder) {
             $name = $c_folder->name;
+
             // skip current folder and it's subfolders
-            if ($len && ($name == $current || strpos($name, $current.$delim) === 0)) {
-                continue;
+            if ($len) {
+                if ($name == $current) {
+                    // Make sure parent folder is listed (might be skipped e.g. if it's namespace root)
+                    if ($p_len && !isset($names[$parent])) {
+                        $names[$parent] = self::object_name($parent);
+                    }
+                    continue;
+                }
+                if (strpos($name, $current.$delim) === 0) {
+                    continue;
+                }
             }
 
             // always show the parent of current folder
-            if ($p_len && $name == $parent) { }
+            if ($p_len && $name == $parent) {
+            }
             // skip folders where user have no rights to create subfolders
             else if ($c_folder->get_owner() != $_SESSION['username']) {
                 $rights = $c_folder->get_myrights();
@@ -558,14 +594,6 @@ class kolab_storage
             $names[$name] = self::object_name($name);
         }
 
-        // Make sure parent folder is listed (might be skipped e.g. if it's namespace root)
-        if ($p_len && !isset($names[$parent])) {
-            $names[$parent] = self::object_name($parent);
-        }
-
-        // Sort folders list
-        asort($names, SORT_LOCALE_STRING);
-
         // Build SELECT field of parent folder
         $attrs['is_escaped'] = true;
         $select = new html_select($attrs);
@@ -643,7 +671,8 @@ class kolab_storage
                     unset($folderdata[$folder]);
                 }
             }
-            return array_keys($folderdata);
+
+            return self::$imap->sort_folder_list(array_keys($folderdata), true);
         }
 
         // Get folders list
@@ -683,26 +712,74 @@ class kolab_storage
      */
     public static function sort_folders($folders)
     {
-        $pad = '  ';
+        $pad     = '  ';
+        $out     = array();
         $nsnames = array('personal' => array(), 'shared' => array(), 'other' => array());
+
         foreach ($folders as $folder) {
             $folders[$folder->name] = $folder;
             $ns = $folder->get_namespace();
             $nsnames[$ns][$folder->name] = strtolower(html_entity_decode(self::object_name($folder->name, $ns), ENT_COMPAT, RCUBE_CHARSET)) . $pad;  // decode »
         }
 
-        $names = array();
-        foreach ($nsnames as $ns => $dummy) {
-            asort($nsnames[$ns], SORT_LOCALE_STRING);
-            $names += $nsnames[$ns];
+        // $folders is a result of get_folders() we can assume folders were already sorted
+        foreach (array_keys($nsnames) as $ns) {
+            // asort($nsnames[$ns], SORT_LOCALE_STRING);
+            foreach (array_keys($nsnames[$ns]) as $utf7name) {
+                $out[] = $folders[$utf7name];
+            }
         }
 
-        $out = array();
-        foreach ($names as $utf7name => $name) {
-            $out[] = $folders[$utf7name];
+        return $out;
+    }
+
+
+    /**
+     * Check the folder tree and add the missing parents as virtual folders
+     *
+     * @param array $folders Folders list
+     *
+     * @return array Folders list
+     */
+    public static function folder_hierarchy($folders)
+    {
+        $_folders = array();
+        $existing = array_map(function($folder){ return $folder->get_name(); }, $folders);
+        $delim    = rcube::get_instance()->get_storage()->get_hierarchy_delimiter();
+
+        foreach ($folders as $idx => $folder) {
+            $path = explode($delim, $folder->name);
+            array_pop($path);
+
+            // skip top folders or ones with a custom displayname
+            if (count($path) <= 1 || kolab_storage::custom_displayname($folder->name)) {
+            }
+            else {
+                $parents = array();
+
+                while (count($path) > 1 && ($parent = join($delim, $path))) {
+                    $name = kolab_storage::object_name($parent, $folder->get_namespace());
+                    if (!in_array($name, $existing)) {
+                        $parents[$parent] = new virtual_kolab_storage_folder($parent, $name, $folder->get_namespace());
+                        $existing[] = $name;
+                    }
+
+                    array_pop($path);
+                }
+
+                if (!empty($parents)) {
+                    $parents = array_reverse(array_values($parents));
+                    foreach ($parents as $parent) {
+                        $_folders[] = $parent;
+                    }
+                }
+            }
+
+            $_folders[] = $folder;
+            unset($folders[$idx]);
         }
 
-        return $out;
+        return $_folders;
     }
 
 
@@ -823,7 +900,7 @@ class kolab_storage
         self::setup();
 
         if (self::$imap->subscribe($folder)) {
-            self::$subscriptions === null;
+            self::$subscriptions = null;
             return true;
         }
 
@@ -843,7 +920,7 @@ class kolab_storage
         self::setup();
 
         if (self::$imap->unsubscribe($folder)) {
-            self::$subscriptions === null;
+            self::$subscriptions = null;
             return true;
         }
 
@@ -1047,3 +1124,33 @@ class kolab_storage
     }
 
 }
+
+/**
+ * Helper class that represents a virtual IMAP folder
+ * with a subset of the kolab_storage_folder API.
+ */
+class virtual_kolab_storage_folder
+{
+    public $id;
+    public $name;
+    public $namespace;
+    public $virtual = true;
+
+    public function __construct($realname, $name, $ns)
+    {
+        $this->id        = kolab_storage::folder_id($realname);
+        $this->name      = $name;
+        $this->namespace = $ns;
+    }
+
+    public function get_namespace()
+    {
+        return $this->namespace;
+    }
+
+    public function get_name()
+    {
+        // this is already kolab_storage::object_name() result
+        return $this->name;
+    }
+}
diff --git a/lib/plugins/libkolab/lib/kolab_storage_cache.php b/lib/plugins/libkolab/lib/kolab_storage_cache.php
index 54047ba..b31cd74 100644
--- a/lib/plugins/libkolab/lib/kolab_storage_cache.php
+++ b/lib/plugins/libkolab/lib/kolab_storage_cache.php
@@ -29,7 +29,7 @@ class kolab_storage_cache
     protected $folder;
     protected $uid2msg;
     protected $objects;
-    protected $index = array();
+    protected $index = null;
     protected $metadata = array();
     protected $folder_id;
     protected $resource_uri;
@@ -43,6 +43,9 @@ class kolab_storage_cache
     protected $max_sync_lock_time = 600;
     protected $binary_items = array();
     protected $extra_cols = array();
+    protected $order_by = null;
+    protected $limit = null;
+    protected $error = 0;
 
 
     /**
@@ -58,8 +61,10 @@ class kolab_storage_cache
             rcube::raise_error(array(
                 'code' => 900,
                 'type' => 'php',
-                'message' => "No kolab_storage_cache class found for folder of type " . $storage_folder->type
+                'message' => "No kolab_storage_cache class found for folder '$storage_folder->name' of type '$storage_folder->type'"
             ), true);
+
+            return new kolab_storage_cache($storage_folder);
         }
     }
 
@@ -73,6 +78,7 @@ class kolab_storage_cache
         $this->db = $rcmail->get_dbh();
         $this->imap = $rcmail->get_storage();
         $this->enabled = $rcmail->config->get('kolab_cache', false);
+        $this->folders_table = $this->db->table_name('kolab_folders');
 
         if ($this->enabled) {
             // always read folder cache and lock state from DB master
@@ -85,6 +91,23 @@ class kolab_storage_cache
             $this->set_folder($storage_folder);
     }
 
+    /**
+     * Direct access to cache by folder_id
+     * (only for internal use)
+     */
+    public function select_by_id($folder_id)
+    {
+        $sql_arr = $this->db->fetch_assoc($this->db->query("SELECT * FROM {$this->folders_table} WHERE folder_id=?", $folder_id));
+        if ($sql_arr) {
+            $this->metadata = $sql_arr;
+            $this->folder_id = $sql_arr['folder_id'];
+            $this->folder = new StdClass;
+            $this->folder->type = $sql_arr['type'];
+            $this->resource_uri = $sql_arr['resource'];
+            $this->cache_table = $this->db->table_name('kolab_cache_' . $sql_arr['type']);
+            $this->ready = true;
+        }
+    }
 
     /**
      * Connect cache with a storage folder
@@ -95,16 +118,15 @@ class kolab_storage_cache
     {
         $this->folder = $storage_folder;
 
-        if (empty($this->folder->name)) {
+        if (empty($this->folder->name) || !$this->folder->valid) {
             $this->ready = false;
             return;
         }
 
         // compose fully qualified ressource uri for this instance
         $this->resource_uri = $this->folder->get_resource_uri();
-        $this->folders_table = $this->db->table_name('kolab_folders');
         $this->cache_table = $this->db->table_name('kolab_cache_' . $this->folder->type);
-        $this->ready = $this->enabled;
+        $this->ready = $this->enabled && !empty($this->folder->type);
         $this->folder_id = null;
     }
 
@@ -117,6 +139,25 @@ class kolab_storage_cache
     }
 
     /**
+     * Getter for the numeric ID used in cache tables
+     */
+    public function get_folder_id()
+    {
+        $this->_read_folder_data();
+        return $this->folder_id;
+    }
+
+    /**
+     * Returns code of last error
+     *
+     * @return int Error code
+     */
+    public function get_error()
+    {
+        return $this->error;
+    }
+
+    /**
      * Synchronize local cache data with remote
      */
     public function synchronize()
@@ -188,6 +229,7 @@ class kolab_storage_cache
             $this->_sync_unlock();
         }
 
+        $this->check_error();
         $this->synched = time();
     }
 
@@ -204,7 +246,12 @@ class kolab_storage_cache
     {
         // delegate to another cache instance
         if ($foldername && $foldername != $this->folder->name) {
-            return kolab_storage::get_folder($foldername)->cache->get($msguid, $type);
+            $success = false;
+            if ($targetfolder = kolab_storage::get_folder($foldername)) {
+                $success = $targetfolder->cache->get($msguid, $type);
+                $this->error = $targetfolder->cache->get_error();
+            }
+            return $success;
         }
 
         // load object if not in memory
@@ -220,17 +267,18 @@ class kolab_storage_cache
                 );
 
                 if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
-                    $this->objects[$msguid] = $this->_unserialize($sql_arr);
+                    $this->objects = array($msguid => $this->_unserialize($sql_arr));  // store only this object in memory (#2827)
                 }
             }
 
             // fetch from IMAP if not present in cache
             if (empty($this->objects[$msguid])) {
                 $result = $this->_fetch(array($msguid), $type, $foldername);
-                $this->objects[$msguid] = $result[0];
+                $this->objects = array($msguid => $result[0]);  // store only this object in memory (#2827)
             }
         }
 
+        $this->check_error();
         return $this->objects[$msguid];
     }
 
@@ -250,8 +298,11 @@ class kolab_storage_cache
 
         // delegate to another cache instance
         if ($foldername && $foldername != $this->folder->name) {
-            kolab_storage::get_folder($foldername)->cache->set($msguid, $object);
-            return;
+          if ($targetfolder = kolab_storage::get_folder($foldername)) {
+              $targetfolder->cache->set($msguid, $object);
+              $this->error = $targetfolder->cache->get_error();
+          }
+          return;
         }
 
         // remove old entry
@@ -269,6 +320,8 @@ class kolab_storage_cache
             // ...or set in-memory cache to false
             $this->objects[$msguid] = $object;
         }
+
+        $this->check_error();
     }
 
 
@@ -320,6 +373,8 @@ class kolab_storage_cache
         // keep a copy in memory for fast access
         $this->objects = array($msguid => $object);
         $this->uid2msg = array($object['uid'] => $msguid);
+
+        $this->check_error();
     }
 
 
@@ -328,20 +383,21 @@ class kolab_storage_cache
      *
      * @param string Entry's IMAP message UID
      * @param string Entry's Object UID
-     * @param string Target IMAP folder to move it to
+     * @param object kolab_storage_folder Target storage folder instance
      */
-    public function move($msguid, $uid, $target_folder)
+    public function move($msguid, $uid, $target)
     {
-        $target = kolab_storage::get_folder($target_folder);
+        // clear cached uid mapping and force new lookup
+        unset($target->cache->uid2msg[$uid]);
 
         // resolve new message UID in target folder
-        if ($new_msguid = $target->cache->uid2msguid($uid)) {
+        if ($this->ready && ($new_msguid = $target->cache->uid2msguid($uid))) {
             $this->_read_folder_data();
 
             $this->db->query(
                 "UPDATE $this->cache_table SET folder_id=?, msguid=? ".
                 "WHERE folder_id=? AND msguid=?",
-                $target->folder_id,
+                $target->cache->get_folder_id(),
                 $new_msguid,
                 $this->folder_id,
                 $msguid
@@ -353,6 +409,7 @@ class kolab_storage_cache
         }
 
         unset($this->uid2msg[$uid]);
+        $this->check_error();
     }
 
 
@@ -367,6 +424,7 @@ class kolab_storage_cache
             "DELETE FROM $this->cache_table WHERE folder_id=?",
             $this->folder_id
         );
+
         return $this->db->affected_rows($result);
     }
 
@@ -377,15 +435,20 @@ class kolab_storage_cache
      */
     public function rename($new_folder)
     {
-        $target = kolab_storage::get_folder($new_folder);
+        if ($target = kolab_storage::get_folder($new_folder)) {
+            // resolve new message UID in target folder
+            $this->db->query(
+                "UPDATE $this->folders_table SET resource=? ".
+                "WHERE resource=?",
+                $target->get_resource_uri(),
+                $this->resource_uri
+            );
 
-        // resolve new message UID in target folder
-        $this->db->query(
-            "UPDATE $this->folders_table SET resource=? ".
-            "WHERE resource=?",
-            $target->get_resource_uri(),
-            $this->resource_uri
-        );
+            $this->check_error();
+        }
+        else {
+            $this->error = kolab_storage::ERROR_IMAP_CONN;
+        }
     }
 
     /**
@@ -398,30 +461,43 @@ class kolab_storage_cache
      */
     public function select($query = array(), $uids = false)
     {
-        $result = array();
+        $result = $uids ? array() : new kolab_storage_dataset($this);
 
         // read from local cache DB (assume it to be synchronized)
         if ($this->ready) {
             $this->_read_folder_data();
 
-            $sql_result = $this->db->query(
-                "SELECT " . ($uids ? 'msguid, uid' : '*') . " FROM $this->cache_table ".
-                "WHERE folder_id=? " . $this->_sql_where($query),
-                $this->folder_id
-            );
+            // fetch full object data on one query if a small result set is expected
+            $fetchall = !$uids && ($this->limit ? $this->limit[0] : $this->count($query)) < 500;
+            $sql_query = "SELECT " . ($fetchall ? '*' : 'msguid AS _msguid, uid') . " FROM $this->cache_table ".
+                         "WHERE folder_id=? " . $this->_sql_where($query);
+            if (!empty($this->order_by)) {
+                $sql_query .= ' ORDER BY ' . $this->order_by;
+            }
+            $sql_result = $this->limit ?
+                $this->db->limitquery($sql_query, $this->limit[1], $this->limit[0], $this->folder_id) :
+                $this->db->query($sql_query, $this->folder_id);
 
             if ($this->db->is_error($sql_result)) {
-                return null;
+                if ($uids) {
+                    return null;
+                }
+                $result->set_error(true);
+                return $result;
             }
 
             while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
                 if ($uids) {
-                    $this->uid2msg[$sql_arr['uid']] = $sql_arr['msguid'];
+                    $this->uid2msg[$sql_arr['uid']] = $sql_arr['_msguid'];
                     $result[] = $sql_arr['uid'];
                 }
-                else if ($object = $this->_unserialize($sql_arr)) {
+                else if ($fetchall && ($object = $this->_unserialize($sql_arr))) {
                     $result[] = $object;
                 }
+                else if (!$fetchall) {
+                    // only add msguid to dataset index
+                    $result[] = $sql_arr;
+                }
             }
         }
         else {
@@ -429,7 +505,7 @@ class kolab_storage_cache
             $filter = $this->_query2assoc($query);
 
             // use 'list' for folder's default objects
-            if ($filter['type'] == $this->type) {
+            if (is_array($this->index) && $filter['type'] == $this->type) {
                 $index = $this->index;
             }
             else {  // search by object type
@@ -437,10 +513,6 @@ class kolab_storage_cache
                 $index  = $this->imap->search_once($this->folder->name, $search)->get();
             }
 
-            if ($index->is_error()) {
-                return null;
-            }
-
             // fetch all messages in $index from IMAP
             $result = $uids ? $this->_fetch_uids($index, $filter['type']) : $this->_fetch($index, $filter['type']);
 
@@ -452,10 +524,12 @@ class kolab_storage_cache
         if (!$uids && count($result) == 1) {
             if ($msguid = $result[0]['_msguid']) {
                 $this->uid2msg[$result[0]['uid']] = $msguid;
-                $this->objects[$msguid] = $result[0];
+                $this->objects = array($msguid => $result[0]);
             }
         }
 
+        $this->check_error();
+
         return $result;
     }
 
@@ -469,7 +543,7 @@ class kolab_storage_cache
     public function count($query = array())
     {
         // cache is in sync, we can count records in local DB
-        if ($this->synched) {
+        if ($this->synched && $this->ready) {
             $this->_read_folder_data();
 
             $sql_result = $this->db->query(
@@ -492,15 +566,37 @@ class kolab_storage_cache
             $index  = $this->imap->search_once($this->folder->name, 'UNDELETED HEADER X-Kolab-Type ' . $ctype);
 
             if ($index->is_error()) {
+                $this->check_error();
                 return null;
             }
 
             $count = $index->count();
         }
 
+        $this->check_error();
         return $count;
     }
 
+    /**
+     * Define ORDER BY clause for cache queries
+     */
+    public function set_order_by($sortcols)
+    {
+        if (!empty($sortcols)) {
+            $this->order_by = join(', ', (array)$sortcols);
+        }
+        else {
+            $this->order_by = null;
+        }
+    }
+
+    /**
+     * Define LIMIT clause for cache queries
+     */
+    public function set_limit($length, $offset = 0)
+    {
+        $this->limit = array($length, $offset);
+    }
 
     /**
      * Helper method to compose a valid SQL query from pseudo filter triplets
@@ -529,7 +625,7 @@ class kolab_storage_cache
                 $qvalue = $this->db->quote('%'.preg_replace('/(^\^|\$$)/', ' ', $param[2]).'%');
             }
             else if ($param[0] == 'tags') {
-                $param[1] = 'LIKE';
+                $param[1] = ($param[1] == '!=' ? 'NOT ' : '' ) . 'LIKE';
                 $qvalue = $this->db->quote('% '.$param[2].' %');
             }
             else {
@@ -571,7 +667,7 @@ class kolab_storage_cache
      */
     protected function _fetch($index, $type = null, $folder = null)
     {
-        $results = array();
+        $results = new kolab_storage_dataset($this);
         foreach ((array)$index as $msguid) {
             if ($object = $this->folder->read_object($msguid, $type, $folder)) {
                 $results[] = $object;
@@ -659,7 +755,9 @@ class kolab_storage_cache
             }
         }
 
-        $sql_data['data'] = serialize($data);
+        // use base64 encoding (Bug #1912, #2662)
+        $sql_data['data'] = base64_encode(serialize($data));
+
         return $sql_data;
     }
 
@@ -668,8 +766,23 @@ class kolab_storage_cache
      */
     protected function _unserialize($sql_arr)
     {
+        // check if data is a base64-encoded string, for backward compat.
+        if (strpos(substr($sql_arr['data'], 0, 64), ':') === false) {
+            $sql_arr['data'] = base64_decode($sql_arr['data']);
+        }
+
         $object = unserialize($sql_arr['data']);
 
+        // de-serialization failed
+        if ($object === false) {
+            rcube::raise_error(array(
+                'code' => 900, 'type' => 'php',
+                'message' => "Malformed data for {$this->resource_uri}/{$sql_arr['msguid']} object."
+            ), true);
+
+            return null;
+        }
+
         // decode binary properties
         foreach ($this->binary_items as $key => $regexp) {
             if (!empty($object[$key]) && preg_match($regexp, $sql_arr['xml'], $m)) {
@@ -677,12 +790,15 @@ class kolab_storage_cache
             }
         }
 
+        $object_type = $sql_arr['type'] ?: $this->folder->type;
+        $format_type = $this->folder->type == 'configuration' ? 'configuration' : $object_type;
+
         // add meta data
-        $object['_type'] = $sql_arr['type'] ?: $this->folder->type;
-        $object['_msguid'] = $sql_arr['msguid'];
-        $object['_mailbox'] = $this->folder->name;
-        $object['_size'] = strlen($sql_arr['xml']);
-        $object['_formatobj'] = kolab_format::factory($object['_type'], 3.0, $sql_arr['xml']);
+        $object['_type']      = $object_type;
+        $object['_msguid']    = $sql_arr['msguid'];
+        $object['_mailbox']   = $this->folder->name;
+        $object['_size']      = strlen($sql_arr['xml']);
+        $object['_formatobj'] = kolab_format::factory($format_type, 3.0, $sql_arr['xml']);
 
         return $object;
     }
@@ -785,6 +901,7 @@ class kolab_storage_cache
 
         // abort if database is not set-up
         if ($this->db->is_error()) {
+            $this->check_error();
             $this->ready = false;
             return;
         }
@@ -819,6 +936,22 @@ class kolab_storage_cache
     }
 
     /**
+     * Check IMAP connection error state
+     */
+    protected function check_error()
+    {
+        if (($err_code = $this->imap->get_error_code()) < 0) {
+            $this->error = kolab_storage::ERROR_IMAP_CONN;
+            if (($res_code = $this->imap->get_response_code()) !== 0 && in_array($res_code, array(rcube_storage::NOPERM, rcube_storage::READONLY))) {
+                $this->error = kolab_storage::ERROR_NO_PERMISSION;
+            }
+        }
+        else if ($this->db->is_error()) {
+            $this->error = kolab_storage::ERROR_CACHE_DB;
+        }
+    }
+
+    /**
      * Resolve an object UID into an IMAP message UID
      *
      * @param string  Kolab object UID
@@ -832,7 +965,7 @@ class kolab_storage_cache
             $index = $this->imap->search_once($this->folder->name, ($deleted ? '' : 'UNDELETED ') .
                 'HEADER SUBJECT ' . rcube_imap_generic::escape($uid));
             $results = $index->get();
-            $this->uid2msg[$uid] = $results[0];
+            $this->uid2msg[$uid] = end($results);
         }
 
         return $this->uid2msg[$uid];
diff --git a/lib/plugins/libkolab/lib/kolab_storage_cache_configuration.php b/lib/plugins/libkolab/lib/kolab_storage_cache_configuration.php
index 8380aa8..c3c7ac4 100644
--- a/lib/plugins/libkolab/lib/kolab_storage_cache_configuration.php
+++ b/lib/plugins/libkolab/lib/kolab_storage_cache_configuration.php
@@ -37,4 +37,52 @@ class kolab_storage_cache_configuration extends kolab_storage_cache
 
         return $sql_data;
     }
-}
\ No newline at end of file
+
+    /**
+     * Select Kolab objects filtered by the given query
+     *
+     * @param array Pseudo-SQL query as list of filter parameter triplets
+     * @param boolean Set true to only return UIDs instead of complete objects
+     * @return array List of Kolab data objects (each represented as hash array) or UIDs
+     */
+    public function select($query = array(), $uids = false)
+    {
+        // modify query for IMAP search: query param 'type' is actually a subtype
+        if (!$this->ready) {
+            foreach ($query as $i => $tuple) {
+                if ($tuple[0] == 'type') {
+                    $tuple[2] = 'configuration.' . $tuple[2];
+                    $query[$i] = $tuple;
+                }
+            }
+        }
+
+        return parent::select($query, $uids);
+    }
+
+    /**
+     * Helper method to compose a valid SQL query from pseudo filter triplets
+     */
+    protected function _sql_where($query)
+    {
+        if (is_array($query)) {
+            foreach ($query as $idx => $param) {
+                // convert category filter
+                if ($param[0] == 'category') {
+                    $param[2] = array_map(function($n) { return 'category:' . $n; }, (array) $param[2]);
+
+                    $query[$idx][0] = 'tags';
+                    $query[$idx][2] = count($param[2]) > 1 ? $param[2] : $param[2][0];
+                }
+                // convert member filter (we support only = operator with single value)
+                else if ($param[0] == 'member') {
+                    $query[$idx][0] = 'words';
+                    $query[$idx][1] = '~';
+                    $query[$idx][2] = '^' . $param[2] . '$';
+                }
+            }
+        }
+
+        return parent::_sql_where($query);
+    }
+}
diff --git a/lib/plugins/libkolab/lib/kolab_storage_cache_contact.php b/lib/plugins/libkolab/lib/kolab_storage_cache_contact.php
index e17923d..9666a39 100644
--- a/lib/plugins/libkolab/lib/kolab_storage_cache_contact.php
+++ b/lib/plugins/libkolab/lib/kolab_storage_cache_contact.php
@@ -23,7 +23,7 @@
 
 class kolab_storage_cache_contact extends kolab_storage_cache
 {
-    protected $extra_cols = array('type');
+    protected $extra_cols = array('type','name','firstname','surname','email');
     protected $binary_items = array(
         'photo'          => '|<photo><uri>[^;]+;base64,([^<]+)</uri></photo>|i',
         'pgppublickey'   => '|<key><uri>date:application/pgp-keys;base64,([^<]+)</uri></key>|i',
@@ -40,6 +40,20 @@ class kolab_storage_cache_contact extends kolab_storage_cache
         $sql_data = parent::_serialize($object);
         $sql_data['type'] = $object['_type'];
 
+        // columns for sorting
+        $sql_data['name']      = rcube_charset::clean($object['name'] . $object['prefix']);
+        $sql_data['firstname'] = rcube_charset::clean($object['firstname'] . $object['middlename'] . $object['surname']);
+        $sql_data['surname']   = rcube_charset::clean($object['surname']   . $object['firstname']  . $object['middlename']);
+        $sql_data['email']     = rcube_charset::clean(is_array($object['email']) ? $object['email'][0] : $object['email']);
+
+        if (is_array($sql_data['email'])) {
+            $sql_data['email'] = $sql_data['email']['address'];
+        }
+        // avoid value being null
+        if (empty($sql_data['email'])) {
+            $sql_data['email'] = '';
+        }
+
         return $sql_data;
     }
 }
\ No newline at end of file
diff --git a/lib/plugins/libkolab/lib/kolab_storage_config.php b/lib/plugins/libkolab/lib/kolab_storage_config.php
new file mode 100644
index 0000000..d58e3c0
--- /dev/null
+++ b/lib/plugins/libkolab/lib/kolab_storage_config.php
@@ -0,0 +1,840 @@
+<?php
+
+/**
+ * Kolab storage class providing access to configuration objects on a Kolab server.
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ * @author Aleksander Machniak <machniak at kolabsys.com>
+ *
+ * Copyright (C) 2012-2014, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class kolab_storage_config
+{
+    const FOLDER_TYPE = 'configuration';
+
+
+    /**
+     * Singleton instace of kolab_storage_config
+     *
+     * @var kolab_storage_config
+     */
+    static protected $instance;
+
+    private $folders;
+    private $default;
+    private $enabled;
+
+
+    /**
+     * This implements the 'singleton' design pattern
+     *
+     * @return kolab_storage_config The one and only instance
+     */
+    static function get_instance()
+    {
+        if (!self::$instance) {
+            self::$instance = new kolab_storage_config();
+        }
+
+        return self::$instance;
+    }
+
+    /**
+     * Private constructor (finds default configuration folder as a config source)
+     */
+    private function __construct()
+    {
+        // get all configuration folders
+        $this->folders = kolab_storage::get_folders(self::FOLDER_TYPE, false);
+
+        foreach ($this->folders as $folder) {
+            if ($folder->default) {
+                $this->default = $folder;
+                break;
+            }
+        }
+
+        // if no folder is set as default, choose the first one
+        if (!$this->default) {
+            $this->default = reset($this->folders);
+        }
+
+        // attempt to create a default folder if it does not exist
+        if (!$this->default) {
+            $folder_name = 'Configuration';
+            $folder_type = self::FOLDER_TYPE . '.default';
+
+            if (kolab_storage::folder_create($folder_name, $folder_type, true)) {
+                $this->default = new kolab_storage_folder($folder_name, $folder_type);
+            }
+        }
+
+        // check if configuration folder exist
+        if ($this->default && $this->default->name) {
+            $this->enabled = true;
+        }
+    }
+
+    /**
+     * Check wether any configuration storage (folder) exists
+     *
+     * @return bool
+     */
+    public function is_enabled()
+    {
+        return $this->enabled;
+    }
+
+    /**
+     * Get configuration objects
+     *
+     * @param array $filter  Search filter
+     * @param bool  $default Enable to get objects only from default folder
+     * @param int   $limit   Max. number of records (per-folder)
+     *
+     * @return array List of objects
+     */
+    public function get_objects($filter = array(), $default = false, $limit = 0)
+    {
+        $list = array();
+
+        foreach ($this->folders as $folder) {
+            // we only want to read from default folder
+            if ($default && !$folder->default) {
+                continue;
+            }
+
+            // for better performance it's good to assume max. number of records
+            if ($limit) {
+                $folder->set_order_and_limit(null, $limit);
+            }
+
+            foreach ($folder->select($filter) as $object) {
+                unset($object['_formatobj']);
+                $list[] = $object;
+            }
+        }
+
+        return $list;
+    }
+
+    /**
+     * Get configuration object
+     *
+     * @param string $uid     Object UID
+     * @param bool   $default Enable to get objects only from default folder
+     *
+     * @return array Object data
+     */
+    public function get_object($uid, $default = false)
+    {
+        foreach ($this->folders as $folder) {
+            // we only want to read from default folder
+            if ($default && !$folder->default) {
+                continue;
+            }
+
+            if ($object = $folder->get_object($uid)) {
+                return $object;
+            }
+        }
+    }
+
+    /**
+     * Create/update configuration object
+     *
+     * @param array  $object Object data
+     * @param string $type   Object type
+     *
+     * @return bool True on success, False on failure
+     */
+    public function save(&$object, $type)
+    {
+        if (!$this->enabled) {
+            return false;
+        }
+
+        $folder = $this->find_folder($object);
+
+        if ($type) {
+            $object['type'] = $type;
+        }
+
+        return $folder->save($object, self::FOLDER_TYPE . '.' . $object['type'], $object['uid']);
+    }
+
+    /**
+     * Remove configuration object
+     *
+     * @param string $uid Object UID
+     *
+     * @return bool True on success, False on failure
+     */
+    public function delete($uid)
+    {
+        if (!$this->enabled) {
+            return false;
+        }
+
+        // fetch the object to find folder
+        $object = $this->get_object($uid);
+
+        if (!$object) {
+            return false;
+        }
+
+        $folder = $this->find_folder($object);
+
+        return $folder->delete($uid);
+    }
+
+    /**
+     * Find folder
+     */
+    public function find_folder($object = array())
+    {
+        // find folder object
+        if ($object['_mailbox']) {
+            foreach ($this->folders as $folder) {
+                if ($folder->name == $object['_mailbox']) {
+                    break;
+                }
+            }
+        }
+        else {
+            $folder = $this->default;
+        }
+
+        return $folder;
+    }
+
+    /**
+     * Builds relation member URI
+     *
+     * @param string|array Object UUID or Message folder, UID, Search headers (Message-Id, Date)
+     *
+     * @return string $url Member URI
+     */
+    public static function build_member_url($params)
+    {
+        // param is object UUID
+        if (is_string($params) && !empty($params)) {
+            return 'urn:uuid:' . $params;
+        }
+
+        if (empty($params) || !strlen($params['folder'])) {
+            return null;
+        }
+
+        $rcube   = rcube::get_instance();
+        $storage = $rcube->get_storage();
+
+        // modify folder spec. according to namespace
+        $folder = $params['folder'];
+        $ns     = $storage->folder_namespace($folder);
+
+        if ($ns == 'shared') {
+            // Note: this assumes there's only one shared namespace root
+            if ($ns = $storage->get_namespace('shared')) {
+                if ($prefix = $ns[0][0]) {
+                    $folder = 'shared' . substr($folder, strlen($prefix));
+                }
+            }
+        }
+        else {
+            if ($ns == 'other') {
+                // Note: this assumes there's only one other users namespace root
+                if ($ns = $storage->get_namespace('shared')) {
+                    if ($prefix = $ns[0][0]) {
+                        $folder = 'user' . substr($folder, strlen($prefix));
+                    }
+                }
+            }
+            else {
+                $folder = 'user' . '/' . $rcube->get_user_name() . '/' . $folder;
+            }
+        }
+
+        $folder = implode('/', array_map('rawurlencode', explode('/', $folder)));
+
+        // build URI
+        $url = 'imap:///' . $folder;
+
+        // UID is optional here because sometimes we want
+        // to build just a member uri prefix
+        if ($params['uid']) {
+            $url .= '/' . $params['uid'];
+        }
+
+        unset($params['folder']);
+        unset($params['uid']);
+
+        if (!empty($params)) {
+            $url .= '?' . http_build_query($params, '', '&');
+        }
+
+        return $url;
+    }
+
+    /**
+     * Parses relation member string
+     *
+     * @param string $url Member URI
+     *
+     * @return array Message folder, UID, Search headers (Message-Id, Date)
+     */
+    public static function parse_member_url($url)
+    {
+        // Look for IMAP URI:
+        // imap:///(user/username@domain|shared)/<folder>/<UID>?<search_params>
+        if (strpos($url, 'imap:///') === 0) {
+            $rcube   = rcube::get_instance();
+            $storage = $rcube->get_storage();
+
+            // parse_url does not work with imap:/// prefix
+            $url   = parse_url(substr($url, 8));
+            $path  = explode('/', $url['path']);
+            parse_str($url['query'], $params);
+
+            $uid  = array_pop($path);
+            $ns   = array_shift($path);
+            $path = array_map('rawurldecode', $path);
+
+            // resolve folder name
+            if ($ns == 'shared') {
+                $folder = implode('/', $path);
+                // Note: this assumes there's only one shared namespace root
+                if ($ns = $storage->get_namespace('shared')) {
+                    if ($prefix = $ns[0][0]) {
+                        $folder = $prefix . '/' . $folder;
+                    }
+                }
+            }
+            else if ($ns == 'user') {
+                $username = array_shift($path);
+                $folder   = implode('/', $path);
+
+                if ($username != $rcube->get_user_name()) {
+                    // Note: this assumes there's only one other users namespace root
+                    if ($ns = $storage->get_namespace('other')) {
+                        if ($prefix = $ns[0][0]) {
+                            $folder = $prefix . '/' . $username . '/' . $folder;
+                        }
+                    }
+                }
+                else if (!strlen($folder)) {
+                    $folder = 'INBOX';
+                }
+            }
+            else {
+                return;
+            }
+
+            return array(
+                'folder' => $folder,
+                'uid'    => $uid,
+                'params' => $params,
+            );
+        }
+
+        return false;
+    }
+
+    /**
+     * Build array of member URIs from set of messages
+     *
+     * @param string $folder   Folder name
+     * @param array  $messages Array of rcube_message objects
+     *
+     * @return array List of members (IMAP URIs)
+     */
+    public static function build_members($folder, $messages)
+    {
+        $members = array();
+
+        foreach ((array) $messages as $msg) {
+            $params = array(
+                'folder' => $folder,
+                'uid'    => $msg->uid,
+            );
+
+            // add search parameters:
+            // we don't want to build "invalid" searches e.g. that
+            // will return false positives (more or wrong messages)
+            if (($messageid = $msg->get('message-id', false)) && ($date = $msg->get('date', false))) {
+                $params['message-id'] = $messageid;
+                $params['date']       = $date;
+
+                if ($subject = $msg->get('subject', false)) {
+                    $params['subject'] = substr($subject, 0, 256);
+                }
+            }
+
+            $members[] = self::build_member_url($params);
+        }
+
+        return $members;
+    }
+
+    /**
+     * Resolve/validate/update members (which are IMAP URIs) of relation object.
+     *
+     * @param array $tag   Tag object
+     * @param bool  $force Force members list update
+     *
+     * @return array Folder/UIDs list
+     */
+    public static function resolve_members(&$tag, $force = true)
+    {
+        $result = array();
+
+        foreach ((array) $tag['members'] as $member) {
+            // IMAP URI members
+            if ($url = self::parse_member_url($member)) {
+                $folder = $url['folder'];
+
+                if (!$force) {
+                    $result[$folder][] = $url['uid'];
+                }
+                else {
+                    $result[$folder]['uid'][]    = $url['uid'];
+                    $result[$folder]['params'][] = $url['params'];
+                    $result[$folder]['member'][] = $member;
+                }
+            }
+        }
+
+        if (empty($result) || !$force) {
+            return $result;
+        }
+
+        $rcube   = rcube::get_instance();
+        $storage = $rcube->get_storage();
+        $search  = array();
+        $missing = array();
+
+        // first we search messages by Folder+UID
+        foreach ($result as $folder => $data) {
+            // @FIXME: maybe better use index() which is cached?
+            // @TODO: consider skip_deleted option
+            $index = $storage->search_once($folder, 'UID ' . rcube_imap_generic::compressMessageSet($data['uid']));
+            $uids  = $index->get();
+
+            // messages that were not found need to be searched by search parameters
+            $not_found = array_diff($data['uid'], $uids);
+            if (!empty($not_found)) {
+                foreach ($not_found as $uid) {
+                    $idx = array_search($uid, $data['uid']);
+
+                    if ($p = $data['params'][$idx]) {
+                        $search[] = $p;
+                    }
+
+                    $missing[] = $result[$folder]['member'][$idx];
+
+                    unset($result[$folder]['uid'][$idx]);
+                    unset($result[$folder]['params'][$idx]);
+                    unset($result[$folder]['member'][$idx]);
+                }
+            }
+
+            $result[$folder] = $uids;
+        }
+
+        // search in all subscribed mail folders using search parameters
+        if (!empty($search)) {
+            // remove not found members from the members list
+            $tag['members'] = array_diff($tag['members'], $missing);
+
+            // get subscribed folders
+            $folders = $storage->list_folders_subscribed('', '*', 'mail', null, true);
+
+            // @TODO: do this search in chunks (for e.g. 10 messages)?
+            $search_str = '';
+
+            foreach ($search as $p) {
+                $search_params = array();
+                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_str .= ' (' . implode(' ', $search_params) . ')';
+            }
+
+            $search_str = trim(str_repeat(' OR', count($search)-1) . $search_str);
+
+            // search
+            $search = $storage->search_once($folders, $search_str);
+
+            // handle search result
+            $folders = (array) $search->get_parameters('MAILBOX');
+
+            foreach ($folders as $folder) {
+                $set  = $search->get_set($folder);
+                $uids = $set->get();
+
+                if (!empty($uids)) {
+                    $msgs    = $storage->fetch_headers($folder, $uids, false);
+                    $members = self::build_members($folder, $msgs);
+
+                    // merge new members into the tag members list
+                    $tag['members'] = array_merge($tag['members'], $members);
+
+                    // add UIDs into the result
+                    $result[$folder] = array_unique(array_merge((array)$result[$folder], $uids));
+                }
+            }
+
+            // update tag object with new members list
+            $tag['members'] = array_unique($tag['members']);
+            kolab_storage_config::get_instance()->save($tag, 'relation', false);
+        }
+
+        return $result;
+    }
+
+    /**
+     * Assign tags to kolab objects
+     *
+     * @param array $records List of kolab objects
+     *
+     * @return array List of tags
+     */
+    public function apply_tags(&$records)
+    {
+        // first convert categories into tags
+        foreach ($records as $i => $rec) {
+            if (!empty($rec['categories'])) {
+                $folder = new kolab_storage_folder($rec['_mailbox']);
+                if ($object = $folder->get_object($rec['uid'])) {
+                    $tags = $rec['categories'];
+
+                    unset($object['categories']);
+                    unset($records[$i]['categories']);
+
+                    $this->save_tags($rec['uid'], $tags);
+                    $folder->save($object, $rec['_type'], $rec['uid']);
+                }
+            }
+        }
+
+        $tags = array();
+
+        // assign tags to objects
+        foreach ($this->get_tags() as $tag) {
+            foreach ($records as $idx => $rec) {
+                $uid = self::build_member_url($rec['uid']);
+                if (in_array($uid, (array) $tag['members'])) {
+                    $records[$idx]['tags'][] = $tag['name'];
+                }
+            }
+
+            $tags[] = $tag['name'];
+        }
+
+        $tags = array_unique($tags);
+
+        return $tags;
+    }
+
+    /**
+     * Update object tags
+     *
+     * @param string $uid  Kolab object UID
+     * @param array  $tags List of tag names
+     */
+    public function save_tags($uid, $tags)
+    {
+        $url       = self::build_member_url($uid);
+        $relations = $this->get_tags();
+
+        foreach ($relations as $idx => $relation) {
+            $selected = !empty($tags) && in_array($relation['name'], $tags);
+            $found    = !empty($relation['members']) && in_array($url, $relation['members']);
+            $update   = false;
+
+            // remove member from the relation
+            if ($found && !$selected) {
+                $relation['members'] = array_diff($relation['members'], (array) $url);
+                $update = true;
+            }
+            // add member to the relation
+            else if (!$found && $selected) {
+                $relation['members'][] = $url;
+                $update = true;
+            }
+
+            if ($update) {
+                if ($this->save($relation, 'relation')) {
+                    $this->tags[$idx] = $relation; // update in-memory cache
+                }
+            }
+
+            if ($selected) {
+                $tags = array_diff($tags, (array)$relation['name']);
+            }
+        }
+
+        // create new relations
+        if (!empty($tags)) {
+            foreach ($tags as $tag) {
+                $relation = array(
+                    'name'     => $tag,
+                    'members'  => (array) $url,
+                    'category' => 'tag',
+                );
+
+                if ($this->save($relation, 'relation')) {
+                    $this->tags[] = $relation; // update in-memory cache
+                }
+            }
+        }
+    }
+
+    /**
+     * Get tags (all or referring to specified object)
+     *
+     * @param string $uid Optional object UID
+     *
+     * @return array List of Relation objects
+     */
+    public function get_tags($uid = '*')
+    {
+        if (!isset($this->tags)) {
+            $default = true;
+            $filter  = array(
+                array('type', '=', 'relation'),
+                array('category', '=', 'tag')
+            );
+
+            // use faster method
+            if ($uid && $uid != '*') {
+                $filter[] = array('member', '=', $uid);
+                $tags = $this->get_objects($filter, $default);
+            }
+            else {
+                $this->tags = $tags = $this->get_objects($filter, $default);
+            }
+        }
+        else {
+            $tags = $this->tags;
+        }
+
+        if ($uid === '*') {
+            return $tags;
+        }
+
+        $result = array();
+        $search = self::build_member_url($uid);
+
+        foreach ($tags as $tag) {
+            if (in_array($search, (array) $tag['members'])) {
+                $result[] = $tag;
+            }
+        }
+
+        return $result;
+    }
+
+    /**
+     * Find objects linked with the given groupware object through a relation
+     *
+     * @param string Object UUID
+     * @param array List of related URIs
+     */
+    public function get_object_links($uid)
+    {
+        $links = array();
+        $object_uri = self::build_member_url($uid);
+
+        foreach ($this->get_relations_for_member($uid) as $relation) {
+            if (in_array($object_uri, (array) $relation['members'])) {
+                // make relation members up-to-date
+                kolab_storage_config::resolve_members($relation);
+
+                foreach ($relation['members'] as $member) {
+                    if ($member != $object_uri) {
+                        $links[] = $member;
+                    }
+                }
+            }
+        }
+
+        return array_unique($links);
+    }
+
+    /**
+     *
+     */
+    public function save_object_links($uid, $links, $remove = array())
+    {
+        $object_uri = self::build_member_url($uid);
+        $relations = $this->get_relations_for_member($uid);
+        $done = false;
+
+        foreach ($relations as $relation) {
+            // make relation members up-to-date
+            kolab_storage_config::resolve_members($relation);
+
+            // remove and add links
+            $members = array_diff($relation['members'], (array)$remove);
+            $members = array_unique(array_merge($members, $links));
+
+            // make sure the object_uri is still a member
+            if (!in_array($object_uri, $members)) {
+                $members[$object_uri];
+            }
+
+            // remove relation if no other members remain
+            if (count($members) <= 1) {
+                $done = $this->delete($relation['uid']);
+            }
+            // update relation object if members changed
+            else if (count(array_diff($members, $relation['members'])) || count(array_diff($relation['members'], $members))) {
+                $relation['members'] = $members;
+                $done = $this->save($relation, 'relation');
+                $links = array();
+            }
+            // no changes, we're happy
+            else {
+                $done = true;
+                $links = array();
+            }
+        }
+
+        // create a new relation
+        if (!$done && !empty($links)) {
+            $relation = array(
+                'members'  => array_merge($links, array($object_uri)),
+                'category' => 'generic',
+            );
+
+            $ret = $this->save($relation, 'relation');
+        }
+
+        return $ret;
+    }
+
+    /**
+     * Find relation objects referring to specified note
+     */
+    public function get_relations_for_member($uid, $reltype = 'generic')
+    {
+        $default = true;
+        $filter  = array(
+            array('type', '=', 'relation'),
+            array('category', '=', $reltype),
+            array('member', '=', $uid),
+        );
+
+        return $this->get_objects($filter, $default, 100);
+    }
+
+    /**
+     * Find kolab objects assigned to specified e-mail message
+     *
+     * @param rcube_message $message E-mail message
+     * @param string        $folder  Folder name
+     * @param string        $type    Result objects type
+     *
+     * @return array List of kolab objects
+     */
+    public function get_message_relations($message, $folder, $type)
+    {
+        static $_cache = array();
+
+        $result  = array();
+        $uids    = array();
+        $default = true;
+        $uri     = self::get_message_uri($message, $folder);
+        $filter  = array(
+            array('type', '=', 'relation'),
+            array('category', '=', 'generic'),
+        );
+
+        // query by message-id
+        $member_id = $message->get('message-id', false);
+        if (empty($member_id)) {
+            // derive message identifier from URI
+            $member_id = md5($uri);
+        }
+        $filter[] = array('member', '=', $member_id);
+
+        if (!isset($_cache[$uri])) {
+            // get UIDs of related groupware objects
+            foreach ($this->get_objects($filter, $default) as $relation) {
+                // we don't need to update members if the URI is found
+                if (!in_array($uri, $relation['members'])) {
+                    // update members...
+                    $messages = kolab_storage_config::resolve_members($relation);
+                    // ...and check again
+                    if (empty($messages[$folder]) || !in_array($message->uid, $messages[$folder])) {
+                        continue;
+                    }
+                }
+
+                // find groupware object UID(s)
+                foreach ($relation['members'] as $member) {
+                    if (strpos($member, 'urn:uuid:') === 0) {
+                        $uids[] = substr($member, 9);
+                    }
+                }
+            }
+
+            // remember this lookup
+            $_cache[$uri] = $uids;
+        }
+        else {
+            $uids = $_cache[$uri];
+        }
+
+        // get kolab objects of specified type
+        if (!empty($uids)) {
+            $query  = array(array('uid', '=', array_unique($uids)));
+            $result = kolab_storage::select($query, $type);
+        }
+
+        return $result;
+    }
+
+    /**
+     * Build a URI representing the given message reference
+     */
+    public static function get_message_uri($headers, $folder)
+    {
+        $params = array(
+            'folder' => $headers->folder ?: $folder,
+            'uid'    => $headers->uid,
+        );
+
+        if (($messageid = $headers->get('message-id', false)) && ($date = $headers->get('date', false))) {
+            $params['message-id'] = $messageid;
+            $params['date']       = $date;
+
+            if ($subject = $headers->get('subject')) {
+                $params['subject'] = $subject;
+            }
+        }
+
+        return self::build_member_url($params);
+    }
+}
diff --git a/lib/plugins/libkolab/lib/kolab_storage_dataset.php b/lib/plugins/libkolab/lib/kolab_storage_dataset.php
new file mode 100644
index 0000000..9ddf3f9
--- /dev/null
+++ b/lib/plugins/libkolab/lib/kolab_storage_dataset.php
@@ -0,0 +1,154 @@
+<?php
+
+/**
+ * Dataset class providing the results of a select operation on a kolab_storage_folder.
+ *
+ * Can be used as a normal array as well as an iterator in foreach() loops.
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli at kolabsys.com>
+ *
+ * Copyright (C) 2014, Kolab Systems AG <contact at kolabsys.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class kolab_storage_dataset implements Iterator, ArrayAccess, Countable
+{
+    private $cache;  // kolab_storage_cache instance to use for fetching data
+    private $memlimit = 0;
+    private $buffer = false;
+    private $index = array();
+    private $data = array();
+    private $iteratorkey = 0;
+    private $error = null;
+
+    /**
+     * Default constructor
+     *
+     * @param object kolab_storage_cache instance to be used for fetching objects upon access
+     */
+    public function __construct($cache)
+    {
+        $this->cache = $cache;
+
+        // enable in-memory buffering up until 1/5 of the available memory
+        if (function_exists('memory_get_usage')) {
+            $this->memlimit = parse_bytes(ini_get('memory_limit')) / 5;
+            $this->buffer = true;
+        }
+    }
+
+    /**
+     * Return error state
+     */
+    public function is_error()
+    {
+        return !empty($this->error);
+    }
+
+    /**
+     * Set error state
+     */
+    public function set_error($err)
+    {
+        $this->error = $err;
+    }
+
+
+    /*** Implement PHP Countable interface ***/
+
+    public function count()
+    {
+        return count($this->index);
+    }
+
+
+    /*** Implement PHP ArrayAccess interface ***/
+
+    public function offsetSet($offset, $value)
+    {
+        $uid = $value['_msguid'];
+
+        if (is_null($offset)) {
+            $offset = count($this->index);
+            $this->index[] = $uid;
+        }
+        else {
+            $this->index[$offset] = $uid;
+        }
+
+        // keep full payload data in memory if possible
+        if ($this->memlimit && $this->buffer && isset($value['_mailbox'])) {
+            $this->data[$offset] = $value;
+
+            // check memory usage and stop buffering
+            if ($offset % 10 == 0) {
+                $this->buffer = memory_get_usage() < $this->memlimit;
+            }
+        }
+    }
+
+    public function offsetExists($offset)
+    {
+        return isset($this->index[$offset]);
+    }
+
+    public function offsetUnset($offset)
+    {
+        unset($this->index[$offset]);
+    }
+
+    public function offsetGet($offset)
+    {
+        if (isset($this->data[$offset])) {
+            return $this->data[$offset];
+        }
+        else if ($msguid = $this->index[$offset]) {
+            return $this->cache->get($msguid);
+        }
+
+        return null;
+    }
+
+
+    /*** Implement PHP Iterator interface ***/
+
+    public function current()
+    {
+        return $this->offsetGet($this->iteratorkey);
+    }
+
+    public function key()
+    {
+        return $this->iteratorkey;
+    }
+
+    public function next()
+    {
+        $this->iteratorkey++;
+        return $this->valid();
+    }
+
+    public function rewind()
+    {
+        $this->iteratorkey = 0;
+    }
+
+    public function valid()
+    {
+        return !empty($this->index[$this->iteratorkey]);
+    }
+
+}
diff --git a/lib/plugins/libkolab/lib/kolab_storage_folder.php b/lib/plugins/libkolab/lib/kolab_storage_folder.php
index 74d95b7..f7370bb 100644
--- a/lib/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/lib/plugins/libkolab/lib/kolab_storage_folder.php
@@ -48,7 +48,13 @@ class kolab_storage_folder
      */
     public $cache;
 
-    private $type_annotation;
+     /**
+      * Indicate validity status
+      * @var boolean
+      */
+    public $valid = false;
+
+    private $error = 0;
     private $namespace;
     private $imap;
     private $info;
@@ -59,12 +65,15 @@ class kolab_storage_folder
 
     /**
      * Default constructor
+     *
+     * @param string The folder name/path
+     * @param string Expected folder type
      */
-    function __construct($name, $type = null)
+    function __construct($name, $type = null, $type_annotation = null)
     {
         $this->imap = rcube::get_instance()->get_storage();
         $this->imap->set_options(array('skip_deleted' => true));
-        $this->set_folder($name, $type);
+        $this->set_folder($name, $type, $type_annotation);
     }
 
 
@@ -74,24 +83,43 @@ class kolab_storage_folder
      * @param string The folder name/path
      * @param string Optional folder type if known
      */
-    public function set_folder($name, $ftype = null)
+    public function set_folder($name, $type = null, $type_annotation = null)
     {
-        $this->type_annotation = $ftype ? $ftype : kolab_storage::folder_type($name);
+        if (empty($type_annotation)) {
+            $type_annotation = kolab_storage::folder_type($name);
+        }
 
         $oldtype = $this->type;
-        list($this->type, $suffix) = explode('.', $this->type_annotation);
+        list($this->type, $suffix) = explode('.', $type_annotation);
         $this->default      = $suffix == 'default';
         $this->name         = $name;
-        $this->resource_uri = null;
+        $this->valid        = !empty($this->type) && $this->type != 'mail' && (!$type || $this->type == $type);
+
+        if (!$this->valid) {
+            $this->error = $this->imap->get_error_code() < 0 ? kolab_storage::ERROR_IMAP_CONN : kolab_storage::ERROR_INVALID_FOLDER;
+        }
+
+        // reset cached object properties
+        $this->owner = $this->namespace = $this->resource_uri = $this->info = $this->idata = null;
 
         // get a new cache instance of folder type changed
-        if (!$this->cache || $type != $oldtype)
+        if (!$this->cache || $this->type != $oldtype)
             $this->cache = kolab_storage_cache::factory($this);
+        else
+            $this->cache->set_folder($this);
 
         $this->imap->set_folder($this->name);
-        $this->cache->set_folder($this);
     }
 
+    /**
+     * Returns code of last error
+     *
+     * @return int Error code
+     */
+    public function get_error()
+    {
+        return $this->error ?: $this->cache->get_error();
+    }
 
     /**
      *
@@ -143,9 +171,10 @@ class kolab_storage_folder
     /**
      * Returns the owner of the folder.
      *
+     * @param boolean  Return a fully qualified owner name (i.e. including domain for shared folders)
      * @return string  The owner of this folder.
      */
-    public function get_owner()
+    public function get_owner($fully_qualified = false)
     {
         // return cached value
         if (isset($this->owner))
@@ -164,16 +193,21 @@ class kolab_storage_folder
             break;
 
         default:
-            list($prefix, $user) = explode($this->imap->get_hierarchy_delimiter(), $info['name']);
-            if (strpos($user, '@') === false) {
-                $domain = strstr($rcmail->get_user_name(), '@');
-                if (!empty($domain))
-                    $user .= $domain;
-            }
-            $this->owner = $user;
+            list($prefix, $this->owner) = explode($this->imap->get_hierarchy_delimiter(), $info['name']);
+            $fully_qualified = true;  // enforce email addresses (backwards compatibility)
             break;
         }
 
+        if ($fully_qualified && strpos($this->owner, '@') === false) {
+            // extract domain from current user name
+            $domain = strstr($rcmail->get_user_name(), '@');
+            // fall back to mail_domain config option
+            if (empty($domain) && ($mdomain = $rcmail->config->mail_domain($this->imap->options['host']))) {
+                $domain = '@' . $mdomain;
+            }
+            $this->owner .= $domain;
+        }
+
         return $this->owner;
     }
 
@@ -259,7 +293,7 @@ class kolab_storage_folder
         }
 
         // compose fully qualified ressource uri for this instance
-        $this->resource_uri = 'imap://' . urlencode($this->get_owner()) . '@' . $this->imap->options['host'] . '/' . $subpath;
+        $this->resource_uri = 'imap://' . urlencode($this->get_owner(true)) . '@' . $this->imap->options['host'] . '/' . $subpath;
         return $this->resource_uri;
     }
 
@@ -280,7 +314,7 @@ class kolab_storage_folder
         }
 
         // generate a folder UID and set it to IMAP
-        $uid = rtrim(chunk_split(md5($this->name . $this->get_owner()), 12, '-'), '-');
+        $uid = rtrim(chunk_split(md5($this->name . $this->get_owner() . uniqid('-', true)), 12, '-'), '-');
         $this->set_uid($uid);
 
         return $uid;
@@ -363,6 +397,10 @@ class kolab_storage_folder
      */
     public function count($query = null)
     {
+        if (!$this->valid) {
+            return 0;
+        }
+
         // synchronize cache first
         $this->cache->synchronize();
 
@@ -380,6 +418,10 @@ class kolab_storage_folder
     {
         if (!$type) $type = $this->type;
 
+        if (!$this->valid) {
+            return array();
+        }
+
         // synchronize caches
         $this->cache->synchronize();
 
@@ -397,9 +439,14 @@ class kolab_storage_folder
      */
     public function select($query = array())
     {
+        if (!$this->valid) {
+            return array();
+        }
+
         // check query argument
-        if (empty($query))
+        if (empty($query)) {
             return $this->get_objects();
+        }
 
         // synchronize caches
         $this->cache->synchronize();
@@ -417,6 +464,10 @@ class kolab_storage_folder
      */
     public function get_uids($query = array())
     {
+        if (!$this->valid) {
+            return array();
+        }
+
         // synchronize caches
         $this->cache->synchronize();
 
@@ -424,6 +475,21 @@ class kolab_storage_folder
         return $this->cache->select($this->_prepare_query($query), true);
     }
 
+    /**
+     * Setter for ORDER BY and LIMIT parameters for cache queries
+     *
+     * @param array   List of columns to order by
+     * @param integer Limit result set to this length
+     * @param integer Offset row
+     */
+    public function set_order_and_limit($sortcols, $length = null, $offset = 0)
+    {
+        $this->cache->set_order_by($sortcols);
+
+        if ($length !== null) {
+            $this->cache->set_limit($length, $offset);
+        }
+    }
 
     /**
      * Helper method to sanitize query arguments
@@ -463,6 +529,10 @@ class kolab_storage_folder
      */
     public function get_object($uid, $type = null)
     {
+        if (!$this->valid) {
+            return false;
+        }
+
         // synchronize caches
         $this->cache->synchronize();
 
@@ -492,7 +562,7 @@ class kolab_storage_folder
      */
     public function get_attachment($uid, $part, $mailbox = null, $print = false, $fp = null, $skip_charset_conv = false)
     {
-        if ($msguid = ($mailbox ? $uid : $this->cache->uid2msguid($uid))) {
+        if ($this->valid && ($msguid = ($mailbox ? $uid : $this->cache->uid2msguid($uid)))) {
             $this->imap->set_folder($mailbox ? $mailbox : $this->name);
             return $this->imap->get_message_part($msguid, $part, null, $print, $fp, $skip_charset_conv);
         }
@@ -513,6 +583,10 @@ class kolab_storage_folder
      */
     public function read_object($msguid, $type = null, $folder = null)
     {
+        if (!$this->valid) {
+            return false;
+        }
+
         if (!$type) $type = $this->type;
         if (!$folder) $folder = $this->name;
 
@@ -658,6 +732,10 @@ class kolab_storage_folder
      */
     public function save(&$object, $type = null, $uid = null)
     {
+        if (!$this->valid) {
+            return false;
+        }
+
         if (!$type)
             $type = $this->type;
 
@@ -850,6 +928,10 @@ class kolab_storage_folder
      */
     public function delete($object, $expunge = true)
     {
+        if (!$this->valid) {
+            return false;
+        }
+
         $msguid = is_array($object) ? $object['_msguid'] : $this->cache->uid2msguid($object);
         $success = false;
 
@@ -877,6 +959,10 @@ class kolab_storage_folder
      */
     public function delete_all()
     {
+        if (!$this->valid) {
+            return false;
+        }
+
         $this->cache->purge();
         $this->cache->bypass(true);
         $result = $this->imap->clear_folder($this->name);
@@ -894,6 +980,10 @@ class kolab_storage_folder
      */
     public function undelete($uid)
     {
+        if (!$this->valid) {
+            return false;
+        }
+
         if ($msguid = $this->cache->uid2msguid($uid, true)) {
             $this->cache->bypass(true);
             $result = $this->imap->set_flag($msguid, 'UNDELETED', $this->name);
@@ -917,9 +1007,16 @@ class kolab_storage_folder
      */
     public function move($uid, $target_folder)
     {
+        if (!$this->valid) {
+            return false;
+        }
+
+        if (is_string($target_folder))
+            $target_folder = kolab_storage::get_folder($target_folder);
+
         if ($msguid = $this->cache->uid2msguid($uid)) {
             $this->cache->bypass(true);
-            $result = $this->imap->move_message($msguid, $target_folder, $this->name);
+            $result = $this->imap->move_message($msguid, $target_folder->name, $this->name);
             $this->cache->bypass(false);
 
             if ($result) {
@@ -1169,8 +1266,6 @@ class kolab_storage_folder
      */
     private function trigger_url($url, $auth_user = null, $auth_passwd = null)
     {
-        require_once('HTTP/Request2.php');
-
         try {
             $request = libkolab::http_request($url);
 




More information about the commits mailing list