[CalendarServer-changes] [2412] CalendarServer/branches/users/cdaboo/server2server-2407

source_changes at macosforge.org source_changes at macosforge.org
Tue May 13 12:28:35 PDT 2008


Revision: 2412
          http://trac.macosforge.org/projects/calendarserver/changeset/2412
Author:   cdaboo at apple.com
Date:     2008-05-13 12:28:33 -0700 (Tue, 13 May 2008)

Log Message:
-----------
Merged forward from trunk.

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

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

Modified: CalendarServer/branches/users/cdaboo/server2server-2407/conf/caldavd-test.plist
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2407/conf/caldavd-test.plist	2008-05-12 22:48:07 UTC (rev 2411)
+++ CalendarServer/branches/users/cdaboo/server2server-2407/conf/caldavd-test.plist	2008-05-13 19:28:33 UTC (rev 2412)
@@ -344,7 +344,36 @@
   <key>EnablePrivateEvents</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>
+  	<key>Anonymous Access</key>
+  	<false/>
+  </dict>
+
   <!--
     Twisted
   -->

Modified: CalendarServer/branches/users/cdaboo/server2server-2407/conf/caldavd.plist
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2407/conf/caldavd.plist	2008-05-12 22:48:07 UTC (rev 2411)
+++ CalendarServer/branches/users/cdaboo/server2server-2407/conf/caldavd.plist	2008-05-13 19:28:33 UTC (rev 2412)
@@ -265,6 +265,35 @@
   <key>EnablePrivateEvents</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>
+  	<key>Anonymous Access</key>
+  	<false/>
+  </dict>
+
 </dict>
 </plist>

Copied: CalendarServer/branches/users/cdaboo/server2server-2407/conf/servertoserver-test.xml (from rev 2408, CalendarServer/branches/users/cdaboo/server2server-2113/conf/servertoserver-test.xml)
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2407/conf/servertoserver-test.xml	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/server2server-2407/conf/servertoserver-test.xml	2008-05-13 19:28:33 UTC (rev 2412)
@@ -0,0 +1,33 @@
+<?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>
+    <hosts>
+    	<host>127.0.0.1</host>
+    </hosts>
+  </server>
+</servers>

Copied: CalendarServer/branches/users/cdaboo/server2server-2407/conf/servertoserver.dtd (from rev 2408, CalendarServer/branches/users/cdaboo/server2server-2113/conf/servertoserver.dtd)
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2407/conf/servertoserver.dtd	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/server2server-2407/conf/servertoserver.dtd	2008-05-13 19:28:33 UTC (rev 2412)
@@ -0,0 +1,35 @@
+<!--
+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?, hosts?) >
+
+		<!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) >
+		<!ELEMENT hosts (host*) >
+			<!ELEMENT host (#PCDATA) >
+			
\ No newline at end of file

Modified: CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/__init__.py	2008-05-12 22:48:07 UTC (rev 2411)
+++ CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/__init__.py	2008-05-13 19:28:33 UTC (rev 2412)
@@ -34,6 +34,7 @@
     "dropbox",
     "extensions",
     "fileops",
+    "freebusyurl",
     "ical",
     "icaldav",
     "index",
@@ -44,8 +45,12 @@
     "resource",
     "root",
     "schedule",
+    "schedule_common",
+    "servertoserver",
+    "servertoserverparser",
     "sql",
     "static",
+    "timezones",
 ]
 
 try:

Modified: CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/config.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/config.py	2008-05-12 22:48:07 UTC (rev 2411)
+++ CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/config.py	2008-05-13 19:28:33 UTC (rev 2412)
@@ -156,7 +156,21 @@
     #
     "EnableDropBox"      : False, # Calendar Drop Box
     "EnablePrivateEvents": False, # Private Events
+    
+    "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
+        "Anonymous Access" : False, # Allow anonymous read access to free-busy URL
+    },
+
     #
     # Implementation details
     #
@@ -193,6 +207,9 @@
 }
 
 class Config (object):
+    """
+    @DynamicAttrs
+    """
     def __init__(self, defaults):
         self.setDefaults(defaults)
         self._data = copy.deepcopy(self._defaults)

Modified: CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/customxml.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/customxml.py	2008-05-12 22:48:07 UTC (rev 2411)
+++ CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/customxml.py	2008-05-13 19:28:33 UTC (rev 2412)
@@ -188,6 +188,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
 ##
@@ -196,3 +212,5 @@
 davxml.ResourceType.dropbox = davxml.ResourceType(davxml.Collection(), DropBox())
 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-2407/twistedcaldav/directory/calendar.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/directory/calendar.py	2008-05-12 22:48:07 UTC (rev 2411)
+++ CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/directory/calendar.py	2008-05-13 19:28:33 UTC (rev 2412)
@@ -37,6 +37,7 @@
 from twistedcaldav.config import config
 from twistedcaldav.dropbox import DropBoxHomeResource
 from twistedcaldav.extensions import ReadOnlyResourceMixIn, DAVResource
+from twistedcaldav.freebusyurl import FreeBusyURLResource
 from twistedcaldav.resource import CalDAVResource
 from twistedcaldav.schedule import ScheduleInboxResource, ScheduleOutboxResource
 from twistedcaldav.directory.idirectory import IDirectoryService
@@ -261,6 +262,10 @@
             childlist += (
                 ("dropbox", DropBoxHomeResource),
             )
+        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-2407/twistedcaldav/freebusyurl.py (from rev 2408, CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/freebusyurl.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/freebusyurl.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/freebusyurl.py	2008-05-13 19:28:33 UTC (rev 2412)
@@ -0,0 +1,247 @@
+##
+# 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):
+        aces = (
+            # 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(),
+            ),
+        )
+        if config.FreeBusyURL["Anonymous Access"]:
+            aces += (
+                # DAV:Read, for unauthenticated principals
+                davxml.ACE(
+                    davxml.Principal(davxml.Unauthenticated()),
+                    davxml.Grant(
+                        davxml.Privilege(davxml.Read()),
+                    ),
+                    davxml.Protected(),
+                ),
+            )
+        return davxml.ACL(*aces)
+
+    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 targeted 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,
+            True,
+        ))
+        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-2407/twistedcaldav/ical.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/ical.py	2008-05-12 22:48:07 UTC (rev 2411)
+++ CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/ical.py	2008-05-13 19:28:33 UTC (rev 2412)
@@ -343,7 +343,8 @@
         @return: the L{Component} of the primary type.
         @raise: L{ValueError} if there is more than one primary type.
         """
-        assert self.name() == "VCALENDAR", "Must be a VCALENDAR: %r" % (self,)
+        if self.name() != "VCALENDAR":
+            assert self.name() == "VCALENDAR", "Must be a VCALENDAR: %r" % (self,)
         
         result = None
         for component in self.subcomponents():

Modified: CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/itip.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/itip.py	2008-05-12 22:48:07 UTC (rev 2411)
+++ CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/itip.py	2008-05-13 19:28:33 UTC (rev 2412)
@@ -46,7 +46,7 @@
 from twistedcaldav.log import Logger
 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
 
 log = Logger()
@@ -54,963 +54,902 @@
 __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.
+        
+        """
+        
+        log.info("Auto-processing iTIP REQUEST for: %s" % (str(self.principal),))
+        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()
     
-    log.info("Auto-processing iTIP REQUEST for: %s" % (str(principal),))
-    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()
+                            log.info("Replaced calendar component %s with new iTIP message in %s." % (calmatch, calURL))
+                        else:
+                            newchild = waitForDeferred(self.writeResource(calURL, updatecal, None, self.calendar))
+                            yield newchild
+                            newchild.getResult()
+                            log.info("Added new calendar component in %s." % (calURL,))
+                    else:
+                        if calmatch:
+                            d = waitForDeferred(self.deleteResource(updatecal, calmatch))
+                            yield d
+                            d.getResult()
+                            log.info("Deleted calendar component %s in %s as update was not accepted." % (calmatch, calURL))
+                            
+                    # Send a reply if needed. 
+                    if doreply:
+                        log.info("Sending iTIP REPLY %s" % (("declined","accepted")[accepted],))
+                        newchild = waitForDeferred(self.writeReply(replycal))
+                        yield newchild
+                        newchild = newchild.getResult()
+                    processed = "processed"
+                except:
+                    log.err("Error while auto-processing iTIP: %s" % (failure.Failure(),))
+                    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()
-                        log.info("Replaced calendar component %s with new iTIP message in %s." % (calmatch, calURL))
+                        log.info("Merged calendar component %s with new iTIP message in %s." % (calmatch, calURL))
                     else:
-                        newchild = waitForDeferred(writeResource(request, calURL, updatecal, None, calendar))
+                        if accepted:
+                            newchild = waitForDeferred(self.writeResource(calURL, updatecal, None, self.calendar))
+                            yield newchild
+                            newchild.getResult()
+                            log.info("Added new calendar component in %s." % (calURL,))
+                            
+                    # Do reply if needed. 
+                    if doreply:
+                        log.info("Sending iTIP REPLY %s" % (("declined","accepted")[accepted],))
+                        newchild = waitForDeferred(self.writeReply(replycal))
                         yield newchild
-                        newchild.getResult()
-                        log.info("Added new calendar component in %s." % (calURL,))
-                else:
-                    if calmatch:
-                        d = waitForDeferred(deleteResource(updatecal, calmatch))
-                        yield d
-                        d.getResult()
-                        log.info("Deleted calendar component %s in %s as update was not accepted." % (calmatch, calURL))
+                        newchild = newchild.getResult()
                         
-                # Send a reply if needed. 
-                if doreply:
-                    log.info("Sending iTIP REPLY %s" % (("declined","accepted")[accepted],))
-                    newchild = waitForDeferred(writeReply(request, principal, replycal, inbox))
-                    yield newchild
-                    newchild = newchild.getResult()
-                    newInboxResource(child, newchild)
-                processed = "processed"
-            except:
-                log.err("Error while auto-processing iTIP: %s" % (failure.Failure(),))
-                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()
-                    log.info("Merged calendar component %s with new iTIP message in %s." % (calmatch, calURL))
-                else:
-                    if accepted:
-                        newchild = waitForDeferred(writeResource(request, calURL, updatecal, None, calendar))
-                        yield newchild
-                        newchild.getResult()
-                        log.info("Added new calendar component in %s." % (calURL,))
-                        
-                # Do reply if needed. 
-                if doreply:
-                    log.info("Sending iTIP REPLY %s" % (("declined","accepted")[accepted],))
-                    newchild = waitForDeferred(writeReply(request, principal, replycal, inbox))
-                    yield newchild
-                    newchild = newchild.getResult()
-                    newInboxResource(child, newchild)
-                    
-                processed = "processed"
-            except:
-                log.err("Error while auto-processing iTIP: %s" % (failure.Failure(),))
-                raise iTipException
-
-    # Remove the now processed incoming request.
-    try:
-        d = waitForDeferred(deleteResource(inbox, child.fp.basename()))
-        yield d
-        d.getResult()
-        log.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]
-                ))
-    except:
-        log.err("Error while auto-processing iTIP: %s" % (failure.Failure(),))
-        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.
-    """
-    log.info("Auto-processing iTIP ADD for: %s" % (str(principal),))
-
-    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:
+                    log.err("Error while auto-processing iTIP: %s" % (failure.Failure(),))
+                    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.
+        """
+        log.info("Auto-processing iTIP ADD for: %s" % (str(self.principal),))
     
-    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.
     
-    log.info("Auto-processing iTIP CANCEL for: %s" % (str(principal),))
-    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...
+        """
+        
+        log.info("Auto-processing iTIP CANCEL for: %s" % (str(self.principal),))
+        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()
-                    log.info("Delete calendar component %s in %s as it was cancelled." % (calmatch, calURL))
-                except:
-                    log.err("Error while auto-processing iTIP: %s" % (failure.Failure(),))
-                    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()
+                        log.info("Delete calendar component %s in %s as it was cancelled." % (calmatch, calURL))
+                    except:
+                        log.err("Error while auto-processing iTIP: %s" % (failure.Failure(),))
+                        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()
-                    log.info("Deleted calendar component %s after cancellations from iTIP message in %s." % (calmatch, calURL))
+                    # 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()
+                        log.info("Deleted calendar component %s after cancellations from iTIP message in %s." % (calmatch, calURL))
+                    else:
+                        # Update the existing calendar object
+                        newchild = waitForDeferred(self.writeResource(calURL, updatecal, calmatch, existing_calendar))
+                        yield newchild
+                        newchild = newchild.getResult()
+                        log.info("Updated calendar component %s with cancellations from iTIP message in %s." % (calmatch, calURL))
+                    processed = "processed"
                 else:
-                    # Update the existing calendar object
-                    newchild = waitForDeferred(writeResource(request, calURL, updatecal, calmatch, existing_calendar))
-                    yield newchild
-                    newchild = newchild.getResult()
-                    log.info("Updated calendar component %s with cancellations from iTIP message in %s." % (calmatch, calURL))
-                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()
-        log.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]
-                ))
-    except:
-        log.err("Error while auto-processing iTIP: %s" % (failure.Failure(),))
-        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 instance 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 figure 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
+                    log.info("Exceeded number of matches whilst trying to find free-time.")
                     break
-            except NumberOfMatchesWithinLimits:
-                accepted = False
-                log.info("Exceeded number of matches whilst trying to find free-time.")
+                
+            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.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:
-        log.info("Could not send reply as %s does not have CALDAV:schedule permission on %s Inbox." % (principal.principalURL(), organizer))
-        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:
+            log.info("Could not send reply as %s does not have CALDAV:schedule permission on %s Inbox." % (self.principal.principalURL(), organizer))
+            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()
-                log.info("Deleted iTIP message %s in Inbox that was older than the new one." % (i[0],))
-            except:
-                log.err("Error while auto-processing iTIP: %s" % (failure.Failure(),))
-                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()
-            log.info("Deleted new iTIP message %s in Inbox because it was older than existing ones." % (child.fp.basename(),))
+            log.info("Deleted new iTIP message %s in Inbox because it has been %s." %
+                (self.childname, processed_state,))
         except:
             log.err("Error while auto-processing iTIP: %s" % (failure.Failure(),))
             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:
-            log.info("Found calendar component %s matching new iTIP message in %s." % (calmatch, calURL))
-            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:
+                log.info("Found calendar component %s matching new iTIP message in %s." % (calmatch, calURL))
+                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")
-        if seq:
-            seq = int(seq)
-        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")
+            if seq:
+                seq = int(seq)
+            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-2407/twistedcaldav/log.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/log.py	2008-05-12 22:48:07 UTC (rev 2411)
+++ CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/log.py	2008-05-13 19:28:33 UTC (rev 2412)
@@ -59,6 +59,15 @@
 
 from twisted.python import log
 
+from StringIO import StringIO
+
+from twisted.internet.defer import deferredGenerator
+from twisted.internet.defer import waitForDeferred
+
+from twisted.web2 import responsecode
+from twisted.web2.dav.util import allDataFromStream
+from twisted.web2.stream import MemoryStream
+
 logLevels = (
     "debug",
     "info",
@@ -78,7 +87,7 @@
     return sorted(levels, cmpLogLevels, reverse=True)[0]
 
 ##
-# Tools for manageing log levels
+# Tools for managing log levels
 ##
 
 defaultLogLevel = "info"
@@ -192,6 +201,79 @@
         """
         return cmpLogLevels(self.level(), level) <= 0
 
+    @deferredGenerator
+    def logRequest(self, level, message, request, **kwargs):
+        """
+        Log an HTTP request.
+        """
+
+        assert level in logLevels
+
+        if self.willLogAtLevel(level):
+            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))
+                    else:
+                        iostr.write("%s: xxxxxxxxx\n" % (name,))
+            iostr.write("\n")
+            
+            # We need to play a trick with the request stream as we can only read it once. So we
+            # read it, store the value in a MemoryStream, and replace the request's stream with that,
+            # so the data can be read again.
+            data = waitForDeferred(allDataFromStream(request.stream))
+            yield data
+            data = data.getResult()
+            iostr.write(data)
+            
+            request.stream = MemoryStream(data)
+            request.stream.doStartReading = None
+        
+            self.emit(level, iostr.getvalue(), **kwargs)
+    
+    @deferredGenerator
+    def logResponse(self, level, message, response, **kwargs):
+        """
+        Log an HTTP request.
+        """
+
+        assert level in logLevels
+
+        if self.willLogAtLevel(level):
+            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 ("WWW-Authenticate",):
+                        iostr.write("%s: %s\n" % (name, value))
+                    else:
+                        iostr.write("%s: xxxxxxxxx\n" % (name,))
+            iostr.write("\n")
+            
+            # We need to play a trick with the response stream to ensure we don't mess it up. So we
+            # read it, store the value in a MemoryStream, and replace the response's stream with that,
+            # so the data can be read again.
+            data = waitForDeferred(allDataFromStream(response.stream))
+            yield data
+            data = data.getResult()
+            iostr.write(data)
+            
+            response.stream = MemoryStream(data)
+            response.stream.doStartReading = None
+        
+            self.emit(level, iostr.getvalue(), **kwargs)
+
 class LoggingMixIn (object):
     """
     Mix-in class for logging methods.

Modified: CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/method/copymove.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/method/copymove.py	2008-05-12 22:48:07 UTC (rev 2411)
+++ CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/method/copymove.py	2008-05-13 19:28:33 UTC (rev 2412)
@@ -31,7 +31,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
 from twistedcaldav.log import Logger
 
@@ -101,7 +101,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,
@@ -111,7 +111,8 @@
         destination_uri = destination_uri,
         destinationparent = destinationparent,
         destinationcal = destinationcal,
-    ))
+    )
+    x = waitForDeferred(storer.run())
     yield x
     yield x.getResult()
 
@@ -185,18 +186,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()
 
@@ -209,15 +211,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-2407/twistedcaldav/method/put.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/method/put.py	2008-05-12 22:48:07 UTC (rev 2411)
+++ CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/method/put.py	2008-05-13 19:28:33 UTC (rev 2412)
@@ -27,7 +27,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
 from twistedcaldav.log import Logger
 
@@ -60,15 +60,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-2407/twistedcaldav/method/put_common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/method/put_common.py	2008-05-12 22:48:07 UTC (rev 2411)
+++ CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/method/put_common.py	2008-05-13 19:28:33 UTC (rev 2412)
@@ -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
@@ -57,56 +59,8 @@
 
 log = Logger()
 
- at deferredGenerator
-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
@@ -114,7 +68,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
@@ -134,32 +89,32 @@
                 log.debug("Rollback: rollback")
                 try:
                     if self.source_copy and self.source_deleted:
-                        self.source_copy.moveTo(source.fp)
-                        log.debug("Rollback: source restored %s to %s" % (self.source_copy.path, source.fp.path))
+                        self.source_copy.moveTo(self.storer.source.fp)
+                        log.debug("Rollback: source restored %s to %s" % (self.source_copy.path, self.storer.source.fp.path))
                         self.source_copy = None
                         self.source_deleted = False
                     if self.destination_copy:
-                        destination.fp.remove()
-                        log.debug("Rollback: destination restored %s to %s" % (self.destination_copy.path, destination.fp.path))
-                        self.destination_copy.moveTo(destination.fp)
+                        self.storer.destination.fp.remove()
+                        log.debug("Rollback: destination restored %s to %s" % (self.destination_copy.path, self.storer.destination.fp.path))
+                        self.destination_copy.moveTo(self.storer.destination.fp)
                         self.destination_copy = None
                     elif self.destination_created:
-                        if destinationcal:
-                            doRemoveDestinationIndex()
-                            log.debug("Rollback: destination index removed %s" % (destination.fp.path,))
+                        if self.storer.destinationcal:
+                            self.storer.doRemoveDestinationIndex()
+                            log.debug("Rollback: destination index removed %s" % (self.storer.destination.fp.path,))
                             self.destination_index_deleted = False
-                        destination.fp.remove()
-                        log.debug("Rollback: destination removed %s" % (destination.fp.path,))
+                        self.storer.destination.fp.remove()
+                        log.debug("Rollback: destination removed %s" % (self.storer.destination.fp.path,))
                         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
-                        log.debug("Rollback: destination re-indexed %s" % (destination.fp.path,))
+                        log.debug("Rollback: destination re-indexed %s" % (self.storer.destination.fp.path,))
                     if self.source_index_deleted:
-                        doSourceIndexRecover()
+                        self.storer.doSourceIndexRecover()
                         self.destination_index_deleted = False
-                        log.debug("Rollback: source re-indexed %s" % (source.fp.path,))
+                        log.debug("Rollback: source re-indexed %s" % (self.storer.source.fp.path,))
                 except:
                     log.err("Rollback: exception caught and not handled: %s" % failure.Failure())
 
@@ -182,57 +137,229 @@
                 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 behavior.
+        
+        @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
+        self.access = None
 
-    def validResourceName():
+    @deferredGenerator
+    def fullValidation(self):
         """
+        Do full validation 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")))
+
+            # Check access
+            if self.destinationcal and config.EnablePrivateEvents:
+                d = waitForDeferred(self.validAccess())
+                yield d
+                d.getResult()
+    
+    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,
+        @return:         tuple: (True/False if the calendar data is valid,
                                  log message string).
         """
         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,
@@ -241,17 +368,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.
@@ -259,7 +386,7 @@
         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)
@@ -267,45 +394,40 @@
         return result, message
 
     @deferredGenerator
-    def validAccess():
+    def validAccess(self):
         """
         Make sure that the X-CALENDARSERVER-ACCESS property is properly dealt with.
         """
         
-        if calendar.hasProperty(Component.ACCESS_PROPERTY):
+        if self.calendar.hasProperty(Component.ACCESS_PROPERTY):
             
             # Must be a value we know about
-            access = calendar.accessLevel(default=None)
-            if access is None:
+            self.access = self.calendar.accessLevel(default=None)
+            if self.access is None:
                 raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (calendarserver_namespace, "valid-access-restriction")))
                 
             # Only DAV:owner is able to set the property to other than PUBLIC
-            d = waitForDeferred(destinationparent.owner(request))
+            d = waitForDeferred(self.destinationparent.owner(self.request))
             yield d
             parent_owner = d.getResult()
             
-            authz = destinationparent.currentPrincipal(request)
-            if davxml.Principal(parent_owner) != authz and access != Component.ACCESS_PUBLIC:
+            authz = self.destinationparent.currentPrincipal(self.request)
+            if davxml.Principal(parent_owner) != authz and self.access != Component.ACCESS_PUBLIC:
                 raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (calendarserver_namespace, "valid-access-restriction-change")))
-
-            yield access, calendardata
         else:
             # Check whether an access property was present before and write that into the calendar data
-            newcalendardata = calendardata
-            if not source and destination.exists() and destination.hasDeadProperty(TwistedCalendarAccessProperty):
-                old_access = str(destination.readDeadProperty(TwistedCalendarAccessProperty))
-                calendar.addProperty(Property(name=Component.ACCESS_PROPERTY, value=old_access))
-                newcalendardata = str(calendar)
+            if not self.source and self.destination.exists() and self.destination.hasDeadProperty(TwistedCalendarAccessProperty):
+                old_access = str(self.destination.readDeadProperty(TwistedCalendarAccessProperty))
+                self.calendar.addProperty(Property(name=Component.ACCESS_PROPERTY, value=old_access))
+                self.calendardata = str(self.calendar)
 
-            yield None, newcalendardata
-
-    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
         different UID.
         @param uid: the UID for the resource being stored.
-        @return: tuple: (True/False if the uid is valid, log message string,
+        @return: tuple: (True/False if the UID is valid, log message string,
             name of conflicted resource).
         """
 
@@ -315,14 +437,14 @@
 
         # 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()):
+        # UID must be unique
+        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.
+            # This can happen if two simultaneous PUTs occur with the same UID.
             # i.e. one PUT has reserved the UID but has not yet written the resource,
             # the other PUT tries to reserve and fails but no index entry exists yet.
             if rname is None:
@@ -332,345 +454,318 @@
             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
-        access = None
-        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")))
-
-            # Check access
-            if destinationcal and config.EnablePrivateEvents:
-                d = waitForDeferred(validAccess())
-                yield d
-                access, calendardata = d.getResult()
-
-            # 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"
-            copyToWithXAttrs(destination.fp, rollback.destination_copy)
-            log.debug("Rollback: backing up destination %s to %s" % (destination.fp.path, rollback.destination_copy.path))
-        else:
-            rollback.destination_created = True
-            log.debug("Rollback: will create new destination %s" % (destination.fp.path,))
+        yield None
 
-        if deletesource:
-            rollback.source_copy = FilePath(source.fp.path)
-            rollback.source_copy.path += ".rollback"
-            copyToWithXAttrs(source.fp, rollback.source_copy)
-            log.debug("Rollback: backing up source %s to %s" % (source.fp.path, rollback.source_copy.path))
-    
+    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"
+            copyToWithXAttrs(self.destination.fp, self.rollback.destination_copy)
+            log.debug("Rollback: backing up destination %s to %s" % (self.destination.fp.path, self.rollback.destination_copy.path))
+        else:
+            self.rollback.destination_created = True
+            log.debug("Rollback: will create new destination %s" % (self.destination.fp.path,))
+
+        if self.deletesource:
+            self.rollback.source_copy = FilePath(self.source.fp.path)
+            self.rollback.source_copy.path += ".rollback"
+            copyToWithXAttrs(self.source.fp, self.rollback.source_copy)
+            log.debug("Rollback: backing up source %s to %s" % (self.source.fp.path, self.rollback.source_copy.path))
+
+    @deferredGenerator
+    def doStore(self):
         # Do put or copy based on whether source exists
-        if source is not None:
-            response = maybeDeferred(copyWithXAttrs, source.fp, destination.fp, destination_uri)
+        if self.source is not None:
+            response = maybeDeferred(copyWithXAttrs, self.source.fp, self.destination.fp, self.destination_uri)
         else:
-            md5 = MD5StreamWrapper(MemoryStream(calendardata))
-            response = maybeDeferred(putWithXAttrs, md5, destination.fp)
+            if self.calendardata is None:
+                self.calendardata = str(self.calendar)
+            md5 = MD5StreamWrapper(MemoryStream(self.calendardata))
+            response = maybeDeferred(putWithXAttrs, 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
+            # Finish MD5 calculation and write dead property
             md5.close()
             md5 = md5.getMD5()
-            destination.writeDeadProperty(TwistedGETContentMD5.fromString(md5))
-
+            self.destination.writeDeadProperty(TwistedGETContentMD5.fromString(md5))
+    
         # Update calendar-access property value on the resource
-        if access:
-            destination.writeDeadProperty(TwistedCalendarAccessProperty(access))
+        if self.access:
+            self.destination.writeDeadProperty(TwistedCalendarAccessProperty(self.access))
             
         # Do not remove the property if access was not specified and we are storing in a calendar.
         # This ensure that clients that do not preserve the iCalendar property do not cause access
         # restrictions to be lost.
-        elif not destinationcal:
-            destination.removeDeadProperty(TwistedCalendarAccessProperty)                
+        elif not self.destinationcal:
+            self.destination.removeDeadProperty(TwistedCalendarAccessProperty)                
 
-        response = IResponse(response)
+        yield 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)
-                log.debug("Destination indexed %s" % (destination.fp.path,))
-            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
+            log.debug("Source index removed %s" % (self.source.fp.path,))
 
-            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
+        log.debug("Source removed %s" % (self.source.fp.path,))
 
-        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
-                log.debug("Destination index removed %s" % (destination.fp.path,))
+        # 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
-                log.debug("Source index removed %s" % (source.fp.path,))
+        # 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
-            log.debug("Source removed %s" % (source.fp.path,))
+        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,
+    @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()
+
+        diff_size = new_dest_size - self.old_dest_size
+
+        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()
+
+        yield None
+
+    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)))
                 ))
 
-            source.writeDeadProperty(davxml.GETContentType.fromString("text/calendar"))
+            self.source.writeDeadProperty(davxml.GETContentType.fromString("text/calendar"))
             return None
 
-        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()
+    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)
+            log.debug("Destination indexed %s" % (self.destination.fp.path,))
+        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 sourcecal:
-                # Change CTag on the parent calendar collection
-                sourceparent.updateCTag()
+        self.destination.writeDeadProperty(davxml.GETContentType.fromString("text/calendar"))
+        return None
 
-        if destinationcal:
-            result = doDestinationIndex(calendar)
-            if result is not None:
-                rollback.Rollback()
-                yield result
-                return
+    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
+            log.debug("Destination index removed %s" % (self.destination.fp.path,))
 
-        # 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))
+    @deferredGenerator
+    def run(self):
+        """
+        Function that does common PUT/COPY/MOVE behavior.
+
+        @return: a Deferred with a status response result.
+        """
+
+        try:
+            reservation = None
+            
+            # Handle all validation operations here.
+            d = waitForDeferred(self.fullValidation())
             yield d
             d.getResult()
 
+            # 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 occurred or changes already committed.
+            if self.rollback:
+                self.rollback.Rollback()
 
-        if destinationcal:
-            # Change CTag on the parent calendar collection
-            destinationparent.updateCTag()
-
-        # Can now commit changes and forget the rollback details
-        rollback.Commit()
-
-        if reserved:
-            destination_index.unreserveUID(uid)
-            reserved = False
-
-        yield response
-        return
-
-    except:
-        if reserved:
-            destination_index.unreserveUID(uid)
-            reserved = False
-
-        # 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
+            raise

Modified: CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/method/report_common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/method/report_common.py	2008-05-12 22:48:07 UTC (rev 2411)
+++ CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/method/report_common.py	2008-05-13 19:28:33 UTC (rev 2412)
@@ -284,7 +284,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.
@@ -299,16 +300,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.
@@ -353,12 +357,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-2407/twistedcaldav/schedule.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/schedule.py	2008-05-12 22:48:07 UTC (rev 2411)
+++ CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/schedule.py	2008-05-13 19:28:33 UTC (rev 2412)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2008 Apple Inc. All rights reserved.
+# 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.
@@ -21,34 +21,26 @@
 __all__ = [
     "ScheduleInboxResource",
     "ScheduleOutboxResource",
+    "ScheduleServerToServerResource",
 ]
 
-import md5
-import time
-
-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.log import LoggingMixIn
-from twistedcaldav.accounting import accountingEnabled, emitAccounting
-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
 
 class CalendarSchedulingCollectionResource (CalDAVResource):
     """
@@ -200,423 +192,86 @@
         yield x
         x.getResult()
 
-        # Must be content-type text/calendar
-        contentType = request.headers.getHeader("content-type")
-        if contentType is not None and (contentType.mediaType, contentType.mediaSubtype) != ("text", "calendar"):
-            self.log_error("MIME type %s not allowed in calendar collection" % (contentType,))
-            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):
-            self.log_error("POST request must have Originator header")
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-specified")))
-        else:
-            originator = originator[0]
-    
-        # Verify that Originator is a valid calendar user (has an INBOX)
-        originatorPrincipal = self.principalForCalendarUserAddress(originator)
-        if originatorPrincipal is None:
-            self.log_error("Could not find principal for originator: %s" % (originator,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+        # This is a local CALDAV scheduling operation.
+        scheduler = CalDAVScheduler(request, self)
 
-        inboxURL = originatorPrincipal.scheduleInboxURL()
-        if inboxURL is None:
-            self.log_error("Could not find inbox for originator: %s" % (originator,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
-    
-        # Verify that Originator matches the authenticated user
-        if davxml.Principal(davxml.HRef(originatorPrincipal.principalURL())) != self.currentPrincipal(request):
-            self.log_error("Originator: %s does not match authorized user: %s" % (originator, self.currentPrincipal(request).children[0],))
-            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):
-            self.log_error("POST request must have at least one Recipient header")
-            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")
-        recipientsState = {"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:
-            self.log_error("Error while handling POST: %s" % (Failure(),))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
- 
-        # Must be a valid calendar
-        try:
-            calendar.validCalendarForCalDAV()
-        except ValueError:
-            self.log_error("POST request calendar component is not valid: %s" % (calendar,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+    def __init__(self, parent):
+        """
+        @param parent: the parent resource of this one.
+        """
+        assert parent is not None
 
-        # Must have a METHOD
-        if not calendar.isValidMethod():
-            self.log_error("POST request must have valid METHOD property in calendar component: %s" % (calendar,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
-        
-        # Verify iTIP behaviour
-        if not calendar.isValidITIP():
-            self.log_error("POST request must have a calendar component that satisfies iTIP requirements: %s" % (calendar,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+        CalDAVResource.__init__(self, principalCollections=parent.principalCollections())
 
-        # X-CALENDARSERVER-ACCESS is not allowed in Outbox POSTs
-        if calendar.hasProperty(Component.ACCESS_PROPERTY):
-            self.log_error("X-CALENDARSERVER-ACCESS not allowed in a calendar component POST request: %s" % (calendar,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (calendarserver_namespace, "no-access-restrictions")))
-    
-        # Verify that the ORGANIZER's cu address maps to the request.uri
-        organizer = calendar.getOrganizer()
-        if organizer is None:
-            organizerPrincipal = None
-        else:
-            organizerPrincipal = self.principalForCalendarUserAddress(organizer)
+        self.parent = parent
 
-        if organizerPrincipal is None:
-            self.log_error("ORGANIZER in calendar data is not valid: %s" % (calendar,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+    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(),
+            ),
+        )
 
-        # Prevent spoofing of ORGANIZER with specific METHODs
-        if (
-            calendar.propertyValue("METHOD") in ("PUBLISH", "REQUEST", "ADD", "CANCEL", "DECLINECOUNTER") and
-            organizerPrincipal.record != self.parent.record
-        ):
-            self.log_error("ORGANIZER in calendar data does not match owner of Outbox: %s" % (calendar,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+    def resourceType(self):
+        return davxml.ResourceType.servertoserverinbox
 
-        # 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:
-                self.log_error("ATTENDEE list in calendar data is wrong: %s" % (calendar,))
-                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
-            
-            # Attendee's Outbox MUST be the request URI
-            attendeePrincipal = self.principalForCalendarUserAddress(attendees[0])
-            if attendeePrincipal is None or attendeePrincipal.record != self.parent.record:
-                self.log_error("ATTENDEE in calendar data does not match owner of Outbox: %s" % (calendar,))
-                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
+    def isCollection(self):
+        return False
 
-        # For free-busy do immediate determination of iTIP result rather than fan-out
-        self.log_info("METHOD: %s, Component: %s" % (calendar.propertyValue("METHOD"), calendar.mainType(),))
-        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:
-                self.log_error("iTIP data is not valid for a VFREEBUSY request: %s" % (calendar,))
-                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:
-                self.log_error("VFREEBUSY start/end not valid: %s" % (calendar,))
-                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
-            timeRange.start = dtstart
-            timeRange.end = dtend
+    def isCalendarCollection(self):
+        return False
 
-            # Look for maksed UID
-            excludeUID = calendar.getMaskUID()
+    def isPseudoCalendarCollection(self):
+        return False
 
-            # Do free busy operation
-            freebusy = True
-        else:
-            # Do regular invite (fan-out)
-            freebusy = False
+    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>"""
 
-        #
-        # Accounting
-        #
-        # Note that we associate logging with the organizer, not the
-        # originator, which is good for looking for why something
-        # shows up in a given principal's calendars, rather than
-        # tracking the activities of a specific user.
-        #
-        if accountingEnabled("iTIP", organizerPrincipal):
-            emitAccounting(
-                "iTIP", organizerPrincipal,
-                "Originator: %s\nRecipients: %s\n\n%s"
-                % (originator, ", ".join(recipients), str(calendar))
-            )
+        response = Response(200, {}, output)
+        response.headers.setHeader("content-type", MimeType("text", "html"))
+        return response
 
-        # Prepare for multiple responses
-        responses = ScheduleResponseQueue("POST", responsecode.OK)
-    
-        # Loop over each recipient and do appropriate action.
-        autoresponses = []
-        for recipient in recipients:
-            # Get the principal resource for this recipient
-            principal = self.principalForCalendarUserAddress(recipient)
-
-            # Map recipient to their inbox
-            inbox = None
-            if principal is None:
-                self.log_error("No schedulable principal for calendar user address: %r" % (recipient,))
-            else:
-                inboxURL = principal.scheduleInboxURL()
-                if inboxURL:
-                    inbox = waitForDeferred(request.locateResource(inboxURL))
-                    yield inbox
-                    inbox = inbox.getResult()
-                else:
-                    self.log_error("No schedule inbox for principal: %s" % (principal,))
-
-            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")
-                recipientsState["BAD"] += 1
-            
-                # Process next recipient
-                continue
-            else:
-                #
-                # Check access controls
-                #
-                try:
-                    d = waitForDeferred(inbox.checkPrivileges(request, (caldavxml.Schedule(),), principal=davxml.Principal(davxml.HRef(organizerPrincipal.principalURL()))))
-                    yield d
-                    d.getResult()
-                except AccessDeniedError:
-                    self.log_error("Could not access Inbox for recipient: %s" % (recipient,))
-                    err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-permisions")))
-                    responses.add(recipient, Failure(exc_value=err), reqstatus="3.8;No authority")
-                    recipientsState["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()
-
-                    # 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 = organizerPrincipal.principalURL() == principal.principalURL()
-
-                        # Now process free-busy set calendars
-                        matchtotal = 0
-                        for calendarResourceURL in fbset:
-                            calendarResource = waitForDeferred(request.locateResource(calendarResourceURL))
-                            yield calendarResource
-                            calendarResource = calendarResource.getResult()
-                            if calendarResource is None or not calendarResource.exists() or not isCalendarCollectionResource(calendarResource):
-                                # 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,
-                                calendarResource,
-                                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 = calendar.getOrganizerProperty(),
-                            attendee = attendeeProp,
-                            uid = calendar.resourceUID(),
-                            method="REPLY"
-                        )
-
-                        responses.add(recipient, responsecode.OK, reqstatus="2.0;Success", calendar=fbresult)
-                        recipientsState["OK"] += 1
-                
-                    except:
-                        self.log_error("Could not determine free busy information: %s" % (recipient,))
-                        err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-permissions")))
-                        responses.add(recipient, Failure(exc_value=err), reqstatus="3.8;No authority")
-                        recipientsState["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")
-                        recipientsState["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:
-                        self.log_error("Could not store data in Inbox : %s" % (inbox,))
-                        err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-permissions")))
-                        responses.add(recipient, Failure(exc_value=err), reqstatus="3.8;No authority")
-                        recipientsState["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 = ServerToServerScheduler(request, self)
 
-class ScheduleResponseQueue (LoggingMixIn):
-    """
-    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
-            self.log_error("Error during %s for %s: %s" % (self.method, recipient, message))
-
-        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-2407/twistedcaldav/schedule_common.py (from rev 2408, CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/schedule_common.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/schedule_common.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/schedule_common.py	2008-05-13 19:28:33 UTC (rev 2412)
@@ -0,0 +1,1049 @@
+##
+# 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, waitForDeferred
+from twisted.python.failure import Failure
+from twisted.web2 import responsecode
+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 twisted.web2.http import HTTPError, Response
+from twisted.web2.http_headers import MimeType
+from twistedcaldav import caldavxml
+from twistedcaldav.accounting import accountingEnabled, emitAccounting
+from twistedcaldav.log import Logger, LoggingMixIn
+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
+
+log = Logger()
+
+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
+    
+    @deferredGenerator
+    def doSchedulingViaPOST(self):
+        """
+        The Scheduling POST operation.
+        """
+    
+        # Do some extra authorization checks
+        self.checkAuthorization()
+
+        #d = waitForDeferred(log.logRequest("debug", "Received POST request:", self.request))
+        #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()
+    
+        # Generate accounting information
+        self.doAccounting()
+
+        # Do scheduling tasks
+        response = waitForDeferred(self.generateSchedulingResponse())
+        yield response
+        response = response.getResult()
+
+        #d = waitForDeferred(log.logResponse("debug", "Sending POST response:", response))
+        #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):
+            log.err("POST request must have Originator header")
+            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):
+            log.err("POST request must have at least one Recipient header")
+            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
+        contentType = self.request.headers.getHeader("content-type")
+        if contentType is not None and (contentType.mediaType, contentType.mediaSubtype) != ("text", "calendar"):
+            log.err("MIME type %s not allowed in calendar collection" % (contentType,))
+            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:
+            log.err("Error while handling POST: %s" % (Failure(),))
+            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:
+            log.err("POST request calendar component is not valid: %s" % (self.calendar,))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+    
+        # Must have a METHOD
+        if not self.calendar.isValidMethod():
+            log.err("POST request must have valid METHOD property in calendar component: %s" % (self.calendar,))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+        
+        # Verify iTIP behavior
+        if not self.calendar.isValidITIP():
+            log.err("POST request must have a calendar component that satisfies iTIP requirements: %s" % (self.calendar,))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+
+        # X-CALENDARSERVER-ACCESS is not allowed in Outbox POSTs
+        if self.calendar.hasProperty(Component.ACCESS_PROPERTY):
+            log.err("X-CALENDARSERVER-ACCESS not allowed in a calendar component POST request: %s" % (self.calendar,))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (calendarserver_namespace, "no-access-restrictions")))
+    
+    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:
+                log.err("iTIP data is not valid for a VFREEBUSY request: %s" % (self.calendar,))
+                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:
+                log.err("VFREEBUSY start/end not valid: %s" % (self.calendar,))
+                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 masked 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
+
+    def doAccounting(self):
+        #
+        # Accounting
+        #
+        # Note that we associate logging with the organizer, not the
+        # originator, which is good for looking for why something
+        # shows up in a given principal's calendars, rather than
+        # tracking the activities of a specific user.
+        #
+        if isinstance(self.organizer, Scheduler.LocalCalendarUser):
+            if accountingEnabled("iTIP", self.organizer.principal):
+                emitAccounting(
+                    "iTIP", self.organizer.principal,
+                    "Originator: %s\nRecipients:\n%s\n%s"
+                    % (
+                        str(self.originator),
+                        "".join(["    %s\n" % (recipient,) for recipient in self.recipients]),
+                        str(self.calendar)
+                    )
+                )
+
+    @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)
+        elif cuaddr.startswith("/"):
+            # Assume relative HTTP URL - i.e. on this server
+            return True
+        
+        result = False
+        
+        for pattern in config.ServerToServer["Local Addresses"]:
+            try:
+                if re.match(pattern, cuaddr) is not None:
+                    result = True
+            except re.error:
+                log.debug("Invalid regular expression for ServerToServer configuration 'Local Addresses': %s" % (pattern,))
+            
+        for pattern in config.ServerToServer["Remote Addresses"]:
+            try:
+                if re.match(pattern, cuaddr) is not None:
+                    result = False
+            except re.error:
+                log.debug("Invalid regular expression for ServerToServer configuration 'Remote Addresses': %s" % (pattern,))
+        
+        return result
+    
+    @deferredGenerator
+    def generateSchedulingResponse(self):
+
+        log.info("METHOD: %s, Component: %s" % (self.calendar.propertyValue("METHOD"), self.calendar.mainType(),))
+
+        # 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 separate 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:
+                        log.err("Could not access Inbox for recipient: %s" % (recipient.cuaddr,))
+                        err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-permissions")))
+                        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 check 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 behavior 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:
+            log.err("Could not store data in Inbox : %s" % (recipient.inbox,))
+            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:
+            log.err("Could not determine free busy information: %s" % (recipient.cuaddr,))
+            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 calendarResourceURL in fbset:
+            calendarResource = waitForDeferred(self.request.locateResource(calendarResourceURL))
+            yield calendarResource
+            calendarResource = calendarResource.getResult()
+            if calendarResource is None or not calendarResource.exists() or not isCalendarCollectionResource(calendarResource):
+                # 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,
+                calendarResource,
+                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 checkAuthorization(self):
+        # Must have an authenticated user
+        if self.resource.currentPrincipal(self.request) == davxml.Principal(davxml.Unauthenticated()):
+            log.err("Unauthenticated originators not allowed: %s" % (self.originator,))
+            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
+        originatorPrincipal = self.resource.principalForCalendarUserAddress(self.originator)
+        if originatorPrincipal is None:
+            # Local requests MUST have a principal.
+            log.err("Could not find principal for originator: %s" % (self.originator,))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+        else:
+            # Must have a valid Inbox.
+            inboxURL = originatorPrincipal.scheduleInboxURL()
+            if inboxURL is None:
+                log.err("Could not find inbox for originator: %s" % (self.originator,))
+                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(originatorPrincipal.principalURL())) != authn_principal:
+                log.err("Originator: %s does not match authorized user: %s" % (self.originator, authn_principal.children[0],))
+                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+
+            self.originator = Scheduler.LocalCalendarUser(self.originator, originatorPrincipal)
+
+    @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):
+                    log.err("No schedulable principal for calendar user address: %s" % (recipient,))
+                    results.append(Scheduler.InvalidCalendarUser(recipient))
+                elif not config.ServerToServer["Enabled"]:
+                    log.err("Unknown calendar user address: %s" % (recipient,))
+                    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:
+                    log.err("No schedule inbox for principal: %s" % (principal,))
+                    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:
+            organizerPrincipal = self.resource.principalForCalendarUserAddress(organizer)
+            if organizerPrincipal:
+                outboxURL = organizerPrincipal.scheduleOutboxURL()
+                if outboxURL:
+                    self.organizer = Scheduler.LocalCalendarUser(organizer, organizerPrincipal)
+                else:
+                    log.err("No outbox for ORGANIZER in calendar data: %s" % (self.calendar,))
+                    raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+            elif self.isCalendarUserAddressInMyDomain(organizer):
+                log.err("No principal for ORGANIZER in calendar data: %s" % (self.calendar,))
+                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+            else:
+                self.organizer = Scheduler.RemoteCalendarUser(organizer) 
+        else:
+            log.err("ORGANIZER missing in calendar data: %s" % (self.calendar,))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+
+    def checkOrganizerAsOriginator(self):
+
+        # Make sure that the ORGANIZER is local
+        if not isinstance(self.organizer, Scheduler.LocalCalendarUser):
+            log.err("ORGANIZER is not local to server in calendar data: %s" % (self.calendar,))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+
+        # Make sure that the ORGANIZER's Outbox is the request URI
+        if self.organizer.principal.scheduleOutboxURL() != self.request.uri:
+            log.err("Wrong outbox for ORGANIZER in calendar data: %s" % (self.calendar,))
+            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:
+            log.err("Wrong number of ATTENDEEs in calendar data: %s" % (self.calendar,))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
+        attendee = attendees[0]
+    
+        # Attendee's Outbox MUST be the request URI
+        attendeePrincipal = self.resource.principalForCalendarUserAddress(attendee)
+        if attendeePrincipal:
+            aoutboxURL = attendeePrincipal.scheduleOutboxURL()
+            if aoutboxURL is None or aoutboxURL != self.request.uri:
+                log.err("ATTENDEE in calendar data does not match owner of Outbox: %s" % (self.calendar,))
+                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
+        else:
+            log.err("Unknown ATTENDEE in calendar data: %s" % (self.calendar,))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
+    
+    def securityChecks(self):
+        """
+        Check that the originator 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:
+            log.err("Unknown iTIP METHOD for security checks: %s" % (self.calendar.propertyValue("METHOD"),))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+
+class ServerToServerScheduler(Scheduler):
+
+    def checkAuthorization(self):
+        # Must have an unauthenticated user
+        if self.resource.currentPrincipal(self.request) != davxml.Principal(davxml.Unauthenticated()):
+            log.err("Authenticated originators not allowed: %s" % (self.originator,))
+            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.
+        originatorPrincipal = self.resource.principalForCalendarUserAddress(self.originator)
+        if originatorPrincipal or self.isCalendarUserAddressInMyDomain(self.originator):
+            log.err("Cannot use originator that is on this server: %s" % (self.originator,))
+            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:
+            log.err("Originator not on recognized server: %s" % (self.originator,))
+            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
+            matched = False
+            compare_with = (server.host,) + tuple(server.client_hosts)
+            if clientip in compare_with:
+                matched = True
+            else:
+                # Now do hostname lookup
+                host, aliases, _ignore_ips = socket.gethostbyaddr(clientip)
+                for host in itertools.chain((host,), aliases):
+                    # Try simple match first
+                    if host in compare_with:
+                        matched = True
+                        break
+                    
+                    # Try pattern match next
+                    for pattern in compare_with:
+                        try:
+                            if re.match(pattern, host) is not None:
+                                matched = True
+                                break
+                        except re.error:
+                            log.debug("Invalid regular expression for ServerToServer white list for server domain %s: %s" % (self.originator.domain, pattern,))
+                    else:
+                        continue
+                    break
+                        
+            if not matched:
+                log.err("Originator not on allowed server: %s" % (self.originator,))
+                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):
+                    log.err("No principal for calendar user address: %s" % (recipient,))
+                    results.append(Scheduler.InvalidCalendarUser(recipient))
+                else:
+                    log.err("Unknown calendar user address: %s" % (recipient,))
+                    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:
+                    log.err("No schedule inbox for principal: %s" % (principal,))
+                    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:
+            organizerPrincipal = self.resource.principalForCalendarUserAddress(organizer)
+            if organizerPrincipal:
+                log.err("Invalid ORGANIZER in calendar data: %s" % (self.calendar,))
+                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+            elif self.isCalendarUserAddressInMyDomain(organizer):
+                log.err("Unsupported ORGANIZER in calendar data: %s" % (self.calendar,))
+                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+            else:
+                self.organizer = Scheduler.RemoteCalendarUser(organizer)
+        else:
+            log.err("ORGANIZER missing in calendar data: %s" % (self.calendar,))
+            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:
+            log.err("Wrong number of ATTENDEEs in calendar data: %s" % (self.calendar,))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
+        attendee = attendees[0]
+    
+        # Attendee cannot be local.
+        attendeePrincipal = self.resource.principalForCalendarUserAddress(attendee)
+        if attendeePrincipal:
+            log.err("Invalid ATTENDEE in calendar data: %s" % (self.calendar,))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
+        elif self.isCalendarUserAddressInMyDomain(attendee):
+            log.err("Unknown ATTENDEE in calendar data: %s" % (self.calendar,))
+            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 originator 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:
+            log.err("Unknown iTIP METHOD for security checks: %s" % (self.calendar.propertyValue("METHOD"),))
+            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 iterable 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 (LoggingMixIn):
+    """
+    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
+            self.log_error("Error during %s for %s: %s" % (self.method, recipient, message))
+
+        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-2407/twistedcaldav/servertoserver.py (from rev 2408, CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/servertoserver.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/servertoserver.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/servertoserver.py	2008-05-13 19:28:33 UTC (rev 2412)
@@ -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.log import Logger
+
+log = Logger()
+
+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)
+            d = waitForDeferred(log.logRequest("debug", "Sending server-to-server POST request:", request))
+            yield d
+            d.getResult()
+            d = waitForDeferred(proto.submitRequest(request))
+            yield d
+            response = d.getResult()
+    
+            d = waitForDeferred(log.logResponse("debug", "Received server-to-server POST response:", response))
+            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
+            log.err("Could not do server-to-server request : %s %s" % (self, e))
+            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-2407/twistedcaldav/servertoserverparser.py (from rev 2408, CalendarServer/branches/users/cdaboo/server2server-2113/twistedcaldav/servertoserverparser.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/servertoserverparser.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/servertoserverparser.py	2008-05-13 19:28:33 UTC (rev 2412)
@@ -0,0 +1,165 @@
+##
+# 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
+
+from twistedcaldav.log import Logger
+
+log = Logger()
+
+ELEMENT_SERVERS                 = "servers"
+ELEMENT_SERVER                  = "server"
+ELEMENT_URI                     = "uri"
+ELEMENT_AUTHENTICATION          = "authentication"
+ATTRIBUTE_TYPE                  = "type"
+ATTRIBUTE_BASICAUTH             = "basic"
+ELEMENT_USER                    = "user"
+ELEMENT_PASSWORD                = "password"
+ELEMENT_ALLOW_REQUESTS_FROM     = "allow-requests-from"
+ELEMENT_ALLOW_REQUESTS_TO       = "allow-requests-to"
+ELEMENT_DOMAINS                 = "domains"
+ELEMENT_DOMAIN                  = "domain"
+ELEMENT_CLIENT_HOSTS            = "hosts"
+ELEMENT_HOST                    = "host"
+
+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:
+            log.error("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.authentication = None
+        self.allow_from = False
+        self.allow_to = True
+        self.domains = []
+        self.client_hosts = []
+
+    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_AUTHENTICATION:
+                self._parseAuthentication(child)
+            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._parseList(child, ELEMENT_DOMAIN, self.domains)
+            elif child_name == ELEMENT_CLIENT_HOSTS:
+                self._parseList(child, ELEMENT_HOST, self.client_hosts)
+            else:
+                raise RuntimeError("[%s] Unknown attribute: %s" % (self.__class__, child_name,))
+        
+        self._parseDetails()
+
+    def _parseList(self, node, element_name, appendto):
+        for child in node._get_childNodes():
+            if child._get_localName() == element_name:
+                if child.firstChild is not None:
+                    appendto.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-2407/twistedcaldav/static.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/static.py	2008-05-12 22:48:07 UTC (rev 2411)
+++ CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/static.py	2008-05-13 19:28:33 UTC (rev 2412)
@@ -53,11 +53,12 @@
 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.resource import CalDAVResource, isCalendarCollectionResource, isPseudoCalendarCollectionResource
-from twistedcaldav.schedule import ScheduleInboxResource, ScheduleOutboxResource
+from twistedcaldav.schedule import ScheduleInboxResource, ScheduleOutboxResource, ScheduleServerToServerResource
 from twistedcaldav.dropbox import DropBoxHomeResource, DropBoxCollectionResource
 from twistedcaldav.directory.calendar import uidsResourceName
 from twistedcaldav.directory.calendar import DirectoryCalendarHomeProvisioningResource
@@ -534,11 +535,16 @@
             DropBoxHomeFileClass = DropBoxHomeFile
         else:
             DropBoxHomeFileClass = None
+        if config.FreeBusyURL["Enabled"]:
+            FreeBusyURLFileClass = FreeBusyURLFile
+        else:
+            FreeBusyURLFileClass = None
             
         cls = {
             "inbox"        : ScheduleInboxFile,
             "outbox"       : ScheduleOutboxFile,
             "dropbox"      : DropBoxHomeFileClass,
+            "freebusy"     : FreeBusyURLFileClass,
         }.get(name, None)
 
         if cls is not None:
@@ -636,6 +642,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-2407/twistedcaldav/tap.py
===================================================================
--- CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/tap.py	2008-05-12 22:48:07 UTC (rev 2411)
+++ CalendarServer/branches/users/cdaboo/server2server-2407/twistedcaldav/tap.py	2008-05-13 19:28:33 UTC (rev 2412)
@@ -51,6 +51,7 @@
 from twistedcaldav.directory.aggregate import AggregateDirectoryService
 from twistedcaldav.directory.sudo import SudoDirectoryService
 from twistedcaldav.static import CalendarHomeProvisioningFile
+from twistedcaldav.static import ServerToServerInboxFile
 from twistedcaldav.timezones import TimezoneCache
 from twistedcaldav import pdmonster
 
@@ -448,9 +449,10 @@
     # default resource classes
     #
 
-    rootResourceClass      = RootResource
-    principalResourceClass = DirectoryPrincipalProvisioningResource
-    calendarResourceClass  = CalendarHomeProvisioningFile
+    rootResourceClass           = RootResource
+    principalResourceClass      = DirectoryPrincipalProvisioningResource
+    calendarResourceClass       = CalendarHomeProvisioningFile
+    servertoserverResourceClass = ServerToServerInboxFile
 
     def makeService_Slave(self, options):
         #
@@ -524,6 +526,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
 
         log.info("Setting up default ACEs on root resource")

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


More information about the calendarserver-changes mailing list