[CalendarServer-changes] [2115] CalendarServer/branches/users/cdaboo/server2server-2113

source_changes at macosforge.org source_changes at macosforge.org
Sun Feb 3 11:53:20 PST 2008


Revision: 2115
          http://trac.macosforge.org/projects/calendarserver/changeset/2115
Author:   cdaboo at apple.com
Date:     2008-02-03 11:53:18 -0800 (Sun, 03 Feb 2008)

Log Message:
-----------
Merge forward of trunk with last server-to-server branch.

Modified Paths:
--------------
    CalendarServer/branches/users/cdaboo/server2server-2113/conf/caldavd-test.plist
    CalendarServer/branches/users/cdaboo/server2server-2113/conf/caldavd.plist
    CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/__init__.py
    CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/config.py
    CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/customxml.py
    CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/directory/calendar.py
    CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/itip.py
    CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/logging.py
    CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/method/copymove.py
    CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/method/put.py
    CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/method/put_common.py
    CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/method/report_common.py
    CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/schedule.py
    CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/static.py
    CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/tap.py

Added Paths:
-----------
    CalendarServer/branches/users/cdaboo/server2server-2113/conf/servertoserver-test.xml
    CalendarServer/branches/users/cdaboo/server2server-2113/conf/servertoserver.dtd
    CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/freebusyurl.py
    CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/schedule_common.py
    CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/servertoserver.py
    CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/servertoserverparser.py

Modified: CalendarServer/branches/users/cdaboo/server2server-2113/conf/caldavd-test.plist
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2113/conf/caldavd-test.plist	2008-02-01 22:00:25 UTC (rev 2114)
+++ CalendarServer/branches/users/cdaboo/server2server-2113/conf/caldavd-test.plist	2008-02-03 19:53:18 UTC (rev 2115)
@@ -328,7 +328,34 @@
   <key>EnableNotifications</key>
   <true/>
 
+  <!-- Server to server protocol -->
+  <key>ServerToServer</key>
+  <dict>
+  	<key>Enabled</key>
+  	<true/>
+  	<key>Email Domain</key>
+  	<string>example.com</string>
+  	<key>HTTP Domain</key>
+  	<string>example.com</string>
+  	<key>Local Addresses</key>
+  	<array>
+  	</array>
+  	<key>Remote Addresses</key>
+  	<array>
+  	</array>
+  	<key>Servers</key>
+  	<string>conf/servertoserver-test.xml</string>
+  </dict>
 
+  <!-- Free-busy URL protocol -->
+  <key>FreeBusyURL</key>
+  <dict>
+  	<key>Enabled</key>
+  	<true/>
+  	<key>Time Period</key>
+  	<integer>14</integer>
+  </dict>
+
   <!--
     Twisted
   -->

Modified: CalendarServer/branches/users/cdaboo/server2server-2113/conf/caldavd.plist
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2113/conf/caldavd.plist	2008-02-01 22:00:25 UTC (rev 2114)
+++ CalendarServer/branches/users/cdaboo/server2server-2113/conf/caldavd.plist	2008-02-03 19:53:18 UTC (rev 2115)
@@ -264,6 +264,33 @@
   <key>EnableNotifications</key>
   <true/>
 
+  <!-- Server to server protocol -->
+  <key>ServerToServer</key>
+  <dict>
+  	<key>Enabled</key>
+  	<true/>
+  	<key>Email Domain</key>
+  	<string></string>
+  	<key>HTTP Domain</key>
+  	<string></string>
+  	<key>Local Addresses</key>
+  	<array>
+  	</array>
+  	<key>Remote Addresses</key>
+  	<array>
+  	</array>
+  	<key>Servers</key>
+  	<string>/etc/caldavd/servertoserver.xml</string>
+  </dict>
 
+  <!-- Free-busy URL protocol -->
+  <key>FreeBusyURL</key>
+  <dict>
+  	<key>Enabled</key>
+  	<true/>
+  	<key>Time Period</key>
+  	<integer>14</integer>
+  </dict>
+
 </dict>
 </plist>

Copied: CalendarServer/branches/users/cdaboo/server2server-2113/conf/servertoserver-test.xml (from rev 2114, CalendarServer/branches/users/cdaboo/server2server-1965/conf/servertoserver-test.xml)
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2113/conf/servertoserver-test.xml	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/server2server-2113/conf/servertoserver-test.xml	2008-02-03 19:53:18 UTC (rev 2115)
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="utf-8"?>
+
+<!--
+Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+ -->
+
+<!DOCTYPE servers SYSTEM "servertoserver.dtd">
+
+<servers>
+  <server>
+    <uri>https://localhost:8543/inbox</uri>
+    <allow-requests-from/>
+    <allow-requests-to/>
+    <domains>
+    	<domain>example.org</domain>
+    </domains>
+  </server>
+</servers>

Copied: CalendarServer/branches/users/cdaboo/server2server-2113/conf/servertoserver.dtd (from rev 2114, CalendarServer/branches/users/cdaboo/server2server-1965/conf/servertoserver.dtd)
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2113/conf/servertoserver.dtd	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/server2server-2113/conf/servertoserver.dtd	2008-02-03 19:53:18 UTC (rev 2115)
@@ -0,0 +1,33 @@
+<!--
+Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+
+DRI: Cyrus Daboo, cdaboo at apple.com
+ -->
+
+<!ELEMENT servers (server*) >
+
+	<!ELEMENT server (uri, authentication?, allow-requests-from, allow-requests-to, domains*) >
+
+		<!ELEMENT uri (#PCDATA) >
+		<!ELEMENT authentication (user, password) >
+		    <!ATTLIST authentication type (basic) "">
+		    <!ELEMENT user (#PCDATA) >
+		    <!ELEMENT password (#PCDATA) >
+
+		<!ELEMENT allow-requests-from EMPTY >
+		<!ELEMENT allow-requests-to EMPTY >
+		<!ELEMENT domains (domain*) >
+
+			<!ELEMENT domain (#PCDATA) >

Modified: CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/__init__.py	2008-02-01 22:00:25 UTC (rev 2114)
+++ CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/__init__.py	2008-02-03 19:53:18 UTC (rev 2115)
@@ -31,6 +31,7 @@
     "directory",
     "dropbox",
     "extensions",
+    "freebusyurl",
     "ical",
     "icaldav",
     "index",
@@ -42,8 +43,12 @@
     "resource",
     "root",
     "schedule",
+    "schedule_common",
+    "servertoserver",
+    "servertoserverparser",
     "sql",
     "static",
+    "timezones",
 ]
 
 try:

Modified: CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/config.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/config.py	2008-02-01 22:00:25 UTC (rev 2114)
+++ CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/config.py	2008-02-03 19:53:18 UTC (rev 2115)
@@ -143,9 +143,22 @@
     #
     # Non-standard CalDAV extensions
     #
-    "EnableDropBox"      : False, # Calendar Drop Box
-    "EnableNotifications": False, # Drop Box Notifications
+    "EnableDropBox"       : False, # Calendar Drop Box
+    "EnableNotifications" : False, # Drop Box Notifications
+    
+    "ServerToServer": {
+        "Enabled"          : False, # Server-to-server protocol
+        "Email Domain"     : "",    # Domain for mailto calendar user addresses on this server
+        "HTTP Domain"      : "",    # Domain for http calendar user addresses on this server
+        "Local Addresses"  : [],    # Reg-ex patterns to match local calendar user addresses
+        "Remote Addresses" : [],    # Reg-ex patterns to match remote calendar user addresses
+    },
 
+    "FreeBusyURL": {
+        "Enabled"         : False, # Per-user free-busy-url protocol
+        "Time Period"     : 14,    # Number of days into the future to generate f-b data if no explicit time-range is specified
+    },
+
     #
     # Implementation details
     #
@@ -178,6 +191,9 @@
 }
 
 class Config (object):
+    """
+    @DynamicAttrs
+    """
     def __init__(self, defaults):
         self.setDefaults(defaults)
         self._data = copy.deepcopy(self._defaults)

Modified: CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/customxml.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/customxml.py	2008-02-01 22:00:25 UTC (rev 2114)
+++ CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/customxml.py	2008-02-03 19:53:18 UTC (rev 2115)
@@ -256,6 +256,22 @@
 
         return found
 
+class ServerToServerInbox (davxml.WebDAVEmptyElement):
+    """
+    Denotes the resourcetype of a server-to-server Inbox.
+    (CalDAV-s2s-xx, section x.x.x)
+    """
+    namespace = calendarserver_namespace
+    name = "server-to-server-inbox"
+
+class FreeBusyURL (davxml.WebDAVEmptyElement):
+    """
+    Denotes the resourcetype of a free-busy URL resource.
+    (CalDAV-s2s-xx, section x.x.x)
+    """
+    namespace = calendarserver_namespace
+    name = "free-busy-url"
+
 ##
 # Extensions to davxml.ResourceType
 ##
@@ -265,3 +281,5 @@
 davxml.ResourceType.notifications = davxml.ResourceType(davxml.Collection(), Notifications())
 davxml.ResourceType.calendarproxyread = davxml.ResourceType(davxml.Principal(), davxml.Collection(), CalendarProxyRead())
 davxml.ResourceType.calendarproxywrite = davxml.ResourceType(davxml.Principal(), davxml.Collection(), CalendarProxyWrite())
+davxml.ResourceType.servertoserverinbox = davxml.ResourceType(ServerToServerInbox())
+davxml.ResourceType.freebusyurl = davxml.ResourceType(FreeBusyURL())

Modified: CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/directory/calendar.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/directory/calendar.py	2008-02-01 22:00:25 UTC (rev 2114)
+++ CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/directory/calendar.py	2008-02-03 19:53:18 UTC (rev 2115)
@@ -32,6 +32,7 @@
 from twistedcaldav.config import config
 from twistedcaldav.dropbox import DropBoxHomeResource
 from twistedcaldav.extensions import ReadOnlyResourceMixIn, DAVResource
+from twistedcaldav.freebusyurl import FreeBusyURLResource
 from twistedcaldav.notifications import NotificationsCollectionResource
 from twistedcaldav.resource import CalDAVResource
 from twistedcaldav.schedule import ScheduleInboxResource, ScheduleOutboxResource
@@ -201,6 +202,10 @@
             childlist += (
                 ("notifications", NotificationsCollectionResource),
             )
+        if config.FreeBusyURL["Enabled"]:
+            childlist += (
+                ("freebusy", FreeBusyURLResource),
+            )
         for name, cls in childlist:
             child = self.provisionChild(name)
             assert isinstance(child, cls), "Child %r is not a %s: %r" % (name, cls.__name__, child)

Copied: CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/freebusyurl.py (from rev 2114, CalendarServer/branches/users/cdaboo/server2server-1965/twistedcaldav/freebusyurl.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/freebusyurl.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/freebusyurl.py	2008-02-03 19:53:18 UTC (rev 2115)
@@ -0,0 +1,235 @@
+##
+# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Free-busy-URL resources.
+"""
+
+__all__ = [
+    "FreeBusyURLResource",
+]
+
+from twisted.internet.defer import deferredGenerator
+from twisted.internet.defer import waitForDeferred
+from twisted.python import log
+from twisted.web2 import responsecode
+from twisted.web2.dav import davxml
+from twisted.web2.dav.http import ErrorResponse
+from twisted.web2.http import HTTPError
+from twisted.web2.http import Response
+from twisted.web2.http import StatusResponse
+from twisted.web2.http_headers import MimeType
+from twisted.web2.stream import MemoryStream
+
+from twistedcaldav import caldavxml
+from twistedcaldav.caldavxml import TimeRange
+from twistedcaldav.config import config
+from twistedcaldav.customxml import calendarserver_namespace
+from twistedcaldav.ical import Property
+from twistedcaldav.ical import parse_datetime
+from twistedcaldav.ical import parse_duration
+from twistedcaldav.resource import CalDAVResource
+from twistedcaldav.schedule_common import Scheduler
+
+from vobject.icalendar import utc
+
+import datetime
+
+class FreeBusyURLResource (CalDAVResource):
+    """
+    Free-busy URL resource.
+
+    Extends L{DAVResource} to provide free-busy URL functionality.
+    """
+
+    def __init__(self, parent):
+        """
+        @param parent: the parent resource of this one.
+        """
+        assert parent is not None
+
+        CalDAVResource.__init__(self, principalCollections=parent.principalCollections())
+
+        self.parent = parent
+
+    def defaultAccessControlList(self):
+        return davxml.ACL(
+            # DAV:Read, CalDAV:schedule for all principals (does not include anonymous)
+            davxml.ACE(
+                davxml.Principal(davxml.Authenticated()),
+                davxml.Grant(
+                    davxml.Privilege(davxml.Read()),
+                    davxml.Privilege(caldavxml.Schedule()),
+                ),
+                davxml.Protected(),
+            ),
+        )
+
+    def resourceType(self):
+        return davxml.ResourceType.freebusyurl
+
+    def isCollection(self):
+        return False
+
+    def isCalendarCollection(self):
+        return False
+
+    def isPseudoCalendarCollection(self):
+        return False
+
+    def render(self, request):
+        output = """<html>
+<head>
+<title>Free-Busy URL Resource</title>
+</head>
+<body>
+<h1>Free-busy URL Resource.</h1>
+</body
+</html>"""
+
+        response = Response(200, {}, output)
+        response.headers.setHeader("content-type", MimeType("text", "html"))
+        return response
+
+    def http_GET(self, request):
+        """
+        The free-busy URL POST method.
+        """
+        return self._processFBURL(request)
+
+    def http_POST(self, request):
+        """
+        The free-busy URL POST method.
+        """
+        return self._processFBURL(request)
+
+    @deferredGenerator
+    def _processFBURL(self, request):
+        
+        #
+        # Check authentication and access controls
+        #
+        x = waitForDeferred(self.authorize(request, (davxml.Read(),)))
+        yield x
+        x.getResult()
+        
+        # Extract query parameters from the URL
+        args = ('start', 'end', 'duration', 'token', 'format', 'user',)
+        for arg in args:
+            setattr(self, arg, request.args.get(arg, [None])[0])
+        
+        # Some things we do not handle
+        if self.token or self.user:
+            raise HTTPError(ErrorResponse(responsecode.NOT_ACCEPTABLE, (calendarserver_namespace, "supported-query-parameter")))
+        
+        # Check format
+        if self.format:
+            self.format = self.format.split(";")[0]
+            if self.format not in ("text/calendar", "text/plain"):
+                raise HTTPError(ErrorResponse(responsecode.NOT_ACCEPTABLE, (calendarserver_namespace, "supported-format")))
+        else:
+            self.format = "text/calendar"
+            
+        # Start/end/duration must be valid iCalendar DATE-TIME UTC or DURATION values
+        try:
+            if self.start:
+                self.start = parse_datetime(self.start)
+                if self.start.tzinfo != utc:
+                    raise ValueError()
+            if self.end:
+                self.end = parse_datetime(self.end)
+                if self.end.tzinfo != utc:
+                    raise ValueError()
+            if self.duration:
+                self.duration = parse_duration(self.duration)
+        except ValueError:
+            raise HTTPError(ErrorResponse(responsecode.BAD_REQUEST, (calendarserver_namespace, "valid-query-parameters")))
+        
+        # Sanity check start/end/duration
+
+        # End and duration cannot both be present
+        if self.end and self.duration:
+            raise HTTPError(ErrorResponse(responsecode.NOT_ACCEPTABLE, (calendarserver_namespace, "valid-query-parameters")))
+        
+        # Duration must be positive
+        if self.duration and self.duration.days < 0:
+            raise HTTPError(ErrorResponse(responsecode.BAD_REQUEST, (calendarserver_namespace, "valid-query-parameters")))
+        
+        # Now fill in the missing pieces
+        if self.start is None:
+            now = datetime.datetime.now()
+            self.start = now.replace(hour=0, minute=0, second=0, tzinfo=utc)
+        if self.duration:
+            self.end = self.start + self.duration
+        if self.end is None:
+            self.end = self.start + datetime.timedelta(days=config.FreeBusyURL["Time Period"])
+            
+        # End > start
+        if self.end <= self.start:
+            raise HTTPError(ErrorResponse(responsecode.BAD_REQUEST, (calendarserver_namespace, "valid-query-parameters")))
+        
+        # TODO: We should probably verify that the actual time-range is within sensible bounds (e.g. not too far in the past or future and not too long)
+        
+        # Now lookup the principal details for the targetted user
+        principal = self.parent.principalForRecord()
+        
+        # Pick the first mailto cu address or the first other type
+        cuaddr = None
+        for item in principal.calendarUserAddresses():
+            if cuaddr is None:
+                cuaddr = item
+            if item.startswith("mailto"):
+                cuaddr = item
+                break
+
+        # Get inbox details
+        inboxURL = principal.scheduleInboxURL()
+        if inboxURL is None:
+            raise HTTPError(StatusResponse(responsecode.INTERNAL_SERVER_ERROR, "No schedule inbox URL for principal: %s" % (principal,)))
+        try:
+            inbox = waitForDeferred(request.locateResource(inboxURL))
+            yield inbox
+            inbox = inbox.getResult()
+        except:
+            log.err("No schedule inbox for principal: %s" % (principal,))
+            inbox = None
+        if inbox is None:
+            raise HTTPError(StatusResponse(responsecode.INTERNAL_SERVER_ERROR, "No schedule inbox for principal: %s" % (principal,)))
+            
+        scheduler = Scheduler(request, self)
+        scheduler.timerange = TimeRange(start="20000101T000000Z", end="20070102T000000Z")
+        scheduler.timerange.start = self.start
+        scheduler.timerange.end = self.end
+        
+        scheduler.organizer = Scheduler.LocalCalendarUser(cuaddr, principal, inbox, inboxURL)
+        
+        attendeeProp = Property("ATTENDEE", scheduler.organizer.cuaddr)
+
+        d = waitForDeferred(scheduler.generateAttendeeFreeBusyResponse(
+            scheduler.organizer,
+            None,
+            None,
+            attendeeProp,
+            False,
+        ))
+        yield d
+        fbresult = d.getResult()
+        
+        response = Response()
+        response.stream = MemoryStream(str(fbresult))
+        response.headers.setHeader("content-type", MimeType.fromString("%s; charset=utf-8" % (self.format,)))
+    
+        yield response

Modified: CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/itip.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/itip.py	2008-02-01 22:00:25 UTC (rev 2114)
+++ CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/itip.py	2008-02-03 19:53:18 UTC (rev 2115)
@@ -46,961 +46,906 @@
 from twistedcaldav import logging
 from twistedcaldav.ical import Property, iCalendarProductID
 from twistedcaldav.method import report_common
-from twistedcaldav.method.put_common import storeCalendarObjectResource
+from twistedcaldav.method.put_common import StoreCalendarObjectResource
 from twistedcaldav.resource import isCalendarCollectionResource
 
 __version__ = "0.0"
 
 __all__ = [
-    "handleRequest",
-    "canAutoRespond",
+    "iTipProcessor",
 ]
 
 class iTipException(Exception):
     pass
 
-def handleRequest(request, principal, inbox, calendar, child):
-    """
-    Handle an iTIP response automatically using a deferredGenerator.
-
-    @param request: the L{twisted.web2.server.Request} for the current request.
-    @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
-    @param inbox: the L{ScheduleInboxFile} for the principal's Inbox.
-    @param calendar: the L{Component} for the iTIP message we are processing.
-    @param child: the L{CalDAVFile} for the iTIP message resource already saved to the Inbox.
-    @return: L{Deferred} that is a L{deferredGenerator}
-    """
+class iTipProcessor(object):
     
-    method = calendar.propertyValue("METHOD")
-    if method == "REQUEST":
-        f = processRequest
-    elif method == "ADD":
-        f = processAdd
-    elif method == "CANCEL":
-        f = processCancel
+    def handleRequest(self, request, principal, inbox, calendar, child):
+        """
+        Handle an iTIP response automatically using a deferredGenerator.
+    
+        @param request: the L{twisted.web2.server.Request} for the current request.
+        @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
+        @param inbox: the L{ScheduleInboxFile} for the principal's Inbox.
+        @param calendar: the L{Component} for the iTIP message we are processing.
+        @param child: the L{CalDAVFile} for the iTIP message resource already saved to the Inbox.
+        @return: L{Deferred} that is a L{deferredGenerator}
+        """
+        
+        method = calendar.propertyValue("METHOD")
+        if method == "REQUEST":
+            f = self.processRequest
+        elif method == "ADD":
+            f = self.processAdd
+        elif method == "CANCEL":
+            f = self.processCancel
 
-    return f(request, principal, inbox, calendar, child)
+        self.request = request
+        self.principal = principal
+        self.inbox = inbox
+        self.calendar = calendar
+        self.child = child
+        if self.child:
+            self.childname = self.child.fp.basename()
+        else:
+            self.childname = ""
+ 
+        return f()
 
-def processRequest(request, principal, inbox, calendar, child):
-    """
-    Process a METHOD=REQUEST.
-    This is a deferredGenerator function so use yield whenever we have a deferred.
-
-    Steps:
+    @deferredGenerator
+    def processRequest(self):
+        """
+        Process a METHOD=REQUEST.
+        This is a deferredGenerator function so use yield whenever we have a deferred.
     
-      1. See if this updates existing ones in Inbox.
-          1. If so,
-              1. Remove existing ones in Inbox.
-              2. See if this updates existing ones in free-busy-set calendars.
-              3. Remove existing ones in those calendars.
-              4. See if this fits into a free slot:
-                  1. If not, send REPLY with failure status
-                  2. If so
-                      1. send REPLY with success
-                      2. add to f-b-s calendar
-          2. If not,
-              1. remove the one we got - its 'stale'
-          3. Delete the request from the Inbox.
+        Steps:
+        
+          1. See if this updates existing ones in Inbox.
+              1. If so,
+                  1. Remove existing ones in Inbox.
+                  2. See if this updates existing ones in free-busy-set calendars.
+                  3. Remove existing ones in those calendars.
+                  4. See if this fits into a free slot:
+                      1. If not, send REPLY with failure status
+                      2. If so
+                          1. send REPLY with success
+                          2. add to f-b-s calendar
+              2. If not,
+                  1. remove the one we got - its 'stale'
+              3. Delete the request from the Inbox.
+        
+        """
+        
+        logging.info("Auto-processing iTIP REQUEST for: %s" % (str(self.principal),), system="iTIP")
+        processed = "ignored"
     
-    @param request: the L{twisted.web2.server.Request} for the current request.
-    @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
-    @param inbox: the L{ScheduleInboxFile} for the principal's Inbox.
-    @param calendar: the L{Component} for the iTIP message we are processing.
-    @param child: the L{CalDAVFile} for the iTIP message resource already saved to the Inbox.
-    """
+        # First determine whether this is a full or partial update. A full update is one containing the master
+        # component in a recurrence set (or non-recurring event). Partial is one where overridden instances only are
+        # being changed.
+        
+        new_master = self.calendar.masterComponent()
     
-    logging.info("Auto-processing iTIP REQUEST for: %s" % (str(principal),), system="iTIP")
-    processed = "ignored"
-
-    # First determine whether this is a full or partial update. A full update is one containing the master
-    # component in a recurrence set (or non-recurring event). Partial is one where overridden instances only are
-    # being changed.
-    
-    new_master = calendar.masterComponent()
-
-    # Next we want to try and find a match to any components on existing calendars listed as contributing
-    # to free-busy as we will need to update those with the new one.
-    d = waitForDeferred(findCalendarMatch(request, principal, calendar))
-    yield d
-    calmatch, updatecal, calURL = d.getResult()
-    
-    if new_master:
-        # So we have a full update. That means we need to delete any existing events completely and
-        # replace with the ones provided so long as the new one is newer.
+        # Next we want to try and find a match to any components on existing calendars listed as contributing
+        # to free-busy as we will need to update those with the new one.
+        d = waitForDeferred(self.findCalendarMatch())
+        yield d
+        calmatch, updatecal, calURL = d.getResult()
         
-        # If we have a match then we need to check whether we are updating etc
-        check_reply = False
-        if calmatch:
-            # See whether the new component is older than any existing ones and throw it away if so
-            newinfo = (None,) + getComponentSyncInfo(new_master)
-            cal = updatecal.iCalendar(calmatch)
-            info = getSyncInfo(calmatch, cal)
-            if compareSyncInfo(info, newinfo) < 0:
-                # Existing resource is older and will be replaced
-                check_reply = True
+        if new_master:
+            # So we have a full update. That means we need to delete any existing events completely and
+            # replace with the ones provided so long as the new one is newer.
+            
+            # If we have a match then we need to check whether we are updating etc
+            check_reply = False
+            if calmatch:
+                # See whether the new component is older than any existing ones and throw it away if so
+                newinfo = (None,) + self.getComponentSyncInfo(new_master)
+                cal = updatecal.iCalendar(calmatch)
+                info = self.getSyncInfo(calmatch, cal)
+                if self.compareSyncInfo(info, newinfo) < 0:
+                    # Existing resource is older and will be replaced
+                    check_reply = True
+                else:
+                    processed = "older"
             else:
-                processed = "older"
+                # We have a new request which we can reply to
+                check_reply = True
+                
+            if check_reply:
+                # Process the reply by determining PARTSTAT and sending the reply and booking the event.
+                d = waitForDeferred(self.checkForReply())
+                yield d
+                doreply, replycal, accepted = d.getResult()
+                
+                try:
+                    if accepted:
+                        if calmatch:
+                            newchild = waitForDeferred(self.writeResource(calURL, updatecal, calmatch, self.calendar))
+                            yield newchild
+                            newchild = newchild.getResult()
+                            logging.info("Replaced calendar component %s with new iTIP message in %s." % (calmatch, calURL), system="iTIP")
+                        else:
+                            newchild = waitForDeferred(self.writeResource(calURL, updatecal, None, self.calendar))
+                            yield newchild
+                            newchild.getResult()
+                            logging.info("Added new calendar component in %s." % (calURL,), system="iTIP")
+                    else:
+                        if calmatch:
+                            d = waitForDeferred(self.deleteResource(updatecal, calmatch))
+                            yield d
+                            d.getResult()
+                            logging.info("Deleted calendar component %s in %s as update was not accepted." % (calmatch, calURL), system="iTIP")
+                            
+                    # Send a reply if needed. 
+                    if doreply:
+                        logging.info("Sending iTIP REPLY %s" % (("declined","accepted")[accepted],), system="iTIP")
+                        newchild = waitForDeferred(self.writeReply(replycal))
+                        yield newchild
+                        newchild = newchild.getResult()
+                    processed = "processed"
+                except:
+                    logging.err("Error while auto-processing iTIP: %s" % (failure.Failure(),), system="iTIP")
+                    raise iTipException
+                
         else:
-            # We have a new request which we can reply to
-            check_reply = True
-            
-        if check_reply:
-            # Process the reply by determining PARTSTAT and sending the reply and booking the event.
-            d = waitForDeferred(checkForReply(request, principal, calendar))
-            yield d
-            doreply, replycal, accepted = d.getResult()
-            
-            try:
-                if accepted:
+            # So we have a partial update. That means we have to do partial updates to instances in
+            # the existing calendar component.
+    
+            # If we have a match then we need to check whether we are updating etc
+            check_reply = False
+            if calmatch:
+                # Check each component to see whether its new
+                cal = updatecal.iCalendar(calmatch)
+                old_master = cal.masterComponent()
+                processed = "older"
+                new_components = [component for component in self.calendar.subcomponents()]
+                for component in new_components:
+                    if component.name() == "VTIMEZONE":
+                        continue
+                    
+                    newinfo = (None,) + self.getComponentSyncInfo(component)
+                    old_component = self.findMatchingComponent(component, cal)
+                    if old_component:
+                        info = (None,) + self.getComponentSyncInfo(old_component)
+                    elif old_master:
+                        info = (None,) + self.getComponentSyncInfo(old_master)
+                    else:
+                        info = None
+                        
+                    if info is None or self.compareSyncInfo(info, newinfo) < 0:
+                        # Existing resource is older and will be replaced
+                        check_reply = True
+                        processed = "processed"
+                    else:
+                        self.calendar.removeComponent(component)
+            else:
+                # We have a new request which we can reply to
+                check_reply = True
+    
+            if check_reply:
+                # Process the reply by determining PARTSTAT and sending the reply and booking the event.
+                d = waitForDeferred(self.checkForReply())
+                yield d
+                doreply, replycal, accepted = d.getResult()
+                
+                try:
                     if calmatch:
-                        newchild = waitForDeferred(writeResource(request, calURL, updatecal, calmatch, calendar))
+                        # Merge the new instances with the old ones
+                        self.mergeComponents(self.calendar, cal)
+                        newchild = waitForDeferred(self.writeResource(calURL, updatecal, calmatch, cal))
                         yield newchild
                         newchild = newchild.getResult()
-                        logging.info("Replaced calendar component %s with new iTIP message in %s." % (calmatch, calURL), system="iTIP")
+                        logging.info("Merged calendar component %s with new iTIP message in %s." % (calmatch, calURL), system="iTIP")
                     else:
-                        newchild = waitForDeferred(writeResource(request, calURL, updatecal, None, calendar))
+                        if accepted:
+                            newchild = waitForDeferred(self.writeResource(calURL, updatecal, None, self.calendar))
+                            yield newchild
+                            newchild.getResult()
+                            logging.info("Added new calendar component in %s." % (calURL,), system="iTIP")
+                            
+                    # Do reply if needed. 
+                    if doreply:
+                        logging.info("Sending iTIP REPLY %s" % (("declined","accepted")[accepted],), system="iTIP")
+                        newchild = waitForDeferred(self.writeReply(replycal))
                         yield newchild
-                        newchild.getResult()
-                        logging.info("Added new calendar component in %s." % (calURL,), system="iTIP")
-                else:
-                    if calmatch:
-                        d = waitForDeferred(deleteResource(updatecal, calmatch))
-                        yield d
-                        d.getResult()
-                        logging.info("Deleted calendar component %s in %s as update was not accepted." % (calmatch, calURL), system="iTIP")
+                        newchild = newchild.getResult()
                         
-                # Send a reply if needed. 
-                if doreply:
-                    logging.info("Sending iTIP REPLY %s" % (("declined","accepted")[accepted],), system="iTIP")
-                    newchild = waitForDeferred(writeReply(request, principal, replycal, inbox))
-                    yield newchild
-                    newchild = newchild.getResult()
-                    newInboxResource(child, newchild)
-                processed = "processed"
-            except:
-                logging.err("Error while auto-processing iTIP: %s" % (failure.Failure(),), system="iTIP")
-                raise iTipException
-            
-    else:
-        # So we have a partial update. That means we have to do partial updates to instances in
-        # the existing calendar component.
-
-        # If we have a match then we need to check whether we are updating etc
-        check_reply = False
-        if calmatch:
-            # Check each component to see whether its new
-            cal = updatecal.iCalendar(calmatch)
-            old_master = cal.masterComponent()
-            processed = "older"
-            new_components = [component for component in calendar.subcomponents()]
-            for component in new_components:
-                if component.name() == "VTIMEZONE":
-                    continue
-                
-                newinfo = (None,) + getComponentSyncInfo(component)
-                old_component = findMatchingComponent(component, cal)
-                if old_component:
-                    info = (None,) + getComponentSyncInfo(old_component)
-                elif old_master:
-                    info = (None,) + getComponentSyncInfo(old_master)
-                else:
-                    info = None
-                    
-                if info is None or compareSyncInfo(info, newinfo) < 0:
-                    # Existing resource is older and will be replaced
-                    check_reply = True
                     processed = "processed"
-                else:
-                    calendar.removeComponent(component)
-        else:
-            # We have a new request which we can reply to
-            check_reply = True
-
-        if check_reply:
-            # Process the reply by determining PARTSTAT and sending the reply and booking the event.
-            d = waitForDeferred(checkForReply(request, principal, calendar))
-            yield d
-            doreply, replycal, accepted = d.getResult()
-            
-            try:
-                if calmatch:
-                    # Merge the new instances with the old ones
-                    mergeComponents(calendar, cal)
-                    newchild = waitForDeferred(writeResource(request, calURL, updatecal, calmatch, cal))
-                    yield newchild
-                    newchild = newchild.getResult()
-                    logging.info("Merged calendar component %s with new iTIP message in %s." % (calmatch, calURL), system="iTIP")
-                else:
-                    if accepted:
-                        newchild = waitForDeferred(writeResource(request, calURL, updatecal, None, calendar))
-                        yield newchild
-                        newchild.getResult()
-                        logging.info("Added new calendar component in %s." % (calURL,), system="iTIP")
-                        
-                # Do reply if needed. 
-                if doreply:
-                    logging.info("Sending iTIP REPLY %s" % (("declined","accepted")[accepted],), system="iTIP")
-                    newchild = waitForDeferred(writeReply(request, principal, replycal, inbox))
-                    yield newchild
-                    newchild = newchild.getResult()
-                    newInboxResource(child, newchild)
-                    
-                processed = "processed"
-            except:
-                logging.err("Error while auto-processing iTIP: %s" % (failure.Failure(),), system="iTIP")
-                raise iTipException
-
-    # Remove the now processed incoming request.
-    try:
-        d = waitForDeferred(deleteResource(inbox, child.fp.basename()))
-        yield d
-        d.getResult()
-        logging.info("Deleted new iTIP message %s in Inbox because it has been %s." %
-            (child.fp.basename(),
-             {"processed":"processed",
-              "older":    "ignored: older",
-              "ignored":  "ignored: no match"}[processed],), system="iTIP")
-    except:
-        logging.err("Error while auto-processing iTIP: %s" % (failure.Failure(),), system="iTIP")
-        raise iTipException
-    yield None
-    return
-
-processRequest = deferredGenerator(processRequest)
-
-def processAdd(request, principal, inbox, calendar, child):
-    """
-    Process a METHOD=ADD.
-    This is a deferredGenerator function so use yield whenever we have a deferred.
-
-    @param request: the L{twisted.web2.server.Request} for the current request.
-    @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
-    @param inbox: the L{ScheduleInboxFile} for the principal's Inbox.
-    @param calendar: the L{Component} for the iTIP message we are processing.
-    @param child: the L{CalDAVFile} for the iTIP message resource already saved to the Inbox.
-    """
-    logging.info("Auto-processing iTIP ADD for: %s" % (str(principal),), system="iTIP")
-
-    raise NotImplementedError
-
-processAdd = deferredGenerator(processAdd)
-
-def processCancel(request, principal, inbox, calendar, child):
-    """
-    Process a METHOD=CANCEL.
-    This is a deferredGenerator function so use yield whenever we have a deferred.
-
-    Policy find all components that match UID, SEQ and R-ID and remove them.
-
-    Steps:
+                except:
+                    logging.err("Error while auto-processing iTIP: %s" % (failure.Failure(),), system="iTIP")
+                    raise iTipException
     
-      1. See if this updates existing ones in Inbox.
-      2. Remove existing ones in Inbox.
-      3. See if this updates existing ones in free-busy-set calendars.
-      4. Remove existing ones in those calendars.
-      5. Remove the incoming request.
+        # Remove the now processed incoming request.
+        if self.inbox:
+            self.deleteInboxResource({
+                "processed":"processed",
+                "older":    "ignored: older",
+                "ignored":  "ignored: no match"
+            }[processed])
 
-    NB Removal can be complex as we need to take RECURRENCE-ID into account - i.e a single
-    instance may be cancelled. What we need to do for this is:
+        yield None
+        return
     
-      1. If the R-ID of iTIP component matches the R-ID of one in Inbox then it is an exact match, so
-         delete the old one.
-      2. If the R-ID of iTIP does not match an R-ID in Inbox, then we are adding a cancellation as an override, so
-         leave the new and existing ones in the Inbox.
-      3. If the R-ID of iTIP component matches the R-ID of an overridden component in an f-b-s calendar, then
-         remove the overridden component from the f-b-s resource.
-      4. Add an EXDATE to the f-b-s resource to 'cancel' that instance.
+    @deferredGenerator
+    def processAdd(self):
+        """
+        Process a METHOD=ADD.
+        This is a deferredGenerator function so use yield whenever we have a deferred.
+        """
+        logging.info("Auto-processing iTIP ADD for: %s" % (str(self.principal),), system="iTIP")
     
-    TODO: Yes, I am going to ignore RANGE= on RECURRENCE-ID for now...
+        raise NotImplementedError
     
-    @param request: the L{twisted.web2.server.Request} for the current request.
-    @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
-    @param inbox: the L{ScheduleInboxFile} for the principal's Inbox.
-    @param calendar: the L{Component} for the iTIP message we are processing.
-    @param child: the L{CalDAVFile} for the iTIP message resource already saved to the Inbox.
-    """
+    @deferredGenerator
+    def processCancel(self):
+        """
+        Process a METHOD=CANCEL.
+        This is a deferredGenerator function so use yield whenever we have a deferred.
     
-    logging.info("Auto-processing iTIP CANCEL for: %s" % (str(principal),), system="iTIP")
-    processed = "ignored"
-
-    # Get all component info for this iTIP message
-    newinfo = getSyncInfo(child.fp.basename(), calendar)
-    info = getAllInfo(inbox, calendar, child)
-
-    # First see if we have a recurrence id which will force extra work
-    has_rid = False
-    if newinfo[4] is not None:
-        has_rid = True
-    else:
-        for i in info:
-            if i[4] is not None:
-                has_rid = True
-                break
-            
-    if not has_rid:
-        # Compare the new one with each existing one.
-        d = waitForDeferred(processOthersInInbox(info, newinfo, inbox, child))
-        yield d
-        delete_child = d.getResult()
-
-        if delete_child:
-            yield None
-            return
-
+        Policy find all components that match UID, SEQ and R-ID and remove them.
+    
+        Steps:
+        
+            1. See if this updates existing ones in Inbox.
+            2. Remove existing ones in Inbox.
+            3. See if this updates existing ones in free-busy-set calendars.
+            4. Remove existing ones in those calendars.
+            5. Remove the incoming request.
+    
+        NB Removal can be complex as we need to take RECURRENCE-ID into account - i.e a single
+        instance may be cancelled. What we need to do for this is:
+        
+            1. If the R-ID of iTIP component matches the R-ID of one in Inbox then it is an exact match, so
+               delete the old one.
+            2. If the R-ID of iTIP does not match an R-ID in Inbox, then we are adding a cancellation as an override, so
+               leave the new and existing ones in the Inbox.
+            3. If the R-ID of iTIP component matches the R-ID of an overridden component in an f-b-s calendar, then
+               remove the overridden component from the f-b-s resource.
+            4. Add an EXDATE to the f-b-s resource to 'cancel' that instance.
+        
+        TODO: Yes, I am going to ignore RANGE= on RECURRENCE-ID for now...
+        """
+        
+        logging.info("Auto-processing iTIP CANCEL for: %s" % (str(self.principal),), system="iTIP")
+        processed = "ignored"
+    
+        # Get all component info for this iTIP message
+        newinfo = self.getSyncInfo(self.childname, self.calendar)
+    
+        # First see if we have a recurrence id which will force extra work
+        has_rid = False
+        if newinfo[4] is not None:
+            has_rid = True
+        else:
+            for i in self.getAllInfo(self.inbox, self.calendar, self.child):
+                if i[4] is not None:
+                    has_rid = True
+                    break
+                
         # Next we want to try and find a match to any components on existing calendars listed as contributing
         # to free-busy as we will need to update those with the new one.
-        d = waitForDeferred(findCalendarMatch(request, principal, calendar))
+        d = waitForDeferred(self.findCalendarMatch())
         yield d
         calmatch, updatecal, calURL = d.getResult()
         
-        # If we have a match then we need to check whether we are updating etc
-        if calmatch:
-            # See whether the current component is older than any existing ones and throw it away if so
-            cal = updatecal.iCalendar(calmatch)
-            info = getSyncInfo(calmatch, cal)
-            if compareSyncInfo(info, newinfo) < 0:
-                # Delete existing resource which has been cancelled
-                try:
-                    d = waitForDeferred(deleteResource(updatecal, calmatch,))
-                    yield d
-                    d.getResult()
-                    logging.info("Delete calendar component %s in %s as it was cancelled." % (calmatch, calURL), system="iTIP")
-                except:
-                    logging.err("Error while auto-processing iTIP: %s" % (failure.Failure(),), system="iTIP")
-                    raise iTipException
-                processed = "processed"
+        if not has_rid:
+            # If we have a match then we need to check whether we are updating etc
+            if calmatch:
+                # See whether the current component is older than any existing ones and throw it away if so
+                cal = updatecal.iCalendar(calmatch)
+                info = self.getSyncInfo(calmatch, cal)
+                if self.compareSyncInfo(info, newinfo) < 0:
+                    # Delete existing resource which has been cancelled
+                    try:
+                        d = waitForDeferred(self.deleteResource(updatecal, calmatch,))
+                        yield d
+                        d.getResult()
+                        logging.info("Delete calendar component %s in %s as it was cancelled." % (calmatch, calURL), system="iTIP")
+                    except:
+                        logging.err("Error while auto-processing iTIP: %s" % (failure.Failure(),), system="iTIP")
+                        raise iTipException
+                    processed = "processed"
+                else:
+                    processed = "older"
             else:
-                processed = "older"
+                # Nothing to do except delete the inbox item as we have nothing to cancel.
+                processed = "ignored"
         else:
-            # Nothing to do except delete the inbox item as we have nothing to cancel.
-            processed = "ignored"
-    else:
-        # Try and find a match to any components on existing calendars listed as contributing
-        # to free-busy as we will need to update those with the new one.
-        d = waitForDeferred(findCalendarMatch(request, principal, calendar))
-        yield d
-        calmatch, updatecal, calURL = d.getResult()
-        
-        # If we have a match then we need to check whether we are updating etc
-        if calmatch:
-            # iTIP CANCEL can contain multiple components being cancelled in the RECURRENCE-ID case.
-            # So we need to iterate over each iTIP component.
-
-            # Get the existing calendar object
-            existing_calendar = updatecal.iCalendar(calmatch)
-            existing_master = existing_calendar.masterComponent()
-            exdates = []
-
-            for component in calendar.subcomponents():
-                if component.name() == "VTIMEZONE":
-                    continue
-            
-                # Find matching component in existing calendar
-                old_component = findMatchingComponent(component, existing_calendar)
+            # If we have a match then we need to check whether we are updating etc
+            if calmatch:
+                # iTIP CANCEL can contain multiple components being cancelled in the RECURRENCE-ID case.
+                # So we need to iterate over each iTIP component.
+    
+                # Get the existing calendar object
+                existing_calendar = updatecal.iCalendar(calmatch)
+                existing_master = existing_calendar.masterComponent()
+                exdates = []
+    
+                for component in self.calendar.subcomponents():
+                    if component.name() == "VTIMEZONE":
+                        continue
                 
-                if old_component:
-                    # We are cancelling an overridden component, so we need to check the
-                    # SEQUENCE/DTSAMP with the master.
-                    if compareComponents(old_component, component) < 0:
-                        # Exclude the cancelled instance
-                        exdates.append(component.getRecurrenceIDUTC())
-                        
-                        # Remove the existing component.
-                        existing_calendar.removeComponent(old_component)
-                elif existing_master:
-                    # We are trying to CANCEL a non-overridden instance, so we need to
-                    # check SEQUENCE/DTSTAMP with the master.
-                    if compareComponents(existing_master, component) < 0:
-                        # Exclude the cancelled instance
-                        exdates.append(component.getRecurrenceIDUTC())
-
-            # If we have any EXDATEs lets add them to the existing calendar object and write
-            # it back.
-            if exdates:
-                if existing_master:
-                    existing_master.addProperty(Property("EXDATE", exdates))
-
-                # See if there are still components in the calendar - we might have deleted the last overridden instance
-                # in which case the calendar object is empty (except for VTIMEZONEs).
-                if existing_calendar.mainType() is None:
-                    # Delete the now empty calendar object
-                    d = waitForDeferred(deleteResource(updatecal, calmatch))
-                    yield d
-                    d.getResult()
-                    logging.info("Deleted calendar component %s after cancellations from iTIP message in %s." % (calmatch, calURL), system="iTIP")
+                    # Find matching component in existing calendar
+                    old_component = self.findMatchingComponent(component, existing_calendar)
+                    
+                    if old_component:
+                        # We are cancelling an overridden component, so we need to check the
+                        # SEQUENCE/DTSAMP with the master.
+                        if self.compareComponents(old_component, component) < 0:
+                            # Exclude the cancelled instance
+                            exdates.append(component.getRecurrenceIDUTC())
+                            
+                            # Remove the existing component.
+                            existing_calendar.removeComponent(old_component)
+                    elif existing_master:
+                        # We are trying to CANCEL a non-overridden instance, so we need to
+                        # check SEQUENCE/DTSTAMP with the master.
+                        if self.compareComponents(existing_master, component) < 0:
+                            # Exclude the cancelled instance
+                            exdates.append(component.getRecurrenceIDUTC())
+    
+                # If we have any EXDATEs lets add them to the existing calendar object and write
+                # it back.
+                if exdates:
+                    if existing_master:
+                        existing_master.addProperty(Property("EXDATE", exdates))
+    
+                    # See if there are still components in the calendar - we might have deleted the last overridden instance
+                    # in which case the calendar object is empty (except for VTIMEZONEs).
+                    if existing_calendar.mainType() is None:
+                        # Delete the now empty calendar object
+                        d = waitForDeferred(self.deleteResource(updatecal, calmatch))
+                        yield d
+                        d.getResult()
+                        logging.info("Deleted calendar component %s after cancellations from iTIP message in %s." % (calmatch, calURL), system="iTIP")
+                    else:
+                        # Update the existing calendar object
+                        newchild = waitForDeferred(self.writeResource(calURL, updatecal, calmatch, existing_calendar))
+                        yield newchild
+                        newchild = newchild.getResult()
+                        logging.info("Updated calendar component %s with cancellations from iTIP message in %s." % (calmatch, calURL), system="iTIP")
+                    processed = "processed"
                 else:
-                    # Update the existing calendar object
-                    newchild = waitForDeferred(writeResource(request, calURL, updatecal, calmatch, existing_calendar))
-                    yield newchild
-                    newchild = newchild.getResult()
-                    logging.info("Updated calendar component %s with cancellations from iTIP message in %s." % (calmatch, calURL), system="iTIP")
-                processed = "processed"
+                    processed = "older"
             else:
-                processed = "older"
-        else:
-            # Nothing to do except delete the inbox item as we have nothing to cancel.
-            processed = "ignored"
-
-    # Remove the now processed incoming request.
-    try:
-        d = waitForDeferred(deleteResource(inbox, child.fp.basename()))
-        yield d
-        d.getResult()
-        logging.info("Deleted new iTIP message %s in Inbox because it has been %s." %
-            (child.fp.basename(),
-             {"processed":"processed",
-              "older":    "ignored: older",
-              "ignored":  "ignored: no match"}[processed],), system="iTIP")
-    except:
-        logging.err("Error while auto-processing iTIP: %s" % (failure.Failure(),), system="iTIP")
-        raise iTipException
-    yield None
-    return
-
-processCancel = deferredGenerator(processCancel)
-
-def checkForReply(request, principal, calendar):
-    """
-    Check whether a reply to the given iTIP message is needed. A reply will be needed if the
-    RSVP=TRUE. A reply will either be positive (accepted
-    invitation) or negative (denied invitation). In addition we will modify calendar to reflect
-    any new state (e.g. remove RSVP, set PARTSTAT to ACCEPTED or DECLINED).
+                # Nothing to do except delete the inbox item as we have nothing to cancel.
+                processed = "ignored"
     
-    BTW The incoming iTIP message may contain multiple components so we need to iterate over all those.
-    At the moment we will treat a failure on one isntances as a DECLINE of the entire set.
+        # Remove the now processed incoming request.
+        if self.inbox:
+            self.deleteInboxResource({
+                "processed":"processed",
+                "older":    "ignored: older",
+                "ignored":  "ignored: no match"
+            }[processed])
 
-    @param request: the L{twisted.web2.server.Request} for the current request.
-    @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
-    @param calendar: the L{Component} for the iTIP message we are processing.
-    @return: C{True} if a reply is needed, C{False} otherwise.
-    """
+        yield None
+        return
     
-    # We need to fugure out whether the specified component will clash with any others in the f-b-set calendars
-    accepted = True
+    @deferredGenerator
+    def checkForReply(self):
+        """
+        Check whether a reply to the given iTIP message is needed. A reply will be needed if the
+        RSVP=TRUE. A reply will either be positive (accepted
+        invitation) or negative (denied invitation). In addition we will modify calendar to reflect
+        any new state (e.g. remove RSVP, set PARTSTAT to ACCEPTED or DECLINED).
         
-    # First expand current one to get instances (only go 1 year into the future)
-    default_future_expansion_duration = datetime.timedelta(days=356*1)
-    expand_max = datetime.date.today() + default_future_expansion_duration
-    instances = calendar.expandTimeRanges(expand_max)
-    
-    # Extract UID from primary component as we want to ignore this one if we match it
-    # in any calendars.
-    comp = calendar.mainComponent(allow_multiple=True)
-    uid = comp.propertyValue("UID")
+        BTW The incoming iTIP message may contain multiple components so we need to iterate over all those.
+        At the moment we will treat a failure on one isntances as a DECLINE of the entire set.
 
-    # Now compare each instance time-range with the index and see if there is an overlap
-    fbset = waitForDeferred(principal.calendarFreeBusyURIs(request))
-    yield fbset
-    fbset = fbset.getResult()
-
-    for calURL in fbset:
-        testcal = waitForDeferred(request.locateResource(calURL))
-        yield testcal
-        testcal = testcal.getResult()
+        @return: C{True} if a reply is needed, C{False} otherwise.
+        """
         
-        # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
-        fbinfo = ([], [], [])
+        # We need to fugure out whether the specified component will clash with any others in the f-b-set calendars
+        accepted = True
+            
+        # First expand current one to get instances (only go 1 year into the future)
+        default_future_expansion_duration = datetime.timedelta(days=356*1)
+        expand_max = datetime.date.today() + default_future_expansion_duration
+        instances = self.calendar.expandTimeRanges(expand_max)
         
-        # Now do search for overlapping time-range
-        for instance in instances.instances.itervalues():
-            try:
-                tr = caldavxml.TimeRange(start="20000101", end="20000101")
-                tr.start = instance.start
-                tr.end = instance.end
-                d = waitForDeferred(report_common.generateFreeBusyInfo(request, testcal, fbinfo, tr, 0, uid))
-                yield d
-                d.getResult()
-                
-                # If any fbinfo entries exist we have an overlap
-                if len(fbinfo[0]) or len(fbinfo[1]) or len(fbinfo[2]):
+        # Extract UID from primary component as we want to ignore this one if we match it
+        # in any calendars.
+        comp = self.calendar.mainComponent(allow_multiple=True)
+        uid = comp.propertyValue("UID")
+    
+        # Now compare each instance time-range with the index and see if there is an overlap
+        calendars = waitForDeferred(self.getCalendarsToMatch())
+        yield calendars
+        calendars = calendars.getResult()
+    
+        for calURL in calendars:
+            testcal = waitForDeferred(self.request.locateResource(calURL))
+            yield testcal
+            testcal = testcal.getResult()
+            
+            # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
+            fbinfo = ([], [], [])
+            
+            # Now do search for overlapping time-range
+            for instance in instances.instances.itervalues():
+                try:
+                    tr = caldavxml.TimeRange(start="20000101", end="20000101")
+                    tr.start = instance.start
+                    tr.end = instance.end
+                    d = waitForDeferred(report_common.generateFreeBusyInfo(self.request, testcal, fbinfo, tr, 0, uid))
+                    yield d
+                    d.getResult()
+                    
+                    # If any fbinfo entries exist we have an overlap
+                    if len(fbinfo[0]) or len(fbinfo[1]) or len(fbinfo[2]):
+                        accepted = False
+                        break
+                except NumberOfMatchesWithinLimits:
                     accepted = False
+                    logging.info("Exceeded number of matches whilst trying to find free-time.", system="iTIP")
                     break
-            except NumberOfMatchesWithinLimits:
-                accepted = False
-                logging.info("Exceeded number of matches whilst trying to find free-time.", system="iTIP")
+                
+            if not accepted:
                 break
-            
-        if not accepted:
-            break
-     
-    # Extract the ATTENDEE property matching current recipient from the calendar data
-    cuas = principal.calendarUserAddresses()
-    attendeeProps = calendar.getAttendeeProperties(cuas)
-    if not attendeeProps:
-        yield False, None, accepted
-        return
-
-    # Look for specific parameters
-    rsvp = False
-    for attendeeProp in attendeeProps:
-        if "RSVP" in attendeeProp.params():
-            if attendeeProp.params()["RSVP"][0] == "TRUE":
-                rsvp = True
+         
+        # Extract the ATTENDEE property matching current recipient from the calendar data
+        cuas = self.principal.calendarUserAddresses()
+        attendeeProps = self.calendar.getAttendeeProperties(cuas)
+        if not attendeeProps:
+            yield False, None, accepted
+            return
     
-            # Now modify the original component
-            del attendeeProp.params()["RSVP"]
-
-    if accepted:
-        partstat = "ACCEPTED"
-    else:
-        partstat = "DECLINED"
-    for attendeeProp in attendeeProps:
-        if "PARTSTAT" in attendeeProp.params():
-            attendeeProp.params()["PARTSTAT"][0] = partstat
-        else:
-            attendeeProp.params()["PARTSTAT"] = [partstat]
+        # Look for specific parameters
+        rsvp = False
+        for attendeeProp in attendeeProps:
+            if "RSVP" in attendeeProp.params():
+                if attendeeProp.params()["RSVP"][0] == "TRUE":
+                    rsvp = True
+        
+                # Now modify the original component
+                del attendeeProp.params()["RSVP"]
     
-    # Now create a new calendar object for the reply
-    
-    # First get useful props from the original
-    replycal = calendar.duplicate()
-    
-    # Change METHOD
-    replycal.getProperty("METHOD").setValue("REPLY")
-    
-    # Change PRODID to this server
-    replycal.getProperty("PRODID").setValue(iCalendarProductID)
-    
-    # Add REQUEST-STATUS
-    for component in replycal.subcomponents():
         if accepted:
-            component.addProperty(Property(name="REQUEST-STATUS", value="2.0; Success."))
+            partstat = "ACCEPTED"
         else:
-            component.addProperty(Property(name="REQUEST-STATUS", value="4.0; Event conflict. Date/time is busy."))
-
-    # Remove all attendees other than ourselves
-    for component in replycal.subcomponents():
-        if component.name() == "VTIMEZONE":
-            continue
-        attendeeProp = component.getAttendeeProperty(cuas)
-        attendees = tuple(component.properties("ATTENDEE"))
-        for attendee in attendees:
-            if attendeeProp is None or (attendee.value() != attendeeProp.value()):
-                component.removeProperty(attendee)
-
-    yield rsvp, replycal, accepted
-
-checkForReply = deferredGenerator(checkForReply)
-
-def writeReply(request, principal, replycal, ainbox):
-    """
-    Write an iTIP message reply into the specified Inbox.
+            partstat = "DECLINED"
+        for attendeeProp in attendeeProps:
+            if "PARTSTAT" in attendeeProp.params():
+                attendeeProp.params()["PARTSTAT"][0] = partstat
+            else:
+                attendeeProp.params()["PARTSTAT"] = [partstat]
+        
+        # Now create a new calendar object for the reply
+        
+        # First get useful props from the original
+        replycal = self.calendar.duplicate()
+        
+        # Change METHOD
+        replycal.getProperty("METHOD").setValue("REPLY")
+        
+        # Change PRODID to this server
+        replycal.getProperty("PRODID").setValue(iCalendarProductID)
+        
+        # Add REQUEST-STATUS
+        for component in replycal.subcomponents():
+            if accepted:
+                component.addProperty(Property(name="REQUEST-STATUS", value="2.0; Success."))
+            else:
+                component.addProperty(Property(name="REQUEST-STATUS", value="4.0; Event conflict. Date/time is busy."))
     
-    @param request: the L{twisted.web2.server.Request} for the current request.
-    @param principal: the L{CalendarPrincipalFile} principal resource for the principal we are dealing with.
-    @param replycal: the L{Component} for the iTIP message reply.
-    @param ainbox: the L{ScheduleInboxFile} for the principal's Inbox.
-    """
+        # Remove all attendees other than ourselves
+        for component in replycal.subcomponents():
+            if component.name() == "VTIMEZONE":
+                continue
+            attendeeProp = component.getAttendeeProperty(cuas)
+            attendees = tuple(component.properties("ATTENDEE"))
+            for attendee in attendees:
+                if attendeeProp is None or (attendee.value() != attendeeProp.value()):
+                    component.mainComponent().removeProperty(attendee)
     
-    # Get the Inbox of the ORGANIZER
-    organizer = replycal.getOrganizer()
-    assert organizer is not None
-    inboxURL = ainbox.principalForCalendarUserAddress(organizer).scheduleInboxURL()
-    assert inboxURL
+        yield rsvp, replycal, accepted
     
-    # Determine whether current principal has CALDAV:schedule right on that Inbox
-    inbox = waitForDeferred(request.locateResource(inboxURL))
-    yield inbox
-    inbox = inbox.getResult()
+    @deferredGenerator
+    def writeReply(self, replycal):
+        """
+        Write an iTIP message reply into the specified Inbox.
 
-    try:
-        d = waitForDeferred(inbox.checkPrivileges(request, (caldavxml.Schedule(),), principal=davxml.Principal(davxml.HRef.fromString(principal.principalURL()))))
-        yield d
-        d.getResult()
-    except AccessDeniedError:
-        logging.info("Could not send reply as %s does not have CALDAV:schedule permission on %s Inbox." % (principal.principalURL(), organizer), system="iTIP")
-        yield None
-        return
+        @param replycal: the L{Component} for the iTIP message reply.
+        """
+        
+        # Get the Inbox of the ORGANIZER
+        organizer = replycal.getOrganizer()
+        assert organizer is not None
+        inboxURL = self.inbox.principalForCalendarUserAddress(organizer).scheduleInboxURL()
+        assert inboxURL
+        
+        # Determine whether current principal has CALDAV:schedule right on that Inbox
+        writeinbox = waitForDeferred(self.request.locateResource(inboxURL))
+        yield writeinbox
+        writeinbox = writeinbox.getResult()
     
-    # Now deposit the new calendar into the inbox
-    d = waitForDeferred(writeResource(request, inboxURL, inbox, None, replycal))
-    yield d
-    yield d.getResult()
+        try:
+            d = waitForDeferred(writeinbox.checkPrivileges(self.request, (caldavxml.Schedule(),), principal=davxml.Principal(davxml.HRef.fromString(self.principal.principalURL()))))
+            yield d
+            d.getResult()
+        except AccessDeniedError:
+            logging.info("Could not send reply as %s does not have CALDAV:schedule permission on %s Inbox." % (self.principal.principalURL(), organizer), system="iTIP")
+            yield None
+            return
+        
+        # Now deposit the new calendar into the inbox
+        newchild = waitForDeferred(self.writeResource(inboxURL, writeinbox, None, replycal))
+        yield newchild
+        newchild = newchild.getResult()
 
-writeReply = deferredGenerator(writeReply)
-
-def writeResource(request, collURL, collection, name, calendar):
-    """
-    Write out the calendar resource (iTIP) message to the specified calendar, either over-writing the named
-    resource or by creating a new one.
+        self.newInboxResource(self.child, newchild)
+        
+        yield newchild
+        return
     
-    @param request: the L{IRequest} for the current request.
-    @param collURL: the C{str} containing the URL of the calendar collection.
-    @param collection: the L{CalDAVFile} for the calendar collection to store the resource in.
-    @param name: the C{str} for the resource name to write into, or {None} to write a new resource.
-    @param calendar: the L{Component} calendar to write.
-    @return: C{tuple} of L{Deferred}, L{CalDAVFile}
-    """
+    @deferredGenerator
+    def writeResource(self, collURL, collection, name, calendar):
+        """
+        Write out the calendar resource (iTIP) message to the specified calendar, either over-writing the named
+        resource or by creating a new one.
+        
+        @param collURL: the C{str} containing the URL of the calendar collection.
+        @param collection: the L{CalDAVFile} for the calendar collection to store the resource in.
+        @param name: the C{str} for the resource name to write into, or {None} to write a new resource.
+        @param calendar: the L{Component} calendar to write.
+        @return: C{tuple} of L{Deferred}, L{CalDAVFile}
+        """
+        
+        # Create a new name if one was not provided
+        if name is None:
+            name =  md5.new(str(calendar) + str(time.time()) + collection.fp.path).hexdigest() + ".ics"
     
-    # Create a new name if one was not provided
-    if name is None:
-        name =  md5.new(str(calendar) + str(time.time()) + collection.fp.path).hexdigest() + ".ics"
-
-    # Get a resource for the new item
-    newchildURL = joinURL(collURL, name)
-    newchild = waitForDeferred(request.locateResource(newchildURL))
-    yield newchild
-    newchild = newchild.getResult()
-    
-    # Modify the original calendar data by removing the METHOD property - everything else is left as-is,
-    # as any other needed changes (e.g. RSVP/PARTSTAT) will have been updated.
-    # NB Only do this when writing to something other than an Inbox or Outbox
-    itipper = True
-    if collection.isCalendarCollection():
-        method = calendar.getProperty("METHOD")
-        if method:
-            calendar.removeProperty(method)
-        itipper = False
-    
-    # Now write it to the resource
-    try:
-        d = waitForDeferred(storeCalendarObjectResource(
-                request=request,
-                sourcecal = False,
-                destination = newchild,
-                destination_uri = newchildURL,
-                calendardata = str(calendar),
-                destinationparent = collection,
-                destinationcal = True,
-                isiTIP = itipper
-            ))
+        # Get a resource for the new item
+        newchildURL = joinURL(collURL, name)
+        newchild = waitForDeferred(self.request.locateResource(newchildURL))
+        yield newchild
+        newchild = newchild.getResult()
+        
+        # Modify the original calendar data by removing the METHOD property - everything else is left as-is,
+        # as any other needed changes (e.g. RSVP/PARTSTAT) will have been updated.
+        # NB Only do this when writing to something other than an Inbox or Outbox
+        itipper = True
+        if collection.isCalendarCollection():
+            method = calendar.getProperty("METHOD")
+            if method:
+                calendar.removeProperty(method)
+            itipper = False
+        
+        # Now write it to the resource
+        storer = StoreCalendarObjectResource(
+                     request=self.request,
+                     destination = newchild,
+                     destination_uri = newchildURL,
+                     destinationparent = collection,
+                     destinationcal = True,
+                     calendar = calendar,
+                     isiTIP = itipper
+                 )
+        d = waitForDeferred(storer.run())
         yield d
         d.getResult()
-    except:
-        yield None
-        return
+        
+        yield newchild
     
-    yield newchild
-
-writeResource = deferredGenerator(writeResource)    
-
-def newInboxResource(child, newchild):
-    """
-    Copy recipient and organizer properties from one iTIP resource, to another,
-    switching them as appropriate for a reply, and also set the state.
-    
-    @param child: the L{CalDAVFile} for the original iTIP message.
-    @param newchild: the L{CalDAVFile} for the iTIP message reply.
-    """
-    # Make previous Recipient the new Originator
-    if child.hasDeadProperty(caldavxml.Recipient):
-        recip = child.readDeadProperty(caldavxml.Recipient)
-        if recip.children:
-            # Store CALDAV:originator property
-            newchild.writeDeadProperty(caldavxml.Originator(davxml.HRef.fromString(str(recip.children[0]))))
-    
-    # Make previous Originator the new Recipient
-    if child.hasDeadProperty(caldavxml.Originator):
-        orig = child.readDeadProperty(caldavxml.Originator)
-        if orig.children:
-            # Store CALDAV:originator property
-            newchild.writeDeadProperty(caldavxml.Recipient(davxml.HRef.fromString(str(orig.children[0]))))
-  
-def deleteResource(collection, name):
-    """
-    Delete the calendar resource in the specified calendar.
-    
-    @param collection: the L{CalDAVFile} for the calendar collection to store the resource in.
-    @param name: the C{str} for the resource name to write into, or {None} to write a new resource.
-    @return: L{Deferred}
-    """
-    
-    delchild = collection.getChild(name)
-    index = collection.index()
-    index.deleteResource(delchild.fp.basename())
-    
-    def _deletedResourced(result):
-        # Change CTag on the parent calendar collection
-        collection.updateCTag()
+    def newInboxResource(self, child, newchild):
+        """
+        Copy recipient and organizer properties from one iTIP resource, to another,
+        switching them as appropriate for a reply, and also set the state.
         
-        return result
-
-    d = maybeDeferred(delete, "", delchild.fp, "0")
-    d.addCallback(_deletedResourced)
-    return d
-
-def canAutoRespond(calendar):
-    """
-    Check whether the METHOD of this iTIP calendar object is one we can process. Also,
-    we will only handle VEVENTs right now.
-
-    @param calendar: L{Component} for calendar to examine.
-    @return: C{True} if we can auto-respond, C{False} if not.
-    """
-
-    try:
-        method = calendar.propertyValue("METHOD")
-        if method not in ("REQUEST", "ADD", "CANCEL"):
-            return False
-        if calendar.mainType() not in ("VEVENT"):
-            return False
-    except ValueError:
-        return False
+        @param child: the L{CalDAVFile} for the original iTIP message.
+        @param newchild: the L{CalDAVFile} for the iTIP message reply.
+        """
+        # Make previous Recipient the new Originator
+        if child.hasDeadProperty(caldavxml.Recipient):
+            recip = child.readDeadProperty(caldavxml.Recipient)
+            if recip.children:
+                # Store CALDAV:originator property
+                newchild.writeDeadProperty(caldavxml.Originator(davxml.HRef.fromString(str(recip.children[0]))))
+        
+        # Make previous Originator the new Recipient
+        if child.hasDeadProperty(caldavxml.Originator):
+            orig = child.readDeadProperty(caldavxml.Originator)
+            if orig.children:
+                # Store CALDAV:originator property
+                newchild.writeDeadProperty(caldavxml.Recipient(davxml.HRef.fromString(str(orig.children[0]))))
     
-    return True
-
-def processOthersInInbox(info, newinfo, inbox, child):
-    # Compare the new one with each existing one.
-    delete_child = False
-    for i in info:
-        # For any that are older, delete them.
-        if compareSyncInfo(i, newinfo) < 0:
-            try:
-                d = waitForDeferred(deleteResource(inbox, i[0]))
-                yield d
-                d.getResult()
-                logging.info("Deleted iTIP message %s in Inbox that was older than the new one." % (i[0],), system="iTIP")
-            except:
-                logging.err("Error while auto-processing iTIP: %s" % (failure.Failure(),), system="iTIP")
-                raise iTipException
-        else:
-            # For any that are newer or the same, mark the new one to be deleted.
-            delete_child = True
-
-    # Delete the new one if so marked.
-    if delete_child:
+    @deferredGenerator
+    def deleteInboxResource(self, processed_state):
+        # Remove the now processed incoming request.
         try:
-            d = waitForDeferred(deleteResource(inbox, child.fp.basename()))
+            d = waitForDeferred(self.deleteResource(self.inbox, self.childname))
             yield d
             d.getResult()
-            logging.info("Deleted new iTIP message %s in Inbox because it was older than existing ones." % (child.fp.basename(),), system="iTIP")
+            logging.info("Deleted new iTIP message %s in Inbox because it has been %s." %
+                (self.childname, processed_state,), system="iTIP")
         except:
             logging.err("Error while auto-processing iTIP: %s" % (failure.Failure(),), system="iTIP")
             raise iTipException
-    
-    yield delete_child
 
-processOthersInInbox = deferredGenerator(processOthersInInbox)    
-
-def findCalendarMatch(request, principal, calendar):
-    # Try and find a match to any components on existing calendars listed as contributing
-    # to free-busy as we will need to update those with the new one.
+    def deleteResource(self, collection, name):
+        """
+        Delete the calendar resource in the specified calendar.
+        
+        @param collection: the L{CalDAVFile} for the calendar collection to store the resource in.
+        @param name: the C{str} for the resource name to write into, or {None} to write a new resource.
+        @return: L{Deferred}
+        """
+        
+        delchild = collection.getChild(name)
+        index = collection.index()
+        index.deleteResource(delchild.fp.basename())
+        
+        def _deletedResourced(result):
+            # Change CTag on the parent calendar collection
+            collection.updateCTag()
+            
+            return result
     
-    # Find the current recipients calendar-free-busy-set
-    fbset = waitForDeferred(principal.calendarFreeBusyURIs(request))
-    yield fbset
-    fbset = fbset.getResult()
-
-    # Find the first calendar in the list with a component matching the one we are processing
-    calmatch = None
-    updatecal = None
-    calURL = None
-    for calURL in fbset:
-        updatecal = waitForDeferred(request.locateResource(calURL))
-        yield updatecal
-        updatecal = updatecal.getResult()
-        if updatecal is None or not updatecal.exists() or not isCalendarCollectionResource(updatecal):
-            # We will ignore missing calendars. If the recipient has failed to
-            # properly manage the free busy set that should not prevent us from working.
-            continue
-        calmatch = matchComponentInCalendar(updatecal, calendar)
-        if calmatch:
-            logging.info("Found calendar component %s matching new iTIP message in %s." % (calmatch, calURL), system="iTIP")
-            break
+        d = maybeDeferred(delete, "", delchild.fp, "0")
+        d.addCallback(_deletedResourced)
+        return d
     
-    if calmatch is None and len(fbset):
-        calURL = fbset[0]
-        updatecal = waitForDeferred(request.locateResource(calURL))
-        yield updatecal
-        updatecal = updatecal.getResult()
-
-    yield calmatch, updatecal, calURL
-
-findCalendarMatch = deferredGenerator(findCalendarMatch)    
-
-def matchComponentInCalendar(collection, calendar):
-    """
-    See if the component in the provided iTIP calendar object matches any in the specified calendar
-    collection.
+    @staticmethod
+    def canAutoRespond(calendar):
+        """
+        Check whether the METHOD of this iTIP calendar object is one we can process. Also,
+        we will only handle VEVENTs right now.
     
-    @param collection: L{CalDAVFile} for the calendar collection to examine.
-    @param calendar: L{Component} for calendar to examine.
-    @return: C{list} of resource names found.
-    """
-
-    try:
-        # Extract UID from primary component (note we allow multiple components to be present
-        # because CANCEL requests can have multiple components).
-        comp = calendar.mainComponent(allow_multiple=True)
-        uid = comp.propertyValue("UID")
+        @param calendar: L{Component} for calendar to examine.
+        @return: C{True} if we can auto-respond, C{False} if not.
+        """
+    
+        try:
+            method = calendar.propertyValue("METHOD")
+            if method not in ("REQUEST", "ADD", "CANCEL"):
+                return False
+            if calendar.mainType() not in ("VEVENT"):
+                return False
+        except ValueError:
+            return False
         
-        # Now use calendar collection index to find all other resources with the same UID
-        index = collection.index()
-        result = index.resourceNamesForUID(uid)
+        return True
+    
+    @deferredGenerator
+    def findCalendarMatch(self):
+        # Try and find a match to any components on existing calendars listed as contributing
+        # to free-busy as we will need to update those with the new one.
         
-        # There can be only one
-        if len(result) > 0: 
-            return result[0]
-        else:
-            return None
-    except ValueError:
-        return None
-
-def findMatchingComponent(component, calendar):
-    """
-    See if any overridden component in the provided iTIP calendar object matches the specified component.
+        # Find the current recipients calendar-free-busy-set
+        calendars = waitForDeferred(self.getCalendarsToMatch())
+        yield calendars
+        calendars = calendars.getResult()
     
-    @param component: the component to try and match.
-    @type component: L{Component}
-    @param calendar: the calendar to find a match in.
-    @type calendar: L{Component}
-    @return: L{Component} for matching component,
-        or C{None} if not found.
-    """
-
-    # Extract RECURRENCE-ID value from component
-    rid = component.getRecurrenceIDUTC()
+        # Find the first calendar in the list with a component matching the one we are processing
+        calmatch = None
+        updatecal = None
+        calURL = None
+        for calURL in calendars:
+            updatecal = waitForDeferred(self.request.locateResource(calURL))
+            yield updatecal
+            updatecal = updatecal.getResult()
+            if updatecal is None or not updatecal.exists() or not isCalendarCollectionResource(updatecal):
+                # We will ignore missing calendars. If the recipient has failed to
+                # properly manage the free busy set that should not prevent us from working.
+                continue
+            calmatch = self.matchComponentInCalendar(updatecal, self.calendar)
+            if calmatch:
+                logging.info("Found calendar component %s matching new iTIP message in %s." % (calmatch, calURL), system="iTIP")
+                break
+        
+        if calmatch is None and len(calendars):
+            calURL = calendars[0]
+            updatecal = waitForDeferred(self.request.locateResource(calURL))
+            yield updatecal
+            updatecal = updatecal.getResult()
     
-    # Return the one that matches in the calendar
-    return calendar.overriddenComponent(rid)
-
-def mergeComponents(newcal, oldcal):
-    """
-    Merge the overridden instance components in newcal into old cal replacing any
-    matching components there.
-
-    @param newcal: the new overridden instances to use.
-    @type newcal: L{Component}
-    @param oldcal: the component to merge into.
-    @type oldcal: L{Component}
-    """
+        yield calmatch, updatecal, calURL
     
-    # FIXME: going to ignore VTIMEZONE - i.e. will assume that the component being added
-    # use a TZID that is already specified in the old component set.
-
-    # We will update the SEQUENCE on the master to the highest value of the current one on the master
-    # or the ones in the components we are changing.
-
-    for component in newcal.subcomponents():
-        if component.name() == "VTIMEZONE":
-            continue
+    @deferredGenerator
+    def getCalendarsToMatch(self):
+        # Determine the set of calendar URIs for a principal need to be searched.
         
-        rid = component.getRecurrenceIDUTC()
-        old_component = oldcal.overriddenComponent(rid)
-        if old_component:
-            oldcal.removeComponent(old_component)
-        oldcal.addComponent(component)
+        # Find the current recipients calendar-free-busy-set
+        fbset = waitForDeferred(self.principal.calendarFreeBusyURIs(self.request))
+        yield fbset
+        fbset = fbset.getResult()
+    
+        yield fbset
 
-def getAllInfo(collection, calendar, ignore):
-    """
-    Find each component in the calendar collection that has a matching UID with
-    the supplied component, and get useful synchronization details from it, ignoring
-    the one with the supplied resource name.
-
-    @param collection: the L{CalDAVFile} for the calendar collection.
-    @param calendar: the L{Component} for the component being compared with.
-    @param ignore: the C{str} containing the name of a resource to ignore,
-        or C{None} if none to ignore.
-    @return: C{list} of synchronization information for each resource found.
-    """
-    names = []
-    try:
-        # Extract UID from primary component (note we allow multiple components to be present
-        # because CANCEL requests can have multiple components).
-        comp = calendar.mainComponent(allow_multiple=True)
-        uid = comp.propertyValue("UID")
+    def matchComponentInCalendar(self, collection, calendar):
+        """
+        See if the component in the provided iTIP calendar object matches any in the specified calendar
+        collection.
         
-        # Now use calendar collection index to find all other resources with the same UID
-        index = collection.index()
-        names = index.resourceNamesForUID(uid)
+        @param collection: L{CalDAVFile} for the calendar collection to examine.
+        @param calendar: L{Component} for calendar to examine.
+        @return: C{list} of resource names found.
+        """
+    
+        try:
+            # Extract UID from primary component (note we allow multiple components to be present
+            # because CANCEL requests can have multiple components).
+            comp = calendar.mainComponent(allow_multiple=True)
+            uid = comp.propertyValue("UID")
+            
+            # Now use calendar collection index to find all other resources with the same UID
+            index = collection.index()
+            result = index.resourceNamesForUID(uid)
+            
+            # There can be only one
+            if len(result) > 0: 
+                return result[0]
+            else:
+                return None
+        except ValueError:
+            return None
+    
+    def findMatchingComponent(self, component, calendar):
+        """
+        See if any overridden component in the provided iTIP calendar object matches the specified component.
         
-        # Remove the one we want to ignore
-        if ignore is not None:
-            names = [name for name in names if name != ignore.fp.basename()]
-    except ValueError:
-        return []
+        @param component: the component to try and match.
+        @type component: L{Component}
+        @param calendar: the calendar to find a match in.
+        @type calendar: L{Component}
+        @return: L{Component} for matching component,
+            or C{None} if not found.
+        """
     
-    # Now get info for each name
-    result = []
-    for name in names:
-        cal = collection.iCalendar(name)
-        result.append(getSyncInfo(name, cal))
-
-    return result
+        # Extract RECURRENCE-ID value from component
+        rid = component.getRecurrenceIDUTC()
+        
+        # Return the one that matches in the calendar
+        return calendar.overriddenComponent(rid)
     
-def getSyncInfo(name, calendar):
-    """
-    Get property value details needed to synchronize iTIP components.
+    def mergeComponents(self, newcal, oldcal):
+        """
+        Merge the overridden instance components in newcal into old cal replacing any
+        matching components there.
     
-    @param calendar: L{Component} for calendar to check.
-    @return: C{tuple} of (uid, seq, dtstamp, r-id) some of which may be C{None} if property does not exist
-    """
-    try:
-        # Extract components from primary component (note we allow multiple components to be present
-        # because CANCEL requests can have multiple components).
-        comp = calendar.mainComponent(allow_multiple=True)
-        uid, seq, dtstamp, rid = getComponentSyncInfo(comp)
+        @param newcal: the new overridden instances to use.
+        @type newcal: L{Component}
+        @param oldcal: the component to merge into.
+        @type oldcal: L{Component}
+        """
         
-    except ValueError:
-        return (name, None, None, None, None)
+        # FIXME: going to ignore VTIMEZONE - i.e. will assume that the component being added
+        # use a TZID that is already specified in the old component set.
     
-    return (name, uid, seq, dtstamp, rid)
-
-def getComponentSyncInfo(component):
-    """
-    Get property value details needed to synchronize iTIP components.
+        # We will update the SEQUENCE on the master to the highest value of the current one on the master
+        # or the ones in the components we are changing.
     
-    @param component: L{Component} to check.
-    @return: C{tuple} of (uid, seq, dtstamp, r-id) some of which may be C{None} if property does not exist
-    """
-    try:
-        # Extract items from component
-        uid = component.propertyValue("UID")
-        seq = component.propertyValue("SEQUENCE")
-        dtstamp = component.propertyValue("DTSTAMP")
-        rid = component.propertyValue("RECURRENCE-ID")
+        for component in newcal.subcomponents():
+            if component.name() == "VTIMEZONE":
+                continue
+            
+            rid = component.getRecurrenceIDUTC()
+            old_component = oldcal.overriddenComponent(rid)
+            if old_component:
+                oldcal.removeComponent(old_component)
+            oldcal.addComponent(component)
+    
+    def getAllInfo(self, collection, calendar, ignore):
+        """
+        Find each component in the calendar collection that has a matching UID with
+        the supplied component, and get useful synchronization details from it, ignoring
+        the one with the supplied resource name.
+    
+        @param collection: the L{CalDAVFile} for the calendar collection.
+        @param calendar: the L{Component} for the component being compared with.
+        @param ignore: the C{str} containing the name of a resource to ignore,
+            or C{None} if none to ignore.
+        @return: C{list} of synchronization information for each resource found.
+        """
+        names = []
+        try:
+            # Extract UID from primary component (note we allow multiple components to be present
+            # because CANCEL requests can have multiple components).
+            comp = calendar.mainComponent(allow_multiple=True)
+            uid = comp.propertyValue("UID")
+            
+            # Now use calendar collection index to find all other resources with the same UID
+            index = collection.index()
+            names = index.resourceNamesForUID(uid)
+            
+            # Remove the one we want to ignore
+            if ignore is not None:
+                names = [name for name in names if name != ignore.fp.basename()]
+        except ValueError:
+            return []
         
-    except ValueError:
-        return (None, None, None, None)
+        # Now get info for each name
+        result = []
+        for name in names:
+            cal = collection.iCalendar(name)
+            result.append(self.getSyncInfo(name, cal))
     
-    return (uid, seq, dtstamp, rid)
-
-def compareComponents(component1, component2):
-    """
-    Compare synchronization information for two components to see if they match according to iTIP.
-
-    @param component1: first component to check.
-    @type component1: L{Component}
-    @param component2: second component to check.
-    @type component2: L{Component}
+        return result
+        
+    def getSyncInfo(self, name, calendar):
+        """
+        Get property value details needed to synchronize iTIP components.
+        
+        @param calendar: L{Component} for calendar to check.
+        @return: C{tuple} of (uid, seq, dtstamp, r-id) some of which may be C{None} if property does not exist
+        """
+        try:
+            # Extract components from primary component (note we allow multiple components to be present
+            # because CANCEL requests can have multiple components).
+            comp = calendar.mainComponent(allow_multiple=True)
+            uid, seq, dtstamp, rid = self.getComponentSyncInfo(comp)
+            
+        except ValueError:
+            return (name, None, None, None, None)
+        
+        return (name, uid, seq, dtstamp, rid)
     
-    @return: 0, 1, -1 as per compareSyncInfo.
-    """
-    info1 = (None,) + getComponentSyncInfo(component1)
-    info2 = (None,) + getComponentSyncInfo(component2)
-    return compareSyncInfo(info1, info2)
-
-def compareSyncInfo(info1, info2):
-    """
-    Compare two synchronization information records.
+    def getComponentSyncInfo(self, component):
+        """
+        Get property value details needed to synchronize iTIP components.
+        
+        @param component: L{Component} to check.
+        @return: C{tuple} of (uid, seq, dtstamp, r-id) some of which may be C{None} if property does not exist
+        """
+        try:
+            # Extract items from component
+            uid = component.propertyValue("UID")
+            seq = component.propertyValue("SEQUENCE")
+            dtstamp = component.propertyValue("DTSTAMP")
+            rid = component.propertyValue("RECURRENCE-ID")
+            
+        except ValueError:
+            return (None, None, None, None)
+        
+        return (uid, seq, dtstamp, rid)
     
-    @param info1: a C{tuple} as returned by L{getSyncInfo}.
-    @param info2: a C{tuple} as returned by L{getSyncInfo}.
-    @return: 1 if info1 > info2, 0 if info1 == info2, -1 if info1 < info2
-    """
-    # UIDs MUST match
-    assert info1[1] == info2[1]
+    def compareComponents(self, component1, component2):
+        """
+        Compare synchronization information for two components to see if they match according to iTIP.
     
-    # Look for sequence
-    if (info1[2] is not None) and (info2[2] is not None):
-        if info1[2] > info2[2]:
+        @param component1: first component to check.
+        @type component1: L{Component}
+        @param component2: second component to check.
+        @type component2: L{Component}
+        
+        @return: 0, 1, -1 as per compareSyncInfo.
+        """
+        info1 = (None,) + self.getComponentSyncInfo(component1)
+        info2 = (None,) + self.getComponentSyncInfo(component2)
+        return self.compareSyncInfo(info1, info2)
+    
+    def compareSyncInfo(self, info1, info2):
+        """
+        Compare two synchronization information records.
+        
+        @param info1: a C{tuple} as returned by L{getSyncInfo}.
+        @param info2: a C{tuple} as returned by L{getSyncInfo}.
+        @return: 1 if info1 > info2, 0 if info1 == info2, -1 if info1 < info2
+        """
+        # UIDs MUST match
+        assert info1[1] == info2[1]
+        
+        # Look for sequence
+        if (info1[2] is not None) and (info2[2] is not None):
+            if info1[2] > info2[2]:
+                return 1
+            if info1[2] < info2[2]:
+                return -1
+        elif (info1[2] is not None) and (info2[2] is None):
             return 1
-        if info1[2] < info2[2]:
+        elif (info1[2] is None) and (info2[2] is not None):
             return -1
-    elif (info1[2] is not None) and (info2[2] is None):
-        return 1
-    elif (info1[2] is None) and (info2[2] is not None):
-        return -1
-
-    # Look for DTSTAMP
-    if (info1[3] is not None) and (info2[3] is not None):
-        if info1[3] > info2[3]:
+    
+        # Look for DTSTAMP
+        if (info1[3] is not None) and (info2[3] is not None):
+            if info1[3] > info2[3]:
+                return 1
+            if info1[3] < info2[3]:
+                return -1
+        elif (info1[3] is not None) and (info2[3] is None):
             return 1
-        if info1[3] < info2[3]:
+        elif (info1[3] is None) and (info2[3] is not None):
             return -1
-    elif (info1[3] is not None) and (info2[3] is None):
-        return 1
-    elif (info1[3] is None) and (info2[3] is not None):
-        return -1
-
-    return 0
+    
+        return 0

Modified: CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/logging.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/logging.py	2008-02-01 22:00:25 UTC (rev 2114)
+++ CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/logging.py	2008-02-03 19:53:18 UTC (rev 2115)
@@ -21,12 +21,18 @@
 import datetime
 import os
 import time
+from cStringIO import StringIO
 
 from twisted.python import log
 from twisted.internet import protocol
+from twisted.internet.defer import deferredGenerator
+from twisted.internet.defer import waitForDeferred
 
 from twisted.web2 import iweb
 from twisted.web2.dav import davxml
+from twisted.web2 import responsecode
+from twisted.web2.dav.util import allDataFromStream
+from twisted.web2.stream import MemoryStream
 from twisted.web2.log import BaseCommonAccessLoggingObserver
 from twisted.web2.log import LogWrapperResource
 
@@ -44,7 +50,7 @@
 
 logtypes = {"none": 0, "error": 1, "warning": 2, "info": 3, "debug": 4}
 
-currentLogLevel = logtypes["error"]
+currentLogLevel = logtypes["debug"]
 previousLogLevel = logtypes["debug"]
 
 def toggle():
@@ -120,6 +126,61 @@
     if canLog("debug"):
         log.msg(message, debug=True, **kwargs)
 
+ at deferredGenerator
+def logRequest(message, request, **kwargs):
+    """
+    Log an HTTP request.
+    """
+    iostr = StringIO()
+    iostr.write("%s\n" % (message,))
+    if hasattr(request, "clientproto"):
+        protocol = "HTTP/%d.%d" % (request.clientproto[0], request.clientproto[1],)
+    else:
+        protocol = "HTTP/1.1"
+    iostr.write("%s %s %s\n" % (request.method, request.uri, protocol,))
+    for name, valuelist in request.headers.getAllRawHeaders():
+        for value in valuelist:
+            # Do not log authorization details
+            if name not in ("Authorization",):
+                iostr.write("%s: %s\n" % (name, value))
+    
+    iostr.write("\n")
+    data = waitForDeferred(allDataFromStream(request.stream))
+    yield data
+    data = data.getResult()
+    iostr.write(data)
+    
+    request.stream = MemoryStream(data)
+    request.stream.doStartReading = None
+
+    log.msg(iostr.getvalue(), **kwargs)
+
+ at deferredGenerator
+def logResponse(message, response, **kwargs):
+    """
+    Log an HTTP request.
+    """
+    iostr = StringIO()
+    iostr.write("%s\n" % (message,))
+    code_message = responsecode.RESPONSES.get(response.code, "Unknown Status")
+    iostr.write("HTTP/1.1 %s %s\n" % (response.code, code_message,))
+    for name, valuelist in response.headers.getAllRawHeaders():
+        for value in valuelist:
+            # Do not log authorization details
+            if name not in ("Authorization",):
+                iostr.write("%s: %s\n" % (name, value))
+    
+    iostr.write("\n")
+    data = waitForDeferred(allDataFromStream(response.stream))
+    yield data
+    data = data.getResult()
+    iostr.write(data)
+    
+    response.stream = MemoryStream(data)
+    response.stream.doStartReading = None
+
+    log.msg(iostr.getvalue(), **kwargs)
+
 class DirectoryLogWrapperResource(LogWrapperResource):
     
     def __init__(self, resource, directory):

Modified: CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/method/copymove.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/method/copymove.py	2008-02-01 22:00:25 UTC (rev 2114)
+++ CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/method/copymove.py	2008-02-03 19:53:18 UTC (rev 2115)
@@ -32,7 +32,7 @@
 from twisted.web2.http import StatusResponse, HTTPError
 
 from twistedcaldav.caldavxml import caldav_namespace
-from twistedcaldav.method.put_common import storeCalendarObjectResource
+from twistedcaldav.method.put_common import StoreCalendarObjectResource
 from twistedcaldav.resource import isCalendarCollectionResource
 
 def http_COPY(self, request):
@@ -99,7 +99,7 @@
     # May need to add a location header
     addLocation(request, destination_uri)
 
-    x = waitForDeferred(storeCalendarObjectResource(
+    storer = StoreCalendarObjectResource(
         request = request,
         source = self,
         source_uri = request.uri,
@@ -109,7 +109,8 @@
         destination_uri = destination_uri,
         destinationparent = destinationparent,
         destinationcal = destinationcal,
-    ))
+    )
+    x = waitForDeferred(storer.run())
     yield x
     yield x.getResult()
 
@@ -183,18 +184,19 @@
     # May need to add a location header
     addLocation(request, destination_uri)
 
-    x = waitForDeferred(storeCalendarObjectResource(
+    storer = StoreCalendarObjectResource(
         request = request,
         source = self,
         source_uri = request.uri,
         sourceparent = sourceparent,
         sourcecal = sourcecal,
+        deletesource = True,
         destination = destination,
         destination_uri = destination_uri,
         destinationparent = destinationparent,
         destinationcal = destinationcal,
-        deletesource = True,
-    ))
+    )
+    x = waitForDeferred(storer.run())
     yield x
     yield x.getResult()
 
@@ -207,15 +209,15 @@
     if that is the case.
     @return: tuple::
         result:           True if special CalDAV processing required, False otherwise
-                          NB If there is any type of error with the request, return False
-                          and allow normal COPY/MOVE processing to return the error.
+            NB If there is any type of error with the request, return False
+            and allow normal COPY/MOVE processing to return the error.
         sourcecal:        True if source is in a calendar collection, False otherwise
         sourceparent:     The parent resource for the source
         destination_uri:  The URI of the destination resource
         destination:      CalDAVFile of destination if special proccesing required,
         None otherwise
         destinationcal:   True if the destination is in a calendar collection,
-                          False otherwise
+            False otherwise
         destinationparent:The parent resource for the destination
         
     """

Modified: CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/method/put.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/method/put.py	2008-02-01 22:00:25 UTC (rev 2114)
+++ CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/method/put.py	2008-02-03 19:53:18 UTC (rev 2115)
@@ -28,7 +28,7 @@
 from twisted.web2.http import HTTPError, StatusResponse
 
 from twistedcaldav.caldavxml import caldav_namespace
-from twistedcaldav.method.put_common import storeCalendarObjectResource
+from twistedcaldav.method.put_common import StoreCalendarObjectResource
 from twistedcaldav.resource import isPseudoCalendarCollectionResource
 
 def http_PUT(self, request):
@@ -58,15 +58,15 @@
                 # Use correct DAV:error response
                 raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
 
-            d = waitForDeferred(storeCalendarObjectResource(
+            storer = StoreCalendarObjectResource(
                 request = request,
-                sourcecal = False,
-                calendardata = calendardata,
                 destination = self,
                 destination_uri = request.uri,
                 destinationcal = True,
-                destinationparent = parent,)
+                destinationparent = parent,
+                calendar = calendardata,
             )
+            d = waitForDeferred(storer.run())
             yield d
             yield d.getResult()
             return

Modified: CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/method/put_common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/method/put_common.py	2008-02-01 22:00:25 UTC (rev 2114)
+++ CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/method/put_common.py	2008-02-03 19:53:18 UTC (rev 2115)
@@ -18,8 +18,10 @@
 PUT/COPY/MOVE common behavior.
 """
 
-__all__ = ["storeCalendarObjectResource"]
+__all__ = ["StoreCalendarObjectResource"]
 
+import types
+
 from twisted.internet import reactor
 from twisted.internet.defer import Deferred
 from twisted.internet.defer import deferredGenerator
@@ -50,55 +52,8 @@
 from twistedcaldav.index import ReservationError
 from twistedcaldav.instance import TooManyInstancesError
 
-def storeCalendarObjectResource(
-    request,
-    sourcecal, destinationcal,
-    source=None, source_uri=None, sourceparent=None,
-    destination=None, destination_uri=None, destinationparent=None,
-    calendardata=None,
-    deletesource=False,
-    isiTIP=False
-):
-    """
-    Function that does common PUT/COPY/MOVE behaviour.
+class StoreCalendarObjectResource(object):
     
-    @param request:           the L{twisted.web2.server.Request} for the current HTTP request.
-    @param source:            the L{CalDAVFile} for the source resource to copy from, or None if source data
-        is to be read from the request.
-    @param source_uri:        the URI for the source resource.
-    @param destination:       the L{CalDAVFile} for the destination resource to copy into.
-    @param destination_uri:   the URI for the destination resource.
-    @param calendardata:      the string data read directly from the request body if there is no source, None otherwise.
-    @param sourcecal:         True if the source resource is in a calendar collection, False otherwise.
-    @param destinationcal:    True if the destination resource is in a calendar collection, False otherwise
-    @param sourceparent:      the L{CalDAVFile} for the source resource's parent collection, or None if source is None.
-    @param destinationparent: the L{CalDAVFile} for the destination resource's parent collection.
-    @param deletesource:      True if the source resource is to be deleted on successful completion, False otherwise.
-    @param isiTIP:            True if relaxed calendar data validation is to be done, False otherwise.
-    @return:                  a Deferred with a status response result.
-    """
-    
-    try:
-        assert destination is not None and destinationparent is not None and destination_uri is not None
-        assert (source is None and sourceparent is None) or (source is not None and sourceparent is not None)
-        assert (calendardata is None and source is not None) or (calendardata is not None and source is None)
-        assert not deletesource or (deletesource and source is not None)
-    except AssertionError:
-        log.err("Invalid arguments to storeCalendarObjectResource():")
-        log.err("request=%s\n" % (request,))
-        log.err("sourcecal=%s\n" % (sourcecal,))
-        log.err("destinationcal=%s\n" % (destinationcal,))
-        log.err("source=%s\n" % (source,))
-        log.err("source_uri=%s\n" % (source_uri,))
-        log.err("sourceparent=%s\n" % (sourceparent,))
-        log.err("destination=%s\n" % (destination,))
-        log.err("destination_uri=%s\n" % (destination_uri,))
-        log.err("destinationparent=%s\n" % (destinationparent,))
-        log.err("calendardata=%s\n" % (calendardata,))
-        log.err("deletesource=%s\n" % (deletesource,))
-        log.err("isiTIP=%s\n" % (isiTIP,))
-        raise
-
     class RollbackState(object):
         """
         This class encapsulates the state needed to rollback the entire PUT/COPY/MOVE
@@ -106,7 +61,8 @@
         processed. The DoRollback method will actually execute the rollback operations.
         """
         
-        def __init__(self):
+        def __init__(self, storer):
+            self.storer = storer
             self.active = True
             self.source_copy = None
             self.destination_copy = None
@@ -126,32 +82,32 @@
                 logging.debug("Rollback: rollback", system="Store Resource")
                 try:
                     if self.source_copy and self.source_deleted:
-                        self.source_copy.moveTo(source.fp)
-                        logging.debug("Rollback: source restored %s to %s" % (self.source_copy.path, source.fp.path), system="Store Resource")
+                        self.source_copy.moveTo(self.storer.source.fp)
+                        logging.debug("Rollback: source restored %s to %s" % (self.source_copy.path, self.storer.source.fp.path), system="Store Resource")
                         self.source_copy = None
                         self.source_deleted = False
                     if self.destination_copy:
-                        destination.fp.remove()
-                        logging.debug("Rollback: destination restored %s to %s" % (self.destination_copy.path, destination.fp.path), system="Store Resource")
-                        self.destination_copy.moveTo(destination.fp)
+                        self.storer.destination.fp.remove()
+                        logging.debug("Rollback: destination restored %s to %s" % (self.destination_copy.path, self.storer.destination.fp.path), system="Store Resource")
+                        self.destination_copy.moveTo(self.storer.destination.fp)
                         self.destination_copy = None
                     elif self.destination_created:
-                        if destinationcal:
-                            doRemoveDestinationIndex()
-                            logging.debug("Rollback: destination index removed %s" % (destination.fp.path,), system="Store Resource")
+                        if self.storer.destinationcal:
+                            self.storer.doRemoveDestinationIndex()
+                            logging.debug("Rollback: destination index removed %s" % (self.storer.destination.fp.path,), system="Store Resource")
                             self.destination_index_deleted = False
-                        destination.fp.remove()
-                        logging.debug("Rollback: destination removed %s" % (destination.fp.path,), system="Store Resource")
+                        self.storer.destination.fp.remove()
+                        logging.debug("Rollback: destination removed %s" % (self.storer.destination.fp.path,), system="Store Resource")
                         self.destination_created = False
                     if self.destination_index_deleted:
                         # Must read in calendar for destination being re-indexed
-                        doDestinationIndex(destination.iCalendar())
+                        self.storer.doDestinationIndex(self.storer.destination.iCalendar())
                         self.destination_index_deleted = False
-                        logging.debug("Rollback: destination re-indexed %s" % (destination.fp.path,), system="Store Resource")
+                        logging.debug("Rollback: destination re-indexed %s" % (self.storer.destination.fp.path,), system="Store Resource")
                     if self.source_index_deleted:
-                        doSourceIndexRecover()
+                        self.storer.doSourceIndexRecover()
                         self.destination_index_deleted = False
-                        logging.debug("Rollback: soyurce re-indexed %s" % (source.fp.path,), system="Store Resource")
+                        logging.debug("Rollback: soyurce re-indexed %s" % (self.storer.source.fp.path,), system="Store Resource")
                 except:
                     log.err("Rollback: exception caught and not handled: %s" % failure.Failure())
 
@@ -174,37 +130,201 @@
                 self.source_deleted = False
                 self.source_index_deleted = False
                 self.destination_index_deleted = False
+
+    class UIDReservation(object):
+        
+        def __init__(self, index, uid, uri):
+            self.reserved = False
+            self.index = index
+            self.uid = uid
+            self.uri = uri
+            
+        @deferredGenerator
+        def reserve(self):
+            
+            # Lets use a deferred for this and loop a few times if we cannot reserve so that we give
+            # time to whoever has the reservation to finish and release it.
+            failure_count = 0
+            while(failure_count < 5):
+                try:
+                    self.index.reserveUID(self.uid)
+                    self.reserved = True
+                    break
+                except ReservationError:
+                    self.reserved = False
+                failure_count += 1
+                
+                d = Deferred()
+                def _timedDeferred():
+                    d.callback(True)
+                reactor.callLater(0.1, _timedDeferred)
+                pause = waitForDeferred(d)
+                yield pause
+                pause.getResult()
+            
+            if self.uri and not self.reserved:
+                raise HTTPError(StatusResponse(responsecode.CONFLICT, "Resource: %s currently in use." % (self.uri,)))
+        
+        def unreserve(self):
+            if self.reserved:
+                self.index.unreserveUID(self.uid)
+                self.reserved = False
+
+    def __init__(
+        self,
+        request,
+        source=None, source_uri=None, sourceparent=None, sourcecal=False, deletesource=False,
+        destination=None, destination_uri=None, destinationparent=None, destinationcal=True,
+        calendar=None,
+        isiTIP=False
+    ):
+        """
+        Function that does common PUT/COPY/MOVE behaviour.
+        
+        @param request:           the L{twisted.web2.server.Request} for the current HTTP request.
+        @param source:            the L{CalDAVFile} for the source resource to copy from, or None if source data
+            is to be read from the request.
+        @param source_uri:        the URI for the source resource.
+        @param destination:       the L{CalDAVFile} for the destination resource to copy into.
+        @param destination_uri:   the URI for the destination resource.
+        @param calendar:          the C{str} or L{Component} calendar data if there is no source, None otherwise.
+        @param sourcecal:         True if the source resource is in a calendar collection, False otherwise.
+        @param destinationcal:    True if the destination resource is in a calendar collection, False otherwise
+        @param sourceparent:      the L{CalDAVFile} for the source resource's parent collection, or None if source is None.
+        @param destinationparent: the L{CalDAVFile} for the destination resource's parent collection.
+        @param deletesource:      True if the source resource is to be deleted on successful completion, False otherwise.
+        @param isiTIP:            True if relaxed calendar data validation is to be done, False otherwise.
+        """
+        
+        # Check that all arguments are valid
+        try:
+            assert destination is not None and destinationparent is not None and destination_uri is not None
+            assert (source is None and sourceparent is None) or (source is not None and sourceparent is not None)
+            assert (calendar is None and source is not None) or (calendar is not None and source is None)
+            assert not deletesource or (deletesource and source is not None)
+        except AssertionError:
+            log.err("Invalid arguments to StoreCalendarObjectResource.__init__():")
+            log.err("request=%s\n" % (request,))
+            log.err("sourcecal=%s\n" % (sourcecal,))
+            log.err("destinationcal=%s\n" % (destinationcal,))
+            log.err("source=%s\n" % (source,))
+            log.err("source_uri=%s\n" % (source_uri,))
+            log.err("sourceparent=%s\n" % (sourceparent,))
+            log.err("destination=%s\n" % (destination,))
+            log.err("destination_uri=%s\n" % (destination_uri,))
+            log.err("destinationparent=%s\n" % (destinationparent,))
+            log.err("calendar=%s\n" % (calendar,))
+            log.err("deletesource=%s\n" % (deletesource,))
+            log.err("isiTIP=%s\n" % (isiTIP,))
+            raise
     
-    rollback = RollbackState()
+        self.request = request
+        self.sourcecal = sourcecal
+        self.destinationcal = destinationcal
+        self.source = source
+        self.source_uri = source_uri
+        self.sourceparent = sourceparent
+        self.destination = destination
+        self.destination_uri = destination_uri
+        self.destinationparent = destinationparent
+        self.calendar = calendar
+        self.calendardata = None
+        self.deletesource = deletesource
+        self.isiTIP = isiTIP
+        
+        self.rollback = None
 
-    def validResourceName():
+    def fullValidation(self):
         """
+        Do full validaion of source and destination calendar data.
+        """
+
+        if self.destinationcal:
+            # Valid resource name check
+            result, message = self.validResourceName()
+            if not result:
+                log.err(message)
+                raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Resource name not allowed"))
+
+            if not self.sourcecal:
+                # Valid content type check on the source resource if its not in a calendar collection
+                if self.source is not None:
+                    result, message = self.validContentType()
+                    if not result:
+                        log.err(message)
+                        raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "supported-calendar-data")))
+                
+                    # At this point we need the calendar data to do more tests
+                    self.calendar = self.source.iCalendar()
+                else:
+                    try:
+                        if type(self.calendar) in (types.StringType, types.UnicodeType,):
+                            self.calendardata = self.calendar
+                            self.calendar = Component.fromString(self.calendar)
+                    except ValueError, e:
+                        log.err(str(e))
+                        raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+                        
+                # Valid calendar data check
+                result, message = self.validCalendarDataCheck()
+                if not result:
+                    log.err(message)
+                    raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+                    
+                # Valid calendar data for CalDAV check
+                result, message = self.validCalDAVDataCheck()
+                if not result:
+                    log.err(message)
+                    raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-object-resource")))
+
+                # Must have a valid UID at this point
+                self.uid = self.calendar.resourceUID()
+            else:
+                # Get uid from original resource
+                self.source_index = self.sourceparent.index()
+                self.uid = self.source_index.resourceUIDForName(self.source.fp.basename())
+                if self.uid is None:
+                    log.err("Source calendar does not have a UID: %s" % self.source.fp.basename())
+                    raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-object-resource")))
+
+                # FIXME: We need this here because we have to re-index the destination. Ideally it
+                # would be better to copy the index entries from the source and add to the destination.
+                self.calendar = self.source.iCalendar()
+
+            # Valid calendar data size check
+            result, message = self.validSizeCheck()
+            if not result:
+                log.err(message)
+                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "max-resource-size")))
+    
+    def validResourceName(self):
+        """
         Make sure that the resource name for the new resource is valid.
         """
         result = True
         message = ""
-        filename = destination.fp.basename()
+        filename = self.destination.fp.basename()
         if filename.startswith("."):
             result = False
             message = "File name %s not allowed in calendar collection" % (filename,)
 
         return result, message
         
-    def validContentType():
+    def validContentType(self):
         """
         Make sure that the content-type of the source resource is text/calendar.
         This test is only needed when the source is not in a calendar collection.
         """
         result = True
         message = ""
-        content_type = source.contentType()
+        content_type = self.source.contentType()
         if not ((content_type.mediaType == "text") and (content_type.mediaSubtype == "calendar")):
             result = False
             message = "MIME type %s not allowed in calendar collection" % (content_type,)
 
         return result, message
         
-    def validCalendarDataCheck():
+    def validCalendarDataCheck(self):
         """
         Check that the calendar data is valid iCalendar.
         @return:         tuple: (True/False if the calendra data is valid,
@@ -212,19 +332,19 @@
         """
         result = True
         message = ""
-        if calendar is None:
+        if self.calendar is None:
             result = False
             message = "Empty resource not allowed in calendar collection"
         else:
             try:
-                calendar.validCalendarForCalDAV()
+                self.calendar.validCalendarForCalDAV()
             except ValueError, e:
                 result = False
                 message = "Invalid calendar data: %s" % (e,)
         
         return result, message
     
-    def validCalDAVDataCheck():
+    def validCalDAVDataCheck(self):
         """
         Check that the calendar data is valid as a CalDAV calendar object resource.
         @return:         tuple: (True/False if the calendar data is valid,
@@ -233,17 +353,17 @@
         result = True
         message = ""
         try:
-            if isiTIP:
-                calendar.validateComponentsForCalDAV(True)
+            if self.isiTIP:
+                self.calendar.validateComponentsForCalDAV(True)
             else:
-                calendar.validateForCalDAV()
+                self.calendar.validateForCalDAV()
         except ValueError, e:
             result = False
             message = "Calendar data does not conform to CalDAV requirements: %s" % (e,)
         
         return result, message
     
-    def validSizeCheck():
+    def validSizeCheck(self):
         """
         Make sure that the content-type of the source resource is text/calendar.
         This test is only needed when the source is not in a calendar collection.
@@ -251,14 +371,14 @@
         result = True
         message = ""
         if config.MaximumAttachmentSize:
-            calsize = len(str(calendar))
+            calsize = len(str(self.calendar))
             if calsize > config.MaximumAttachmentSize:
                 result = False
                 message = "Data size %d bytes is larger than allowed limit %d bytes" % (calsize, config.MaximumAttachmentSize)
 
         return result, message
 
-    def noUIDConflict(uid):
+    def noUIDConflict(self, uid):
         """
         Check that the UID of the new calendar object conforms to the requirements of
         CalDAV, i.e. it must be unique in the collection and we must not overwrite a
@@ -274,12 +394,12 @@
 
         # Adjust for a move into same calendar collection
         oldname = None
-        if sourceparent and (sourceparent.fp.path == destinationparent.fp.path) and deletesource:
-            oldname = source.fp.basename()
+        if self.sourceparent and (self.sourceparent.fp.path == self.destinationparent.fp.path) and self.deletesource:
+            oldname = self.source.fp.basename()
 
         # UID must be unqiue
-        index = destinationparent.index()
-        if not index.isAllowedUID(uid, oldname, destination.fp.basename()):
+        index = self.destinationparent.index()
+        if not index.isAllowedUID(uid, oldname, self.destination.fp.basename()):
             rname = index.resourceNameForUID(uid)
             # This can happen if two simulataneous PUTs occur with the same UID.
             # i.e. one PUT has reserved the UID but has not yet written the resource,
@@ -291,330 +411,306 @@
             message = "Calendar resource %s already exists with same UID %s" % (rname, uid)
         else:
             # Cannot overwrite a resource with different UID
-            if destination.fp.exists():
-                olduid = index.resourceUIDForName(destination.fp.basename())
+            if self.destination.fp.exists():
+                olduid = index.resourceUIDForName(self.destination.fp.basename())
                 if olduid != uid:
-                    rname = destination.fp.basename()
+                    rname = self.destination.fp.basename()
                     result = False
                     message = "Cannot overwrite calendar resource %s with different UID %s" % (rname, olduid)
         
         return result, message, rname
 
-    try:
+    @deferredGenerator
+    def checkQuota(self):
         """
-        Handle validation operations here.
+        Get quota details for destination and source before we start messing with adding other files.
         """
-        reserved = False
-        if destinationcal:
-            # Valid resource name check
-            result, message = validResourceName()
-            if not result:
-                log.err(message)
-                raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Resource name not allowed"))
 
-            if not sourcecal:
-                # Valid content type check on the source resource if its not in a calendar collection
-                if source is not None:
-                    result, message = validContentType()
-                    if not result:
-                        log.err(message)
-                        raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "supported-calendar-data")))
-                
-                    # At this point we need the calendar data to do more tests
-                    calendar = source.iCalendar()
-                else:
-                    try:
-                        calendar = Component.fromString(calendardata)
-                    except ValueError, e:
-                        log.err(str(e))
-                        raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
-                        
-                # Valid calendar data check
-                result, message = validCalendarDataCheck()
-                if not result:
-                    log.err(message)
-                    raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
-                    
-                # Valid calendar data for CalDAV check
-                result, message = validCalDAVDataCheck()
-                if not result:
-                    log.err(message)
-                    raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-object-resource")))
-
-                # Must have a valid UID at this point
-                uid = calendar.resourceUID()
-            else:
-                # Get uid from original resource
-                source_index = sourceparent.index()
-                uid = source_index.resourceUIDForName(source.fp.basename())
-                if uid is None:
-                    log.err("Source calendar does not have a UID: %s" % source.fp.basename())
-                    raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-object-resource")))
-
-                # FIXME: We need this here because we have to re-index the destination. Ideally it
-                # would be better to copy the index entries from the source and add to the destination.
-                calendar = source.iCalendar()
-
-            # Valid calendar data size check
-            result, message = validSizeCheck()
-            if not result:
-                log.err(message)
-                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "max-resource-size")))
-
-            # Reserve UID
-            destination_index = destinationparent.index()
-            
-            # Lets use a deferred for this and loop a few times if we cannot reserve so that we give
-            # time to whoever has the reservation to finish and release it.
-            failure_count = 0
-            while(failure_count < 5):
-                try:
-                    destination_index.reserveUID(uid)
-                    reserved = True
-                    break
-                except ReservationError:
-                    reserved = False
-                failure_count += 1
-                
-                d = Deferred()
-                def _timedDeferred():
-                    d.callback(True)
-                reactor.callLater(0.1, _timedDeferred)
-                pause = waitForDeferred(d)
-                yield pause
-                pause.getResult()
-            
-            if destination_uri and not reserved:
-                raise HTTPError(StatusResponse(responsecode.CONFLICT, "Resource: %s currently in use." % (destination_uri,)))
-        
-            # uid conflict check - note we do this after reserving the UID to avoid a race condition where two requests
-            # try to write the same calendar data to two different resource URIs.
-            if not isiTIP:
-                result, message, rname = noUIDConflict(uid)
-                if not result:
-                    log.err(message)
-                    raise HTTPError(ErrorResponse(responsecode.FORBIDDEN,
-                        NoUIDConflict(davxml.HRef.fromString(joinURL(parentForURL(destination_uri), rname.encode("utf-8"))))
-                    ))
-            
-        """
-        Handle rollback setup here.
-        """
-
-        # Do quota checks on destination and source before we start messing with adding other files
-        if request is None:
-            destquota = None
+        if self.request is None:
+            self.destquota = None
         else:
-            destquota = waitForDeferred(destination.quota(request))
-            yield destquota
-            destquota = destquota.getResult()
-            if destquota is not None and destination.exists():
-                old_dest_size = waitForDeferred(destination.quotaSize(request))
-                yield old_dest_size
-                old_dest_size = old_dest_size.getResult()
+            self.destquota = waitForDeferred(self.destination.quota(self.request))
+            yield self.destquota
+            self.destquota = self.destquota.getResult()
+            if self.destquota is not None and self.destination.exists():
+                self.old_dest_size = waitForDeferred(self.destination.quotaSize(self.request))
+                yield self.old_dest_size
+                self.old_dest_size = self.old_dest_size.getResult()
             else:
-                old_dest_size = 0
+                self.old_dest_size = 0
             
-        if request is None:
-            sourcequota = None
-        elif source is not None:
-            sourcequota = waitForDeferred(source.quota(request))
-            yield sourcequota
-            sourcequota = sourcequota.getResult()
-            if sourcequota is not None and source.exists():
-                old_source_size = waitForDeferred(source.quotaSize(request))
-                yield old_source_size
-                old_source_size = old_source_size.getResult()
+        if self.request is None:
+            self.sourcequota = None
+        elif self.source is not None:
+            self.sourcequota = waitForDeferred(self.source.quota(self.request))
+            yield self.sourcequota
+            self.sourcequota = self.sourcequota.getResult()
+            if self.sourcequota is not None and self.source.exists():
+                self.old_source_size = waitForDeferred(self.source.quotaSize(self.request))
+                yield self.old_source_size
+                self.old_source_size = self.old_source_size.getResult()
             else:
-                old_source_size = 0
+                self.old_source_size = 0
         else:
-            sourcequota = None
-            old_source_size = 0
+            self.sourcequota = None
+            self.old_source_size = 0
 
-        # We may need to restore the original resource data if the PUT/COPY/MOVE fails,
-        # so rename the original file in case we need to rollback.
-        overwrite = destination.exists()
-        if overwrite:
-            rollback.destination_copy = FilePath(destination.fp.path)
-            rollback.destination_copy.path += ".rollback"
-            destination.fp.copyTo(rollback.destination_copy)
-            logging.debug("Rollback: backing up destination %s to %s" % (destination.fp.path, rollback.destination_copy.path), system="Store Resource")
-        else:
-            rollback.destination_created = True
-            logging.debug("Rollback: will create new destination %s" % (destination.fp.path,), system="Store Resource")
+        yield None
 
-        if deletesource:
-            rollback.source_copy = FilePath(source.fp.path)
-            rollback.source_copy.path += ".rollback"
-            source.fp.copyTo(rollback.source_copy)
-            logging.debug("Rollback: backing up source %s to %s" % (source.fp.path, rollback.source_copy.path), system="Store Resource")
-    
+    def setupRollback(self):
         """
-        Handle actual store operations here.
-        
-        The order in which this is done is import:
-            
-            1. Do store operation for new data
-            2. Delete source and source index if needed
-            3. Do new indexing if needed
-            
-        Note that we need to remove the source index BEFORE doing the destination index to cover the
-        case of a resource being 'renamed', i.e. moved within the same collection. Since the index UID
-        column must be unique in SQL, we cannot add the new index before remove the old one.
+        We may need to restore the original resource data if the PUT/COPY/MOVE fails,
+        so rename the original file in case we need to rollback.
         """
 
+        self.rollback = StoreCalendarObjectResource.RollbackState(self)
+        self.overwrite = self.destination.exists()
+        if self.overwrite:
+            self.rollback.destination_copy = FilePath(self.destination.fp.path)
+            self.rollback.destination_copy.path += ".rollback"
+            self.destination.fp.copyTo(self.rollback.destination_copy)
+            logging.debug("Rollback: backing up destination %s to %s" % (self.destination.fp.path, self.rollback.destination_copy.path), system="Store Resource")
+        else:
+            self.rollback.destination_created = True
+            logging.debug("Rollback: will create new destination %s" % (self.destination.fp.path,), system="Store Resource")
+
+        if self.deletesource:
+            self.rollback.source_copy = FilePath(self.source.fp.path)
+            self.rollback.source_copy.path += ".rollback"
+            self.source.fp.copyTo(self.rollback.source_copy)
+            logging.debug("Rollback: backing up source %s to %s" % (self.source.fp.path, self.rollback.source_copy.path), system="Store Resource")
+
+    @deferredGenerator
+    def doStore(self):
         # Do put or copy based on whether source exists
-        if source is not None:
-            response = maybeDeferred(copy, source.fp, destination.fp, destination_uri, "0")
+        if self.source is not None:
+            response = maybeDeferred(copy, self.source.fp, self.destination.fp, self.destination_uri, "0")
         else:
-            md5 = MD5StreamWrapper(MemoryStream(calendardata))
-            response = maybeDeferred(put, md5, destination.fp)
+            if self.calendardata is None:
+                self.calendardata = str(self.calendar)
+            md5 = MD5StreamWrapper(MemoryStream(self.calendardata))
+            response = maybeDeferred(put, md5, self.destination.fp)
         response = waitForDeferred(response)
         yield response
         response = response.getResult()
 
         # Update the MD5 value on the resource
-        if source is not None:
+        if self.source is not None:
             # Copy MD5 value from source to destination
-            if source.hasDeadProperty(TwistedGETContentMD5):
-                md5 = source.readDeadProperty(TwistedGETContentMD5)
-                destination.writeDeadProperty(md5)
+            if self.source.hasDeadProperty(TwistedGETContentMD5):
+                md5 = self.source.readDeadProperty(TwistedGETContentMD5)
+                self.destination.writeDeadProperty(md5)
         else:
             # Finish MD5 calc and write dead property
             md5.close()
             md5 = md5.getMD5()
-            destination.writeDeadProperty(TwistedGETContentMD5.fromString(md5))
+            self.destination.writeDeadProperty(TwistedGETContentMD5.fromString(md5))
+    
+        yield IResponse(response)
 
-        response = IResponse(response)
-        
-        def doDestinationIndex(caltoindex):
-            """
-            Do destination resource indexing, replacing any index previous stored.
-            
-            @return: None if successful, ErrorResponse on failure
-            """
-            
-            # Delete index for original item
-            if overwrite:
-                doRemoveDestinationIndex()
-            
-            # Add or update the index for this resource.
-            try:
-                destination_index.addResource(destination.fp.basename(), caltoindex)
-                logging.debug("Destination indexed %s" % (destination.fp.path,), system="Store Resource")
-            except TooManyInstancesError, ex:
-                log.err("Cannot index calendar resource as there are too many recurrence instances %s" % destination)
-                raise HTTPError(ErrorResponse(
-                    responsecode.FORBIDDEN,
-                    NumberOfRecurrencesWithinLimits(PCDATAElement(str(ex.max_allowed)))
-                ))
-            except (ValueError, TypeError), ex:
-                log.err("Cannot index calendar resource: %s" % (ex,))
-                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+    @deferredGenerator
+    def doSourceDelete(self):
+        # Delete index for original item
+        if self.sourcecal:
+            self.source_index.deleteResource(self.source.fp.basename())
+            self.rollback.source_index_deleted = True
+            logging.debug("Source index removed %s" % (self.source.fp.path,), system="Store Resource")
 
-            destination.writeDeadProperty(davxml.GETContentType.fromString("text/calendar"))
-            return None
+        # Delete the source resource
+        delete(self.source_uri, self.source.fp, "0")
+        self.rollback.source_deleted = True
+        logging.debug("Source removed %s" % (self.source.fp.path,), system="Store Resource")
 
-        def doRemoveDestinationIndex():
-            """
-            Remove any existing destination index.
-            """
-            
-            # Delete index for original item
-            if destinationcal:
-                destination_index.deleteResource(destination.fp.basename())
-                rollback.destination_index_deleted = True
-                logging.debug("Destination index removed %s" % (destination.fp.path,), system="Store Resource")
+        # Update quota
+        if self.sourcequota is not None:
+            delete_size = 0 - self.old_source_size
+            d = waitForDeferred(self.source.quotaSizeAdjust(self.request, delete_size))
+            yield d
+            d.getResult()
 
-        def doSourceDelete():
-            # Delete index for original item
-            if sourcecal:
-                source_index.deleteResource(source.fp.basename())
-                rollback.source_index_deleted = True
-                logging.debug("Source index removed %s" % (source.fp.path,), system="Store Resource")
+        # Change CTag on the parent calendar collection
+        if self.sourcecal:
+            self.sourceparent.updateCTag()
 
-            # Delete the source resource
-            delete(source_uri, source.fp, "0")
-            rollback.source_deleted = True
-            logging.debug("Source removed %s" % (source.fp.path,), system="Store Resource")
+        yield None
 
-        def doSourceIndexRecover():
-            """
-            Do source resource indexing. This only gets called when restoring
-            the source after its index has been deleted.
-            
-            @return: None if successful, ErrorResponse on failure
-            """
-            
-            # Add or update the index for this resource.
-            try:
-                source_index.addResource(source.fp.basename(), calendar)
-            except TooManyInstancesError, ex:
-                raise HTTPError(ErrorResponse(
-                    responsecode.FORBIDDEN,
-                    NumberOfRecurrencesWithinLimits(PCDATAElement(str(ex.max_allowed)))
-                ))
+    @deferredGenerator
+    def doDestinationQuotaCheck(self):
+        # Get size of new/old resources
+        new_dest_size = waitForDeferred(self.destination.quotaSize(self.request))
+        yield new_dest_size
+        new_dest_size = new_dest_size.getResult()
 
-            source.writeDeadProperty(davxml.GETContentType.fromString("text/calendar"))
-            return None
+        diff_size = new_dest_size - self.old_dest_size
 
-        if deletesource:
-            doSourceDelete()
-            # Update quota
-            if sourcequota is not None:
-                delete_size = 0 - old_source_size
-                d = waitForDeferred(source.quotaSizeAdjust(request, delete_size))
-                yield d
-                d.getResult()
+        if diff_size >= self.destquota[0]:
+            log.err("Over quota: available %d, need %d" % (self.destquota[0], diff_size))
+            raise HTTPError(ErrorResponse(responsecode.INSUFFICIENT_STORAGE_SPACE, (dav_namespace, "quota-not-exceeded")))
+        d = waitForDeferred(self.destination.quotaSizeAdjust(self.request, diff_size))
+        yield d
+        d.getResult()
 
-            if sourcecal:
-                # Change CTag on the parent calendar collection
-                sourceparent.updateCTag()
+        yield None
 
-        if destinationcal:
-            result = doDestinationIndex(calendar)
-            if result is not None:
-                rollback.Rollback()
-                yield result
-                return
+    def doSourceIndexRecover(self):
+        """
+        Do source resource indexing. This only gets called when restoring
+        the source after its index has been deleted.
+        
+        @return: None if successful, ErrorResponse on failure
+        """
+        
+        # Add or update the index for this resource.
+        try:
+            self.source_index.addResource(self.source.fp.basename(), self.calendar)
+        except TooManyInstancesError, ex:
+            raise HTTPError(ErrorResponse(
+                responsecode.FORBIDDEN,
+                    NumberOfRecurrencesWithinLimits(PCDATAElement(str(ex.max_allowed)))
+                ))
 
-        # Do quota check on destination
-        if destquota is not None:
-            # Get size of new/old resources
-            new_dest_size = waitForDeferred(destination.quotaSize(request))
-            yield new_dest_size
-            new_dest_size = new_dest_size.getResult()
-            diff_size = new_dest_size - old_dest_size
-            if diff_size >= destquota[0]:
-                log.err("Over quota: available %d, need %d" % (destquota[0], diff_size))
-                raise HTTPError(ErrorResponse(responsecode.INSUFFICIENT_STORAGE_SPACE, (dav_namespace, "quota-not-exceeded")))
-            d = waitForDeferred(destination.quotaSizeAdjust(request, diff_size))
-            yield d
-            d.getResult()
+            self.source.writeDeadProperty(davxml.GETContentType.fromString("text/calendar"))
+            return None
 
+    def doDestinationIndex(self, caltoindex):
+        """
+        Do destination resource indexing, replacing any index previous stored.
+        
+        @return: None if successful, ErrorResponse on failure
+        """
+        
+        # Delete index for original item
+        if self.overwrite:
+            self.doRemoveDestinationIndex()
+        
+        # Add or update the index for this resource.
+        try:
+            self.destination_index.addResource(self.destination.fp.basename(), caltoindex)
+            logging.debug("Destination indexed %s" % (self.destination.fp.path,), system="Store Resource")
+        except TooManyInstancesError, ex:
+            log.err("Cannot index calendar resource as there are too many recurrence instances %s" % self.destination)
+            raise HTTPError(ErrorResponse(
+                responsecode.FORBIDDEN,
+                NumberOfRecurrencesWithinLimits(PCDATAElement(str(ex.max_allowed)))
+            ))
+        except (ValueError, TypeError), ex:
+            log.err("Cannot index calendar resource: %s" % (ex,))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
 
-        if destinationcal:
-            # Change CTag on the parent calendar collection
-            destinationparent.updateCTag()
+        self.destination.writeDeadProperty(davxml.GETContentType.fromString("text/calendar"))
+        return None
 
-        # Can now commit changes and forget the rollback details
-        rollback.Commit()
+    def doRemoveDestinationIndex(self):
+        """
+        Remove any existing destination index.
+        """
+        
+        # Delete index for original item
+        if self.destinationcal:
+            self.destination_index.deleteResource(self.destination.fp.basename())
+            self.rollback.destination_index_deleted = True
+            logging.debug("Destination index removed %s" % (self.destination.fp.path,), system="Store Resource")
 
-        if reserved:
-            destination_index.unreserveUID(uid)
-            reserved = False
+    @deferredGenerator
+    def run(self):
+        """
+        Function that does common PUT/COPY/MOVE behaviour.
 
-        yield response
-        return
+        @return: a Deferred with a status response result.
+        """
 
-    except:
-        if reserved:
-            destination_index.unreserveUID(uid)
-            reserved = False
+        try:
+            reservation = None
+            
+            # Handle all validation operations here.
+            self.fullValidation()
 
-        # Roll back changes to original server state. Note this may do nothing
-        # if the rollback has already ocurred or changes already committed.
-        rollback.Rollback()
-        raise
+            # Reservation and UID conflict checking is next.
+            if self.destinationcal:    
+                # Reserve UID
+                self.destination_index = self.destinationparent.index()
+                reservation = StoreCalendarObjectResource.UIDReservation(self.destination_index, self.uid, self.destination_uri)
+                d = waitForDeferred(reservation.reserve())
+                yield d
+                d.getResult()
+            
+                # uid conflict check - note we do this after reserving the UID to avoid a race condition where two requests
+                # try to write the same calendar data to two different resource URIs.
+                if not self.isiTIP:
+                    result, message, rname = self.noUIDConflict(self.uid)
+                    if not result:
+                        log.err(message)
+                        raise HTTPError(ErrorResponse(responsecode.FORBIDDEN,
+                            NoUIDConflict(davxml.HRef.fromString(joinURL(parentForURL(self.destination_uri), rname.encode("utf-8"))))
+                        ))
+            
+            # Get current quota state.
+            d = waitForDeferred(self.checkQuota())
+            yield d
+            d.getResult()
+    
+            # Initialize the rollback system
+            self.setupRollback()
+        
+            """
+            Handle actual store operations here.
+            
+            The order in which this is done is import:
+                
+                1. Do store operation for new data
+                2. Delete source and source index if needed
+                3. Do new indexing if needed
+                
+            Note that we need to remove the source index BEFORE doing the destination index to cover the
+            case of a resource being 'renamed', i.e. moved within the same collection. Since the index UID
+            column must be unique in SQL, we cannot add the new index before remove the old one.
+            """
+    
+            # Do the actual put or copy
+            response = waitForDeferred(self.doStore())
+            yield response
+            response = response.getResult()
+            
+            # Delete the original source if needed.
+            if self.deletesource:
+                d = waitForDeferred(self.doSourceDelete())
+                yield d
+                d.getResult()
+    
+            # Index the new resource if storing to a calendar.
+            if self.destinationcal:
+                result = self.doDestinationIndex(self.calendar)
+                if result is not None:
+                    self.rollback.Rollback()
+                    yield result
+                    return
+    
+            # Do quota check on destination
+            if self.destquota is not None:
+                d = waitForDeferred(self.doDestinationQuotaCheck())
+                yield d
+                d.getResult()
+    
+            if self.destinationcal:
+                # Change CTag on the parent calendar collection
+                self.destinationparent.updateCTag()
+    
+            # Can now commit changes and forget the rollback details
+            self.rollback.Commit()
+    
+            if reservation:
+                reservation.unreserve()
+    
+            yield response
+            return
+    
+        except:
+            if reservation:
+                reservation.unreserve()
+    
+            # Roll back changes to original server state. Note this may do nothing
+            # if the rollback has already ocurred or changes already committed.
+            if self.rollback:
+                self.rollback.Rollback()
 
-storeCalendarObjectResource = deferredGenerator(storeCalendarObjectResource)
+            raise

Modified: CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/method/report_common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/method/report_common.py	2008-02-01 22:00:25 UTC (rev 2114)
+++ CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/method/report_common.py	2008-02-03 19:53:18 UTC (rev 2115)
@@ -262,7 +262,8 @@
 _namedPropertiesForResource = deferredGenerator(_namedPropertiesForResource)
     
 def generateFreeBusyInfo(request, calresource, fbinfo, timerange, matchtotal,
-                         excludeuid=None, organizer=None, same_calendar_user=False):
+                         excludeuid=None, organizer=None, same_calendar_user=False,
+                         servertoserver=False):
     """
     Run a free busy report on the specified calendar collection
     accumulating the free busy info for later processing.
@@ -277,16 +278,19 @@
         This is used in conjunction with the UID value to process exclusions.
     @param same_calendar_user:   a C{bool} indicating whether the calendar user requesting tyhe free-busy information
         is the same as the calendar user being targeted.
+    @param servertoserver:       a C{bool} indicating whether we are doing a local or remote lookup request.
     """
     
     # First check the privilege on this collection
-    try:
-        d = waitForDeferred(calresource.checkPrivileges(request, (caldavxml.ReadFreeBusy(),)))
-        yield d
-        d.getResult()
-    except AccessDeniedError:
-        yield matchtotal
-        return
+    # TODO: for server-to-server we bypass this right now as we have no way to authorize external users.
+    if not servertoserver:
+        try:
+            d = waitForDeferred(calresource.checkPrivileges(request, (caldavxml.ReadFreeBusy(),)))
+            yield d
+            d.getResult()
+        except AccessDeniedError:
+            yield matchtotal
+            return
 
     #
     # What we do is a fake calendar-query for VEVENT/VFREEBUSYs in the specified time-range.
@@ -331,12 +335,14 @@
         yield child
         child = child.getResult()
 
-        try:
-            d = waitForDeferred(child.checkPrivileges(request, (caldavxml.ReadFreeBusy(),), inherited_aces=filteredaces))
-            yield d
-            d.getResult()
-        except AccessDeniedError:
-            continue
+        # TODO: for server-to-server we bypass this right now as we have no way to authorize external users.
+        if not servertoserver:
+            try:
+                d = waitForDeferred(child.checkPrivileges(request, (caldavxml.ReadFreeBusy(),), inherited_aces=filteredaces))
+                yield d
+                d.getResult()
+            except AccessDeniedError:
+                continue
 
         calendar = calresource.iCalendar(name)
         

Modified: CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/schedule.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/schedule.py	2008-02-01 22:00:25 UTC (rev 2114)
+++ CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/schedule.py	2008-02-03 19:53:18 UTC (rev 2115)
@@ -21,34 +21,27 @@
 __all__ = [
     "ScheduleInboxResource",
     "ScheduleOutboxResource",
+    "ScheduleServerToServerResource",
 ]
 
-from twisted.internet import reactor
-from twisted.internet.defer import deferredGenerator, maybeDeferred, succeed, waitForDeferred
-from twisted.python.failure import Failure
+from twisted.internet.defer import deferredGenerator, succeed, waitForDeferred
 from twisted.web2 import responsecode
-from twisted.web2.http import HTTPError, Response
-from twisted.web2.http_headers import MimeType
 from twisted.web2.dav import davxml
-from twisted.web2.dav.http import ErrorResponse, errorForFailure, messageForFailure, statusForFailure
-from twisted.web2.dav.resource import AccessDeniedError
+from twisted.web2.dav.http import ErrorResponse
 from twisted.web2.dav.util import joinURL
+from twisted.web2.http import HTTPError
+from twisted.web2.http import Response
+from twisted.web2.http_headers import MimeType
 
 from twistedcaldav import caldavxml
-from twistedcaldav import itip
-from twistedcaldav import logging
-from twistedcaldav.resource import CalDAVResource
-from twistedcaldav.caldavxml import caldav_namespace, TimeRange
+from twistedcaldav.caldavxml import caldav_namespace
 from twistedcaldav.config import config
 from twistedcaldav.customxml import calendarserver_namespace
-from twistedcaldav.ical import Component
-from twistedcaldav.method import report_common
-from twistedcaldav.method.put_common import storeCalendarObjectResource
+from twistedcaldav.resource import CalDAVResource
 from twistedcaldav.resource import isCalendarCollectionResource
+from twistedcaldav.schedule_common import CalDAVScheduler
+from twistedcaldav.schedule_common import ServerToServerScheduler
 
-import md5
-import time
-
 class CalendarSchedulingCollectionResource (CalDAVResource):
     """
     CalDAV principal resource.
@@ -166,6 +159,8 @@
     Extends L{DAVResource} to provide CalDAV functionality.
     """
 
+    _schedulerClass = CalDAVScheduler
+
     def defaultAccessControlList(self):
         if config.EnableProxyPrincipals:
             myPrincipal = self.parent.principalForRecord()
@@ -199,399 +194,88 @@
         yield x
         x.getResult()
 
-        # Must be content-type text/calendar
-        content_type = request.headers.getHeader("content-type")
-        if content_type is not None and (content_type.mediaType, content_type.mediaSubtype) != ("text", "calendar"):
-            logging.err("MIME type %s not allowed in calendar collection" % (content_type,), system="CalDAV Outbox POST")
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "supported-calendar-data")))
-    
-        # Must have Originator header
-        originator = request.headers.getRawHeaders("originator")
-        if originator is None or (len(originator) != 1):
-            logging.err("POST request must have Originator header", system="CalDAV Outbox POST")
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-specified")))
-        else:
-            originator = originator[0]
-    
-        # Verify that Originator is a valid calendar user (has an INBOX)
-        oprincipal = self.principalForCalendarUserAddress(originator)
-        if oprincipal is None:
-            logging.err("Could not find principal for originator: %s" % (originator,), system="CalDAV Outbox POST")
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+        # This is a local CALDAV scheduling operation.
+        scheduler = ScheduleOutboxResource._schedulerClass(request, self)
 
-        inboxURL = oprincipal.scheduleInboxURL()
-        if inboxURL is None:
-            logging.err("Could not find inbox for originator: %s" % (originator,), system="CalDAV Outbox POST")
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
-    
-        # Verify that Originator matches the authenticated user
-        if davxml.Principal(davxml.HRef(oprincipal.principalURL())) != self.currentPrincipal(request):
-            logging.err("Originator: %s does not match authorized user: %s" % (originator, self.currentPrincipal(request).children[0],), system="CalDAV Outbox POST")
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+        # Do the POST processing treating
+        x = waitForDeferred(scheduler.doSchedulingViaPOST())
+        yield x
+        yield x.getResult()
 
-        # Get list of Recipient headers
-        rawrecipients = request.headers.getRawHeaders("recipient")
-        if rawrecipients is None or (len(rawrecipients) == 0):
-            logging.err("POST request must have at least one Recipient header", system="CalDAV Outbox POST")
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-specified")))
 
-        # Recipient header may be comma separated list
-        recipients = []
-        for rawrecipient in rawrecipients:
-            for r in rawrecipient.split(","):
-                r = r.strip()
-                if len(r):
-                    recipients.append(r)
+class ScheduleServerToServerResource (CalDAVResource):
+    """
+    Server-to-server schedule Inbox resource.
 
-        timerange = TimeRange(start="20000101", end="20000102")
-        recipients_state = {"OK":0, "BAD":0}
+    Extends L{DAVResource} to provide Server-to-server functionality.
+    """
 
-        # Parse the calendar object from the HTTP request stream
-        try:
-            d = waitForDeferred(Component.fromIStream(request.stream))
-            yield d
-            calendar = d.getResult()
-        except:
-            logging.err("Error while handling POST: %s" % (Failure(),), system="CalDAV Outbox POST")
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
- 
-        # Must be a valid calendar
-        try:
-            calendar.validCalendarForCalDAV()
-        except ValueError:
-            logging.err("POST request calendar component is not valid: %s" % (calendar,), system="CalDAV Outbox POST")
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+    _schedulerClass = ServerToServerScheduler
 
-        # Must have a METHOD
-        if not calendar.isValidMethod():
-            logging.err("POST request must have valid METHOD property in calendar component: %s" % (calendar,), system="CalDAV Outbox POST")
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
-        
-        # Verify iTIP behaviour
-        if not calendar.isValidITIP():
-            logging.err("POST request must have a calendar component that satisfies iTIP requirements: %s" % (calendar,), system="CalDAV Outbox POST")
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
-    
-        # Verify that the ORGANIZER's cu address maps to the request.uri
-        outboxURL = None
-        organizer = calendar.getOrganizer()
-        if organizer is not None:
-            oprincipal = self.principalForCalendarUserAddress(organizer)
-            if oprincipal is not None:
-                outboxURL = oprincipal.scheduleOutboxURL()
-        if outboxURL is None:
-            logging.err("ORGANIZER in calendar data is not valid: %s" % (calendar,), system="CalDAV Outbox POST")
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+    def __init__(self, parent):
+        """
+        @param parent: the parent resource of this one.
+        """
+        assert parent is not None
 
-        # Prevent spoofing of ORGANIZER with specific METHODs
-        if (calendar.propertyValue("METHOD") in ("PUBLISH", "REQUEST", "ADD", "CANCEL", "DECLINECOUNTER")) and (outboxURL != request.uri):
-            logging.err("ORGANIZER in calendar data does not match owner of Outbox: %s" % (calendar,), system="CalDAV Outbox POST")
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+        CalDAVResource.__init__(self, principalCollections=parent.principalCollections())
 
-        # Prevent spoofing when doing reply-like METHODs
-        if calendar.propertyValue("METHOD") in ("REPLY", "COUNTER", "REFRESH"):
-            # Verify that there is a single ATTENDEE property and that the Originator has permission
-            # to send on behalf of that ATTENDEE
-            attendees = calendar.getAttendees()
-        
-            # Must have only one
-            if len(attendees) != 1:
-                logging.err("ATTENDEE list in calendar data is wrong: %s" % (calendar,), system="CalDAV Outbox POST")
-                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
-            
-            # Attendee's Outbox MUST be the request URI
-            aoutboxURL = None
-            aprincipal = self.principalForCalendarUserAddress(attendees[0])
-            if aprincipal is not None:
-                aoutboxURL = aprincipal.scheduleOutboxURL()
-            if aoutboxURL is None or aoutboxURL != request.uri:
-                logging.err("ATTENDEE in calendar data does not match owner of Outbox: %s" % (calendar,), system="CalDAV Outbox POST")
-                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
+        self.parent = parent
 
-        # For free-busy do immediate determination of iTIP result rather than fan-out
-        logging.info("METHOD: %s, Component: %s" % (calendar.propertyValue("METHOD"), calendar.mainType(),), system="CalDAV Outbox POST")
-        if (calendar.propertyValue("METHOD") == "REQUEST") and (calendar.mainType() == "VFREEBUSY"):
-            # Extract time range from VFREEBUSY object
-            vfreebusies = [v for v in calendar.subcomponents() if v.name() == "VFREEBUSY"]
-            if len(vfreebusies) != 1:
-                logging.err("iTIP data is not valid for a VFREEBUSY request: %s" % (calendar,), system="CalDAV Outbox POST")
-                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
-            dtstart = vfreebusies[0].getStartDateUTC()
-            dtend = vfreebusies[0].getEndDateUTC()
-            if dtstart is None or dtend is None:
-                logging.err("VFREEBUSY start/end not valid: %s" % (calendar,), system="CalDAV Outbox POST")
-                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
-            timerange.start = dtstart
-            timerange.end = dtend
+    def defaultAccessControlList(self):
+        return davxml.ACL(
+            # DAV:Read, CalDAV:schedule for all principals (includes anonymous)
+            davxml.ACE(
+                davxml.Principal(davxml.All()),
+                davxml.Grant(
+                    davxml.Privilege(davxml.Read()),
+                    davxml.Privilege(caldavxml.Schedule()),
+                ),
+                davxml.Protected(),
+            ),
+        )
 
-            # Look for maksed UID
-            excludeuid = calendar.getMaskUID()
+    def resourceType(self):
+        return davxml.ResourceType.servertoserverinbox
 
-            # Do free busy operation
-            freebusy = True
-        else:
-            # Do regular invite (fan-out)
-            freebusy = False
+    def isCollection(self):
+        return False
 
-        # Prepare for multiple responses
-        responses = ScheduleResponseQueue("POST", responsecode.OK)
-    
-        # Extract the ORGANIZER property and UID value from the calendar data for use later
-        organizerProp = calendar.getOrganizerProperty()
-        uid = calendar.resourceUID()
+    def isCalendarCollection(self):
+        return False
 
-        # Loop over each recipient and do appropriate action.
-        autoresponses = []
-        for recipient in recipients:
-            # Get the principal resource for this recipient
-            principal = self.principalForCalendarUserAddress(recipient)
+    def isPseudoCalendarCollection(self):
+        return False
 
-            # Map recipient to their inbox
-            inbox = None
-            if principal is None:
-                logging.err("No principal for calendar user address: %s" % (recipient,), system="CalDAV Outbox POST")
-            else:
-                inboxURL = principal.scheduleInboxURL()
-                if inboxURL:
-                    inbox = waitForDeferred(request.locateResource(inboxURL))
-                    yield inbox
-                    inbox = inbox.getResult()
-                else:
-                    logging.err("No schedule inbox for principal: %s" % (principal,), system="CalDAV Outbox POST")
+    def render(self, request):
+        output = """<html>
+<head>
+<title>Server To Server Inbox Resource</title>
+</head>
+<body>
+<h1>Server To Server Inbox Resource.</h1>
+</body
+</html>"""
 
-            if inbox is None:
-                err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-exists")))
-                responses.add(recipient, Failure(exc_value=err), reqstatus="3.7;Invalid Calendar User")
-                recipients_state["BAD"] += 1
-            
-                # Process next recipient
-                continue
-            else:
-                #
-                # Check access controls
-                #
-                try:
-                    d = waitForDeferred(inbox.checkPrivileges(request, (caldavxml.Schedule(),), principal=davxml.Principal(davxml.HRef(oprincipal.principalURL()))))
-                    yield d
-                    d.getResult()
-                except AccessDeniedError:
-                    logging.err("Could not access Inbox for recipient: %s" % (recipient,), system="CalDAV Outbox POST")
-                    err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-permisions")))
-                    responses.add(recipient, Failure(exc_value=err), reqstatus="3.8;No authority")
-                    recipients_state["BAD"] += 1
-                
-                    # Process next recipient
-                    continue
-    
-                # Different behaviour for free-busy vs regular invite
-                if freebusy:
-                    # Extract the ATTENDEE property matching current recipient from the calendar data
-                    cuas = principal.calendarUserAddresses()
-                    attendeeProp = calendar.getAttendeeProperty(cuas)
-            
-                    # Find the current recipients calendar-free-busy-set
-                    fbset = waitForDeferred(principal.calendarFreeBusyURIs(request))
-                    yield fbset
-                    fbset = fbset.getResult()
+        response = Response(200, {}, output)
+        response.headers.setHeader("content-type", MimeType("text", "html"))
+        return response
 
-                    # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
-                    fbinfo = ([], [], [])
-                
-                    try:
-                        # Process the availability property from the Inbox.
-                        has_prop = waitForDeferred(inbox.hasProperty((calendarserver_namespace, "calendar-availability"), request))
-                        yield has_prop
-                        has_prop = has_prop.getResult()
-                        if has_prop:
-                            availability = waitForDeferred(inbox.readProperty((calendarserver_namespace, "calendar-availability"), request))
-                            yield availability
-                            availability = availability.getResult()
-                            availability = availability.calendar()
-                            report_common.processAvailabilityFreeBusy(availability, fbinfo, timerange)
-
-                        # Check to see if the recipient is the same calendar user as the organizer.
-                        # Needed for masked UID stuff.
-                        same_calendar_user = oprincipal.principalURL() == principal.principalURL()
-
-                        # Now process free-busy set calendars
-                        matchtotal = 0
-                        for calURL in fbset:
-                            cal = waitForDeferred(request.locateResource(calURL))
-                            yield cal
-                            cal = cal.getResult()
-                            if cal is None or not cal.exists() or not isCalendarCollectionResource(cal):
-                                # We will ignore missing calendars. If the recipient has failed to
-                                # properly manage the free busy set that should not prevent us from working.
-                                continue
-                         
-                            matchtotal = waitForDeferred(report_common.generateFreeBusyInfo(
-                                request,
-                                cal,
-                                fbinfo,
-                                timerange,
-                                matchtotal,
-                                excludeuid=excludeuid,
-                                organizer=organizer,
-                                same_calendar_user=same_calendar_user))
-                            yield matchtotal
-                            matchtotal = matchtotal.getResult()
-                    
-                        # Build VFREEBUSY iTIP reply for this recipient
-                        fbresult = report_common.buildFreeBusyResult(fbinfo, timerange, organizer=organizerProp, attendee=attendeeProp, uid=uid, method="REPLY")
-
-                        responses.add(recipient, responsecode.OK, reqstatus="2.0;Success", calendar=fbresult)
-                        recipients_state["OK"] += 1
-                
-                    except:
-                        logging.err("Could not determine free busy information: %s" % (recipient,), system="CalDAV Outbox POST")
-                        err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-permissions")))
-                        responses.add(recipient, Failure(exc_value=err), reqstatus="3.8;No authority")
-                        recipients_state["BAD"] += 1
-                
-                else:
-                    # Hash the iCalendar data for use as the last path element of the URI path
-                    name = md5.new(str(calendar) + str(time.time()) + inbox.fp.path).hexdigest() + ".ics"
-                
-                    # Get a resource for the new item
-                    childURL = joinURL(inboxURL, name)
-                    child = waitForDeferred(request.locateResource(childURL))
-                    yield child
-                    child = child.getResult()
-            
-                    # Copy calendar to inbox (doing fan-out)
-                    d = waitForDeferred(
-                            maybeDeferred(
-                                storeCalendarObjectResource,
-                                request=request,
-                                sourcecal = False,
-                                destination = child,
-                                destination_uri = childURL,
-                                calendardata = str(calendar),
-                                destinationparent = inbox,
-                                destinationcal = True,
-                                isiTIP = True
-                            )
-                         )
-                    yield d
-                    try:
-                        d.getResult()
-                        responses.add(recipient, responsecode.OK, reqstatus="2.0;Success")
-                        recipients_state["OK"] += 1
-        
-                        # Store CALDAV:originator property
-                        child.writeDeadProperty(caldavxml.Originator(davxml.HRef(originator)))
-                    
-                        # Store CALDAV:recipient property
-                        child.writeDeadProperty(caldavxml.Recipient(davxml.HRef(recipient)))
-                    
-                        # Look for auto-schedule option
-                        if principal.autoSchedule():
-                            autoresponses.append((principal, inbox, child))
-                    except:
-                        logging.err("Could not store data in Inbox : %s" % (inbox,), system="CalDAV Outbox POST")
-                        err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-permissions")))
-                        responses.add(recipient, Failure(exc_value=err), reqstatus="3.8;No authority")
-                        recipients_state["BAD"] += 1
-
-        # Now we have to do auto-respond
-        if len(autoresponses) != 0:
-            # First check that we have a method that we can auto-respond to
-            if not itip.canAutoRespond(calendar):
-                autoresponses = []
-            
-        # Now do the actual auto response
-        for principal, inbox, child in autoresponses:
-            # Add delayed reactor task to handle iTIP responses
-            reactor.callLater(0.0, itip.handleRequest, *(request, principal, inbox, calendar.duplicate(), child)) #@UndefinedVariable
-            #reactor.callInThread(itip.handleRequest, *(request, principal, inbox, calendar.duplicate(), child)) #@UndefinedVariable
-
-        # Return with final response if we are done
-        yield responses.response()
-
-class ScheduleResponseResponse (Response):
-    """
-    ScheduleResponse L{Response} object.
-    Renders itself as a CalDAV:schedule-response XML document.
-    """
-    def __init__(self, xml_responses, location=None):
+    @deferredGenerator
+    def http_POST(self, request):
         """
-        @param xml_responses: an interable of davxml.Response objects.
-        @param location:      the value of the location header to return in the response,
-            or None.
+        The server-to-server POST method.
         """
 
-        Response.__init__(self, code=responsecode.OK,
-                          stream=caldavxml.ScheduleResponse(*xml_responses).toxml())
+        # Check authentication and access controls
+        x = waitForDeferred(self.authorize(request, (caldavxml.Schedule(),)))
+        yield x
+        x.getResult()
 
-        self.headers.setHeader("content-type", MimeType("text", "xml"))
-    
-        if location is not None:
-            self.headers.setHeader("location", location)
+        # This is a server-to-server scheduling operation.
+        scheduler = ScheduleServerToServerResource._schedulerClass(request, self)
 
-class ScheduleResponseQueue (object):
-    """
-    Stores a list of (typically error) responses for use in a
-    L{ScheduleResponse}.
-    """
-    def __init__(self, method, success_response):
-        """
-        @param method: the name of the method generating the queue.
-        @param success_response: the response to return in lieu of a
-            L{ScheduleResponse} if no responses are added to this queue.
-        """
-        self.responses         = []
-        self.method            = method
-        self.success_response  = success_response
-        self.location          = None
-
-    def setLocation(self, location):
-        """
-        @param location:      the value of the location header to return in the response,
-            or None.
-        """
-        self.location = location
-
-    def add(self, recipient, what, reqstatus=None, calendar=None):
-        """
-        Add a response.
-        @param recipient: the recipient for this response.
-        @param what: a status code or a L{Failure} for the given recipient.
-        @param status: the iTIP request-status for the given recipient.
-        @param calendar: the calendar data for the given recipient response.
-        """
-        if type(what) is int:
-            code    = what
-            error   = None
-            message = responsecode.RESPONSES[code]
-        elif isinstance(what, Failure):
-            code    = statusForFailure(what)
-            error   = errorForFailure(what)
-            message = messageForFailure(what)
-        else:
-            raise AssertionError("Unknown data type: %r" % (what,))
-
-        if code > 400: # Error codes only
-            logging.err("Error during %s for %s: %s" % (self.method, recipient, message), system="CalDAV Outbox POST")
-
-        children = []
-        children.append(caldavxml.Recipient(davxml.HRef.fromString(recipient)))
-        children.append(caldavxml.RequestStatus(reqstatus))
-        if calendar is not None:
-            children.append(caldavxml.CalendarData.fromCalendar(calendar))
-        if error is not None:
-            children.append(error)
-        if message is not None:
-            children.append(davxml.ResponseDescription(message))
-        self.responses.append(caldavxml.Response(*children))
-
-    def response(self):
-        """
-        Generate a L{ScheduleResponseResponse} with the responses contained in the
-        queue or, if no such responses, return the C{success_response} provided
-        to L{__init__}.
-        @return: the response.
-        """
-        if self.responses:
-            return ScheduleResponseResponse(self.responses, self.location)
-        else:
-            return self.success_response
+        # Do the POST processing treating this as a non-local schedule
+        x = waitForDeferred(scheduler.doSchedulingViaPOST())
+        yield x
+        yield x.getResult()

Copied: CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/schedule_common.py (from rev 2114, CalendarServer/branches/users/cdaboo/server2server-1965/twistedcaldav/schedule_common.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/schedule_common.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/schedule_common.py	2008-02-03 19:53:18 UTC (rev 2115)
@@ -0,0 +1,989 @@
+##
+# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+CalDAV/Server-to-Server scheduling behavior.
+"""
+
+__all__ = [
+    "Scheduler",
+    "CalDAVScheduler",
+    "ServerToServerScheduler",
+]
+
+from twisted.internet import reactor
+from twisted.internet.defer import DeferredList
+from twisted.internet.defer import deferredGenerator, maybeDeferred, waitForDeferred
+from twisted.python.failure import Failure
+from twisted.web2 import responsecode
+from twisted.web2.http import HTTPError, Response
+from twisted.web2.http_headers import MimeType
+from twisted.web2.dav import davxml
+from twisted.web2.dav.http import ErrorResponse, errorForFailure, messageForFailure, statusForFailure
+from twisted.web2.dav.resource import AccessDeniedError
+from twisted.web2.dav.util import joinURL
+
+from twistedcaldav import caldavxml
+from twistedcaldav import logging
+from twistedcaldav.caldavxml import caldav_namespace, TimeRange
+from twistedcaldav.config import config
+from twistedcaldav.customxml import calendarserver_namespace
+from twistedcaldav.ical import Component
+from twistedcaldav.itip import iTipProcessor
+from twistedcaldav.method import report_common
+from twistedcaldav.method.put_common import StoreCalendarObjectResource
+from twistedcaldav.resource import isCalendarCollectionResource
+from twistedcaldav.servertoserver import ServerToServer
+from twistedcaldav.servertoserver import ServerToServerRequest
+
+import itertools
+import md5
+import re
+import socket
+import time
+
+class Scheduler(object):
+    
+    class CalendarUser(object):
+        def __init__(self, cuaddr):
+            self.cuaddr = cuaddr
+
+    class LocalCalendarUser(CalendarUser):
+        def __init__(self, cuaddr, principal, inbox=None, inboxURL=None):
+            self.cuaddr = cuaddr
+            self.principal = principal
+            self.inbox = inbox
+            self.inboxURL = inboxURL
+        
+        def __str__(self):
+            return "Local calendar user: %s" % (self.cuaddr,)
+
+    class RemoteCalendarUser(CalendarUser):
+        def __init__(self, cuaddr):
+            self.cuaddr = cuaddr
+            self.extractDomain()
+
+        def __str__(self):
+            return "Remote calendar user: %s" % (self.cuaddr,)
+        
+        def extractDomain(self):
+            if self.cuaddr.startswith("mailto:"):
+                splits = self.cuaddr[7:].split("?")
+                self.domain = splits[0].split("@")[1]
+            elif self.cuaddr.startswith("http://") or self.cuaddr.startswith("https://"):
+                splits = self.cuaddr.split(":")[1][2:].split("/")
+                self.domain = splits[0]
+            else:
+                self.domain = ""
+
+    class InvalidCalendarUser(CalendarUser):
+        
+        def __str__(self):
+            return "Invalid calendar user: %s" % (self.cuaddr,)
+
+            
+    def __init__(self, request, resource):
+        self.request = request
+        self.resource = resource
+        self.originator = None
+        self.recipients = None
+        self.calendar = None
+        self.organizer = None
+        self.timerange = None
+        self.excludeuid = None
+        self.logsystem = "Scheduling"
+    
+    @deferredGenerator
+    def doSchedulingViaPOST(self):
+        """
+        The Scheduling POST operation.
+        """
+    
+        # Do some extra authorization checks
+        self.checkAuthorization()
+
+        if logging.canLog("debug"):
+            d = waitForDeferred(logging.logRequest("Received POST request:", self.request, system=self.logsystem))
+            yield d
+            d.getResult()
+
+        # Load various useful bits doing some basic checks on those
+        self.loadOriginator()
+        self.loadRecipients()
+        d = waitForDeferred(self.loadCalendar())
+        yield d
+        d.getResult()
+
+        # Check validity of Originator header.
+        self.checkOriginator()
+    
+        # Get recipient details.
+        d = waitForDeferred(self.checkRecipients())
+        yield d
+        d.getResult()
+    
+        # Check calendar data.
+        self.checkCalendarData()
+    
+        # Check validity of ORGANIZER
+        self.checkOrganizer()
+    
+        # Do security checks (e.g. spoofing)
+        self.securityChecks()
+    
+        # Do scheduling tasks
+        response = waitForDeferred(self.generateSchedulingResponse())
+        yield response
+        response = response.getResult()
+
+        if logging.canLog("debug"):
+            d = waitForDeferred(logging.logResponse("Sending POST response:", response, system=self.logsystem))
+            yield d
+            d.getResult()
+
+        yield response
+
+    def loadOriginator(self):
+        # Must have Originator header
+        originator = self.request.headers.getRawHeaders("originator")
+        if originator is None or (len(originator) != 1):
+            logging.err("POST request must have Originator header", system=self.logsystem)
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-specified")))
+        else:
+            self.originator = originator[0]
+    
+    def loadRecipients(self):
+        # Get list of Recipient headers
+        rawrecipients = self.request.headers.getRawHeaders("recipient")
+        if rawrecipients is None or (len(rawrecipients) == 0):
+            logging.err("POST request must have at least one Recipient header", system=self.logsystem)
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-specified")))
+    
+        # Recipient header may be comma separated list
+        self.recipients = []
+        for rawrecipient in rawrecipients:
+            for r in rawrecipient.split(","):
+                r = r.strip()
+                if len(r):
+                    self.recipients.append(r)
+        
+    @deferredGenerator
+    def loadCalendar(self):
+        # Must be content-type text/calendar
+        content_type = self.request.headers.getHeader("content-type")
+        if content_type is not None and (content_type.mediaType, content_type.mediaSubtype) != ("text", "calendar"):
+            logging.err("MIME type %s not allowed in calendar collection" % (content_type,), system=self.logsystem)
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "supported-calendar-data")))
+    
+        # Parse the calendar object from the HTTP request stream
+        try:
+            d = waitForDeferred(Component.fromIStream(self.request.stream))
+            yield d
+            self.calendar = d.getResult()
+        except:
+            logging.err("Error while handling POST: %s" % (Failure(),), system=self.logsystem)
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+
+    def checkAuthorization(self):
+        raise NotImplementedError
+
+    def checkOriginator(self):
+        raise NotImplementedError
+
+    def checkRecipient(self):
+        raise NotImplementedError
+
+    def checkOrganizer(self):
+        raise NotImplementedError
+
+    def checkOrganizerAsOriginator(self):
+        raise NotImplementedError
+
+    def checkAttendeeAsOriginator(self):
+        raise NotImplementedError
+
+    def checkCalendarData(self):
+        # Must be a valid calendar
+        try:
+            self.calendar.validCalendarForCalDAV()
+        except ValueError:
+            logging.err("POST request calendar component is not valid: %s" % (self.calendar,), system=self.logsystem)
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+    
+        # Must have a METHOD
+        if not self.calendar.isValidMethod():
+            logging.err("POST request must have valid METHOD property in calendar component: %s" % (self.calendar,), system=self.logsystem)
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+        
+        # Verify iTIP behaviour
+        if not self.calendar.isValidITIP():
+            logging.err("POST request must have a calendar component that satisfies iTIP requirements: %s" % (self.calendar,), system=self.logsystem)
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+
+    def checkForFreeBusy(self):
+        if (self.calendar.propertyValue("METHOD") == "REQUEST") and (self.calendar.mainType() == "VFREEBUSY"):
+            # Extract time range from VFREEBUSY object
+            vfreebusies = [v for v in self.calendar.subcomponents() if v.name() == "VFREEBUSY"]
+            if len(vfreebusies) != 1:
+                logging.err("iTIP data is not valid for a VFREEBUSY request: %s" % (self.calendar,), system=self.logsystem)
+                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+            dtstart = vfreebusies[0].getStartDateUTC()
+            dtend = vfreebusies[0].getEndDateUTC()
+            if dtstart is None or dtend is None:
+                logging.err("VFREEBUSY start/end not valid: %s" % (self.calendar,), system=self.logsystem)
+                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+            self.timerange = TimeRange(start="20000101T000000Z", end="20070102T000000Z")
+            self.timerange.start = dtstart
+            self.timerange.end = dtend
+    
+            # Look for maksed UID
+            self.excludeuid = self.calendar.getMaskUID()
+    
+            # Do free busy operation
+            return True
+        else:
+            # Do regular invite (fan-out)
+            return False
+    
+    def securityChecks(self):
+        raise NotImplementedError
+
+    @staticmethod
+    def isCalendarUserAddressInMyDomain(cuaddr):
+        """
+        Check whether the supplied calendar user address corresponds to one that ought to be within
+        this server's domain.
+        
+        For now we will try to match email and http domains against ones in our config.
+         
+        @param cuaddr: the calendar user address to check.
+        @type cuaddr: C{str}
+        
+        @return: C{True} if the address is within the server's domain,
+            C{False} otherwise.
+        """
+        
+        if config.ServerToServer["Email Domain"] and cuaddr.startswith("mailto:"):
+            splits = cuaddr[7:].split("?")
+            domain = config.ServerToServer["Email Domain"]
+            return splits[0].endswith(domain)
+        elif config.ServerToServer["HTTP Domain"] and (cuaddr.startswith("http://") or cuaddr.startswith("https://")):
+            splits = cuaddr.split(":")[0][2:].split("?")
+            domain = config.ServerToServer["HTTP Domain"]
+            return splits[0].endswith(domain)
+        
+        result = False
+        
+        for pattern in config.ServerToServer["Local Addresses"]:
+            if re.match(pattern, cuaddr) is not None:
+                result = True
+        
+        for pattern in config.ServerToServer["Remote Addresses"]:
+            if re.match(pattern, cuaddr) is not None:
+                result = False
+        
+        return result
+    
+    @deferredGenerator
+    def generateSchedulingResponse(self):
+
+        logging.info("METHOD: %s, Component: %s" % (self.calendar.propertyValue("METHOD"), self.calendar.mainType(),), system=self.logsystem)
+
+        # For free-busy do immediate determination of iTIP result rather than fan-out
+        freebusy = self.checkForFreeBusy()
+    
+        # Prepare for multiple responses
+        responses = ScheduleResponseQueue("POST", responsecode.OK)
+    
+        # Extract the ORGANIZER property and UID value from the calendar data for use later
+        organizerProp = self.calendar.getOrganizerProperty()
+        uid = self.calendar.resourceUID()
+    
+        # Loop over each recipient and do appropriate action.
+        remote_recipients = []
+        autoresponses = []
+        for recipient in self.recipients:
+    
+            if isinstance(recipient, Scheduler.InvalidCalendarUser):
+                err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-exists")))
+                responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus="3.7;Invalid Calendar User")
+            
+                # Process next recipient
+                continue
+            elif isinstance(recipient, Scheduler.RemoteCalendarUser):
+                # Pool remote recipients into a seperate list for processing after the local ones.
+                remote_recipients.append(recipient)
+            
+                # Process next recipient
+                continue
+            elif isinstance(recipient, Scheduler.LocalCalendarUser):
+                #
+                # Check access controls
+                #
+                if isinstance(self.organizer, Scheduler.LocalCalendarUser):
+                    try:
+                        d = waitForDeferred(recipient.inbox.checkPrivileges(self.request, (caldavxml.Schedule(),), principal=davxml.Principal(davxml.HRef(self.organizer.principal.principalURL()))))
+                        yield d
+                        d.getResult()
+                    except AccessDeniedError:
+                        logging.err("Could not access Inbox for recipient: %s" % (recipient.cuaddr,), system=self.logsystem)
+                        err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-permisions")))
+                        responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus="3.8;No authority")
+                    
+                        # Process next recipient
+                        continue
+                else:
+                    # TODO: need to figure out how best to do server-to-server authorization.
+                    # First thing would be to checkk for DAV:unauthenticated privilege.
+                    # Next would be to allow the calendar user address of the organizer/originator to be used
+                    # as a principal. 
+                    pass
+    
+                # Different behaviour for free-busy vs regular invite
+                if freebusy:
+                    d = waitForDeferred(self.generateLocalFreeBusyResponse(recipient, responses, organizerProp, uid))
+                else:
+                    d = waitForDeferred(self.generateLocalResponse(recipient, responses, autoresponses))
+                yield d
+                d.getResult()
+    
+        # Now process remote recipients
+        if remote_recipients:
+            d = waitForDeferred(self.generateRemoteSchedulingResponses(remote_recipients, responses))
+            yield d
+            d.getResult()
+
+        # Now we have to do auto-respond
+        if len(autoresponses) != 0:
+            # First check that we have a method that we can auto-respond to
+            if not iTipProcessor.canAutoRespond(self.calendar):
+                autoresponses = []
+            
+        # Now do the actual auto response
+        for principal, inbox, child in autoresponses:
+            # Add delayed reactor task to handle iTIP responses
+            itip = iTipProcessor()
+            reactor.callLater(0.0, itip.handleRequest, *(self.request, principal, inbox, self.calendar.duplicate(), child)) #@UndefinedVariable
+    
+        # Return with final response if we are done
+        yield responses.response()
+    
+    @deferredGenerator
+    def generateRemoteSchedulingResponses(self, recipients, responses):
+        """
+        Generate scheduling responses for remote recipients.
+        """
+        
+        # Group recipients by server so that we can do a single request with multiple recipients
+        # to each different server.
+        groups = {}
+        servermgr = ServerToServer()
+        for recipient in recipients:
+            # Map the recipient's domain to a server
+            server = servermgr.mapDomain(recipient.domain)
+            if not server:
+                # Cannot do server-to-server for this recipient.
+                err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-allowed")))
+                responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus="5.3;No scheduling support for user")
+            
+                # Process next recipient
+                continue
+            
+            if not server.allow_to:
+                # Cannot do server-to-server outgoing requests for this server.
+                err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-allowed")))
+                responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus="5.1;Service unavailable")
+            
+                # Process next recipient
+                continue
+            
+            groups.setdefault(server, []).append(recipient)
+        
+        if len(groups) == 0:
+            yield None
+            return
+
+        # Now we process each server: let's use a DeferredList to aggregate all the Deferred's
+        # we will generate for each request. That way we can have parallel requests in progress
+        # rather than serialize them.
+        deferreds = []
+        for server, recipients in groups.iteritems():
+            requestor = ServerToServerRequest(self, server, recipients, responses)
+            deferreds.append(requestor.doRequest())
+
+        d = waitForDeferred(DeferredList(deferreds))
+        yield d
+        d.getResult()
+
+    @deferredGenerator
+    def generateLocalResponse(self, recipient, responses, autoresponses):
+        # Hash the iCalendar data for use as the last path element of the URI path
+        calendar_str = str(self.calendar)
+        name = md5.new(calendar_str + str(time.time()) + recipient.inbox.fp.path).hexdigest() + ".ics"
+    
+        # Get a resource for the new item
+        childURL = joinURL(recipient.inboxURL, name)
+        child = waitForDeferred(self.request.locateResource(childURL))
+        yield child
+        child = child.getResult()
+
+        # Copy calendar to inbox (doing fan-out)
+        try:
+            storer = StoreCalendarObjectResource(
+                         request=self.request,
+                         destination = child,
+                         destination_uri = childURL,
+                         destinationparent = recipient.inbox,
+                         destinationcal = True,
+                         calendar = self.calendar,
+                         isiTIP = True
+                     )
+            d = waitForDeferred(storer.run())
+            yield d
+            d.getResult()
+            responses.add(recipient.cuaddr, responsecode.OK, reqstatus="2.0;Success")
+
+            # Store CALDAV:originator property
+            child.writeDeadProperty(caldavxml.Originator(davxml.HRef(self.originator.cuaddr)))
+        
+            # Store CALDAV:recipient property
+            child.writeDeadProperty(caldavxml.Recipient(davxml.HRef(recipient.cuaddr)))
+        
+            # Look for auto-schedule option
+            if recipient.principal.autoSchedule():
+                autoresponses.append((recipient.principal, recipient.inbox, child))
+                
+            yield True
+        except:
+            logging.err("Could not store data in Inbox : %s" % (recipient.inbox,), system=self.logsystem)
+            err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-permissions")))
+            responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus="3.8;No authority")
+            yield False
+    
+    @deferredGenerator
+    def generateLocalFreeBusyResponse(self, recipient, responses, organizerProp, uid):
+
+        # Extract the ATTENDEE property matching current recipient from the calendar data
+        cuas = recipient.principal.calendarUserAddresses()
+        attendeeProp = self.calendar.getAttendeeProperty(cuas)
+
+        remote = isinstance(self.organizer, Scheduler.RemoteCalendarUser)
+
+        try:
+            d = waitForDeferred(self.generateAttendeeFreeBusyResponse(
+                recipient,
+                organizerProp,
+                uid,
+                attendeeProp,
+                remote,
+            ))
+            yield d
+            fbresult = d.getResult()
+
+            responses.add(recipient.cuaddr, responsecode.OK, reqstatus="2.0;Success", calendar=fbresult)
+            
+            yield True
+        except:
+            logging.err("Could not determine free busy information: %s" % (recipient.cuaddr,), system=self.logsystem)
+            err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-permissions")))
+            responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus="3.8;No authority")
+            
+            yield False
+    
+    @deferredGenerator
+    def generateAttendeeFreeBusyResponse(self, recipient, organizerProp, uid, attendeeProp, remote):
+
+        # Find the current recipients calendar-free-busy-set
+        fbset = waitForDeferred(recipient.principal.calendarFreeBusyURIs(self.request))
+        yield fbset
+        fbset = fbset.getResult()
+
+        # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
+        fbinfo = ([], [], [])
+    
+        # Process the availability property from the Inbox.
+        has_prop = waitForDeferred(recipient.inbox.hasProperty((calendarserver_namespace, "calendar-availability"), self.request))
+        yield has_prop
+        has_prop = has_prop.getResult()
+        if has_prop:
+            availability = waitForDeferred(recipient.inbox.readProperty((calendarserver_namespace, "calendar-availability"), self.request))
+            yield availability
+            availability = availability.getResult()
+            availability = availability.calendar()
+            report_common.processAvailabilityFreeBusy(availability, fbinfo, self.timerange)
+
+        # Check to see if the recipient is the same calendar user as the organizer.
+        # Needed for masked UID stuff.
+        if isinstance(self.organizer, Scheduler.LocalCalendarUser):
+            same_calendar_user = self.organizer.principal.principalURL() == recipient.principal.principalURL()
+        else:
+            same_calendar_user = False
+
+        # Now process free-busy set calendars
+        matchtotal = 0
+        for calURL in fbset:
+            cal = waitForDeferred(self.request.locateResource(calURL))
+            yield cal
+            cal = cal.getResult()
+            if cal is None or not cal.exists() or not isCalendarCollectionResource(cal):
+                # We will ignore missing calendars. If the recipient has failed to
+                # properly manage the free busy set that should not prevent us from working.
+                continue
+         
+            matchtotal = waitForDeferred(report_common.generateFreeBusyInfo(
+                self.request,
+                cal,
+                fbinfo,
+                self.timerange,
+                matchtotal,
+                excludeuid=self.excludeuid,
+                organizer=self.organizer.cuaddr,
+                same_calendar_user=same_calendar_user,
+                servertoserver=remote))
+            yield matchtotal
+            matchtotal = matchtotal.getResult()
+    
+        # Build VFREEBUSY iTIP reply for this recipient
+        fbresult = report_common.buildFreeBusyResult(fbinfo, self.timerange, organizer=organizerProp, attendee=attendeeProp, uid=uid, method="REPLY")
+
+        yield fbresult
+    
+    def generateRemoteResponse(self):
+        raise NotImplementedError
+    
+    def generateRemoteFreeBusyResponse(self):
+        raise NotImplementedError
+        
+class CalDAVScheduler(Scheduler):
+
+    def __init__(self, request, resource):
+        super(CalDAVScheduler, self).__init__(request, resource)
+        self.logsystem = ("caldav", "Scheduling",)
+
+    def checkAuthorization(self):
+        # Must have an authenticated user
+        if self.resource.currentPrincipal(self.request) == davxml.Principal(davxml.Unauthenticated()):
+            logging.err("Unauthenticated originators not allowed: %s" % (self.originator,), system=self.logsystem)
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+
+    def checkOriginator(self):
+        """
+        Check the validity of the Originator header. Extract the corresponding principal.
+        """
+    
+        # Verify that Originator is a valid calendar user
+        originator_principal = self.resource.principalForCalendarUserAddress(self.originator)
+        if originator_principal is None:
+            # Local requests MUST have a principal.
+            logging.err("Could not find principal for originator: %s" % (self.originator,), system=self.logsystem)
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+        else:
+            # Must have a valid Inbox.
+            inboxURL = originator_principal.scheduleInboxURL()
+            if inboxURL is None:
+                logging.err("Could not find inbox for originator: %s" % (self.originator,), system=self.logsystem)
+                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+        
+            # Verify that Originator matches the authenticated user.
+            authn_principal = self.resource.currentPrincipal(self.request)
+            if davxml.Principal(davxml.HRef(originator_principal.principalURL())) != authn_principal:
+                logging.err("Originator: %s does not match authorized user: %s" % (self.originator, authn_principal.children[0],), system=self.logsystem)
+                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+
+            self.originator = Scheduler.LocalCalendarUser(self.originator, originator_principal)
+
+    @deferredGenerator
+    def checkRecipients(self):
+        """
+        Check the validity of the Recipient header values. Map these into local or
+        remote CalendarUsers.
+        """
+        
+        results = []
+        for recipient in self.recipients:
+            # Get the principal resource for this recipient
+            principal = self.resource.principalForCalendarUserAddress(recipient)
+            
+            # If no principal we may have a remote recipient but we should check whether
+            # the address is one that ought to be on our server and treat that as a missing
+            # user. Also if server-to-server is not enabled then remote addresses are not allowed.
+            if principal is None:
+                if self.isCalendarUserAddressInMyDomain(recipient):
+                    logging.err("No principal for calendar user address: %s" % (recipient,), system=self.logsystem)
+                    results.append(Scheduler.InvalidCalendarUser(recipient))
+                elif not config.ServerToServer["Enabled"]:
+                    logging.err("Unknown calendar user address: %s" % (recipient,), system=self.logsystem)
+                    results.append(Scheduler.InvalidCalendarUser(recipient))
+                else:
+                    results.append(Scheduler.RemoteCalendarUser(recipient))
+            else:
+                # Map recipient to their inbox
+                inbox = None
+                inboxURL = principal.scheduleInboxURL()
+                if inboxURL:
+                    inbox = waitForDeferred(self.request.locateResource(inboxURL))
+                    yield inbox
+                    inbox = inbox.getResult()
+
+                if inbox:
+                    results.append(Scheduler.LocalCalendarUser(recipient, principal, inbox, inboxURL))
+                else:
+                    logging.err("No schedule inbox for principal: %s" % (principal,), system=self.logsystem)
+                    results.append(Scheduler.InvalidCalendarUser(recipient))
+        
+        self.recipients = results
+
+    def checkOrganizer(self):
+        """
+        Check the validity of the ORGANIZER value. ORGANIZER must be local.
+        """
+        
+        # Verify that the ORGANIZER's cu address maps to a valid user
+        organizer = self.calendar.getOrganizer()
+        if organizer:
+            orgprincipal = self.resource.principalForCalendarUserAddress(organizer)
+            if orgprincipal:
+                outboxURL = orgprincipal.scheduleOutboxURL()
+                if outboxURL:
+                    self.organizer = Scheduler.LocalCalendarUser(organizer, orgprincipal)
+                else:
+                    logging.err("No outbox for ORGANIZER in calendar data: %s" % (self.calendar,), system=self.logsystem)
+                    raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+            elif self.isCalendarUserAddressInMyDomain(organizer):
+                logging.err("No principal for ORGANIZER in calendar data: %s" % (self.calendar,), system=self.logsystem)
+                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+            else:
+                self.organizer = Scheduler.RemoteCalendarUser(organizer) 
+        else:
+            logging.err("ORGANIZER missing in calendar data: %s" % (self.calendar,), system=self.logsystem)
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+
+    def checkOrganizerAsOriginator(self):
+        # Make sure that the ORGANIZER's Outbox is the request URI
+        if self.organizer.principal.scheduleOutboxURL() != self.request.uri:
+            logging.err("Wrong outbox for ORGANIZER in calendar data: %s" % (self.calendar,), system=self.logsystem)
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+
+    def checkAttendeeAsOriginator(self):
+        """
+        Check the validity of the ATTENDEE value as this is the originator of the iTIP message.
+        Only local attendees are allowed for message originating from this server.
+        """
+        
+        # Verify that there is a single ATTENDEE property
+        attendees = self.calendar.getAttendees()
+    
+        # Must have only one
+        if len(attendees) != 1:
+            logging.err("Wrong number of ATTENDEEs in calendar data: %s" % (self.calendar,), system=self.logsystem)
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
+        attendee = attendees[0]
+    
+        # Attendee's Outbox MUST be the request URI
+        aprincipal = self.resource.principalForCalendarUserAddress(attendee)
+        if aprincipal:
+            aoutboxURL = aprincipal.scheduleOutboxURL()
+            if aoutboxURL is None or aoutboxURL != self.request.uri:
+                logging.err("ATTENDEE in calendar data does not match owner of Outbox: %s" % (self.calendar,), system=self.logsystem)
+                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
+        else:
+            logging.err("Unkown ATTENDEE in calendar data: %s" % (self.calendar,), system=self.logsystem)
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
+    
+    def securityChecks(self):
+        """
+        Check that the orginator has the appropriate rights to send this type of iTIP message.
+        """
+    
+        # Prevent spoofing of ORGANIZER with specific METHODs when local
+        if self.calendar.propertyValue("METHOD") in ("PUBLISH", "REQUEST", "ADD", "CANCEL", "DECLINECOUNTER"):
+            self.checkOrganizerAsOriginator()
+    
+        # Prevent spoofing when doing reply-like METHODs
+        elif self.calendar.propertyValue("METHOD") in ("REPLY", "COUNTER", "REFRESH"):
+            self.checkAttendeeAsOriginator()
+            
+        else:
+            logging.err("Unknown iTIP METHOD for security checks: %s" % (self.calendar.propertyValue("METHOD"),), system=self.logsystem)
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+
+class ServerToServerScheduler(Scheduler):
+
+    def __init__(self, request, resource):
+        super(ServerToServerScheduler, self).__init__(request, resource)
+        self.logsystem = ("Server-to-server Recieve", "Scheduling",)
+
+    def checkAuthorization(self):
+        # Must have an unauthenticated user
+        if self.resource.currentPrincipal(self.request) != davxml.Principal(davxml.Unauthenticated()):
+            logging.err("Authenticated originators not allowed: %s" % (self.originator,), system=self.logsystem)
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+
+    def checkOriginator(self):
+        """
+        Check the validity of the Originator header.
+        """
+    
+        # For remote requests we do not allow the originator to be a local user or one within our domain.
+        originator_principal = self.resource.principalForCalendarUserAddress(self.originator)
+        if originator_principal or self.isCalendarUserAddressInMyDomain(self.originator):
+            logging.err("Cannot use originator that is on this server: %s" % (self.originator,), system=self.logsystem)
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+        else:
+            self.originator = Scheduler.RemoteCalendarUser(self.originator)
+            
+        # We will only accept originator in known domains.
+        servermgr = ServerToServer()
+        server = servermgr.mapDomain(self.originator.domain)
+        if not server or not server.allow_from:
+            logging.err("Originator not on recognized server: %s" % (self.originator,), system=self.logsystem)
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+        else:
+            # Get the request IP and map to hostname.
+            clientip = self.request.remoteAddr.host
+            
+            # First compare as dotted IP
+            if clientip != server.host:
+                # Now do hostname lookup
+                host, aliases, _ignore_ips = socket.gethostbyaddr(clientip)
+                for host in itertools.chain((host,), aliases):
+                    if host == server.host:
+                        break
+                else:
+                    logging.err("Originator not on allowed server: %s" % (self.originator,), system=self.logsystem)
+                    raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+
+    @deferredGenerator
+    def checkRecipients(self):
+        """
+        Check the validity of the Recipient header values. These must all be local as there
+        is no concept of server-to-server relaying.
+        """
+        
+        results = []
+        for recipient in self.recipients:
+            # Get the principal resource for this recipient
+            principal = self.resource.principalForCalendarUserAddress(recipient)
+            
+            # If no principal we may have a remote recipient but we should check whether
+            # the address is one that ought to be on our server and treat that as a missing
+            # user. Also if server-to-server is not enabled then remote addresses are not allowed.
+            if principal is None:
+                if self.isCalendarUserAddressInMyDomain(recipient):
+                    logging.err("No principal for calendar user address: %s" % (recipient,), system=self.logsystem)
+                    results.append(Scheduler.InvalidCalendarUser(recipient))
+                else:
+                    logging.err("Unknown calendar user address: %s" % (recipient,), system=self.logsystem)
+                    results.append(Scheduler.InvalidCalendarUser(recipient))
+            else:
+                # Map recipient to their inbox
+                inbox = None
+                inboxURL = principal.scheduleInboxURL()
+                if inboxURL:
+                    inbox = waitForDeferred(self.request.locateResource(inboxURL))
+                    yield inbox
+                    inbox = inbox.getResult()
+
+                if inbox:
+                    results.append(Scheduler.LocalCalendarUser(recipient, principal, inbox, inboxURL))
+                else:
+                    logging.err("No schedule inbox for principal: %s" % (principal,), system=self.logsystem)
+                    results.append(Scheduler.InvalidCalendarUser(recipient))
+        
+        self.recipients = results
+
+    def checkOrganizer(self):
+        """
+        Delay ORGANIZER check until we know what their role is.
+        """
+        pass
+
+    def checkOrganizerAsOriginator(self):
+        """
+        Check the validity of the ORGANIZER value. ORGANIZER must not be local.
+        """
+        
+        # Verify that the ORGANIZER's cu address does not map to a valid user
+        organizer = self.calendar.getOrganizer()
+        if organizer:
+            orgprincipal = self.resource.principalForCalendarUserAddress(organizer)
+            if orgprincipal:
+                logging.err("Invalid ORGANIZER in calendar data: %s" % (self.calendar,), system=self.logsystem)
+                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+            elif self.isCalendarUserAddressInMyDomain(organizer):
+                logging.err("Unsupported ORGANIZER in calendar data: %s" % (self.calendar,), system=self.logsystem)
+                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+            else:
+                self.organizer = Scheduler.RemoteCalendarUser(organizer)
+        else:
+            logging.err("ORGANIZER missing in calendar data: %s" % (self.calendar,), system=self.logsystem)
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+
+    def checkAttendeeAsOriginator(self):
+        """
+        Check the validity of the ATTENDEE value as this is the originator of the iTIP message.
+        Only local attendees are allowed for message originating from this server.
+        """
+        
+        # Verify that there is a single ATTENDEE property
+        attendees = self.calendar.getAttendees()
+    
+        # Must have only one
+        if len(attendees) != 1:
+            logging.err("Wrong number of ATTENDEEs in calendar data: %s" % (self.calendar,), system=self.logsystem)
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
+        attendee = attendees[0]
+    
+        # Attendee cannot be local.
+        aprincipal = self.resource.principalForCalendarUserAddress(attendee)
+        if aprincipal:
+            logging.err("Invalid ATTENDEE in calendar data: %s" % (self.calendar,), system=self.logsystem)
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
+        elif self.isCalendarUserAddressInMyDomain(attendee):
+            logging.err("Unkown ATTENDEE in calendar data: %s" % (self.calendar,), system=self.logsystem)
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
+    
+        # TODO: in this case we should check that the ORGANIZER is the sole recipient.
+
+    def securityChecks(self):
+        """
+        Check that the orginator has the appropriate rights to send this type of iTIP message.
+        """
+
+        # Prevent spoofing of ORGANIZER with specific METHODs when local
+        if self.calendar.propertyValue("METHOD") in ("PUBLISH", "REQUEST", "ADD", "CANCEL", "DECLINECOUNTER"):
+            self.checkOrganizerAsOriginator()
+    
+        # Prevent spoofing when doing reply-like METHODs
+        elif self.calendar.propertyValue("METHOD") in ("REPLY", "COUNTER", "REFRESH"):
+            self.checkAttendeeAsOriginator()
+            
+        else:
+            logging.err("Unknown iTIP METHOD for security checks: %s" % (self.calendar.propertyValue("METHOD"),), system=self.logsystem)
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+
+class ScheduleResponseResponse (Response):
+    """
+    ScheduleResponse L{Response} object.
+    Renders itself as a CalDAV:schedule-response XML document.
+    """
+    def __init__(self, xml_responses, location=None):
+        """
+        @param xml_responses: an interable of davxml.Response objects.
+        @param location:      the value of the location header to return in the response,
+            or None.
+        """
+
+        Response.__init__(self, code=responsecode.OK,
+                          stream=caldavxml.ScheduleResponse(*xml_responses).toxml())
+
+        self.headers.setHeader("content-type", MimeType("text", "xml"))
+    
+        if location is not None:
+            self.headers.setHeader("location", location)
+
+class ScheduleResponseQueue (object):
+    """
+    Stores a list of (typically error) responses for use in a
+    L{ScheduleResponse}.
+    """
+    def __init__(self, method, success_response):
+        """
+        @param method: the name of the method generating the queue.
+        @param success_response: the response to return in lieu of a
+            L{ScheduleResponse} if no responses are added to this queue.
+        """
+        self.responses         = []
+        self.method            = method
+        self.success_response  = success_response
+        self.location          = None
+
+    def setLocation(self, location):
+        """
+        @param location:      the value of the location header to return in the response,
+            or None.
+        """
+        self.location = location
+
+    def add(self, recipient, what, reqstatus=None, calendar=None):
+        """
+        Add a response.
+        @param recipient: the recipient for this response.
+        @param what: a status code or a L{Failure} for the given recipient.
+        @param status: the iTIP request-status for the given recipient.
+        @param calendar: the calendar data for the given recipient response.
+        """
+        if type(what) is int:
+            code    = what
+            error   = None
+            message = responsecode.RESPONSES[code]
+        elif isinstance(what, Failure):
+            code    = statusForFailure(what)
+            error   = errorForFailure(what)
+            message = messageForFailure(what)
+        else:
+            raise AssertionError("Unknown data type: %r" % (what,))
+
+        if code > 400: # Error codes only
+            logging.err("Error during %s for %s: %s" % (self.method, recipient, message), system="Scheduling")
+
+        children = []
+        children.append(caldavxml.Recipient(davxml.HRef.fromString(recipient)))
+        children.append(caldavxml.RequestStatus(reqstatus))
+        if calendar is not None:
+            children.append(caldavxml.CalendarData.fromCalendar(calendar))
+        if error is not None:
+            children.append(error)
+        if message is not None:
+            children.append(davxml.ResponseDescription(message))
+        self.responses.append(caldavxml.Response(*children))
+
+    def clone(self, clone):
+        """
+        Add a response cloned from an existing caldavxml.Response element.
+        @param clone: the response to clone.
+        """
+        if not isinstance(clone, caldavxml.Response):
+            raise AssertionError("Incorrect element type: %r" % (clone,))
+
+        recipient = clone.childOfType(caldavxml.Recipient)
+        request_status = clone.childOfType(caldavxml.RequestStatus)
+        calendar_data = clone.childOfType(caldavxml.CalendarData)
+        error = clone.childOfType(davxml.Error)
+        desc = clone.childOfType(davxml.ResponseDescription)
+
+        children = []
+        children.append(recipient)
+        children.append(request_status)
+        if calendar_data is not None:
+            children.append(calendar_data)
+        if error is not None:
+            children.append(error)
+        if desc is not None:
+            children.append(desc)
+        self.responses.append(caldavxml.Response(*children))
+
+    def response(self):
+        """
+        Generate a L{ScheduleResponseResponse} with the responses contained in the
+        queue or, if no such responses, return the C{success_response} provided
+        to L{__init__}.
+        @return: the response.
+        """
+        if self.responses:
+            return ScheduleResponseResponse(self.responses, self.location)
+        else:
+            return self.success_response

Copied: CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/servertoserver.py (from rev 2114, CalendarServer/branches/users/cdaboo/server2server-1965/twistedcaldav/servertoserver.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/servertoserver.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/servertoserver.py	2008-02-03 19:53:18 UTC (rev 2115)
@@ -0,0 +1,171 @@
+##
+# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+"""
+Server to server utility functions and client requests.
+"""
+
+__all__ = [
+    "ServerToServer",
+    "ServerToServerRequest",
+]
+
+
+from twisted.internet.defer import deferredGenerator
+from twisted.internet.defer import waitForDeferred
+from twisted.internet.protocol import ClientCreator
+from twisted.python.failure import Failure
+from twisted.python.filepath import FilePath
+from twisted.web2 import responsecode
+from twisted.web2.client.http import ClientRequest
+from twisted.web2.client.http import HTTPClientProtocol
+from twisted.web2.dav.http import ErrorResponse
+from twisted.web2.dav.util import davXMLFromStream
+from twisted.web2.http import HTTPError
+from twisted.web2.http_headers import Headers
+from twisted.web2.http_headers import MimeType
+
+from twistedcaldav.caldavxml import caldav_namespace
+from twistedcaldav.config import config
+from twistedcaldav.servertoserverparser import ServerToServerParser
+from twistedcaldav import caldavxml
+from twistedcaldav import logging
+
+class ServerToServer(object):
+    
+    _fileInfo = None
+    _xmlFile = None
+    _servers = None
+    _domainMap = None
+    
+    def __init__(self):
+        
+        self._loadConfig()
+
+    def _loadConfig(self):
+        if ServerToServer._servers is None:
+            ServerToServer._xmlFile = FilePath(config.ServerToServer["Servers"])
+        ServerToServer._xmlFile.restat()
+        fileInfo = (ServerToServer._xmlFile.getmtime(), ServerToServer._xmlFile.getsize())
+        if fileInfo != ServerToServer._fileInfo:
+            parser = ServerToServerParser(ServerToServer._xmlFile)
+            ServerToServer._servers = parser.servers
+            self._mapDomains()
+            ServerToServer._fileInfo = fileInfo
+        
+    def _mapDomains(self):
+        ServerToServer._domainMap = {}
+        for server in ServerToServer._servers:
+            for domain in server.domains:
+                ServerToServer._domainMap[domain] = server
+    
+    def mapDomain(self, domain):
+        """
+        Map a calendar user address domain to a suitable server that can
+        handle server-to-server requests for that user.
+        """
+        return ServerToServer._domainMap.get(domain)
+
+class ServerToServerRequest(object):
+    
+    def __init__(self, scheduler, server, recipients, responses):
+
+        self.scheduler = scheduler
+        self.server = server
+        self.recipients = recipients
+        self.responses = responses
+        
+        self._generateHeaders()
+        self._prepareData()
+        
+    @deferredGenerator
+    def doRequest(self):
+        
+        # Generate an HTTP client request
+        try:
+            from twisted.internet import reactor
+            if self.server.ssl:
+                from tap import ChainingOpenSSLContextFactory
+                context = ChainingOpenSSLContextFactory(config.SSLPrivateKey, config.SSLCertificate, certificateChainFile=config.SSLAuthorityChain)
+                d = waitForDeferred(ClientCreator(reactor, HTTPClientProtocol).connectSSL(self.server.host, self.server.port, context))
+            else:
+                d = waitForDeferred(ClientCreator(reactor, HTTPClientProtocol).connectTCP(self.server.host, self.server.port))
+            yield d
+            proto = d.getResult()
+            
+            request = ClientRequest("POST", self.server.path, self.headers, self.data)
+            if logging.canLog("debug"):
+                d = waitForDeferred(logging.logRequest("Sending server-to-server POST request:", request, system="Server-to-server Send"))
+                yield d
+                d.getResult()
+            d = waitForDeferred(proto.submitRequest(request))
+            yield d
+            response = d.getResult()
+    
+            if logging.canLog("debug"):
+                d = waitForDeferred(logging.logResponse("Received server-to-server POST response:", response, system="Server-to-server Send"))
+                yield d
+                d.getResult()
+            d = waitForDeferred(davXMLFromStream(response.stream))
+            yield d
+            xml = d.getResult()
+    
+            self._parseResponse(xml)
+        except Exception, e:
+            # Generated failed responses for each recipient
+            logging.err("Could not do server-to-server request : %s %s" % (self, e), system="Server-to-server Send")
+            for recipient in self.recipients:
+                err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-failed")))
+                self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus="5.1;Service unavailable")
+
+    def _generateHeaders(self):
+        self.headers = Headers()
+        self.headers.setHeader('Host', self.server.host + ":%s" % (self.server.port,))
+        self.headers.addRawHeader('Originator', self.scheduler.originator.cuaddr)
+        self._doAuthentication()
+        for recipient in self.recipients:
+            self.headers.addRawHeader('Recipient', recipient.cuaddr)
+        self.headers.setHeader('Content-Type', MimeType("text", "calendar", params={"charset":"utf-8"}))
+
+    def _doAuthentication(self):
+        if self.server.authentication and self.server.authentication[0] == "basic":
+            self.headers.setHeader(
+                'Authorization',
+                ('Basic', ("%s:%s" % (self.server.authentication[1], self.server.authentication[2],)).encode('base64')[:-1])
+            )
+
+    def _prepareData(self):
+        self.data = str(self.scheduler.calendar)
+
+    def _parseResponse(self, xml):
+
+        # Check for correct root element
+        schedule_response = xml.root_element
+        if not isinstance(schedule_response, caldavxml.ScheduleResponse) or not schedule_response.children:
+            raise HTTPError(responsecode.BAD_REQUEST)
+        
+        # Parse each response - do this twice: once looking for errors that will
+        # result in all recipients shown as failures; the second loop adds all the
+        # valid responses to the actual result.
+        for response in schedule_response.children:
+            if not isinstance(response, caldavxml.Response) or not response.children:
+                raise HTTPError(responsecode.BAD_REQUEST)
+            recipient = response.childOfType(caldavxml.Recipient)
+            request_status = response.childOfType(caldavxml.RequestStatus)
+            if not recipient or not request_status:
+                raise HTTPError(responsecode.BAD_REQUEST)
+        for response in schedule_response.children:
+            self.responses.clone(response)

Copied: CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/servertoserverparser.py (from rev 2114, CalendarServer/branches/users/cdaboo/server2server-1965/twistedcaldav/servertoserverparser.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/servertoserverparser.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/servertoserverparser.py	2008-02-03 19:53:18 UTC (rev 2115)
@@ -0,0 +1,156 @@
+##
+# Copyright (c) 2006-2007 Apple Inc. All rights reserved.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+##
+
+
+"""
+XML based server-to-server configuration file handling.
+"""
+
+__all__ = [
+    "ServerToServerParser",
+    "ServerToServerRecord",
+]
+
+import xml.dom.minidom
+
+ELEMENT_SERVERS                 = "servers"
+ELEMENT_SERVER                  = "server"
+ELEMENT_URI                     = "uri"
+ELEMENT_ALLOW_REQUESTS_FROM     = "allow-requests-from"
+ELEMENT_ALLOW_REQUESTS_TO       = "allow-requests-to"
+ELEMENT_DOMAINS                 = "domains"
+ELEMENT_DOMAIN                  = "domain"
+ELEMENT_AUTHENTICATION          = "authentication"
+ATTRIBUTE_TYPE                  = "type"
+ATTRIBUTE_BASICAUTH             = "basic"
+ELEMENT_USER                    = "user"
+ELEMENT_PASSWORD                = "password"
+
+class ServerToServerParser(object):
+    """
+    Server-to-server configuration file parser.
+    """
+    def __repr__(self):
+        return "<%s %r>" % (self.__class__.__name__, self.xmlFile)
+
+    def __init__(self, xmlFile):
+
+        self.servers = []
+        
+        # Read in XML
+        fd = open(xmlFile.path, "r")
+        doc = xml.dom.minidom.parse(fd)
+        fd.close()
+
+        # Verify that top-level element is correct
+        servers_node = doc._get_documentElement()
+        if servers_node._get_localName() != ELEMENT_SERVERS:
+            self.log("Ignoring file %r because it is not a server-to-server config file" % (self.xmlFile,))
+            return
+        self._parseXML(servers_node)
+        
+    def _parseXML(self, node):
+        """
+        Parse the XML root node from the server-to-server configuration document.
+        @param node: the L{Node} to parse.
+        """
+
+        for child in node._get_childNodes():
+            child_name = child._get_localName()
+            if child_name is None:
+                continue
+            elif child_name == ELEMENT_SERVER:
+                self.servers.append(ServerToServerRecord())
+                self.servers[-1].parseXML(child)
+                
+class ServerToServerRecord (object):
+    """
+    Contains server-to-server details.
+    """
+    def __init__(self):
+        """
+        @param recordType: record type for directory entry.
+        """
+        self.uri = ""
+        self.allow_from = False
+        self.allow_to = True
+        self.domains = []
+        self.authentication = None
+
+    def parseXML(self, node):
+        for child in node._get_childNodes():
+            child_name = child._get_localName()
+            if child_name is None:
+                continue
+            elif child_name == ELEMENT_URI:
+                if child.firstChild is not None:
+                    self.uri = child.firstChild.data.encode("utf-8")
+            elif child_name == ELEMENT_ALLOW_REQUESTS_FROM:
+                self.allow_from = True
+            elif child_name == ELEMENT_ALLOW_REQUESTS_TO:
+                self.allow_to = True
+            elif child_name == ELEMENT_DOMAINS:
+                self._parseDomains(child)
+            elif child_name == ELEMENT_AUTHENTICATION:
+                self._parseAuthentication(child)
+            else:
+                raise RuntimeError("[%s] Unknown attribute: %s" % (self.__class__, child_name,))
+        
+        self._parseDetails()
+
+    def _parseDomains(self, node):
+        for child in node._get_childNodes():
+            if child._get_localName() == ELEMENT_DOMAIN:
+                if child.firstChild is not None:
+                    self.domains.append(child.firstChild.data.encode("utf-8"))
+
+    def _parseAuthentication(self, node):
+        if node.hasAttribute(ATTRIBUTE_TYPE):
+            type = node.getAttribute(ATTRIBUTE_TYPE).encode("utf-8")
+            if type != ATTRIBUTE_BASICAUTH:
+                return
+        else:
+            return
+
+        for child in node._get_childNodes():
+            if child._get_localName() == ELEMENT_USER:
+                if child.firstChild is not None:
+                    user = child.firstChild.data.encode("utf-8")
+            elif child._get_localName() == ELEMENT_PASSWORD:
+                if child.firstChild is not None:
+                    password = child.firstChild.data.encode("utf-8")
+        
+        self.authentication = ("basic", user, password,)
+
+    def _parseDetails(self):
+        # Extract scheme, host, port and path
+        if self.uri.startswith("http://"):
+            self.ssl = False
+            rest = self.uri[7:]
+        elif self.uri.startswith("https://"):
+            self.ssl = True
+            rest = self.uri[8:]
+        
+        splits = rest.split("/", 1)
+        hostport = splits[0].split(":")
+        self.host = hostport[0]
+        if len(hostport) > 1:
+            self.port = int(hostport[1])
+        else:
+            self.port = {False:80, True:443}[self.ssl]
+        self.path = "/"
+        if len(splits) > 1:
+            self.path += splits[1]

Modified: CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/static.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/static.py	2008-02-01 22:00:25 UTC (rev 2114)
+++ CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/static.py	2008-02-03 19:53:18 UTC (rev 2115)
@@ -55,12 +55,13 @@
 from twistedcaldav.caldavxml import caldav_namespace
 from twistedcaldav.config import config
 from twistedcaldav.extensions import DAVFile
+from twistedcaldav.freebusyurl import FreeBusyURLResource
 from twistedcaldav.ical import Component as iComponent
 from twistedcaldav.ical import Property as iProperty
 from twistedcaldav.index import Index, IndexSchedule
 from twistedcaldav.notifications import NotificationsCollectionResource, NotificationResource
 from twistedcaldav.resource import CalDAVResource, isCalendarCollectionResource, isPseudoCalendarCollectionResource
-from twistedcaldav.schedule import ScheduleInboxResource, ScheduleOutboxResource
+from twistedcaldav.schedule import ScheduleInboxResource, ScheduleOutboxResource, ScheduleServerToServerResource
 from twistedcaldav.dropbox import DropBoxHomeResource, DropBoxCollectionResource, DropBoxChildResource
 from twistedcaldav.directory.calendar import DirectoryCalendarHomeProvisioningResource
 from twistedcaldav.directory.calendar import DirectoryCalendarHomeTypeProvisioningResource
@@ -501,12 +502,17 @@
             NotificationsCollectionFileClass = NotificationsCollectionFile
         else:
             NotificationsCollectionFileClass = None
+        if config.FreeBusyURL["Enabled"]:
+            FreeBusyURLFileClass = FreeBusyURLFile
+        else:
+            FreeBusyURLFileClass = None
             
         cls = {
             "inbox"        : ScheduleInboxFile,
             "outbox"       : ScheduleOutboxFile,
             "dropbox"      : DropBoxHomeFileClass,
             "notifications": NotificationsCollectionFileClass,
+            "freebusy"     : FreeBusyURLFileClass,
         }.get(name, None)
 
         if cls is not None:
@@ -604,6 +610,80 @@
     def __repr__(self):
         return "<%s (calendar outbox collection): %s>" % (self.__class__.__name__, self.fp.path)
 
+class ServerToServerInboxFile (ScheduleServerToServerResource, CalDAVFile):
+    """
+    Server-to-server scheduling inbox resource.
+    """
+    def __init__(self, path, parent):
+        CalDAVFile.__init__(self, path, principalCollections=parent.principalCollections())
+        ScheduleServerToServerResource.__init__(self, parent)
+        
+        self.fp.open("w").close()
+        self.fp.restat(False)
+
+    def __repr__(self):
+        return "<%s (server-to-server inbox resource): %s>" % (self.__class__.__name__, self.fp.path)
+
+    def isCollection(self):
+        return False
+
+    def createSimilarFile(self, path):
+        if path == self.fp.path:
+            return self
+        else:
+            return responsecode.NOT_FOUND
+
+    def http_PUT        (self, request): return responsecode.FORBIDDEN
+    def http_COPY       (self, request): return responsecode.FORBIDDEN
+    def http_MOVE       (self, request): return responsecode.FORBIDDEN
+    def http_DELETE     (self, request): return responsecode.FORBIDDEN
+    def http_MKCOL      (self, request): return responsecode.FORBIDDEN
+
+    def http_MKCALENDAR(self, request):
+        return ErrorResponse(
+            responsecode.FORBIDDEN,
+            (caldav_namespace, "calendar-collection-location-ok")
+        )
+
+class FreeBusyURLFile (AutoProvisioningFileMixIn, FreeBusyURLResource, CalDAVFile):
+    """
+    Free-busy URL resource.
+    """
+    def __init__(self, path, parent):
+        CalDAVFile.__init__(self, path, principalCollections=parent.principalCollections())
+        FreeBusyURLResource.__init__(self, parent)
+
+    def __repr__(self):
+        return "<%s (free-busy URL resource): %s>" % (self.__class__.__name__, self.fp.path)
+
+    def isCollection(self):
+        return False
+
+    def createSimilarFile(self, path):
+        if path == self.fp.path:
+            return self
+        else:
+            return responsecode.NOT_FOUND
+
+    def http_PUT        (self, request): return responsecode.FORBIDDEN
+    def http_COPY       (self, request): return responsecode.FORBIDDEN
+    def http_MOVE       (self, request): return responsecode.FORBIDDEN
+    def http_DELETE     (self, request): return responsecode.FORBIDDEN
+    def http_MKCOL      (self, request): return responsecode.FORBIDDEN
+
+    def http_MKCALENDAR(self, request):
+        return ErrorResponse(
+            responsecode.FORBIDDEN,
+            (caldav_namespace, "calendar-collection-location-ok")
+        )
+
+    ##
+    # ACL
+    ##
+
+    def supportedPrivileges(self, request):
+        return succeed(schedulePrivilegeSet)
+
 class DropBoxHomeFile (AutoProvisioningFileMixIn, DropBoxHomeResource, CalDAVFile):
     def __init__(self, path, parent):
         DropBoxHomeResource.__init__(self)

Modified: CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/tap.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/tap.py	2008-02-01 22:00:25 UTC (rev 2114)
+++ CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/tap.py	2008-02-03 19:53:18 UTC (rev 2115)
@@ -50,6 +50,7 @@
 
 from twistedcaldav import pdmonster
 from twistedcaldav.static import CalendarHomeProvisioningFile
+from twistedcaldav.static import ServerToServerInboxFile
 from twistedcaldav.timezones import TimezoneCache
 
 try:
@@ -340,9 +341,10 @@
     # default resource classes
     #
 
-    rootResourceClass      = RootResource
-    principalResourceClass = DirectoryPrincipalProvisioningResource
-    calendarResourceClass  = CalendarHomeProvisioningFile
+    rootResourceClass           = RootResource
+    principalResourceClass      = DirectoryPrincipalProvisioningResource
+    calendarResourceClass       = CalendarHomeProvisioningFile
+    servertoserverResourceClass = ServerToServerInboxFile
 
     def makeService_Slave(self, options):
         
@@ -418,6 +420,15 @@
         root.putChild('principals', principalCollection)
         root.putChild('calendars', calendarCollection)
 
+        if config.ServerToServer["Enabled"]:
+            log.msg("Setting up server-to-server resource: %r" % (self.servertoserverResourceClass,))
+    
+            servertoserver = self.servertoserverResourceClass(
+                os.path.join(config.DocumentRoot, 'inbox'),
+                root,
+            )
+            root.putChild('inbox', servertoserver)
+
         # Configure default ACLs on the root resource
 
         logging.info("Setting up default ACEs on root resource", system="startup")

-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20080203/f40590d0/attachment-0001.html


More information about the calendarserver-changes mailing list