[CalendarServer-changes] [2679] CalendarServer/branches/users/cdaboo/implicit-2660

source_changes at macosforge.org source_changes at macosforge.org
Fri Jul 11 07:44:50 PDT 2008


Revision: 2679
          http://trac.macosforge.org/projects/calendarserver/changeset/2679
Author:   cdaboo at apple.com
Date:     2008-07-11 07:44:49 -0700 (Fri, 11 Jul 2008)
Log Message:
-----------
Major refactoring of scheduling code to ultimately make it easier to implement implicit scheduling.
There is a now a separate set of classes to handle sending of iTIP messages, once the destination
service for each attendee has been determined. It is now also, possible to do iMIP and iSchedule
at the same time. config object was redesigned to put all scheduling related options into one
dict value. Needs more tests...

Modified Paths:
--------------
    CalendarServer/branches/users/cdaboo/implicit-2660/conf/caldavd-test.plist
    CalendarServer/branches/users/cdaboo/implicit-2660/conf/caldavd.plist
    CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/config.py
    CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/customxml.py
    CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/freebusyurl.py
    CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/schedule.py
    CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/static.py
    CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/tap.py
    CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/test/test_imip.py

Added Paths:
-----------
    CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/
    CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/__init__.py
    CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/addressmapping.py
    CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/caldav.py
    CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/cuaddress.py
    CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/delivery.py
    CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/imip.py
    CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/ischedule.py
    CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/ischeduleservers.py
    CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/scheduler.py

Removed Paths:
-------------
    CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/schedule_common.py
    CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/schedule_imip.py
    CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/servertoserver.py
    CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/servertoserverparser.py

Modified: CalendarServer/branches/users/cdaboo/implicit-2660/conf/caldavd-test.plist
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2660/conf/caldavd-test.plist	2008-07-11 14:26:44 UTC (rev 2678)
+++ CalendarServer/branches/users/cdaboo/implicit-2660/conf/caldavd-test.plist	2008-07-11 14:44:49 UTC (rev 2679)
@@ -376,22 +376,55 @@
   <true/>
 
   <!-- Server to server protocol -->
-  <key>ServerToServer</key>
+  <key>Scheduling</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>
+    <!--  CalDAV protocol options -->
+  	<key>CalDAV</key>
+  	<dict>
+  	  <key>EmailDomain</key>
+  	  <string></string>
+  	  <key>HTTPDomain</key>
+  	  <string></string>
+  	  <key>AddressPatterns</key>
+  	  <array>
+  	  </array>
+  	</dict>
+    <!--  iSchedule protocol options -->
+  	<key>iSchedule</key>
+  	<dict>
+  	  <key>Enabled</key>
+  	  <false/>
+  	  <key>Remote Addresses</key>
+  	  <array>
+  	  </array>
+      <key>Servers</key>
+      <string>/etc/caldavd/servertoserver.xml</string>
+  	</dict>
+    <!--  iMIP protocol options -->
+  	<key>iMIP</key>
+  	<dict>
+  	  <key>Enabled</key>
+  	  <false/>
+  	  <key>Sending</key>
+  	  <dict>
+  	    <key>Server</key>
+  	    <string></string>
+  	    <key>Port</key>
+  	    <integer>587</integer>
+      </dict>
+      <key>Receiving</key>
+      <dict>
+  	    <key>Server</key>
+  	    <string></string>
+  	    <key>Port</key>
+  	    <integer>0</integer>
+  	    <key>Type</key>
+  	    <string></string> <!-- Either 'pop3' or 'imap' -->
+  	  </dict>
+  	  <key>RemoteAddresses</key>
+  	  <array>
+  	  </array>
+  	</dict>
   </dict>
 
   <!-- Free-busy URL protocol -->

Modified: CalendarServer/branches/users/cdaboo/implicit-2660/conf/caldavd.plist
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2660/conf/caldavd.plist	2008-07-11 14:26:44 UTC (rev 2678)
+++ CalendarServer/branches/users/cdaboo/implicit-2660/conf/caldavd.plist	2008-07-11 14:44:49 UTC (rev 2679)
@@ -302,22 +302,55 @@
   <true/>
 
   <!-- Server to server protocol -->
-  <key>ServerToServer</key>
+  <key>Scheduling</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>
+    <!--  CalDAV protocol options -->
+  	<key>CalDAV</key>
+  	<dict>
+  	  <key>EmailDomain</key>
+  	  <string></string>
+  	  <key>HTTPDomain</key>
+  	  <string></string>
+  	  <key>AddressPatterns</key>
+  	  <array>
+  	  </array>
+  	</dict>
+    <!--  iSchedule protocol options -->
+  	<key>iSchedule</key>
+  	<dict>
+  	  <key>Enabled</key>
+  	  <false/>
+  	  <key>AddressPatterns</key>
+  	  <array>
+  	  </array>
+      <key>Servers</key>
+      <string>/etc/caldavd/servertoserver.xml</string>
+  	</dict>
+    <!--  iMIP protocol options -->
+  	<key>iMIP</key>
+  	<dict>
+  	  <key>Enabled</key>
+  	  <false/>
+  	  <key>Sending</key>
+  	  <dict>
+  	    <key>Server</key>
+  	    <string></string>
+  	    <key>Port</key>
+  	    <integer>587</integer>
+      </dict>
+      <key>Receiving</key>
+      <dict>
+  	    <key>Server</key>
+  	    <string></string>
+  	    <key>Port</key>
+  	    <integer>0</integer>
+  	    <key>Type</key>
+  	    <string></string> <!-- Either 'pop3' or 'imap' -->
+  	  </dict>
+  	  <key>AddressPatterns</key>
+  	  <array>
+  	  </array>
+  	</dict>
   </dict>
 
   <!-- Free-busy URL protocol -->

Modified: CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/config.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/config.py	2008-07-11 14:26:44 UTC (rev 2678)
+++ CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/config.py	2008-07-11 14:44:49 UTC (rev 2679)
@@ -174,18 +174,44 @@
     "EnablePrivateEvents"   : False, # Private Events
     "EnableTimezoneService" : False, # Timezone service
     
-    "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
+    #
+    # Scheduling related options
+    #
+
+    "Scheduling": {
+        
+        "CalDAV": {
+            "EmailDomain"      : "",    # Domain for mailto calendar user addresses on this server
+            "HTTPDomain"       : "",    # Domain for http calendar user addresses on this server
+            "AddressPatterns"  : [],    # Reg-ex patterns to match local calendar user addresses
+        },
+
+        "iSchedule": {
+            "Enabled"          : False, # iSchedule protocol
+            "AddressPatterns"  : [],    # Reg-ex patterns to match iSchedule-able calendar user addresses
+            "Servers"          : "/etc/caldavd/servertoserver.xml",    # iSchedule server configurations
+        },
+
+        "iMIP": {
+            "Enabled"          : False, # Server-to-iMIP protocol
+            "Sending": {
+                "Server"       : "",    # SMTP server to relay messages through
+                "Port"         : 587,   # SMTP server port to relay messages through
+            },
+            "Receiving": {
+                "Server"       : "",    # Server to retrieve email messages from
+                "Port"         : 0,     # Server port to retrieve email messages from
+                "Type"         : "",    # Type of message access server: 'pop3' or 'imap'
+            },
+            "AddressPatterns"  : [],    # Reg-ex patterns to match iMIP-able 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
+        "TimePeriod"       : 14,    # Number of days into the future to generate f-b data if no explicit time-range is specified
+        "AnonymousAccess"  : False, # Allow anonymous read access to free-busy URL
     },
 
     #

Modified: CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/customxml.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/customxml.py	2008-07-11 14:26:44 UTC (rev 2678)
+++ CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/customxml.py	2008-07-11 14:44:49 UTC (rev 2679)
@@ -244,13 +244,13 @@
     namespace = calendarserver_namespace
     name = "utc-offset"
 
-class ServerToServerInbox (davxml.WebDAVEmptyElement):
+class IScheduleInbox (davxml.WebDAVEmptyElement):
     """
-    Denotes the resourcetype of a server-to-server Inbox.
+    Denotes the resourcetype of a iSchedule Inbox.
     (CalDAV-s2s-xx, section x.x.x)
     """
     namespace = calendarserver_namespace
-    name = "server-to-server-inbox"
+    name = "ischedule-inbox"
 
 class FreeBusyURL (davxml.WebDAVEmptyElement):
     """
@@ -269,5 +269,5 @@
 davxml.ResourceType.calendarproxyread = davxml.ResourceType(davxml.Principal(), davxml.Collection(), CalendarProxyRead())
 davxml.ResourceType.calendarproxywrite = davxml.ResourceType(davxml.Principal(), davxml.Collection(), CalendarProxyWrite())
 davxml.ResourceType.timezones = davxml.ResourceType(Timezones())
-davxml.ResourceType.servertoserverinbox = davxml.ResourceType(ServerToServerInbox())
+davxml.ResourceType.ischeduleinbox = davxml.ResourceType(IScheduleInbox())
 davxml.ResourceType.freebusyurl = davxml.ResourceType(FreeBusyURL())

Modified: CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/freebusyurl.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/freebusyurl.py	2008-07-11 14:26:44 UTC (rev 2678)
+++ CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/freebusyurl.py	2008-07-11 14:44:49 UTC (rev 2679)
@@ -41,7 +41,9 @@
 from twistedcaldav.ical import parse_datetime
 from twistedcaldav.ical import parse_duration
 from twistedcaldav.resource import CalDAVResource
-from twistedcaldav.schedule_common import Scheduler
+from twistedcaldav.scheduling.caldav import ScheduleViaCalDAV
+from twistedcaldav.scheduling.cuaddress import LocalCalendarUser
+from twistedcaldav.scheduling.scheduler import Scheduler
 
 from vobject.icalendar import utc
 
@@ -76,7 +78,7 @@
                 davxml.Protected(),
             ),
         )
-        if config.FreeBusyURL["Anonymous Access"]:
+        if config.FreeBusyURL["AnonymousAccess"]:
             aces += (
                 # DAV:Read, for unauthenticated principals
                 davxml.ACE(
@@ -184,7 +186,7 @@
         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"])
+            self.end = self.start + datetime.timedelta(days=config.FreeBusyURL["TimePeriod"])
             
         # End > start
         if self.end <= self.start:
@@ -221,11 +223,12 @@
         scheduler.timerange.start = self.start
         scheduler.timerange.end = self.end
         
-        scheduler.organizer = Scheduler.LocalCalendarUser(cuaddr, principal, inbox, inboxURL)
+        scheduler.organizer = LocalCalendarUser(cuaddr, principal, inbox, inboxURL)
         
         attendeeProp = Property("ATTENDEE", scheduler.organizer.cuaddr)
 
-        fbresult = (yield scheduler.generateAttendeeFreeBusyResponse(
+        requestor = ScheduleViaCalDAV(scheduler, (), [], True)
+        fbresult = (yield requestor.generateAttendeeFreeBusyResponse(
             scheduler.organizer,
             None,
             None,

Modified: CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/schedule.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/schedule.py	2008-07-11 14:26:44 UTC (rev 2678)
+++ CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/schedule.py	2008-07-11 14:44:49 UTC (rev 2679)
@@ -21,7 +21,7 @@
 __all__ = [
     "ScheduleInboxResource",
     "ScheduleOutboxResource",
-    "ScheduleServerToServerResource",
+    "IScheduleInboxResource",
 ]
 
 from twisted.internet.defer import succeed, inlineCallbacks, returnValue
@@ -39,8 +39,7 @@
 from twistedcaldav.customxml import calendarserver_namespace
 from twistedcaldav.resource import CalDAVResource
 from twistedcaldav.resource import isCalendarCollectionResource
-from twistedcaldav.schedule_common import CalDAVScheduler
-from twistedcaldav.schedule_common import ServerToServerScheduler
+from twistedcaldav.scheduling.scheduler import CalDAVScheduler, IScheduleScheduler
 
 class CalendarSchedulingCollectionResource (CalDAVResource):
     """
@@ -138,7 +137,7 @@
                 old_calendars = set([str(href) for href in self.readDeadProperty(property).children])
             added_calendars = new_calendars.difference(old_calendars)
             for href in added_calendars:
-                cal = yield request.locateResource(str(href))
+                cal = (yield request.locateResource(str(href)))
                 if cal is None or not cal.exists() or not isCalendarCollectionResource(cal):
                     # Validate that href's point to a valid calendar.
                     raise HTTPError(ErrorResponse(
@@ -190,14 +189,14 @@
         scheduler = CalDAVScheduler(request, self)
 
         # Do the POST processing treating
-        response = yield scheduler.doSchedulingViaPOST()
+        response = (yield scheduler.doSchedulingViaPOST())
         returnValue(response)
 
-class ScheduleServerToServerResource (CalDAVResource):
+class IScheduleInboxResource (CalDAVResource):
     """
-    Server-to-server schedule Inbox resource.
+    iSchedule Inbox resource.
 
-    Extends L{DAVResource} to provide Server-to-server functionality.
+    Extends L{DAVResource} to provide iSchedule inbox functionality.
     """
 
     def __init__(self, parent):
@@ -224,7 +223,7 @@
         )
 
     def resourceType(self):
-        return davxml.ResourceType.servertoserverinbox
+        return davxml.ResourceType.ischeduleinbox
 
     def isCollection(self):
         return False
@@ -259,8 +258,8 @@
         yield self.authorize(request, (caldavxml.Schedule(),))
 
         # This is a server-to-server scheduling operation.
-        scheduler = ServerToServerScheduler(request, self)
+        scheduler = IScheduleScheduler(request, self)
 
         # Do the POST processing treating this as a non-local schedule
-        response = yield scheduler.doSchedulingViaPOST()
+        response = (yield scheduler.doSchedulingViaPOST())
         returnValue(response)

Deleted: CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/schedule_common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/schedule_common.py	2008-07-11 14:26:44 UTC (rev 2678)
+++ CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/schedule_common.py	2008-07-11 14:44:49 UTC (rev 2679)
@@ -1,1019 +0,0 @@
-##
-# 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 inlineCallbacks, returnValue
-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.schedule_imip import ServerToIMip
-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
-    
-    @inlineCallbacks
-    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()
-        yield self.loadCalendar()
-
-        # Check validity of Originator header.
-        self.checkOriginator()
-    
-        # Get recipient details.
-        yield self.checkRecipients()
-    
-        # 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 = yield self.generateSchedulingResponse()
-
-        #yield log.logResponse("debug", "Sending POST response:", response)
-
-        returnValue(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)
-        
-    @inlineCallbacks
-    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:
-            self.calendar = yield Component.fromIStream(self.request.stream)
-        except:
-            # FIXME: Bare 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
-    
-    @inlineCallbacks
-    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:
-                        yield recipient.inbox.checkPrivileges(self.request, (caldavxml.Schedule(),), principal=davxml.Principal(davxml.HRef(self.organizer.principal.principalURL())))
-                    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:
-                    yield self.generateLocalFreeBusyResponse(recipient, responses, organizerProp, uid)
-                else:
-                    yield self.generateLocalResponse(recipient, responses, autoresponses)
-    
-        # Now process remote recipients
-        if remote_recipients:
-            #yield self.generateRemoteSchedulingResponses(remote_recipients, responses, freebusy)
-            yield self.generateIMIPSchedulingResponses(remote_recipients, responses, freebusy)
-
-        # 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
-        returnValue(responses.response())
-    
-    @inlineCallbacks
-    def generateRemoteSchedulingResponses(self, recipients, responses, freebusy):
-        """
-        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:
-            returnValue(None)
-
-        # 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())
-
-        yield DeferredList(deferreds)
-
-    @inlineCallbacks
-    def generateIMIPSchedulingResponses(self, recipients, responses, freebusy):
-        """
-        Generate scheduling responses for iMIP recipients.
-        """
-        
-        # 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.
-        
-        requestor = ServerToIMip(self, recipients, responses)
-        yield requestor.doEMail(freebusy)
-
-    @inlineCallbacks
-    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 = yield self.request.locateResource(childURL)
-
-        # Copy calendar to inbox (doing fan-out)
-        try:
-            yield StoreCalendarObjectResource(
-                         request=self.request,
-                         destination = child,
-                         destination_uri = childURL,
-                         destinationparent = recipient.inbox,
-                         destinationcal = True,
-                         calendar = self.calendar,
-                         isiTIP = True
-                     ).run()
-        except:
-            # FIXME: Bare 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")
-            returnValue(False)
-        else:
-            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))
-                
-            returnValue(True)
-    
-    @inlineCallbacks
-    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:
-            fbresult = yield self.generateAttendeeFreeBusyResponse(
-                recipient,
-                organizerProp,
-                uid,
-                attendeeProp,
-                remote,
-            )
-        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")
-            returnValue(False)
-        else:
-            responses.add(recipient.cuaddr, responsecode.OK, reqstatus="2.0;Success", calendar=fbresult)
-            returnValue(True)
-    
-    @inlineCallbacks
-    def generateAttendeeFreeBusyResponse(self, recipient, organizerProp, uid, attendeeProp, remote):
-
-        # Find the current recipients calendar-free-busy-set
-        fbset = yield recipient.principal.calendarFreeBusyURIs(self.request)
-
-        # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
-        fbinfo = ([], [], [])
-    
-        # Process the availability property from the Inbox.
-        has_prop = yield recipient.inbox.hasProperty((calendarserver_namespace, "calendar-availability"), self.request)
-        if has_prop:
-            availability = yield recipient.inbox.readProperty((calendarserver_namespace, "calendar-availability"), self.request)
-            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 = yield self.request.locateResource(calendarResourceURL)
-            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 = yield 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)
-    
-        # Build VFREEBUSY iTIP reply for this recipient
-        fbresult = report_common.buildFreeBusyResult(
-            fbinfo,
-            self.timeRange,
-            organizer = organizerProp,
-            attendee = attendeeProp,
-            uid = uid,
-            method = "REPLY"
-        )
-
-        returnValue(fbresult)
-        
-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)
-
-    @inlineCallbacks
-    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 = yield self.request.locateResource(inboxURL)
-
-                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")))
-
-    @inlineCallbacks
-    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 = yield self.request.locateResource(inboxURL)
-
-                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

Deleted: CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/schedule_imip.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/schedule_imip.py	2008-07-11 14:26:44 UTC (rev 2678)
+++ CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/schedule_imip.py	2008-07-11 14:44:49 UTC (rev 2679)
@@ -1,254 +0,0 @@
-##
-# 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.
-##
-from twisted.mail.smtp import messageid
-from twisted.mail.smtp import rfc822date
-from twisted.mail.smtp import sendmail
-import datetime
-import base64
-import MimeWriter
-import cStringIO
-
-"""
-Server to iMIP scheduling functions.
-"""
-
-__all__ = [
-    "ServerToIMip",
-]
-
-from twisted.internet.defer import inlineCallbacks
-from twisted.python.failure import Failure
-from twisted.web2 import responsecode
-from twisted.web2.dav.http import ErrorResponse
-from twisted.web2.http import HTTPError
-
-from twistedcaldav.caldavxml import caldav_namespace
-from twistedcaldav.log import Logger
-
-log = Logger()
-
-class ServerToIMip(object):
-    
-    def __init__(self, scheduler, recipients, responses):
-
-        self.scheduler = scheduler
-        self.recipients = recipients
-        self.responses = responses
-        
-    @inlineCallbacks
-    def doEMail(self, freebusy):
-        
-        # Generate an HTTP client request
-        try:
-            # We do not do freebusy requests via iMIP
-            if freebusy:
-                raise ValueError("iMIP VFREEBUSY REQUESTs not supported.")
-
-            message = self._generateTemplateMessage(self.scheduler.calendar)
-            fromAddr = self.scheduler.originator.cuaddr
-            if not fromAddr.startswith("mailto:"):
-                raise ValueError("ORGANIZER address '%s' must be mailto: for iMIP operation." % (fromAddr,))
-            fromAddr = fromAddr[7:]
-            message = message.replace("${fromaddress}", fromAddr)
-            
-            for recipient in self.recipients:
-                try:
-                    toAddr = recipient.cuaddr
-                    if not toAddr.startswith("mailto:"):
-                        raise ValueError("ATTENDEE address '%s' must be mailto: for iMIP operation." % (toAddr,))
-                    toAddr = toAddr[7:]
-                    sendit = message.replace("${toaddress}", toAddr)
-                    yield sendmail("relay.apple.com", fromAddr, toAddr, sendit)
-        
-                except Exception, e:
-                    # Generated failed response for this recipient
-                    log.err("Could not do server-to-imip request : %s %s" % (self, e))
-                    err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-failed")))
-                    self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus="5.1;Service unavailable")
-                
-                else:
-                    self.responses.add(recipient.cuaddr, responsecode.OK, reqstatus="2.0;Success")
-
-        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 _generateTemplateMessage(self, calendar):
-
-        caldata = str(calendar)
-        data = cStringIO.StringIO()
-        writer = MimeWriter.MimeWriter(data)
-    
-        writer.addheader("From", "${fromaddress}")
-        writer.addheader("To", "${toaddress}")
-        writer.addheader("Date", rfc822date())
-        writer.addheader("Subject", "DO NOT REPLY: calendar invitation test")
-        writer.addheader("Message-ID", messageid())
-        writer.addheader("Mime-Version", "1.0")
-        writer.flushheaders()
-    
-        writer.startmultipartbody("mixed")
-    
-        # message body
-        part = writer.nextpart()
-        body = part.startbody("text/plain")
-        body.write("""Hi,
-You've been invited to a cool event by CalendarServer's new iMIP processor.
-
-%s
-""" % (self._generateCalendarSummary(calendar),))
-    
-        part = writer.nextpart()
-        encoding = "7bit"
-        for i in caldata:
-            if ord(i) > 127:
-                encoding = "base64"
-                caldata = base64.encodestring(caldata)
-                break
-        part.addheader("Content-Transfer-Encoding", encoding)
-        body = part.startbody("text/calendar; charset=utf-8")
-        body.write(caldata.replace("\r", ""))
-    
-        # finish
-        writer.lastpart()
-
-        return data.getvalue()
-
-    def _generateCalendarSummary(self, calendar):
-
-        # Get the most appropriate component
-        component = calendar.masterComponent()
-        if component is None:
-            component = calendar.mainComponent(True)
-            
-        organizer = component.getOrganizerProperty()
-        if "CN" in organizer.params():
-            organizer = "%s <%s>" % (organizer.params()["CN"][0], organizer.value(),)
-        else:
-            organizer = organizer.value()
-            
-        dtinfo = self._getDateTimeInfo(component)
-        
-        summary = component.propertyValue("SUMMARY")
-        if summary is None:
-            summary = ""
-
-        description = component.propertyValue("DESCRIPTION")
-        if description is None:
-            description = ""
-
-        return """---- Begin Calendar Event Summary ----
-
-Organizer:   %s
-Summary:     %s
-%sDescription: %s
-
-----  End Calendar Event Summary  ----
-""" % (organizer, summary, dtinfo, description,)
-
-    def _getDateTimeInfo(self, component):
-        
-        dtstart = component.propertyNativeValue("DTSTART")
-        tzid_start = component.getProperty("DTSTART").params().get("TZID", "UTC")
-
-        dtend = component.propertyNativeValue("DTEND")
-        if dtend:
-            tzid_end = component.getProperty("DTEND").params().get("TZID", "UTC")
-            duration = dtend - dtstart
-        else:
-            duration = component.propertyNativeValue("DURATION")
-            if duration:
-                dtend = dtstart + duration
-                tzid_end = tzid_start
-            else:
-                if isinstance(dtstart, datetime.date):
-                    dtend = None
-                    duration = datetime.timedelta(days=1)
-                else:
-                    dtend = dtstart + datetime.timedelta(days=1)
-                    dtend.hour = dtend.minute = dtend.second = 0
-                    duration = dtend - dtstart
-        result = "Starts:      %s\n" % (self._getDateTimeText(dtstart, tzid_start),)
-        if dtend is not None:
-            result += "Ends:        %s\n" % (self._getDateTimeText(dtend, tzid_end),)
-        result += "Duration:    %s\n" % (self._getDurationText(duration),)
-        
-        if not isinstance(dtstart, datetime.datetime):
-            result += "All Day\n"
-        
-        for property_name in ("RRULE", "RDATE", "EXRULE", "EXDATE", "RECURRENCE-ID",):
-            if component.hasProperty(property_name):
-                result += "Recurring\n"
-                break
-            
-        return result
-
-    def _getDateTimeText(self, dtvalue, tzid):
-        
-        if isinstance(dtvalue, datetime.datetime):
-            timeformat = "%A, %B %e, %Y %I:%M %p"
-        elif isinstance(dtvalue, datetime.date):
-            timeformat = "%A, %B %e, %Y"
-            tzid = ""
-        if tzid:
-            tzid = " (%s)" % (tzid,)
-
-        return "%s%s" % (dtvalue.strftime(timeformat), tzid,)
-        
-    def _getDurationText(self, duration):
-        
-        result = ""
-        if duration.days > 0:
-            result += "%d %s" % (
-                duration.days,
-                self._pluralize(duration.days, "day", "days")
-            )
-
-        hours = duration.seconds / 3600
-        minutes = divmod(duration.seconds / 60, 60)[1]
-        seconds = divmod(duration.seconds, 60)[1]
-        
-        if hours > 0:
-            if result:
-                result += ", "
-            result += "%d %s" % (
-                hours,
-                self._pluralize(hours, "hour", "hours")
-            )
-        
-        if minutes > 0:
-            if result:
-                result += ", "
-            result += "%d %s" % (
-                minutes,
-                self._pluralize(minutes, "minute", "minutes")
-            )
-        
-        if seconds > 0:
-            if result:
-                result += ", "
-            result += "%d %s" % (
-                seconds,
-                self._pluralize(seconds, "second", "seconds")
-            )
-
-        return result
-
-    def _pluralize(self, number, unit1, unitS):
-        return unit1 if number == 1 else unitS

Added: CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/__init__.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/__init__.py	2008-07-11 14:44:49 UTC (rev 2679)
@@ -0,0 +1,25 @@
+##
+# 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.
+##
+
+__all__ = [
+    "addressmapping",
+    "caldav",
+    "cuaddress",
+    "delivery",
+    "imip",
+    "ischedule",
+    "scheduler",
+]

Added: CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/addressmapping.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/addressmapping.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/addressmapping.py	2008-07-11 14:44:49 UTC (rev 2679)
@@ -0,0 +1,88 @@
+##
+# 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.
+##
+
+from twistedcaldav.config import config
+from twistedcaldav.log import Logger
+from twistedcaldav.memcacher import Memcacher
+from twistedcaldav.scheduling.caldav import ScheduleViaCalDAV
+from twistedcaldav.scheduling.delivery import DeliveryService
+from twistedcaldav.scheduling.imip import ScheduleViaIMip
+from twistedcaldav.scheduling.ischedule import ScheduleViaISchedule
+from twistedcaldav.scheduling.cuaddress import LocalCalendarUser,\
+    RemoteCalendarUser, EmailCalendarUser, InvalidCalendarUser
+from twisted.internet.defer import inlineCallbacks, returnValue
+
+__all__ = [
+    "ScheduleAddressMapper",
+]
+
+log = Logger()
+
+"""
+Handle mapping a calendar user address to a schedule delivery type.
+"""
+
+class ScheduleAddressMapper(object):
+    """
+    Class that maps a calendar user address into a delivery service type.
+    """
+    
+    def __init__(self):
+        
+        # We are going to cache mappings whilst running
+        self.cache = Memcacher("ScheduleAddressMapper", no_invalidation=True)
+
+    @inlineCallbacks
+    def getCalendarUser(self, cuaddr, principal):
+        
+        # Get the type
+        cuaddr_type = (yield self.getCalendarUserServiceType(cuaddr))
+        if cuaddr_type == DeliveryService.serviceType_caldav:
+            returnValue(LocalCalendarUser(cuaddr, principal))
+        elif cuaddr_type == DeliveryService.serviceType_ischedule:
+            returnValue(RemoteCalendarUser(cuaddr))
+        elif cuaddr_type == DeliveryService.serviceType_imip:
+            returnValue(EmailCalendarUser(cuaddr))
+        else:
+            returnValue(InvalidCalendarUser(cuaddr))
+
+    @inlineCallbacks
+    def getCalendarUserServiceType(self, cuaddr):
+
+        # Try cache first
+        cuaddr_type = (yield self.cache.get(str(cuaddr)))
+        if cuaddr_type is None:
+            
+            serviceTypes = (ScheduleViaCalDAV,)
+            if config.Scheduling[DeliveryService.serviceType_ischedule]["Enabled"]:
+                serviceTypes += (ScheduleViaISchedule,)
+            if config.Scheduling[DeliveryService.serviceType_imip]["Enabled"]:
+                serviceTypes += (ScheduleViaIMip,)
+            for service in serviceTypes:
+                if service.matchCalendarUserAddress(cuaddr):
+                    yield self.cache.set(str(cuaddr), service.serviceType())
+                    returnValue(service.serviceType())
+
+        returnValue(cuaddr_type)
+
+    @inlineCallbacks
+    def isCalendarUserInMyDomain(self, cuaddr):
+
+        # Check whether it is a possible local address
+        serviceType = (yield self.getCalendarUserServiceType(cuaddr))
+        returnValue(serviceType == DeliveryService.serviceType_caldav)
+
+mapper = ScheduleAddressMapper()

Added: CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/caldav.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/caldav.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/caldav.py	2008-07-11 14:44:49 UTC (rev 2679)
@@ -0,0 +1,261 @@
+##
+# 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.
+##
+
+from twisted.internet import reactor
+from twisted.internet.defer import inlineCallbacks, returnValue
+
+from twisted.python.failure import Failure
+
+from twisted.web2 import responsecode
+from twisted.web2.dav import davxml
+from twisted.web2.dav.http import ErrorResponse
+from twisted.web2.dav.resource import AccessDeniedError
+from twisted.web2.dav.util import joinURL
+from twisted.web2.http import HTTPError
+
+from twistedcaldav import caldavxml
+from twistedcaldav.caldavxml import caldav_namespace
+from twistedcaldav.config import config
+from twistedcaldav.customxml import calendarserver_namespace
+from twistedcaldav.itip import iTipProcessor
+from twistedcaldav.log import Logger
+from twistedcaldav.method import report_common
+from twistedcaldav.method.put_common import StoreCalendarObjectResource
+from twistedcaldav.resource import isCalendarCollectionResource
+from twistedcaldav.scheduling.cuaddress import LocalCalendarUser,\
+    RemoteCalendarUser
+from twistedcaldav.scheduling.delivery import DeliveryService
+
+import md5
+import time
+
+"""
+Class that handles delivery of scheduling messages via CalDAV.
+"""
+
+__all__ = [
+    "ScheduleViaCalDAV",
+]
+
+log = Logger()
+
+class ScheduleViaCalDAV(DeliveryService):
+    
+    def __init__(self, scheduler, recipients, responses, freebusy):
+
+        self.scheduler = scheduler
+        self.recipients = recipients
+        self.responses = responses
+        self.freebusy = freebusy
+
+    @classmethod
+    def serviceType(cls):
+        return DeliveryService.serviceType_caldav
+
+    @classmethod
+    def matchCalendarUserAddress(cls, cuaddr):
+
+        # Check for local address matches first
+        if cuaddr.startswith("mailto:") and config.Scheduling[cls.serviceType()]["EmailDomain"]:
+            splits = cuaddr[7:].split("?")
+            domain = config.Scheduling[cls.serviceType()]["EmailDomain"]
+            if splits[0].endswith(domain):
+                return True
+
+        elif (cuaddr.startswith("http://") or cuaddr.startswith("https://")) and config.Scheduling[cls.serviceType()]["HTTPDomain"]:
+            splits = cuaddr.split(":")[0][2:].split("?")
+            domain = config.Scheduling[cls.serviceType()]["HTTPDomain"]
+            if splits[0].endswith(domain):
+                return True
+
+        elif cuaddr.startswith("/"):
+            # Assume relative HTTP URL - i.e. on this server
+            return True
+        
+        # Do default match
+        return super(ScheduleViaCalDAV, cls).matchCalendarUserAddress(cuaddr)
+
+    @inlineCallbacks
+    def generateSchedulingResponses(self):
+        
+        # Extract the ORGANIZER property and UID value from the calendar data for use later
+        organizerProp = self.scheduler.calendar.getOrganizerProperty()
+        uid = self.scheduler.calendar.resourceUID()
+
+        autoresponses = []
+        for recipient in self.recipients:
+
+            #
+            # Check access controls
+            #
+            if isinstance(self.scheduler.organizer, LocalCalendarUser):
+                try:
+                    yield recipient.inbox.checkPrivileges(self.scheduler.request, (caldavxml.Schedule(),), principal=davxml.Principal(davxml.HRef(self.scheduler.organizer.principal.principalURL())))
+                except AccessDeniedError:
+                    log.err("Could not access Inbox for recipient: %s" % (recipient.cuaddr,))
+                    err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-permissions")))
+                    self.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 self.freebusy:
+                yield self.generateLocalFreeBusyResponse(recipient, self.responses, organizerProp, uid)
+            else:
+                yield self.generateLocalResponse(recipient, self.responses, autoresponses)
+
+        # 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.scheduler.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.scheduler.request, principal, inbox, self.scheduler.calendar.duplicate(), child))
+
+    @inlineCallbacks
+    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.scheduler.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 = (yield self.scheduler.request.locateResource(childURL))
+
+        # Copy calendar to inbox (doing fan-out)
+        try:
+            yield StoreCalendarObjectResource(
+                         request=self.scheduler.request,
+                         destination = child,
+                         destination_uri = childURL,
+                         destinationparent = recipient.inbox,
+                         destinationcal = True,
+                         calendar = self.scheduler.calendar,
+                         isiTIP = True
+                     ).run()
+        except:
+            # FIXME: Bare 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")
+            returnValue(False)
+        else:
+            responses.add(recipient.cuaddr, responsecode.OK, reqstatus="2.0;Success")
+
+            # Store CALDAV:originator property
+            child.writeDeadProperty(caldavxml.Originator(davxml.HRef(self.scheduler.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))
+                
+            returnValue(True)
+    
+    @inlineCallbacks
+    def generateLocalFreeBusyResponse(self, recipient, responses, organizerProp, uid):
+
+        # Extract the ATTENDEE property matching current recipient from the calendar data
+        cuas = recipient.principal.calendarUserAddresses()
+        attendeeProp = self.scheduler.calendar.getAttendeeProperty(cuas)
+
+        remote = isinstance(self.scheduler.organizer, RemoteCalendarUser)
+
+        try:
+            fbresult = (yield self.generateAttendeeFreeBusyResponse(
+                recipient,
+                organizerProp,
+                uid,
+                attendeeProp,
+                remote,
+            ))
+        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")
+            returnValue(False)
+        else:
+            responses.add(recipient.cuaddr, responsecode.OK, reqstatus="2.0;Success", calendar=fbresult)
+            returnValue(True)
+    
+    @inlineCallbacks
+    def generateAttendeeFreeBusyResponse(self, recipient, organizerProp, uid, attendeeProp, remote):
+
+        # Find the current recipients calendar-free-busy-set
+        fbset = (yield recipient.principal.calendarFreeBusyURIs(self.scheduler.request))
+
+        # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
+        fbinfo = ([], [], [])
+    
+        # Process the availability property from the Inbox.
+        has_prop = (yield recipient.inbox.hasProperty((calendarserver_namespace, "calendar-availability"), self.scheduler.request))
+        if has_prop:
+            availability = (yield recipient.inbox.readProperty((calendarserver_namespace, "calendar-availability"), self.scheduler.request))
+            availability = availability.calendar()
+            report_common.processAvailabilityFreeBusy(availability, fbinfo, self.scheduler.timeRange)
+
+        # Check to see if the recipient is the same calendar user as the organizer.
+        # Needed for masked UID stuff.
+        if isinstance(self.scheduler.organizer, LocalCalendarUser):
+            same_calendar_user = self.scheduler.organizer.principal.principalURL() == recipient.principal.principalURL()
+        else:
+            same_calendar_user = False
+
+        # Now process free-busy set calendars
+        matchtotal = 0
+        for calendarResourceURL in fbset:
+            calendarResource = (yield self.scheduler.request.locateResource(calendarResourceURL))
+            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 = (yield report_common.generateFreeBusyInfo(
+                self.scheduler.request,
+                calendarResource,
+                fbinfo,
+                self.scheduler.timeRange,
+                matchtotal,
+                excludeuid = self.scheduler.excludeUID,
+                organizer = self.scheduler.organizer.cuaddr,
+                same_calendar_user = same_calendar_user,
+                servertoserver=remote
+            ))
+    
+        # Build VFREEBUSY iTIP reply for this recipient
+        fbresult = report_common.buildFreeBusyResult(
+            fbinfo,
+            self.scheduler.timeRange,
+            organizer = organizerProp,
+            attendee = attendeeProp,
+            uid = uid,
+            method = "REPLY"
+        )
+
+        returnValue(fbresult)

Added: CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/cuaddress.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/cuaddress.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/cuaddress.py	2008-07-11 14:44:49 UTC (rev 2679)
@@ -0,0 +1,77 @@
+##
+# 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.
+##
+
+from twistedcaldav.log import Logger
+from twistedcaldav.scheduling.delivery import DeliveryService
+
+__all__ = [
+    "LocalCalendarUser",
+    "RemoteCalendarUser",
+    "EmailCalendarUser",
+    "InvalidCalendarUser",
+]
+
+log = Logger()
+
+class CalendarUser(object):
+    def __init__(self, cuaddr):
+        self.cuaddr = cuaddr
+        self.serviceType = None
+
+class LocalCalendarUser(CalendarUser):
+    def __init__(self, cuaddr, principal, inbox=None, inboxURL=None):
+        self.cuaddr = cuaddr
+        self.principal = principal
+        self.inbox = inbox
+        self.inboxURL = inboxURL
+        self.serviceType = DeliveryService.serviceType_caldav
+    
+    def __str__(self):
+        return "Local calendar user: %s" % (self.cuaddr,)
+
+class RemoteCalendarUser(CalendarUser):
+    def __init__(self, cuaddr):
+        self.cuaddr = cuaddr
+        self.extractDomain()
+        self.serviceType = DeliveryService.serviceType_ischedule
+
+    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 EmailCalendarUser(CalendarUser):
+    
+    def __init__(self, cuaddr, principal, inbox=None, inboxURL=None):
+        self.cuaddr = cuaddr
+        self.serviceType = DeliveryService.serviceType_imip
+    
+    def __str__(self):
+        return "Email/iMIP calendar user: %s" % (self.cuaddr,)
+
+class InvalidCalendarUser(CalendarUser):
+    
+    def __str__(self):
+        return "Invalid calendar user: %s" % (self.cuaddr,)
+

Added: CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/delivery.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/delivery.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/delivery.py	2008-07-11 14:44:49 UTC (rev 2679)
@@ -0,0 +1,64 @@
+##
+# 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.
+##
+
+from twistedcaldav.config import config
+from twistedcaldav.log import Logger
+
+import re
+
+__all__ = [
+    "DeliveryService",
+]
+
+log = Logger()
+
+class DeliveryService(object):
+    """
+    Abstract base class that defines a delivery method for a scheduling message.
+    """
+    
+    # Known types
+    
+    serviceType_caldav    = 'CalDAV'
+    serviceType_ischedule = 'iSchedule'
+    serviceType_imip      = 'iMIP'
+
+    def __init__(self, scheduler, recipients, responses, freebusy):
+
+        self.scheduler = scheduler
+        self.recipients = recipients
+        self.responses = responses
+        self.freebusy = freebusy
+
+    @classmethod
+    def serviceType(cls):
+        raise NotImplementedError
+
+    @classmethod
+    def matchCalendarUserAddress(cls, cuaddr):
+        
+        # Do the pattern match
+        for pattern in config.Scheduling[cls.serviceType()]["AddressPatterns"]:
+            try:
+                if re.match(pattern, cuaddr) is not None:
+                    return True
+            except re.error:
+                log.error("Invalid regular expression for Scheduling configuration '%s/LocalAddresses': %s" % (cls.serviceType(), pattern,))
+
+        return False
+
+    def generateSchedulingResponses(self):
+        raise NotImplementedError

Added: CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/imip.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/imip.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/imip.py	2008-07-11 14:44:49 UTC (rev 2679)
@@ -0,0 +1,258 @@
+##
+# 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.
+##
+
+from twisted.internet.defer import inlineCallbacks
+
+from twisted.mail.smtp import messageid
+from twisted.mail.smtp import rfc822date
+from twisted.mail.smtp import sendmail
+
+from twisted.python.failure import Failure
+
+from twisted.web2 import responsecode
+from twisted.web2.dav.http import ErrorResponse
+from twisted.web2.http import HTTPError
+
+from twistedcaldav.caldavxml import caldav_namespace
+from twistedcaldav.config import config
+from twistedcaldav.log import Logger
+from twistedcaldav.scheduling.delivery import DeliveryService
+
+import MimeWriter
+import base64
+import cStringIO
+import datetime
+
+"""
+Class that handles delivery of scheduling messages via iMIP.
+"""
+
+__all__ = [
+    "ScheduleViaIMip",
+]
+
+log = Logger()
+
+class ScheduleViaIMip(DeliveryService):
+    
+    @classmethod
+    def serviceType(cls):
+        return DeliveryService.serviceType_imip
+
+    @inlineCallbacks
+    def generateSchedulingResponses(self):
+        
+        # Generate an HTTP client request
+        try:
+            # We do not do freebusy requests via iMIP
+            if self.freebusy:
+                raise ValueError("iMIP VFREEBUSY REQUESTs not supported.")
+
+            message = self._generateTemplateMessage(self.scheduler.calendar)
+            fromAddr = self.scheduler.originator.cuaddr
+            if not fromAddr.startswith("mailto:"):
+                raise ValueError("ORGANIZER address '%s' must be mailto: for iMIP operation." % (fromAddr,))
+            fromAddr = fromAddr[7:]
+            message = message.replace("${fromaddress}", fromAddr)
+            
+            for recipient in self.recipients:
+                try:
+                    toAddr = recipient.cuaddr
+                    if not toAddr.startswith("mailto:"):
+                        raise ValueError("ATTENDEE address '%s' must be mailto: for iMIP operation." % (toAddr,))
+                    toAddr = toAddr[7:]
+                    sendit = message.replace("${toaddress}", toAddr)
+                    yield sendmail(config.Scheduling[self.serviceType()]["Sending"]["Server"], fromAddr, toAddr, sendit)
+        
+                except Exception, e:
+                    # Generated failed response for this recipient
+                    log.err("Could not do server-to-imip request : %s %s" % (self, e))
+                    err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-failed")))
+                    self.responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus="5.1;Service unavailable")
+                
+                else:
+                    self.responses.add(recipient.cuaddr, responsecode.OK, reqstatus="2.0;Success")
+
+        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 _generateTemplateMessage(self, calendar):
+
+        caldata = str(calendar)
+        data = cStringIO.StringIO()
+        writer = MimeWriter.MimeWriter(data)
+    
+        writer.addheader("From", "${fromaddress}")
+        writer.addheader("To", "${toaddress}")
+        writer.addheader("Date", rfc822date())
+        writer.addheader("Subject", "DO NOT REPLY: calendar invitation test")
+        writer.addheader("Message-ID", messageid())
+        writer.addheader("Mime-Version", "1.0")
+        writer.flushheaders()
+    
+        writer.startmultipartbody("mixed")
+    
+        # message body
+        part = writer.nextpart()
+        body = part.startbody("text/plain")
+        body.write("""Hi,
+You've been invited to a cool event by CalendarServer's new iMIP processor.
+
+%s
+""" % (self._generateCalendarSummary(calendar),))
+    
+        part = writer.nextpart()
+        encoding = "7bit"
+        for i in caldata:
+            if ord(i) > 127:
+                encoding = "base64"
+                caldata = base64.encodestring(caldata)
+                break
+        part.addheader("Content-Transfer-Encoding", encoding)
+        body = part.startbody("text/calendar; charset=utf-8")
+        body.write(caldata.replace("\r", ""))
+    
+        # finish
+        writer.lastpart()
+
+        return data.getvalue()
+
+    def _generateCalendarSummary(self, calendar):
+
+        # Get the most appropriate component
+        component = calendar.masterComponent()
+        if component is None:
+            component = calendar.mainComponent(True)
+            
+        organizer = component.getOrganizerProperty()
+        if "CN" in organizer.params():
+            organizer = "%s <%s>" % (organizer.params()["CN"][0], organizer.value(),)
+        else:
+            organizer = organizer.value()
+            
+        dtinfo = self._getDateTimeInfo(component)
+        
+        summary = component.propertyValue("SUMMARY")
+        if summary is None:
+            summary = ""
+
+        description = component.propertyValue("DESCRIPTION")
+        if description is None:
+            description = ""
+
+        return """---- Begin Calendar Event Summary ----
+
+Organizer:   %s
+Summary:     %s
+%sDescription: %s
+
+----  End Calendar Event Summary  ----
+""" % (organizer, summary, dtinfo, description,)
+
+    def _getDateTimeInfo(self, component):
+        
+        dtstart = component.propertyNativeValue("DTSTART")
+        tzid_start = component.getProperty("DTSTART").params().get("TZID", "UTC")
+
+        dtend = component.propertyNativeValue("DTEND")
+        if dtend:
+            tzid_end = component.getProperty("DTEND").params().get("TZID", "UTC")
+            duration = dtend - dtstart
+        else:
+            duration = component.propertyNativeValue("DURATION")
+            if duration:
+                dtend = dtstart + duration
+                tzid_end = tzid_start
+            else:
+                if isinstance(dtstart, datetime.date):
+                    dtend = None
+                    duration = datetime.timedelta(days=1)
+                else:
+                    dtend = dtstart + datetime.timedelta(days=1)
+                    dtend.hour = dtend.minute = dtend.second = 0
+                    duration = dtend - dtstart
+        result = "Starts:      %s\n" % (self._getDateTimeText(dtstart, tzid_start),)
+        if dtend is not None:
+            result += "Ends:        %s\n" % (self._getDateTimeText(dtend, tzid_end),)
+        result += "Duration:    %s\n" % (self._getDurationText(duration),)
+        
+        if not isinstance(dtstart, datetime.datetime):
+            result += "All Day\n"
+        
+        for property_name in ("RRULE", "RDATE", "EXRULE", "EXDATE", "RECURRENCE-ID",):
+            if component.hasProperty(property_name):
+                result += "Recurring\n"
+                break
+            
+        return result
+
+    def _getDateTimeText(self, dtvalue, tzid):
+        
+        if isinstance(dtvalue, datetime.datetime):
+            timeformat = "%A, %B %e, %Y %I:%M %p"
+        elif isinstance(dtvalue, datetime.date):
+            timeformat = "%A, %B %e, %Y"
+            tzid = ""
+        if tzid:
+            tzid = " (%s)" % (tzid,)
+
+        return "%s%s" % (dtvalue.strftime(timeformat), tzid,)
+        
+    def _getDurationText(self, duration):
+        
+        result = ""
+        if duration.days > 0:
+            result += "%d %s" % (
+                duration.days,
+                self._pluralize(duration.days, "day", "days")
+            )
+
+        hours = duration.seconds / 3600
+        minutes = divmod(duration.seconds / 60, 60)[1]
+        seconds = divmod(duration.seconds, 60)[1]
+        
+        if hours > 0:
+            if result:
+                result += ", "
+            result += "%d %s" % (
+                hours,
+                self._pluralize(hours, "hour", "hours")
+            )
+        
+        if minutes > 0:
+            if result:
+                result += ", "
+            result += "%d %s" % (
+                minutes,
+                self._pluralize(minutes, "minute", "minutes")
+            )
+        
+        if seconds > 0:
+            if result:
+                result += ", "
+            result += "%d %s" % (
+                seconds,
+                self._pluralize(seconds, "second", "seconds")
+            )
+
+        return result
+
+    def _pluralize(self, number, unit1, unitS):
+        return unit1 if number == 1 else unitS

Added: CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/ischedule.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/ischedule.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/ischedule.py	2008-07-11 14:44:49 UTC (rev 2679)
@@ -0,0 +1,184 @@
+##
+# 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.
+##
+
+from twisted.internet.defer import inlineCallbacks, DeferredList
+from twisted.internet.protocol import ClientCreator
+
+from twisted.python.failure import Failure
+
+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 import caldavxml
+from twistedcaldav.caldavxml import caldav_namespace
+from twistedcaldav.config import config
+from twistedcaldav.log import Logger
+from twistedcaldav.scheduling.delivery import DeliveryService
+from twistedcaldav.scheduling.ischeduleservers import IScheduleServers
+
+"""
+Server to server utility functions and client requests.
+"""
+
+__all__ = [
+    "ScheduleViaISchedule",
+]
+
+log = Logger()
+
+class ScheduleViaISchedule(DeliveryService):
+    
+    @classmethod
+    def serviceType(cls):
+        return DeliveryService.serviceType_ischedule
+
+    @classmethod
+    def matchCalendarUserAddress(cls, cuaddr):
+
+        # TODO: here is where we would attempt service discovery based on the cuaddr.
+        
+        # Do default match
+        return super(ScheduleViaISchedule, cls).matchCalendarUserAddress(cuaddr)
+
+    @inlineCallbacks
+    def generateSchedulingResponses(self):
+        """
+        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 = IScheduleServers()
+        for recipient in self.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")))
+                self.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")))
+                self.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:
+            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 = IScheduleRequest(self.scheduler, server, recipients, self.responses)
+            deferreds.append(requestor.doRequest())
+
+        yield DeferredList(deferreds)
+
+class IScheduleRequest(object):
+    
+    def __init__(self, scheduler, server, recipients, responses):
+
+        self.scheduler = scheduler
+        self.server = server
+        self.recipients = recipients
+        self.responses = responses
+        
+        self._generateHeaders()
+        self._prepareData()
+        
+    @inlineCallbacks
+    def doRequest(self):
+        
+        # Generate an HTTP client request
+        try:
+            from twisted.internet import reactor
+            if self.server.ssl:
+                from twistedcaldav.tap import ChainingOpenSSLContextFactory
+                context = ChainingOpenSSLContextFactory(config.SSLPrivateKey, config.SSLCertificate, certificateChainFile=config.SSLAuthorityChain)
+                proto = (yield ClientCreator(reactor, HTTPClientProtocol).connectSSL(self.server.host, self.server.port, context))
+            else:
+                proto = (yield ClientCreator(reactor, HTTPClientProtocol).connectTCP(self.server.host, self.server.port))
+            
+            request = ClientRequest("POST", self.server.path, self.headers, self.data)
+            yield log.logRequest("debug", "Sending server-to-server POST request:", request)
+            response = (yield proto.submitRequest(request))
+    
+            yield log.logResponse("debug", "Received server-to-server POST response:", response)
+            xml = (yield davXMLFromStream(response.stream))
+    
+            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)

Added: CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/ischeduleservers.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/ischeduleservers.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/ischeduleservers.py	2008-07-11 14:44:49 UTC (rev 2679)
@@ -0,0 +1,202 @@
+##
+# 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.
+##
+
+from twisted.python.filepath import FilePath
+
+from twistedcaldav.config import config
+from twistedcaldav.log import Logger
+from twistedcaldav.scheduling.delivery import DeliveryService
+
+import xml.dom.minidom
+
+"""
+XML based iSchedule configuration file handling.
+"""
+
+__all__ = [
+    "IScheduleServers",
+]
+
+log = Logger()
+
+class IScheduleServers(object):
+    
+    _fileInfo = None
+    _xmlFile = None
+    _servers = None
+    _domainMap = None
+    
+    def __init__(self):
+        
+        self._loadConfig()
+
+    def _loadConfig(self):
+        if IScheduleServers._servers is None:
+            IScheduleServers._xmlFile = FilePath(config.Scheduling[DeliveryService.serviceType_ischedule]["Servers"])
+        IScheduleServers._xmlFile.restat()
+        fileInfo = (IScheduleServers._xmlFile.getmtime(), IScheduleServers._xmlFile.getsize())
+        if fileInfo != IScheduleServers._fileInfo:
+            parser = IScheduleServersParser(IScheduleServers._xmlFile)
+            IScheduleServers._servers = parser.servers
+            self._mapDomains()
+            IScheduleServers._fileInfo = fileInfo
+        
+    def _mapDomains(self):
+        IScheduleServers._domainMap = {}
+        for server in IScheduleServers._servers:
+            for domain in server.domains:
+                IScheduleServers._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 IScheduleServers._domainMap.get(domain)
+
+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 IScheduleServersParser(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(IScheduleServerRecord())
+                self.servers[-1].parseXML(child)
+                
+class IScheduleServerRecord (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]

Added: CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/scheduler.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/scheduler.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/scheduler.py	2008-07-11 14:44:49 UTC (rev 2679)
@@ -0,0 +1,774 @@
+##
+# 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.
+##
+
+from twisted.internet.defer import inlineCallbacks, returnValue
+
+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.http import HTTPError, Response
+from twisted.web2.http_headers import MimeType
+
+from twistedcaldav import caldavxml
+from twistedcaldav.accounting import accountingEnabled, emitAccounting
+from twistedcaldav.caldavxml import caldav_namespace, TimeRange
+from twistedcaldav.customxml import calendarserver_namespace
+from twistedcaldav.ical import Component
+from twistedcaldav.log import Logger, LoggingMixIn
+from twistedcaldav.scheduling import addressmapping
+from twistedcaldav.scheduling.caldav import ScheduleViaCalDAV
+from twistedcaldav.scheduling.cuaddress import InvalidCalendarUser,\
+    LocalCalendarUser, RemoteCalendarUser, EmailCalendarUser
+from twistedcaldav.scheduling.imip import ScheduleViaIMip
+from twistedcaldav.scheduling.ischedule import ScheduleViaISchedule
+from twistedcaldav.scheduling.ischeduleservers import IScheduleServers
+
+import itertools
+import re
+import socket
+
+"""
+CalDAV/Server-to-Server scheduling behavior.
+"""
+
+__all__ = [
+    "Scheduler",
+    "CalDAVScheduler",
+    "IScheduleScheduler",
+]
+
+
+log = Logger()
+
+class Scheduler(object):
+    
+    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
+    
+    @inlineCallbacks
+    def doSchedulingViaPOST(self):
+        """
+        The Scheduling POST operation on an Outbox.
+        """
+    
+        # Do some extra authorization checks
+        self.checkAuthorization()
+
+        # Load various useful bits doing some basic checks on those
+        self.loadOriginatorFromRequestHeaders()
+        self.loadRecipientsFromRequestHeaders()
+        yield self.loadCalendarFromRequest()
+
+        response = (yield self.doScheduling())
+        returnValue(response)
+
+    @inlineCallbacks
+    def doSchedulingViaPUT(self, originator, recipients, calendar):
+        """
+        The implicit scheduling PUT operation.
+        """
+    
+        # Do some extra authorization checks
+        self.checkAuthorization()
+
+        # Load various useful bits doing some basic checks on those
+        self.originator = originator
+        self.recipients = recipients
+        self.calendar = calendar
+
+        response = (yield self.doScheduling())
+        returnValue(response)
+
+    @inlineCallbacks
+    def doScheduling(self):
+        # Check validity of Originator header.
+        yield self.checkOriginator()
+    
+        # Get recipient details.
+        yield self.checkRecipients()
+    
+        # Check calendar data.
+        self.checkCalendarData()
+    
+        # Check validity of ORGANIZER
+        yield self.checkOrganizer()
+    
+        # Do security checks (e.g. spoofing)
+        yield self.securityChecks()
+    
+        # Generate accounting information
+        self.doAccounting()
+
+        # Do scheduling tasks
+        response = (yield self.generateSchedulingResponse())
+
+        returnValue(response)
+
+    def loadOriginatorFromRequestHeaders(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 loadRecipientsFromRequestHeaders(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)
+        
+    @inlineCallbacks
+    def loadCalendarFromRequest(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:
+            self.calendar = (yield Component.fromIStream(self.request.stream))
+        except:
+            # FIXME: Bare 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, 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)
+                    )
+                )
+
+    @inlineCallbacks
+    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)
+    
+        # Loop over each recipient and aggregate into lists by service types.
+        caldav_recipients = []
+        remote_recipients = []
+        imip_recipients = []
+        for recipient in self.recipients:
+    
+            if isinstance(recipient, LocalCalendarUser):
+                caldav_recipients.append(recipient)
+
+            elif isinstance(recipient, RemoteCalendarUser):
+                remote_recipients.append(recipient)
+
+            elif isinstance(recipient, EmailCalendarUser):
+                imip_recipients.append(recipient)
+
+            else:
+                err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-exists")))
+                responses.add(recipient.cuaddr, Failure(exc_value=err), reqstatus="3.7;Invalid Calendar User")
+            
+        # Now process local recipients
+        if caldav_recipients:
+            yield self.generateLocalSchedulingResponses(caldav_recipients, responses, freebusy)
+
+        # Now process remote recipients
+        if remote_recipients:
+            yield self.generateRemoteSchedulingResponses(remote_recipients, responses, freebusy)
+
+        # Now process iMIP recipients
+        if imip_recipients:
+            yield self.generateIMIPSchedulingResponses(imip_recipients, responses, freebusy)
+    
+        # Return with final response if we are done
+        returnValue(responses.response())
+    
+    @inlineCallbacks
+    def generateLocalSchedulingResponses(self, recipients, responses, freebusy):
+        """
+        Generate scheduling responses for CalDAV recipients.
+        """
+
+        # Create the scheduler and run it.
+        requestor = ScheduleViaCalDAV(self, recipients, responses, freebusy)
+        yield requestor.generateSchedulingResponses()
+
+    @inlineCallbacks
+    def generateRemoteSchedulingResponses(self, recipients, responses, freebusy):
+        """
+        Generate scheduling responses for remote recipients.
+        """
+
+        # Create the scheduler and run it.
+        requestor = ScheduleViaISchedule(self, recipients, responses, freebusy)
+        yield requestor.generateSchedulingResponses()
+
+    @inlineCallbacks
+    def generateIMIPSchedulingResponses(self, recipients, responses, freebusy):
+        """
+        Generate scheduling responses for iMIP recipients.
+        """
+
+        # Create the scheduler and run it.
+        requestor = ScheduleViaIMip(self, recipients, responses, freebusy)
+        yield requestor.generateSchedulingResponses(freebusy)
+
+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 = LocalCalendarUser(self.originator, originatorPrincipal)
+
+    @inlineCallbacks
+    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:
+                address = (yield addressmapping.mapper.getCalendarUser(recipient, principal))
+                if isinstance(address, InvalidCalendarUser):
+                    log.err("Unknown calendar user address: %s" % (recipient,))
+                results.append(address)
+            else:
+                # Map recipient to their inbox
+                inbox = None
+                inboxURL = principal.scheduleInboxURL()
+                if inboxURL:
+                    inbox = (yield self.request.locateResource(inboxURL))
+
+                if inbox:
+                    results.append(LocalCalendarUser(recipient, principal, inbox, inboxURL))
+                else:
+                    log.err("No schedule inbox for principal: %s" % (principal,))
+                    results.append(InvalidCalendarUser(recipient))
+        
+        self.recipients = results
+
+    @inlineCallbacks
+    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 = 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")))
+            else:
+                localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(organizer))
+                if localUser:
+                    log.err("No principal for ORGANIZER in calendar data: %s" % (self.calendar,))
+                    raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+                else:
+                    self.organizer = 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, 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 IScheduleScheduler(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")))
+
+    @inlineCallbacks
+    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)
+        localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(self.originator))
+        if originatorPrincipal or localUser:
+            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 = RemoteCalendarUser(self.originator)
+            
+        # We will only accept originator in known domains.
+        servermgr = IScheduleServers()
+        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")))
+
+    @inlineCallbacks
+    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:
+                localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(recipient))
+                if localUser:
+                    log.err("No principal for calendar user address: %s" % (recipient,))
+                else:
+                    log.err("Unknown calendar user address: %s" % (recipient,))
+                results.append(InvalidCalendarUser(recipient))
+            else:
+                # Map recipient to their inbox
+                inbox = None
+                inboxURL = principal.scheduleInboxURL()
+                if inboxURL:
+                    inbox = (yield self.request.locateResource(inboxURL))
+
+                if inbox:
+                    results.append(LocalCalendarUser(recipient, principal, inbox, inboxURL))
+                else:
+                    log.err("No schedule inbox for principal: %s" % (principal,))
+                    results.append(InvalidCalendarUser(recipient))
+        
+        self.recipients = results
+
+    def checkOrganizer(self):
+        """
+        Delay ORGANIZER check until we know what their role is.
+        """
+        pass
+
+    @inlineCallbacks
+    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")))
+            else:
+                localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(organizer))
+                if localUser:
+                    log.err("Unsupported ORGANIZER in calendar data: %s" % (self.calendar,))
+                    raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+                else:
+                    self.organizer = RemoteCalendarUser(organizer)
+        else:
+            log.err("ORGANIZER missing in calendar data: %s" % (self.calendar,))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+
+    @inlineCallbacks
+    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")))
+        else:
+            localUser = (yield addressmapping.mapper.isCalendarUserInMyDomain(attendee))
+            if localUser:
+                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.
+
+    @inlineCallbacks
+    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"):
+            yield self.checkOrganizerAsOriginator()
+    
+        # Prevent spoofing when doing reply-like METHODs
+        elif self.calendar.propertyValue("METHOD") in ("REPLY", "COUNTER", "REFRESH"):
+            yield 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

Deleted: CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/servertoserver.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/servertoserver.py	2008-07-11 14:26:44 UTC (rev 2678)
+++ CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/servertoserver.py	2008-07-11 14:44:49 UTC (rev 2679)
@@ -1,160 +0,0 @@
-##
-# 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 inlineCallbacks
-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()
-        
-    @inlineCallbacks
-    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)
-                proto = yield ClientCreator(reactor, HTTPClientProtocol).connectSSL(self.server.host, self.server.port, context)
-            else:
-                proto = yield ClientCreator(reactor, HTTPClientProtocol).connectTCP(self.server.host, self.server.port)
-            
-            request = ClientRequest("POST", self.server.path, self.headers, self.data)
-            yield log.logRequest("debug", "Sending server-to-server POST request:", request)
-            response = yield proto.submitRequest(request)
-    
-            yield log.logResponse("debug", "Received server-to-server POST response:", response)
-            xml = yield davXMLFromStream(response.stream)
-    
-            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)

Deleted: CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/servertoserverparser.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/servertoserverparser.py	2008-07-11 14:26:44 UTC (rev 2678)
+++ CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/servertoserverparser.py	2008-07-11 14:44:49 UTC (rev 2679)
@@ -1,165 +0,0 @@
-##
-# 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/implicit-2660/twistedcaldav/static.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/static.py	2008-07-11 14:26:44 UTC (rev 2678)
+++ CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/static.py	2008-07-11 14:44:49 UTC (rev 2679)
@@ -61,7 +61,7 @@
 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, ScheduleServerToServerResource
+from twistedcaldav.schedule import ScheduleInboxResource, ScheduleOutboxResource, IScheduleInboxResource
 from twistedcaldav.dropbox import DropBoxHomeResource, DropBoxCollectionResource
 from twistedcaldav.directory.calendar import uidsResourceName
 from twistedcaldav.directory.calendar import DirectoryCalendarHomeProvisioningResource
@@ -690,13 +690,13 @@
     def __repr__(self):
         return "<%s (calendar outbox collection): %s>" % (self.__class__.__name__, self.fp.path)
 
-class ServerToServerInboxFile (ScheduleServerToServerResource, CalDAVFile):
+class IScheduleInboxFile (IScheduleInboxResource, CalDAVFile):
     """
     Server-to-server scheduling inbox resource.
     """
     def __init__(self, path, parent):
         CalDAVFile.__init__(self, path, principalCollections=parent.principalCollections())
-        ScheduleServerToServerResource.__init__(self, parent)
+        IScheduleInboxResource.__init__(self, parent)
         
         self.fp.open("w").close()
         self.fp.restat(False)

Modified: CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/tap.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/tap.py	2008-07-11 14:26:44 UTC (rev 2678)
+++ CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/tap.py	2008-07-11 14:44:49 UTC (rev 2679)
@@ -51,7 +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.static import IScheduleInboxFile
 from twistedcaldav.static import TimezoneServiceFile
 from twistedcaldav.timezones import TimezoneCache
 from twistedcaldav import pdmonster
@@ -435,7 +435,7 @@
     rootResourceClass            = RootResource
     principalResourceClass       = DirectoryPrincipalProvisioningResource
     calendarResourceClass        = CalendarHomeProvisioningFile
-    servertoserverResourceClass = ServerToServerInboxFile
+    iScheduleResourceClass       = IScheduleInboxFile
     timezoneServiceResourceClass = TimezoneServiceFile
 
     def makeService_Slave(self, options):
@@ -526,15 +526,15 @@
             )
             root.putChild('timezones', timezoneService)
 
-        # Server-to-server service is optional
-        if config.ServerToServer["Enabled"]:
-            log.msg("Setting up server-to-server resource: %r" % (self.servertoserverResourceClass,))
+        # iSchedule service is optional
+        if config.Scheduling["iSchedule"]["Enabled"]:
+            log.msg("Setting up iSchedule inbox resource: %r" % (self.iScheduleResourceClass,))
     
-            servertoserver = self.servertoserverResourceClass(
+            ischedule = self.iScheduleResourceClass(
                 os.path.join(config.DocumentRoot, 'inbox'),
                 root,
             )
-            root.putChild('inbox', servertoserver)
+            root.putChild('inbox', ischedule)
 
         #
         # Configure ancillary data

Modified: CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/test/test_imip.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/test/test_imip.py	2008-07-11 14:26:44 UTC (rev 2678)
+++ CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/test/test_imip.py	2008-07-11 14:44:49 UTC (rev 2679)
@@ -13,11 +13,11 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 ##
-from twistedcaldav.ical import Component
 
 import datetime
 
-from twistedcaldav.schedule_imip import ServerToIMip
+from twistedcaldav.ical import Component
+from twistedcaldav.scheduling.imip import ScheduleViaIMip
 import twistedcaldav.test.util
 
 class iMIP (twistedcaldav.test.util.TestCase):
@@ -31,7 +31,7 @@
 
     def test_datetime_text(self):
         
-        imip = ServerToIMip(None, [], [])
+        imip = ScheduleViaIMip(None, [], [], False)
         
         data = (
             (
@@ -51,7 +51,7 @@
         
     def test_duration_text(self):
         
-        imip = ServerToIMip(None, [], [])
+        imip = ScheduleViaIMip(None, [], [], False)
         
         data = (
             (
@@ -178,7 +178,7 @@
         
         
         for data, result in data:
-            imip = ServerToIMip(self.DummyScheduler(Component.fromString(data)), [], [])
+            imip = ScheduleViaIMip(self.DummyScheduler(Component.fromString(data)), [], [], False)
             self.assertEqual(imip._getDateTimeInfo(imip.scheduler.calendar.masterComponent()), result)
         
     def test_calendar_summary(self):
@@ -274,7 +274,7 @@
         
         
         for data, result in data:
-            imip = ServerToIMip(self.DummyScheduler(Component.fromString(data)), [], [])
+            imip = ScheduleViaIMip(self.DummyScheduler(Component.fromString(data)), [], [], False)
             self.assertEqual(imip._generateCalendarSummary(imip.scheduler.calendar), result)
         
         
@@ -495,6 +495,6 @@
             return "\n".join(newlines)
 
         for data, result in data:
-            imip = ServerToIMip(self.DummyScheduler(Component.fromString(data)), [], [])
+            imip = ScheduleViaIMip(self.DummyScheduler(Component.fromString(data)), [], [], False)
             self.assertEqual(_normalizeMessage(imip._generateTemplateMessage(imip.scheduler.calendar)), result)
         
-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20080711/d7ac41f0/attachment-0001.html 


More information about the calendarserver-changes mailing list