2 commits - config/main.inc.php.dist docs/SQL docs/syncroton.sql lib/ext lib/kolab_sync_data_calendar.php lib/kolab_sync_data_gal.php lib/kolab_sync.php

Aleksander Machniak machniak at kolabsys.com
Sat Jan 12 16:38:10 CET 2013


 config/main.inc.php.dist                      |    8 +
 docs/SQL/mysql/2013011200.sql                 |   11 +
 docs/syncroton.sql                            |   13 +
 lib/ext/Syncroton/Command/FolderCreate.php    |   11 -
 lib/ext/Syncroton/Command/FolderSync.php      |    6 
 lib/ext/Syncroton/Command/GetItemEstimate.php |    4 
 lib/ext/Syncroton/Command/ICommand.php        |    7 
 lib/ext/Syncroton/Command/ItemOperations.php  |   29 +++
 lib/ext/Syncroton/Command/MeetingResponse.php |   87 +++++++++++
 lib/ext/Syncroton/Command/Options.php         |   10 -
 lib/ext/Syncroton/Command/Ping.php            |   28 +--
 lib/ext/Syncroton/Command/SendMail.php        |    6 
 lib/ext/Syncroton/Command/Sync.php            |  177 ++++++++++++++++++-----
 lib/ext/Syncroton/Command/Wbxml.php           |   35 ++++
 lib/ext/Syncroton/Data/AData.php              |  109 +++++++++-----
 lib/ext/Syncroton/Data/Calendar.php           |   37 +---
 lib/ext/Syncroton/Data/Contacts.php           |  137 +-----------------
 lib/ext/Syncroton/Data/Email.php              |   76 +---------
 lib/ext/Syncroton/Data/IDataCalendar.php      |   26 +++
 lib/ext/Syncroton/Data/IDataEmail.php         |    6 
 lib/ext/Syncroton/Data/Tasks.php              |   27 ---
 lib/ext/Syncroton/Model/AEntry.php            |    4 
 lib/ext/Syncroton/Model/Email.php             |   19 +-
 lib/ext/Syncroton/Model/FileReference.php     |    1 
 lib/ext/Syncroton/Model/Folder.php            |    1 
 lib/ext/Syncroton/Model/IDevice.php           |    1 
 lib/ext/Syncroton/Model/IFolder.php           |    1 
 lib/ext/Syncroton/Model/MeetingResponse.php   |   46 ++++++
 lib/ext/Syncroton/Model/SyncCollection.php    |  194 +++++++++++++++-----------
 lib/ext/Syncroton/Registry.php                |   34 ++++
 lib/ext/Syncroton/Server.php                  |  131 +++++++++++------
 lib/ext/Syncroton/Wbxml/Abstract.php          |    6 
 lib/kolab_sync.php                            |    8 -
 lib/kolab_sync_data_calendar.php              |   15 +-
 lib/kolab_sync_data_gal.php                   |    4 
 35 files changed, 825 insertions(+), 490 deletions(-)

New commits:
commit 0dffe2b42b0fe77a756ad96b3bccc4a4b2953d5e
Author: Aleksander Machniak <alec at alec.pl>
Date:   Sat Jan 12 16:37:25 2013 +0100

    Update Syncroton, added activesync_ping_timeout and activesync_quiet_time options

diff --git a/config/main.inc.php.dist b/config/main.inc.php.dist
index 77f04ac..56f5808 100644
--- a/config/main.inc.php.dist
+++ b/config/main.inc.php.dist
@@ -42,3 +42,11 @@ $rcmail_config['activesync_gal_fieldmap'] = null;
 // List of Roundcube plugins
 // WARNING: Not all plugins used in Roundcube can be listed here
 $rcmail_config['activesync_plugins'] = array();
+
+// Defines for how many seconds we'll sleep between every
+// action for detecting changes in folders. Default: 60
+$rcmail_config['activesync_ping_timeout'] = 60;
+
+// We start detecting changes n seconds since the last sync of a folder
+// Default: 180
+$rcmail_config['activesync_quiet_time'] = 180;
diff --git a/docs/SQL/mysql/2013011200.sql b/docs/SQL/mysql/2013011200.sql
new file mode 100644
index 0000000..bcf8453
--- /dev/null
+++ b/docs/SQL/mysql/2013011200.sql
@@ -0,0 +1,11 @@
+ALTER TABLE `syncroton_device` ADD `lastsynccollection` longblob DEFAULT NULL;
+ALTER TABLE `syncroton_folder` ADD `supportedfields` longblob DEFAULT NULL;
+ALTER TABLE `syncroton_data` CHANGE `type` `class` varchar(40) NOT NULL;
+CREATE TABLE `syncroton_data_folder` (
+    `id` varchar(40) NOT NULL,
+    `type` int(11) NOT NULL,
+    `name` varchar(255) NOT NULL,
+    `owner_id` varchar(40) NOT NULL,
+    `parent_id` varchar(40) DEFAULT NULL,
+    PRIMARY KEY (`id`)
+) ENGINE=InnoDB;
diff --git a/docs/syncroton.sql b/docs/syncroton.sql
index 5e140d9..b35a6b4 100644
--- a/docs/syncroton.sql
+++ b/docs/syncroton.sql
@@ -66,6 +66,7 @@ CREATE TABLE IF NOT EXISTS `syncroton_device` (
     `pinglifetime` int(11) DEFAULT NULL,
     `remotewipe` int(11) DEFAULT '0',
     `pingfolder` longblob,
+    `lastsynccollection` longblob DEFAULT NULL,
     `contactsfilter_id` varchar(40) DEFAULT NULL,
     `calendarfilter_id` varchar(40) DEFAULT NULL,
     `tasksfilter_id` varchar(40) DEFAULT NULL,
@@ -84,6 +85,7 @@ CREATE TABLE IF NOT EXISTS `syncroton_folder` (
     `type` int(11) NOT NULL,
     `creation_time` datetime NOT NULL,
     `lastfiltertype` int(11) DEFAULT NULL,
+    `supportedfields` longblob DEFAULT NULL,
     PRIMARY KEY (`id`),
     UNIQUE KEY `device_id--class--folderid` (`device_id`(40),`class`(40),`folderid`(40)),
     KEY `folderstates::device_id--devices::id` (`device_id`),
@@ -118,8 +120,17 @@ CREATE TABLE IF NOT EXISTS `syncroton_content` (
 
 CREATE TABLE IF NOT EXISTS `syncroton_data` (
     `id` varchar(40) NOT NULL,
-    `type` varchar(40) NOT NULL,
+    `class` varchar(40) NOT NULL,
     `folder_id` varchar(40) NOT NULL,
     `data` longblob,
     PRIMARY KEY (`id`)
 ) ENGINE=InnoDB;
+
+CREATE TABLE IF NOT EXISTS `syncroton_data_folder` (
+    `id` varchar(40) NOT NULL,
+    `type` int(11) NOT NULL,
+    `name` varchar(255) NOT NULL,
+    `owner_id` varchar(40) NOT NULL,
+    `parent_id` varchar(40) DEFAULT NULL,
+    PRIMARY KEY (`id`)
+) ENGINE=InnoDB;
diff --git a/lib/ext/Syncroton/Command/FolderCreate.php b/lib/ext/Syncroton/Command/FolderCreate.php
index d87cd9d..e7eda47 100644
--- a/lib/ext/Syncroton/Command/FolderCreate.php
+++ b/lib/ext/Syncroton/Command/FolderCreate.php
@@ -70,12 +70,13 @@ class Syncroton_Command_FolderCreate extends Syncroton_Command_Wbxml
                 break;
         }
         
-        $folder->deviceId     = $this->_device;
-        $folder->creationTime = $this->_syncTimeStamp;
-        
-        $dataController = Syncroton_Data_Factory::factory($folder->class, $this->_device, $this->_syncTimeStamp);
         
-        $this->_folder = $dataController->createFolder($folder);
+        $dataController = Syncroton_Data_Factory::factory($folder->class, $this->_device, $this->_syncTimeStamp);
+        
+        $this->_folder = $dataController->createFolder($folder);
+        $this->_folder->class        = $folder->class;
+        $this->_folder->deviceId     = $this->_device;
+        $this->_folder->creationTime = $this->_syncTimeStamp;
         
         $this->_folderBackend->create($this->_folder);
     }
diff --git a/lib/ext/Syncroton/Command/FolderSync.php b/lib/ext/Syncroton/Command/FolderSync.php
index 8c4917d..84e761e 100644
--- a/lib/ext/Syncroton/Command/FolderSync.php
+++ b/lib/ext/Syncroton/Command/FolderSync.php
@@ -115,6 +115,12 @@ class Syncroton_Command_FolderSync extends Syncroton_Command_Wbxml
             return $this->_outputDom;
         }
 
+        // send headers from options command also when FolderSync SyncKey is 0
+        if ($this->_syncState->counter == 0) {
+            $optionsCommand = new Syncroton_Command_Options();
+            $this->_headers = array_merge($this->_headers, $optionsCommand->getHeaders());
+        }
+        
         $adds = array();
         $deletes = array();
 
diff --git a/lib/ext/Syncroton/Command/GetItemEstimate.php b/lib/ext/Syncroton/Command/GetItemEstimate.php
index 79ed73d..5eb0f4c 100644
--- a/lib/ext/Syncroton/Command/GetItemEstimate.php
+++ b/lib/ext/Syncroton/Command/GetItemEstimate.php
@@ -100,7 +100,6 @@ class Syncroton_Command_GetItemEstimate extends Syncroton_Command_Wbxml
                 
                 $response->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Status', self::STATUS_INVALID_COLLECTION));
                 $collection = $response->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Collection'));
-                $collection->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Class', $collectionData['class']));
                 $collection->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'CollectionId', $collectionData['collectionId']));
                 $collection->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Estimate', 0));
                 
@@ -114,14 +113,12 @@ class Syncroton_Command_GetItemEstimate extends Syncroton_Command_Wbxml
                  * 
                     $response->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Status', self::STATUS_INVALID_SYNC_KEY));
                     $collection = $response->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Collection'));
-                    $collection->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Class', $collectionData['class']));
                     $collection->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'CollectionId', $collectionData['collectionId']));  
                     $collection->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Estimate', 0));
                  */
                                                               
                 $response->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Status', self::STATUS_SUCCESS));
                 $collection = $response->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Collection'));
-                $collection->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Class', $collectionData['class']));
                 $collection->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'CollectionId', $collectionData['collectionId']));  
                 $collection->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Estimate', 1));                                              
             } else {
@@ -129,7 +126,6 @@ class Syncroton_Command_GetItemEstimate extends Syncroton_Command_Wbxml
                 
                 $response->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Status', self::STATUS_SUCCESS));
                 $collection = $response->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Collection'));
-                $collection->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'Class', $collectionData['class']));
                 $collection->appendChild($this->_outputDom->createElementNS('uri:ItemEstimate', 'CollectionId', $collectionData['collectionId']));
                 if($collectionData['syncState']->counter === 0) {
                     // this is the first sync. in most cases there are data on the server.
diff --git a/lib/ext/Syncroton/Command/ICommand.php b/lib/ext/Syncroton/Command/ICommand.php
index fd286d4..d6edb23 100644
--- a/lib/ext/Syncroton/Command/ICommand.php
+++ b/lib/ext/Syncroton/Command/ICommand.php
@@ -35,4 +35,11 @@ interface Syncroton_Command_ICommand
      * create the response
      */
     public function getResponse();
+    
+    /**
+     * return headers of command
+     * 
+     * @return array list of headers
+     */
+    public function getHeaders();
 }
diff --git a/lib/ext/Syncroton/Command/ItemOperations.php b/lib/ext/Syncroton/Command/ItemOperations.php
index a89bd5f..c3ee287 100644
--- a/lib/ext/Syncroton/Command/ItemOperations.php
+++ b/lib/ext/Syncroton/Command/ItemOperations.php
@@ -105,6 +105,8 @@ class Syncroton_Command_ItemOperations extends Syncroton_Command_Wbxml
     
     /**
      * generate ItemOperations response
+     * 
+     * @todo add multipart support to all types of fetches
      */
     public function getResponse()
     {
@@ -151,9 +153,30 @@ class Syncroton_Command_ItemOperations extends Syncroton_Command_Wbxml
                     $fetchTag->appendChild($this->_outputDom->createElementNS('uri:AirSyncBase', 'FileReference', $fetch['fileReference']));
 
                     $properties = $this->_outputDom->createElementNS('uri:ItemOperations', 'Properties');
-                    $dataController
-                        ->getFileReference($fetch['fileReference'])
-                        ->appendXML($properties, $this->_device);
+                    
+                    $fileReference = $dataController->getFileReference($fetch['fileReference']);
+                    
+                    // unset data field and move content to stream
+                    if ($this->_requestParameters['acceptMultipart'] == true) {
+                        $this->_headers['Content-Type'] = 'application/vnd.ms-sync.multipart';
+                        
+                        $partStream = fopen("php://temp", 'r+');
+                        
+                        if (is_resource($fileReference->data)) {
+                            stream_copy_to_stream($fileReference->data, $partStream);
+                        } else {
+                            fwrite($partStream, $fileReference->data);
+                        }
+                        
+                        unset($fileReference->data);
+                        
+                        $this->_parts[] = $partStream;
+                        
+                        $fileReference->part = count($this->_parts);
+                    }
+                    
+                    $fileReference->appendXML($properties, $this->_device);
+                    
                     $fetchTag->appendChild($properties);
                 }
             } catch (Syncroton_Exception_NotFound $e) {
diff --git a/lib/ext/Syncroton/Command/MeetingResponse.php b/lib/ext/Syncroton/Command/MeetingResponse.php
new file mode 100644
index 0000000..47eb12c
--- /dev/null
+++ b/lib/ext/Syncroton/Command/MeetingResponse.php
@@ -0,0 +1,87 @@
+<?php
+/**
+ * Syncroton
+ *
+ * @package     Syncroton
+ * @subpackage  Command
+ * @license     http://www.tine20.org/licenses/lgpl.html LGPL Version 3
+ * @copyright   Copyright (c) 2012-2012 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Lars Kneschke <l.kneschke at metaways.de>
+ */
+
+/**
+ * class to handle ActiveSync MeetingResponse command
+ *
+ * @package     Syncroton
+ * @subpackage  Command
+ */
+class Syncroton_Command_MeetingResponse extends Syncroton_Command_Wbxml
+{
+    protected $_results = array();
+    
+    protected $_defaultNameSpace = 'uri:MeetingResponse';
+    protected $_documentElement  = 'MeetingResponse';
+
+    /**
+     * parse MeetingResponse request
+     */
+    public function handle()
+    {
+        $dataController = Syncroton_Data_Factory::factory(Syncroton_Data_Factory::CLASS_CALENDAR, $this->_device, $this->_syncTimeStamp);
+        
+        $xml = simplexml_import_dom($this->_requestBody);
+
+        foreach ($xml as $meetingResponse) {
+            $request = new Syncroton_Model_MeetingResponse($meetingResponse);
+            
+            try {
+                $calendarId = $dataController->setAttendeeStatus($request);
+                
+                $this->_results[] = array(
+                    'calendarId' => $calendarId,
+                    'request'    => $request,
+                    'status'     => 1
+                );
+                
+            } catch (Syncroton_Exception_Status_MeetingResponse $sesmr) {
+                $this->_results[] = array(
+                    'request' => $request,
+                    'status'  => $sesmr->getCode()
+                );
+            }
+        }
+
+        if ($this->_logger instanceof Zend_Log)
+            $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " results: " . print_r($this->_results, true));
+    }
+
+    /**
+     * generate MeetingResponse response
+     */
+    public function getResponse()
+    {
+        $this->_outputDom->documentElement->setAttributeNS('http://www.w3.org/2000/xmlns/', 'xmlns:Search', 'uri:Search');
+        
+        $meetingResponse = $this->_outputDom->documentElement;
+
+        foreach ($this->_results as $result) {
+            $resultElement = $this->_outputDom->createElementNS('uri:MeetingResponse', 'Result');
+            
+            if (isset($result['request']->requestId)) {
+                $resultElement->appendChild($this->_outputDom->createElementNS('uri:MeetingResponse', 'RequestId', $result['request']->requestId));
+            } elseif (isset($result['request']->longId)) {
+                $resultElement->appendChild($this->_outputDom->createElementNS('uri:Search', 'LongId', $result['request']->longId));
+            }
+            
+            $resultElement->appendChild($this->_outputDom->createElementNS('uri:MeetingResponse', 'Status', $result['status']));
+            
+            if (isset($result['calendarId'])) {
+                $resultElement->appendChild($this->_outputDom->createElementNS('uri:MeetingResponse', 'CalendarId', $result['calendarId']));
+            }
+            
+            $meetingResponse->appendChild($resultElement);
+        } 
+        
+        return $this->_outputDom;
+    }
+}
diff --git a/lib/ext/Syncroton/Command/Options.php b/lib/ext/Syncroton/Command/Options.php
index 252899c..f126cc5 100644
--- a/lib/ext/Syncroton/Command/Options.php
+++ b/lib/ext/Syncroton/Command/Options.php
@@ -22,11 +22,13 @@ class Syncroton_Command_Options
      * 
      * @return void
      */
-    public function getResponse()
+    public function getHeaders()
     {
         // same header like Exchange 2xxx???
-        header('MS-Server-ActiveSync:  14.00.0536.000');
-        header("MS-ASProtocolVersions: 2.5,12.0,12.1,14.0,14.1");
-        header("MS-ASProtocolCommands: CreateCollection,DeleteCollection,FolderCreate,FolderDelete,FolderSync,FolderUpdate,GetAttachment,GetHierarchy,GetItemEstimate,ItemOperations,MeetingResponse,MoveCollection,MoveItems,Provision,ResolveRecipients,Ping,SendMail,Search,Settings,SmartForward,SmartReply,Sync,ValidateCert");
+        return array(
+            'MS-Server-ActiveSync'  => '14.00.0536.000',
+            'MS-ASProtocolVersions' => '2.5,12.0,12.1,14.0,14.1',
+            'MS-ASProtocolCommands' => 'CreateCollection,DeleteCollection,FolderCreate,FolderDelete,FolderSync,FolderUpdate,GetAttachment,GetHierarchy,GetItemEstimate,ItemOperations,MeetingResponse,MoveCollection,MoveItems,Provision,ResolveRecipients,Ping,SendMail,Search,Settings,SmartForward,SmartReply,Sync,ValidateCert'
+        );
     }    
 }
diff --git a/lib/ext/Syncroton/Command/Ping.php b/lib/ext/Syncroton/Command/Ping.php
index 3270d53..1bb7da8 100644
--- a/lib/ext/Syncroton/Command/Ping.php
+++ b/lib/ext/Syncroton/Command/Ping.php
@@ -31,18 +31,6 @@ class Syncroton_Command_Ping extends Syncroton_Command_Wbxml
     protected $_changesDetected = false;
     
     /**
-     * 
-     * @var int
-     */
-    public static $pingTimeout = 60;
-    
-    /**
-     * 
-     * @var int
-     */
-    public static $quietTime = 180;
-    
-    /**
      * @var Syncroton_Backend_StandAlone_Abstract
      */
     protected $_dataBackend;
@@ -108,7 +96,8 @@ class Syncroton_Command_Ping extends Syncroton_Command_Wbxml
             $folderWithChanges = array();
             
             do {
-                sleep(self::$pingTimeout);
+                // take a break to save battery lifetime
+                sleep(Syncroton_Registry::getPingTimeout());
                 
                 $now = new DateTime('now', new DateTimeZone('utc'));
                 
@@ -136,8 +125,8 @@ class Syncroton_Command_Ping extends Syncroton_Command_Wbxml
                             continue;
                         }
                         
-                        // safe battery time by skipping folders which got synchronied less than self::$quietTime seconds ago
-                        if (($now->getTimestamp() - $syncState->lastsync->getTimestamp()) < self::$quietTime) {
+                        // safe battery time by skipping folders which got synchronied less than Syncroton_Registry::getQuietTime() seconds ago
+                        if (($now->getTimestamp() - $syncState->lastsync->getTimestamp()) < Syncroton_Registry::getQuietTime()) {
                             continue;
                         }
                         
@@ -167,8 +156,13 @@ class Syncroton_Command_Ping extends Syncroton_Command_Wbxml
                 
                 if ($this->_logger instanceof Zend_Log)
                     $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " DeviceId: " . $this->_device->deviceid . " seconds left: " . $secondsLeft);
-                
-            } while ($secondsLeft > 0);
+            
+            // See: http://www.tine20.org/forum/viewtopic.php?f=12&t=12146
+            //
+            // break if there are less than PingTimeout + 10 seconds left for the next loop
+            // otherwise the response will be returned after the client has finished his Ping
+            // request already maybe
+            } while ($secondsLeft > (Syncroton_Registry::getPingTimeout() + 10));
         }
         
         if ($this->_logger instanceof Zend_Log) 
diff --git a/lib/ext/Syncroton/Command/SendMail.php b/lib/ext/Syncroton/Command/SendMail.php
index c25f813..7fff25e 100644
--- a/lib/ext/Syncroton/Command/SendMail.php
+++ b/lib/ext/Syncroton/Command/SendMail.php
@@ -37,9 +37,9 @@ class Syncroton_Command_SendMail extends Syncroton_Command_Wbxml
             $this->_replaceMime   = false;
             
             $this->_source = array(
-                    'collectionId' => $this->_requestParameters['collectionId'],
-                    'itemId'       => $this->_requestParameters['itemId'],
-                    'instanceId'   => null
+                'collectionId' => $this->_requestParameters['collectionId'],
+                'itemId'       => $this->_requestParameters['itemId'],
+                'instanceId'   => null
             );
             
         } else {
diff --git a/lib/ext/Syncroton/Command/Sync.php b/lib/ext/Syncroton/Command/Sync.php
index 204f7ab..46e38c2 100644
--- a/lib/ext/Syncroton/Command/Sync.php
+++ b/lib/ext/Syncroton/Command/Sync.php
@@ -123,14 +123,52 @@ class Syncroton_Command_Sync extends Syncroton_Command_Wbxml
         
         $this->_globalWindowSize = isset($xml->WindowSize) ? (int)$xml->WindowSize : 100;
         
+        
         if ($this->_globalWindowSize > $this->_maxWindowSize) {
             $this->_globalWindowSize = $this->_maxWindowSize;
         }
         
-        foreach ($xml->Collections->Collection as $xmlCollection) {
-            $collectionData = new Syncroton_Model_SyncCollection($xmlCollection);
+        $collections = array();
+        $isPartialRequest = isset($xml->Partial);
+        
+        // try to restore collections from previous request
+        if ($isPartialRequest) {
+            $decodedCollections = Zend_Json::decode($this->_device->lastsynccollection);
+            
+            if (is_array($decodedCollections)) {
+                foreach ($decodedCollections as $collection) {
+                    $collections[$collection['collectionId']] = new Syncroton_Model_SyncCollection($collection);
+                }
+            }
+        }
+        
+        // Collections element is optional when Partial element is sent
+        if (isset($xml->Collections)) {
+            foreach ($xml->Collections->Collection as $xmlCollection) {
+                $collectionId = (string)$xmlCollection->CollectionId;
+                
+                // do we have to update a collection sent in previous sync request?
+                if ($isPartialRequest && isset($collections[$collectionId])) {
+                    $collections[$collectionId]->setFromSimpleXMLElement($xmlCollection);
+                } else {
+                    $collections[$collectionId] = new Syncroton_Model_SyncCollection($xmlCollection);
+                }
+            }
             
-            // got the folder synchronized to the device already
+            // store current value of $collections for next Sync command request
+            $collectionsToSave = array();
+        
+            foreach ($collections as $collection) {
+                $collectionsToSave[$collection->collectionId] = $collection->toArray();
+            }
+        
+            $this->_device->lastsynccollection = Zend_Json::encode($collectionsToSave);
+        
+            Syncroton_Registry::getDeviceBackend()->update($this->_device);
+        }
+        
+        foreach ($collections as $collectionData) {
+            // has the folder been synchronised to the device already
             try {
                 $collectionData->folder = $this->_folderBackend->getFolder($this->_device, $collectionData->collectionId);
                 
@@ -143,11 +181,11 @@ class Syncroton_Command_Sync extends Syncroton_Command_Wbxml
                 if ($collectionData->syncKey > 0) {
                     $collectionData->folder    = new Syncroton_Model_Folder(array(
                         'deviceId' => $this->_device,
-                        'serverId'  => $collectionData->collectionId
+                        'serverId' => $collectionData->collectionId
                     ));
                 }
                 
-                $this->_collections[] = $collectionData;
+                $this->_collections[$collectionData->collectionId] = $collectionData;
                 
                 continue;
             }
@@ -171,7 +209,7 @@ class Syncroton_Command_Sync extends Syncroton_Command_Wbxml
                     'lastsync'  => $this->_syncTimeStamp
                 ));
                 
-                $this->_collections[] = $collectionData;
+                $this->_collections[$collectionData->collectionId] = $collectionData;
                 
                 continue;
             }
@@ -185,7 +223,7 @@ class Syncroton_Command_Sync extends Syncroton_Command_Wbxml
                 $this->_syncStateBackend->resetState($this->_device, $collectionData->folder);
                 $this->_contentStateBackend->resetState($this->_device, $collectionData->folder);
                 
-                $this->_collections[] = $collectionData;
+                $this->_collections[$collectionData->collectionId] = $collectionData;
                 
                 continue;
             }
@@ -359,7 +397,7 @@ class Syncroton_Command_Sync extends Syncroton_Command_Wbxml
                 $collectionData->toBeFetched = $toBeFetched;
             }
             
-            $this->_collections[] = $collectionData;
+            $this->_collections[$collectionData->collectionId] = $collectionData;
             $this->_modifications[$collectionData->collectionId] = $clientModifications;
         }
     }
@@ -372,6 +410,12 @@ class Syncroton_Command_Sync extends Syncroton_Command_Wbxml
     {
         $sync = $this->_outputDom->documentElement;
         
+        if (count($this->_collections) == 0) {
+            $sync->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_RESEND_FULL_XML));
+            
+            return $this->_outputDom;
+        }
+        
         $collections = $sync->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Collections'));
 
         $totalChanges = 0;
@@ -381,6 +425,11 @@ class Syncroton_Command_Sync extends Syncroton_Command_Wbxml
             $intervalStart = time();
             
             do {
+                // take a break to save battery lifetime
+                sleep(Syncroton_Registry::getPingTimeout());
+                
+                $now = new DateTime(null, new DateTimeZone('utc'));
+
                 foreach($this->_collections as $collectionData) {
                     // countinue immediately if folder does not exist 
                     if (! ($collectionData->folder instanceof Syncroton_Model_IFolder)) {
@@ -391,6 +440,56 @@ class Syncroton_Command_Sync extends Syncroton_Command_Wbxml
                         break 2;
                         
                     } else {
+                        if ($collectionData->getChanges !== true) {
+                            continue;
+                        }
+                        
+                        try {
+                            // just check if the folder still exists
+                            $this->_folderBackend->get($collectionData->folder);
+                        } catch (Syncroton_Exception_NotFound $senf) {
+                            if ($this->_logger instanceof Zend_Log)
+                                $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " collection does not exist anymore: " . $collectionData->collectionId);
+                            
+                            $collectionData->getChanges = false;
+                            
+                            // make sure this is the last while loop
+                            // no break 2 here, as we like to check the other folders too
+                            $intervalStart -= $this->_heartbeatInterval;
+                        }
+                        
+                        // check that the syncstate still exists and is still valid
+                        try {
+                            $syncState = $this->_syncStateBackend->getSyncState($this->_device, $collectionData->folder);
+                            
+                            // another process synchronized data of this folder already. let's skip it
+                            if ($syncState->id !== $collectionData->syncState->id) {
+                                if ($this->_logger instanceof Zend_Log)
+                                    $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " syncstate changed during heartbeat interval for collection: " . $collectionData->folder->serverId);
+                                
+                                $collectionData->getChanges = false;
+                                
+                                // make sure this is the last while loop
+                                // no break 2 here, as we like to check the other folders too
+                                $intervalStart -= $this->_heartbeatInterval;
+                            }
+                        } catch (Syncroton_Exception_NotFound $senf) {
+                            if ($this->_logger instanceof Zend_Log)
+                                $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " no syncstate found anymore for collection: " . $collectionData->folder->serverId);
+                            
+                            $collectionData->syncState = null;
+                            
+                            // make sure this is the last while loop
+                            // no break 2 here, as we like to check the other folders too
+                            $intervalStart -= $this->_heartbeatInterval;
+                        }
+                        
+                        
+                        // safe battery time by skipping folders which got synchronied less than Syncroton_Command_Ping::$quietTime seconds ago
+                        if (($now->getTimestamp() - $collectionData->syncState->lastsync->getTimestamp()) < Syncroton_Registry::getQuietTime()) {
+                            continue;
+                        }
+                                                
                         $dataController = Syncroton_Data_Factory::factory($collectionData->folder->class , $this->_device, $this->_syncTimeStamp);
                         
                         $estimate = $dataController->getCountOfChanges($this->_contentStateBackend, $collectionData->folder, $collectionData->syncState);
@@ -402,15 +501,29 @@ class Syncroton_Command_Sync extends Syncroton_Command_Wbxml
                     }
                 }
                 
-                // wait some PING_TIMEOUT seconds until neext loop
-                sleep(Syncroton_Command_Ping::$pingTimeout);
+                $this->_syncTimeStamp = clone $now;
                 
-            } while (time() - $intervalStart < $this->_heartbeatInterval);
+            // See: http://www.tine20.org/forum/viewtopic.php?f=12&t=12146
+            //
+            // break if there are less than PingTimeout + 10 seconds left for the next loop
+            // otherwise the response will be returned after the client has finished his Ping
+            // request already maybe
+            } while (time() - $intervalStart < $this->_heartbeatInterval - (Syncroton_Registry::getPingTimeout() + 10));
         }
         
         foreach($this->_collections as $collectionData) {
             $collectionChanges = 0;
             
+            /**
+             * keep track of entries added on server side
+             */
+            $newContentStates = array();
+            
+            /**
+             * keep track of entries deleted on server side
+             */
+            $deletedContentStates = array();
+            
             // invalid collectionid provided
             if (! ($collectionData->folder instanceof Syncroton_Model_IFolder)) {
                 $collection = $collections->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Collection'));
@@ -484,7 +597,7 @@ class Syncroton_Command_Sync extends Syncroton_Command_Wbxml
 
                             // entries to be deleted
                             $serverModifications['deleted'] = array_diff($allClientEntries, $allServerEntries);
-
+                            
                             // fetch entries changed since last sync
                             $serverModifications['changed'] = $dataController->getChangedEntries($collectionData->collectionId, $collectionData->syncState->lastsync, $this->_syncTimeStamp);
                             $serverModifications['changed'] = array_merge($serverModifications['changed'], $clientModifications['forceChange']);
@@ -533,7 +646,7 @@ class Syncroton_Command_Sync extends Syncroton_Command_Wbxml
                     $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Class', $collectionData->folder->class));
                 }
                 
-                $syncKeyNode = $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'SyncKey'));
+                $syncKeyElement = $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'SyncKey'));
                 
                 $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'CollectionId', $collectionData->collectionId));
                 $collection->appendChild($this->_outputDom->createElementNS('uri:AirSync', 'Status', self::STATUS_SUCCESS));
@@ -596,13 +709,8 @@ class Syncroton_Command_Sync extends Syncroton_Command_Wbxml
                 
                 $commands = $this->_outputDom->createElementNS('uri:AirSync', 'Commands');
                 
-                /**
-                 * process entries added on server side
-                 */
-                $newContentStates = array();
-                
                 foreach($serverModifications['added'] as $id => $serverId) {
-                    if($collectionChanges === $collectionData->windowSize || $totalChanges + $collectionChanges >= $this->_globalWindowSize) {
+                    if($collectionChanges == $collectionData->windowSize || $totalChanges + $collectionChanges >= $this->_globalWindowSize) {
                         break;
                     }
                     
@@ -652,14 +760,14 @@ class Syncroton_Command_Sync extends Syncroton_Command_Wbxml
                         'creation_time'    => $this->_syncTimeStamp,
                         'creation_synckey' => $collectionData->syncState->counter + 1
                     ));
-                    unset($serverModifications['added'][$id]);    
+                    unset($serverModifications['added'][$id]);
                 }
 
                 /**
                  * process entries changed on server side
                  */
                 foreach($serverModifications['changed'] as $id => $serverId) {
-                    if($collectionChanges === $collectionData->windowSize || $totalChanges + $collectionChanges >= $this->_globalWindowSize) {
+                    if($collectionChanges == $collectionData->windowSize || $totalChanges + $collectionChanges >= $this->_globalWindowSize) {
                         break;
                     }
                     
@@ -685,13 +793,8 @@ class Syncroton_Command_Sync extends Syncroton_Command_Wbxml
                     unset($serverModifications['changed'][$id]);    
                 }
 
-                /**
-                 * process entries deleted on server side
-                 */
-                $deletedContentStates = array();
-                
                 foreach($serverModifications['deleted'] as $id => $serverId) {
-                    if($collectionChanges === $collectionData->windowSize || $totalChanges + $collectionChanges >= $this->_globalWindowSize) {
+                    if($collectionChanges == $collectionData->windowSize || $totalChanges + $collectionChanges >= $this->_globalWindowSize) {
                         break;
                     }
                     
@@ -745,7 +848,7 @@ class Syncroton_Command_Sync extends Syncroton_Command_Wbxml
                     // then increase SyncKey
                     $collectionData->syncState->counter++;
                 }
-                $syncKeyNode->appendChild($this->_outputDom->createTextNode($collectionData->syncState->counter));
+                $syncKeyElement->appendChild($this->_outputDom->createTextNode($collectionData->syncState->counter));
                 
                 if ($this->_logger instanceof Zend_Log) 
                     $this->_logger->info(__METHOD__ . '::' . __LINE__ . " new synckey is ". $collectionData->syncState->counter);
@@ -753,7 +856,10 @@ class Syncroton_Command_Sync extends Syncroton_Command_Wbxml
             
             if (isset($collectionData->syncState) && $collectionData->syncState instanceof Syncroton_Model_ISyncState && 
                 $collectionData->syncState->counter != $collectionData->syncKey) {
-                        
+                
+                if ($this->_logger instanceof Zend_Log)
+                    $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " update syncState for collection: " . $collectionData->collectionId);
+                
                 // store pending data in sync state when needed
                 if(isset($countOfPendingChanges) && $countOfPendingChanges > 0) {
                     $collectionData->syncState->pendingdata = array(
@@ -785,18 +891,13 @@ class Syncroton_Command_Sync extends Syncroton_Command_Wbxml
                     $this->_syncStateBackend->create($collectionData->syncState, $keepPreviousSyncKey);
                     
                     // store contentstates for new entries added to client
-                    if (isset($newContentStates)) {
-                        foreach($newContentStates as $state) {
-                            #$state->creation_synckey = $collectionData->syncState->counter;
-                            $this->_contentStateBackend->create($state);
-                        }
+                    foreach($newContentStates as $state) {
+                        $this->_contentStateBackend->create($state);
                     }
                     
                     // remove contentstates for entries to be deleted on client
-                    if (isset($deletedContentStates)) {
-                        foreach($deletedContentStates as $state) {
-                            $this->_contentStateBackend->delete($state);
-                        }
+                    foreach($deletedContentStates as $state) {
+                        $this->_contentStateBackend->delete($state);
                     }
                     
                     Syncroton_Registry::getTransactionManager()->commitTransaction($transactionId);
diff --git a/lib/ext/Syncroton/Command/Wbxml.php b/lib/ext/Syncroton/Command/Wbxml.php
index cab5cc9..e927cc2 100644
--- a/lib/ext/Syncroton/Command/Wbxml.php
+++ b/lib/ext/Syncroton/Command/Wbxml.php
@@ -118,6 +118,20 @@ abstract class Syncroton_Command_Wbxml implements Syncroton_Command_ICommand
     protected $_logger;
     
     /**
+     * list of part streams
+     * 
+     * @var array
+     */
+    protected $_parts = array();
+    
+    /**
+     * list of headers
+     * 
+     * @var array
+     */
+    protected $_headers = array();
+    
+    /**
      * the constructor
      *
      * @param  mixed                   $requestBody
@@ -142,6 +156,9 @@ abstract class Syncroton_Command_Wbxml implements Syncroton_Command_ICommand
         
         $this->_syncTimeStamp = new DateTime(null, new DateTimeZone('UTC'));
         
+        // set default content type
+        $this->_headers['Content-Type'] = 'application/vnd.ms-sync.wbxml';
+        
         if ($this->_logger instanceof Zend_Log) 
             $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " sync timestamp: " . $this->_syncTimeStamp->format('Y-m-d H:i:s'));
         
@@ -184,4 +201,22 @@ abstract class Syncroton_Command_Wbxml implements Syncroton_Command_ICommand
             }
         }
     }
+    
+    /**
+     * (non-PHPdoc)
+     * @see Syncroton_Command_ICommand::getHeaders()
+     */
+    public function getHeaders()
+    {
+        return $this->_headers;
+    }
+    /**
+     * return array of part streams
+     * 
+     * @return array
+     */
+    public function getParts()
+    {
+        return $this->_parts;
+    }
 }
diff --git a/lib/ext/Syncroton/Data/AData.php b/lib/ext/Syncroton/Data/AData.php
index 4e02ef5..1382f68 100644
--- a/lib/ext/Syncroton/Data/AData.php
+++ b/lib/ext/Syncroton/Data/AData.php
@@ -16,53 +16,71 @@
 
 abstract class Syncroton_Data_AData implements Syncroton_Data_IData
 {
-    /**
-     * used by unit tests only to simulated added folders
-     */
-    public static $folders = array();
+    const LONGID_DELIMITER = "\xe2\x87\x94"; # UTF8 ⇔
     
     /**
      * used by unit tests only to simulated added folders
      */
-    public static $entries = array();
-    
-    /**
-    * used by unit tests only to simulated added folders
-    */
     public static $changedEntries = array();
     
     public function __construct(Syncroton_Model_IDevice $_device, DateTime $_timeStamp)
     {
-        $this->_device = $_device;
-        $this->_timestamp = $_timeStamp;
-        
+        $this->_device      = $_device;
+        $this->_timestamp   = $_timeStamp;
         $this->_db          = Syncroton_Registry::getDatabase();
         $this->_tablePrefix = 'Syncroton_';
+        $this->_ownerId     = '1234';
+    }
+    
+    public function getFolder($id)
+    {
+        $select = $this->_db->select()
+            ->from($this->_tablePrefix . 'data_folder')
+            ->where('owner_id = ?', $this->_ownerId)
+            ->where('id = ?', $id);
         
-        $this->_initData();
+        $stmt   = $this->_db->query($select);
+        $folder = $stmt->fetch();
+        $stmt = null; # see https://bugs.php.net/bug.php?id=44081
+        
+        if ($folder === false) {
+            throw new Syncroton_Exception_NotFound("folder $id not found");
+        }
+        
+        return new Syncroton_Model_Folder(array(
+            'serverId'    => $folder['id'],
+            'displayName' => $folder['name'],
+            'type'        => $folder['type'],
+            'parentId'    => !empty($folder['parent_id']) ? $folder['parent_id'] : null
+        ));
     }
     
     public function createFolder(Syncroton_Model_IFolder $folder)
     {
-        $folder->id = sha1(mt_rand(). microtime());
+        if (!in_array($folder->type, $this->_supportedFolderTypes)) {
+            throw new Syncroton_Exception_UnexpectedValue();
+        }
         
-        // normaly generated on server backend
-        $folder->serverId = sha1(mt_rand(). microtime());
-    
-        Syncroton_Data_AData::$folders[get_class($this)][$folder->serverId] = $folder;
-    
-        return Syncroton_Data_AData::$folders[get_class($this)][$folder->serverId];
+        $id = !empty($folder->serverId) ? $folder->serverId : sha1(mt_rand(). microtime());
+        
+        $this->_db->insert($this->_tablePrefix . 'data_folder', array(
+            'id'        => $id,
+            'type'      => $folder->type,
+            'name'      => $folder->displayName,
+            'owner_id'  => $this->_ownerId,
+            'parent_id' => $folder->parentId
+        ));
+        
+        return $this->getFolder($id);
     }
     
     public function createEntry($_folderId, Syncroton_Model_IEntry $_entry)
     {
         $id = sha1(mt_rand(). microtime());
     
-        #Syncroton_Data_AData::$entries[get_class($this)][$_folderId][$id] = $_entry;
-        
         $this->_db->insert($this->_tablePrefix . 'data', array(
             'id'        => $id,
-            'type'      => get_class($this),
+            'class'     => get_class($_entry),
             'folder_id' => $_folderId,
             'data'      => serialize($_entry)
         ));
@@ -72,26 +90,46 @@ abstract class Syncroton_Data_AData implements Syncroton_Data_IData
     
     public function deleteEntry($_folderId, $_serverId, $_collectionData)
     {
-        #$folderId = $_folderId instanceof Syncroton_Model_IFolder ? $_folderId->serverId : $_folderId;
+        $folderId = $_folderId instanceof Syncroton_Model_IFolder ? $_folderId->serverId : $_folderId;
         
         $result = $this->_db->delete($this->_tablePrefix . 'data', array('id = ?' => $_serverId));
         
         return (bool) $result;
-        
-        #unset(Syncroton_Data_AData::$entries[get_class($this)][$folderId][$_serverId]);
     }
     
     public function deleteFolder($_folderId)
     {
         $folderId = $_folderId instanceof Syncroton_Model_IFolder ? $_folderId->serverId : $_folderId;
-    
-        unset(Syncroton_Data_AData::$folders[get_class($this)][$folderId]);
-        unset(Syncroton_Data_AData::$entries[get_class($this)][$folderId]);
+        
+        $result = $this->_db->delete($this->_tablePrefix . 'data', array('folder_id = ?' => $folderId));
+        $result = $this->_db->delete($this->_tablePrefix . 'data_folder', array('id = ?' => $folderId));
+        
+        return (bool) $result;
     }
     
     public function getAllFolders()
     {
-        return Syncroton_Data_AData::$folders[get_class($this)];
+        $select = $this->_db->select()
+            ->from($this->_tablePrefix . 'data_folder')
+            ->where('type IN (?)', $this->_supportedFolderTypes)
+            ->where('owner_id = ?', $this->_ownerId);
+        
+        $stmt    = $this->_db->query($select);
+        $folders = $stmt->fetchAll();
+        $stmt = null; # see https://bugs.php.net/bug.php?id=44081
+        
+        $result = array();
+        
+        foreach ((array) $folders as $folder) {
+            $result[$folder['id']] =  new Syncroton_Model_Folder(array(
+                'serverId'    => $folder['id'],
+                'displayName' => $folder['name'],
+                'type'        => $folder['type'],
+                'parentId'    => $folder['parent_id']
+            ));
+        }
+        
+        return $result;
     }
     
     public function getChangedEntries($_folderId, DateTime $_startTimeStamp, DateTime $_endTimeStamp = NULL)
@@ -177,7 +215,6 @@ abstract class Syncroton_Data_AData implements Syncroton_Data_IData
     public function updateEntry($_folderId, $_serverId, Syncroton_Model_IEntry $_entry)
     {
         $this->_db->update($this->_tablePrefix . 'data', array(
-            'type'      => get_class($this),
             'folder_id' => $_folderId,
             'data'      => serialize($_entry)
         ), array(
@@ -187,10 +224,12 @@ abstract class Syncroton_Data_AData implements Syncroton_Data_IData
     
     public function updateFolder(Syncroton_Model_IFolder $folder)
     {
-        Syncroton_Data_AData::$folders[get_class($this)][$folder->serverId] = $folder;
+        $this->_db->update($this->_tablePrefix . 'data_folder', array(
+            'name'      => $folder->displayName,
+            'parent_id' => $folder->parentId
+        ), array(
+            'id = ?' => $folder->serverId
+        ));
     }
-    
-    
-    abstract protected function _initData();
 }
 
diff --git a/lib/ext/Syncroton/Data/Calendar.php b/lib/ext/Syncroton/Data/Calendar.php
index 9f24590..66c8d25 100644
--- a/lib/ext/Syncroton/Data/Calendar.php
+++ b/lib/ext/Syncroton/Data/Calendar.php
@@ -13,31 +13,22 @@
  *
  * @package     Model
  */
-
-class Syncroton_Data_Calendar extends Syncroton_Data_AData
+class Syncroton_Data_Calendar extends Syncroton_Data_AData implements Syncroton_Data_IDataCalendar
 {
-    protected function _initData()
+    protected $_supportedFolderTypes = array(
+        Syncroton_Command_FolderSync::FOLDERTYPE_CALENDAR,
+        Syncroton_Command_FolderSync::FOLDERTYPE_CALENDAR_USER_CREATED
+    );
+    
+    /**
+     * set attendee status for meeting
+     * 
+     * @param  Syncroton_Model_MeetingResponse  $request  the meeting response
+     * @return  string  id of new calendar entry
+     */
+    public function setAttendeeStatus(Syncroton_Model_MeetingResponse $reponse)
     {
-        /**
-        * used by unit tests only to simulated added folders
-        */
-        Syncroton_Data_AData::$folders[get_class($this)] = array(
-                'calendarFolderId' => new Syncroton_Model_Folder(array(
-                    'id'          => sha1(mt_rand(). microtime()),
-                    'serverId'    => 'calendarFolderId',
-                    'parentId'    => 0,
-                    'displayName' => 'Default Contacts Folder',
-                    'type'        => Syncroton_Command_FolderSync::FOLDERTYPE_CALENDAR
-                ))
-        );
-        
-        #/**
-        # * used by unit tests only to simulated added folders
-        # */
-        #Syncroton_Data_AData::$entries[get_class($this)] = array(
-        #        'calendarFolderId' => array(
-        #        )
-        #);
+        return $reponse->requestId;
     }
 }
 
diff --git a/lib/ext/Syncroton/Data/Contacts.php b/lib/ext/Syncroton/Data/Contacts.php
index 1f191c2..a9b8d58 100644
--- a/lib/ext/Syncroton/Data/Contacts.php
+++ b/lib/ext/Syncroton/Data/Contacts.php
@@ -16,13 +16,18 @@
 
 class Syncroton_Data_Contacts extends Syncroton_Data_AData implements Syncroton_Data_IDataSearch
 {
+    protected $_supportedFolderTypes = array(
+        Syncroton_Command_FolderSync::FOLDERTYPE_CONTACT,
+        Syncroton_Command_FolderSync::FOLDERTYPE_CONTACT_USER_CREATED
+    );
+    
     /**
      * (non-PHPdoc)
      * @see Syncroton_Data_IDataSearch::getSearchEntry()
      */
     public function getSearchEntry($longId, $options)
     {
-        list($collectionId, $serverId) = explode('-', $longId, 2);
+        list($collectionId, $serverId) = explode(Syncroton_Data_AData::LONGID_DELIMITER, $longId, 2);
         
         $contact = $this->getEntry(new Syncroton_Model_SyncCollection(array('collectionId' => $collectionId)), $serverId);
         
@@ -56,8 +61,8 @@ class Syncroton_Data_Contacts extends Syncroton_Data_AData implements Syncroton_
                     continue;
                 }
                 $found[] = new Syncroton_Model_StoreResponseResult(array(
-                    'longId' => 'addressbookFolderId-' .  $serverId,
-                    'properties' => $this->getSearchEntry('addressbookFolderId-' .  $serverId, $store->options)
+                    'longId' => 'addressbookFolderId' . Syncroton_Data_AData::LONGID_DELIMITER .  $serverId,
+                    'properties' => $this->getSearchEntry('addressbookFolderId' . Syncroton_Data_AData::LONGID_DELIMITER .  $serverId, $store->options)
                 ));
             }
         }
@@ -72,131 +77,5 @@ class Syncroton_Data_Contacts extends Syncroton_Data_AData implements Syncroton_
         
         return $storeResponse;
     }
-    
-    protected function _initData()
-    {
-        /**
-        * used by unit tests only to simulated added folders
-        */
-        if (!isset(Syncroton_Data_AData::$folders[get_class($this)])) {
-            Syncroton_Data_AData::$folders[get_class($this)] = array(
-                'addressbookFolderId' => new Syncroton_Model_Folder(array(
-                    'id'          => sha1(mt_rand(). microtime()),
-                    'serverId'    => 'addressbookFolderId',
-                    'parentId'    => 0,
-                    'displayName' => 'Default Contacts Folder',
-                    'type'        => Syncroton_Command_FolderSync::FOLDERTYPE_CONTACT
-                )),
-                'anotherAddressbookFolderId' => new Syncroton_Model_Folder(array(
-                    'id'          => sha1(mt_rand(). microtime()),
-                    'serverId'    => 'anotherAddressbookFolderId',
-                    'parentId'    => 0,
-                    'displayName' => 'Another Contacts Folder',
-                    'type'        => Syncroton_Command_FolderSync::FOLDERTYPE_CONTACT_USER_CREATED
-                ))
-            );
-        }
-        
-        /**
-         * used by unit tests only to simulated added folders
-         */
-        $entries = $this->getServerEntries('addressbookFolderId', 1);
-        
-        if (count($entries) == 0) {
-            $testData = array(
-                'addressbookFolderId' => array(
-                    'contact1' => new Syncroton_Model_Contact(array(
-                        'firstName' => 'Lars', 
-                        'lastName'  => 'Kneschke'
-                    )),
-                    'contact2' => new Syncroton_Model_Contact(array(
-                        'firstName' => 'Cornelius', 
-                        'lastName'  => 'Weiß'
-                    )),
-                    'contact3' => new Syncroton_Model_Contact(array(
-                        'firstName' => 'Lars', 
-                        'lastName'  => 'Kneschke'
-                    )),
-                    'contact4' => new Syncroton_Model_Contact(array(
-                        'firstName' => 'Cornelius', 
-                        'lastName'  => 'Weiß'
-                    )),
-                    'contact5' => new Syncroton_Model_Contact(array(
-                        'firstName' => 'Lars', 
-                        'lastName'  => 'Kneschke'
-                    )),
-                    'contact6' => new Syncroton_Model_Contact(array(
-                        'firstName' => 'Cornelius', 
-                        'lastName'  => 'Weiß'
-                    )),
-                    'contact7' => new Syncroton_Model_Contact(array(
-                        'firstName' => 'Lars', 
-                        'lastName'  => 'Kneschke'
-                    )),
-                    'contact8' => new Syncroton_Model_Contact(array(
-                        'firstName' => 'Cornelius', 
-                        'lastName'  => 'Weiß'
-                    )),
-                    'contact9' => new Syncroton_Model_Contact(array(
-                        'firstName' => 'Lars', 
-                        'lastName'  => 'Kneschke'
-                    )),
-                    'contact10' => new Syncroton_Model_Contact(array(
-                        'firstName' => 'Cornelius', 
-                        'lastName'  => 'Weiß'
-                    ))
-                ),
-                'anotherAddressbookFolderId' => array(
-                    'contact1' => new Syncroton_Model_Contact(array(
-                        'firstName' => 'Lars', 
-                        'lastName'  => 'Kneschke'
-                    )),
-                    'contact2' => new Syncroton_Model_Contact(array(
-                        'firstName' => 'Cornelius', 
-                        'lastName'  => 'Weiß'
-                    )),
-                    'contact3' => new Syncroton_Model_Contact(array(
-                        'firstName' => 'Lars', 
-                        'lastName'  => 'Kneschke'
-                    )),
-                    'contact4' => new Syncroton_Model_Contact(array(
-                        'firstName' => 'Cornelius', 
-                        'lastName'  => 'Weiß'
-                    )),
-                    'contact5' => new Syncroton_Model_Contact(array(
-                        'firstName' => 'Lars', 
-                        'lastName'  => 'Kneschke'
-                    )),
-                    'contact6' => new Syncroton_Model_Contact(array(
-                        'firstName' => 'Cornelius', 
-                        'lastName'  => 'Weiß'
-                    )),
-                    'contact7' => new Syncroton_Model_Contact(array(
-                        'firstName' => 'Lars', 
-                        'lastName'  => 'Kneschke'
-                    )),
-                    'contact8' => new Syncroton_Model_Contact(array(
-                        'firstName' => 'Cornelius', 
-                        'lastName'  => 'Weiß'
-                    )),
-                    'contact9' => new Syncroton_Model_Contact(array(
-                        'firstName' => 'Lars', 
-                        'lastName'  => 'Kneschke'
-                    )),
-                    'contact10' => new Syncroton_Model_Contact(array(
-                        'firstName' => 'Cornelius', 
-                        'lastName'  => 'Weiß'
-                    ))
-                )
-            );
-            
-            foreach ($testData['addressbookFolderId'] as $data) {
-                $this->createEntry('addressbookFolderId', $data);
-            }
-            foreach ($testData['anotherAddressbookFolderId'] as $data) {
-                $this->createEntry('anotherAddressbookFolderId', $data);
-            }
-        }
-    }
 }
 
diff --git a/lib/ext/Syncroton/Data/Email.php b/lib/ext/Syncroton/Data/Email.php
index 9c84aae..5005c97 100644
--- a/lib/ext/Syncroton/Data/Email.php
+++ b/lib/ext/Syncroton/Data/Email.php
@@ -16,10 +16,14 @@
  */
 class Syncroton_Data_Email extends Syncroton_Data_AData implements Syncroton_Data_IDataEmail
 {
-    /**
-     * used by unit tests only to simulated added folders
-     */
-    public static $entries = array();
+    protected $_supportedFolderTypes = array(
+        Syncroton_Command_FolderSync::FOLDERTYPE_DELETEDITEMS,
+        Syncroton_Command_FolderSync::FOLDERTYPE_DRAFTS,
+        Syncroton_Command_FolderSync::FOLDERTYPE_INBOX,
+        Syncroton_Command_FolderSync::FOLDERTYPE_MAIL_USER_CREATED,
+        Syncroton_Command_FolderSync::FOLDERTYPE_OUTBOX,
+        Syncroton_Command_FolderSync::FOLDERTYPE_SENTMAIL
+    );
     
     /**
      * (non-PHPdoc)
@@ -40,7 +44,7 @@ class Syncroton_Data_Email extends Syncroton_Data_AData implements Syncroton_Dat
      */
     public function getFileReference($fileReference)
     {
-        list($messageId, $partId) = explode('-', $fileReference, 2);
+        list($messageId, $partId) = explode(Syncroton_Data_AData::LONGID_DELIMITER, $fileReference, 2);
     
         // example code
         return new Syncroton_Model_FileReference(array(
@@ -78,67 +82,5 @@ class Syncroton_Data_Email extends Syncroton_Data_AData implements Syncroton_Dat
         }
         // send email
     }
-    
-    protected function _initData()
-    {
-        /**
-        * used by unit tests only to simulated added folders
-        */
-        Syncroton_Data_AData::$folders[get_class($this)] = array(
-            'emailInboxFolderId' => new Syncroton_Model_Folder(array(
-                'id'          => sha1(mt_rand(). microtime()),
-                'serverId'    => 'emailInboxFolderId',
-                'parentId'    => 0,
-                'displayName' => 'Inbox',
-                'type'        => Syncroton_Command_FolderSync::FOLDERTYPE_INBOX
-            )),
-            'emailSentFolderId' => new Syncroton_Model_Folder(array(
-                'id'          => sha1(mt_rand(). microtime()),
-                'serverId'    => 'emailSentFolderId',
-                'parentId'    => 0,
-                'displayName' => 'Sent',
-                'type'        => Syncroton_Command_FolderSync::FOLDERTYPE_SENTMAIL
-            ))
-        );
-        
-        /**
-         * used by unit tests only to simulated added folders
-         */
-        
-        $entries = $this->getServerEntries('emailInboxFolderId', 1);
-        
-        if (count($entries) == 0) {
-            $testData = array(
-                'emailInboxFolderId' => array(
-                    'email1' => new Syncroton_Model_Email(array(
-                        'accountId'    => 'FooBar',
-                        'attachments'  => array(
-                            new Syncroton_Model_EmailAttachment(array(
-                                'fileReference' => '12345abcd',
-                                'umAttOrder'    => 1
-                            ))
-                        ),
-                        'categories'   => array('123', '456'),
-                        'cc'           => 'l.kneschke at metaways.de',
-                        'dateReceived' => new DateTime('2012-03-21 14:00:00', new DateTimeZone('UTC')), 
-                        'from'         => 'k.kneschke at metaways.de',
-                        'subject'      => 'Test Subject',
-                        'to'           => 'j.kneschke at metaways.de',
-                        'read'         => 1,
-                        'body'         => new Syncroton_Model_EmailBody(array(
-                            'type'              => Syncroton_Model_EmailBody::TYPE_PLAINTEXT, 
-                            'data'              => 'Hello!', 
-                            'truncated'         => true, 
-                            'estimatedDataSize' => 600
-                        ))
-                    )),
-                )
-            );
-            
-            foreach ($testData['emailInboxFolderId'] as $data) {
-                $this->createEntry('emailInboxFolderId', $data);
-            }
-        }
-    }
 }
 
diff --git a/lib/ext/Syncroton/Data/IDataCalendar.php b/lib/ext/Syncroton/Data/IDataCalendar.php
new file mode 100644
index 0000000..2ea6831
--- /dev/null
+++ b/lib/ext/Syncroton/Data/IDataCalendar.php
@@ -0,0 +1,26 @@
+<?php
+/**
+ * Syncroton
+ *
+ * @package     Model
+ * @license     http://www.tine20.org/licenses/lgpl.html LGPL Version 3
+ * @copyright   Copyright (c) 2012-2012 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Lars Kneschke <l.kneschke at metaways.de>
+ */
+
+/**
+ * interface for extended calendar backend
+ *
+ * @package     Model
+ */
+interface Syncroton_Data_IDataCalendar
+{
+    /**
+     * set attendee status for meeting
+     * 
+     * @param  Syncroton_Model_MeetingResponse  $request  the meeting response
+     * @return  string  id of new calendar entry
+     */
+    public function setAttendeeStatus(Syncroton_Model_MeetingResponse $request);
+}
+
diff --git a/lib/ext/Syncroton/Data/IDataEmail.php b/lib/ext/Syncroton/Data/IDataEmail.php
index 7ecbb37..bca70b9 100644
--- a/lib/ext/Syncroton/Data/IDataEmail.php
+++ b/lib/ext/Syncroton/Data/IDataEmail.php
@@ -9,7 +9,7 @@
  */
 
 /**
- * intace for email backend
+ * interface for extended email backend
  *
  * @package     Model
  */
@@ -26,7 +26,7 @@ interface Syncroton_Data_IDataEmail
     /**
      * forward an email
      * 
-     * @param  string|array  $source       is either a string(LongId) or an arrey with following properties collectionId, itemId and instanceId
+     * @param  string|array  $source       is either a string(LongId) or an array with following properties collectionId, itemId and instanceId
      * @param  string        $inputStream
      * @param  string        $saveInSent
      */
@@ -35,7 +35,7 @@ interface Syncroton_Data_IDataEmail
     /**
      * reply to an email
      * 
-     * @param  string|array  $source       is either a string(LongId) or an arrey with following properties collectionId, itemId and instanceId
+     * @param  string|array  $source       is either a string(LongId) or an array with following properties collectionId, itemId and instanceId
      * @param  string        $inputStream
      * @param  string        $saveInSent
      */
diff --git a/lib/ext/Syncroton/Data/Tasks.php b/lib/ext/Syncroton/Data/Tasks.php
index 30a80d2..a0c4756 100644
--- a/lib/ext/Syncroton/Data/Tasks.php
+++ b/lib/ext/Syncroton/Data/Tasks.php
@@ -16,28 +16,9 @@
 
 class Syncroton_Data_Tasks extends Syncroton_Data_AData
 {
-    protected function _initData()
-    {
-        /**
-        * used by unit tests only to simulated added folders
-        */
-        Syncroton_Data_AData::$folders[get_class($this)] = array(
-            'tasksFolderId' => new Syncroton_Model_Folder(array(
-                'id'          => sha1(mt_rand(). microtime()),
-                'serverId'    => 'tasksFolderId',
-                'parentId'    => 0,
-                'displayName' => 'Default Tasks Folder',
-                'type'        => Syncroton_Command_FolderSync::FOLDERTYPE_TASK
-            ))
-        );
-        
-        /**
-         * used by unit tests only to simulated added folders
-         */
-        #Syncroton_Data_AData::$entries[get_class($this)] = array(
-        #    'tasksFolderId' => array(
-        #    )
-        #);
-    }
+    protected $_supportedFolderTypes = array(
+        Syncroton_Command_FolderSync::FOLDERTYPE_TASK,
+        Syncroton_Command_FolderSync::FOLDERTYPE_TASK_USER_CREATED
+    );
 }
 
diff --git a/lib/ext/Syncroton/Model/AEntry.php b/lib/ext/Syncroton/Model/AEntry.php
index ed8c496..1a58827 100644
--- a/lib/ext/Syncroton/Model/AEntry.php
+++ b/lib/ext/Syncroton/Model/AEntry.php
@@ -129,8 +129,6 @@ abstract class Syncroton_Model_AEntry implements Syncroton_Model_IEntry, Iterato
      */
     public function setFromArray(array $properties)
     {
-        $this->_elements = array();
-        
         foreach($properties as $key => $value) {
             try {
                 $this->$key = $value; //echo __LINE__ . PHP_EOL;
@@ -153,8 +151,6 @@ abstract class Syncroton_Model_AEntry implements Syncroton_Model_IEntry, Iterato
             throw new InvalidArgumentException('Unexpected element name: ' . $properties->getName());
         }
     
-        $this->_elements = array();
-    
         foreach (array_keys($this->_properties) as $namespace) {
             if ($namespace == 'Internal') {
                 continue;
diff --git a/lib/ext/Syncroton/Model/Email.php b/lib/ext/Syncroton/Model/Email.php
index 65cb674..35f2661 100644
--- a/lib/ext/Syncroton/Model/Email.php
+++ b/lib/ext/Syncroton/Model/Email.php
@@ -12,16 +12,23 @@
  * class to handle ActiveSync email
  *
  * @package     Model
- * @property    array    attachments
- * @property    string   contentType
- * @property    array    flag
+ * @property    array     attachments
+ * @property    string    contentType
+ * @property    array     flag
  * @property    Syncroton_Model_EmailBody    body
- * @property    array    cc
- * @property    array    to
- * @property    int      read
+ * @property    array     cc
+ * @property    array     to
+ * @property    int       lastVerbExecuted
+ * @property    DateTime  lastVerbExecutionTime
+ * @property    int       read
  */
 class Syncroton_Model_Email extends Syncroton_Model_AEntry
 {
+    const LASTVERB_UNKNOWN       = 0;
+    const LASTVERB_REPLYTOSENDER = 1;
+    const LASTVERB_REPLYTOALL    = 2;
+    const LASTVERB_FORWARD       = 3;
+    
     protected $_xmlBaseElement = 'ApplicationData';
     
     protected $_properties = array(
diff --git a/lib/ext/Syncroton/Model/FileReference.php b/lib/ext/Syncroton/Model/FileReference.php
index 3aa00cb..9f0bbad 100644
--- a/lib/ext/Syncroton/Model/FileReference.php
+++ b/lib/ext/Syncroton/Model/FileReference.php
@@ -26,6 +26,7 @@ class Syncroton_Model_FileReference extends Syncroton_Model_AEntry
         ),
         'ItemOperations' => array(
             'data'        => array('type' => 'string', 'encoding' => 'base64'),
+            'part'        => array('type' => 'number')
         )
     );
         
diff --git a/lib/ext/Syncroton/Model/Folder.php b/lib/ext/Syncroton/Model/Folder.php
index 33412cf..dfabc34 100644
--- a/lib/ext/Syncroton/Model/Folder.php
+++ b/lib/ext/Syncroton/Model/Folder.php
@@ -29,6 +29,7 @@ class Syncroton_Model_Folder extends Syncroton_Model_AEntry implements Syncroton
         'Internal' => array(
             'id'             => array('type' => 'string'),
             'deviceId'       => array('type' => 'string'),
+            'ownerId'        => array('type' => 'string'),
             'class'          => array('type' => 'string'),
             'creationTime'   => array('type' => 'datetime'),
             'lastfiltertype' => array('type' => 'number')
diff --git a/lib/ext/Syncroton/Model/IDevice.php b/lib/ext/Syncroton/Model/IDevice.php
index b1177cb..e0c59ab 100644
--- a/lib/ext/Syncroton/Model/IDevice.php
+++ b/lib/ext/Syncroton/Model/IDevice.php
@@ -36,6 +36,7 @@
  * @property    string   calendarfilter_id
  * @property    string   tasksfilter_id
  * @property    string   emailfilter_id
+ * @property    string   lastsynccollection
  */
 interface Syncroton_Model_IDevice
 {
diff --git a/lib/ext/Syncroton/Model/IFolder.php b/lib/ext/Syncroton/Model/IFolder.php
index 4ec3700..f27f523 100644
--- a/lib/ext/Syncroton/Model/IFolder.php
+++ b/lib/ext/Syncroton/Model/IFolder.php
@@ -21,6 +21,7 @@
  * @property    string   displayName
  * @property    string   creationTime
  * @property    string   lastfiltertype
+ * @property    string   type
  */
 
 interface Syncroton_Model_IFolder
diff --git a/lib/ext/Syncroton/Model/MeetingResponse.php b/lib/ext/Syncroton/Model/MeetingResponse.php
new file mode 100644
index 0000000..4d1e171
--- /dev/null
+++ b/lib/ext/Syncroton/Model/MeetingResponse.php
@@ -0,0 +1,46 @@
+<?php
+/**
+ * Syncroton
+ *
+ * @package     Model
+ * @license     http://www.tine20.org/licenses/lgpl.html LGPL Version 3
+ * @copyright   Copyright (c) 2012-2012 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author      Lars Kneschke <l.kneschke at metaways.de>
+ */
+
+/**
+ * class to handle MeetingResponse request
+ *
+ * @package     Model
+ * @property    int     userResponse
+ * @property    string  collectionId
+ * @property    string  calendarId
+ * @property    string  requestId
+ * @property    string  instanceId
+ * @property    string  longId
+ */
+
+class Syncroton_Model_MeetingResponse extends Syncroton_Model_AEntry
+{
+    protected $_xmlBaseElement = 'Request';
+    
+    /**
+     * attendee status
+     */
+    const RESPONSE_ACCEPTED  = 1;
+    const RESPONSE_TENTATIVE = 2;
+    const RESPONSE_DECLINED  = 3;
+    
+    protected $_properties = array(
+        'MeetingResponse' => array(
+            'userResponse'  => array('type' => 'number'),
+            'collectionId'  => array('type' => 'string'),
+            'calendarId'    => array('type' => 'string'),
+            'requestId'     => array('type' => 'string'),
+            'instanceId'    => array('type' => 'datetime'),
+        ),
+        'Search' => array(
+            'longId'        => array('type' => 'string')
+        )
+    );
+}
\ No newline at end of file
diff --git a/lib/ext/Syncroton/Model/SyncCollection.php b/lib/ext/Syncroton/Model/SyncCollection.php
index 736c582..220226b 100644
--- a/lib/ext/Syncroton/Model/SyncCollection.php
+++ b/lib/ext/Syncroton/Model/SyncCollection.php
@@ -20,19 +20,40 @@
  * @property    int     windowSize
  */
 
-class Syncroton_Model_SyncCollection
+class Syncroton_Model_SyncCollection extends Syncroton_Model_AEntry
 {
-    protected $_collection = array();
+    protected $_elements = array(
+        'syncState' => null,
+        'folder'    => null
+    );
     
     protected $_xmlCollection;
     
+    protected $_xmlBaseElement = 'Collection';
+    
     public function __construct($properties = null)
     {
         if ($properties instanceof SimpleXMLElement) {
             $this->setFromSimpleXMLElement($properties);
         } elseif (is_array($properties)) {
             $this->setFromArray($properties);
-        }
+        }
+        
+        if (!isset($this->_elements['options'])) {
+            $this->_elements['options'] = array();
+        }
+        if (!isset($this->_elements['options']['filterType'])) {
+            $this->_elements['options']['filterType'] = Syncroton_Command_Sync::FILTER_NOTHING;
+        }
+        if (!isset($this->_elements['options']['mimeSupport'])) {
+            $this->_elements['options']['mimeSupport'] = Syncroton_Command_Sync::MIMESUPPORT_DONT_SEND_MIME;
+        }
+        if (!isset($this->_elements['options']['mimeTruncation'])) {
+            $this->_elements['options']['mimeTruncation'] = Syncroton_Command_Sync::TRUNCATE_NOTHING;
+        }
+            if (!isset($this->_elements['options']['bodyPreferences'])) {
+            $this->_elements['options']['bodyPreferences'] = array();
+        }
     }
     
     /**
@@ -100,7 +121,7 @@ class Syncroton_Model_SyncCollection
     public function hasClientAdds()
     {
         if (! $this->_xmlCollection instanceof SimpleXMLElement) {
-            throw new InvalidArgumentException('no collection xml element set');
+            return false;
         }
         
         return isset($this->_xmlCollection->Commands->Add);
@@ -115,7 +136,7 @@ class Syncroton_Model_SyncCollection
     public function hasClientChanges()
     {
         if (! $this->_xmlCollection instanceof SimpleXMLElement) {
-            throw new InvalidArgumentException('no collection xml element set');
+            return false;
         }
         
         return isset($this->_xmlCollection->Commands->Change);
@@ -130,7 +151,7 @@ class Syncroton_Model_SyncCollection
     public function hasClientDeletes()
     {
         if (! $this->_xmlCollection instanceof SimpleXMLElement) {
-            throw new InvalidArgumentException('no collection xml element set');
+            return false;
         }
         
         return isset($this->_xmlCollection->Commands->Delete);
@@ -145,97 +166,113 @@ class Syncroton_Model_SyncCollection
     public function hasClientFetches()
     {
         if (! $this->_xmlCollection instanceof SimpleXMLElement) {
-            throw new InvalidArgumentException('no collection xml element set');
+            return false;
         }
         
         return isset($this->_xmlCollection->Commands->Fetch);
     }
     
-    public function setFromArray(array $properties)
-    {
-        $this->_collection = array('options' => array(
-            'filterType'      => Syncroton_Command_Sync::FILTER_NOTHING,
-            'mimeSupport'     => Syncroton_Command_Sync::MIMESUPPORT_DONT_SEND_MIME,
-            'mimeTruncation'  => Syncroton_Command_Sync::TRUNCATE_NOTHING,
-            'bodyPreferences' => array()
-        ));
-    
-        foreach($properties as $key => $value) {
-            try {
-                $this->$key = $value; //echo __LINE__ . PHP_EOL;
-            } catch (InvalidArgumentException $iae) {
-                //ignore invalid properties
-                //echo __LINE__ . PHP_EOL;
-            }
-        }
-    }
-    
     /**
+     * this functions does not only set from SimpleXMLElement but also does merge from SimpleXMLElement
+     * to support partial sync requests
      * 
-     * @param SimpleXMLElement $xmlCollection
+     * @param SimpleXMLElement $properties
      * @throws InvalidArgumentException
      */
-    public function setFromSimpleXMLElement(SimpleXMLElement $xmlCollection)
+    public function setFromSimpleXMLElement(SimpleXMLElement $properties)
     {
-        if ($xmlCollection->getName() !== 'Collection') {
-            throw new InvalidArgumentException('Unexpected element name: ' . $xmlCollection->getName());
+        if (!in_array($properties->getName(), (array) $this->_xmlBaseElement)) {
+            throw new InvalidArgumentException('Unexpected element name: ' . $properties->getName());
         }
         
-        $this->_xmlCollection = $xmlCollection;
+        $this->_xmlCollection = $properties;
         
-        $this->_collection = array(
-            'syncKey'          => (int)$xmlCollection->SyncKey,
-            'collectionId'     => (string)$xmlCollection->CollectionId,
-            'deletesAsMoves'   => isset($xmlCollection->DeletesAsMoves)   && (string)$xmlCollection->DeletesAsMoves   === '0' ? false : true,
-            'conversationMode' => isset($xmlCollection->ConversationMode) && (string)$xmlCollection->ConversationMode === '0' ? false : true,
-            'getChanges'       => isset($xmlCollection->GetChanges) && (string) $xmlCollection->GetChanges === '0' ? false : true,
-            'windowSize'       => isset($xmlCollection->WindowSize) ? (int)$xmlCollection->WindowSize : 100,
-            'class'            => isset($xmlCollection->Class) ? (string)$xmlCollection->Class : null,
-            'options'          => array(
-                'filterType'      => Syncroton_Command_Sync::FILTER_NOTHING,
-                'mimeSupport'     => Syncroton_Command_Sync::MIMESUPPORT_DONT_SEND_MIME,
-                'mimeTruncation'  => Syncroton_Command_Sync::TRUNCATE_NOTHING,
-                'bodyPreferences' => array()
-            ),
-            
-            'syncState'        => null,
-            'folder'           => null
-        );
+        if (isset($properties->CollectionId)) {
+            $this->_elements['collectionId'] = (string)$properties->CollectionId;
+        }
+        
+        if (isset($properties->SyncKey)) {
+            $this->_elements['syncKey'] = (int)$properties->SyncKey;
+        }
+        
+        if (isset($properties->Class)) {
+            $this->_elements['class'] = (string)$properties->Class;
+        } elseif (!array_key_exists('class', $this->_elements)) {
+            $this->_elements['class'] = null;
+        }
         
-        if (isset($xmlCollection->Supported)) {
-            // @todo collected supported elements
+        if (isset($properties->WindowSize)) {
+            $this->_elements['windowSize'] = (string)$properties->WindowSize;
+        } elseif (!array_key_exists('windowSize', $this->_elements)) {
+            $this->_elements['windowSize'] = 100;
+        }
+        
+        if (isset($properties->DeletesAsMoves)) {
+            if ((string)$properties->DeletesAsMoves === '0') {
+                $this->_elements['deletesAsMoves'] = false;
+            } else {
+                $this->_elements['deletesAsMoves'] = true;
+            }
+        } elseif (!array_key_exists('deletesAsMoves', $this->_elements)) {
+            $this->_elements['deletesAsMoves'] = true;
+        }
+        
+        if (isset($properties->ConversationMode)) {
+            if ((string)$properties->ConversationMode === '0') {
+                $this->_elements['conversationMode'] = false;
+            } else {
+                $this->_elements['conversationMode'] = true;
+            }
+        } elseif (!array_key_exists('conversationMode', $this->_elements)) {
+            $this->_elements['conversationMode'] = true;
+        }
+        
+        if (isset($properties->GetChanges)) {
+            if ((string)$properties->GetChanges === '0') {
+                $this->_elements['getChanges'] = false;
+            } else {
+                $this->_elements['getChanges'] = true;
+            }
+        } elseif (!array_key_exists('getChanges', $this->_elements)) {
+            $this->_elements['getChanges'] = true;
+        }
+        
+        if (isset($properties->Supported)) {
+            // @todo collect supported elements
         }
         
         // process options
-        if (isset($xmlCollection->Options)) {
+        if (isset($properties->Options)) {
+            $this->_elements['options'] = array();
+            
             // optional parameters
-            if (isset($xmlCollection->Options->FilterType)) {
-                $this->_collection['options']['filterType'] = (int)$xmlCollection->Options->FilterType;
+            if (isset($properties->Options->FilterType)) {
+                $this->_elements['options']['filterType'] = (int)$properties->Options->FilterType;
             }
-            if (isset($xmlCollection->Options->MIMESupport)) {
-                $this->_collection['options']['mimeSupport'] = (int)$xmlCollection->Options->MIMESupport;
+            if (isset($properties->Options->MIMESupport)) {
+                $this->_elements['options']['mimeSupport'] = (int)$properties->Options->MIMESupport;
             }
-            if (isset($xmlCollection->Options->MIMETruncation)) {
-                $this->_collection['options']['mimeTruncation'] = (int)$xmlCollection->Options->MIMETruncation;
+            if (isset($properties->Options->MIMETruncation)) {
+                $this->_elements['options']['mimeTruncation'] = (int)$properties->Options->MIMETruncation;
             }
-            if (isset($xmlCollection->Options->Class)) {
-                $this->_collection['options']['class'] = (string)$xmlCollection->Options->Class;
+            if (isset($properties->Options->Class)) {
+                $this->_elements['options']['class'] = (string)$properties->Options->Class;
             }
             
             // try to fetch element from AirSyncBase:BodyPreference
-            $airSyncBase = $xmlCollection->Options->children('uri:AirSyncBase');
+            $airSyncBase = $properties->Options->children('uri:AirSyncBase');
             
             if (isset($airSyncBase->BodyPreference)) {
                 
                 foreach ($airSyncBase->BodyPreference as $bodyPreference) {
                     $type = (int) $bodyPreference->Type;
-                    $this->_collection['options']['bodyPreferences'][$type] = array(
+                    $this->_elements['options']['bodyPreferences'][$type] = array(
                         'type' => $type
                     );
                     
                     // optional
                     if (isset($bodyPreference->TruncationSize)) {
-                        $this->_collection['options']['bodyPreferences'][$type]['truncationSize'] = (int) $bodyPreference->TruncationSize;
+                        $this->_elements['options']['bodyPreferences'][$type]['truncationSize'] = (int) $bodyPreference->TruncationSize;
                     }
                 }
             }
@@ -246,27 +283,30 @@ class Syncroton_Model_SyncCollection
         }
     }
     
+    public function toArray()
+    {
+        $result = array();
+        
+        foreach (array('syncKey', 'collectionId', 'deletesAsMoves', 'conversationMode', 'getChanges', 'windowSize', 'class', 'options') as $key) {
+            if (isset($this->$key)) {
+                $result[$key] = $this->$key;
+            }
+        }
+        
+        return $result;
+    }
+    
     public function &__get($name)
     {
-        if (array_key_exists($name, $this->_collection)) {
-            return $this->_collection[$name];
+        if (array_key_exists($name, $this->_elements)) {
+            return $this->_elements[$name];
         }
-        //echo $name . PHP_EOL;
+        echo $name . PHP_EOL;
         return null; 
     }
     
     public function __set($name, $value)
     {
-        $this->_collection[$name] = $value;
-    }
-    
-    public function __isset($name)
-    {
-        return isset($this->_collection[$name]);
-    }
-    
-    public function __unset($name)
-    {
-        unset($this->_collection[$name]);
+        $this->_elements[$name] = $value;
     }
 }
\ No newline at end of file
diff --git a/lib/ext/Syncroton/Registry.php b/lib/ext/Syncroton/Registry.php
index 6903c5d..1495f8f 100644
--- a/lib/ext/Syncroton/Registry.php
+++ b/lib/ext/Syncroton/Registry.php
@@ -36,6 +36,8 @@ class Syncroton_Registry extends ArrayObject
     const GAL_DATA_CLASS      = 'gal_data_class';
     
     const DEFAULT_POLICY      = 'default_policy';
+    const PING_TIMEOUT        = 'ping_timeout';
+    const QUIET_TIME          = 'quiet_time';
     
     const DATABASE            = 'database';
     const TRANSACTIONMANAGER  = 'transactionmanager';
@@ -241,6 +243,22 @@ class Syncroton_Registry extends ArrayObject
     }
 
     /**
+     * return ping timeout 
+     * 
+     * sleep "ping timeout" seconds between folder checks in Ping and Sync command 
+     * 
+     * @return int
+     */
+    public static function getPingTimeout()
+    {
+        if (!self::isRegistered(self::PING_TIMEOUT)) {
+            return 60;
+        }
+        
+        return self::get(self::PING_TIMEOUT);
+    }
+    
+    /**
      * returns policy backend
      * 
      * creates Syncroton_Backend_Policy on the fly if not set before via
@@ -258,6 +276,22 @@ class Syncroton_Registry extends ArrayObject
     }
 
     /**
+     * return quiet time 
+     * 
+     * don't check folders if last sync was "quiet time" seconds ago 
+     * 
+     * @return int
+     */
+    public static function getQuietTime()
+    {
+        if (!self::isRegistered(self::QUIET_TIME)) {
+            return 180;
+        }
+        
+        return self::get(self::QUIET_TIME);
+    }
+    
+    /**
      * returns syncstate backend
      * 
      * creates Syncroton_Backend_SyncState on the fly if not before via
diff --git a/lib/ext/Syncroton/Server.php b/lib/ext/Syncroton/Server.php
index a9bc110..dc1064e 100644
--- a/lib/ext/Syncroton/Server.php
+++ b/lib/ext/Syncroton/Server.php
@@ -15,6 +15,11 @@
  */
 class Syncroton_Server
 {
+    const PARAMETER_ATTACHMENTNAME = 0;
+    const PARAMETER_COLLECTIONID   = 1;
+    const PARAMETER_ITEMID         = 3;
+    const PARAMETER_OPTIONS        = 7;
+    
     protected $_body;
     
     /**
@@ -71,16 +76,25 @@ class Syncroton_Server
     }
     
     /**
-    * handle options request
-    *
-    */
+     * handle options request
+     */
     protected function _handleOptions()
     {
         $command = new Syncroton_Command_Options();
     
-        $command->getResponse();
+        $this->_sendHeaders($command->getHeaders());
     }
     
+    protected function _sendHeaders(array $headers)
+    {
+        foreach ($headers as $name => $value) {
+            header($name . ': ' . $value);
+        }
+    } 
+    
+    /**
+     * handle post request
+     */
     protected function _handlePost()
     {
         $requestParameters = $this->_getRequestParameters($this->_request);
@@ -118,9 +132,7 @@ class Syncroton_Server
             $requestBody = $this->_body;
         }
         
-        if (PHP_SAPI !== 'cli') {
-            header("MS-Server-ActiveSync: 14.00.0536.000");
-        }
+        header("MS-Server-ActiveSync: 14.00.0536.000");
 
         try {
             $command = new $className($requestBody, $device, $requestParameters);
@@ -142,8 +154,6 @@ class Syncroton_Server
                 return;
             }
             
-            
-            
         } catch (Exception $e) {
             if ($this->_logger instanceof Zend_Log)
                 $this->_logger->crit(__METHOD__ . '::' . __LINE__ . " unexpected exception occured: " . get_class($e));
@@ -161,22 +171,61 @@ class Syncroton_Server
             if ($this->_logger instanceof Zend_Log) {
                 $response->formatOutput = true;
                 $this->_logger->debug(__METHOD__ . '::' . __LINE__ . " xml response:\n" . $response->saveXML());
+                $response->formatOutput = false;
             }
-        
+            
+            if (isset($command) && $command instanceof Syncroton_Command_ICommand) {
+                $this->_sendHeaders($command->getHeaders());
+            }
+            
             $outputStream = fopen("php://temp", 'r+');
-        
+            
             $encoder = new Syncroton_Wbxml_Encoder($outputStream, 'UTF-8', 3);
             $encoder->encode($response);
-
-            // avoid sending headers while running on cli (phpunit)
-            if (PHP_SAPI !== 'cli') {
-                header("Content-Type: application/vnd.ms-sync.wbxml");
+            
+            if ($requestParameters['acceptMultipart'] == true) {
+                $parts = $command->getParts();
+                
+                // output multipartheader
+                $bodyPartCount = 1 + count($parts);
+                
+                // number of parts (4 bytes)
+                $header  = pack('i', $bodyPartCount);
+                
+                $partOffset = 4 + (($bodyPartCount * 2) * 4);
+                
+                // wbxml body start and length
+                $streamStat = fstat($outputStream);
+                $header .= pack('ii', $partOffset, $streamStat['size']);
+                
+                $partOffset += $streamStat['size'];
+                
+                // calculate start and length of parts
+                foreach ($parts as $partId => $partStream) {
+                    rewind($partStream);
+                    $streamStat = fstat($partStream);
+                    
+                    // part start and length
+                    $header .= pack('ii', $partOffset, $streamStat['size']);
+                    $partOffset += $streamStat['size'];
+                }
+                
+                echo $header;
             }
-        
+                        
+            // output body
             rewind($outputStream);
             fpassthru($outputStream);
+            
+            // output multiparts
+            if (isset($parts)) {
+                foreach ($parts as $partStream) {
+                    rewind($partStream);
+                    fpassthru($partStream);
+                }
+            }
         }
-    }
+    }    
     
     /**
      * return request params
@@ -185,7 +234,7 @@ class Syncroton_Server
      */
     protected function _getRequestParameters(Zend_Controller_Request_Http $request)
     {
-        if (!isset($_GET['DeviceId'])) {
+        if (strpos($request->getRequestUri(), '&') === false) {
             $commands = array(
                 0  => 'Sync',
                 1  => 'SendMail',
@@ -208,15 +257,8 @@ class Syncroton_Server
                 22 => 'ValidateCert'
             );
             
-            // find the first $_GET parameter which has a key but no value
-            // the key are the request parameters
-            foreach ($_GET as $key => $value) {
-                if (empty($value)) {
-                    $requestParameters = $key;
-                    break;
-                }
-            }
-            
+            $requestParameters = substr($request->getRequestUri(), strpos($request->getRequestUri(), '?'));
+
             $stream = fopen("php://temp", 'r+');
             fwrite($stream, base64_decode($requestParameters));
             rewind($stream);
@@ -224,14 +266,17 @@ class Syncroton_Server
             // unpack the first 4 bytes
             $unpacked = unpack('CprotocolVersion/Ccommand/vlocale', fread($stream, 4));
             
-            $protocolVersion = $unpacked['protocolVersion'];
+            // 140 => 14.0
+            $protocolVersion = substr($unpacked['protocolVersion'], 0, -1) . '.' . substr($unpacked['protocolVersion'], -1);
             $command = $commands[$unpacked['command']];
             $locale = $unpacked['locale'];
             
             // unpack deviceId
             $length = ord(fread($stream, 1));
             if ($length > 0) {
-                $unpacked = unpack('H' . $length*2 . 'string', fread($stream, $length));
+                $toUnpack = fread($stream, $length);
+                
+                $unpacked = unpack("H" . ($length * 2) . "string", $toUnpack);
                 $deviceId = $unpacked['string'];
             }
             
@@ -241,7 +286,7 @@ class Syncroton_Server
                 $unpacked  = unpack('Vstring', fread($stream, $length));
                 $policyKey = $unpacked['string'];
             }
-
+            
             // unpack device type
             $length = ord(fread($stream, 1));
             if ($length > 0) {
@@ -252,27 +297,27 @@ class Syncroton_Server
             while (! feof($stream)) {
                 $tag    = ord(fread($stream, 1));
                 $length = ord(fread($stream, 1));
-                
+
                 switch ($tag) {
-                    case 0:
+                    case self::PARAMETER_ATTACHMENTNAME:
                         $unpacked = unpack('A' . $length . 'string', fread($stream, $length));
                         
                         $attachmentName = $unpacked['string'];
                         break;
                         
-                    case 1:
+                    case self::PARAMETER_COLLECTIONID:
                         $unpacked = unpack('A' . $length . 'string', fread($stream, $length));
                         
                         $collectionId = $unpacked['string'];
                         break;
                         
-                    case 3:
+                    case self::PARAMETER_ITEMID:
                         $unpacked = unpack('A' . $length . 'string', fread($stream, $length));
                         
                         $itemId = $unpacked['string'];
                         break;
                         
-                    case 7:
+                    case self::PARAMETER_OPTIONS:
                         $options = ord(fread($stream, 1));
                         
                         $saveInSent      = !!($options & 0x01); 
@@ -290,12 +335,13 @@ class Syncroton_Server
                 'protocolVersion' => $protocolVersion,
                 'command'         => $command,
                 'deviceId'        => $deviceId,
-                'deviceType'      => isset($deviceType)     ? $deviceType     : null,
-                'policyKey'       => isset($policyKey)      ? $policyKey      : null,
-                'saveInSent'      => isset($saveInSent)     ? $saveInSent     : false,
-                'collectionId'    => isset($collectionId)   ? $collectionId   : null,
-                'itemId'          => isset($itemId)         ? $itemId         : null,
-                'attachmentName'  => isset($attachmentName) ? $attachmentName : null
+                'deviceType'      => isset($deviceType)      ? $deviceType      : null,
+                'policyKey'       => isset($policyKey)       ? $policyKey       : null,
+                'saveInSent'      => isset($saveInSent)      ? $saveInSent      : false,
+                'collectionId'    => isset($collectionId)    ? $collectionId    : null,
+                'itemId'          => isset($itemId)          ? $itemId          : null,
+                'attachmentName'  => isset($attachmentName)  ? $attachmentName  : null,
+                'acceptMultipart' => isset($acceptMultiPart) ? $acceptMultiPart : false
             );
         } else {
             $result = array(
@@ -307,7 +353,8 @@ class Syncroton_Server
                 'saveInSent'      => $request->getQuery('SaveInSent') == 'T',
                 'collectionId'    => $request->getQuery('CollectionId'),
                 'itemId'          => $request->getQuery('ItemId'),
-                'attachmentName'  => $request->getQuery('AttachmentName'),
+                'attachmentName'  => $request->getQuery('AttachmentName'),
+                'acceptMultipart' => $request->getServer('HTTP_MS_ASACCEPTMULTIPART') == 'T'
             );
         }
         
diff --git a/lib/ext/Syncroton/Wbxml/Abstract.php b/lib/ext/Syncroton/Wbxml/Abstract.php
index 6c86806..7fdd653 100644
--- a/lib/ext/Syncroton/Wbxml/Abstract.php
+++ b/lib/ext/Syncroton/Wbxml/Abstract.php
@@ -111,10 +111,12 @@ abstract class Syncroton_Wbxml_Abstract
      */
     public function getDPI($_uInt = 0)
     {
-        if(($dpi = @constant('Syncroton_Wbxml_Abstract::DPI_' . $_uInt)) === NULL) {
+        if(!defined('Syncroton_Wbxml_Abstract::DPI_' . $_uInt)) {
             throw new Syncroton_Wbxml_Exception('unknown wellknown identifier: ' . $_uInt);
         }
-
+        
+        $dpi = constant('Syncroton_Wbxml_Abstract::DPI_' . $_uInt);
+        
         return $dpi;
     }
     
diff --git a/lib/kolab_sync.php b/lib/kolab_sync.php
index 03001b7..9316b7a 100644
--- a/lib/kolab_sync.php
+++ b/lib/kolab_sync.php
@@ -43,7 +43,7 @@ class kolab_sync extends rcube
     public $user;
 
     const CHARSET = 'UTF-8';
-    const VERSION = "2.0";
+    const VERSION = "2.1";
 
 
     /**
@@ -140,7 +140,7 @@ class kolab_sync extends rcube
         Syncroton_Registry::set(Syncroton_Registry::FOLDERBACKEND,       new kolab_sync_backend_folder);
         Syncroton_Registry::set(Syncroton_Registry::SYNCSTATEBACKEND,    new kolab_sync_backend_state);
         Syncroton_Registry::set(Syncroton_Registry::CONTENTSTATEBACKEND, new kolab_sync_backend_content);
-        Syncroton_Registry::set(Syncroton_Registry::POLICYBACKEND, new kolab_sync_backend_policy);
+        Syncroton_Registry::set(Syncroton_Registry::POLICYBACKEND,       new kolab_sync_backend_policy);
 
         Syncroton_Registry::setContactsDataClass('kolab_sync_data_contacts');
         Syncroton_Registry::setCalendarDataClass('kolab_sync_data_calendar');
@@ -148,6 +148,10 @@ class kolab_sync extends rcube
         Syncroton_Registry::setTasksDataClass('kolab_sync_data_tasks');
         Syncroton_Registry::setGALDataClass('kolab_sync_data_gal');
 
+        // Configuration
+        Syncroton_Registry::set(Syncroton_Registry::PING_TIMEOUT, $this->config->get('activesync_ping_timeout', 60));
+        Syncroton_Registry::set(Syncroton_Registry::QUIET_TIME,   $this->config->get('activesync_quiet_time', 180));
+
         // Run Syncroton
         $syncroton = new Syncroton_Server($userid);
         $syncroton->handle();
diff --git a/lib/kolab_sync_data_calendar.php b/lib/kolab_sync_data_calendar.php
index 7cf2ae2..a0c828b 100644
--- a/lib/kolab_sync_data_calendar.php
+++ b/lib/kolab_sync_data_calendar.php
@@ -26,7 +26,7 @@
 /**
  * Calendar (Events) data class for Syncroton
  */
-class kolab_sync_data_calendar extends kolab_sync_data
+class kolab_sync_data_calendar extends kolab_sync_data implements Syncroton_Data_IDataCalendar
 {
     /**
      * Mapping from ActiveSync Calendar namespace fields
@@ -472,6 +472,18 @@ class kolab_sync_data_calendar extends kolab_sync_data
         return $event;
     }
 
+    /**
+     * Set attendee status for meeting
+     *
+     * @param Syncroton_Model_MeetingResponse $request The meeting response
+     *
+     * @return string ID of new calendar entry
+     */
+    public function setAttendeeStatus(Syncroton_Model_MeetingResponse $request)
+    {
+        // @TODO: not implemented
+        throw new Syncroton_Exception_Status_MeetingResponse(Syncroton_Exception_Status_MeetingMeeting::MEETING_ERROR);
+    }
 
     /**
      * Returns filter query array according to specified ActiveSync FilterType
@@ -508,7 +520,6 @@ class kolab_sync_data_calendar extends kolab_sync_data
         return $filter;
     }
 
-
     /**
      * Converts libkolab alarms string into number of minutes
      */


commit 59e4d286e2f7cd05774e5bc2e46090eabdffdea1
Author: Aleksander Machniak <alec at alec.pl>
Date:   Sat Jan 12 16:34:10 2013 +0100

    Fix GAL search for records with 'email:*', but not 'email' fields

diff --git a/lib/kolab_sync_data_gal.php b/lib/kolab_sync_data_gal.php
index ed47c46..95e3e17 100644
--- a/lib/kolab_sync_data_gal.php
+++ b/lib/kolab_sync_data_gal.php
@@ -222,6 +222,10 @@ class kolab_sync_data_gal extends kolab_sync_data implements Syncroton_Data_IDat
 
             while ($row = $result->next()) {
                 $row['sourceid'] = $idx;
+
+                // make sure 'email' item is there, convert all email:* into one
+                $row['email'] = $book->get_col_values('email', $row, true);
+
                 $key = $this->contact_key($row);
                 unset($row['_raw_attrib']); // save some memory, @TODO: do this in rcube_ldap
                 $records[$key] = $row;





More information about the commits mailing list