lib/ext lib/kolab_sync_backend.php lib/kolab_sync_data_notes.php lib/kolab_sync.php lib/plugins

Aleksander Machniak machniak at kolabsys.com
Sat Mar 15 15:33:24 CET 2014


 lib/ext/Syncroton/Command/FolderCreate.php                 |    4 
 lib/ext/Syncroton/Command/FolderSync.php                   |    3 
 lib/ext/Syncroton/Command/Sync.php                         |    9 
 lib/ext/Syncroton/Data/Factory.php                         |    5 
 lib/ext/Syncroton/Data/Notes.php                           |   24 
 lib/ext/Syncroton/Model/Note.php                           |   38 +
 lib/ext/Syncroton/Registry.php                             |   10 
 lib/kolab_sync.php                                         |    1 
 lib/kolab_sync_backend.php                                 |    3 
 lib/kolab_sync_data_notes.php                              |  148 +++++
 lib/plugins/libkolab/lib/kolab_date_recurrence.php         |   12 
 lib/plugins/libkolab/lib/kolab_format.php                  |   51 +
 lib/plugins/libkolab/lib/kolab_format_contact.php          |   59 +-
 lib/plugins/libkolab/lib/kolab_format_distributionlist.php |   26 
 lib/plugins/libkolab/lib/kolab_format_note.php             |   43 +
 lib/plugins/libkolab/lib/kolab_format_xcal.php             |   84 +--
 lib/plugins/libkolab/lib/kolab_storage_cache.php           |  360 +++++++------
 lib/plugins/libkolab/lib/kolab_storage_cache_contact.php   |   16 
 lib/plugins/libkolab/lib/kolab_storage_cache_event.php     |    8 
 lib/plugins/libkolab/lib/kolab_storage_cache_task.php      |    4 
 lib/plugins/libkolab/lib/kolab_storage_dataset.php         |  154 +++++
 lib/plugins/libkolab/lib/kolab_storage_folder.php          |   49 +
 22 files changed, 852 insertions(+), 259 deletions(-)

New commits:
commit f4d368d74b050b55e053ad04d9643b493456c40f
Author: Aleksander Machniak <machniak at kolabsys.com>
Date:   Sat Mar 15 15:32:52 2014 +0100

    Support Notes synchronization, updated libkolab plugin

diff --git a/lib/ext/Syncroton/Command/FolderCreate.php b/lib/ext/Syncroton/Command/FolderCreate.php
index e7eda47..f3787f3 100644
--- a/lib/ext/Syncroton/Command/FolderCreate.php
+++ b/lib/ext/Syncroton/Command/FolderCreate.php
@@ -60,6 +60,10 @@ class Syncroton_Command_FolderCreate extends Syncroton_Command_Wbxml
             case Syncroton_Command_FolderSync::FOLDERTYPE_MAIL_USER_CREATED:
                 $folder->class = Syncroton_Data_Factory::CLASS_EMAIL;
                 break;
+
+            case Syncroton_Command_FolderSync::FOLDERTYPE_NOTE_USER_CREATED:
+                $folder->class = Syncroton_Data_Factory::CLASS_NOTES;
+                break;
                 
             case Syncroton_Command_FolderSync::FOLDERTYPE_TASK_USER_CREATED:
                 $folder->class = Syncroton_Data_Factory::CLASS_TASKS;
diff --git a/lib/ext/Syncroton/Command/FolderSync.php b/lib/ext/Syncroton/Command/FolderSync.php
index 87d9d3f..ed3a350 100644
--- a/lib/ext/Syncroton/Command/FolderSync.php
+++ b/lib/ext/Syncroton/Command/FolderSync.php
@@ -48,7 +48,7 @@ class Syncroton_Command_FolderSync extends Syncroton_Command_Wbxml
     const FOLDERTYPE_CONTACT_USER_CREATED   = 14;
     const FOLDERTYPE_TASK_USER_CREATED      = 15;
     const FOLDERTYPE_JOURNAL_USER_CREATED   = 16;
-    const FOLDERTYPE_NOTES_USER_CREATED     = 17;
+    const FOLDERTYPE_NOTE_USER_CREATED      = 17;
     const FOLDERTYPE_UNKOWN                 = 18;
     
     protected $_defaultNameSpace    = 'uri:FolderHierarchy';
@@ -58,6 +58,7 @@ class Syncroton_Command_FolderSync extends Syncroton_Command_Wbxml
         Syncroton_Data_Factory::CLASS_CALENDAR,
         Syncroton_Data_Factory::CLASS_CONTACTS,
         Syncroton_Data_Factory::CLASS_EMAIL,
+        Syncroton_Data_Factory::CLASS_NOTES,
         Syncroton_Data_Factory::CLASS_TASKS
     );
 
diff --git a/lib/ext/Syncroton/Command/Sync.php b/lib/ext/Syncroton/Command/Sync.php
index 8feb0ef..339808b 100644
--- a/lib/ext/Syncroton/Command/Sync.php
+++ b/lib/ext/Syncroton/Command/Sync.php
@@ -235,27 +235,26 @@ class Syncroton_Command_Sync extends Syncroton_Command_Wbxml
             switch($collectionData->folder->class) {
                 case Syncroton_Data_Factory::CLASS_CALENDAR:
                     $dataClass = 'Syncroton_Model_Event';
-                    
                     break;
                     
                 case Syncroton_Data_Factory::CLASS_CONTACTS:
                     $dataClass = 'Syncroton_Model_Contact';
-                    
                     break;
                     
                 case Syncroton_Data_Factory::CLASS_EMAIL:
                     $dataClass = 'Syncroton_Model_Email';
-                    
                     break;
                     
+                case Syncroton_Data_Factory::CLASS_NOTES:
+                    $dataClass = 'Syncroton_Model_Note';
+                    break;
+
                 case Syncroton_Data_Factory::CLASS_TASKS:
                     $dataClass = 'Syncroton_Model_Task';
-                    
                     break;
                     
                 default:
                     throw new Syncroton_Exception_UnexpectedValue('invalid class provided');
-                    
                     break;
             }
             
diff --git a/lib/ext/Syncroton/Data/Factory.php b/lib/ext/Syncroton/Data/Factory.php
index 2904b28..994cf46 100644
--- a/lib/ext/Syncroton/Data/Factory.php
+++ b/lib/ext/Syncroton/Data/Factory.php
@@ -21,6 +21,7 @@ class Syncroton_Data_Factory
     const CLASS_CALENDAR = 'Calendar';
     const CLASS_CONTACTS = 'Contacts';
     const CLASS_EMAIL    = 'Email';
+    const CLASS_NOTES    = 'Notes';
     const CLASS_TASKS    = 'Tasks';
     const STORE_EMAIL    = 'Mailbox';
     const STORE_GAL      = 'GAL';
@@ -50,6 +51,10 @@ class Syncroton_Data_Factory
                 $className = Syncroton_Registry::get(Syncroton_Registry::EMAIL_DATA_CLASS);
                 break;
                 
+            case self::CLASS_NOTES:
+                $className = Syncroton_Registry::get(Syncroton_Registry::NOTES_DATA_CLASS);
+                break;
+
             case self::CLASS_TASKS:
                 $className = Syncroton_Registry::get(Syncroton_Registry::TASKS_DATA_CLASS);
                 break;
diff --git a/lib/ext/Syncroton/Data/Notes.php b/lib/ext/Syncroton/Data/Notes.php
new file mode 100644
index 0000000..0ca89ad
--- /dev/null
+++ b/lib/ext/Syncroton/Data/Notes.php
@@ -0,0 +1,24 @@
+<?php
+/**
+ * Syncroton
+ *
+ * @package     Syncroton
+ * @subpackage  Data
+ * @license     http://www.tine20.org/licenses/lgpl.html LGPL Version 3
+ * @copyright   Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Lars Kneschke <l.kneschke at metaways.de>
+ */
+
+/**
+ * class to handle ActiveSync Sync command
+ *
+ * @package     Syncroton
+ * @subpackage  Data
+ */
+class Syncroton_Data_Notes extends Syncroton_Data_AData
+{
+    protected $_supportedFolderTypes = array(
+        Syncroton_Command_FolderSync::FOLDERTYPE_NOTE,
+        Syncroton_Command_FolderSync::FOLDERTYPE_NOTE_USER_CREATED
+    );
+}
diff --git a/lib/ext/Syncroton/Model/Note.php b/lib/ext/Syncroton/Model/Note.php
new file mode 100644
index 0000000..5f4b542
--- /dev/null
+++ b/lib/ext/Syncroton/Model/Note.php
@@ -0,0 +1,38 @@
+<?php
+/**
+ * Syncroton
+ *
+ * @package     Syncroton
+ * @subpackage  Model
+ * @license     http://www.tine20.org/licenses/lgpl.html LGPL Version 3
+ * @copyright   Copyright (c) 2014 Kolab Systems AG (http://www.kolabsys.com)
+ * @author      Aleksander Machniak <machniak at kolabsys.com>
+ */
+
+/**
+ * class to handle ActiveSync note
+ *
+ * @package     Syncroton
+ * @subpackage  Model
+ * @property    Syncroton_Model_EmailBody body
+ * @property    array                     categories
+ * @property    DateTime                  lastModifiedDate
+ * @property    string                    messageClass
+ * @property    string                    subject
+ */
+class Syncroton_Model_Note extends Syncroton_Model_AXMLEntry
+{
+    protected $_xmlBaseElement = 'ApplicationData';
+
+    protected $_properties = array(
+        'AirSyncBase' => array(
+            'body'             => array('type' => 'container', 'class' => 'Syncroton_Model_EmailBody')
+        ),
+        'Notes' => array(
+            'categories'       => array('type' => 'container', 'childElement' => 'category'),
+            'lastModifiedDate' => array('type' => 'datetime'),
+            'messageClass'     => array('type' => 'string'),
+            'subject'          => array('type' => 'string'),
+        )
+    );
+}
\ No newline at end of file
diff --git a/lib/ext/Syncroton/Registry.php b/lib/ext/Syncroton/Registry.php
index 5f9ac46..5460f96 100644
--- a/lib/ext/Syncroton/Registry.php
+++ b/lib/ext/Syncroton/Registry.php
@@ -30,6 +30,7 @@ class Syncroton_Registry extends ArrayObject
     const CALENDAR_DATA_CLASS = 'calendar_data_class';
     const CONTACTS_DATA_CLASS = 'contacts_data_class';
     const EMAIL_DATA_CLASS    = 'email_data_class';
+    const NOTES_DATA_CLASS    = 'notes_data_class';
     const TASKS_DATA_CLASS    = 'tasks_data_class';
     const GAL_DATA_CLASS      = 'gal_data_class';
     
@@ -356,6 +357,15 @@ class Syncroton_Registry extends ArrayObject
         self::set(self::EMAIL_DATA_CLASS, $className);
     }
     
+    public static function setNotesDataClass($className)
+    {
+        if (!class_exists($className)) {
+            throw new InvalidArgumentException('invalid $_className provided');
+        }
+
+        self::set(self::NOTES_DATA_CLASS, $className);
+    }
+
     public static function setTasksDataClass($className)
     {
         if (!class_exists($className)) {
diff --git a/lib/kolab_sync.php b/lib/kolab_sync.php
index 1870996..3ae473c 100644
--- a/lib/kolab_sync.php
+++ b/lib/kolab_sync.php
@@ -149,6 +149,7 @@ class kolab_sync extends rcube
         Syncroton_Registry::setContactsDataClass('kolab_sync_data_contacts');
         Syncroton_Registry::setCalendarDataClass('kolab_sync_data_calendar');
         Syncroton_Registry::setEmailDataClass('kolab_sync_data_email');
+        Syncroton_Registry::setNotesDataClass('kolab_sync_data_notes');
         Syncroton_Registry::setTasksDataClass('kolab_sync_data_tasks');
         Syncroton_Registry::setGALDataClass('kolab_sync_data_gal');
 
diff --git a/lib/kolab_sync_backend.php b/lib/kolab_sync_backend.php
index 7e7ea66..cd984bb 100644
--- a/lib/kolab_sync_backend.php
+++ b/lib/kolab_sync_backend.php
@@ -61,6 +61,7 @@ class kolab_sync_backend
         Syncroton_Data_Factory::CLASS_CALENDAR => 'event',
         Syncroton_Data_Factory::CLASS_CONTACTS => 'contact',
         Syncroton_Data_Factory::CLASS_EMAIL    => 'mail',
+        Syncroton_Data_Factory::CLASS_NOTES    => 'note',
         Syncroton_Data_Factory::CLASS_TASKS    => 'task',
     );
 
@@ -521,9 +522,11 @@ class kolab_sync_backend
             'mail.outbox',
             'event.default',
             'contact.default',
+            'note.default',
             'task.default',
             'event',
             'contact',
+            'note',
             'task'
         );
 
diff --git a/lib/kolab_sync_data_notes.php b/lib/kolab_sync_data_notes.php
new file mode 100644
index 0000000..1bc151f
--- /dev/null
+++ b/lib/kolab_sync_data_notes.php
@@ -0,0 +1,148 @@
+<?php
+
+/**
+ +--------------------------------------------------------------------------+
+ | Kolab Sync (ActiveSync for Kolab)                                        |
+ |                                                                          |
+ | Copyright (C) 2011-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/>      |
+ +--------------------------------------------------------------------------+
+ | Author: Aleksander Machniak <machniak at kolabsys.com>                      |
+ +--------------------------------------------------------------------------+
+*/
+
+/**
+ * Notes data class for Syncroton
+ */
+class kolab_sync_data_notes extends kolab_sync_data
+{
+    /**
+     * Mapping from ActiveSync Calendar namespace fields
+     */
+    protected $mapping = array(
+        'body'             => 'description',
+        'categories'       => 'categories',
+        'lastModifiedDate' => 'changed',
+        //'messageClass'     => 'messageClass',
+        'subject'          => 'title',
+    );
+
+
+
+    /**
+     * Kolab object type
+     *
+     * @var string
+     */
+    protected $modelName = 'note';
+
+    /**
+     * Type of the default folder
+     *
+     * @var int
+     */
+    protected $defaultFolderType = Syncroton_Command_FolderSync::FOLDERTYPE_NOTE;
+
+    /**
+     * Default container for new entries
+     *
+     * @var string
+     */
+    protected $defaultFolder = 'Notes';
+
+    /**
+     * Type of user created folders
+     *
+     * @var int
+     */
+    protected $folderType = Syncroton_Command_FolderSync::FOLDERTYPE_NOTE_USER_CREATED;
+
+
+    /**
+     * Appends note data to xml element
+     *
+     * @param Syncroton_Model_SyncCollection $collection Collection data
+     * @param string                         $serverId   Local entry identifier
+     * @param boolean                        $as_array   Return entry as an array
+     *
+     * @return array|Syncroton_Model_Note|array Note object
+     */
+    public function getEntry(Syncroton_Model_SyncCollection $collection, $serverId, $as_array = false)
+    {
+        $note   = is_array($serverId) ? $serverId : $this->getObject($collection->collectionId, $serverId);
+        $config = $this->getFolderConfig($note['_mailbox']);
+        $result = array();
+
+        // Calendar namespace fields
+        foreach ($this->mapping as $key => $name) {
+            $value = $this->getKolabDataItem($note, $name);
+
+            switch ($name) {
+            case 'changed':
+                $value = self::date_from_kolab($value);
+                break;
+
+            case 'description':
+                $value = $this->setBody($value);
+                break;
+            }
+
+            if (empty($value) || is_array($value)) {
+                continue;
+            }
+
+            $result[$key] = $value;
+        }
+
+        $result['messageClass'] = 'IPM.StickyNote';
+
+        return $as_array ? $result : new Syncroton_Model_Note($result);
+    }
+
+    /**
+     * convert note from xml to libkolab array
+     *
+     * @param Syncroton_Model_IEntry $data     Note to convert
+     * @param string                 $folderid Folder identifier
+     * @param array                  $entry    Existing entry
+     *
+     * @return array
+     */
+    public function toKolab(Syncroton_Model_IEntry $data, $folderid, $entry = null)
+    {
+        $note       = !empty($entry) ? $entry : array();
+        $foldername = isset($note['_mailbox']) ? $note['_mailbox'] : $this->getFolderName($folderid);
+        $config     = $this->getFolderConfig($foldername);
+
+        // Calendar namespace fields
+        foreach ($this->mapping as $key => $name) {
+            $value = $data->$key;
+
+            switch ($name) {
+            case 'description':
+                $value = $this->getBody($value, Syncroton_Model_EmailBody::TYPE_PLAINTEXT);
+                // If description isn't specified keep old description
+                if ($value === null) {
+                    continue 2;
+                }
+                break;
+            }
+
+            $this->setKolabDataItem($note, $name, $value);
+        }
+
+        return $note;
+    }
+}
diff --git a/lib/plugins/libkolab/lib/kolab_date_recurrence.php b/lib/plugins/libkolab/lib/kolab_date_recurrence.php
index 85ffd91..06dd331 100644
--- a/lib/plugins/libkolab/lib/kolab_date_recurrence.php
+++ b/lib/plugins/libkolab/lib/kolab_date_recurrence.php
@@ -101,7 +101,7 @@ class kolab_date_recurrence
     /**
      * Get the end date of the occurence of this recurrence cycle
      *
-     * @return mixed Timestamp with end date of the last event or False if recurrence exceeds limit
+     * @return DateTime|bool End datetime of the last event or False if recurrence exceeds limit
      */
     public function end()
     {
@@ -109,25 +109,25 @@ class kolab_date_recurrence
 
         // recurrence end date is given
         if ($event['recurrence']['UNTIL'] instanceof DateTime) {
-            return $event['recurrence']['UNTIL']->format('U');
+            return $event['recurrence']['UNTIL'];
         }
 
         // let libkolab do the work
         if ($this->engine && ($cend = $this->engine->getLastOccurrence()) && ($end_dt = kolab_format::php_datetime(new cDateTime($cend)))) {
-            return $end_dt->format('U');
+            return $end_dt;
         }
 
         // determine a reasonable end date if none given
-        if (!$event['recurrence']['COUNT']) {
+        if (!$event['recurrence']['COUNT'] && $event['end'] instanceof DateTime) {
           switch ($event['recurrence']['FREQ']) {
             case 'YEARLY':  $intvl = 'P100Y'; break;
             case 'MONTHLY': $intvl = 'P20Y';  break;
             default:        $intvl = 'P10Y';  break;
           }
 
-          $end_dt = clone $event['start'];
+          $end_dt = clone $event['end'];
           $end_dt->add(new DateInterval($intvl));
-          return $end_dt->format('U');
+          return $end_dt;
         }
 
         return false;
diff --git a/lib/plugins/libkolab/lib/kolab_format.php b/lib/plugins/libkolab/lib/kolab_format.php
index 679ae10..11a5c4f 100644
--- a/lib/plugins/libkolab/lib/kolab_format.php
+++ b/lib/plugins/libkolab/lib/kolab_format.php
@@ -45,7 +45,7 @@ abstract class kolab_format
     protected $version = '3.0';
 
     const KTYPE_PREFIX = 'application/x-vnd.kolab.';
-    const PRODUCT_ID = 'Roundcube-libkolab-0.9';
+    const PRODUCT_ID   = 'Roundcube-libkolab-0.9';
 
     /**
      * Factory method to instantiate a kolab_format object of the given type and version
@@ -242,7 +242,8 @@ abstract class kolab_format
                 break;
             case kolabformat::Warning:
                 $ret = false;
-                $log = "Warning";
+                $uid = is_object($this->obj) ? $this->obj->uid() : $this->data['uid'];
+                $log = "Warning @ $uid";
                 break;
             default:
                 $ret = true;
@@ -496,4 +497,50 @@ abstract class kolab_format
     {
         return array();
     }
+
+    protected function get_attachments(&$object)
+    {
+        // handle attachments
+        $vattach = $this->obj->attachments();
+        for ($i=0; $i < $vattach->size(); $i++) {
+            $attach = $vattach->get($i);
+
+            // skip cid: attachments which are mime message parts handled by kolab_storage_folder
+            if (substr($attach->uri(), 0, 4) != 'cid:' && $attach->label()) {
+                $name    = $attach->label();
+                $content = $attach->data();
+                $object['_attachments'][$name] = array(
+                    'name'     => $name,
+                    'mimetype' => $attach->mimetype(),
+                    'size'     => strlen($content),
+                    'content'  => $content,
+                );
+            }
+            else if (substr($attach->uri(), 0, 4) == 'http') {
+                $object['links'][] = $attach->uri();
+            }
+        }
+    }
+
+    protected function set_attachments($object)
+    {
+        // save attachments
+        $vattach = new vectorattachment;
+        foreach ((array) $object['_attachments'] as $cid => $attr) {
+            if (empty($attr))
+                continue;
+            $attach = new Attachment;
+            $attach->setLabel((string)$attr['name']);
+            $attach->setUri('cid:' . $cid, $attr['mimetype']);
+            $vattach->push($attach);
+        }
+
+        foreach ((array) $object['links'] as $link) {
+            $attach = new Attachment;
+            $attach->setUri($link, 'unknown');
+            $vattach->push($attach);
+        }
+
+        $this->obj->setAttachments($vattach);
+    }
 }
diff --git a/lib/plugins/libkolab/lib/kolab_format_contact.php b/lib/plugins/libkolab/lib/kolab_format_contact.php
index 0d0bc75..63efe9a 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])
@@ -407,6 +408,22 @@ class kolab_format_contact extends kolab_format
     }
 
     /**
+     * Callback for kolab_storage_cache to get object specific tags to cache
+     *
+     * @return array List of tags to save in cache
+     */
+    public function get_tags()
+    {
+        $tags = array();
+
+        if (!empty($this->data['birthday'])) {
+            $tags[] = 'x-has-birthday';
+        }
+
+        return $tags;
+    }
+
+    /**
      * Helper method to copy contents of an Address vector to the contact data object
      */
     private function read_addresses($addresses, &$object, $type = null)
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_note.php b/lib/plugins/libkolab/lib/kolab_format_note.php
index 04a8421..1f49dee 100644
--- a/lib/plugins/libkolab/lib/kolab_format_note.php
+++ b/lib/plugins/libkolab/lib/kolab_format_note.php
@@ -31,6 +31,11 @@ class kolab_format_note extends kolab_format
     protected $read_func = 'readNote';
     protected $write_func = 'writeNote';
 
+    protected $sensitivity_map = array(
+        'public'       => kolabformat::ClassPublic,
+        'private'      => kolabformat::ClassPrivate,
+        'confidential' => kolabformat::ClassConfidential,
+    );
 
     /**
      * Set properties to the kolabformat object
@@ -42,7 +47,12 @@ class kolab_format_note extends kolab_format
         // set common object properties
         parent::set($object);
 
-        // TODO: set object propeties
+        $this->obj->setSummary($object['title']);
+        $this->obj->setDescription($object['description']);
+        $this->obj->setClassification($this->sensitivity_map[$object['sensitivity']]);
+        $this->obj->setCategories(self::array2vector($object['categories']));
+
+        $this->set_attachments($object);
 
         // cache this data
         $this->data = $object;
@@ -73,10 +83,35 @@ class kolab_format_note extends kolab_format
         // read common object props into local data object
         $object = parent::to_array($data);
 
-        // TODO: read object properties
+        $sensitivity_map = array_flip($this->sensitivity_map);
 
-        $this->data = $object;
-        return $this->data;
+        // read object properties
+        $object += array(
+            'sensitivity' => $sensitivity_map[$this->obj->classification()],
+            'categories'  => self::vector2array($this->obj->categories()),
+            'title'       => $this->obj->summary(),
+            'description' => $this->obj->description(),
+        );
+
+        $this->get_attachments($object);
+
+        return $this->data = $object;
+    }
+
+    /**
+     * Callback for kolab_storage_cache to get object specific tags to cache
+     *
+     * @return array List of tags to save in cache
+     */
+    public function get_tags()
+    {
+        $tags = array();
+
+        foreach ((array) $this->data['categories'] as $cat) {
+            $tags[] = rcube_utils::normalize_string($cat);
+        }
+
+        return $tags;
     }
 
 }
diff --git a/lib/plugins/libkolab/lib/kolab_format_xcal.php b/lib/plugins/libkolab/lib/kolab_format_xcal.php
index 500dfa2..a2544f4 100644
--- a/lib/plugins/libkolab/lib/kolab_format_xcal.php
+++ b/lib/plugins/libkolab/lib/kolab_format_xcal.php
@@ -137,6 +137,16 @@ abstract class kolab_format_xcal extends kolab_format
             $attendee = $attvec->get($i);
             $cr = $attendee->contact();
             if ($cr->email() != $object['organizer']['email']) {
+                $delegators = $delegatees = array();
+                $vdelegators = $attendee->delegatedFrom();
+                for ($j=0; $j < $vdelegators->size(); $j++) {
+                    $delegators[] = $vdelegators->get($j)->email();
+                }
+                $vdelegatees = $attendee->delegatedTo();
+                for ($j=0; $j < $vdelegatees->size(); $j++) {
+                    $delegatees[] = $vdelegatees->get($j)->email();
+                }
+
                 $object['attendees'][] = array(
                     'role' => $role_map[$attendee->role()],
                     'cutype' => $cutype_map[$attendee->cutype()],
@@ -144,6 +154,8 @@ abstract class kolab_format_xcal extends kolab_format
                     'rsvp' => $attendee->rsvp(),
                     'email' => $cr->email(),
                     'name' => $cr->name(),
+                    'delegated-from' => $delegators,
+                    'delegated-to' => $delegatees,
                 );
             }
         }
@@ -191,6 +203,13 @@ 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);
@@ -218,26 +237,7 @@ abstract class kolab_format_xcal extends kolab_format
             }
         }
 
-        // handle attachments
-        $vattach = $this->obj->attachments();
-        for ($i=0; $i < $vattach->size(); $i++) {
-            $attach = $vattach->get($i);
-
-            // skip cid: attachments which are mime message parts handled by kolab_storage_folder
-            if (substr($attach->uri(), 0, 4) != 'cid:' && $attach->label()) {
-                $name    = $attach->label();
-                $content = $attach->data();
-                $object['_attachments'][$name] = array(
-                    'name'     => $name,
-                    'mimetype' => $attach->mimetype(),
-                    'size'     => strlen($content),
-                    'content'  => $content,
-                );
-            }
-            else if (substr($attach->uri(), 0, 4) == 'http') {
-                $object['links'][] = $attach->uri();
-            }
-        }
+        $this->get_attachments($object);
 
         return $object;
     }
@@ -287,6 +287,21 @@ abstract class kolab_format_xcal extends kolab_format
                 $att->setCutype($this->cutype_map[$attendee['cutype']] ? $this->cutype_map[$attendee['cutype']] : kolabformat::CutypeIndividual);
                 $att->setRSVP((bool)$attendee['rsvp']);
 
+                if (!empty($attendee['delegated-from'])) {
+                    $vdelegators = new vectorcontactref;
+                    foreach ((array)$attendee['delegated-from'] as $delegator) {
+                        $vdelegators->push(new ContactReference(ContactReference::EmailReference, $delegator));
+                    }
+                    $att->setDelegatedFrom($vdelegators);
+                }
+                if (!empty($attendee['delegated-to'])) {
+                    $vdelegatees = new vectorcontactref;
+                    foreach ((array)$attendee['delegated-to'] as $delegatee) {
+                        $vdelegatees->push(new ContactReference(ContactReference::EmailReference, $delegatee));
+                    }
+                    $att->setDelegatedTo($vdelegatees);
+                }
+
                 if ($att->isValid()) {
                     $attendees->push($att);
                 }
@@ -311,7 +326,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,6 +383,14 @@ 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']) {
@@ -401,24 +424,7 @@ abstract class kolab_format_xcal extends kolab_format
         }
         $this->obj->setAlarms($valarms);
 
-        // save attachments
-        $vattach = new vectorattachment;
-        foreach ((array)$object['_attachments'] as $cid => $attr) {
-            if (empty($attr))
-                continue;
-            $attach = new Attachment;
-            $attach->setLabel((string)$attr['name']);
-            $attach->setUri('cid:' . $cid, $attr['mimetype']);
-            $vattach->push($attach);
-        }
-
-        foreach ((array)$object['links'] as $link) {
-            $attach = new Attachment;
-            $attach->setUri($link, 'unknown');
-            $vattach->push($attach);
-        }
-
-        $this->obj->setAttachments($vattach);
+        $this->set_attachments($object);
     }
 
     /**
diff --git a/lib/plugins/libkolab/lib/kolab_storage_cache.php b/lib/plugins/libkolab/lib/kolab_storage_cache.php
index db174e5..9c1368f 100644
--- a/lib/plugins/libkolab/lib/kolab_storage_cache.php
+++ b/lib/plugins/libkolab/lib/kolab_storage_cache.php
@@ -24,12 +24,13 @@
 
 class kolab_storage_cache
 {
+    const DB_DATE_FORMAT = 'Y-m-d H:i:s';
+
     protected $db;
     protected $imap;
     protected $folder;
     protected $uid2msg;
     protected $objects;
-    protected $index = array();
     protected $metadata = array();
     protected $folder_id;
     protected $resource_uri;
@@ -43,6 +44,8 @@ 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;
 
 
     /**
@@ -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);
         }
     }
 
@@ -85,6 +90,24 @@ 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)
+    {
+        $folders_table = $this->db->table_name('kolab_folders');
+        $sql_arr = $this->db->fetch_assoc($this->db->query("SELECT * FROM $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
@@ -104,7 +127,7 @@ class kolab_storage_cache
         $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;
     }
 
@@ -137,64 +160,71 @@ class kolab_storage_cache
         // increase time limit
         @set_time_limit($this->max_sync_lock_time);
 
-        // read cached folder metadata
-        $this->_read_folder_data();
-
-        // check cache status hash first ($this->metadata is set in _read_folder_data())
-        if ($this->metadata['ctag'] != $this->folder->get_ctag()) {
-            // lock synchronization for this folder or wait if locked
-            $this->_sync_lock();
+        if (!$this->ready) {
+            // kolab cache is disabled, synchronize IMAP mailbox cache only
+            $this->imap->folder_sync($this->folder->name);
+        }
+        else {
+            // read cached folder metadata
+            $this->_read_folder_data();
 
-            // disable messages cache if configured to do so
-            $this->bypass(true);
+            // check cache status hash first ($this->metadata is set in _read_folder_data())
+            if ($this->metadata['ctag'] != $this->folder->get_ctag()) {
+                // lock synchronization for this folder or wait if locked
+                $this->_sync_lock();
 
-            // synchronize IMAP mailbox cache
-            $this->imap->folder_sync($this->folder->name);
+                // disable messages cache if configured to do so
+                $this->bypass(true);
 
-            // compare IMAP index with object cache index
-            $imap_index = $this->imap->index($this->folder->name, null, null, true, true);
-            $this->index = $imap_index->get();
+                // synchronize IMAP mailbox cache
+                $this->imap->folder_sync($this->folder->name);
 
-            // determine objects to fetch or to invalidate
-            if ($this->ready) {
-                // read cache index
-                $sql_result = $this->db->query(
-                    "SELECT msguid, uid FROM $this->cache_table WHERE folder_id=?",
-                    $this->folder_id
-                );
+                // compare IMAP index with object cache index
+                $imap_index = $this->imap->index($this->folder->name, null, null, true, true);
 
-                $old_index = array();
-                while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
-                    $old_index[] = $sql_arr['msguid'];
-                    $this->uid2msg[$sql_arr['uid']] = $sql_arr['msguid'];
-                }
+                // determine objects to fetch or to invalidate
+                if (!$imap_index->is_error()) {
+                    $imap_index = $imap_index->get();
 
-                // fetch new objects from imap
-                foreach (array_diff($this->index, $old_index) as $msguid) {
-                    if ($object = $this->folder->read_object($msguid, '*')) {
-                        $this->_extended_insert($msguid, $object);
-                    }
-                }
-                $this->_extended_insert(0, null);
-
-                // delete invalid entries from local DB
-                $del_index = array_diff($old_index, $this->index);
-                if (!empty($del_index)) {
-                    $quoted_ids = join(',', array_map(array($this->db, 'quote'), $del_index));
-                    $this->db->query(
-                        "DELETE FROM $this->cache_table WHERE folder_id=? AND msguid IN ($quoted_ids)",
+                    // read cache index
+                    $sql_result = $this->db->query(
+                        "SELECT msguid, uid FROM $this->cache_table WHERE folder_id=?",
                         $this->folder_id
                     );
-                }
 
-                // update ctag value (will be written to database in _sync_unlock())
-                $this->metadata['ctag'] = $this->folder->get_ctag();
-            }
+                    $old_index = array();
+                    while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
+                        $old_index[] = $sql_arr['msguid'];
+                        $this->uid2msg[$sql_arr['uid']] = $sql_arr['msguid'];
+                    }
 
-            $this->bypass(false);
+                    // fetch new objects from imap
+                    foreach (array_diff($imap_index, $old_index) as $msguid) {
+                        if ($object = $this->folder->read_object($msguid, '*')) {
+                            $this->_extended_insert($msguid, $object);
+                        }
+                    }
+                    $this->_extended_insert(0, null);
+
+                    // delete invalid entries from local DB
+                    $del_index = array_diff($old_index, $imap_index);
+                    if (!empty($del_index)) {
+                        $quoted_ids = join(',', array_map(array($this->db, 'quote'), $del_index));
+                        $this->db->query(
+                            "DELETE FROM $this->cache_table WHERE folder_id=? AND msguid IN ($quoted_ids)",
+                            $this->folder_id
+                        );
+                    }
+
+                    // update ctag value (will be written to database in _sync_unlock())
+                    $this->metadata['ctag'] = $this->folder->get_ctag();
+                }
 
-            // remove lock
-            $this->_sync_unlock();
+                $this->bypass(false);
+
+                // remove lock
+                $this->_sync_unlock();
+            }
         }
 
         $this->synched = time();
@@ -229,14 +259,14 @@ 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)
             }
         }
 
@@ -272,7 +302,7 @@ class kolab_storage_cache
 
         if ($object) {
             // insert new object data...
-            $this->insert($msguid, $object);
+            $this->save($msguid, $object);
         }
         else {
             // ...or set in-memory cache to false
@@ -282,41 +312,48 @@ class kolab_storage_cache
 
 
     /**
-     * Insert a cache entry
+     * Insert (or update) a cache entry
      *
-     * @param string Related IMAP message UID
+     * @param int    Related IMAP message UID
      * @param mixed  Hash array with object properties to save or false to delete the cache entry
+     * @param int    Optional old message UID (for update)
      */
-    public function insert($msguid, $object)
+    public function save($msguid, $object, $olduid = null)
     {
         // write to cache
         if ($this->ready) {
             $this->_read_folder_data();
 
             $sql_data = $this->_serialize($object);
+            $sql_data['folder_id'] = $this->folder_id;
+            $sql_data['msguid']    = $msguid;
+            $sql_data['uid']       = $object['uid'];
 
-            $extra_cols   = $this->extra_cols ? ', ' . join(', ', $this->extra_cols) : '';
-            $extra_fields = $this->extra_cols ? str_repeat(', ?', count($this->extra_cols)) : '';
+            $args = array();
+            $cols = array('folder_id', 'msguid', 'uid', 'changed', 'data', 'xml', 'tags', 'words');
+            $cols = array_merge($cols, $this->extra_cols);
 
-            $args = array(
-                "INSERT INTO $this->cache_table ".
-                " (folder_id, msguid, uid, created, changed, data, xml, tags, words $extra_cols)".
-                " VALUES (?, ?, ?, " . $this->db->now() . ", ?, ?, ?, ?, ? $extra_fields)",
-                $this->folder_id,
-                $msguid,
-                $object['uid'],
-                $sql_data['changed'],
-                $sql_data['data'],
-                $sql_data['xml'],
-                $sql_data['tags'],
-                $sql_data['words'],
-            );
+            foreach ($cols as $idx => $col) {
+                $cols[$idx] = $this->db->quote_identifier($col);
+                $args[]     = $sql_data[$col];
+            }
 
-            foreach ($this->extra_cols as $col) {
-                $args[] = $sql_data[$col];
+            if ($olduid) {
+                foreach ($cols as $idx => $col) {
+                    $cols[$idx] = "$col = ?";
+                }
+
+                $query = "UPDATE $this->cache_table SET " . implode(', ', $cols)
+                    . " WHERE folder_id = ? AND msguid = ?";
+                $args[] = $this->folder_id;
+                $args[] = $olduid;
+            }
+            else {
+                $query = "INSERT INTO $this->cache_table (created, " . implode(', ', $cols)
+                    . ") VALUES (" . $this->db->now() . str_repeat(', ?', count($cols)) . ")";
             }
 
-            $result = call_user_func_array(array($this->db, 'query'), $args);
+            $result = $this->db->query($query, $args);
 
             if (!$this->db->affected_rows($result)) {
                 rcube::raise_error(array(
@@ -341,22 +378,27 @@ class kolab_storage_cache
      */
     public function move($msguid, $uid, $target_folder)
     {
-        $target = kolab_storage::get_folder($target_folder);
+        if ($this->ready) {
+            $target = kolab_storage::get_folder($target_folder);
 
-        // resolve new message UID in target folder
-        if ($new_msguid = $target->cache->uid2msguid($uid)) {
-            $this->_read_folder_data();
+            // resolve new message UID in target folder
+            if ($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->cache->get_folder_id(),
-                $new_msguid,
-                $this->folder_id,
-                $msguid
-            );
+                $this->db->query(
+                    "UPDATE $this->cache_table SET folder_id=?, msguid=? ".
+                    "WHERE folder_id=? AND msguid=?",
+                    $target->cache->get_folder_id(),
+                    $new_msguid,
+                    $this->folder_id,
+                    $msguid
+                );
+
+                $result = $this->db->affected_rows();
+            }
         }
-        else {
+
+        if (empty($result)) {
             // just clear cache entry
             $this->set($msguid, false);
         }
@@ -370,12 +412,17 @@ class kolab_storage_cache
      */
     public function purge($type = null)
     {
+        if (!$this->ready) {
+            return true;
+        }
+
         $this->_read_folder_data();
 
         $result = $this->db->query(
             "DELETE FROM $this->cache_table WHERE folder_id=?",
             $this->folder_id
         );
+
         return $this->db->affected_rows($result);
     }
 
@@ -386,6 +433,10 @@ class kolab_storage_cache
      */
     public function rename($new_folder)
     {
+        if (!$this->ready) {
+            return;
+        }
+
         $target = kolab_storage::get_folder($new_folder);
 
         // resolve new message UID in target folder
@@ -407,51 +458,67 @@ 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 {
+                    // only add msguid to dataset index
+                    $result[] = $sql_arr;
+                }
             }
         }
+        // use IMAP
         else {
-            // extract object type from query parameter
             $filter = $this->_query2assoc($query);
 
-            // use 'list' for folder's default objects
-            if ($filter['type'] == $this->type) {
-                $index = $this->index;
-            }
-            else {  // search by object type
+            if ($filter['type']) {
                 $search = 'UNDELETED HEADER X-Kolab-Type ' . kolab_format::KTYPE_PREFIX . $filter['type'];
-                $index  = $this->imap->search_once($this->folder->name, $search)->get();
+                $index  = $this->imap->search_once($this->folder->name, $search);
+            }
+            else {
+                $index = $this->imap->index($this->folder->name, null, null, true, true);
             }
 
             if ($index->is_error()) {
-                return null;
+                if ($uids) {
+                    return null;
+                }
+                $result->set_error(true);
+                return $result;
             }
 
-            // fetch all messages in $index from IMAP
-            $result = $uids ? $this->_fetch_uids($index, $filter['type']) : $this->_fetch($index, $filter['type']);
+            $index  = $index->get();
+            $result = $uids ? $index : $this->_fetch($index, $filter['type']);
 
             // TODO: post-filter result according to query
         }
@@ -461,7 +528,7 @@ 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]);
             }
         }
 
@@ -477,8 +544,8 @@ class kolab_storage_cache
      */
     public function count($query = array())
     {
-        // cache is in sync, we can count records in local DB
-        if ($this->synched) {
+        // read from local cache DB (assume it to be synchronized)
+        if ($this->ready) {
             $this->_read_folder_data();
 
             $sql_result = $this->db->query(
@@ -494,22 +561,50 @@ class kolab_storage_cache
             $sql_arr = $this->db->fetch_assoc($sql_result);
             $count   = intval($sql_arr['numrows']);
         }
+        // use IMAP
         else {
-            // search IMAP by object type
             $filter = $this->_query2assoc($query);
-            $ctype  = kolab_format::KTYPE_PREFIX . $filter['type'];
-            $index  = $this->imap->search_once($this->folder->name, 'UNDELETED HEADER X-Kolab-Type ' . $ctype);
+
+            if ($filter['type']) {
+                $search = 'UNDELETED HEADER X-Kolab-Type ' . kolab_format::KTYPE_PREFIX . $filter['type'];
+                $index  = $this->imap->search_once($this->folder->name, $search);
+            }
+            else {
+                $index = $this->imap->index($this->folder->name, null, null, true, true);
+            }
 
             if ($index->is_error()) {
                 return null;
             }
 
+            // TODO: post-filter result according to query
+
             $count = $index->count();
         }
 
         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
@@ -580,7 +675,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;
@@ -591,43 +686,6 @@ class kolab_storage_cache
         return $results;
     }
 
-
-    /**
-     * Fetch object UIDs (aka message subjects) from IMAP
-     *
-     * @param array List of message UIDs to fetch
-     * @param string Requested object type or * for all
-     * @param string IMAP folder to read from
-     * @return array List of parsed Kolab objects
-     */
-    protected function _fetch_uids($index, $type = null)
-    {
-        if (!$type)
-            $type = $this->folder->type;
-
-        $this->bypass(true);
-
-        $results = array();
-        $headers = $this->imap->fetch_headers($this->folder->name, $index, false);
-
-        $this->bypass(false);
-
-        foreach ((array)$headers as $msguid => $headers) {
-            $object_type = kolab_format::mime2object_type($headers->others['x-kolab-type']);
-
-            // check object type header and abort on mismatch
-            if ($type != '*' && $object_type != $type)
-                return false;
-
-            $uid = $headers->subject;
-            $this->uid2msg[$uid] = $msguid;
-            $results[] = $uid;
-        }
-
-        return $results;
-    }
-
-
     /**
      * Helper method to convert the given Kolab object into a dataset to be written to cache
      */
@@ -783,7 +841,7 @@ class kolab_storage_cache
     protected function _read_folder_data()
     {
         // already done
-        if (!empty($this->folder_id))
+        if (!empty($this->folder_id) || !$this->ready)
             return;
 
         $sql_arr = $this->db->fetch_assoc($this->db->query("SELECT folder_id, synclock, ctag FROM $this->folders_table WHERE resource=?", $this->resource_uri));
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_cache_event.php b/lib/plugins/libkolab/lib/kolab_storage_cache_event.php
index 876c3b4..5fc44cd 100644
--- a/lib/plugins/libkolab/lib/kolab_storage_cache_event.php
+++ b/lib/plugins/libkolab/lib/kolab_storage_cache_event.php
@@ -34,14 +34,14 @@ class kolab_storage_cache_event extends kolab_storage_cache
     {
         $sql_data = parent::_serialize($object);
 
-        // database runs in server's timezone so using date() is what we want
-        $sql_data['dtstart'] = date('Y-m-d H:i:s', is_object($object['start']) ? $object['start']->format('U') : $object['start']);
-        $sql_data['dtend']   = date('Y-m-d H:i:s', is_object($object['end'])   ? $object['end']->format('U')   : $object['end']);
+        $sql_data['dtstart'] = is_object($object['start']) ? $object['start']->format(self::DB_DATE_FORMAT) : date(self::DB_DATE_FORMAT, $object['start']);
+        $sql_data['dtend']   = is_object($object['end'])   ? $object['end']->format(self::DB_DATE_FORMAT)   : date(self::DB_DATE_FORMAT, $object['end']);
 
         // extend date range for recurring events
         if ($object['recurrence'] && $object['_formatobj']) {
             $recurrence = new kolab_date_recurrence($object['_formatobj']);
-            $sql_data['dtend'] = date('Y-m-d 23:59:59', $recurrence->end() ?: strtotime('now +10 years'));
+            $dtend = $recurrence->end() ?: new DateTime('now +10 years');
+            $sql_data['dtend'] = $dtend->format(self::DB_DATE_FORMAT);
         }
 
         return $sql_data;
diff --git a/lib/plugins/libkolab/lib/kolab_storage_cache_task.php b/lib/plugins/libkolab/lib/kolab_storage_cache_task.php
index a1953f6..7bf5c79 100644
--- a/lib/plugins/libkolab/lib/kolab_storage_cache_task.php
+++ b/lib/plugins/libkolab/lib/kolab_storage_cache_task.php
@@ -35,9 +35,9 @@ class kolab_storage_cache_task extends kolab_storage_cache
         $sql_data = parent::_serialize($object) + array('dtstart' => null, 'dtend' => null);
 
         if ($object['start'])
-            $sql_data['dtstart'] = date('Y-m-d H:i:s', is_object($object['start']) ? $object['start']->format('U') : $object['start']);
+            $sql_data['dtstart'] = is_object($object['start']) ? $object['start']->format(self::DB_DATE_FORMAT) : date(self::DB_DATE_FORMAT, $object['start']);
         if ($object['due'])
-            $sql_data['dtend']   = date('Y-m-d H:i:s', is_object($object['due'])   ? $object['due']->format('U')   : $object['due']);
+            $sql_data['dtend']   = is_object($object['due'])   ? $object['due']->format(self::DB_DATE_FORMAT)   : date(self::DB_DATE_FORMAT, $object['due']);
 
         return $sql_data;
     }
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 12da5e9..1580314 100644
--- a/lib/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/lib/plugins/libkolab/lib/kolab_storage_folder.php
@@ -74,9 +74,9 @@ 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)
     {
-        $this->type_annotation = $ftype ? $ftype : kolab_storage::folder_type($name);
+        $this->type_annotation = $type ? $type : kolab_storage::folder_type($name);
 
         $oldtype = $this->type;
         list($this->type, $suffix) = explode('.', $this->type_annotation);
@@ -92,7 +92,6 @@ class kolab_storage_folder
         $this->cache->set_folder($this);
     }
 
-
     /**
      *
      */
@@ -424,6 +423,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
@@ -753,24 +767,27 @@ class kolab_storage_folder
 
             $result = $this->imap->save_message($this->name, $raw_msg, null, false, null, null, $binary);
 
-            // delete old message
-            if ($result && !empty($object['_msguid']) && !empty($object['_mailbox'])) {
-                $this->cache->bypass(true);
-                $this->imap->delete_message($object['_msguid'], $object['_mailbox']);
-                $this->cache->bypass(false);
-                $this->cache->set($object['_msguid'], false, $object['_mailbox']);
-            }
-
             // update cache with new UID
             if ($result) {
+                $old_uid = $object['_msguid'];
+
                 $object['_msguid'] = $result;
                 $object['_mailbox'] = $this->name;
-                $this->cache->insert($result, $object);
 
-                // remove temp file
-                if ($body_file) {
-                    @unlink($body_file);
+                if ($old_uid) {
+                    // delete old message
+                    $this->cache->bypass(true);
+                    $this->imap->delete_message($old_uid, $object['_mailbox']);
+                    $this->cache->bypass(false);
                 }
+
+                // insert/update message in cache
+                $this->cache->save($result, $object, $old_uid);
+            }
+
+            // remove temp file
+            if ($body_file) {
+                @unlink($body_file);
             }
         }
 
@@ -806,7 +823,7 @@ class kolab_storage_folder
                         $recurrence = new kolab_date_recurrence($object['_formatobj']);
                         if ($end = $recurrence->end()) {
                             unset($exception['recurrence']['COUNT']);
-                            $exception['recurrence']['UNTIL'] = new DateTime('@'.$end);
+                            $exception['recurrence']['UNTIL'] = $end;
                         }
                     }
 




More information about the commits mailing list