[CalendarServer-changes] [2805] CalendarServer/branches/users/cdaboo/implicit-2799

source_changes at macosforge.org source_changes at macosforge.org
Mon Aug 11 18:48:19 PDT 2008


Revision: 2805
          http://trac.macosforge.org/projects/calendarserver/changeset/2805
Author:   cdaboo at apple.com
Date:     2008-08-11 18:48:19 -0700 (Mon, 11 Aug 2008)
Log Message:
-----------
Merged forward from trunk.

Modified Paths:
--------------
    CalendarServer/branches/users/cdaboo/implicit-2799/conf/caldavd-test.plist
    CalendarServer/branches/users/cdaboo/implicit-2799/conf/caldavd.plist
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/__init__.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/caldavxml.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/config.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/customxml.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/directory/calendar.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/directory/principal.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/extensions.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/fileops.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/ical.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/itip.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/log.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/copymove.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/delete.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/get.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/mkcalendar.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/propfind.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/put.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/put_common.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/report.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/report_calquery.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/report_common.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/report_freebusy.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/report_multiget.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/resource.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/schedule.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/static.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/tap.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/test/test_icalendar.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/test/test_options.py

Added Paths:
-----------
    CalendarServer/branches/users/cdaboo/implicit-2799/conf/servertoserver-test.xml
    CalendarServer/branches/users/cdaboo/implicit-2799/conf/servertoserver.dtd
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/freebusyurl.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/__init__.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/addressmapping.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/caldav.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/cuaddress.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/delivery.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/icaldiff.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/imip.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/implicit.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/ischedule.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/ischeduleservers.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/scheduler.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/test/
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/test/__init__.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/test/test_icaldiff.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/test/test_imip.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/test/test_itip.py

Removed Paths:
-------------
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/__init__.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/addressmapping.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/caldav.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/cuaddress.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/delivery.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/icaldiff.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/imip.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/implicit.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/ischedule.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/ischeduleservers.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/scheduler.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/test/
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/test/__init__.py
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/test/test_icaldiff.py

Property Changed:
----------------
    CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/test/

Modified: CalendarServer/branches/users/cdaboo/implicit-2799/conf/caldavd-test.plist
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/conf/caldavd-test.plist	2008-08-11 22:41:16 UTC (rev 2804)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/conf/caldavd-test.plist	2008-08-12 01:48:19 UTC (rev 2805)
@@ -379,7 +379,71 @@
   <key>EnableTimezoneService</key>
   <true/>
 
+  <!-- Server to server protocol -->
+  <key>Scheduling</key>
+  <dict>
+    <!--  CalDAV protocol options -->
+  	<key>CalDAV</key>
+  	<dict>
+  	  <key>EmailDomain</key>
+  	  <string></string>
+  	  <key>HTTPDomain</key>
+  	  <string></string>
+  	  <key>AddressPatterns</key>
+  	  <array>
+  	  </array>
+  	  <key>OldDraftCompatability</key>
+  	  <true/>
+  	</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 -->
+  <key>FreeBusyURL</key>
+  <dict>
+  	<key>Enabled</key>
+  	<true/>
+  	<key>Time Period</key>
+  	<integer>14</integer>
+  	<key>Anonymous Access</key>
+  	<false/>
+  </dict>
+
   <!--
     Twisted
   -->

Modified: CalendarServer/branches/users/cdaboo/implicit-2799/conf/caldavd.plist
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/conf/caldavd.plist	2008-08-11 22:41:16 UTC (rev 2804)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/conf/caldavd.plist	2008-08-12 01:48:19 UTC (rev 2805)
@@ -348,6 +348,70 @@
   <key>EnableTimezoneService</key>
   <true/>
 
+  <!-- Server to server protocol -->
+  <key>Scheduling</key>
+  <dict>
+    <!--  CalDAV protocol options -->
+  	<key>CalDAV</key>
+  	<dict>
+  	  <key>EmailDomain</key>
+  	  <string></string>
+  	  <key>HTTPDomain</key>
+  	  <string></string>
+  	  <key>AddressPatterns</key>
+  	  <array>
+  	  </array>
+  	  <key>OldDraftCompatability</key>
+  	  <true/>
+  	</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 -->
+  <key>FreeBusyURL</key>
+  <dict>
+  	<key>Enabled</key>
+  	<true/>
+  	<key>Time Period</key>
+  	<integer>14</integer>
+  	<key>Anonymous Access</key>
+  	<false/>
+  </dict>
+
 </dict>
 </plist>

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

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

Modified: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/__init__.py	2008-08-11 22:41:16 UTC (rev 2804)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/__init__.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -34,6 +34,7 @@
     "dropbox",
     "extensions",
     "fileops",
+    "freebusyurl",
     "ical",
     "icaldav",
     "index",
@@ -46,8 +47,13 @@
     "resource",
     "root",
     "schedule",
+    "schedule_common",
+    "schedule_imip",
+    "servertoserver",
+    "servertoserverparser",
     "sql",
     "static",
+    "timezones",
 ]
 
 try:

Modified: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/caldavxml.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/caldavxml.py	2008-08-11 22:41:16 UTC (rev 2804)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/caldavxml.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -45,13 +45,21 @@
 
 caldav_namespace = "urn:ietf:params:xml:ns:caldav"
 
-caldav_compliance = (
+caldav_full_compliance = (
     "calendar-access",
     "calendar-schedule",
+    "calendar-auto-schedule",
     "calendar-availability",
     "inbox-availability",
 )
 
+caldav_implicit_compliance = (
+    "calendar-access",
+    "calendar-auto-schedule",
+    "calendar-availability",
+    "inbox-availability",
+)
+
 class CalDAVElement (davxml.WebDAVElement):
     """
     CalDAV XML element.
@@ -1558,15 +1566,47 @@
 class CalendarFreeBusySet (CalDAVElement):
     """
     The list of calendar URIs that contribute to free-busy for this principal's calendar user.
-    (CalDAV-schedule, section x.x.x)
+    This was defined in the old caldav scheduling spec but has been removed from the new one.
+    We still need to support it for backwards compatibility.
     """
     name = "calendar-free-busy-set"
     hidden = True
 
     allowed_children = { (davxml.dav_namespace, "href"): (0, None) }
 
-class ScheduleInboxURL (CalDAVTextElement):
+class ScheduleCalendarTransp (CalDAVElement):
     """
+    Indicates whether a calendar should be used for freebusy lookups.
+    """
+    name = "schedule-calendar-transp"
+
+    allowed_children = {
+        (caldav_namespace,     "opaque"      ): (0, 1),
+        (caldav_namespace,     "transparent" ): (0, 1),
+    }
+
+class Opaque (CalDAVEmptyElement):
+    """
+    Indicates that a calendar is used in freebusy lookups.
+    """
+    name = "opaque"
+
+class Transparent (CalDAVEmptyElement):
+    """
+    Indicates that a calendar is not used in freebusy lookups.
+    """
+    name = "transparent"
+
+class ScheduleDefaultCalendarURL (CalDAVElement):
+    """
+    A single href indicating which calendar is the default for scheduling.
+    """
+    name = "schedule-default-calendar-URL"
+
+    allowed_children = { (davxml.dav_namespace, "href"): (0, 1) }
+
+class ScheduleInboxURL (CalDAVElement):
+    """
     A principal property to indicate the schedule INBOX for the principal.
     (CalDAV-schedule, section x.x.x)
     """
@@ -1576,7 +1616,7 @@
 
     allowed_children = { (davxml.dav_namespace, "href"): (0, 1) }
 
-class ScheduleOutboxURL (CalDAVTextElement):
+class ScheduleOutboxURL (CalDAVElement):
     """
     A principal property to indicate the schedule OUTBOX for the principal.
     (CalDAV-schedule, section x.x.x)
@@ -1666,6 +1706,33 @@
     """
     name = "schedule"
     
+class ScheduleState (CalDAVElement):
+    """
+    Indicates whether a scheduling message in an inbox has been processed
+    by the server.
+    """
+    name = "schedule-state"
+    protected = True
+
+    allowed_children = {
+        (caldav_namespace,     "schedule-processed"   ): (0, 1),
+        (caldav_namespace,     "schedule-unprocessed" ): (0, 1),
+    }
+
+class ScheduleProcessed (CalDAVEmptyElement):
+    """
+    Indicates that a scheduling message in an inbox has been processed
+    by the server.
+    """
+    name = "schedule-processed"
+
+class ScheduleUnprocessed (CalDAVEmptyElement):
+    """
+    Indicates that a scheduling message in an inbox has not been processed
+    by the server.
+    """
+    name = "schedule-unprocessed"
+
 ##
 # Extensions to davxml.ResourceType
 ##

Modified: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/config.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/config.py	2008-08-11 22:41:16 UTC (rev 2804)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/config.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -175,7 +175,48 @@
     "EnablePrivateEvents"     : False, # Private Events
     "EnableTimezoneService"   : False, # Timezone service
     "EnableAutoAcceptTrigger" : False, # Manually trigger auto-accept behavior
+    
+    #
+    # 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
+            "OldDraftCompatability" : True   # Whether to maintain compatibility with non-implicit mode
+        },
+
+        "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
+        "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
+    },
+
     #
     # Notifications
     #
@@ -268,6 +309,9 @@
 
 
 class Config (object):
+    """
+    @DynamicAttrs
+    """
     def __init__(self, defaults):
         self.setDefaults(defaults)
         self._data = copy.deepcopy(self._defaults)

Modified: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/customxml.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/customxml.py	2008-08-11 22:41:16 UTC (rev 2804)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/customxml.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -253,7 +253,21 @@
     name = "xmpp-uri"
     protected = True
 
+class IScheduleInbox (davxml.WebDAVEmptyElement):
+    """
+    Denotes the resourcetype of a iSchedule Inbox.
+    (CalDAV-s2s-xx, section x.x.x)
+    """
+    namespace = calendarserver_namespace
+    name = "ischedule-inbox"
 
+class FreeBusyURL (davxml.WebDAVEmptyElement):
+    """
+    Denotes the resourcetype of a free-busy URL resource.
+    (CalDAV-s2s-xx, section x.x.x)
+    """
+    namespace = calendarserver_namespace
+    name = "free-busy-url"
 
 ##
 # Extensions to davxml.ResourceType
@@ -264,3 +278,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.ischeduleinbox = davxml.ResourceType(IScheduleInbox())
+davxml.ResourceType.freebusyurl = davxml.ResourceType(FreeBusyURL())

Modified: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/directory/calendar.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/directory/calendar.py	2008-08-11 22:41:16 UTC (rev 2804)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/directory/calendar.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -38,6 +38,7 @@
 from twistedcaldav.config import config
 from twistedcaldav.dropbox import DropBoxHomeResource
 from twistedcaldav.extensions import ReadOnlyResourceMixIn, DAVResource
+from twistedcaldav.freebusyurl import FreeBusyURLResource
 from twistedcaldav.resource import CalDAVResource
 from twistedcaldav.schedule import ScheduleInboxResource, ScheduleOutboxResource
 from twistedcaldav.directory.idirectory import IDirectoryService
@@ -268,6 +269,10 @@
             childlist += (
                 ("dropbox", DropBoxHomeResource),
             )
+        if config.FreeBusyURL["Enabled"]:
+            childlist += (
+                ("freebusy", FreeBusyURLResource),
+            )
         for name, cls in childlist:
             child = self.provisionChild(name)
             assert isinstance(child, cls), "Child %r is not a %s: %r" % (name, cls.__name__, child)
@@ -281,20 +286,24 @@
         child = self.provisionChild(childName)
         assert isinstance(child, CalDAVResource), "Child %r is not a %s: %r" % (childName, CalDAVResource.__name__, child)
 
-        def setupChild(_):
-            # Set calendar-free-busy-set on inbox
-            inbox = self.getChild("inbox")
+        def setupFreeBusy(_):
+            # Default calendar is initially opaque to freebusy
+            child.writeDeadProperty(caldavxml.ScheduleCalendarTransp(caldavxml.Opaque()))
+
             # FIXME: Shouldn't have to call provision() on another resource
             # We cheat here because while inbox will auto-provision itself when located,
             # we need to write a dead property to it pre-emptively.
-            # Possible fix: store the free/busy set property on this resource instead.
+            # This will go away once we remove the free-busy-set property on inbox.
+
+            # Set calendar-free-busy-set on inbox
+            inbox = self.getChild("inbox")
             inbox.provision()
             inbox.writeDeadProperty(caldavxml.CalendarFreeBusySet(davxml.HRef(childURL)))
 
             return self
 
         d = child.createCalendarCollection()
-        d.addCallback(setupChild)
+        d.addCallback(setupFreeBusy)
         return d
 
     def provisionChild(self, name):
@@ -323,6 +332,9 @@
     def owner(self, request):
         return succeed(davxml.HRef(self.principalForRecord().principalURL()))
 
+    def ownerPrincipal(self, request):
+        return succeed(self.principalForRecord())
+
     def defaultAccessControlList(self):
         myPrincipal = self.principalForRecord()
 

Modified: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/directory/principal.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/directory/principal.py	2008-08-11 22:41:16 UTC (rev 2804)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/directory/principal.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -32,9 +32,8 @@
 from urlparse import urlparse
 
 from twisted.python.failure import Failure
-from twisted.internet.defer import deferredGenerator
+from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.internet.defer import succeed
-from twisted.internet.defer import waitForDeferred
 from twisted.web2 import responsecode
 from twisted.web2.http import HTTPError
 from twisted.web2.dav import davxml
@@ -145,7 +144,7 @@
         return self.getChild(uidsResourceName).getChild(uid)
 
     def _principalForURI(self, uri):
-        scheme, netloc, path, params, query, fragment = urlparse(uri)
+        scheme, netloc, path, _ignore_params, _ignore_query, _ignore_fragment = urlparse(uri)
 
         if scheme == "":
             pass
@@ -411,22 +410,16 @@
     # HTTP
     ##
 
-    @deferredGenerator
+    @inlineCallbacks
     def renderDirectoryBody(self, request):
 
-        d = waitForDeferred(super(DirectoryPrincipalResource, self).renderDirectoryBody(request))
-        yield d
-        output = d.getResult()
+        output = (yield super(DirectoryPrincipalResource, self).renderDirectoryBody(request))
         
-        d = waitForDeferred(self.groupMembers())
-        yield d
-        members = d.getResult()
+        members = (yield self.groupMembers())
         
-        d = waitForDeferred(self.groupMemberships())
-        yield d
-        memberships = d.getResult()
+        memberships = (yield self.groupMemberships())
 
-        yield "".join((
+        returnValue("".join((
             """<div class="directory-listing">"""
             """<h1>Principal Details</h1>"""
             """<pre><blockquote>"""
@@ -448,7 +441,7 @@
             """\nGroup memberships:\n"""       , format_principals(memberships),
             """</pre></blockquote></div>""",
             output
-        ))
+        )))
 
     ##
     # DAV
@@ -502,7 +495,6 @@
 
         if record not in records:
             records.add(record)
-            myRecordType = self.record.recordType
             for relative in getattr(record, method)():
                 if relative not in records:
                     found = self.parent.principalForRecord(relative)
@@ -523,7 +515,7 @@
     def groupMembers(self):
         return succeed(self._getRelatives("members"))
 
-    @deferredGenerator
+    @inlineCallbacks
     def groupMemberships(self):
         groups = self._getRelatives("groups")
 
@@ -534,9 +526,7 @@
 
             # Get proxy group UIDs and map to principal resources
             proxies = []
-            d = waitForDeferred(self._calendar_user_proxy_index().getMemberships(self.principalUID()))
-            yield d
-            memberships = d.getResult()
+            memberships = (yield self._calendar_user_proxy_index().getMemberships(self.principalUID()))
             for uid in memberships:
                 subprincipal = self.parent.principalForUID(uid)
                 if subprincipal:
@@ -544,7 +534,7 @@
 
             groups.update(proxies)
 
-        yield groups
+        returnValue(groups)
 
     def principalCollections(self):
         return self.parent.principalCollections()
@@ -580,22 +570,16 @@
     """
     Directory calendar principal resource.
     """
-    @deferredGenerator
+    @inlineCallbacks
     def renderDirectoryBody(self, request):
 
-        d = waitForDeferred(super(DirectoryPrincipalResource, self).renderDirectoryBody(request))
-        yield d
-        output = d.getResult()
+        output = (yield super(DirectoryPrincipalResource, self).renderDirectoryBody(request))
         
-        d = waitForDeferred(self.groupMembers())
-        yield d
-        members = d.getResult()
+        members = (yield self.groupMembers())
         
-        d = waitForDeferred(self.groupMemberships())
-        yield d
-        memberships = d.getResult()
+        memberships = (yield self.groupMemberships())
         
-        yield "".join((
+        returnValue("".join((
             """<div class="directory-listing">"""
             """<h1>Principal Details</h1>"""
             """<pre><blockquote>"""
@@ -619,7 +603,7 @@
             """\nCalendar user addresses:\n""" , format_list(format_link(a) for a in self.calendarUserAddresses()),
             """</pre></blockquote></div>""",
             output
-        ))
+        )))
 
     ##
     # CalDAV
@@ -655,7 +639,7 @@
         return self.record.hasEditableProxyMembership()
 
     def scheduleInbox(self, request):
-        home = self._calendarHome()
+        home = self.calendarHome()
         if home is None:
             return succeed(None)
 
@@ -666,7 +650,7 @@
         return succeed(inbox)
 
     def calendarHomeURLs(self):
-        home = self._calendarHome()
+        home = self.calendarHome()
         if home is None:
             return ()
         else:
@@ -685,13 +669,13 @@
             return None
 
     def _homeChildURL(self, name):
-        home = self._calendarHome()
+        home = self.calendarHome()
         if home is None:
             return None
         else:
             return joinURL(home.url(), name)
 
-    def _calendarHome(self):
+    def calendarHome(self):
         # FIXME: self.record.service.calendarHomesCollection smells like a hack
         # See CalendarHomeProvisioningFile.__init__()
         service = self.record.service

Modified: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/extensions.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/extensions.py	2008-08-11 22:41:16 UTC (rev 2804)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/extensions.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -33,7 +33,7 @@
 import cgi
 import time
 
-from twisted.internet.defer import succeed, deferredGenerator, waitForDeferred, DeferredList
+from twisted.internet.defer import succeed, DeferredList, inlineCallbacks, returnValue
 from twisted.internet.defer import maybeDeferred
 from twisted.web2 import responsecode
 from twisted.web2.http import HTTPError, Response, RedirectResponse
@@ -118,6 +118,7 @@
 
         return super(SudoSACLMixin, self).findPrincipalForAuthID(authid)
 
+    @inlineCallbacks
     def authorizationPrincipal(self, request, authid, authnPrincipal):
         """
         Determine the authorization principal for the given request and authentication principal.
@@ -165,8 +166,7 @@
 
                     if authzPrincipal is not None:
                         log.msg("Allow proxy: user '%s' as '%s'" % (authid, authz,))
-                        yield authzPrincipal
-                        return
+                        returnValue(authzPrincipal)
                     else:
                         log.msg("Could not find authorization user id: '%s'" % 
                                 (authz,))
@@ -179,14 +179,10 @@
             raise HTTPError(responsecode.FORBIDDEN)
         else:
             # No proxy - do default behavior
-            d = waitForDeferred(super(SudoSACLMixin, self).authorizationPrincipal(request, authid, authnPrincipal))
-            yield d
-            yield d.getResult()
-            return
+            result = (yield super(SudoSACLMixin, self).authorizationPrincipal(request, authid, authnPrincipal))
+            returnValue(result)
 
-    authorizationPrincipal = deferredGenerator(authorizationPrincipal)
 
-
 def updateCacheTokenOnCallback(f):
     def fun(self, *args, **kwargs):
         def _updateToken(response):
@@ -208,6 +204,12 @@
     Extended L{twisted.web2.dav.resource.DAVResource} implementation.
     """
 
+    def renderHTTP(self, request):
+        
+        log.info("%s %s %s" % (request.method, urllib.unquote(request.uri), "HTTP/%s.%s" % request.clientproto))
+
+        return super(DAVResource, self).renderHTTP(request)
+
     @updateCacheTokenOnCallback
     def http_PROPPATCH(self, request):
         return super(DAVResource, self).http_PROPPATCH(request)
@@ -222,7 +224,8 @@
     def http_ACL(self, request):
         return super(DAVResource, self).http_ACL(request)
 
-
+    
+    @inlineCallbacks
     def findChildrenFaster(self, depth, request, okcallback, badcallback, names, privileges, inherited_aces):
         """
         See L{IDAVResource.findChildren}.
@@ -244,14 +247,11 @@
         assert depth in ("0", "1", "infinity"), "Invalid depth: %s" % (depth,)
 
         if depth == "0" or not self.isCollection():
-            yield None
-            return
+            returnValue(None)
 
         # First find all depth 1 children
         #children = []
-        #d = waitForDeferred(self.findChildren("1", request, lambda x, y: children.append((x, y)), privileges=None, inherited_aces=None))
-        #yield d
-        #d.getResult()
+        #yield self.findChildren("1", request, lambda x, y: children.append((x, y)), privileges=None, inherited_aces=None)
 
         children = []
         basepath = request.urlForResource(self)
@@ -260,9 +260,7 @@
             if names and childname not in names:
                 continue
             childpath = joinURL(basepath, childname)
-            d = waitForDeferred(request.locateChildResource(self, childname))
-            yield d
-            child = d.getResult()
+            child = (yield request.locateChildResource(self, childname))
             if child is None:
                 children.append((None, childpath + "/"))
             else:
@@ -274,21 +272,15 @@
         # Generate (acl,supported_privs) map
         aclmap = {}
         for resource, url in children:
-            acl = waitForDeferred(resource.accessControlList(request, inheritance=False, inherited_aces=inherited_aces))
-            yield acl
-            acl = acl.getResult()
-            supportedPrivs = waitForDeferred(resource.supportedPrivileges(request))
-            yield supportedPrivs
-            supportedPrivs = supportedPrivs.getResult()
+            acl = (yield resource.accessControlList(request, inheritance=False, inherited_aces=inherited_aces))
+            supportedPrivs = (yield resource.supportedPrivileges(request))
             aclmap.setdefault((pickle.dumps(acl), supportedPrivs), (acl, supportedPrivs, []))[2].append((resource, url))           
 
         # Now determine whether each ace satisfies privileges
         #print aclmap
         allowed_collections = []
         for items in aclmap.itervalues():
-            checked = waitForDeferred(self.checkACLPrivilege(request, items[0], items[1], privileges, inherited_aces))
-            yield checked
-            checked = checked.getResult()
+            checked = (yield self.checkACLPrivilege(request, items[0], items[1], privileges, inherited_aces))
             if checked:
                 for resource, url in items[2]:
                     if okcallback:
@@ -303,22 +295,16 @@
         # TODO: Depth: infinity support
         if depth == "infinity":
             for collection, url in allowed_collections:
-                collection_inherited_aces = waitForDeferred(collection.inheritedACEsforChildren(request))
-                yield collection_inherited_aces
-                collection_inherited_aces = collection_inherited_aces.getResult()
-                d = waitForDeferred(collection.findChildrenFaster(depth, request, okcallback, badcallback, names, privileges, inherited_aces=collection_inherited_aces))
-                yield d
-                d.getResult()
+                collection_inherited_aces = (yield collection.inheritedACEsforChildren(request))
+                yield collection.findChildrenFaster(depth, request, okcallback, badcallback, names, privileges, inherited_aces=collection_inherited_aces)
                 
-        yield None
-  
-    findChildrenFaster = deferredGenerator(findChildrenFaster)
+        returnValue(None)
 
+    @inlineCallbacks
     def checkACLPrivilege(self, request, acl, privyset, privileges, inherited_aces):
         
         if acl is None:
-            yield False
-            return
+            returnValue(False)
 
         principal = self.currentPrincipal(request)
 
@@ -338,9 +324,7 @@
                 if not self.matchPrivilege(davxml.Privilege(privilege), ace.privileges, privyset):
                     continue
 
-                match = waitForDeferred(self.matchPrincipal(principal, ace.principal, request))
-                yield match
-                match = match.getResult()
+                match = (yield self.matchPrincipal(principal, ace.principal, request))
 
                 if match:
                     if ace.invert:
@@ -354,10 +338,8 @@
                 if not ace.allow:
                     denied.append(privilege)
 
-        yield len(denied) + len(pending) == 0
+        returnValue(len(denied) + len(pending) == 0)
 
-    checkACLPrivilege = deferredGenerator(checkACLPrivilege)
-
     def fullAccessControlList(self, acl, inherited_aces):
         """
         See L{IDAVResource.accessControlList}.
@@ -395,7 +377,7 @@
 
         return acl
     
-    @deferredGenerator
+    @inlineCallbacks
     def matchPrincipal(self, principal1, principal2, request):
         """
         Implementation of DAVResource.matchPrincipal that caches the principal match
@@ -412,12 +394,10 @@
 
         match = request.matchPrincipalCache.get(cache_key, None)
         if match is None:
-            match = waitForDeferred(super(DAVResource, self).matchPrincipal(principal1, principal2, request))
-            yield match
-            match = match.getResult()
+            match = (yield super(DAVResource, self).matchPrincipal(principal1, principal2, request))
             request.matchPrincipalCache[cache_key] = match
             
-        yield match
+        returnValue(match)
 
 class DAVPrincipalResource (SuperDAVPrincipalResource, LoggingMixIn):
     """

Modified: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/fileops.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/fileops.py	2008-08-11 22:41:16 UTC (rev 2804)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/fileops.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -13,15 +13,14 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 ##
-from twisted.internet.defer import deferredGenerator
-from twisted.web2.dav.fileop import put
-from twisted.internet.defer import waitForDeferred
-from twisted.web2.dav.fileop import copy
 
 """
 Various file utilities.
 """
 
+from twisted.internet.defer import inlineCallbacks, returnValue
+from twisted.web2.dav.fileop import copy
+from twisted.web2.dav.fileop import put
 from twisted.web2.dav.xattrprops import xattrPropertyStore
 
 # This class simulates a DAVFile with enough information for use with xattrPropertyStore.
@@ -30,7 +29,7 @@
     def __init__(self, fp):
         self.fp = fp
 
- at deferredGenerator
+ at inlineCallbacks
 def putWithXAttrs(stream, filepath):
     """
     Write a file to a possibly existing path and preserve any xattrs at that path.
@@ -50,9 +49,7 @@
         xold = None
     
     # First do the actual file copy
-    d = waitForDeferred(put(stream, filepath))
-    yield d
-    response = d.getResult()
+    response = (yield put(stream, filepath))
 
     # Restore original xattrs.
     if props:
@@ -61,9 +58,9 @@
             xnew.set(prop)
         xnew = None
 
-    yield response
+    returnValue(response)
 
- at deferredGenerator
+ at inlineCallbacks
 def copyWithXAttrs(source_filepath, destination_filepath, destination_uri):
     """
     Copy a file from one path to another and also copy xattrs we care about.
@@ -77,14 +74,12 @@
     """
     
     # First do the actual file copy
-    d = waitForDeferred(copy(source_filepath, destination_filepath, destination_uri, "0"))
-    yield d
-    response = d.getResult()
+    response = (yield copy(source_filepath, destination_filepath, destination_uri, "0"))
 
     # Now copy over xattrs.
     copyXAttrs(source_filepath, destination_filepath)
     
-    yield response
+    returnValue(response)
 
 def copyToWithXAttrs(from_fp, to_fp):
     """

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

Modified: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/ical.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/ical.py	2008-08-11 22:41:16 UTC (rev 2804)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/ical.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -29,26 +29,17 @@
     "parse_duration",
 ]
 
-import datetime
-import cStringIO as StringIO
-
-from vobject import newFromBehavior, readComponents
-from vobject.base import Component as vComponent
-from vobject.base import ContentLine as vContentLine
-from vobject.base import ParseError as vParseError
-from vobject.icalendar import TimezoneComponent
-from vobject.icalendar import dateTimeToString
-from vobject.icalendar import deltaToOffset
-from vobject.icalendar import getTransition
-from vobject.icalendar import stringToDate, stringToDateTime, stringToDurations
-from vobject.icalendar import utc
-
+from twisted.web2.dav.util import allDataFromStream
 from twisted.web2.stream import IStream
-from twisted.web2.dav.util import allDataFromStream
-
 from twistedcaldav.dateops import compareDateTime, normalizeToUTC, timeRangesOverlap
 from twistedcaldav.instance import InstanceList
 from twistedcaldav.log import Logger
+from vobject import newFromBehavior, readComponents
+from vobject.base import Component as vComponent, ContentLine as vContentLine, ParseError as vParseError
+from vobject.icalendar import TimezoneComponent, dateTimeToString, deltaToOffset, getTransition, stringToDate, stringToDateTime, stringToDurations, utc
+import cStringIO as StringIO
+import datetime
+import heapq
 
 log = Logger()
 
@@ -97,7 +88,7 @@
     def __ne__(self, other): return not self.__eq__(other)
     def __eq__(self, other):
         if not isinstance(other, Property): return False
-        return self.name() == other.name() and self.value() == other.value()
+        return self.name() == other.name() and self.value() == other.value() and self.params() == other.params()
 
     def __gt__(self, other): return not (self.__eq__(other) or self.__lt__(other))
     def __lt__(self, other):
@@ -346,7 +337,8 @@
         @return: the L{Component} of the primary type.
         @raise: L{ValueError} if there is more than one primary type.
         """
-        assert self.name() == "VCALENDAR", "Must be a VCALENDAR: %r" % (self,)
+        if self.name() != "VCALENDAR":
+            assert self.name() == "VCALENDAR", "Must be a VCALENDAR: %r" % (self,)
         
         result = None
         for component in self.subcomponents():
@@ -716,7 +708,7 @@
         newcomp = instance.component.duplicate()
  
         # Strip out unwanted recurrence properties
-        for property in newcomp.properties():
+        for property in tuple(newcomp.properties()):
             if property.name() in ["RRULE", "RDATE", "EXRULE", "EXDATE", "RECURRENCE-ID"]:
                 newcomp.removeProperty(property)
         
@@ -771,6 +763,41 @@
         instances.expandTimeRanges(componentSet, limit)
         return instances
 
+    def deriveInstance(self, rid):
+        """
+        Derive an instance from the master component that has the provided RECURRENCE-ID, but
+        with all other properties, components etc from the master.
+
+        @param rid: recurrence-id value
+        @type rid: L{datetime.datetime}
+        """
+        
+        # Must have a master component
+        master = self.masterComponent()
+        if master is None:
+            return None
+
+        # TODO: Check that the recurrence-id is a valid instance
+        
+        # Create the derived instance
+        newcomp = master.duplicate()
+
+        # Strip out unwanted recurrence properties
+        for property in tuple(newcomp.properties()):
+            if property.name() in ["RRULE", "RDATE", "EXRULE", "EXDATE", "RECURRENCE-ID"]:
+                newcomp.removeProperty(property)
+        
+        # Adjust times
+        offset = rid - newcomp.getStartDateUTC()
+        dtstart = newcomp.getProperty("DTSTART")
+        dtstart.setValue(dtstart.value() + offset)
+        if newcomp.hasProperty("DTEND"):
+            dtend = newcomp.getProperty("DTEND")
+            dtend.setValue(dtend.value() + offset)
+        newcomp.addProperty(Property("RECURRENCE-ID", dtstart.value()))
+            
+        return newcomp
+        
     def resourceUID(self):
         """
         @return: the UID of the subcomponents in this component.
@@ -998,6 +1025,32 @@
 
         return None
 
+    def getOrganizersByInstance(self):
+        """
+        Get the organizer value for each instance.
+        
+        @return: a list of tuples of (organizer value, recurrence-id)
+        """
+        
+        # Extract appropriate sub-component if this is a VCALENDAR
+        if self.name() == "VCALENDAR":
+            result = ()
+            for component in self.subcomponents():
+                if component.name() != "VTIMEZONE":
+                    result += component.getOrganizersByInstance()
+            return result
+        else:
+            try:
+                # Should be just one ORGANIZER
+                org = self.propertyValue("ORGANIZER")
+                rid = self.getRecurrenceIDUTC()
+                if org:
+                    return ((org, rid),)
+            except ValueError:
+                pass
+
+        return ()
+
     def getOrganizerProperty(self):
         """
         Get the organizer value. Works on either a VCALENDAR or on a component.
@@ -1038,6 +1091,27 @@
 
         return None
 
+    def getAttendeesByInstance(self):
+        """
+        Get the organizer value for each instance.
+        
+        @return: a list of tuples of (organizer value, recurrence-id)
+        """
+        
+        # Extract appropriate sub-component if this is a VCALENDAR
+        if self.name() == "VCALENDAR":
+            result = ()
+            for component in self.subcomponents():
+                if component.name() != "VTIMEZONE":
+                    result += component.getAttendeesByInstance()
+            return result
+        else:
+            result = ()
+            rid = self.getRecurrenceIDUTC()
+            for attendee in self.properties("ATTENDEE"):
+                result += ((attendee.value(), rid),)
+            return result
+
     def getAttendeeProperty(self, match):
         """
         Get the attendees matching a value. Works on either a VCALENDAR or on a component.
@@ -1113,6 +1187,99 @@
 
         return None
 
+    def attendeesView(self, attendees):
+        """
+        Filter out any components that all attendees are not present in. Use EXDATEs
+        on the master to account for changes.
+        """
+
+        assert self.name() == "VCALENDAR", "Not a calendar: %r" % (self,)
+            
+        # Modify any components that reference the attendee, make note of the ones that don't
+        remove_components = []
+        master_component = None
+        removed_master = False
+        for component in self.subcomponents():
+            if component.name() == "VTIMEZONE":
+                continue
+            found_all_attendees = True
+            for attendee in attendees:
+                if component.getAttendeeProperty((attendee,)) is None:
+                    found_all_attendees = False
+                    break
+            if not found_all_attendees:
+                remove_components.append(component)
+            if component.getRecurrenceIDUTC() is None:
+                master_component = component
+                if not found_all_attendees:
+                    removed_master = True
+                
+        # Now remove the unwanted components - but we may need to exdate the master
+        exdates = []
+        for component in remove_components:
+            rid = component.getRecurrenceIDUTC()
+            if rid is not None:
+                exdates.append(rid)
+            self.removeComponent(component)
+            
+        if not removed_master and master_component is not None:
+            for exdate in exdates:
+                master_component.addProperty(Property("EXDATE", (exdate,)))
+        
+    def removeAllButOneAttendee(self, attendee):
+        """
+        Remove all ATTENDEE properties except for the one specified.
+        """
+
+        assert self.name() == "VCALENDAR", "Not a calendar: %r" % (self,)
+
+        if self.name() == "VCALENDAR":
+            for component in self.subcomponents():
+                if component.name() == "VTIMEZONE":
+                    continue
+                [component.removeProperty(p) for p in tuple(component.properties("ATTENDEE")) if p.value() != attendee]
+            
+    def removeAlarms(self):
+        """
+        Remove all Alarms components
+        """
+
+        if self.name() == "VCALENDAR":
+            for component in self.subcomponents():
+                if component.name() == "VTIMEZONE":
+                    continue
+                component.removeAlarms()
+        else:
+            for component in tuple(self.subcomponents()):
+                if component.name() == "VALARM":
+                    self.removeComponent(component)
+                
+    def removeUnwantedProperties(self, keep_properties):
+        """
+        Remove all properties that do not match the provided set.
+        """
+
+        assert self.name() == "VCALENDAR", "Not a calendar: %r" % (self,)
+
+        if self.name() == "VCALENDAR":
+            for component in self.subcomponents():
+                if component.name() == "VTIMEZONE":
+                    continue
+                [component.removeProperty(p) for p in tuple(component.properties()) if p.name() not in keep_properties]
+                
+    def removeXProperties(self):
+        """
+        Remove all X- properties.
+        """
+
+        assert self.name() == "VCALENDAR", "Not a calendar: %r" % (self,)
+
+        if self.name() == "VCALENDAR":
+            for component in self.subcomponents():
+                if component.name() == "VTIMEZONE":
+                    continue
+                [component.removeProperty(p) for p in tuple(component.properties()) if p.name().startswith("X-")]
+            
 ##
 # Dates and date-times
 ##
@@ -1277,9 +1444,8 @@
 
 #
 # This function is from "Python Cookbook, 2d Ed., by Alex Martelli, Anna
-# Martelli Ravenscroft, and Davis Ascher (O'Reilly Media, 2005) 0-596-00797-3."
+# Martelli Ravenscroft, and David Ascher (O'Reilly Media, 2005) 0-596-00797-3."
 #
-import heapq
 def merge(*iterables):
     """
     Merge sorted iterables into one sorted iterable.

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

Modified: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/log.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/log.py	2008-08-11 22:41:16 UTC (rev 2804)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/log.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -59,6 +59,14 @@
 
 from twisted.python import log
 
+from StringIO import StringIO
+
+from twisted.internet.defer import inlineCallbacks
+
+from twisted.web2 import responsecode
+from twisted.web2.dav.util import allDataFromStream
+from twisted.web2.stream import MemoryStream
+
 logLevels = (
     "debug",
     "info",
@@ -78,7 +86,7 @@
     return sorted(levels, cmpLogLevels, reverse=True)[0]
 
 ##
-# Tools for manageing log levels
+# Tools for managing log levels
 ##
 
 def logLevelForNamespace(namespace):
@@ -193,6 +201,75 @@
         """
         return cmpLogLevels(self.level(), level) <= 0
 
+    @inlineCallbacks
+    def logRequest(self, level, message, request, **kwargs):
+        """
+        Log an HTTP request.
+        """
+
+        assert level in logLevels
+
+        if self.willLogAtLevel(level):
+            iostr = StringIO()
+            iostr.write("%s\n" % (message,))
+            if hasattr(request, "clientproto"):
+                protocol = "HTTP/%d.%d" % (request.clientproto[0], request.clientproto[1],)
+            else:
+                protocol = "HTTP/1.1"
+            iostr.write("%s %s %s\n" % (request.method, request.uri, protocol,))
+            for name, valuelist in request.headers.getAllRawHeaders():
+                for value in valuelist:
+                    # Do not log authorization details
+                    if name not in ("Authorization",):
+                        iostr.write("%s: %s\n" % (name, value))
+                    else:
+                        iostr.write("%s: xxxxxxxxx\n" % (name,))
+            iostr.write("\n")
+            
+            # We need to play a trick with the request stream as we can only read it once. So we
+            # read it, store the value in a MemoryStream, and replace the request's stream with that,
+            # so the data can be read again.
+            data = yield allDataFromStream(request.stream)
+            iostr.write(data)
+            
+            request.stream = MemoryStream(data)
+            request.stream.doStartReading = None
+        
+            self.emit(level, iostr.getvalue(), **kwargs)
+    
+    @inlineCallbacks
+    def logResponse(self, level, message, response, **kwargs):
+        """
+        Log an HTTP request.
+        """
+
+        assert level in logLevels
+
+        if self.willLogAtLevel(level):
+            iostr = StringIO()
+            iostr.write("%s\n" % (message,))
+            code_message = responsecode.RESPONSES.get(response.code, "Unknown Status")
+            iostr.write("HTTP/1.1 %s %s\n" % (response.code, code_message,))
+            for name, valuelist in response.headers.getAllRawHeaders():
+                for value in valuelist:
+                    # Do not log authorization details
+                    if name not in ("WWW-Authenticate",):
+                        iostr.write("%s: %s\n" % (name, value))
+                    else:
+                        iostr.write("%s: xxxxxxxxx\n" % (name,))
+            iostr.write("\n")
+            
+            # We need to play a trick with the response stream to ensure we don't mess it up. So we
+            # read it, store the value in a MemoryStream, and replace the response's stream with that,
+            # so the data can be read again.
+            data = yield allDataFromStream(response.stream)
+            iostr.write(data)
+            
+            response.stream = MemoryStream(data)
+            response.stream.doStartReading = None
+        
+            self.emit(level, iostr.getvalue(), **kwargs)
+
 class LoggingMixIn (object):
     """
     Mix-in class for logging methods.

Modified: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/copymove.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/copymove.py	2008-08-11 22:41:16 UTC (rev 2804)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/copymove.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -22,7 +22,7 @@
 
 from urlparse import urlsplit
 
-from twisted.internet.defer import deferredGenerator, waitForDeferred
+from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.web2 import responsecode
 from twisted.web2.filter.location import addLocation
 from twisted.web2.dav import davxml
@@ -31,12 +31,14 @@
 from twisted.web2.http import StatusResponse, HTTPError
 
 from twistedcaldav.caldavxml import caldav_namespace
-from twistedcaldav.method.put_common import storeCalendarObjectResource
-from twistedcaldav.resource import isCalendarCollectionResource
+from twistedcaldav.method.put_common import StoreCalendarObjectResource
+from twistedcaldav.resource import isCalendarCollectionResource,\
+    isPseudoCalendarCollectionResource
 from twistedcaldav.log import Logger
 
 log = Logger()
 
+ at inlineCallbacks
 def http_COPY(self, request):
     """
     Special handling of COPY request if parents are calendar collections.
@@ -45,35 +47,22 @@
     the destination if its a calendar collection.
     """
 
-    r = waitForDeferred(checkForCalendarAction(self, request))
-    yield r
-    result, sourcecal, sourceparent, destination_uri, destination, destinationcal, destinationparent = r.getResult()
+    result, sourcecal, sourceparent, destination_uri, destination, destinationcal, destinationparent = (yield checkForCalendarAction(self, request))
     if not result or not destinationcal:
         # Do default WebDAV action
-        d = waitForDeferred(super(CalDAVFile, self).http_COPY(request))
-        yield d
-        yield d.getResult()
-        return
+        result = (yield super(CalDAVFile, self).http_COPY(request))
+        returnValue(result)
 
     #
     # Check authentication and access controls
     #
-    x = waitForDeferred(self.authorize(request, (davxml.Read(),), recurse=True))
-    yield x
-    x.getResult()
+    yield self.authorize(request, (davxml.Read(),), recurse=True)
 
     if destination.exists():
-        x = waitForDeferred(destination.authorize(request, (davxml.WriteContent(), davxml.WriteProperties()), recurse=True))
-        yield x
-        x.getResult()
+        yield destination.authorize(request, (davxml.WriteContent(), davxml.WriteProperties()), recurse=True)
     else:
-        destparent = waitForDeferred(request.locateResource(parentForURL(destination_uri)))
-        yield destparent
-        destparent = destparent.getResult()
- 
-        x = waitForDeferred(destparent.authorize(request, (davxml.Bind(),)))
-        yield x
-        x.getResult()
+        destparent = (yield request.locateResource(parentForURL(destination_uri)))
+        yield destparent.authorize(request, (davxml.Bind(),))
 
     # Check for existing destination resource
     overwrite = request.headers.getHeader("overwrite", True)
@@ -101,7 +90,7 @@
     # May need to add a location header
     addLocation(request, destination_uri)
 
-    x = waitForDeferred(storeCalendarObjectResource(
+    storer = StoreCalendarObjectResource(
         request = request,
         source = self,
         source_uri = request.uri,
@@ -111,12 +100,11 @@
         destination_uri = destination_uri,
         destinationparent = destinationparent,
         destinationcal = destinationcal,
-    ))
-    yield x
-    yield x.getResult()
+    )
+    result = (yield storer.run())
+    returnValue(result)
 
-http_COPY = deferredGenerator(http_COPY)
-
+ at inlineCallbacks
 def http_MOVE(self, request):
     """
     Special handling of MOVE request if parent is a calendar collection.
@@ -124,40 +112,31 @@
     since its effectively being deleted. We do need to do an index update for
     the destination if its a calendar collection
     """
-    r = waitForDeferred(checkForCalendarAction(self, request))
-    yield r
-    result, sourcecal, sourceparent, destination_uri, destination, destinationcal, destinationparent = r.getResult()
+    result, sourcecal, sourceparent, destination_uri, destination, destinationcal, destinationparent = (yield checkForCalendarAction(self, request))
     if not result:
+        is_calendar_collection = isPseudoCalendarCollectionResource(self)
+
         # Do default WebDAV action
-        d = waitForDeferred(super(CalDAVFile, self).http_MOVE(request))
-        yield d
-        yield d.getResult()
-        return
+        result = (yield super(CalDAVFile, self).http_MOVE(request))
         
+        if is_calendar_collection:
+            # Do some clean up
+            yield self.movedCalendar(request, destination, destination_uri)
+
+        returnValue(result)
+        
     #
     # Check authentication and access controls
     #
-    parent = waitForDeferred(request.locateResource(parentForURL(request.uri)))
-    yield parent
-    parent = parent.getResult()
+    parent = (yield request.locateResource(parentForURL(request.uri)))
+    yield parent.authorize(request, (davxml.Unbind(),))
 
-    x = waitForDeferred(parent.authorize(request, (davxml.Unbind(),)))
-    yield x
-    x.getResult()
-
     if destination.exists():
-        x = waitForDeferred(destination.authorize(request, (davxml.Bind(), davxml.Unbind()), recurse=True))
-        yield x
-        x.getResult()
+        yield destination.authorize(request, (davxml.Bind(), davxml.Unbind()), recurse=True)
     else:
-        destparent = waitForDeferred(request.locateResource(parentForURL(destination_uri)))
-        yield destparent
-        destparent = destparent.getResult()
+        destparent = (yield request.locateResource(parentForURL(destination_uri)))
+        yield destparent.authorize(request, (davxml.Bind(),))
 
-        x = waitForDeferred(destparent.authorize(request, (davxml.Bind(),)))
-        yield x
-        x.getResult()
-
     # Check for existing destination resource
     overwrite = request.headers.getHeader("overwrite", True)
     if destination.exists() and not overwrite:
@@ -185,23 +164,22 @@
     # May need to add a location header
     addLocation(request, destination_uri)
 
-    x = waitForDeferred(storeCalendarObjectResource(
+    storer = StoreCalendarObjectResource(
         request = request,
         source = self,
         source_uri = request.uri,
         sourceparent = sourceparent,
         sourcecal = sourcecal,
+        deletesource = True,
         destination = destination,
         destination_uri = destination_uri,
         destinationparent = destinationparent,
         destinationcal = destinationcal,
-        deletesource = True,
-    ))
-    yield x
-    yield x.getResult()
+    )
+    result = (yield storer.run())
+    returnValue(result)
 
-http_MOVE = deferredGenerator(http_MOVE)
-
+ at inlineCallbacks
 def checkForCalendarAction(self, request):
     """
     Check to see whether the source or destination of the copy/move
@@ -209,15 +187,15 @@
     if that is the case.
     @return: tuple::
         result:           True if special CalDAV processing required, False otherwise
-                          NB If there is any type of error with the request, return False
-                          and allow normal COPY/MOVE processing to return the error.
+            NB If there is any type of error with the request, return False
+            and allow normal COPY/MOVE processing to return the error.
         sourcecal:        True if source is in a calendar collection, False otherwise
         sourceparent:     The parent resource for the source
         destination_uri:  The URI of the destination resource
         destination:      CalDAVFile of destination if special proccesing required,
         None otherwise
         destinationcal:   True if the destination is in a calendar collection,
-                          False otherwise
+            False otherwise
         destinationparent:The parent resource for the destination
         
     """
@@ -235,9 +213,7 @@
         ))
 
     # Check for parent calendar collection
-    sourceparent = waitForDeferred(request.locateResource(parentForURL(request.uri)))
-    yield sourceparent
-    sourceparent = sourceparent.getResult()
+    sourceparent = (yield request.locateResource(parentForURL(request.uri)))
     if isCalendarCollectionResource(sourceparent):
         result = True
         sourcecal = True
@@ -252,19 +228,13 @@
         log.err(msg)
         raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, msg))
     
-    destination = waitForDeferred(request.locateResource(destination_uri))
-    yield destination
-    destination = destination.getResult()
+    destination = (yield request.locateResource(destination_uri))
 
     # Check for parent calendar collection
     destination_uri = urlsplit(destination_uri)[2]
-    destinationparent = waitForDeferred(request.locateResource(parentForURL(destination_uri)))
-    yield destinationparent
-    destinationparent = destinationparent.getResult()
+    destinationparent = (yield request.locateResource(parentForURL(destination_uri)))
     if isCalendarCollectionResource(destinationparent):
         result = True
         destinationcal = True
 
-    yield (result, sourcecal, sourceparent, destination_uri, destination, destinationcal, destinationparent)
-
-checkForCalendarAction = deferredGenerator(checkForCalendarAction)
+    returnValue((result, sourcecal, sourceparent, destination_uri, destination, destinationcal, destinationparent))

Modified: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/delete.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/delete.py	2008-08-11 22:41:16 UTC (rev 2804)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/delete.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -20,35 +20,55 @@
 
 __all__ = ["http_DELETE"]
 
+from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.web2 import responsecode
 from twisted.web2.dav.util import parentForURL
 
-from twistedcaldav.resource import isPseudoCalendarCollectionResource
+from twistedcaldav.resource import isPseudoCalendarCollectionResource,\
+    isCalendarCollectionResource
+from twistedcaldav.scheduling.implicit import ImplicitScheduler
 
+ at inlineCallbacks
 def http_DELETE(self, request):
     #
     # Override base DELETE request handling to ensure that the calendar
     # index file has the entry for the deleted calendar component removed.
     #
-    def gotParent(parent):
-        def gotResponse(response):
-            if response == responsecode.NO_CONTENT:
-                if isPseudoCalendarCollectionResource(parent):
-                    index = parent.index()
-                    index.deleteResource(self.fp.basename())
 
-                    # Change CTag on the parent calendar collection
-                    d = parent.updateCTag()
-                    d.addCallback(lambda _: response)
-                    return d
+    # TODO: need to use transaction based delete on live scheduling object resources
+    # as the iTIP operation may fail and may need to prevent the delete from happening.
 
-            return response
+    parentURL = parentForURL(request.uri)
+    parent = (yield request.locateResource(parentURL))
 
-        d = super(CalDAVFile, self).http_DELETE(request)
-        d.addCallback(gotResponse)
-        return d
+    calendar = None
+    is_calendar_collection = False
+    is_calendar_resource = False
+    if self.exists():
+        if isCalendarCollectionResource(parent):
+            is_calendar_resource = True
+            calendar = self.iCalendar()
+        elif isCalendarCollectionResource(self):
+            is_calendar_collection = True
 
-    parentURL = parentForURL(request.uri)
-    d = request.locateResource(parentURL)
-    d.addCallback(gotParent)
-    return d
+    response = (yield super(CalDAVFile, self).http_DELETE(request))
+
+    if response == responsecode.NO_CONTENT:
+        if is_calendar_resource:
+
+            index = parent.index()
+            index.deleteResource(self.fp.basename())
+
+            # Change CTag on the parent calendar collection
+            yield parent.updateCTag()
+
+            # Do scheduling
+            scheduler = ImplicitScheduler()
+            yield scheduler.doImplicitScheduling(request, self, calendar, True)
+ 
+        elif is_calendar_collection:
+            
+            # Do some clean up
+            yield self.deletedCalendar(request)
+
+    returnValue(response)

Modified: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/get.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/get.py	2008-08-11 22:41:16 UTC (rev 2804)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/get.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -47,7 +47,7 @@
             yield self.authorize(request, (davxml.Read(),))
 
             # Non DAV:owner's have limited access to the data
-            isowner = yield self.isOwner(request)
+            isowner = (yield self.isOwner(request))
             
             if not isowner:
                 # Now "filter" the resource calendar data through the CALDAV:calendar-data element and apply
@@ -60,5 +60,5 @@
                 returnValue(response)
 
     # Do normal GET behavior
-    response = yield super(CalDAVFile, self).http_GET(request)
+    response = (yield super(CalDAVFile, self).http_GET(request))
     returnValue(response)

Modified: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/mkcalendar.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/mkcalendar.py	2008-08-11 22:41:16 UTC (rev 2804)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/mkcalendar.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -20,7 +20,7 @@
 
 __all__ = ["http_MKCALENDAR"]
 
-from twisted.internet.defer import deferredGenerator, waitForDeferred
+from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.python.failure import Failure
 from twisted.web2 import responsecode
 from twisted.web2.dav import davxml
@@ -34,6 +34,7 @@
 
 log = Logger()
 
+ at inlineCallbacks
 def http_MKCALENDAR(self, request):
     """
     Respond to a MKCALENDAR request.
@@ -43,14 +44,9 @@
     #
     # Check authentication and access controls
     #
-    parent = waitForDeferred(request.locateResource(parentForURL(request.uri)))
-    yield parent
-    parent = parent.getResult()
+    parent = (yield request.locateResource(parentForURL(request.uri)))
+    yield parent.authorize(request, (davxml.Bind(),))
 
-    x = waitForDeferred(parent.authorize(request, (davxml.Bind(),)))
-    yield x
-    x.getResult()
-
     if self.exists():
         log.err("Attempt to create collection where file exists: %s"
                 % (self.fp.path,))
@@ -71,13 +67,8 @@
     # Read request body
     #
     try:
-        doc = waitForDeferred(davXMLFromStream(request.stream))
-        yield doc
-        doc = doc.getResult()
-
-        result = waitForDeferred(self.createCalendar(request))
-        yield result
-        result = result.getResult()
+        doc = (yield davXMLFromStream(request.stream))
+        yield self.createCalendar(request)
     except ValueError, e:
         log.err("Error while handling MKCALENDAR: %s" % (e,))
         raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, str(e)))
@@ -103,9 +94,7 @@
                     if property.qname() == (caldavxml.caldav_namespace, "supported-calendar-component-set"):
                         self.writeDeadProperty(property)
                     else:
-                        p = waitForDeferred(self.writeProperty(property, request))
-                        yield p
-                        p.getResult()
+                        yield self.writeProperty(property, request)
                 except HTTPError:
                     errors.add(Failure(), property)
                     got_an_error = True
@@ -119,6 +108,4 @@
             errors.error()
             raise HTTPError(MultiStatusResponse([errors.response()]))
 
-    yield responsecode.CREATED
-
-http_MKCALENDAR = deferredGenerator(http_MKCALENDAR)
+    returnValue(responsecode.CREATED)

Modified: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/propfind.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/propfind.py	2008-08-11 22:41:16 UTC (rev 2804)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/propfind.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -28,7 +28,7 @@
 __all__ = ["http_PROPFIND"]
 
 from twisted.python.failure import Failure
-from twisted.internet.defer import deferredGenerator, waitForDeferred
+from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.web2.http import HTTPError
 from twisted.web2 import responsecode
 from twisted.web2.http import StatusResponse
@@ -45,6 +45,7 @@
 findChildrenFaster method to optimize child privilege checking.
 """
 
+ at inlineCallbacks
 def http_PROPFIND(self, request):
     """
     Respond to a PROPFIND request. (RFC 2518, section 8.1)
@@ -56,17 +57,13 @@
     #
     # Check authentication and access controls
     #
-    x = waitForDeferred(self.authorize(request, (davxml.Read(),)))
-    yield x
-    x.getResult()
+    yield self.authorize(request, (davxml.Read(),))
 
     #
     # Read request body
     #
     try:
-        doc = waitForDeferred(davXMLFromStream(request.stream))
-        yield doc
-        doc = doc.getResult()
+        doc = (yield davXMLFromStream(request.stream))
     except ValueError, e:
         log.err("Error while handling PROPFIND body: %s" % (e,))
         raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, str(e)))
@@ -116,23 +113,16 @@
 
     # Do some optimization of access control calculation by determining any inherited ACLs outside of
     # the child resource loop and supply those to the checkPrivileges on each child.
-    filtered_aces = waitForDeferred(self.inheritedACEsforChildren(request))
-    yield filtered_aces
-    filtered_aces = filtered_aces.getResult()
+    filtered_aces = (yield self.inheritedACEsforChildren(request))
 
     resources = [(self, my_url)]
 
-    d = self.findChildrenFaster(depth, request, lambda x, y: resources.append((x, y)), None, None, (davxml.Read(),), inherited_aces=filtered_aces)
-    x = waitForDeferred(d)
-    yield x
-    x.getResult()
+    yield self.findChildrenFaster(depth, request, lambda x, y: resources.append((x, y)), None, None, (davxml.Read(),), inherited_aces=filtered_aces)
 
     for resource, uri in resources:
         if search_properties is "names":
             try:
-                resource_properties = waitForDeferred(resource.listProperties(request))
-                yield resource_properties
-                resource_properties = resource_properties.getResult()
+                resource_properties = (yield resource.listProperties(request))
             except:
                 log.err("Unable to get properties for resource %r" % (resource,))
                 raise
@@ -147,21 +137,15 @@
             }
 
             if search_properties is "all":
-                properties_to_enumerate = waitForDeferred(resource.listAllprop(request))
-                yield properties_to_enumerate
-                properties_to_enumerate = properties_to_enumerate.getResult()
+                properties_to_enumerate = (yield resource.listAllprop(request))
             else:
                 properties_to_enumerate = search_properties
 
             for property in properties_to_enumerate:
-                has = waitForDeferred(resource.hasProperty(property, request))
-                yield has
-                has = has.getResult()
+                has = (yield resource.hasProperty(property, request))
                 if has:
                     try:
-                        resource_property = waitForDeferred(resource.readProperty(property, request))
-                        yield resource_property
-                        resource_property = resource_property.getResult()
+                        resource_property = (yield resource.readProperty(property, request))
                     except:
                         f = Failure()
 
@@ -196,9 +180,8 @@
     #
     # Return response
     #
-    yield MultiStatusResponse(xml_responses)
+    returnValue(MultiStatusResponse(xml_responses))
 
-http_PROPFIND = deferredGenerator(http_PROPFIND)
 
 ##
 # Utilities

Modified: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/put.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/put.py	2008-08-11 22:41:16 UTC (rev 2804)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/put.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -20,25 +20,24 @@
 
 __all__ = ["http_PUT"]
 
-from twisted.internet.defer import deferredGenerator, waitForDeferred
+from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.web2 import responsecode
 from twisted.web2.dav.http import ErrorResponse
 from twisted.web2.dav.util import allDataFromStream, parentForURL
 from twisted.web2.http import HTTPError, StatusResponse
 
 from twistedcaldav.caldavxml import caldav_namespace
-from twistedcaldav.method.put_common import storeCalendarObjectResource
+from twistedcaldav.method.put_common import StoreCalendarObjectResource
 from twistedcaldav.resource import isPseudoCalendarCollectionResource
 from twistedcaldav.log import Logger
 
 log = Logger()
 
+ at inlineCallbacks
 def http_PUT(self, request):
 
     parentURL = parentForURL(request.uri)
-    parent = waitForDeferred(request.locateResource(parentURL))
-    yield parent
-    parent = parent.getResult()
+    parent = (yield request.locateResource(parentURL))
 
     if isPseudoCalendarCollectionResource(parent):
         self.fp.restat(False)
@@ -51,35 +50,28 @@
             
         # Read the calendar component from the stream
         try:
-            d = waitForDeferred(allDataFromStream(request.stream))
-            yield d
-            calendardata = d.getResult()
+            calendardata = (yield allDataFromStream(request.stream))
 
             # We must have some data at this point
             if calendardata is None:
                 # Use correct DAV:error response
                 raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
 
-            d = waitForDeferred(storeCalendarObjectResource(
+            storer = StoreCalendarObjectResource(
                 request = request,
-                sourcecal = False,
-                calendardata = calendardata,
                 destination = self,
                 destination_uri = request.uri,
                 destinationcal = True,
-                destinationparent = parent,)
+                destinationparent = parent,
+                calendar = calendardata,
             )
-            yield d
-            yield d.getResult()
-            return
+            result = (yield storer.run())
+            returnValue(result)
 
         except ValueError, e:
             log.err("Error while handling (calendar) PUT: %s" % (e,))
             raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, str(e)))
 
     else:
-        d = waitForDeferred(super(CalDAVFile, self).http_PUT(request))
-        yield d
-        yield d.getResult()
-
-http_PUT = deferredGenerator(http_PUT)
+        result = (yield super(CalDAVFile, self).http_PUT(request))
+        returnValue(result)

Modified: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/put_common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/put_common.py	2008-08-11 22:41:16 UTC (rev 2804)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/put_common.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -18,13 +18,13 @@
 PUT/COPY/MOVE common behavior.
 """
 
-__all__ = ["storeCalendarObjectResource"]
+__all__ = ["StoreCalendarObjectResource"]
 
+import types
+
 from twisted.internet import reactor
-from twisted.internet.defer import Deferred
-from twisted.internet.defer import deferredGenerator
-from twisted.internet.defer import maybeDeferred
-from twisted.internet.defer import waitForDeferred
+from twisted.internet.defer import Deferred, inlineCallbacks
+from twisted.internet.defer import maybeDeferred, returnValue
 from twisted.python import failure
 from twisted.python.filepath import FilePath
 from twisted.web2 import responsecode
@@ -54,59 +54,12 @@
 from twistedcaldav.index import ReservationError
 from twistedcaldav.instance import TooManyInstancesError
 from twistedcaldav.log import Logger
+from twistedcaldav.scheduling.implicit import ImplicitScheduler
 
 log = Logger()
 
- at deferredGenerator
-def storeCalendarObjectResource(
-    request,
-    sourcecal, destinationcal,
-    source=None, source_uri=None, sourceparent=None,
-    destination=None, destination_uri=None, destinationparent=None,
-    calendardata=None,
-    deletesource=False,
-    isiTIP=False
-):
-    """
-    Function that does common PUT/COPY/MOVE behaviour.
+class StoreCalendarObjectResource(object):
     
-    @param request:           the L{twisted.web2.server.Request} for the current HTTP request.
-    @param source:            the L{CalDAVFile} for the source resource to copy from, or None if source data
-        is to be read from the request.
-    @param source_uri:        the URI for the source resource.
-    @param destination:       the L{CalDAVFile} for the destination resource to copy into.
-    @param destination_uri:   the URI for the destination resource.
-    @param calendardata:      the string data read directly from the request body if there is no source, None otherwise.
-    @param sourcecal:         True if the source resource is in a calendar collection, False otherwise.
-    @param destinationcal:    True if the destination resource is in a calendar collection, False otherwise
-    @param sourceparent:      the L{CalDAVFile} for the source resource's parent collection, or None if source is None.
-    @param destinationparent: the L{CalDAVFile} for the destination resource's parent collection.
-    @param deletesource:      True if the source resource is to be deleted on successful completion, False otherwise.
-    @param isiTIP:            True if relaxed calendar data validation is to be done, False otherwise.
-    @return:                  a Deferred with a status response result.
-    """
-    
-    try:
-        assert destination is not None and destinationparent is not None and destination_uri is not None
-        assert (source is None and sourceparent is None) or (source is not None and sourceparent is not None)
-        assert (calendardata is None and source is not None) or (calendardata is not None and source is None)
-        assert not deletesource or (deletesource and source is not None)
-    except AssertionError:
-        log.err("Invalid arguments to storeCalendarObjectResource():")
-        log.err("request=%s\n" % (request,))
-        log.err("sourcecal=%s\n" % (sourcecal,))
-        log.err("destinationcal=%s\n" % (destinationcal,))
-        log.err("source=%s\n" % (source,))
-        log.err("source_uri=%s\n" % (source_uri,))
-        log.err("sourceparent=%s\n" % (sourceparent,))
-        log.err("destination=%s\n" % (destination,))
-        log.err("destination_uri=%s\n" % (destination_uri,))
-        log.err("destinationparent=%s\n" % (destinationparent,))
-        log.err("calendardata=%s\n" % (calendardata,))
-        log.err("deletesource=%s\n" % (deletesource,))
-        log.err("isiTIP=%s\n" % (isiTIP,))
-        raise
-
     class RollbackState(object):
         """
         This class encapsulates the state needed to rollback the entire PUT/COPY/MOVE
@@ -114,7 +67,8 @@
         processed. The DoRollback method will actually execute the rollback operations.
         """
         
-        def __init__(self):
+        def __init__(self, storer):
+            self.storer = storer
             self.active = True
             self.source_copy = None
             self.destination_copy = None
@@ -134,32 +88,32 @@
                 log.debug("Rollback: rollback")
                 try:
                     if self.source_copy and self.source_deleted:
-                        self.source_copy.moveTo(source.fp)
-                        log.debug("Rollback: source restored %s to %s" % (self.source_copy.path, source.fp.path))
+                        self.source_copy.moveTo(self.storer.source.fp)
+                        log.debug("Rollback: source restored %s to %s" % (self.source_copy.path, self.storer.source.fp.path))
                         self.source_copy = None
                         self.source_deleted = False
                     if self.destination_copy:
-                        destination.fp.remove()
-                        log.debug("Rollback: destination restored %s to %s" % (self.destination_copy.path, destination.fp.path))
-                        self.destination_copy.moveTo(destination.fp)
+                        self.storer.destination.fp.remove()
+                        log.debug("Rollback: destination restored %s to %s" % (self.destination_copy.path, self.storer.destination.fp.path))
+                        self.destination_copy.moveTo(self.storer.destination.fp)
                         self.destination_copy = None
                     elif self.destination_created:
-                        if destinationcal:
-                            doRemoveDestinationIndex()
-                            log.debug("Rollback: destination index removed %s" % (destination.fp.path,))
+                        if self.storer.destinationcal:
+                            self.storer.doRemoveDestinationIndex()
+                            log.debug("Rollback: destination index removed %s" % (self.storer.destination.fp.path,))
                             self.destination_index_deleted = False
-                        destination.fp.remove()
-                        log.debug("Rollback: destination removed %s" % (destination.fp.path,))
+                        self.storer.destination.fp.remove()
+                        log.debug("Rollback: destination removed %s" % (self.storer.destination.fp.path,))
                         self.destination_created = False
                     if self.destination_index_deleted:
                         # Must read in calendar for destination being re-indexed
-                        doDestinationIndex(destination.iCalendar())
+                        self.storer.doDestinationIndex(self.storer.destination.iCalendar())
                         self.destination_index_deleted = False
-                        log.debug("Rollback: destination re-indexed %s" % (destination.fp.path,))
+                        log.debug("Rollback: destination re-indexed %s" % (self.storer.destination.fp.path,))
                     if self.source_index_deleted:
-                        doSourceIndexRecover()
+                        self.storer.doSourceIndexRecover()
                         self.destination_index_deleted = False
-                        log.debug("Rollback: source re-indexed %s" % (source.fp.path,))
+                        log.debug("Rollback: source re-indexed %s" % (self.storer.source.fp.path,))
                 except:
                     log.err("Rollback: exception caught and not handled: %s" % failure.Failure())
 
@@ -182,57 +136,226 @@
                 self.source_deleted = False
                 self.source_index_deleted = False
                 self.destination_index_deleted = False
+
+    class UIDReservation(object):
+        
+        def __init__(self, index, uid, uri):
+            self.reserved = False
+            self.index = index
+            self.uid = uid
+            self.uri = uri
+            
+        @inlineCallbacks
+        def reserve(self):
+            
+            # Lets use a deferred for this and loop a few times if we cannot reserve so that we give
+            # time to whoever has the reservation to finish and release it.
+            failure_count = 0
+            while(failure_count < 10):
+                try:
+                    yield self.index.reserveUID(self.uid)
+                    self.reserved = True
+                    break
+                except ReservationError:
+                    self.reserved = False
+                failure_count += 1
+                
+                pause = Deferred()
+                def _timedDeferred():
+                    pause.callback(True)
+                reactor.callLater(0.5, _timedDeferred)
+                yield pause
+            
+            if self.uri and not self.reserved:
+                raise HTTPError(StatusResponse(responsecode.CONFLICT, "Resource: %s currently in use." % (self.uri,)))
+        
+        @inlineCallbacks
+        def unreserve(self):
+            if self.reserved:
+                yield self.index.unreserveUID(self.uid)
+                self.reserved = False
+
+    def __init__(
+        self,
+        request,
+        source=None, source_uri=None, sourceparent=None, sourcecal=False, deletesource=False,
+        destination=None, destination_uri=None, destinationparent=None, destinationcal=True,
+        calendar=None,
+        isiTIP=False
+    ):
+        """
+        Function that does common PUT/COPY/MOVE behavior.
+        
+        @param request:           the L{twisted.web2.server.Request} for the current HTTP request.
+        @param source:            the L{CalDAVFile} for the source resource to copy from, or None if source data
+            is to be read from the request.
+        @param source_uri:        the URI for the source resource.
+        @param destination:       the L{CalDAVFile} for the destination resource to copy into.
+        @param destination_uri:   the URI for the destination resource.
+        @param calendar:          the C{str} or L{Component} calendar data if there is no source, None otherwise.
+        @param sourcecal:         True if the source resource is in a calendar collection, False otherwise.
+        @param destinationcal:    True if the destination resource is in a calendar collection, False otherwise
+        @param sourceparent:      the L{CalDAVFile} for the source resource's parent collection, or None if source is None.
+        @param destinationparent: the L{CalDAVFile} for the destination resource's parent collection.
+        @param deletesource:      True if the source resource is to be deleted on successful completion, False otherwise.
+        @param isiTIP:            True if relaxed calendar data validation is to be done, False otherwise.
+        """
+        
+        # Check that all arguments are valid
+        try:
+            assert destination is not None and destinationparent is not None and destination_uri is not None
+            assert (source is None and sourceparent is None) or (source is not None and sourceparent is not None)
+            assert (calendar is None and source is not None) or (calendar is not None and source is None)
+            assert not deletesource or (deletesource and source is not None)
+        except AssertionError:
+            log.err("Invalid arguments to StoreCalendarObjectResource.__init__():")
+            log.err("request=%s\n" % (request,))
+            log.err("sourcecal=%s\n" % (sourcecal,))
+            log.err("destinationcal=%s\n" % (destinationcal,))
+            log.err("source=%s\n" % (source,))
+            log.err("source_uri=%s\n" % (source_uri,))
+            log.err("sourceparent=%s\n" % (sourceparent,))
+            log.err("destination=%s\n" % (destination,))
+            log.err("destination_uri=%s\n" % (destination_uri,))
+            log.err("destinationparent=%s\n" % (destinationparent,))
+            log.err("calendar=%s\n" % (calendar,))
+            log.err("deletesource=%s\n" % (deletesource,))
+            log.err("isiTIP=%s\n" % (isiTIP,))
+            raise
     
-    rollback = RollbackState()
+        self.request = request
+        self.sourcecal = sourcecal
+        self.destinationcal = destinationcal
+        self.source = source
+        self.source_uri = source_uri
+        self.sourceparent = sourceparent
+        self.destination = destination
+        self.destination_uri = destination_uri
+        self.destinationparent = destinationparent
+        self.calendar = calendar
+        self.calendardata = None
+        self.deletesource = deletesource
+        self.isiTIP = isiTIP
+        
+        self.rollback = None
+        self.access = None
 
-    def validResourceName():
+    @inlineCallbacks
+    def fullValidation(self):
         """
+        Do full validation of source and destination calendar data.
+        """
+
+        if self.destinationcal:
+            # Valid resource name check
+            result, message = self.validResourceName()
+            if not result:
+                log.err(message)
+                raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Resource name not allowed"))
+
+            if not self.sourcecal:
+                # Valid content type check on the source resource if its not in a calendar collection
+                if self.source is not None:
+                    result, message = self.validContentType()
+                    if not result:
+                        log.err(message)
+                        raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "supported-calendar-data")))
+                
+                    # At this point we need the calendar data to do more tests
+                    self.calendar = self.source.iCalendar()
+                else:
+                    try:
+                        if type(self.calendar) in (types.StringType, types.UnicodeType,):
+                            self.calendardata = self.calendar
+                            self.calendar = Component.fromString(self.calendar)
+                    except ValueError, e:
+                        log.err(str(e))
+                        raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+                        
+                # Valid calendar data check
+                result, message = self.validCalendarDataCheck()
+                if not result:
+                    log.err(message)
+                    raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+                    
+                # Valid calendar data for CalDAV check
+                result, message = self.validCalDAVDataCheck()
+                if not result:
+                    log.err(message)
+                    raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-object-resource")))
+
+                # Must have a valid UID at this point
+                self.uid = self.calendar.resourceUID()
+            else:
+                # Get UID from original resource
+                self.source_index = self.sourceparent.index()
+                self.uid = self.source_index.resourceUIDForName(self.source.fp.basename())
+                if self.uid is None:
+                    log.err("Source calendar does not have a UID: %s" % self.source.fp.basename())
+                    raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-object-resource")))
+
+                # FIXME: We need this here because we have to re-index the destination. Ideally it
+                # would be better to copy the index entries from the source and add to the destination.
+                self.calendar = self.source.iCalendar()
+
+            # Valid calendar data size check
+            result, message = self.validSizeCheck()
+            if not result:
+                log.err(message)
+                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "max-resource-size")))
+
+            # Check access
+            if self.destinationcal and config.EnablePrivateEvents:
+                yield self.validAccess()
+    
+    def validResourceName(self):
+        """
         Make sure that the resource name for the new resource is valid.
         """
         result = True
         message = ""
-        filename = destination.fp.basename()
+        filename = self.destination.fp.basename()
         if filename.startswith("."):
             result = False
             message = "File name %s not allowed in calendar collection" % (filename,)
 
         return result, message
         
-    def validContentType():
+    def validContentType(self):
         """
         Make sure that the content-type of the source resource is text/calendar.
         This test is only needed when the source is not in a calendar collection.
         """
         result = True
         message = ""
-        content_type = source.contentType()
+        content_type = self.source.contentType()
         if not ((content_type.mediaType == "text") and (content_type.mediaSubtype == "calendar")):
             result = False
             message = "MIME type %s not allowed in calendar collection" % (content_type,)
 
         return result, message
         
-    def validCalendarDataCheck():
+    def validCalendarDataCheck(self):
         """
         Check that the calendar data is valid iCalendar.
-        @return:         tuple: (True/False if the calendra data is valid,
+        @return:         tuple: (True/False if the calendar data is valid,
                                  log message string).
         """
         result = True
         message = ""
-        if calendar is None:
+        if self.calendar is None:
             result = False
             message = "Empty resource not allowed in calendar collection"
         else:
             try:
-                calendar.validCalendarForCalDAV()
+                self.calendar.validCalendarForCalDAV()
             except ValueError, e:
                 result = False
                 message = "Invalid calendar data: %s" % (e,)
         
         return result, message
     
-    def validCalDAVDataCheck():
+    def validCalDAVDataCheck(self):
         """
         Check that the calendar data is valid as a CalDAV calendar object resource.
         @return:         tuple: (True/False if the calendar data is valid,
@@ -241,17 +364,17 @@
         result = True
         message = ""
         try:
-            if isiTIP:
-                calendar.validateComponentsForCalDAV(True)
+            if self.isiTIP:
+                self.calendar.validateComponentsForCalDAV(True)
             else:
-                calendar.validateForCalDAV()
+                self.calendar.validateForCalDAV()
         except ValueError, e:
             result = False
             message = "Calendar data does not conform to CalDAV requirements: %s" % (e,)
         
         return result, message
     
-    def validSizeCheck():
+    def validSizeCheck(self):
         """
         Make sure that the content-type of the source resource is text/calendar.
         This test is only needed when the source is not in a calendar collection.
@@ -259,53 +382,46 @@
         result = True
         message = ""
         if config.MaximumAttachmentSize:
-            calsize = len(str(calendar))
+            calsize = len(str(self.calendar))
             if calsize > config.MaximumAttachmentSize:
                 result = False
                 message = "Data size %d bytes is larger than allowed limit %d bytes" % (calsize, config.MaximumAttachmentSize)
 
         return result, message
 
-    @deferredGenerator
-    def validAccess():
+    @inlineCallbacks
+    def validAccess(self):
         """
         Make sure that the X-CALENDARSERVER-ACCESS property is properly dealt with.
         """
         
-        if calendar.hasProperty(Component.ACCESS_PROPERTY):
+        if self.calendar.hasProperty(Component.ACCESS_PROPERTY):
             
             # Must be a value we know about
-            access = calendar.accessLevel(default=None)
-            if access is None:
+            self.access = self.calendar.accessLevel(default=None)
+            if self.access is None:
                 raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (calendarserver_namespace, "valid-access-restriction")))
                 
             # Only DAV:owner is able to set the property to other than PUBLIC
-            d = waitForDeferred(destinationparent.owner(request))
-            yield d
-            parent_owner = d.getResult()
+            parent_owner = (yield self.destinationparent.owner(self.request))
             
-            authz = destinationparent.currentPrincipal(request)
-            if davxml.Principal(parent_owner) != authz and access != Component.ACCESS_PUBLIC:
+            authz = self.destinationparent.currentPrincipal(self.request)
+            if davxml.Principal(parent_owner) != authz and self.access != Component.ACCESS_PUBLIC:
                 raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (calendarserver_namespace, "valid-access-restriction-change")))
-
-            yield access, calendardata
         else:
             # Check whether an access property was present before and write that into the calendar data
-            newcalendardata = calendardata
-            if not source and destination.exists() and destination.hasDeadProperty(TwistedCalendarAccessProperty):
-                old_access = str(destination.readDeadProperty(TwistedCalendarAccessProperty))
-                calendar.addProperty(Property(name=Component.ACCESS_PROPERTY, value=old_access))
-                newcalendardata = str(calendar)
+            if not self.source and self.destination.exists() and self.destination.hasDeadProperty(TwistedCalendarAccessProperty):
+                old_access = str(self.destination.readDeadProperty(TwistedCalendarAccessProperty))
+                self.calendar.addProperty(Property(name=Component.ACCESS_PROPERTY, value=old_access))
+                self.calendardata = str(self.calendar)
 
-            yield None, newcalendardata
-
-    def noUIDConflict(uid):
+    def noUIDConflict(self, uid):
         """
         Check that the UID of the new calendar object conforms to the requirements of
         CalDAV, i.e. it must be unique in the collection and we must not overwrite a
         different UID.
         @param uid: the UID for the resource being stored.
-        @return: tuple: (True/False if the uid is valid, log message string,
+        @return: tuple: (True/False if the UID is valid, log message string,
             name of conflicted resource).
         """
 
@@ -315,14 +431,14 @@
 
         # Adjust for a move into same calendar collection
         oldname = None
-        if sourceparent and (sourceparent.fp.path == destinationparent.fp.path) and deletesource:
-            oldname = source.fp.basename()
+        if self.sourceparent and (self.sourceparent.fp.path == self.destinationparent.fp.path) and self.deletesource:
+            oldname = self.source.fp.basename()
 
-        # UID must be unqiue
-        index = destinationparent.index()
-        if not index.isAllowedUID(uid, oldname, destination.fp.basename()):
+        # UID must be unique
+        index = self.destinationparent.index()
+        if not index.isAllowedUID(uid, oldname, self.destination.fp.basename()):
             rname = index.resourceNameForUID(uid)
-            # This can happen if two simulataneous PUTs occur with the same UID.
+            # This can happen if two simultaneous PUTs occur with the same UID.
             # i.e. one PUT has reserved the UID but has not yet written the resource,
             # the other PUT tries to reserve and fails but no index entry exists yet.
             if rname is None:
@@ -332,355 +448,293 @@
             message = "Calendar resource %s already exists with same UID %s" % (rname, uid)
         else:
             # Cannot overwrite a resource with different UID
-            if destination.fp.exists():
-                olduid = index.resourceUIDForName(destination.fp.basename())
+            if self.destination.fp.exists():
+                olduid = index.resourceUIDForName(self.destination.fp.basename())
                 if olduid != uid:
-                    rname = destination.fp.basename()
+                    rname = self.destination.fp.basename()
                     result = False
                     message = "Cannot overwrite calendar resource %s with different UID %s" % (rname, olduid)
         
         return result, message, rname
 
-    try:
+    @inlineCallbacks
+    def checkQuota(self):
         """
-        Handle validation operations here.
+        Get quota details for destination and source before we start messing with adding other files.
         """
-        reserved = False
-        access = None
-        if destinationcal:
-            # Valid resource name check
-            result, message = validResourceName()
-            if not result:
-                log.err(message)
-                raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Resource name not allowed"))
 
-            if not sourcecal:
-                # Valid content type check on the source resource if its not in a calendar collection
-                if source is not None:
-                    result, message = validContentType()
-                    if not result:
-                        log.err(message)
-                        raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "supported-calendar-data")))
-                
-                    # At this point we need the calendar data to do more tests
-                    calendar = source.iCalendar()
-                else:
-                    try:
-                        calendar = Component.fromString(calendardata)
-                    except ValueError, e:
-                        log.err(str(e))
-                        raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
-                        
-                # Valid calendar data check
-                result, message = validCalendarDataCheck()
-                if not result:
-                    log.err(message)
-                    raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
-                    
-                # Valid calendar data for CalDAV check
-                result, message = validCalDAVDataCheck()
-                if not result:
-                    log.err(message)
-                    raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-object-resource")))
-
-                # Must have a valid UID at this point
-                uid = calendar.resourceUID()
-            else:
-                # Get uid from original resource
-                source_index = sourceparent.index()
-                uid = source_index.resourceUIDForName(source.fp.basename())
-                if uid is None:
-                    log.err("Source calendar does not have a UID: %s" % source.fp.basename())
-                    raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-object-resource")))
-
-                # FIXME: We need this here because we have to re-index the destination. Ideally it
-                # would be better to copy the index entries from the source and add to the destination.
-                calendar = source.iCalendar()
-
-            # Valid calendar data size check
-            result, message = validSizeCheck()
-            if not result:
-                log.err(message)
-                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "max-resource-size")))
-
-            # Check access
-            if destinationcal and config.EnablePrivateEvents:
-                d = waitForDeferred(validAccess())
-                yield d
-                access, calendardata = d.getResult()
-
-            # Reserve UID
-            destination_index = destinationparent.index()
-            
-            # Lets use a deferred for this and loop a few times if we cannot reserve so that we give
-            # time to whoever has the reservation to finish and release it.
-            failure_count = 0
-            while(failure_count < 10):
-                try:
-                    d = waitForDeferred(destination_index.reserveUID(uid))
-                    yield d
-                    d.getResult()
-                    reserved = True
-                    break
-                except ReservationError:
-                    reserved = False
-                failure_count += 1
-                
-                d = Deferred()
-                def _timedDeferred():
-                    d.callback(True)
-                reactor.callLater(0.5, _timedDeferred)
-                pause = waitForDeferred(d)
-                yield pause
-                pause.getResult()
-            
-            if destination_uri and not reserved:
-                raise HTTPError(StatusResponse(responsecode.CONFLICT, "Resource: %s currently in use." % (destination_uri,)))
-        
-            # uid conflict check - note we do this after reserving the UID to avoid a race condition where two requests
-            # try to write the same calendar data to two different resource URIs.
-            if not isiTIP:
-                result, message, rname = noUIDConflict(uid)
-                if not result:
-                    log.err(message)
-                    raise HTTPError(ErrorResponse(responsecode.FORBIDDEN,
-                        NoUIDConflict(davxml.HRef.fromString(joinURL(parentForURL(destination_uri), rname.encode("utf-8"))))
-                    ))
-            
-        """
-        Handle rollback setup here.
-        """
-
-        # Do quota checks on destination and source before we start messing with adding other files
-        if request is None:
-            destquota = None
+        if self.request is None:
+            self.destquota = None
         else:
-            destquota = waitForDeferred(destination.quota(request))
-            yield destquota
-            destquota = destquota.getResult()
-            if destquota is not None and destination.exists():
-                old_dest_size = waitForDeferred(destination.quotaSize(request))
-                yield old_dest_size
-                old_dest_size = old_dest_size.getResult()
+            self.destquota = (yield self.destination.quota(self.request))
+            if self.destquota is not None and self.destination.exists():
+                self.old_dest_size = (yield self.destination.quotaSize(self.request))
             else:
-                old_dest_size = 0
+                self.old_dest_size = 0
             
-        if request is None:
-            sourcequota = None
-        elif source is not None:
-            sourcequota = waitForDeferred(source.quota(request))
-            yield sourcequota
-            sourcequota = sourcequota.getResult()
-            if sourcequota is not None and source.exists():
-                old_source_size = waitForDeferred(source.quotaSize(request))
-                yield old_source_size
-                old_source_size = old_source_size.getResult()
+        if self.request is None:
+            self.sourcequota = None
+        elif self.source is not None:
+            self.sourcequota = (yield self.source.quota(self.request))
+            if self.sourcequota is not None and self.source.exists():
+                self.old_source_size = (yield self.source.quotaSize(self.request))
             else:
-                old_source_size = 0
+                self.old_source_size = 0
         else:
-            sourcequota = None
-            old_source_size = 0
+            self.sourcequota = None
+            self.old_source_size = 0
 
-        # We may need to restore the original resource data if the PUT/COPY/MOVE fails,
-        # so rename the original file in case we need to rollback.
-        overwrite = destination.exists()
-        if overwrite:
-            rollback.destination_copy = FilePath(destination.fp.path)
-            rollback.destination_copy.path += ".rollback"
-            copyToWithXAttrs(destination.fp, rollback.destination_copy)
-            log.debug("Rollback: backing up destination %s to %s" % (destination.fp.path, rollback.destination_copy.path))
-        else:
-            rollback.destination_created = True
-            log.debug("Rollback: will create new destination %s" % (destination.fp.path,))
+        returnValue(None)
 
-        if deletesource:
-            rollback.source_copy = FilePath(source.fp.path)
-            rollback.source_copy.path += ".rollback"
-            copyToWithXAttrs(source.fp, rollback.source_copy)
-            log.debug("Rollback: backing up source %s to %s" % (source.fp.path, rollback.source_copy.path))
-    
+    def setupRollback(self):
         """
-        Handle actual store operations here.
-        
-        The order in which this is done is import:
-            
-            1. Do store operation for new data
-            2. Delete source and source index if needed
-            3. Do new indexing if needed
-            
-        Note that we need to remove the source index BEFORE doing the destination index to cover the
-        case of a resource being 'renamed', i.e. moved within the same collection. Since the index UID
-        column must be unique in SQL, we cannot add the new index before remove the old one.
+        We may need to restore the original resource data if the PUT/COPY/MOVE fails,
+        so rename the original file in case we need to rollback.
         """
 
+        self.rollback = StoreCalendarObjectResource.RollbackState(self)
+        self.overwrite = self.destination.exists()
+        if self.overwrite:
+            self.rollback.destination_copy = FilePath(self.destination.fp.path)
+            self.rollback.destination_copy.path += ".rollback"
+            copyToWithXAttrs(self.destination.fp, self.rollback.destination_copy)
+            log.debug("Rollback: backing up destination %s to %s" % (self.destination.fp.path, self.rollback.destination_copy.path))
+        else:
+            self.rollback.destination_created = True
+            log.debug("Rollback: will create new destination %s" % (self.destination.fp.path,))
+
+        if self.deletesource:
+            self.rollback.source_copy = FilePath(self.source.fp.path)
+            self.rollback.source_copy.path += ".rollback"
+            copyToWithXAttrs(self.source.fp, self.rollback.source_copy)
+            log.debug("Rollback: backing up source %s to %s" % (self.source.fp.path, self.rollback.source_copy.path))
+
+    @inlineCallbacks
+    def doStore(self):
         # Do put or copy based on whether source exists
-        if source is not None:
-            response = maybeDeferred(copyWithXAttrs, source.fp, destination.fp, destination_uri)
+        if self.source is not None:
+            response = maybeDeferred(copyWithXAttrs, self.source.fp, self.destination.fp, self.destination_uri)
         else:
-            md5 = MD5StreamWrapper(MemoryStream(calendardata))
-            response = maybeDeferred(putWithXAttrs, md5, destination.fp)
-        response = waitForDeferred(response)
-        yield response
-        response = response.getResult()
+            if self.calendardata is None:
+                self.calendardata = str(self.calendar)
+            md5 = MD5StreamWrapper(MemoryStream(self.calendardata))
+            response = maybeDeferred(putWithXAttrs, md5, self.destination.fp)
+        response = (yield response)
 
         # Update the MD5 value on the resource
-        if source is not None:
+        if self.source is not None:
             # Copy MD5 value from source to destination
-            if source.hasDeadProperty(TwistedGETContentMD5):
-                md5 = source.readDeadProperty(TwistedGETContentMD5)
-                destination.writeDeadProperty(md5)
+            if self.source.hasDeadProperty(TwistedGETContentMD5):
+                md5 = self.source.readDeadProperty(TwistedGETContentMD5)
+                self.destination.writeDeadProperty(md5)
         else:
-            # Finish MD5 calc and write dead property
+            # Finish MD5 calculation and write dead property
             md5.close()
             md5 = md5.getMD5()
-            destination.writeDeadProperty(TwistedGETContentMD5.fromString(md5))
-
+            self.destination.writeDeadProperty(TwistedGETContentMD5.fromString(md5))
+    
         # Update calendar-access property value on the resource
-        if access:
-            destination.writeDeadProperty(TwistedCalendarAccessProperty(access))
+        if self.access:
+            self.destination.writeDeadProperty(TwistedCalendarAccessProperty(self.access))
             
         # Do not remove the property if access was not specified and we are storing in a calendar.
         # This ensure that clients that do not preserve the iCalendar property do not cause access
         # restrictions to be lost.
-        elif not destinationcal:
-            destination.removeDeadProperty(TwistedCalendarAccessProperty)                
+        elif not self.destinationcal:
+            self.destination.removeDeadProperty(TwistedCalendarAccessProperty)                
 
-        response = IResponse(response)
+        returnValue(IResponse(response))
 
-        def doDestinationIndex(caltoindex):
-            """
-            Do destination resource indexing, replacing any index previous stored.
-            
-            @return: None if successful, ErrorResponse on failure
-            """
-            
-            # Delete index for original item
-            if overwrite:
-                doRemoveDestinationIndex()
-            
-            # Add or update the index for this resource.
-            try:
-                destination_index.addResource(destination.fp.basename(), caltoindex)
-                log.debug("Destination indexed %s" % (destination.fp.path,))
-            except TooManyInstancesError, ex:
-                log.err("Cannot index calendar resource as there are too many recurrence instances %s" % destination)
-                raise HTTPError(ErrorResponse(
-                    responsecode.FORBIDDEN,
-                    NumberOfRecurrencesWithinLimits(PCDATAElement(str(ex.max_allowed)))
-                ))
-            except (ValueError, TypeError), ex:
-                log.err("Cannot index calendar resource: %s" % (ex,))
-                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+    @inlineCallbacks
+    def doSourceDelete(self):
+        # Delete index for original item
+        if self.sourcecal:
+            self.source_index.deleteResource(self.source.fp.basename())
+            self.rollback.source_index_deleted = True
+            log.debug("Source index removed %s" % (self.source.fp.path,))
 
-            destination.writeDeadProperty(davxml.GETContentType.fromString("text/calendar"))
-            return None
+        # Delete the source resource
+        delete(self.source_uri, self.source.fp, "0")
+        self.rollback.source_deleted = True
+        log.debug("Source removed %s" % (self.source.fp.path,))
 
-        def doRemoveDestinationIndex():
-            """
-            Remove any existing destination index.
-            """
-            
-            # Delete index for original item
-            if destinationcal:
-                destination_index.deleteResource(destination.fp.basename())
-                rollback.destination_index_deleted = True
-                log.debug("Destination index removed %s" % (destination.fp.path,))
+        # Update quota
+        if self.sourcequota is not None:
+            delete_size = 0 - self.old_source_size
+            yield self.source.quotaSizeAdjust(self.request, delete_size)
 
-        def doSourceDelete():
-            # Delete index for original item
-            if sourcecal:
-                source_index.deleteResource(source.fp.basename())
-                rollback.source_index_deleted = True
-                log.debug("Source index removed %s" % (source.fp.path,))
+        # Change CTag on the parent calendar collection
+        if self.sourcecal:
+            yield self.sourceparent.updateCTag()
+  
+        returnValue(None)
 
-            # Delete the source resource
-            delete(source_uri, source.fp, "0")
-            rollback.source_deleted = True
-            log.debug("Source removed %s" % (source.fp.path,))
+    @inlineCallbacks
+    def doDestinationQuotaCheck(self):
+        # Get size of new/old resources
+        new_dest_size = (yield self.destination.quotaSize(self.request))
 
-        def doSourceIndexRecover():
-            """
-            Do source resource indexing. This only gets called when restoring
-            the source after its index has been deleted.
-            
-            @return: None if successful, ErrorResponse on failure
-            """
-            
-            # Add or update the index for this resource.
-            try:
-                source_index.addResource(source.fp.basename(), calendar)
-            except TooManyInstancesError, ex:
-                raise HTTPError(ErrorResponse(
-                    responsecode.FORBIDDEN,
+        diff_size = new_dest_size - self.old_dest_size
+
+        if diff_size >= self.destquota[0]:
+            log.err("Over quota: available %d, need %d" % (self.destquota[0], diff_size))
+            raise HTTPError(ErrorResponse(responsecode.INSUFFICIENT_STORAGE_SPACE, (dav_namespace, "quota-not-exceeded")))
+        yield self.destination.quotaSizeAdjust(self.request, diff_size)
+
+        returnValue(None)
+
+    def doSourceIndexRecover(self):
+        """
+        Do source resource indexing. This only gets called when restoring
+        the source after its index has been deleted.
+        
+        @return: None if successful, ErrorResponse on failure
+        """
+        
+        # Add or update the index for this resource.
+        try:
+            self.source_index.addResource(self.source.fp.basename(), self.calendar)
+        except TooManyInstancesError, ex:
+            raise HTTPError(ErrorResponse(
+                responsecode.FORBIDDEN,
                     NumberOfRecurrencesWithinLimits(PCDATAElement(str(ex.max_allowed)))
                 ))
 
-            source.writeDeadProperty(davxml.GETContentType.fromString("text/calendar"))
+            self.source.writeDeadProperty(davxml.GETContentType.fromString("text/calendar"))
             return None
 
-        if deletesource:
-            doSourceDelete()
-            # Update quota
-            if sourcequota is not None:
-                delete_size = 0 - old_source_size
-                d = waitForDeferred(source.quotaSizeAdjust(request, delete_size))
-                yield d
-                d.getResult()
+    def doDestinationIndex(self, caltoindex):
+        """
+        Do destination resource indexing, replacing any index previous stored.
+        
+        @return: None if successful, ErrorResponse on failure
+        """
+        
+        # Delete index for original item
+        if self.overwrite:
+            self.doRemoveDestinationIndex()
+        
+        # Add or update the index for this resource.
+        try:
+            self.destination_index.addResource(self.destination.fp.basename(), caltoindex)
+            log.debug("Destination indexed %s" % (self.destination.fp.path,))
+        except TooManyInstancesError, ex:
+            log.err("Cannot index calendar resource as there are too many recurrence instances %s" % self.destination)
+            raise HTTPError(ErrorResponse(
+                responsecode.FORBIDDEN,
+                NumberOfRecurrencesWithinLimits(PCDATAElement(str(ex.max_allowed)))
+            ))
+        except (ValueError, TypeError), ex:
+            log.err("Cannot index calendar resource: %s" % (ex,))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
 
-            if sourcecal:
-                # Change CTag on the parent calendar collection
-                d = waitForDeferred(sourceparent.updateCTag())
-                yield d
-                d.getResult()
+        self.destination.writeDeadProperty(davxml.GETContentType.fromString("text/calendar"))
+        return None
 
-        if destinationcal:
-            result = doDestinationIndex(calendar)
-            if result is not None:
-                rollback.Rollback()
-                yield result
-                return
+    def doRemoveDestinationIndex(self):
+        """
+        Remove any existing destination index.
+        """
+        
+        # Delete index for original item
+        if self.destinationcal:
+            self.destination_index.deleteResource(self.destination.fp.basename())
+            self.rollback.destination_index_deleted = True
+            log.debug("Destination index removed %s" % (self.destination.fp.path,))
 
-        # Do quota check on destination
-        if destquota is not None:
-            # Get size of new/old resources
-            new_dest_size = waitForDeferred(destination.quotaSize(request))
-            yield new_dest_size
-            new_dest_size = new_dest_size.getResult()
-            diff_size = new_dest_size - old_dest_size
-            if diff_size >= destquota[0]:
-                log.err("Over quota: available %d, need %d" % (destquota[0], diff_size))
-                raise HTTPError(ErrorResponse(responsecode.INSUFFICIENT_STORAGE_SPACE, (dav_namespace, "quota-not-exceeded")))
-            d = waitForDeferred(destination.quotaSizeAdjust(request, diff_size))
-            yield d
-            d.getResult()
+    @inlineCallbacks
+    def run(self):
+        """
+        Function that does common PUT/COPY/MOVE behavior.
 
+        @return: a Deferred with a status response result.
+        """
 
-        if destinationcal:
-            # Change CTag on the parent calendar collection
-            d = waitForDeferred(destinationparent.updateCTag())
-            yield d
-            d.getResult()
+        try:
+            reservation = None
+            
+            # Handle all validation operations here.
+            yield self.fullValidation()
 
-        # Can now commit changes and forget the rollback details
-        rollback.Commit()
+            # Reservation and UID conflict checking is next.
+            if self.destinationcal:    
+                # Reserve UID
+                self.destination_index = self.destinationparent.index()
+                reservation = StoreCalendarObjectResource.UIDReservation(self.destination_index, self.uid, self.destination_uri)
+                yield reservation.reserve()
+            
+                # UID conflict check - note we do this after reserving the UID to avoid a race condition where two requests
+                # try to write the same calendar data to two different resource URIs.
+                if not self.isiTIP:
+                    result, message, rname = self.noUIDConflict(self.uid)
+                    if not result:
+                        log.err(message)
+                        raise HTTPError(ErrorResponse(responsecode.FORBIDDEN,
+                            NoUIDConflict(davxml.HRef.fromString(joinURL(parentForURL(self.destination_uri), rname.encode("utf-8"))))
+                        ))
+            
+            # Get current quota state.
+            yield self.checkQuota()
 
-        if reserved:
-            d = waitForDeferred(destination_index.unreserveUID(uid))
-            yield d
-            d.getResult()
-            reserved = False
+            # Do scheduling
+            if not self.isiTIP:
+                scheduler = ImplicitScheduler()
+                self.calendar = (yield scheduler.doImplicitScheduling(self.request, self.destination, self.calendar, False))
 
-        yield response
-        return
+            # Initialize the rollback system
+            self.setupRollback()
 
-    except Exception, err:
-        if reserved:
-            d = waitForDeferred(destination_index.unreserveUID(uid))
-            yield d
-            d.getResult()
-            reserved = False
+            """
+            Handle actual store operations here.
+            
+            The order in which this is done is import:
+                
+                1. Do store operation for new data
+                2. Delete source and source index if needed
+                3. Do new indexing if needed
+                
+            Note that we need to remove the source index BEFORE doing the destination index to cover the
+            case of a resource being 'renamed', i.e. moved within the same collection. Since the index UID
+            column must be unique in SQL, we cannot add the new index before remove the old one.
+            """
+    
+            # Do the actual put or copy
+            response = (yield self.doStore())
+            
+            # Delete the original source if needed.
+            if self.deletesource:
+                yield self.doSourceDelete()
+    
+            # Index the new resource if storing to a calendar.
+            if self.destinationcal:
+                result = self.doDestinationIndex(self.calendar)
+                if result is not None:
+                    self.rollback.Rollback()
+                    returnValue(result)
+    
+            # Do quota check on destination
+            if self.destquota is not None:
+                yield self.doDestinationQuotaCheck()
+    
+            if self.destinationcal:
+                # Change CTag on the parent calendar collection
+                yield self.destinationparent.updateCTag()
+    
+            # Can now commit changes and forget the rollback details
+            self.rollback.Commit()
+    
+            if reservation:
+                yield reservation.unreserve()
+    
+            returnValue(response)
+    
+        except Exception, err:
+            if reservation:
+                yield reservation.unreserve()
+    
+            # Roll back changes to original server state. Note this may do nothing
+            # if the rollback has already occurred or changes already committed.
+            if self.rollback:
+                self.rollback.Rollback()
 
-        # Roll back changes to original server state. Note this may do nothing
-        # if the rollback has already ocurred or changes already committed.
-        rollback.Rollback()
-        raise err
+            raise err

Modified: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/report.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/report.py	2008-08-11 22:41:16 UTC (rev 2804)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/report.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -29,7 +29,7 @@
 
 import string
 
-from twisted.internet.defer import deferredGenerator, waitForDeferred
+from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.web2 import responsecode
 from twisted.web2.http import HTTPError, StatusResponse
 from twisted.web2.dav import davxml
@@ -47,6 +47,7 @@
 class NumberOfMatchesWithinLimits(Exception):
     pass
 
+ at inlineCallbacks
 def http_REPORT(self, request):
     """
     Respond to a REPORT request. (RFC 3253, section 3.6)
@@ -59,9 +60,7 @@
     # Read request body
     #
     try:
-        doc = waitForDeferred(davXMLFromStream(request.stream))
-        yield doc
-        doc = doc.getResult()
+        doc = (yield davXMLFromStream(request.stream))
     except ValueError, e:
         log.err("Error while handling REPORT body: %s" % (e,))
         raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, str(e)))
@@ -122,12 +121,7 @@
     privileges = (davxml.Read(),)
     if method_name == "report_urn_ietf_params_xml_ns_caldav_free_busy_query":
         privileges = (caldavxml.ReadFreeBusy(),)
-    x = waitForDeferred(self.authorize(request, privileges))
-    yield x
-    x.getResult()
+    yield self.authorize(request, privileges)
 
-    d = waitForDeferred(method(request, doc.root_element))
-    yield d
-    yield d.getResult()
-
-http_REPORT = deferredGenerator(http_REPORT)
+    result = (yield method(request, doc.root_element))
+    returnValue(result)

Modified: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/report_calquery.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/report_calquery.py	2008-08-11 22:41:16 UTC (rev 2804)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/report_calquery.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -22,7 +22,7 @@
 
 import urllib
 
-from twisted.internet.defer import deferredGenerator, succeed, waitForDeferred
+from twisted.internet.defer import succeed, inlineCallbacks, returnValue
 from twisted.web2 import responsecode
 from twisted.web2.dav import davxml
 from twisted.web2.dav.element.base import dav_namespace
@@ -40,6 +40,7 @@
 
 max_number_of_results = 1000
 
+ at inlineCallbacks
 def report_urn_ietf_params_xml_ns_caldav_calendar_query(self, request, calendar_query):
     """
     Generate a calendar-query REPORT.
@@ -51,9 +52,7 @@
         raise ValueError("{CalDAV:}calendar-query expected as root element, not %s." % (calendar_query.sname(),))
 
     if not self.isCollection():
-        parent = waitForDeferred(self.locateParent(request, request.uri))
-        yield parent
-        parent = parent.getResult()
+        parent = (yield self.locateParent(request, request.uri))
         if not parent.isPseudoCalendarCollection():
             log.err("calendar-query report is not allowed on a resource outside of a calendar collection %s" % (self,))
             raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Must be calendar collection or calendar resource"))
@@ -99,6 +98,8 @@
         raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-filter")))
 
     matchcount = [0]
+    
+    @inlineCallbacks
     def doQuery(calresource, uri):
         """
         Run a query on the specified calendar collection
@@ -144,25 +145,17 @@
         if calresource.isPseudoCalendarCollection():
             # Get the timezone property from the collection if one was not set in the query,
             # and store in the query filter for later use
-            has_prop = waitForDeferred(calresource.hasProperty((caldav_namespace, "calendar-timezone"), request))
-            yield has_prop
-            has_prop = has_prop.getResult()
+            has_prop = (yield calresource.hasProperty((caldav_namespace, "calendar-timezone"), request))
             if query_tz is None and has_prop:
-                tz = waitForDeferred(calresource.readProperty((caldav_namespace, "calendar-timezone"), request))
-                yield tz
-                tz = tz.getResult()
+                tz = (yield calresource.readProperty((caldav_namespace, "calendar-timezone"), request))
                 filter.settimezone(tz)
 
             # Do some optimisation of access control calculation by determining any inherited ACLs outside of
             # the child resource loop and supply those to the checkPrivileges on each child.
-            filteredaces = waitForDeferred(calresource.inheritedACEsforChildren(request))
-            yield filteredaces
-            filteredaces = filteredaces.getResult()
+            filteredaces = (yield calresource.inheritedACEsforChildren(request))
 
             # Check private events access status
-            d = waitForDeferred(calresource.isOwner(request))
-            yield d
-            isowner = d.getResult()
+            isowner = (yield calresource.isOwner(request))
 
             # Check for disabled access
             if filteredaces is not None:
@@ -172,12 +165,11 @@
                 # Get list of children that match the search and have read access
                 names = [name for name, ignore_uid, ignore_type in calresource.index().search(filter)]
                 if not names:
-                    yield None
                     return
                   
                 # Now determine which valid resources are readable and which are not
                 ok_resources = []
-                d = calresource.findChildrenFaster(
+                yield calresource.findChildrenFaster(
                     "1",
                     request,
                     lambda x, y: ok_resources.append((x, y)),
@@ -186,9 +178,6 @@
                     (davxml.Read(),),
                     inherited_aces=filteredaces
                 )
-                x = waitForDeferred(d)
-                yield x
-                x.getResult()
                 
                 for child, child_uri in ok_resources:
                     child_uri_name = child_uri[child_uri.rfind("/") + 1:]
@@ -200,50 +189,34 @@
                     else:
                         calendar = None
                     
-                    d = waitForDeferred(queryCalendarObjectResource(child, uri, child_uri_name, calendar, query_ok = index_query_ok, isowner=isowner))
-                    yield d
-                    d.getResult()
+                    yield queryCalendarObjectResource(child, uri, child_uri_name, calendar, query_ok = index_query_ok, isowner=isowner)
         else:
             # Get the timezone property from the collection if one was not set in the query,
             # and store in the query object for later use
             if query_tz is None:
 
-                parent = waitForDeferred(calresource.locateParent(request, uri))
-                yield parent
-                parent = parent.getResult()
+                parent = (yield calresource.locateParent(request, uri))
                 assert parent is not None and parent.isPseudoCalendarCollection()
 
-                has_prop = waitForDeferred(parent.hasProperty((caldav_namespace, "calendar-timezone"), request))
-                yield has_prop
-                has_prop = has_prop.getResult()
+                has_prop = (yield parent.hasProperty((caldav_namespace, "calendar-timezone"), request))
                 if has_prop:
-                    tz = waitForDeferred(parent.readProperty((caldav_namespace, "calendar-timezone"), request))
-                    yield tz
-                    tz = tz.getResult()
+                    tz = (yield parent.readProperty((caldav_namespace, "calendar-timezone"), request))
                     filter.settimezone(tz)
 
             # Check private events access status
-            d = waitForDeferred(calresource.isOwner(request))
-            yield d
-            isowner = d.getResult()
+            isowner = (yield calresource.isOwner(request))
 
             calendar = calresource.iCalendar()
-            d = waitForDeferred(queryCalendarObjectResource(calresource, uri, None, calendar))
-            yield d
-            d.getResult()
+            yield queryCalendarObjectResource(calresource, uri, None, calendar)
 
-    doQuery = deferredGenerator(doQuery)
+        returnValue(True)
 
     # Run report taking depth into account
     try:
         depth = request.headers.getHeader("depth", "0")
-        d = waitForDeferred(report_common.applyToCalendarCollections(self, request, request.uri, depth, doQuery, (davxml.Read(),)))
-        yield d
-        d.getResult()
+        yield report_common.applyToCalendarCollections(self, request, request.uri, depth, doQuery, (davxml.Read(),))
     except NumberOfMatchesWithinLimits:
         log.err("Too many matching components in calendar-query report")
         raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (dav_namespace, "number-of-matches-within-limits")))
     
-    yield MultiStatusResponse(responses)
-
-report_urn_ietf_params_xml_ns_caldav_calendar_query = deferredGenerator(report_urn_ietf_params_xml_ns_caldav_calendar_query)
+    returnValue(MultiStatusResponse(responses))

Modified: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/report_common.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/report_common.py	2008-08-11 22:41:16 UTC (rev 2804)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/report_common.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -32,7 +32,7 @@
 
 from vobject.icalendar import utc
 
-from twisted.internet.defer import deferredGenerator, waitForDeferred
+from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.python.failure import Failure
 from twisted.web2 import responsecode
 from twisted.web2.dav import davxml
@@ -54,6 +54,7 @@
 
 log = Logger()
 
+ at inlineCallbacks
 def applyToCalendarCollections(resource, request, request_uri, depth, apply, privileges):
     """
     Run an operation on all calendar collections, starting at the specified
@@ -70,13 +71,11 @@
     """
 
     # First check the privilege on this resource
-    try:
-        d = waitForDeferred(resource.checkPrivileges(request, privileges))
-        yield d
-        d.getResult()
-    except AccessDeniedError:
-        yield None
-        return
+    if privileges:
+        try:
+            yield resource.checkPrivileges(request, privileges)
+        except AccessDeniedError:
+            return
 
     # When scanning we only go down as far as a calendar collection - not into one
     if resource.isPseudoCalendarCollection():
@@ -85,17 +84,13 @@
         resources = [(resource, request_uri)]
     else:
         resources = []
-        d = waitForDeferred(resource.findCalendarCollections(depth, request, lambda x, y: resources.append((x, y)), privileges = privileges))
-        yield d
-        d.getResult()
+        yield resource.findCalendarCollections(depth, request, lambda x, y: resources.append((x, y)), privileges = privileges)
          
     for calresource, uri in resources:
-        d = waitForDeferred(apply(calresource, uri))
-        yield d
-        d.getResult()
+        result = (yield apply(calresource, uri))
+        if not result:
+            break
 
-applyToCalendarCollections = deferredGenerator(applyToCalendarCollections)
-
 def responseForHref(request, responses, href, resource, calendar, propertiesForResource, propertyreq, isowner=True):
     """
     Create an appropriate property status response for the given resource.
@@ -215,6 +210,7 @@
 
     return result, message, generate_calendar_data
 
+ at inlineCallbacks
 def _namedPropertiesForResource(request, props, resource, calendar=None, isowner=True):
     """
     Return the specified properties on the specified resource.
@@ -258,15 +254,11 @@
         else:
             qname = property
     
-        props = waitForDeferred(resource.listProperties(request))
-        yield props
-        props = props.getResult()
+        props = (yield resource.listProperties(request))
 
         if qname in props:
             try:
-                prop = waitForDeferred(resource.readProperty(qname, request))
-                yield prop
-                prop = prop.getResult()
+                prop = (yield resource.readProperty(qname, request))
                 properties_by_status[responsecode.OK].append(prop)
             except HTTPError:
                 f = Failure()
@@ -279,12 +271,12 @@
         else:
             properties_by_status[responsecode.NOT_FOUND].append(propertyName(qname))
     
-    yield properties_by_status
+    returnValue(properties_by_status)
 
-_namedPropertiesForResource = deferredGenerator(_namedPropertiesForResource)
-    
+ at inlineCallbacks
 def generateFreeBusyInfo(request, calresource, fbinfo, timerange, matchtotal,
-                         excludeuid=None, organizer=None, same_calendar_user=False):
+                         excludeuid=None, organizer=None, same_calendar_user=False,
+                         servertoserver=False):
     """
     Run a free busy report on the specified calendar collection
     accumulating the free busy info for later processing.
@@ -299,16 +291,16 @@
         This is used in conjunction with the UID value to process exclusions.
     @param same_calendar_user:   a C{bool} indicating whether the calendar user requesting tyhe free-busy information
         is the same as the calendar user being targeted.
+    @param servertoserver:       a C{bool} indicating whether we are doing a local or remote lookup request.
     """
     
     # First check the privilege on this collection
-    try:
-        d = waitForDeferred(calresource.checkPrivileges(request, (caldavxml.ReadFreeBusy(),)))
-        yield d
-        d.getResult()
-    except AccessDeniedError:
-        yield matchtotal
-        return
+    # TODO: for server-to-server we bypass this right now as we have no way to authorize external users.
+    if not servertoserver:
+        try:
+            yield calresource.checkPrivileges(request, (caldavxml.ReadFreeBusy(),))
+        except AccessDeniedError:
+            returnValue(matchtotal)
 
     #
     # What we do is a fake calendar-query for VEVENT/VFREEBUSYs in the specified time-range.
@@ -329,36 +321,28 @@
 
     # Get the timezone property from the collection, and store in the query filter
     # for use during the query itself.
-    has_prop = waitForDeferred(calresource.hasProperty((caldav_namespace, "calendar-timezone"), request))
-    yield has_prop
-    has_prop = has_prop.getResult()
+    has_prop = (yield calresource.hasProperty((caldav_namespace, "calendar-timezone"), request))
     if has_prop:
-        tz = waitForDeferred(calresource.readProperty((caldav_namespace, "calendar-timezone"), request))
-        yield tz
-        tz = tz.getResult()
+        tz = (yield calresource.readProperty((caldav_namespace, "calendar-timezone"), request))
     else:
         tz = None
     tzinfo = filter.settimezone(tz)
 
     # Do some optimisation of access control calculation by determining any inherited ACLs outside of
     # the child resource loop and supply those to the checkPrivileges on each child.
-    filteredaces = waitForDeferred(calresource.inheritedACEsforChildren(request))
-    yield filteredaces
-    filteredaces = filteredaces.getResult()
+    filteredaces = (yield calresource.inheritedACEsforChildren(request))
 
     for name, uid, type in calresource.index().search(filter): #@UnusedVariable
         
         # Check privileges - must have at least CalDAV:read-free-busy
-        child = waitForDeferred(request.locateChildResource(calresource, name))
-        yield child
-        child = child.getResult()
+        child = (yield request.locateChildResource(calresource, name))
 
-        try:
-            d = waitForDeferred(child.checkPrivileges(request, (caldavxml.ReadFreeBusy(),), inherited_aces=filteredaces))
-            yield d
-            d.getResult()
-        except AccessDeniedError:
-            continue
+        # TODO: for server-to-server we bypass this right now as we have no way to authorize external users.
+        if not servertoserver:
+            try:
+                yield child.checkPrivileges(request, (caldavxml.ReadFreeBusy(),), inherited_aces=filteredaces)
+            except AccessDeniedError:
+                continue
 
         calendar = calresource.iCalendar(name)
         
@@ -395,10 +379,8 @@
             else:
                 assert "Free-busy query returned unwanted component: %s in %r", (name, calresource,)
     
-    yield matchtotal
+    returnValue(matchtotal)
 
-generateFreeBusyInfo = deferredGenerator(generateFreeBusyInfo)
-
 def processEventFreeBusy(calendar, fbinfo, timerange, tzinfo):
     """
     Extract free busy data from a VEVENT component.

Modified: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/report_freebusy.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/report_freebusy.py	2008-08-11 22:41:16 UTC (rev 2804)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/report_freebusy.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -20,7 +20,7 @@
 
 __all__ = ["report_urn_ietf_params_xml_ns_caldav_free_busy_query"]
 
-from twisted.internet.defer import deferredGenerator, waitForDeferred
+from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.web2 import responsecode
 from twisted.web2.dav.element.base import dav_namespace
 from twisted.web2.dav.http import ErrorResponse
@@ -35,6 +35,7 @@
 
 log = Logger()
 
+ at inlineCallbacks
 def report_urn_ietf_params_xml_ns_caldav_free_busy_query(self, request, freebusy): #@UnusedVariable
     """
     Generate a free-busy REPORT.
@@ -55,6 +56,8 @@
     fbinfo = ([], [], [])
     
     matchcount = [0]
+    
+    @inlineCallbacks
     def generateFreeBusyInfo(calresource, uri): #@UnusedVariable
         """
         Run a free busy report on the specified calendar collection
@@ -62,18 +65,13 @@
         @param calresource: the L{CalDAVFile} for a calendar collection.
         @param uri: the uri for the calendar collecton resource.
         """
-        d = waitForDeferred(report_common.generateFreeBusyInfo(request, calresource, fbinfo, timerange, matchcount[0]))
-        yield d
-        matchcount[0] = d.getResult()
-    
-    generateFreeBusyInfo = deferredGenerator(generateFreeBusyInfo)
+        matchcount[0] = (yield report_common.generateFreeBusyInfo(request, calresource, fbinfo, timerange, matchcount[0]))
+        returnValue(True)
 
     # Run report taking depth into account
     try:
         depth = request.headers.getHeader("depth", "0")
-        d = waitForDeferred(report_common.applyToCalendarCollections(self, request, request.uri, depth, generateFreeBusyInfo, (caldavxml.ReadFreeBusy(),)))
-        yield d
-        d.getResult()
+        yield report_common.applyToCalendarCollections(self, request, request.uri, depth, generateFreeBusyInfo, (caldavxml.ReadFreeBusy(),))
     except NumberOfMatchesWithinLimits:
         log.err("Too many matching components in free-busy report")
         raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (dav_namespace, "number-of-matches-within-limits")))
@@ -85,6 +83,4 @@
     response.stream = MemoryStream(str(fbcalendar))
     response.headers.setHeader("content-type", MimeType.fromString("text/calendar; charset=utf-8"))
 
-    yield response
-
-report_urn_ietf_params_xml_ns_caldav_free_busy_query = deferredGenerator(report_urn_ietf_params_xml_ns_caldav_free_busy_query)
+    returnValue(response)

Modified: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/report_multiget.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/report_multiget.py	2008-08-11 22:41:16 UTC (rev 2804)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/method/report_multiget.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -52,7 +52,7 @@
 
     # Make sure target resource is of the right type
     if not self.isCollection():
-        parent = yield self.locateParent(request, request.uri)
+        parent = (yield self.locateParent(request, request.uri))
         if not parent.isPseudoCalendarCollection():
             log.err("calendar-multiget report is not allowed on a resource outside of a calendar collection %s" % (self,))
             raise HTTPError(StatusResponse(responsecode.FORBIDDEN, "Must be calendar resource"))
@@ -105,14 +105,14 @@
 
         # Do some optimisation of access control calculation by determining any inherited ACLs outside of
         # the child resource loop and supply those to the checkPrivileges on each child.
-        filteredaces = yield self.inheritedACEsforChildren(request)
+        filteredaces = (yield self.inheritedACEsforChildren(request))
     
         # Check for disabled access
         if filteredaces is None:
             disabled = True
             
         # Check private events access status
-        isowner = yield self.isOwner(request)
+        isowner = (yield self.isOwner(request))
 
     elif self.isCollection():
         requestURIis = "collection"
@@ -191,13 +191,13 @@
                         responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.NOT_FOUND)))
                         continue
      
-                    child = yield request.locateResource(resource_uri)
+                    child = (yield request.locateResource(resource_uri))
     
                     if not child or not child.exists():
                         responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.NOT_FOUND)))
                         continue
     
-                    parent = yield child.locateParent(request, resource_uri)
+                    parent = (yield child.locateParent(request, resource_uri))
     
                     if not parent.isCalendarCollection() or not parent.index().resourceExists(name):
                         responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.FORBIDDEN)))
@@ -216,17 +216,17 @@
                 
                         # Do some optimisation of access control calculation by determining any inherited ACLs outside of
                         # the child resource loop and supply those to the checkPrivileges on each child.
-                        filteredaces = yield parent.inheritedACEsforChildren(request)
+                        filteredaces = (yield parent.inheritedACEsforChildren(request))
 
                         # Check private events access status
-                        isowner = yield parent.isOwner(request)
+                        isowner = (yield parent.isOwner(request))
                 else:
                     name = unquote(resource_uri[resource_uri.rfind("/") + 1:])
                     if (resource_uri != request.uri) or not self.exists():
                         responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.NOT_FOUND)))
                         continue
     
-                    parent = yield self.locateParent(request, resource_uri)
+                    parent = (yield self.locateParent(request, resource_uri))
     
                     if not parent.isPseudoCalendarCollection() or not parent.index().resourceExists(name):
                         responses.append(davxml.StatusResponse(href, davxml.Status.fromResponseCode(responsecode.FORBIDDEN)))
@@ -235,10 +235,10 @@
             
                     # Do some optimisation of access control calculation by determining any inherited ACLs outside of
                     # the child resource loop and supply those to the checkPrivileges on each child.
-                    filteredaces = yield parent.inheritedACEsforChildren(request)
+                    filteredaces = (yield parent.inheritedACEsforChildren(request))
 
                     # Check private events access status
-                    isowner = yield parent.isOwner(request)
+                    isowner = (yield parent.isOwner(request))
         
                 # Check privileges - must have at least DAV:read
                 try:

Modified: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/resource.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/resource.py	2008-08-11 22:41:16 UTC (rev 2804)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/resource.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -31,8 +31,7 @@
 
 from twisted.internet import reactor
 from twisted.internet.defer import Deferred, maybeDeferred, succeed
-from twisted.internet.defer import waitForDeferred
-from twisted.internet.defer import deferredGenerator
+from twisted.internet.defer import inlineCallbacks, returnValue
 from twisted.web2 import responsecode
 from twisted.web2.dav import davxml
 from twisted.web2.dav.idav import IDAVPrincipalCollectionResource
@@ -68,7 +67,10 @@
 class CalDAVComplianceMixIn(object):
 
     def davComplianceClasses(self):
-        extra_compliance = caldavxml.caldav_compliance
+        if config.Scheduling["CalDAV"]["OldDraftCompatability"]:
+            extra_compliance = caldavxml.caldav_full_compliance
+        else:
+            extra_compliance = caldavxml.caldav_implicit_compliance
         if config.EnableProxyPrincipals:
             extra_compliance += customxml.calendarserver_proxy_compliance
         if config.EnablePrivateEvents:
@@ -236,6 +238,7 @@
 
         return super(CalDAVResource, self).readProperty(property, request)
 
+    @inlineCallbacks
     def writeProperty(self, property, request):
         assert isinstance(property, davxml.WebDAVElement), (
             "%r is not a WebDAVElement instance" % (property,)
@@ -270,8 +273,25 @@
                     (caldav_namespace, "valid-calendar-data")
                 ))
 
-        return super(CalDAVResource, self).writeProperty(property, request)
+        elif property.qname() == (caldav_namespace, "schedule-calendar-transp"):
+            if not self.isCalendarCollection():
+                raise HTTPError(StatusResponse(
+                    responsecode.FORBIDDEN,
+                    "Property %s may only be set on calendar collection." % (property,)
+                ))
 
+            # For backwards compatibility we need to sync this up with the calendar-free-busy-set on the inbox
+            principal = (yield self.ownerPrincipal(request))
+            
+            # Map owner to their inbox
+            inboxURL = principal.scheduleInboxURL()
+            if inboxURL:
+                inbox = (yield request.locateResource(inboxURL))
+                inbox.processFreeBusyCalendar(request.path, property.children[0] == caldavxml.Opaque())
+
+        result = (yield super(CalDAVResource, self).writeProperty(property, request))
+        returnValue(result)
+
     def writeDeadProperty(self, property):
         val = super(CalDAVResource, self).writeDeadProperty(property)
 
@@ -283,20 +303,16 @@
     ##
 
     # FIXME: Perhaps this is better done in authorize() instead.
-    @deferredGenerator
+    @inlineCallbacks
     def accessControlList(self, request, *args, **kwargs):
-        d = waitForDeferred(super(CalDAVResource, self).accessControlList(request, *args, **kwargs))
-        yield d
-        acls = d.getResult()
+        acls = (yield super(CalDAVResource, self).accessControlList(request, *args, **kwargs))
 
         # Look for private events access classification
         if self.hasDeadProperty(TwistedCalendarAccessProperty):
             access = self.readDeadProperty(TwistedCalendarAccessProperty)
             if access.getValue() in (Component.ACCESS_PRIVATE, Component.ACCESS_CONFIDENTIAL, Component.ACCESS_RESTRICTED,):
                 # Need to insert ACE to prevent non-owner principals from seeing this resource
-                d = waitForDeferred(self.owner(request))
-                yield d
-                owner = d.getResult()
+                owner = (yield self.owner(request))
                 if access.getValue() == Component.ACCESS_PRIVATE:
                     ace = davxml.ACE(
                         davxml.Invert(
@@ -326,35 +342,41 @@
                     )
 
                 acls = davxml.ACL(ace, *acls.children)
-        yield acls
+        returnValue(acls)
 
-    @deferredGenerator
+    @inlineCallbacks
     def owner(self, request):
         """
         Return the DAV:owner property value (MUST be a DAV:href or None).
         """
-        d = waitForDeferred(self.locateParent(request, request.urlForResource(self)))
-        yield d
-        parent = d.getResult()
+        parent = (yield self.locateParent(request, request.urlForResource(self)))
         if parent and isinstance(parent, CalDAVResource):
-            d = waitForDeferred(parent.owner(request))
-            yield d
-            yield d.getResult()
+            result = (yield parent.owner(request))
+            returnValue(result)
         else:
-            yield None
+            returnValue(None)
 
-    @deferredGenerator
+    @inlineCallbacks
+    def ownerPrincipal(self, request):
+        """
+        Return the DAV:owner property value (MUST be a DAV:href or None).
+        """
+        parent = (yield self.locateParent(request, request.urlForResource(self)))
+        if parent and isinstance(parent, CalDAVResource):
+            result = (yield parent.ownerPrincipal(request))
+            returnValue(result)
+        else:
+            returnValue(None)
+
+    @inlineCallbacks
     def isOwner(self, request):
         """
         Determine whether the DAV:owner of this resource matches the currently authorized principal
         in the request.
         """
 
-        d = waitForDeferred(self.owner(request))
-        yield d
-        owner = d.getResult()
-        result = (davxml.Principal(owner) == self.currentPrincipal(request))
-        yield result
+        owner = (yield self.owner(request))
+        returnValue(davxml.Principal(owner) == self.currentPrincipal(request))
 
     ##
     # CalDAV
@@ -448,6 +470,49 @@
         """
         unimplemented(self)
 
+    @inlineCallbacks
+    def deletedCalendar(self, request):
+        """
+        Calendar has been deleted. Need to do some extra clean-up.
+
+        @param request:
+        @type request:
+        """
+        
+        # For backwards compatibility we need to sync this up with the calendar-free-busy-set on the inbox
+        principal = (yield self.ownerPrincipal(request))
+        inboxURL = principal.scheduleInboxURL()
+        if inboxURL:
+            inbox = (yield request.locateResource(inboxURL))
+            inbox.processFreeBusyCalendar(request.path, False)
+
+    @inlineCallbacks
+    def movedCalendar(self, request, destination, destination_uri):
+        """
+        Calendar has been deleted. Need to do some extra clean-up.
+
+        @param request:
+        @type request:
+        """
+        
+        # For backwards compatibility we need to sync this up with the calendar-free-busy-set on the inbox
+        principal = (yield self.ownerPrincipal(request))
+        inboxURL = principal.scheduleInboxURL()
+        if inboxURL:
+            inbox = (yield request.locateResource(inboxURL))
+            inbox.processFreeBusyCalendar(request.path, False)
+            inbox.processFreeBusyCalendar(destination_uri, destination.isCalendarOpaque())
+
+    def isCalendarOpaque(self):
+        
+        assert self.isCalendarCollection()
+        
+        if self.hasDeadProperty((caldav_namespace, "schedule-calendar-transp")):
+            property = self.readDeadProperty((caldav_namespace, "schedule-calendar-transp"))
+            return property.children[0] == caldavxml.Opaque()
+        else:
+            return False
+
     def iCalendar(self, name=None):
         """
         See L{ICalDAVResource.iCalendar}.

Modified: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/schedule.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/schedule.py	2008-08-11 22:41:16 UTC (rev 2804)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/schedule.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -1,5 +1,5 @@
 ##
-# Copyright (c) 2005-2008 Apple Inc. All rights reserved.
+# Copyright (c) 2005-2007 Apple Inc. All rights reserved.
 #
 # Licensed under the Apache License, Version 2.0 (the "License");
 # you may not use this file except in compliance with the License.
@@ -21,35 +21,27 @@
 __all__ = [
     "ScheduleInboxResource",
     "ScheduleOutboxResource",
+    "IScheduleInboxResource",
 ]
 
-import md5
-import time
-
-from twisted.internet import reactor
-from twisted.internet.defer import maybeDeferred, succeed, inlineCallbacks, returnValue
-from twisted.python.failure import Failure
+from twisted.internet.defer import succeed, inlineCallbacks, returnValue
 from twisted.web2 import responsecode
-from twisted.web2.http import HTTPError, Response
-from twisted.web2.http_headers import MimeType
 from twisted.web2.dav import davxml
-from twisted.web2.dav.http import ErrorResponse, errorForFailure, messageForFailure, statusForFailure
+from twisted.web2.dav.http import ErrorResponse
 from twisted.web2.dav.idav import IDAVResource
 from twisted.web2.dav.resource import AccessDeniedError
 from twisted.web2.dav.util import joinURL
+from twisted.web2.http import HTTPError
+from twisted.web2.http import Response
+from twisted.web2.http_headers import MimeType
 
 from twistedcaldav import caldavxml
-from twistedcaldav import itip
-from twistedcaldav.log import LoggingMixIn
-from twistedcaldav.accounting import accountingEnabled, emitAccounting
-from twistedcaldav.resource import CalDAVResource
-from twistedcaldav.caldavxml import caldav_namespace, TimeRange
+from twistedcaldav.caldavxml import caldav_namespace
 from twistedcaldav.config import config
 from twistedcaldav.customxml import calendarserver_namespace
-from twistedcaldav.ical import Component
-from twistedcaldav.method import report_common
-from twistedcaldav.method.put_common import storeCalendarObjectResource
+from twistedcaldav.resource import CalDAVResource
 from twistedcaldav.resource import isCalendarCollectionResource
+from twistedcaldav.scheduling.scheduler import CalDAVScheduler, IScheduleScheduler
 
 class CalendarSchedulingCollectionResource (CalDAVResource):
     """
@@ -93,6 +85,7 @@
 
     liveProperties = CalendarSchedulingCollectionResource.liveProperties + (
         (caldav_namespace, "calendar-free-busy-set"),
+        (caldav_namespace, "schedule-default-calendar-URL"),
     )
 
     def resourceType(self):
@@ -119,6 +112,10 @@
             # Always return at least an empty list
             if not self.hasDeadProperty(property):
                 return succeed(caldavxml.CalendarFreeBusySet())
+        elif qname == (caldav_namespace, "schedule-default-calendar-URL"):
+            # Always return at least an empty item
+            if not self.hasDeadProperty(property):
+                return succeed(caldavxml.ScheduleDefaultCalendarURL())
             
         return super(ScheduleInboxResource, self).readProperty(property, request)
 
@@ -147,7 +144,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(
@@ -155,8 +152,35 @@
                         (caldav_namespace, "valid-calendar-url")
                     ))
 
+        elif property.qname() == (caldav_namespace, "schedule-default-calendar-URL"):
+            # Verify that the calendar added in the PROPPATCH is valid.
+            new_calendar = [str(href) for href in property.children]
+            if len(new_calendar) == 1:
+                cal = (yield request.locateResource(str(new_calendar[0])))
+                if cal is None or not cal.exists() or not isCalendarCollectionResource(cal):
+                    # Validate that href's point to a valid calendar.
+                    raise HTTPError(ErrorResponse(
+                        responsecode.CONFLICT,
+                        (caldav_namespace, "valid-calendar-url")
+                    ))
+
         yield super(ScheduleInboxResource, self).writeProperty(property, request)
 
+    def processFreeBusyCalendar(self, uri, addit):
+        if not self.hasDeadProperty((caldav_namespace, "calendar-free-busy-set")):
+            fbset = set()
+        else:
+            fbset = set([str(href) for href in self.readDeadProperty((caldav_namespace, "calendar-free-busy-set")).children])
+        if addit:
+            if uri not in fbset:
+                fbset.add(uri)
+                self.writeDeadProperty(caldavxml.CalendarFreeBusySet(*[davxml.HRef(url) for url in fbset]))
+        else:
+            if uri in fbset:
+                fbset.remove(uri)
+                self.writeDeadProperty(caldavxml.CalendarFreeBusySet(*[davxml.HRef(url) for url in fbset]))
+
+
     @inlineCallbacks
     def http_GET(self, request):
         """
@@ -269,402 +293,81 @@
         # Check authentication and access controls
         yield self.authorize(request, (caldavxml.Schedule(),))
 
-        # Must be content-type text/calendar
-        contentType = request.headers.getHeader("content-type")
-        if contentType is not None and (contentType.mediaType, contentType.mediaSubtype) != ("text", "calendar"):
-            self.log_error("MIME type %s not allowed in calendar collection" % (contentType,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "supported-calendar-data")))
-    
-        # Must have Originator header
-        originator = request.headers.getRawHeaders("originator")
-        if originator is None or (len(originator) != 1):
-            self.log_error("POST request must have Originator header")
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-specified")))
-        else:
-            originator = originator[0]
-    
-        # Verify that Originator is a valid calendar user (has an INBOX)
-        originatorPrincipal = self.principalForCalendarUserAddress(originator)
-        if originatorPrincipal is None:
-            self.log_error("Could not find principal for originator: %s" % (originator,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+        # This is a local CALDAV scheduling operation.
+        scheduler = CalDAVScheduler(request, self)
 
-        inboxURL = originatorPrincipal.scheduleInboxURL()
-        if inboxURL is None:
-            self.log_error("Could not find inbox for originator: %s" % (originator,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
-    
-        # Verify that Originator matches the authenticated user
-        if davxml.Principal(davxml.HRef(originatorPrincipal.principalURL())) != self.currentPrincipal(request):
-            self.log_error("Originator: %s does not match authorized user: %s" % (originator, self.currentPrincipal(request).children[0],))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "originator-allowed")))
+        # Do the POST processing treating
+        response = (yield scheduler.doSchedulingViaPOST())
+        returnValue(response)
 
-        # Get list of Recipient headers
-        rawRecipients = request.headers.getRawHeaders("recipient")
-        if rawRecipients is None or (len(rawRecipients) == 0):
-            self.log_error("POST request must have at least one Recipient header")
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-specified")))
+class IScheduleInboxResource (CalDAVResource):
+    """
+    iSchedule Inbox resource.
 
-        # Recipient header may be comma separated list
-        recipients = []
-        for rawRecipient in rawRecipients:
-            for r in rawRecipient.split(","):
-                r = r.strip()
-                if len(r):
-                    recipients.append(r)
+    Extends L{DAVResource} to provide iSchedule inbox functionality.
+    """
 
-        timeRange = TimeRange(start="20000101", end="20000102")
-        recipientsState = {"OK":0, "BAD":0}
+    def __init__(self, parent):
+        """
+        @param parent: the parent resource of this one.
+        """
+        assert parent is not None
 
-        # Parse the calendar object from the HTTP request stream
-        try:
-            calendar = yield Component.fromIStream(request.stream)
-        except:
-            self.log_error("Error while handling POST: %s" % (Failure(),))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
- 
-        # Must be a valid calendar
-        try:
-            calendar.validCalendarForCalDAV()
-        except ValueError:
-            self.log_error("POST request calendar component is not valid: %s" % (calendar,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+        CalDAVResource.__init__(self, principalCollections=parent.principalCollections())
 
-        # Must have a METHOD
-        if not calendar.isValidMethod():
-            self.log_error("POST request must have valid METHOD property in calendar component: %s" % (calendar,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
-        
-        # Verify iTIP behaviour
-        if not calendar.isValidITIP():
-            self.log_error("POST request must have a calendar component that satisfies iTIP requirements: %s" % (calendar,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+        self.parent = parent
 
-        # X-CALENDARSERVER-ACCESS is not allowed in Outbox POSTs
-        if calendar.hasProperty(Component.ACCESS_PROPERTY):
-            self.log_error("X-CALENDARSERVER-ACCESS not allowed in a calendar component POST request: %s" % (calendar,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (calendarserver_namespace, "no-access-restrictions")))
-    
-        # Verify that the ORGANIZER's cu address maps to the request.uri
-        organizer = calendar.getOrganizer()
-        if organizer is None:
-            organizerPrincipal = None
-        else:
-            organizerPrincipal = self.principalForCalendarUserAddress(organizer)
+    def defaultAccessControlList(self):
+        return davxml.ACL(
+            # DAV:Read, CalDAV:schedule for all principals (includes anonymous)
+            davxml.ACE(
+                davxml.Principal(davxml.All()),
+                davxml.Grant(
+                    davxml.Privilege(davxml.Read()),
+                    davxml.Privilege(caldavxml.Schedule()),
+                ),
+                davxml.Protected(),
+            ),
+        )
 
-        if organizerPrincipal is None:
-            self.log_error("ORGANIZER in calendar data is not valid: %s" % (calendar,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+    def resourceType(self):
+        return davxml.ResourceType.ischeduleinbox
 
-        # Prevent spoofing of ORGANIZER with specific METHODs
-        if (
-            calendar.propertyValue("METHOD") in ("PUBLISH", "REQUEST", "ADD", "CANCEL", "DECLINECOUNTER") and
-            organizerPrincipal.record != self.parent.record
-        ):
-            self.log_error("ORGANIZER in calendar data does not match owner of Outbox: %s" % (calendar,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "organizer-allowed")))
+    def isCollection(self):
+        return False
 
-        # Prevent spoofing when doing reply-like METHODs
-        if calendar.propertyValue("METHOD") in ("REPLY", "COUNTER", "REFRESH"):
-            # Verify that there is a single ATTENDEE property and that the Originator has permission
-            # to send on behalf of that ATTENDEE
-            attendees = calendar.getAttendees()
-        
-            # Must have only one
-            if len(attendees) != 1:
-                self.log_error("ATTENDEE list in calendar data is wrong: %s" % (calendar,))
-                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
-            
-            # Attendee's Outbox MUST be the request URI
-            attendeePrincipal = self.principalForCalendarUserAddress(attendees[0])
-            if attendeePrincipal is None or attendeePrincipal.record != self.parent.record:
-                self.log_error("ATTENDEE in calendar data does not match owner of Outbox: %s" % (calendar,))
-                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "attendee-allowed")))
+    def isCalendarCollection(self):
+        return False
 
-        # For free-busy do immediate determination of iTIP result rather than fan-out
-        self.log_debug("METHOD: %s, Component: %s" % (calendar.propertyValue("METHOD"), calendar.mainType(),))
-        if (calendar.propertyValue("METHOD") == "REQUEST") and (calendar.mainType() == "VFREEBUSY"):
-            # Extract time range from VFREEBUSY object
-            vfreebusies = [v for v in calendar.subcomponents() if v.name() == "VFREEBUSY"]
-            if len(vfreebusies) != 1:
-                self.log_error("iTIP data is not valid for a VFREEBUSY request: %s" % (calendar,))
-                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
-            dtstart = vfreebusies[0].getStartDateUTC()
-            dtend = vfreebusies[0].getEndDateUTC()
-            if dtstart is None or dtend is None:
-                self.log_error("VFREEBUSY start/end not valid: %s" % (calendar,))
-                raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
-            timeRange.start = dtstart
-            timeRange.end = dtend
+    def isPseudoCalendarCollection(self):
+        return False
 
-            # Look for masked UID
-            excludeUID = calendar.getMaskUID()
+    def render(self, request):
+        output = """<html>
+<head>
+<title>Server To Server Inbox Resource</title>
+</head>
+<body>
+<h1>Server To Server Inbox Resource.</h1>
+</body
+</html>"""
 
-            # Do free busy operation
-            freebusy = True
-        else:
-            # Do regular invite (fan-out)
-            freebusy = False
+        response = Response(200, {}, output)
+        response.headers.setHeader("content-type", MimeType("text", "html"))
+        return response
 
-        #
-        # Accounting
-        #
-        # Note that we associate logging with the organizer, not the
-        # originator, which is good for looking for why something
-        # shows up in a given principal's calendars, rather than
-        # tracking the activities of a specific user.
-        #
-        if accountingEnabled("iTIP", organizerPrincipal):
-            emitAccounting(
-                "iTIP", organizerPrincipal,
-                "Originator: %s\nRecipients: %s\n\n%s"
-                % (originator, ", ".join(recipients), str(calendar))
-            )
-
-        # Prepare for multiple responses
-        responses = ScheduleResponseQueue("POST", responsecode.OK)
-    
-        # Loop over each recipient and do appropriate action.
-        autoresponses = []
-        for recipient in recipients:
-            # Get the principal resource for this recipient
-            principal = self.principalForCalendarUserAddress(recipient)
-
-            # Map recipient to their inbox
-            inbox = None
-            if principal is None:
-                self.log_error("No schedulable principal for calendar user address: %r" % (recipient,))
-            else:
-                inboxURL = principal.scheduleInboxURL()
-                if inboxURL:
-                    inbox = yield request.locateResource(inboxURL)
-                else:
-                    self.log_error("No schedule inbox for principal: %s" % (principal,))
-
-            if inbox is None:
-                err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-exists")))
-                responses.add(recipient, Failure(exc_value=err), reqstatus="3.7;Invalid Calendar User")
-                recipientsState["BAD"] += 1
-            
-                # Process next recipient
-                continue
-            else:
-                #
-                # Check access controls
-                #
-                try:
-                    yield inbox.checkPrivileges(request, (caldavxml.Schedule(),), principal=davxml.Principal(davxml.HRef(organizerPrincipal.principalURL())))
-                except AccessDeniedError:
-                    self.log_error("Could not access Inbox for recipient: %s" % (recipient,))
-                    err = HTTPError(ErrorResponse(responsecode.NOT_FOUND, (caldav_namespace, "recipient-permisions")))
-                    responses.add(recipient, Failure(exc_value=err), reqstatus="3.8;No authority")
-                    recipientsState["BAD"] += 1
-                
-                    # Process next recipient
-                    continue
-    
-                # Different behaviour for free-busy vs regular invite
-                if freebusy:
-                    # Extract the ATTENDEE property matching current recipient from the calendar data
-                    cuas = principal.calendarUserAddresses()
-                    attendeeProp = calendar.getAttendeeProperty(cuas)
-            
-                    # Find the current recipients calendar-free-busy-set
-                    fbset = yield principal.calendarFreeBusyURIs(request)
-
-                    # First list is BUSY, second BUSY-TENTATIVE, third BUSY-UNAVAILABLE
-                    fbinfo = ([], [], [])
-                
-                    try:
-                        # Process the availability property from the Inbox.
-                        has_prop = yield inbox.hasProperty((calendarserver_namespace, "calendar-availability"), request)
-                        if has_prop:
-                            availability = yield inbox.readProperty((calendarserver_namespace, "calendar-availability"), request)
-                            availability = availability.calendar()
-                            report_common.processAvailabilityFreeBusy(availability, fbinfo, timeRange)
-
-                        # Check to see if the recipient is the same calendar user as the organizer.
-                        # Needed for masked UID stuff.
-                        same_calendar_user = organizerPrincipal.principalURL() == principal.principalURL()
-
-                        # Now process free-busy set calendars
-                        matchtotal = 0
-                        for calendarResourceURL in fbset:
-                            calendarResource = yield 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(
-                                request,
-                                calendarResource,
-                                fbinfo,
-                                timeRange,
-                                matchtotal,
-                                excludeuid = excludeUID,
-                                organizer = organizer,
-                                same_calendar_user = same_calendar_user
-                            )
-                    
-                        # Build VFREEBUSY iTIP reply for this recipient
-                        fbresult = report_common.buildFreeBusyResult(
-                            fbinfo,
-                            timeRange,
-                            organizer = calendar.getOrganizerProperty(),
-                            attendee = attendeeProp,
-                            uid = calendar.resourceUID(),
-                            method="REPLY"
-                        )
-
-                        responses.add(recipient, responsecode.OK, reqstatus="2.0;Success", calendar=fbresult)
-                        recipientsState["OK"] += 1
-                
-                    except:
-                        self.log_error("Could not determine free busy information: %s" % (recipient,))
-                        err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-permissions")))
-                        responses.add(recipient, Failure(exc_value=err), reqstatus="3.8;No authority")
-                        recipientsState["BAD"] += 1
-                
-                else:
-                    # Hash the iCalendar data for use as the last path element of the URI path
-                    name = md5.new(str(calendar) + str(time.time()) + inbox.fp.path).hexdigest() + ".ics"
-                
-                    # Get a resource for the new item
-                    childURL = joinURL(inboxURL, name)
-                    child = yield request.locateResource(childURL)
-            
-                    try:
-                        # Copy calendar to inbox (doing fan-out)
-                        yield maybeDeferred(
-                            storeCalendarObjectResource,
-                            request=request,
-                            sourcecal = False,
-                            destination = child,
-                            destination_uri = childURL,
-                            calendardata = str(calendar),
-                            destinationparent = inbox,
-                            destinationcal = True,
-                            isiTIP = True
-                        )
-                    except: # FIXME: bare except
-                        self.log_error("Could not store data in Inbox : %s" % (inbox,))
-                        err = HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "recipient-permissions")))
-                        responses.add(recipient, Failure(exc_value=err), reqstatus="3.8;No authority")
-                        recipientsState["BAD"] += 1
-                    else:
-                        responses.add(recipient, responsecode.OK, reqstatus="2.0;Success")
-                        recipientsState["OK"] += 1
-        
-                        # Store CALDAV:originator property
-                        child.writeDeadProperty(caldavxml.Originator(davxml.HRef(originator)))
-                    
-                        # Store CALDAV:recipient property
-                        child.writeDeadProperty(caldavxml.Recipient(davxml.HRef(recipient)))
-                    
-                        # Look for auto-schedule option
-                        if principal.autoSchedule():
-                            autoresponses.append((principal, inbox, child))
-
-        # Now we have to do auto-respond
-        if len(autoresponses) != 0:
-            # First check that we have a method that we can auto-respond to
-            if not itip.canAutoRespond(calendar):
-                autoresponses = []
-            
-        # Now do the actual auto response
-        for principal, inbox, child in autoresponses:
-            # Add delayed reactor task to handle iTIP responses
-            reactor.callLater(0.0, itip.handleRequest, *(request, principal, inbox, calendar.duplicate(), child)) #@UndefinedVariable
-            #reactor.callInThread(itip.handleRequest, *(request, principal, inbox, calendar.duplicate(), child)) #@UndefinedVariable
-
-        # Return with final response if we are done
-        returnValue(responses.response())
-
-class ScheduleResponseResponse (Response):
-    """
-    ScheduleResponse L{Response} object.
-    Renders itself as a CalDAV:schedule-response XML document.
-    """
-    def __init__(self, xml_responses, location=None):
+    @inlineCallbacks
+    def http_POST(self, request):
         """
-        @param xml_responses: an interable of davxml.Response objects.
-        @param location:      the value of the location header to return in the response,
-            or None.
+        The server-to-server POST method.
         """
 
-        Response.__init__(self, code=responsecode.OK,
-                          stream=caldavxml.ScheduleResponse(*xml_responses).toxml())
+        # Check authentication and access controls
+        yield self.authorize(request, (caldavxml.Schedule(),))
 
-        self.headers.setHeader("content-type", MimeType("text", "xml"))
-    
-        if location is not None:
-            self.headers.setHeader("location", location)
+        # This is a server-to-server scheduling operation.
+        scheduler = IScheduleScheduler(request, self)
 
-class ScheduleResponseQueue (LoggingMixIn):
-    """
-    Stores a list of (typically error) responses for use in a
-    L{ScheduleResponse}.
-    """
-    def __init__(self, method, success_response):
-        """
-        @param method: the name of the method generating the queue.
-        @param success_response: the response to return in lieu of a
-            L{ScheduleResponse} if no responses are added to this queue.
-        """
-        self.responses         = []
-        self.method            = method
-        self.success_response  = success_response
-        self.location          = None
-
-    def setLocation(self, location):
-        """
-        @param location:      the value of the location header to return in the response,
-            or None.
-        """
-        self.location = location
-
-    def add(self, recipient, what, reqstatus=None, calendar=None):
-        """
-        Add a response.
-        @param recipient: the recipient for this response.
-        @param what: a status code or a L{Failure} for the given recipient.
-        @param status: the iTIP request-status for the given recipient.
-        @param calendar: the calendar data for the given recipient response.
-        """
-        if type(what) is int:
-            code    = what
-            error   = None
-            message = responsecode.RESPONSES[code]
-        elif isinstance(what, Failure):
-            code    = statusForFailure(what)
-            error   = errorForFailure(what)
-            message = messageForFailure(what)
-        else:
-            raise AssertionError("Unknown data type: %r" % (what,))
-
-        if code > 400: # Error codes only
-            self.log_error("Error during %s for %s: %s" % (self.method, recipient, message))
-
-        children = []
-        children.append(caldavxml.Recipient(davxml.HRef.fromString(recipient)))
-        children.append(caldavxml.RequestStatus(reqstatus))
-        if calendar is not None:
-            children.append(caldavxml.CalendarData.fromCalendar(calendar))
-        if error is not None:
-            children.append(error)
-        if message is not None:
-            children.append(davxml.ResponseDescription(message))
-        self.responses.append(caldavxml.Response(*children))
-
-    def response(self):
-        """
-        Generate a L{ScheduleResponseResponse} with the responses contained in the
-        queue or, if no such responses, return the C{success_response} provided
-        to L{__init__}.
-        @return: the response.
-        """
-        if self.responses:
-            return ScheduleResponseResponse(self.responses, self.location)
-        else:
-            return self.success_response
+        # Do the POST processing treating this as a non-local schedule
+        response = (yield scheduler.doSchedulingViaPOST())
+        returnValue(response)

Copied: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling (from rev 2800, CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling)

Deleted: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/__init__.py	2008-08-11 21:12:04 UTC (rev 2800)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/__init__.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -1,26 +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.
-##
-
-__all__ = [
-    "addressmapping",
-    "caldav",
-    "cuaddress",
-    "delivery",
-    "imip",
-    "implicit",
-    "ischedule",
-    "scheduler",
-]

Copied: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/__init__.py (from rev 2800, CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/__init__.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/__init__.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/__init__.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -0,0 +1,26 @@
+##
+# 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",
+    "implicit",
+    "ischedule",
+    "scheduler",
+]

Deleted: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/addressmapping.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/addressmapping.py	2008-08-11 21:12:04 UTC (rev 2800)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/addressmapping.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -1,91 +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 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:
-            if principal:
-                returnValue(LocalCalendarUser(cuaddr, principal))
-            else:
-                returnValue(InvalidCalendarUser(cuaddr))
-        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()

Copied: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/addressmapping.py (from rev 2800, CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/addressmapping.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/addressmapping.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/addressmapping.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -0,0 +1,91 @@
+##
+# 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:
+            if principal:
+                returnValue(LocalCalendarUser(cuaddr, principal))
+            else:
+                returnValue(InvalidCalendarUser(cuaddr))
+        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()

Deleted: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/caldav.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/caldav.py	2008-08-11 21:12:04 UTC (rev 2800)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/caldav.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -1,264 +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.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.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.generateFreeBusyResponse(recipient, self.responses, organizerProp, uid)
-            else:
-                yield self.generateResponse(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 generateResponse(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:
-            from twistedcaldav.method.put_common import StoreCalendarObjectResource
-            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)))
-        
-            # Store CALDAV:schedule-state property
-            child.writeDeadProperty(caldavxml.ScheduleState(caldavxml.ScheduleUnprocessed()))
-        
-            # Look for auto-schedule option
-            if recipient.principal.autoSchedule():
-                autoresponses.append((recipient.principal, recipient.inbox, child))
-                
-            returnValue(True)
-    
-    @inlineCallbacks
-    def generateFreeBusyResponse(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)

Copied: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/caldav.py (from rev 2800, CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/caldav.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/caldav.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/caldav.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -0,0 +1,264 @@
+##
+# 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.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.generateFreeBusyResponse(recipient, self.responses, organizerProp, uid)
+            else:
+                yield self.generateResponse(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 generateResponse(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:
+            from twistedcaldav.method.put_common import StoreCalendarObjectResource
+            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)))
+        
+            # Store CALDAV:schedule-state property
+            child.writeDeadProperty(caldavxml.ScheduleState(caldavxml.ScheduleUnprocessed()))
+        
+            # Look for auto-schedule option
+            if recipient.principal.autoSchedule():
+                autoresponses.append((recipient.principal, recipient.inbox, child))
+                
+            returnValue(True)
+    
+    @inlineCallbacks
+    def generateFreeBusyResponse(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)

Deleted: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/cuaddress.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/cuaddress.py	2008-08-11 21:12:04 UTC (rev 2800)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/cuaddress.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -1,77 +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 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):
-        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,)
-

Copied: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/cuaddress.py (from rev 2800, CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/cuaddress.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/cuaddress.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/cuaddress.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -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):
+        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,)
+

Deleted: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/delivery.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/delivery.py	2008-08-11 21:12:04 UTC (rev 2800)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/delivery.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -1,64 +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 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

Copied: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/delivery.py (from rev 2800, CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/delivery.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/delivery.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/delivery.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -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

Deleted: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/icaldiff.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/icaldiff.py	2008-08-11 21:12:04 UTC (rev 2800)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/icaldiff.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -1,209 +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 twistedcaldav.ical import Component
-from twistedcaldav.log import Logger
-
-"""
-Class that handles diff'ing two calendar objects.
-"""
-
-__all__ = [
-    "iCalDiff",
-]
-
-log = Logger()
-
-class iCalDiff(object):
-    
-    def __init__(self, calendar1, calendar2):
-        """
-        
-        @param calendar1:
-        @type calendar1:
-        @param calendar2:
-        @type calendar2:
-        """
-        
-        self.calendar1 = calendar1
-        self.calendar2 = calendar2
-    
-    def organizerDiff(self):
-        """
-        Diff the two calendars looking for changes that should trigger implicit scheduling if
-        changed by an organizer. Basically any change except for anything related to a VALARM.
-        """
-        
-        # Do straight comparison without alarms
-        self.calendar1 = self.calendar1.duplicate()
-        self.calendar1.removeAlarms()
-        self.calendar2 = self.calendar2.duplicate()
-        self.calendar2.removeAlarms()
-
-        return self.calendar1 == self.calendar2
-
-    def attendeeMerge(self, attendee):
-        """
-        Merge the ATTENDEE specific changes with the organizer's view of the attendee's event.
-        This will remove any attempt by the attendee to change things like the time or location.
-       
-        @param attendee: the value of the ATTENDEE property corresponding to the attendee making the change
-        @type attendee: C{str}
-        """
-        
-        self.attendee = attendee
-
-        # Do straight comparison without alarms
-        self.calendar1 = self.calendar1.duplicate()
-        self.calendar1.removeAlarms()
-        self.calendar1.removeXProperties()
-        self.calendar1.attendeesView((attendee,))
-
-        self.calendar2 = self.calendar2.duplicate()
-        self.calendar2.removeAlarms()
-        self.calendar2.removeXProperties()
-
-        if self.calendar1 == self.calendar2:
-            return True, True
-
-        # Need to look at each component and do special comparisons
-        
-        # Make sure the same VCALENDAR properties match
-        if not self._checkVCALENDARProperties():
-            return False, False
-        
-        # Make sure the same VTIMEZONE components appear
-        if not self._compareVTIMEZONEs():
-            return False, False
-        
-        # Compare each component instance from the new calendar with each derived
-        # component instance from the old one
-        return self._compareComponents()
-    
-    def _checkVCALENDARProperties(self):
-
-        # Get property differences in the VCALENDAR objects
-        propdiff = set(self.calendar1.properties()) ^ set(self.calendar2.properties())
-        
-        # Ignore certain properties
-        ignored = ("PRODID", "CALSCALE",)
-        propdiff = set([prop for prop in propdiff if prop.name() not in ignored])
-        
-        result = len(propdiff) == 0
-        if not result:
-            log.debug("VCALENDAR properties differ: %s" % (propdiff,))
-        return result
-
-    def _compareVTIMEZONEs(self):
-
-        # FIXME: clients may re-write timezones so the best we can do is
-        # compare TZIDs. That is not ideal as a client could have an old version
-        # of a VTIMEZONE and thus could show events at different times than the
-        # organizer.
-        
-        def extractTZIDs(calendar):
-
-            tzids = set()
-            for component in calendar.subcomponents():
-                if component.name() == "VTIMEZONE":
-                    tzids.add(component.propertyValue("TZID"))
-            return tzids
-        
-        tzids1 = extractTZIDs(self.calendar1)
-        tzids2 = extractTZIDs(self.calendar2)
-        result = tzids1 == tzids2
-        if not result:
-            log.debug("Different VTIMEZONES: %s %s" % (tzids1, tzids2))
-        return result
-
-    def _compareComponents(self):
-        
-        # First get uid/rid map of components
-        def mapComponents(calendar):
-            map = {}
-            for component in calendar.subcomponents():
-                if component.name() == "VTIMEZONE":
-                    continue
-                name = component.name()
-                uid = component.propertyValue("UID")
-                rid = component.getRecurrenceIDUTC()
-                map[(name, uid, rid,)] = component
-            return map
-        
-        map1 = mapComponents(self.calendar1)
-        set1 = set(map1.keys())
-        map2 = mapComponents(self.calendar2)
-        set2 = set(map2.keys())
-
-        # All the components in calendar1 must be in calendar2
-        result = set1 - set2
-        if result:
-            log.debug("Missing components from first calendar: %s" % (result,))
-        if result:
-            return False, False
-
-        # Now verify that each component in set1 matches what is in set2
-        attendee_unchanged = True
-        for key, value in map1.iteritems():
-            component1 = value
-            component2 = map2[key]
-            
-            nomismatch, no_attendee_change = self._testComponents(component1, component2)
-            if not nomismatch:
-                return False, False
-            attendee_unchanged &= no_attendee_change
-        
-        # Now verify that each additional component in set2 matches a derived component in set1
-        for key in set2 - set1:
-            component1 = self.calendar1.deriveInstance(key[2])
-            if component1 is None:
-                return False, False
-            component2 = map2[key]
-            
-            nomismatch, no_attendee_change = self._testComponents(component1, component2)
-            if not nomismatch:
-                return False, False
-            attendee_unchanged &= no_attendee_change
-            
-        return True, attendee_unchanged
-
-    def _testComponents(self, comp1, comp2):
-        
-        assert isinstance(comp1, Component) and isinstance(comp2, Component)
-        
-        if comp1.name() != comp2.name():
-            log.debug("Component names are different: '%s' and '%s'" % (comp1.name(), comp2.name()))
-            return False, False
-        
-        # Only accept a change to this attendee's own ATTENDEE property
-        propdiff = set(comp1.properties()) ^ set(comp2.properties())
-        for prop in tuple(propdiff):
-            # These ones are OK to change
-            if prop.name() in ("TRANSP", "DTSTAMP", "CREATED", "LAST-MODIFIED",):
-                propdiff.remove(prop)
-                continue
-            if prop.name() != "ATTENDEE" or prop.value() != self.attendee:
-                log.debug("Component properties are different: %s" % (propdiff,))
-                return False, False
-
-        # Compare subcomponents.
-        # NB at this point we assume VALARMS have been removed.
-        result = set(comp1.subcomponents()) ^ set(comp2.subcomponents())
-        if result:
-            log.debug("Sub-components are different: %s" % (result,))
-            return False, False
-        
-        return True, len(propdiff) == 0

Copied: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/icaldiff.py (from rev 2800, CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/icaldiff.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/icaldiff.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/icaldiff.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -0,0 +1,209 @@
+##
+# 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.ical import Component
+from twistedcaldav.log import Logger
+
+"""
+Class that handles diff'ing two calendar objects.
+"""
+
+__all__ = [
+    "iCalDiff",
+]
+
+log = Logger()
+
+class iCalDiff(object):
+    
+    def __init__(self, calendar1, calendar2):
+        """
+        
+        @param calendar1:
+        @type calendar1:
+        @param calendar2:
+        @type calendar2:
+        """
+        
+        self.calendar1 = calendar1
+        self.calendar2 = calendar2
+    
+    def organizerDiff(self):
+        """
+        Diff the two calendars looking for changes that should trigger implicit scheduling if
+        changed by an organizer. Basically any change except for anything related to a VALARM.
+        """
+        
+        # Do straight comparison without alarms
+        self.calendar1 = self.calendar1.duplicate()
+        self.calendar1.removeAlarms()
+        self.calendar2 = self.calendar2.duplicate()
+        self.calendar2.removeAlarms()
+
+        return self.calendar1 == self.calendar2
+
+    def attendeeMerge(self, attendee):
+        """
+        Merge the ATTENDEE specific changes with the organizer's view of the attendee's event.
+        This will remove any attempt by the attendee to change things like the time or location.
+       
+        @param attendee: the value of the ATTENDEE property corresponding to the attendee making the change
+        @type attendee: C{str}
+        """
+        
+        self.attendee = attendee
+
+        # Do straight comparison without alarms
+        self.calendar1 = self.calendar1.duplicate()
+        self.calendar1.removeAlarms()
+        self.calendar1.removeXProperties()
+        self.calendar1.attendeesView((attendee,))
+
+        self.calendar2 = self.calendar2.duplicate()
+        self.calendar2.removeAlarms()
+        self.calendar2.removeXProperties()
+
+        if self.calendar1 == self.calendar2:
+            return True, True
+
+        # Need to look at each component and do special comparisons
+        
+        # Make sure the same VCALENDAR properties match
+        if not self._checkVCALENDARProperties():
+            return False, False
+        
+        # Make sure the same VTIMEZONE components appear
+        if not self._compareVTIMEZONEs():
+            return False, False
+        
+        # Compare each component instance from the new calendar with each derived
+        # component instance from the old one
+        return self._compareComponents()
+    
+    def _checkVCALENDARProperties(self):
+
+        # Get property differences in the VCALENDAR objects
+        propdiff = set(self.calendar1.properties()) ^ set(self.calendar2.properties())
+        
+        # Ignore certain properties
+        ignored = ("PRODID", "CALSCALE",)
+        propdiff = set([prop for prop in propdiff if prop.name() not in ignored])
+        
+        result = len(propdiff) == 0
+        if not result:
+            log.debug("VCALENDAR properties differ: %s" % (propdiff,))
+        return result
+
+    def _compareVTIMEZONEs(self):
+
+        # FIXME: clients may re-write timezones so the best we can do is
+        # compare TZIDs. That is not ideal as a client could have an old version
+        # of a VTIMEZONE and thus could show events at different times than the
+        # organizer.
+        
+        def extractTZIDs(calendar):
+
+            tzids = set()
+            for component in calendar.subcomponents():
+                if component.name() == "VTIMEZONE":
+                    tzids.add(component.propertyValue("TZID"))
+            return tzids
+        
+        tzids1 = extractTZIDs(self.calendar1)
+        tzids2 = extractTZIDs(self.calendar2)
+        result = tzids1 == tzids2
+        if not result:
+            log.debug("Different VTIMEZONES: %s %s" % (tzids1, tzids2))
+        return result
+
+    def _compareComponents(self):
+        
+        # First get uid/rid map of components
+        def mapComponents(calendar):
+            map = {}
+            for component in calendar.subcomponents():
+                if component.name() == "VTIMEZONE":
+                    continue
+                name = component.name()
+                uid = component.propertyValue("UID")
+                rid = component.getRecurrenceIDUTC()
+                map[(name, uid, rid,)] = component
+            return map
+        
+        map1 = mapComponents(self.calendar1)
+        set1 = set(map1.keys())
+        map2 = mapComponents(self.calendar2)
+        set2 = set(map2.keys())
+
+        # All the components in calendar1 must be in calendar2
+        result = set1 - set2
+        if result:
+            log.debug("Missing components from first calendar: %s" % (result,))
+        if result:
+            return False, False
+
+        # Now verify that each component in set1 matches what is in set2
+        attendee_unchanged = True
+        for key, value in map1.iteritems():
+            component1 = value
+            component2 = map2[key]
+            
+            nomismatch, no_attendee_change = self._testComponents(component1, component2)
+            if not nomismatch:
+                return False, False
+            attendee_unchanged &= no_attendee_change
+        
+        # Now verify that each additional component in set2 matches a derived component in set1
+        for key in set2 - set1:
+            component1 = self.calendar1.deriveInstance(key[2])
+            if component1 is None:
+                return False, False
+            component2 = map2[key]
+            
+            nomismatch, no_attendee_change = self._testComponents(component1, component2)
+            if not nomismatch:
+                return False, False
+            attendee_unchanged &= no_attendee_change
+            
+        return True, attendee_unchanged
+
+    def _testComponents(self, comp1, comp2):
+        
+        assert isinstance(comp1, Component) and isinstance(comp2, Component)
+        
+        if comp1.name() != comp2.name():
+            log.debug("Component names are different: '%s' and '%s'" % (comp1.name(), comp2.name()))
+            return False, False
+        
+        # Only accept a change to this attendee's own ATTENDEE property
+        propdiff = set(comp1.properties()) ^ set(comp2.properties())
+        for prop in tuple(propdiff):
+            # These ones are OK to change
+            if prop.name() in ("TRANSP", "DTSTAMP", "CREATED", "LAST-MODIFIED",):
+                propdiff.remove(prop)
+                continue
+            if prop.name() != "ATTENDEE" or prop.value() != self.attendee:
+                log.debug("Component properties are different: %s" % (propdiff,))
+                return False, False
+
+        # Compare subcomponents.
+        # NB at this point we assume VALARMS have been removed.
+        result = set(comp1.subcomponents()) ^ set(comp2.subcomponents())
+        if result:
+            log.debug("Sub-components are different: %s" % (result,))
+            return False, False
+        
+        return True, len(propdiff) == 0

Deleted: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/imip.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/imip.py	2008-08-11 21:12:04 UTC (rev 2800)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/imip.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -1,259 +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.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)
-                    log.debug("Sending iMIP message To: '%s', From :'%s'\n%s" % (toAddr, fromAddr, sendit,))
-                    yield sendmail(config.Scheduling[self.serviceType()]["Sending"]["Server"], fromAddr, toAddr, sendit, port=config.Scheduling[self.serviceType()]["Sending"]["Port"])
-        
-                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-imip 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

Copied: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/imip.py (from rev 2800, CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/imip.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/imip.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/imip.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -0,0 +1,259 @@
+##
+# 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)
+                    log.debug("Sending iMIP message To: '%s', From :'%s'\n%s" % (toAddr, fromAddr, sendit,))
+                    yield sendmail(config.Scheduling[self.serviceType()]["Sending"]["Server"], fromAddr, toAddr, sendit, port=config.Scheduling[self.serviceType()]["Sending"]["Port"])
+        
+                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-imip 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

Deleted: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/implicit.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/implicit.py	2008-08-11 21:12:04 UTC (rev 2800)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/implicit.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -1,369 +0,0 @@
-#
-# Copyright (c) 2005-2008 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, succeed
-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.itip import iTipGenerator
-from twistedcaldav.log import Logger
-from twistedcaldav.scheduling.scheduler import CalDAVScheduler
-from twistedcaldav.method import report_common
-from twistedcaldav.scheduling.icaldiff import iCalDiff
-
-__all__ = [
-    "ImplicitScheduler",
-]
-
-log = Logger()
-
-# TODO:
-#
-# Handle the case where a PUT removes the ORGANIZER property. That should be equivalent to cancelling the entire meeting.
-#
-# Figure out proper behavior for what happens when an attendee deletes their copy of an event. For now disallow.
-
-class ImplicitScheduler(object):
-    
-    def __init__(self):
-        pass
-
-    @inlineCallbacks
-    def doImplicitScheduling(self, request, resource, calendar, deleting):
-        """
-        Do implicit scheduling operation based on the calendar data that is being PUT
-
-        @param request:
-        @type request:
-        @param resource:
-        @type resource:
-        @param calendar: the calendar data being written, or None if deleting
-        @type calendar: L{Component} or C{None}
-        @param deleting: C{True} if the resource is being deleting
-        @type deleting: bool
-
-        @return: a new calendar object modified with scheduling information
-        """
-        
-        self.request = request
-        self.resource = resource
-        self.calendar = calendar
-        self.calendar_owner = (yield self.resource.owner(self.request))
-        self.deleting = deleting
-
-        # When deleting we MUST have the calendar as the actual resource
-        # will have been deleted by now
-        assert deleting and calendar or not deleting
-
-        # Get some useful information from the calendar
-        self.extractCalendarData()
-
-        # Determine what type of scheduling this is: Organizer triggered or Attendee triggered
-        if self.isOrganizerScheduling():
-            yield self.doImplicitOrganizer()
-        elif self.isAttendeeScheduling():
-            yield self.doImplicitAttendee()
-
-        returnValue(self.calendar)
-
-    def extractCalendarData(self):
-        
-        # Get the ORGANIZER and verify it is the same for all components
-        organizers = self.calendar.getOrganizersByInstance()
-        self.organizer = None
-        for organizer, _ignore in organizers:
-            if self.organizer:
-                if organizer != self.organizer:
-                    # We have different ORGANIZERs in the same iCalendar object - this is an error
-                    log.error("Only one ORGANIZER is allowed in an iCalendar object:\n%s" % (self.calendar,))
-                    raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "single-organizer")))
-            else:
-                self.organizer = organizer
-        
-        # Get the ATTENDEEs
-        self.attendeesByInstance = self.calendar.getAttendeesByInstance()
-        self.attendees = set()
-        for attendee, _ignore in self.attendeesByInstance:
-            self.attendees.add(attendee)
-            
-        # Some other useful things
-        self.uid = self.calendar.resourceUID()
-    
-    def isOrganizerScheduling(self):
-        """
-        Test whether this is a scheduling operation by an organizer
-        """
-        
-        # First must have organizer property
-        if not self.organizer:
-            return False
-        
-        # Organizer must map to a valid principal
-        self.organizerPrincipal = self.resource.principalForCalendarUserAddress(self.organizer)
-        if not self.organizerPrincipal:
-            return False
-        
-        # Organizer must be the owner of the calendar resource
-        if str(self.calendar_owner) != self.organizerPrincipal.principalURL():
-            return False
-
-        return True
-
-    def isAttendeeScheduling(self):
-        
-        # First must have organizer property
-        if not self.organizer:
-            return False
-        
-        # Check to see whether any attendee is the owner
-        for attendee in self.attendees:
-            attendeePrincipal = self.resource.principalForCalendarUserAddress(attendee)
-            if attendeePrincipal and attendeePrincipal.principalURL() == str(self.calendar_owner):
-                self.attendee = attendee
-                self.attendeePrincipal = attendeePrincipal
-                return True
-        
-        return False
-
-    @inlineCallbacks
-    def doImplicitOrganizer(self):
-        
-        # Check for a delete
-        if self.deleting:
-
-            log.debug("Implicit - organizer '%s' is deleting UID: '%s'" % (self.organizer, self.uid))
-            self.oldcalendar = self.calendar
-
-            # Cancel all attendees
-            self.cancelledAttendees = [(attendee, None) for attendee in self.attendees]
-
-        # Check for a new resource or an update
-        elif self.resource.exists():
-
-            # Read in existing data
-            self.oldcalendar = self.resource.iCalendar()
-            
-            # Significant change
-            if self.isChangeInsignificant():
-                # Nothing to do
-                log.debug("Implicit - organizer '%s' is updating UID: '%s' but change is not significant" % (self.organizer, self.uid))
-                return
-            
-            log.debug("Implicit - organizer '%s' is updating UID: '%s'" % (self.organizer, self.uid))
-
-            # Check for removed attendees
-            self.findRemovedAttendees()
-        else:
-            log.debug("Implicit - organizer '%s' is creating UID: '%s'" % (self.organizer, self.uid))
-            self.oldcalendar = None
-            self.cancelledAttendees = ()   
-            
-        yield self.scheduleWithAttendees()
-
-    def isChangeInsignificant(self):
-        
-        differ = iCalDiff(self.oldcalendar, self.calendar)
-        return differ.organizerDiff()
-    
-    def findRemovedAttendees(self):
-        """
-        Look for attendees that have been removed from any instances. Save those off
-        as users that need to be sent a cancel.
-        """
-        
-        oldAttendeesByInstance = self.oldcalendar.getAttendeesByInstance()
-        
-        mappedOld = set(oldAttendeesByInstance)
-        mappedNew = set(self.attendeesByInstance)
-        
-        self.cancelledAttendees = mappedOld.difference(mappedNew)
-
-    @inlineCallbacks
-    def scheduleWithAttendees(self):
-        
-        # First process cancelled attendees
-        yield self.processCancels()
-        
-        # Process regular requests next
-        if not self.deleting:
-            yield self.processRequests()
-
-    @inlineCallbacks
-    def processCancels(self):
-        
-        # TODO: a better policy here is to aggregate by attendees with the same set of instances
-        # being cancelled, but for now we will do one scheduling message per attendee.
-
-        # Do one per attendee
-        aggregated = {}
-        for attendee, rid in self.cancelledAttendees:
-            aggregated.setdefault(attendee, []).append(rid)
-            
-        for attendee, rids in aggregated.iteritems():
-            
-            # Don't send message back to the ORGANIZER
-            if attendee in self.organizerPrincipal.calendarUserAddresses():
-                continue
-
-            # Generate an iTIP CANCEL message for this attendee, cancelling
-            # each instance or the whole
-            
-            if None in rids:
-                # One big CANCEL will do
-                itipmsg = iTipGenerator.generateCancel(self.oldcalendar, (attendee,), None)
-            else:
-                # Multiple CANCELs
-                itipmsg = iTipGenerator.generateCancel(self.oldcalendar, (attendee,), rids)
-
-            # Send scheduling message
-            
-            # This is a local CALDAV scheduling operation.
-            scheduler = CalDAVScheduler(self.request, self.resource)
-    
-            # Do the PUT processing
-            log.info("Implicit CANCEL - organizer: '%s' to attendee: '%s', UID: '%s', RIDs: '%s'" % (self.organizer, attendee, self.uid, rids))
-            response = (yield scheduler.doSchedulingViaPUT(self.organizer, (attendee,), itipmsg))
-            self.handleSchedulingResponse(response, True)
-            
-    @inlineCallbacks
-    def processRequests(self):
-        
-        # TODO: a better policy here is to aggregate by attendees with the same set of instances
-        # being requested, but for now we will do one scheduling message per attendee.
-
-        # Do one per attendee
-        for attendee, _ignore in self.attendeesByInstance:
-
-            # Don't send message back to the ORGANIZER
-            if attendee in self.organizerPrincipal.calendarUserAddresses():
-                continue
-
-            itipmsg = iTipGenerator.generateAttendeeRequest(self.calendar, (attendee,))
-
-            # Send scheduling message
-
-            # This is a local CALDAV scheduling operation.
-            scheduler = CalDAVScheduler(self.request, self.resource)
-    
-            # Do the PUT processing
-            log.info("Implicit REQUEST - organizer: '%s' to attendee: '%s', UID: '%s'" % (self.organizer, attendee, self.uid,))
-            response = (yield scheduler.doSchedulingViaPUT(self.organizer, (attendee,), itipmsg))
-            self.handleSchedulingResponse(response, True)
-
-    def handleSchedulingResponse(self, response, is_organizer):
-        
-        # TODO: need to figure out how to process the response
-        pass
-
-    @inlineCallbacks
-    def doImplicitAttendee(self):
-
-        if self.deleting:
-            #log.error("Attendee '%s' is not allowed to delete an organized event: UID:%s" % (self.attendeePrincipal, self.uid,))
-            #raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-attendee-change")))
-            log.debug("Implicit - attendee '%s' is cancelling UID: '%s'" % (self.attendee, self.uid))
-            yield self.scheduleCancelWithOrganizer()
-        
-        else:
-            # Get the ORGANIZER's current copy of the calendar object
-            yield self.getOrganizersCopy()
-            assert self.organizer_calendar, "Must have the organizer's copy of an invite"
-            
-            # Determine whether the current change is allowed
-            if self.isAttendeeChangeInsignificant():
-                log.debug("Implicit - attendee '%s' is updating UID: '%s' but change is not significant" % (self.attendee, self.uid))
-                return
-                
-            log.debug("Implicit - attendee '%s' is updating UID: '%s'" % (self.attendee, self.uid))
-            yield self.scheduleWithOrganizer()
-
-    @inlineCallbacks
-    def getOrganizersCopy(self):
-        """
-        Get the Organizer's copy of the event being processed.
-        
-        NB it is possible that the Organizer is not hosted on this server
-        so the result here will be None. In that case we have to trust that
-        the attendee does the right thing about changing the details in the event.
-        """
-        
-        self.organizer_calendar = None
-        if self.organizerPrincipal:
-            # Get Organizer's calendar-home
-            calendar_home = self.organizerPrincipal.calendarHome()
-            
-            # FIXME: because of the URL->resource request mapping thing, we have to force the request
-            # to recognize this resource
-            self.request._rememberResource(calendar_home, calendar_home.url())
-    
-            # Run a UID query against the UID
-
-            def queryCalendarCollection(collection, uri):
-                rname = collection.index().resourceNameForUID(self.uid)
-                if rname:
-                    self.organizer_calendar = collection.iCalendar(rname)
-                    return succeed(False)
-                else:
-                    return succeed(True)
-            
-            # NB We are by-passing privilege checking here. That should be OK as the data found is not
-            # exposed to the user.
-            yield report_common.applyToCalendarCollections(calendar_home, self.request, calendar_home.url(), "infinity", queryCalendarCollection, None)
-    
-    def isAttendeeChangeInsignificant(self):
-        """
-        Check whether the change is significant (PARTSTAT) or allowed
-        (attendee can only change their property, alarms, TRANSP, and
-        instances. Raise an exception if it is not allowed.
-        """
-        
-        differ = iCalDiff(self.organizer_calendar, self.calendar)
-        change_allowed, no_itip = differ.attendeeMerge(self.attendee)
-        if not change_allowed:
-            log.error("Attendee '%s' is not allowed to make an unauthorized change to an organized event: UID:%s" % (self.attendeePrincipal, self.uid,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-attendee-change")))
-
-        return no_itip
-
-    @inlineCallbacks
-    def scheduleWithOrganizer(self):
-
-        itipmsg = iTipGenerator.generateAttendeeReply(self.calendar, self.attendee)
-
-        # Send scheduling message
-        yield self.sendToOrganizer("REPLY", itipmsg)
-
-    @inlineCallbacks
-    def scheduleCancelWithOrganizer(self):
-
-        itipmsg = iTipGenerator.generateAttendeeReply(self.calendar, self.attendee, True)
-
-        # Send scheduling message
-        yield self.sendToOrganizer("CANCEL", itipmsg)
-
-    @inlineCallbacks
-    def sendToOrganizer(self, action, itipmsg):
-
-        # Send scheduling message
-
-        # This is a local CALDAV scheduling operation.
-        scheduler = CalDAVScheduler(self.request, self.resource)
-
-        # Do the PUT processing
-        log.info("Implicit %s - attendee: '%s' to organizer: '%s', UID: '%s'" % (action, self.attendee, self.organizer, self.uid,))
-        response = (yield scheduler.doSchedulingViaPUT(self.attendee, (self.organizer,), itipmsg))
-        self.handleSchedulingResponse(response, False)

Copied: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/implicit.py (from rev 2800, CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/implicit.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/implicit.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/implicit.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -0,0 +1,369 @@
+#
+# Copyright (c) 2005-2008 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, succeed
+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.itip import iTipGenerator
+from twistedcaldav.log import Logger
+from twistedcaldav.scheduling.scheduler import CalDAVScheduler
+from twistedcaldav.method import report_common
+from twistedcaldav.scheduling.icaldiff import iCalDiff
+
+__all__ = [
+    "ImplicitScheduler",
+]
+
+log = Logger()
+
+# TODO:
+#
+# Handle the case where a PUT removes the ORGANIZER property. That should be equivalent to cancelling the entire meeting.
+#
+# Figure out proper behavior for what happens when an attendee deletes their copy of an event. For now disallow.
+
+class ImplicitScheduler(object):
+    
+    def __init__(self):
+        pass
+
+    @inlineCallbacks
+    def doImplicitScheduling(self, request, resource, calendar, deleting):
+        """
+        Do implicit scheduling operation based on the calendar data that is being PUT
+
+        @param request:
+        @type request:
+        @param resource:
+        @type resource:
+        @param calendar: the calendar data being written, or None if deleting
+        @type calendar: L{Component} or C{None}
+        @param deleting: C{True} if the resource is being deleting
+        @type deleting: bool
+
+        @return: a new calendar object modified with scheduling information
+        """
+        
+        self.request = request
+        self.resource = resource
+        self.calendar = calendar
+        self.calendar_owner = (yield self.resource.owner(self.request))
+        self.deleting = deleting
+
+        # When deleting we MUST have the calendar as the actual resource
+        # will have been deleted by now
+        assert deleting and calendar or not deleting
+
+        # Get some useful information from the calendar
+        self.extractCalendarData()
+
+        # Determine what type of scheduling this is: Organizer triggered or Attendee triggered
+        if self.isOrganizerScheduling():
+            yield self.doImplicitOrganizer()
+        elif self.isAttendeeScheduling():
+            yield self.doImplicitAttendee()
+
+        returnValue(self.calendar)
+
+    def extractCalendarData(self):
+        
+        # Get the ORGANIZER and verify it is the same for all components
+        organizers = self.calendar.getOrganizersByInstance()
+        self.organizer = None
+        for organizer, _ignore in organizers:
+            if self.organizer:
+                if organizer != self.organizer:
+                    # We have different ORGANIZERs in the same iCalendar object - this is an error
+                    log.error("Only one ORGANIZER is allowed in an iCalendar object:\n%s" % (self.calendar,))
+                    raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "single-organizer")))
+            else:
+                self.organizer = organizer
+        
+        # Get the ATTENDEEs
+        self.attendeesByInstance = self.calendar.getAttendeesByInstance()
+        self.attendees = set()
+        for attendee, _ignore in self.attendeesByInstance:
+            self.attendees.add(attendee)
+            
+        # Some other useful things
+        self.uid = self.calendar.resourceUID()
+    
+    def isOrganizerScheduling(self):
+        """
+        Test whether this is a scheduling operation by an organizer
+        """
+        
+        # First must have organizer property
+        if not self.organizer:
+            return False
+        
+        # Organizer must map to a valid principal
+        self.organizerPrincipal = self.resource.principalForCalendarUserAddress(self.organizer)
+        if not self.organizerPrincipal:
+            return False
+        
+        # Organizer must be the owner of the calendar resource
+        if str(self.calendar_owner) != self.organizerPrincipal.principalURL():
+            return False
+
+        return True
+
+    def isAttendeeScheduling(self):
+        
+        # First must have organizer property
+        if not self.organizer:
+            return False
+        
+        # Check to see whether any attendee is the owner
+        for attendee in self.attendees:
+            attendeePrincipal = self.resource.principalForCalendarUserAddress(attendee)
+            if attendeePrincipal and attendeePrincipal.principalURL() == str(self.calendar_owner):
+                self.attendee = attendee
+                self.attendeePrincipal = attendeePrincipal
+                return True
+        
+        return False
+
+    @inlineCallbacks
+    def doImplicitOrganizer(self):
+        
+        # Check for a delete
+        if self.deleting:
+
+            log.debug("Implicit - organizer '%s' is deleting UID: '%s'" % (self.organizer, self.uid))
+            self.oldcalendar = self.calendar
+
+            # Cancel all attendees
+            self.cancelledAttendees = [(attendee, None) for attendee in self.attendees]
+
+        # Check for a new resource or an update
+        elif self.resource.exists():
+
+            # Read in existing data
+            self.oldcalendar = self.resource.iCalendar()
+            
+            # Significant change
+            if self.isChangeInsignificant():
+                # Nothing to do
+                log.debug("Implicit - organizer '%s' is updating UID: '%s' but change is not significant" % (self.organizer, self.uid))
+                return
+            
+            log.debug("Implicit - organizer '%s' is updating UID: '%s'" % (self.organizer, self.uid))
+
+            # Check for removed attendees
+            self.findRemovedAttendees()
+        else:
+            log.debug("Implicit - organizer '%s' is creating UID: '%s'" % (self.organizer, self.uid))
+            self.oldcalendar = None
+            self.cancelledAttendees = ()   
+            
+        yield self.scheduleWithAttendees()
+
+    def isChangeInsignificant(self):
+        
+        differ = iCalDiff(self.oldcalendar, self.calendar)
+        return differ.organizerDiff()
+    
+    def findRemovedAttendees(self):
+        """
+        Look for attendees that have been removed from any instances. Save those off
+        as users that need to be sent a cancel.
+        """
+        
+        oldAttendeesByInstance = self.oldcalendar.getAttendeesByInstance()
+        
+        mappedOld = set(oldAttendeesByInstance)
+        mappedNew = set(self.attendeesByInstance)
+        
+        self.cancelledAttendees = mappedOld.difference(mappedNew)
+
+    @inlineCallbacks
+    def scheduleWithAttendees(self):
+        
+        # First process cancelled attendees
+        yield self.processCancels()
+        
+        # Process regular requests next
+        if not self.deleting:
+            yield self.processRequests()
+
+    @inlineCallbacks
+    def processCancels(self):
+        
+        # TODO: a better policy here is to aggregate by attendees with the same set of instances
+        # being cancelled, but for now we will do one scheduling message per attendee.
+
+        # Do one per attendee
+        aggregated = {}
+        for attendee, rid in self.cancelledAttendees:
+            aggregated.setdefault(attendee, []).append(rid)
+            
+        for attendee, rids in aggregated.iteritems():
+            
+            # Don't send message back to the ORGANIZER
+            if attendee in self.organizerPrincipal.calendarUserAddresses():
+                continue
+
+            # Generate an iTIP CANCEL message for this attendee, cancelling
+            # each instance or the whole
+            
+            if None in rids:
+                # One big CANCEL will do
+                itipmsg = iTipGenerator.generateCancel(self.oldcalendar, (attendee,), None)
+            else:
+                # Multiple CANCELs
+                itipmsg = iTipGenerator.generateCancel(self.oldcalendar, (attendee,), rids)
+
+            # Send scheduling message
+            
+            # This is a local CALDAV scheduling operation.
+            scheduler = CalDAVScheduler(self.request, self.resource)
+    
+            # Do the PUT processing
+            log.info("Implicit CANCEL - organizer: '%s' to attendee: '%s', UID: '%s', RIDs: '%s'" % (self.organizer, attendee, self.uid, rids))
+            response = (yield scheduler.doSchedulingViaPUT(self.organizer, (attendee,), itipmsg))
+            self.handleSchedulingResponse(response, True)
+            
+    @inlineCallbacks
+    def processRequests(self):
+        
+        # TODO: a better policy here is to aggregate by attendees with the same set of instances
+        # being requested, but for now we will do one scheduling message per attendee.
+
+        # Do one per attendee
+        for attendee, _ignore in self.attendeesByInstance:
+
+            # Don't send message back to the ORGANIZER
+            if attendee in self.organizerPrincipal.calendarUserAddresses():
+                continue
+
+            itipmsg = iTipGenerator.generateAttendeeRequest(self.calendar, (attendee,))
+
+            # Send scheduling message
+
+            # This is a local CALDAV scheduling operation.
+            scheduler = CalDAVScheduler(self.request, self.resource)
+    
+            # Do the PUT processing
+            log.info("Implicit REQUEST - organizer: '%s' to attendee: '%s', UID: '%s'" % (self.organizer, attendee, self.uid,))
+            response = (yield scheduler.doSchedulingViaPUT(self.organizer, (attendee,), itipmsg))
+            self.handleSchedulingResponse(response, True)
+
+    def handleSchedulingResponse(self, response, is_organizer):
+        
+        # TODO: need to figure out how to process the response
+        pass
+
+    @inlineCallbacks
+    def doImplicitAttendee(self):
+
+        if self.deleting:
+            #log.error("Attendee '%s' is not allowed to delete an organized event: UID:%s" % (self.attendeePrincipal, self.uid,))
+            #raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-attendee-change")))
+            log.debug("Implicit - attendee '%s' is cancelling UID: '%s'" % (self.attendee, self.uid))
+            yield self.scheduleCancelWithOrganizer()
+        
+        else:
+            # Get the ORGANIZER's current copy of the calendar object
+            yield self.getOrganizersCopy()
+            assert self.organizer_calendar, "Must have the organizer's copy of an invite"
+            
+            # Determine whether the current change is allowed
+            if self.isAttendeeChangeInsignificant():
+                log.debug("Implicit - attendee '%s' is updating UID: '%s' but change is not significant" % (self.attendee, self.uid))
+                return
+                
+            log.debug("Implicit - attendee '%s' is updating UID: '%s'" % (self.attendee, self.uid))
+            yield self.scheduleWithOrganizer()
+
+    @inlineCallbacks
+    def getOrganizersCopy(self):
+        """
+        Get the Organizer's copy of the event being processed.
+        
+        NB it is possible that the Organizer is not hosted on this server
+        so the result here will be None. In that case we have to trust that
+        the attendee does the right thing about changing the details in the event.
+        """
+        
+        self.organizer_calendar = None
+        if self.organizerPrincipal:
+            # Get Organizer's calendar-home
+            calendar_home = self.organizerPrincipal.calendarHome()
+            
+            # FIXME: because of the URL->resource request mapping thing, we have to force the request
+            # to recognize this resource
+            self.request._rememberResource(calendar_home, calendar_home.url())
+    
+            # Run a UID query against the UID
+
+            def queryCalendarCollection(collection, uri):
+                rname = collection.index().resourceNameForUID(self.uid)
+                if rname:
+                    self.organizer_calendar = collection.iCalendar(rname)
+                    return succeed(False)
+                else:
+                    return succeed(True)
+            
+            # NB We are by-passing privilege checking here. That should be OK as the data found is not
+            # exposed to the user.
+            yield report_common.applyToCalendarCollections(calendar_home, self.request, calendar_home.url(), "infinity", queryCalendarCollection, None)
+    
+    def isAttendeeChangeInsignificant(self):
+        """
+        Check whether the change is significant (PARTSTAT) or allowed
+        (attendee can only change their property, alarms, TRANSP, and
+        instances. Raise an exception if it is not allowed.
+        """
+        
+        differ = iCalDiff(self.organizer_calendar, self.calendar)
+        change_allowed, no_itip = differ.attendeeMerge(self.attendee)
+        if not change_allowed:
+            log.error("Attendee '%s' is not allowed to make an unauthorized change to an organized event: UID:%s" % (self.attendeePrincipal, self.uid,))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-attendee-change")))
+
+        return no_itip
+
+    @inlineCallbacks
+    def scheduleWithOrganizer(self):
+
+        itipmsg = iTipGenerator.generateAttendeeReply(self.calendar, self.attendee)
+
+        # Send scheduling message
+        yield self.sendToOrganizer("REPLY", itipmsg)
+
+    @inlineCallbacks
+    def scheduleCancelWithOrganizer(self):
+
+        itipmsg = iTipGenerator.generateAttendeeReply(self.calendar, self.attendee, True)
+
+        # Send scheduling message
+        yield self.sendToOrganizer("CANCEL", itipmsg)
+
+    @inlineCallbacks
+    def sendToOrganizer(self, action, itipmsg):
+
+        # Send scheduling message
+
+        # This is a local CALDAV scheduling operation.
+        scheduler = CalDAVScheduler(self.request, self.resource)
+
+        # Do the PUT processing
+        log.info("Implicit %s - attendee: '%s' to organizer: '%s', UID: '%s'" % (action, self.attendee, self.organizer, self.uid,))
+        response = (yield scheduler.doSchedulingViaPUT(self.attendee, (self.organizer,), itipmsg))
+        self.handleSchedulingResponse(response, False)

Deleted: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/ischedule.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/ischedule.py	2008-08-11 21:12:04 UTC (rev 2800)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/ischedule.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -1,184 +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.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)

Copied: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/ischedule.py (from rev 2800, CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/ischedule.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/ischedule.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/ischedule.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -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)

Deleted: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/ischeduleservers.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/ischeduleservers.py	2008-08-11 21:12:04 UTC (rev 2800)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/ischeduleservers.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -1,202 +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.
-##
-
-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]

Copied: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/ischeduleservers.py (from rev 2800, CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/ischeduleservers.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/ischeduleservers.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/ischeduleservers.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -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]

Deleted: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/scheduler.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/scheduler.py	2008-08-11 21:12:04 UTC (rev 2800)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/scheduler.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -1,831 +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.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, StatusResponse
-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
-from twistedcaldav.config import config
-
-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
-        self.fakeTheResult = False
-        self.method = "Unknown"
-    
-    @inlineCallbacks
-    def doSchedulingViaPOST(self):
-        """
-        The Scheduling POST operation on an Outbox.
-        """
-    
-        self.method = "POST"
-
-        # 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.
-        """
-    
-        self.method = "PUT"
-
-        # 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 some final checks after we have gathered all our information
-        self.finalChecks()
-
-        # 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("%s request must have Originator header" % (self.method,))
-            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("%s request must have at least one Recipient header" % (self.method,))
-            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 %s: %s" % (self.method, 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, e:
-            log.err("%s request calendar component is not valid:%s %s" % (self.method, e, self.calendar,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
-    
-        # Must have a METHOD
-        if not self.calendar.isValidMethod():
-            log.err("%s request must have valid METHOD property in calendar component: %s" % (self.method, self.calendar,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
-        
-        # Verify iTIP behavior
-        if not self.calendar.isValidITIP():
-            log.err("%s request must have a calendar component that satisfies iTIP requirements: %s" % (self.method, 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 %s request: %s" % (self.method, self.calendar,))
-            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (calendarserver_namespace, "no-access-restrictions")))
-    
-    def checkForFreeBusy(self):
-        if not hasattr(self, "isfreebusy"):
-            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
-                self.isfreebusy = True
-            else:
-                # Do regular invite (fan-out)
-                self.isfreebusy = False
-        
-        return self.isfreebusy
-    
-    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)
-                    )
-                )
-
-    def finalChecks(self):
-        """
-        Final checks before doing the actual scheduling.
-        """
-        pass
-
-    @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(self.method, 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 self.fakeTheResult:
-                responses.add(recipient.cuaddr, responsecode.OK, reqstatus="2.0;Success")
-                
-            elif 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()
-
-class CalDAVScheduler(Scheduler):
-
-    def __init__(self, request, resource):
-        super(CalDAVScheduler, self).__init__(request, resource)
-        self.doingPOST = False
-
-    @inlineCallbacks
-    def doSchedulingViaPOST(self):
-        """
-        The Scheduling POST operation on an Outbox.
-        """
-        self.doingPOST = True
-        result = (yield super(CalDAVScheduler, self).doSchedulingViaPOST())
-        returnValue(result)
-
-    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.doingPOST and 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:
-            if self.doingPOST and attendeePrincipal.scheduleOutboxURL() != 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")))
-
-    def finalChecks(self):
-        """
-        Final checks before doing the actual scheduling.
-        """
-        
-        # With implicit scheduling only certain types of iTIP operations are allowed for POST.
-        
-        if self.doingPOST:
-            # Freebusy requests always processed
-            if self.checkForFreeBusy():
-                return
-            
-            # COUNTER and DECLINE-COUNTER allowed
-            if self.calendar.propertyValue("METHOD") in ("COUNTER", "DECLINECOUNTER"):
-                return
-            
-            # Anything else is not allowed. However, for compatIbility we will optionally 
-            # return a success response for all attendees.
-            if config.Scheduling["CalDAV"]["OldDraftCompatability"]:
-                self.fakeTheResult = True
-            else:
-                raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Invalid iTIP message for implicit scheduling"))
-
-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

Copied: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/scheduler.py (from rev 2800, CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/scheduler.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/scheduler.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/scheduler.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -0,0 +1,831 @@
+##
+# 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, StatusResponse
+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
+from twistedcaldav.config import config
+
+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
+        self.fakeTheResult = False
+        self.method = "Unknown"
+    
+    @inlineCallbacks
+    def doSchedulingViaPOST(self):
+        """
+        The Scheduling POST operation on an Outbox.
+        """
+    
+        self.method = "POST"
+
+        # 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.
+        """
+    
+        self.method = "PUT"
+
+        # 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 some final checks after we have gathered all our information
+        self.finalChecks()
+
+        # 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("%s request must have Originator header" % (self.method,))
+            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("%s request must have at least one Recipient header" % (self.method,))
+            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 %s: %s" % (self.method, 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, e:
+            log.err("%s request calendar component is not valid:%s %s" % (self.method, e, self.calendar,))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+    
+        # Must have a METHOD
+        if not self.calendar.isValidMethod():
+            log.err("%s request must have valid METHOD property in calendar component: %s" % (self.method, self.calendar,))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-calendar-data")))
+        
+        # Verify iTIP behavior
+        if not self.calendar.isValidITIP():
+            log.err("%s request must have a calendar component that satisfies iTIP requirements: %s" % (self.method, 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 %s request: %s" % (self.method, self.calendar,))
+            raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (calendarserver_namespace, "no-access-restrictions")))
+    
+    def checkForFreeBusy(self):
+        if not hasattr(self, "isfreebusy"):
+            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
+                self.isfreebusy = True
+            else:
+                # Do regular invite (fan-out)
+                self.isfreebusy = False
+        
+        return self.isfreebusy
+    
+    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)
+                    )
+                )
+
+    def finalChecks(self):
+        """
+        Final checks before doing the actual scheduling.
+        """
+        pass
+
+    @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(self.method, 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 self.fakeTheResult:
+                responses.add(recipient.cuaddr, responsecode.OK, reqstatus="2.0;Success")
+                
+            elif 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()
+
+class CalDAVScheduler(Scheduler):
+
+    def __init__(self, request, resource):
+        super(CalDAVScheduler, self).__init__(request, resource)
+        self.doingPOST = False
+
+    @inlineCallbacks
+    def doSchedulingViaPOST(self):
+        """
+        The Scheduling POST operation on an Outbox.
+        """
+        self.doingPOST = True
+        result = (yield super(CalDAVScheduler, self).doSchedulingViaPOST())
+        returnValue(result)
+
+    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.doingPOST and 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:
+            if self.doingPOST and attendeePrincipal.scheduleOutboxURL() != 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")))
+
+    def finalChecks(self):
+        """
+        Final checks before doing the actual scheduling.
+        """
+        
+        # With implicit scheduling only certain types of iTIP operations are allowed for POST.
+        
+        if self.doingPOST:
+            # Freebusy requests always processed
+            if self.checkForFreeBusy():
+                return
+            
+            # COUNTER and DECLINE-COUNTER allowed
+            if self.calendar.propertyValue("METHOD") in ("COUNTER", "DECLINECOUNTER"):
+                return
+            
+            # Anything else is not allowed. However, for compatIbility we will optionally 
+            # return a success response for all attendees.
+            if config.Scheduling["CalDAV"]["OldDraftCompatability"]:
+                self.fakeTheResult = True
+            else:
+                raise HTTPError(StatusResponse(responsecode.BAD_REQUEST, "Invalid iTIP message for implicit scheduling"))
+
+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

Copied: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/test (from rev 2800, CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/test)

Deleted: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/test/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/test/__init__.py	2008-08-11 21:12:04 UTC (rev 2800)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/test/__init__.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -1,15 +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.
-##

Copied: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/test/__init__.py (from rev 2800, CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/test/__init__.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/test/__init__.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/test/__init__.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -0,0 +1,15 @@
+##
+# 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.
+##

Deleted: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/test/test_icaldiff.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/test/test_icaldiff.py	2008-08-11 21:12:04 UTC (rev 2800)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/test/test_icaldiff.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -1,1165 +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 twistedcaldav.scheduling.icaldiff import iCalDiff
-
-from twistedcaldav.ical import Component
-import twistedcaldav.test.util
-
-class ICalDiff (twistedcaldav.test.util.TestCase):
-    """
-    iCalendar support tests
-    """
-
-    def test_organizer_diff(self):
-        
-        data = (
-            (
-                "#1.1 Simple component, no change",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                True,
-            ),
-            (
-                "#1.2 Simple component, one property change",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user3 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                False,
-            ),
-            (
-                "#1.3 Simple component, one parameter change",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER;CN="User 01 Again":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                False,
-            ),
-            (
-                "#1.4 Simple component, no change, re-ordered",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-END:VEVENT
-END:VCALENDAR
-""",
-                True,
-            ),
-            (
-                "#1.5 Simple component, no change, re-ordered, added alarm",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""",
-                True,
-            ),
-            (
-                "#1.6 Simple component, no change, re-ordered, removed alarm",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-END:VEVENT
-END:VCALENDAR
-""",
-                True,
-            ),
-            (
-                "#1.7 Simple component, no change, re-ordered, changed alarm",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test
-TRIGGER;RELATED=START:-PT15M
-END:VALARM
-UID:12345-67890
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-END:VEVENT
-END:VCALENDAR
-""",
-                True,
-            ),
-            (
-                "#2.1 Recurring component, no change",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-METHOD:REPLY
-PRODID:-//PYVOBJECT//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890-3
-DTSTART:20071114T000000Z
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER:mailto:user1 at example.com
-RRULE:FREQ=YEARLY
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20081114T000000Z
-DTSTART:20071114T010000Z
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-METHOD:REPLY
-PRODID:-//PYVOBJECT//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20081114T000000Z
-DTSTART:20071114T010000Z
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER:mailto:user1 at example.com
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890-3
-DTSTART:20071114T000000Z
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER:mailto:user1 at example.com
-RRULE:FREQ=YEARLY
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""",
-                True,
-            ),
-            (
-                "#2.2 Recurring component, one change",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-METHOD:REPLY
-PRODID:-//PYVOBJECT//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890-3
-DTSTART:20071114T000000Z
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER:mailto:user1 at example.com
-RRULE:FREQ=YEARLY
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20081114T000000Z
-DTSTART:20071114T010000Z
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-METHOD:REPLY
-PRODID:-//PYVOBJECT//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890-3
-DTSTART:20071114T000000Z
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER:mailto:user1 at example.com
-RRULE:FREQ=YEARLY
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20081114T000000Z
-DTSTART:20071114T010000Z
-ATTENDEE:mailto:user3 at example.com
-ORGANIZER:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                False,
-            ),
-            (
-                "#2.3 Recurring component, no change, add alarm",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-METHOD:REPLY
-PRODID:-//PYVOBJECT//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890-3
-DTSTART:20071114T000000Z
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER:mailto:user1 at example.com
-RRULE:FREQ=YEARLY
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20081114T000000Z
-DTSTART:20071114T010000Z
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-METHOD:REPLY
-PRODID:-//PYVOBJECT//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20081114T000000Z
-DTSTART:20071114T010000Z
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER:mailto:user1 at example.com
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890-3
-DTSTART:20071114T000000Z
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER:mailto:user1 at example.com
-RRULE:FREQ=YEARLY
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""",
-                True,
-            ),
-            (
-                "#2.4 Recurring component, one change, remove component",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-METHOD:REPLY
-PRODID:-//PYVOBJECT//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890-3
-DTSTART:20071114T000000Z
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER:mailto:user1 at example.com
-RRULE:FREQ=YEARLY
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20081114T000000Z
-DTSTART:20071114T010000Z
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-METHOD:REPLY
-PRODID:-//PYVOBJECT//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20081114T000000Z
-DTSTART:20071114T010000Z
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                False,
-            ),
-        )
-
-        for description, calendar1, calendar2, result in data:
-            differ = iCalDiff(Component.fromString(calendar1), Component.fromString(calendar2))
-            self.assertEqual(differ.organizerDiff(), result, msg=description)
-
-    def test_attendee_diff_simple(self):
-        
-        data = (
-            (
-                "#1.1 Simple component, no change",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                "mailto:user2 at example.com",
-                (True, True,)
-            ),
-            (
-                "#1.2 Simple component, PARTSTAT change",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=ACCEPTED:mailto:user2 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                "mailto:user2 at example.com",
-                (True, False,)
-            ),
-            (
-                "#1.3 Simple component, bad change",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T123000Z
-DTEND:20080601T130000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                "mailto:user2 at example.com",
-                (False, False,)
-            ),
-            (
-                "#1.4 Simple component, valarm change",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test for Organizer
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test for Attendee
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""",
-                "mailto:user2 at example.com",
-                (True, True,)
-            ),
-            (
-                "#1.5 Simple component, vcalendar props change ok",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test for Organizer
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//EXAMPLE.ORG//NONSGML Version 1//EN
-CALSCALE:GREGORIAN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test for Attendee
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""",
-                "mailto:user2 at example.com",
-                (True, True,)
-            ),
-            (
-                "#1.6 Simple component, vcalendar props change bad",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test for Organizer
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-X-WR-CALID:12345
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test for Attendee
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""",
-                "mailto:user2 at example.com",
-                (False, False,)
-            ),
-            (
-                "#1.7 Simple component, vtimezone no change",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VTIMEZONE
-LAST-MODIFIED:20040110T032845Z
-TZID:US-Eastern
-BEGIN:DAYLIGHT
-DTSTART:19900404T010000
-RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
-TZNAME:EDT
-TZOFFSETFROM:-0500
-TZOFFSETTO:-0400
-END:DAYLIGHT
-BEGIN:STANDARD
-DTSTART:19901026T060000
-RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
-TZNAME:EST
-TZOFFSETFROM:-0400
-TZOFFSETTO:-0500
-END:STANDARD
-END:VTIMEZONE
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART;TZID=US-Eastern:20080601T120000
-DTEND;TZID=US-Eastern:20080601T130000
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART;TZID=US-Eastern:20080601T120000
-DTEND;TZID=US-Eastern:20080601T130000
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-END:VEVENT
-BEGIN:VTIMEZONE
-LAST-MODIFIED:20040110T032845Z
-TZID:US-Eastern
-BEGIN:DAYLIGHT
-DTSTART:19900404T010000
-RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
-TZNAME:EDT
-TZOFFSETFROM:-0500
-TZOFFSETTO:-0400
-END:DAYLIGHT
-BEGIN:STANDARD
-DTSTART:19901026T060000
-RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
-TZNAME:EST
-TZOFFSETFROM:-0400
-TZOFFSETTO:-0500
-END:STANDARD
-END:VTIMEZONE
-END:VCALENDAR
-""",
-                "mailto:user2 at example.com",
-                (True, True,)
-            ),
-            (
-                "#1.8 Simple component, vtimezone bad change",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VTIMEZONE
-LAST-MODIFIED:20040110T032845Z
-TZID:US-Eastern
-BEGIN:DAYLIGHT
-DTSTART:19900404T010000
-RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
-TZNAME:EDT
-TZOFFSETFROM:-0500
-TZOFFSETTO:-0400
-END:DAYLIGHT
-BEGIN:STANDARD
-DTSTART:19901026T060000
-RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
-TZNAME:EST
-TZOFFSETFROM:-0400
-TZOFFSETTO:-0500
-END:STANDARD
-END:VTIMEZONE
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART;TZID=US-Eastern:20080601T120000
-DTEND;TZID=US-Eastern:20080601T130000
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART;TZID=US-Pacific:20080601T120000
-DTEND;TZID=US-Pacific:20080601T130000
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-END:VEVENT
-BEGIN:VTIMEZONE
-LAST-MODIFIED:20040110T032845Z
-TZID:US-Pacific
-BEGIN:DAYLIGHT
-DTSTART:19900404T010000
-RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
-TZNAME:EDT
-TZOFFSETFROM:-0500
-TZOFFSETTO:-0400
-END:DAYLIGHT
-BEGIN:STANDARD
-DTSTART:19901026T060000
-RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
-TZNAME:EST
-TZOFFSETFROM:-0400
-TZOFFSETTO:-0500
-END:STANDARD
-END:VTIMEZONE
-END:VCALENDAR
-""",
-                "mailto:user2 at example.com",
-                (False, False,)
-            ),
-        )
-
-        for description, calendar1, calendar2, attendee, result in data:
-            differ = iCalDiff(Component.fromString(calendar1), Component.fromString(calendar2))
-            self.assertEqual(differ.attendeeDiff(attendee), result, msg=description)
-
-    def test_attendee_diff_complex(self):
-        
-        data = (
-            (
-                "#1.1 Complex component, no change",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T123000Z
-DTEND:20080602T130000Z
-ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-ORGANIZER:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T123000Z
-DTEND:20080602T130000Z
-ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-ORGANIZER:mailto:user1 at example.com
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-END:VCALENDAR
-""",
-                "mailto:user2 at example.com",
-                (True, True,)
-            ),
-            (
-                "#1.2 Complex component, alarm change",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T123000Z
-DTEND:20080602T130000Z
-ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-ORGANIZER:mailto:user1 at example.com
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test for Organizer
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-END:VEVENT
-END:VCALENDAR
-""",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-BEGIN:VALARM
-ACTION:DISPLAY
-DESCRIPTION:Test for Attendee
-TRIGGER;RELATED=START:-PT10M
-END:VALARM
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T123000Z
-DTEND:20080602T130000Z
-ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-ORGANIZER:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                "mailto:user2 at example.com",
-                (True, True,)
-            ),
-            (
-                "#1.3 Complex component, missing override",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T123000Z
-DTEND:20080602T130000Z
-ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-ORGANIZER:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-END:VCALENDAR
-""",
-                "mailto:user2 at example.com",
-                (False, False,)
-            ),
-            (
-                "#1.4 Complex component, additional override no change ok",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T123000Z
-DTEND:20080602T130000Z
-ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-ORGANIZER:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T123000Z
-DTEND:20080602T130000Z
-ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-ORGANIZER:mailto:user1 at example.com
-END:VEVENT
-BEGIN:VEVENT
-RECURRENCE-ID:20080604T120000Z
-UID:12345-67890
-DTSTART:20080604T120000Z
-DTEND:20080604T130000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                "mailto:user2 at example.com",
-                (True, True,)
-            ),
-            (
-                "#1.5 Complex component, additional override change ok",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T123000Z
-DTEND:20080602T130000Z
-ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-ORGANIZER:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T123000Z
-DTEND:20080602T130000Z
-ATTENDEE;RSVP=TRUE;PARTSTAT=ACCEPTED:mailto:user2 at example.com
-ORGANIZER:mailto:user1 at example.com
-END:VEVENT
-BEGIN:VEVENT
-RECURRENCE-ID:20080604T120000Z
-UID:12345-67890
-DTSTART:20080604T120000Z
-DTEND:20080604T130000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;RSVP=TRUE;PARTSTAT=DECLINED:mailto:user2 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                "mailto:user2 at example.com",
-                (True, False,)
-            ),
-            (
-                "#1.6 Complex component, additional override bad",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T123000Z
-DTEND:20080602T130000Z
-ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-ORGANIZER:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T123000Z
-DTEND:20080602T130000Z
-ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-ORGANIZER:mailto:user1 at example.com
-END:VEVENT
-BEGIN:VEVENT
-RECURRENCE-ID:20080604T120000Z
-UID:12345-67890
-DTSTART:20080604T123000Z
-DTEND:20080604T130000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                "mailto:user2 at example.com",
-                (False, False,)
-            ),
-        )
-
-        for description, calendar1, calendar2, attendee, result in data:
-            differ = iCalDiff(Component.fromString(calendar1), Component.fromString(calendar2))
-            self.assertEqual(differ.attendeeDiff(attendee), result, msg=description)

Copied: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/test/test_icaldiff.py (from rev 2800, CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/scheduling/test/test_icaldiff.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/test/test_icaldiff.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/scheduling/test/test_icaldiff.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -0,0 +1,1165 @@
+##
+# 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.scheduling.icaldiff import iCalDiff
+
+from twistedcaldav.ical import Component
+import twistedcaldav.test.util
+
+class ICalDiff (twistedcaldav.test.util.TestCase):
+    """
+    iCalendar support tests
+    """
+
+    def test_organizer_diff(self):
+        
+        data = (
+            (
+                "#1.1 Simple component, no change",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                True,
+            ),
+            (
+                "#1.2 Simple component, one property change",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user3 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                False,
+            ),
+            (
+                "#1.3 Simple component, one parameter change",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01 Again":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                False,
+            ),
+            (
+                "#1.4 Simple component, no change, re-ordered",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+END:VEVENT
+END:VCALENDAR
+""",
+                True,
+            ),
+            (
+                "#1.5 Simple component, no change, re-ordered, added alarm",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""",
+                True,
+            ),
+            (
+                "#1.6 Simple component, no change, re-ordered, removed alarm",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+END:VEVENT
+END:VCALENDAR
+""",
+                True,
+            ),
+            (
+                "#1.7 Simple component, no change, re-ordered, changed alarm",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT15M
+END:VALARM
+UID:12345-67890
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+END:VEVENT
+END:VCALENDAR
+""",
+                True,
+            ),
+            (
+                "#2.1 Recurring component, no change",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REPLY
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-3
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+RRULE:FREQ=YEARLY
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20081114T000000Z
+DTSTART:20071114T010000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REPLY
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20081114T000000Z
+DTSTART:20071114T010000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-3
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+RRULE:FREQ=YEARLY
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""",
+                True,
+            ),
+            (
+                "#2.2 Recurring component, one change",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REPLY
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-3
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+RRULE:FREQ=YEARLY
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20081114T000000Z
+DTSTART:20071114T010000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REPLY
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-3
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+RRULE:FREQ=YEARLY
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20081114T000000Z
+DTSTART:20071114T010000Z
+ATTENDEE:mailto:user3 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                False,
+            ),
+            (
+                "#2.3 Recurring component, no change, add alarm",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REPLY
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-3
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+RRULE:FREQ=YEARLY
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20081114T000000Z
+DTSTART:20071114T010000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REPLY
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20081114T000000Z
+DTSTART:20071114T010000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-3
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+RRULE:FREQ=YEARLY
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""",
+                True,
+            ),
+            (
+                "#2.4 Recurring component, one change, remove component",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REPLY
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-3
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+RRULE:FREQ=YEARLY
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20081114T000000Z
+DTSTART:20071114T010000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REPLY
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20081114T000000Z
+DTSTART:20071114T010000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                False,
+            ),
+        )
+
+        for description, calendar1, calendar2, result in data:
+            differ = iCalDiff(Component.fromString(calendar1), Component.fromString(calendar2))
+            self.assertEqual(differ.organizerDiff(), result, msg=description)
+
+    def test_attendee_diff_simple(self):
+        
+        data = (
+            (
+                "#1.1 Simple component, no change",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                "mailto:user2 at example.com",
+                (True, True,)
+            ),
+            (
+                "#1.2 Simple component, PARTSTAT change",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                "mailto:user2 at example.com",
+                (True, False,)
+            ),
+            (
+                "#1.3 Simple component, bad change",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T123000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                "mailto:user2 at example.com",
+                (False, False,)
+            ),
+            (
+                "#1.4 Simple component, valarm change",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test for Organizer
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test for Attendee
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""",
+                "mailto:user2 at example.com",
+                (True, True,)
+            ),
+            (
+                "#1.5 Simple component, vcalendar props change ok",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test for Organizer
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//EXAMPLE.ORG//NONSGML Version 1//EN
+CALSCALE:GREGORIAN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test for Attendee
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""",
+                "mailto:user2 at example.com",
+                (True, True,)
+            ),
+            (
+                "#1.6 Simple component, vcalendar props change bad",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test for Organizer
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+X-WR-CALID:12345
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test for Attendee
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""",
+                "mailto:user2 at example.com",
+                (False, False,)
+            ),
+            (
+                "#1.7 Simple component, vtimezone no change",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VTIMEZONE
+LAST-MODIFIED:20040110T032845Z
+TZID:US-Eastern
+BEGIN:DAYLIGHT
+DTSTART:19900404T010000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:19901026T060000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART;TZID=US-Eastern:20080601T120000
+DTEND;TZID=US-Eastern:20080601T130000
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART;TZID=US-Eastern:20080601T120000
+DTEND;TZID=US-Eastern:20080601T130000
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+END:VEVENT
+BEGIN:VTIMEZONE
+LAST-MODIFIED:20040110T032845Z
+TZID:US-Eastern
+BEGIN:DAYLIGHT
+DTSTART:19900404T010000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:19901026T060000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+END:VTIMEZONE
+END:VCALENDAR
+""",
+                "mailto:user2 at example.com",
+                (True, True,)
+            ),
+            (
+                "#1.8 Simple component, vtimezone bad change",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VTIMEZONE
+LAST-MODIFIED:20040110T032845Z
+TZID:US-Eastern
+BEGIN:DAYLIGHT
+DTSTART:19900404T010000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:19901026T060000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+END:VTIMEZONE
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART;TZID=US-Eastern:20080601T120000
+DTEND;TZID=US-Eastern:20080601T130000
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART;TZID=US-Pacific:20080601T120000
+DTEND;TZID=US-Pacific:20080601T130000
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+END:VEVENT
+BEGIN:VTIMEZONE
+LAST-MODIFIED:20040110T032845Z
+TZID:US-Pacific
+BEGIN:DAYLIGHT
+DTSTART:19900404T010000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+BEGIN:STANDARD
+DTSTART:19901026T060000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+END:VTIMEZONE
+END:VCALENDAR
+""",
+                "mailto:user2 at example.com",
+                (False, False,)
+            ),
+        )
+
+        for description, calendar1, calendar2, attendee, result in data:
+            differ = iCalDiff(Component.fromString(calendar1), Component.fromString(calendar2))
+            self.assertEqual(differ.attendeeDiff(attendee), result, msg=description)
+
+    def test_attendee_diff_complex(self):
+        
+        data = (
+            (
+                "#1.1 Complex component, no change",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T123000Z
+DTEND:20080602T130000Z
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T123000Z
+DTEND:20080602T130000Z
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+                "mailto:user2 at example.com",
+                (True, True,)
+            ),
+            (
+                "#1.2 Complex component, alarm change",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T123000Z
+DTEND:20080602T130000Z
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test for Organizer
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test for Attendee
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T123000Z
+DTEND:20080602T130000Z
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                "mailto:user2 at example.com",
+                (True, True,)
+            ),
+            (
+                "#1.3 Complex component, missing override",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T123000Z
+DTEND:20080602T130000Z
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+                "mailto:user2 at example.com",
+                (False, False,)
+            ),
+            (
+                "#1.4 Complex component, additional override no change ok",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T123000Z
+DTEND:20080602T130000Z
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T123000Z
+DTEND:20080602T130000Z
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+BEGIN:VEVENT
+RECURRENCE-ID:20080604T120000Z
+UID:12345-67890
+DTSTART:20080604T120000Z
+DTEND:20080604T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                "mailto:user2 at example.com",
+                (True, True,)
+            ),
+            (
+                "#1.5 Complex component, additional override change ok",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T123000Z
+DTEND:20080602T130000Z
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T123000Z
+DTEND:20080602T130000Z
+ATTENDEE;RSVP=TRUE;PARTSTAT=ACCEPTED:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+BEGIN:VEVENT
+RECURRENCE-ID:20080604T120000Z
+UID:12345-67890
+DTSTART:20080604T120000Z
+DTEND:20080604T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=DECLINED:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                "mailto:user2 at example.com",
+                (True, False,)
+            ),
+            (
+                "#1.6 Complex component, additional override bad",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T123000Z
+DTEND:20080602T130000Z
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T123000Z
+DTEND:20080602T130000Z
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+BEGIN:VEVENT
+RECURRENCE-ID:20080604T120000Z
+UID:12345-67890
+DTSTART:20080604T123000Z
+DTEND:20080604T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;RSVP=TRUE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                "mailto:user2 at example.com",
+                (False, False,)
+            ),
+        )
+
+        for description, calendar1, calendar2, attendee, result in data:
+            differ = iCalDiff(Component.fromString(calendar1), Component.fromString(calendar2))
+            self.assertEqual(differ.attendeeDiff(attendee), result, msg=description)

Modified: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/static.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/static.py	2008-08-11 22:41:16 UTC (rev 2804)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/static.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -57,11 +57,12 @@
 from twistedcaldav.customxml import TwistedCalendarAccessProperty
 from twistedcaldav.extensions import DAVFile
 from twistedcaldav.extensions import CachingXattrPropertyStore
+from twistedcaldav.freebusyurl import FreeBusyURLResource
 from twistedcaldav.ical import Component as iComponent
 from twistedcaldav.ical import Property as iProperty
 from twistedcaldav.index import Index, IndexSchedule
 from twistedcaldav.resource import CalDAVResource, isCalendarCollectionResource, isPseudoCalendarCollectionResource
-from twistedcaldav.schedule import ScheduleInboxResource, ScheduleOutboxResource
+from twistedcaldav.schedule import ScheduleInboxResource, ScheduleOutboxResource, IScheduleInboxResource
 from twistedcaldav.dropbox import DropBoxHomeResource, DropBoxCollectionResource
 from twistedcaldav.directory.calendar import uidsResourceName
 from twistedcaldav.directory.calendar import DirectoryCalendarHomeProvisioningResource
@@ -146,6 +147,9 @@
             # Initialize CTag on the calendar collection
             d1 = self.updateCTag()
 
+            # Calendar is initially transparent to freebusy
+            self.writeDeadProperty(caldavxml.ScheduleCalendarTransp(caldavxml.Transparent()))
+
             # Create the index so its ready when the first PUTs come in
             d1.addCallback(lambda _: self.index().create())
             d1.addCallback(lambda _: status)
@@ -609,10 +613,16 @@
         else:
             DropBoxHomeFileClass = None
 
+        if config.FreeBusyURL["Enabled"]:
+            FreeBusyURLFileClass = FreeBusyURLFile
+        else:
+            FreeBusyURLFileClass = None
+            
         cls = {
             "inbox"        : ScheduleInboxFile,
             "outbox"       : ScheduleOutboxFile,
             "dropbox"      : DropBoxHomeFileClass,
+            "freebusy"     : FreeBusyURLFileClass,
         }.get(name, None)
 
         if cls is not None:
@@ -736,6 +746,80 @@
     def __repr__(self):
         return "<%s (calendar outbox collection): %s>" % (self.__class__.__name__, self.fp.path)
 
+class IScheduleInboxFile (IScheduleInboxResource, CalDAVFile):
+    """
+    Server-to-server scheduling inbox resource.
+    """
+    def __init__(self, path, parent):
+        CalDAVFile.__init__(self, path, principalCollections=parent.principalCollections())
+        IScheduleInboxResource.__init__(self, parent)
+        
+        self.fp.open("w").close()
+        self.fp.restat(False)
+
+    def __repr__(self):
+        return "<%s (server-to-server inbox resource): %s>" % (self.__class__.__name__, self.fp.path)
+
+    def isCollection(self):
+        return False
+
+    def createSimilarFile(self, path):
+        if path == self.fp.path:
+            return self
+        else:
+            return responsecode.NOT_FOUND
+
+    def http_PUT        (self, request): return responsecode.FORBIDDEN
+    def http_COPY       (self, request): return responsecode.FORBIDDEN
+    def http_MOVE       (self, request): return responsecode.FORBIDDEN
+    def http_DELETE     (self, request): return responsecode.FORBIDDEN
+    def http_MKCOL      (self, request): return responsecode.FORBIDDEN
+
+    def http_MKCALENDAR(self, request):
+        return ErrorResponse(
+            responsecode.FORBIDDEN,
+            (caldav_namespace, "calendar-collection-location-ok")
+        )
+
+class FreeBusyURLFile (AutoProvisioningFileMixIn, FreeBusyURLResource, CalDAVFile):
+    """
+    Free-busy URL resource.
+    """
+    def __init__(self, path, parent):
+        CalDAVFile.__init__(self, path, principalCollections=parent.principalCollections())
+        FreeBusyURLResource.__init__(self, parent)
+
+    def __repr__(self):
+        return "<%s (free-busy URL resource): %s>" % (self.__class__.__name__, self.fp.path)
+
+    def isCollection(self):
+        return False
+
+    def createSimilarFile(self, path):
+        if path == self.fp.path:
+            return self
+        else:
+            return responsecode.NOT_FOUND
+
+    def http_PUT        (self, request): return responsecode.FORBIDDEN
+    def http_COPY       (self, request): return responsecode.FORBIDDEN
+    def http_MOVE       (self, request): return responsecode.FORBIDDEN
+    def http_DELETE     (self, request): return responsecode.FORBIDDEN
+    def http_MKCOL      (self, request): return responsecode.FORBIDDEN
+
+    def http_MKCALENDAR(self, request):
+        return ErrorResponse(
+            responsecode.FORBIDDEN,
+            (caldav_namespace, "calendar-collection-location-ok")
+        )
+
+    ##
+    # ACL
+    ##
+
+    def supportedPrivileges(self, request):
+        return succeed(schedulePrivilegeSet)
+
 class DropBoxHomeFile (AutoProvisioningFileMixIn, DropBoxHomeResource, CalDAVFile):
     def __init__(self, path, parent):
         DropBoxHomeResource.__init__(self)

Modified: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/tap.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/tap.py	2008-08-11 22:41:16 UTC (rev 2804)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/tap.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -20,6 +20,7 @@
 from zope.interface import implements
 
 from twisted.internet.address import IPv4Address
+#from twisted.internet import defer
 
 from twisted.python.log import FileLogObserver
 from twisted.python.usage import Options, UsageError
@@ -51,6 +52,7 @@
 from twistedcaldav.directory.aggregate import AggregateDirectoryService
 from twistedcaldav.directory.sudo import SudoDirectoryService
 from twistedcaldav.static import CalendarHomeProvisioningFile
+from twistedcaldav.static import IScheduleInboxFile
 from twistedcaldav.static import TimezoneServiceFile
 from twistedcaldav.timezones import TimezoneCache
 from twistedcaldav import pdmonster
@@ -435,6 +437,7 @@
     rootResourceClass            = RootResource
     principalResourceClass       = DirectoryPrincipalProvisioningResource
     calendarResourceClass        = CalendarHomeProvisioningFile
+    iScheduleResourceClass       = IScheduleInboxFile
     timezoneServiceResourceClass = TimezoneServiceFile
 
     def makeService_Slave(self, options):
@@ -534,6 +537,16 @@
             )
             root.putChild('timezones', timezoneService)
 
+        # iSchedule service is optional
+        if config.Scheduling["iSchedule"]["Enabled"]:
+            log.msg("Setting up iSchedule inbox resource: %r" % (self.iScheduleResourceClass,))
+    
+            ischedule = self.iScheduleResourceClass(
+                os.path.join(config.DocumentRoot, 'inbox'),
+                root,
+            )
+            root.putChild('inbox', ischedule)
+
         #
         # Configure ancillary data
         #
@@ -720,6 +733,9 @@
     makeService_Single   = makeService_Slave
 
     def makeService(self, options):
+        
+        #defer.setDebugging(True)
+
         serverType = config.ProcessType
 
         serviceMethod = getattr(self, "makeService_%s" % (serverType,), None)


Property changes on: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/test
___________________________________________________________________
Name: svn:ignore
   + oddata
s2s-data


Modified: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/test/test_icalendar.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/test/test_icalendar.py	2008-08-11 22:41:16 UTC (rev 2804)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/test/test_icalendar.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -16,10 +16,12 @@
 
 import os
 import datetime
+from dateutil.tz import tzutc
 
 from twisted.trial.unittest import SkipTest
 
-from twistedcaldav.ical import *
+from twistedcaldav.ical import Component, parse_date, parse_datetime,\
+    parse_date_or_datetime, parse_duration
 import twistedcaldav.test.util
 
 from vobject.icalendar import utc
@@ -127,7 +129,7 @@
 
         year = 2002
 
-        instances = calendar.expandTimeRanges(datetime.date(2100, 0, 0))
+        instances = calendar.expandTimeRanges(datetime.date(2100, 1, 1))
         for key in instances:
             instance = instances[key]
             start = instance.start
@@ -148,7 +150,7 @@
         """
         calendar = Component.fromStream(file(os.path.join(self.data_dir, "Holidays", "C318ABFE-1ED0-11D9-A5E0-000A958A3252.ics")))
 
-        instances = calendar.expandTimeRanges(datetime.date(2100, 0, 0))
+        instances = calendar.expandTimeRanges(datetime.date(2100, 1, 1))
         for key in instances:
             instance = instances[key]
             start = instance.start
@@ -157,7 +159,7 @@
             self.assertEqual(end, datetime.datetime(2004, 11, 27))
             break;
 
-    test_component_timerange.todo = "recurrance expansion should give us no end date here"
+    #test_component_timerange.todo = "recurrance expansion should give us no end date here"
 
     def test_parse_date(self):
         """
@@ -238,3 +240,889 @@
 
         component = Component.fromString(data)
         self.assertEqual(component.getAttendeeProperties(("user3 at example.com",)), [])
+
+    def test_organizers_by_instance(self):
+        
+        data = (
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20071114T000000Z
+END:VEVENT
+END:VCALENDAR
+""",
+                ()
+            ),
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20071114T000000Z
+ORGANIZER:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                (
+                    ("mailto:user1 at example.com", None),
+                )
+            ),
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20071114T000000Z
+ORGANIZER:mailto:user1 at example.com
+ORGANIZER:mailto:user2 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                ()
+            ),
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20071114T000000Z
+ORGANIZER:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+RRULE:FREQ=YEARLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20081114T000000Z
+DTSTART:20071114T010000Z
+ORGANIZER:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                (
+                    ("mailto:user1 at example.com", None),
+                    ("mailto:user1 at example.com", datetime.datetime(2008, 11, 14, 0, 0, tzinfo=tzutc()))
+                )
+            ),
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20071114T000000Z
+ORGANIZER:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+RRULE:FREQ=YEARLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20091114T000000Z
+DTSTART:20071114T020000Z
+ORGANIZER:mailto:user3 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                (
+                    ("mailto:user1 at example.com", None),
+                    ("mailto:user3 at example.com", datetime.datetime(2009, 11, 14, 0, 0, tzinfo=tzutc()))
+                )
+            ),
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20071114T000000Z
+ORGANIZER:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+RRULE:FREQ=YEARLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20091114T000000Z
+DTSTART:20071114T020000Z
+ORGANIZER:mailto:user3 at example.com
+ORGANIZER:mailto:user4 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                (
+                    ("mailto:user1 at example.com", None),
+                )
+            ),
+        )
+        
+        for caldata, result in data:
+            component = Component.fromString(caldata)
+            self.assertEqual(component.getOrganizersByInstance(), result)
+
+    def test_attendees_by_instance(self):
+        
+        data = (
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20071114T000000Z
+END:VEVENT
+END:VCALENDAR
+""",
+                ()
+            ),
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20071114T000000Z
+ORGANIZER:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                (
+                    ("mailto:user2 at example.com", None),
+                )
+            ),
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20071114T000000Z
+ORGANIZER:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                (
+                    ("mailto:user2 at example.com", None),
+                    ("mailto:user3 at example.com", None),
+                )
+            ),
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20071114T000000Z
+ORGANIZER:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+RRULE:FREQ=YEARLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20081114T000000Z
+DTSTART:20071114T010000Z
+ORGANIZER:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                (
+                    ("mailto:user2 at example.com", None),
+                    ("mailto:user2 at example.com", datetime.datetime(2008, 11, 14, 0, 0, tzinfo=tzutc())),
+                    ("mailto:user3 at example.com", datetime.datetime(2008, 11, 14, 0, 0, tzinfo=tzutc()))
+                )
+            ),
+        )
+        
+        for caldata, result in data:
+            component = Component.fromString(caldata)
+            self.assertEqual(component.getAttendeesByInstance(), result)
+
+    def test_attendees_views(self):
+        
+        data = (
+            # Simple component, no Attendees - no filtering
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+END:VEVENT
+END:VCALENDAR
+""",
+                ()
+            ),
+
+            # Simple component, no Attendees - filtering
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-2
+DTSTART:20071114T000000Z
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+END:VCALENDAR
+""",
+                ("mailto:user01 at example.com",)
+            ),
+
+            # Simple component, with one attendee - filtering match
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-3
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-3
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                ("mailto:user2 at example.com",)
+            ),
+
+            # Simple component, with one attendee - no filtering match
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-4
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+END:VCALENDAR
+""",
+                ("mailto:user3 at example.com",)
+            ),
+
+            # Recurring component with one instance, each with one attendee - filtering match
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-3
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+RRULE:FREQ=YEARLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20081114T000000Z
+DTSTART:20071114T010000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-3
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+RRULE:FREQ=YEARLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20081114T000000Z
+DTSTART:20071114T010000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                ("mailto:user2 at example.com",)
+            ),
+
+            # Recurring component with one instance, each with one attendee - no filtering match
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-4
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+RRULE:FREQ=YEARLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20081114T000000Z
+DTSTART:20071114T010000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+END:VCALENDAR
+""",
+                ("mailto:user3 at example.com",)
+            ),        
+
+            # Recurring component with one instance, master with one attendee, instance without attendee - filtering match
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-3
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+RRULE:FREQ=YEARLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20081114T000000Z
+DTSTART:20071114T010000Z
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-3
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+EXDATE:20081114T000000Z
+ORGANIZER:mailto:user1 at example.com
+RRULE:FREQ=YEARLY
+END:VEVENT
+END:VCALENDAR
+""",
+                ("mailto:user2 at example.com",)
+            ),
+
+            # Recurring component with one instance, master with one attendee, instance without attendee - no filtering match
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-4
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+RRULE:FREQ=YEARLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20081114T000000Z
+DTSTART:20071114T010000Z
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+END:VCALENDAR
+""",
+                ("mailto:user3 at example.com",)
+            ),
+
+            # Recurring component with one instance, master without attendee, instance with attendee - filtering match
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-3
+DTSTART:20071114T000000Z
+ORGANIZER:mailto:user1 at example.com
+RRULE:FREQ=YEARLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20081114T000000Z
+DTSTART:20071114T010000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20081114T000000Z
+DTSTART:20071114T010000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                ("mailto:user2 at example.com",)
+            ),
+
+            # Recurring component with one instance, master without attendee, instance with attendee - no filtering match
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-4
+DTSTART:20071114T000000Z
+ORGANIZER:mailto:user1 at example.com
+RRULE:FREQ=YEARLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20081114T000000Z
+DTSTART:20071114T010000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+END:VCALENDAR
+""",
+                ("mailto:user3 at example.com",)
+            ),
+        )
+        
+        for original, filtered, attendees in data:
+            component = Component.fromString(original)
+            component.attendeesView(attendees)
+            self.assertEqual(filtered, str(component).replace("\r", ""))
+
+    def test_all_but_one_attendee(self):
+        
+        data = (
+            # One component, no attendees
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+END:VEVENT
+END:VCALENDAR
+""",
+                "mailto:user02 at example.com",
+            ),
+
+            # One component, one attendee - removed
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-2
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-2
+DTSTART:20071114T000000Z
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                "mailto:user1 at example.com",
+            ),
+
+            # One component, one attendee - left
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-3
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-3
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                "mailto:user2 at example.com",
+            ),
+
+            # One component, two attendees - none left
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-4
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-4
+DTSTART:20071114T000000Z
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                "mailto:user1 at example.com",
+            ),
+
+            # One component, two attendees - one left
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-5
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-5
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                "mailto:user2 at example.com",
+            ),
+
+        )
+        
+        for original, result, attendee in data:
+            component = Component.fromString(original)
+            component.removeAllButOneAttendee(attendee)
+            self.assertEqual(result, str(component).replace("\r", ""))
+
+    def test_remove_unwanted_properties(self):
+        
+        data = (
+            # One component
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REPLY
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+SUMMARY:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REPLY
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                ("UID", "RECURRENCE-ID", "SEQUENCE", "DTSTAMP", "ORGANIZER", "ATTENDEE",),
+            ),
+
+            # Multiple components
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REPLY
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-3
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+RRULE:FREQ=YEARLY
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20081114T000000Z
+DTSTART:20071114T010000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REPLY
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-3
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20081114T000000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                ("UID", "RECURRENCE-ID", "SEQUENCE", "DTSTAMP", "ORGANIZER", "ATTENDEE",),
+            ),
+
+        )
+        
+        for original, result, keep_properties in data:
+            component = Component.fromString(original)
+            component.removeUnwantedProperties(keep_properties)
+            self.assertEqual(result, str(component).replace("\r", ""))
+
+    def test_remove_alarms(self):
+        
+        data = (
+            # One component, no alarms
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+END:VEVENT
+END:VCALENDAR
+""",
+            ),
+
+            # One component, one alarm
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-2
+DTSTART:20071114T000000Z
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-2
+DTSTART:20071114T000000Z
+END:VEVENT
+END:VCALENDAR
+""",
+            ),
+
+            # Multiple components, one alarm
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-3
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+RRULE:FREQ=YEARLY
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20081114T000000Z
+DTSTART:20071114T010000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-3
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+RRULE:FREQ=YEARLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20081114T000000Z
+DTSTART:20071114T010000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+            ),
+
+            # Multiple components, multiple alarms
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-3
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+RRULE:FREQ=YEARLY
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20081114T000000Z
+DTSTART:20071114T010000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-3
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+RRULE:FREQ=YEARLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20081114T000000Z
+DTSTART:20071114T010000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+            ),
+        )
+        
+        for original, result in data:
+            component = Component.fromString(original)
+            component.removeAlarms()
+            self.assertEqual(result, str(component).replace("\r", ""))

Copied: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/test/test_imip.py (from rev 2800, CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/test/test_imip.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/test/test_imip.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/test/test_imip.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -0,0 +1,500 @@
+##
+# 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.
+##
+
+import datetime
+
+from twistedcaldav.ical import Component
+from twistedcaldav.scheduling.imip import ScheduleViaIMip
+import twistedcaldav.test.util
+
+class iMIP (twistedcaldav.test.util.TestCase):
+    """
+    iCalendar support tests
+    """
+
+    class DummyScheduler(object):
+        def __init__(self, calendar):
+            self.calendar = calendar
+
+    def test_datetime_text(self):
+        
+        imip = ScheduleViaIMip(None, [], [], False)
+        
+        data = (
+            (
+                datetime.datetime(2008, 06, 01, 12, 0, 0),
+                "America/New_York",
+                "Sunday, June  1, 2008 12:00 PM (America/New_York)",
+            ),
+            (
+                datetime.date(2008, 06, 02),
+                "",
+                "Monday, June  2, 2008",
+            ),
+        )
+        
+        for dt, tzid, result in data:
+            self.assertEqual(imip._getDateTimeText(dt, tzid), result)
+        
+    def test_duration_text(self):
+        
+        imip = ScheduleViaIMip(None, [], [], False)
+        
+        data = (
+            (
+                datetime.timedelta(days=1),
+                "1 day",
+            ),
+            (
+                datetime.timedelta(days=2),
+                "2 days",
+            ),
+            (
+                datetime.timedelta(seconds=1*60*60),
+                "1 hour",
+            ),
+            (
+                datetime.timedelta(seconds=2*60*60),
+                "2 hours",
+            ),
+            (
+                datetime.timedelta(seconds=1*60),
+                "1 minute",
+            ),
+            (
+                datetime.timedelta(seconds=2*60),
+                "2 minutes",
+            ),
+            (
+                datetime.timedelta(seconds=1),
+                "1 second",
+            ),
+            (
+                datetime.timedelta(seconds=2),
+                "2 seconds",
+            ),
+            (
+                datetime.timedelta(days=1, seconds=1*60*60),
+                "1 day, 1 hour",
+            ),
+            (
+                datetime.timedelta(days=1, seconds=1*60),
+                "1 day, 1 minute",
+            ),
+            (
+                datetime.timedelta(days=1, seconds=1),
+                "1 day, 1 second",
+            ),
+            (
+                datetime.timedelta(days=1, seconds=1*60*60 + 2*60),
+                "1 day, 1 hour, 2 minutes",
+            ),
+            (
+                datetime.timedelta(seconds=2*60*60 + 15*60),
+                "2 hours, 15 minutes",
+            ),
+        )
+        
+        for duration, result in data:
+            self.assertEqual(imip._getDurationText(duration), result)
+
+    def test_datetime_info(self):
+        data = (
+                   (
+                       """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                        """Starts:      Sunday, June  1, 2008 12:00 PM (UTC)
+Ends:        Sunday, June  1, 2008 01:00 PM (UTC)
+Duration:    1 hour
+""",
+                    ),
+                   (
+                       """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART;VALUE=DATE:20080601
+DTEND;VALUE=DATE:20080602
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                        """Starts:      Sunday, June  1, 2008
+Ends:        Monday, June  2, 2008
+Duration:    1 day
+All Day
+""",
+                    ),
+                   (
+                       """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART;VALUE=DATE:20080601
+DTEND;VALUE=DATE:20080602
+RRULE:FREQ=YEARLY
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                        """Starts:      Sunday, June  1, 2008
+Ends:        Monday, June  2, 2008
+Duration:    1 day
+All Day
+Recurring
+""",
+                    ),
+                )
+        
+        
+        for data, result in data:
+            imip = ScheduleViaIMip(self.DummyScheduler(Component.fromString(data)), [], [], False)
+            self.assertEqual(imip._getDateTimeInfo(imip.scheduler.calendar.masterComponent()), result)
+        
+    def test_calendar_summary(self):
+        data = (
+                   (
+                       """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+SUMMARY:This is an event
+END:VEVENT
+END:VCALENDAR
+""",
+                        """---- Begin Calendar Event Summary ----
+
+Organizer:   User 01 <mailto:user1 at example.com>
+Summary:     This is an event
+Starts:      Sunday, June  1, 2008 12:00 PM (UTC)
+Ends:        Sunday, June  1, 2008 01:00 PM (UTC)
+Duration:    1 hour
+Description: 
+
+----  End Calendar Event Summary  ----
+""",
+                    ),
+                   (
+                       """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART;VALUE=DATE:20080601
+DTEND;VALUE=DATE:20080602
+ORGANIZER;CN="User 02":mailto:user2 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+SUMMARY:This is an event
+DESCRIPTION:Testing iMIP from the calendar server.
+END:VEVENT
+END:VCALENDAR
+""",
+                        """---- Begin Calendar Event Summary ----
+
+Organizer:   User 02 <mailto:user2 at example.com>
+Summary:     This is an event
+Starts:      Sunday, June  1, 2008
+Ends:        Monday, June  2, 2008
+Duration:    1 day
+All Day
+Description: Testing iMIP from the calendar server.
+
+----  End Calendar Event Summary  ----
+""",
+                    ),
+                   (
+                       """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART;VALUE=DATE:20080601
+DTEND;VALUE=DATE:20080602
+RRULE:FREQ=YEARLY
+ORGANIZER;CN="User 03":mailto:user3 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+SUMMARY:This is an event
+DESCRIPTION:Testing iMIP from the calendar server.
+END:VEVENT
+END:VCALENDAR
+""",
+                        """---- Begin Calendar Event Summary ----
+
+Organizer:   User 03 <mailto:user3 at example.com>
+Summary:     This is an event
+Starts:      Sunday, June  1, 2008
+Ends:        Monday, June  2, 2008
+Duration:    1 day
+All Day
+Recurring
+Description: Testing iMIP from the calendar server.
+
+----  End Calendar Event Summary  ----
+""",
+                    ),
+                )
+        
+        
+        for data, result in data:
+            imip = ScheduleViaIMip(self.DummyScheduler(Component.fromString(data)), [], [], False)
+            self.assertEqual(imip._generateCalendarSummary(imip.scheduler.calendar), result)
+        
+        
+    def test_template_message(self):
+        data = (
+                   (
+                       """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+SUMMARY:This is an event
+END:VEVENT
+END:VCALENDAR
+""",
+                        """From: ${fromaddress}
+To: ${toaddress}
+Subject: DO NOT REPLY: calendar invitation test
+Mime-Version: 1.0
+Content-Type: multipart/mixed;
+    boundary="boundary"
+
+
+--boundary
+Content-Type: text/plain
+
+Hi,
+You've been invited to a cool event by CalendarServer's new iMIP processor.
+
+---- Begin Calendar Event Summary ----
+
+Organizer:   User 01 <mailto:user1 at example.com>
+Summary:     This is an event
+Starts:      Sunday, June  1, 2008 12:00 PM (UTC)
+Ends:        Sunday, June  1, 2008 01:00 PM (UTC)
+Duration:    1 hour
+Description: 
+
+----  End Calendar Event Summary  ----
+
+
+--boundary
+Content-Type: text/calendar; charset=utf-8
+Content-Transfer-Encoding: 7bit
+
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+SUMMARY:This is an event
+END:VEVENT
+END:VCALENDAR
+
+--boundary--
+""",
+                    ),
+                   (
+                       """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART;VALUE=DATE:20080601
+DTEND;VALUE=DATE:20080602
+ORGANIZER;CN=User 02:mailto:user2 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+SUMMARY:This is an event
+DESCRIPTION:Testing iMIP from the calendar server.
+END:VEVENT
+END:VCALENDAR
+""",
+                        """From: ${fromaddress}
+To: ${toaddress}
+Subject: DO NOT REPLY: calendar invitation test
+Mime-Version: 1.0
+Content-Type: multipart/mixed;
+    boundary="boundary"
+
+
+--boundary
+Content-Type: text/plain
+
+Hi,
+You've been invited to a cool event by CalendarServer's new iMIP processor.
+
+---- Begin Calendar Event Summary ----
+
+Organizer:   User 02 <mailto:user2 at example.com>
+Summary:     This is an event
+Starts:      Sunday, June  1, 2008
+Ends:        Monday, June  2, 2008
+Duration:    1 day
+All Day
+Description: Testing iMIP from the calendar server.
+
+----  End Calendar Event Summary  ----
+
+
+--boundary
+Content-Type: text/calendar; charset=utf-8
+Content-Transfer-Encoding: 7bit
+
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART;VALUE=DATE:20080601
+DTEND;VALUE=DATE:20080602
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+DESCRIPTION:Testing iMIP from the calendar server.
+ORGANIZER;CN=User 02:mailto:user2 at example.com
+SUMMARY:This is an event
+END:VEVENT
+END:VCALENDAR
+
+--boundary--
+""",
+                    ),
+                   (
+                       """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART;VALUE=DATE:20080601
+DTEND;VALUE=DATE:20080602
+RRULE:FREQ=YEARLY
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 03:mailto:user3 at example.com
+SUMMARY:This is an event
+DESCRIPTION:Testing iMIP from the calendar server.
+END:VEVENT
+END:VCALENDAR
+""",
+                        """From: ${fromaddress}
+To: ${toaddress}
+Subject: DO NOT REPLY: calendar invitation test
+Mime-Version: 1.0
+Content-Type: multipart/mixed;
+    boundary="boundary"
+
+
+--boundary
+Content-Type: text/plain
+
+Hi,
+You've been invited to a cool event by CalendarServer's new iMIP processor.
+
+---- Begin Calendar Event Summary ----
+
+Organizer:   User 03 <mailto:user3 at example.com>
+Summary:     This is an event
+Starts:      Sunday, June  1, 2008
+Ends:        Monday, June  2, 2008
+Duration:    1 day
+All Day
+Recurring
+Description: Testing iMIP from the calendar server.
+
+----  End Calendar Event Summary  ----
+
+
+--boundary
+Content-Type: text/calendar; charset=utf-8
+Content-Transfer-Encoding: 7bit
+
+BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART;VALUE=DATE:20080601
+DTEND;VALUE=DATE:20080602
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+DESCRIPTION:Testing iMIP from the calendar server.
+ORGANIZER;CN=User 03:mailto:user3 at example.com
+RRULE:FREQ=YEARLY
+SUMMARY:This is an event
+END:VEVENT
+END:VCALENDAR
+
+--boundary--
+""",
+                    ),
+                )
+        
+        def _normalizeMessage(text):
+            # First get rid of unwanted headers
+            lines = text.split("\n")
+            lines = [line for line in lines if line.split(":")[0] not in ("Date", "Message-ID",)]
+            
+            # Now get rid of boundary string
+            boundary = None
+            newlines = []
+            for line in lines:
+                if line.startswith("    boundary=\""):
+                    boundary = line[len("    boundary=\""):-1]
+                    line = line.replace(boundary, "boundary")
+                if boundary and line.find(boundary) != -1:
+                    line = line.replace(boundary, "boundary")
+                newlines.append(line)
+            return "\n".join(newlines)
+
+        for data, result in data:
+            imip = ScheduleViaIMip(self.DummyScheduler(Component.fromString(data)), [], [], False)
+            self.assertEqual(_normalizeMessage(imip._generateTemplateMessage(imip.scheduler.calendar)), result)
+        

Copied: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/test/test_itip.py (from rev 2800, CalendarServer/branches/users/cdaboo/implicit-2660/twistedcaldav/test/test_itip.py)
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/test/test_itip.py	                        (rev 0)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/test/test_itip.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -0,0 +1,515 @@
+##
+# 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.ical import Component
+from twistedcaldav.itip import iTipGenerator
+import twistedcaldav.test.util
+from dateutil.tz import tzutc
+import datetime
+import os
+
+class iTIPGenerator (twistedcaldav.test.util.TestCase):
+    """
+    iCalendar support tests
+    """
+    data_dir = os.path.join(os.path.dirname(__file__), "data")
+
+    def test_request(self):
+        
+        data = (
+            # Simple component, no Attendees - no filtering
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REQUEST
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+END:VEVENT
+END:VCALENDAR
+""",
+                ()
+            ),
+
+            # Simple component, no Attendees - filtering
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-2
+DTSTART:20071114T000000Z
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REQUEST
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+END:VCALENDAR
+""",
+                ("mailto:user01 at example.com",)
+            ),
+
+            # Simple component, with one attendee - filtering match
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-3
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REQUEST
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-3
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                ("mailto:user2 at example.com",)
+            ),
+
+            # Simple component, with one attendee - no filtering match
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-4
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REQUEST
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+END:VCALENDAR
+""",
+                ("mailto:user3 at example.com",)
+            ),
+
+            # Recurring component with one instance, each with one attendee - filtering match
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-3
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+RRULE:FREQ=YEARLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20081114T000000Z
+DTSTART:20071114T010000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REQUEST
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-3
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+RRULE:FREQ=YEARLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20081114T000000Z
+DTSTART:20071114T010000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                ("mailto:user2 at example.com",)
+            ),
+
+            # Recurring component with one instance, each with one attendee - no filtering match
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-4
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+RRULE:FREQ=YEARLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20081114T000000Z
+DTSTART:20071114T010000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REQUEST
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+END:VCALENDAR
+""",
+                ("mailto:user3 at example.com",)
+            ),        
+
+            # Recurring component with one instance, master with one attendee, instance without attendee - filtering match
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-3
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+RRULE:FREQ=YEARLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20081114T000000Z
+DTSTART:20071114T010000Z
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REQUEST
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-3
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+EXDATE:20081114T000000Z
+ORGANIZER:mailto:user1 at example.com
+RRULE:FREQ=YEARLY
+END:VEVENT
+END:VCALENDAR
+""",
+                ("mailto:user2 at example.com",)
+            ),
+
+            # Recurring component with one instance, master with one attendee, instance without attendee - no filtering match
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-4
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+RRULE:FREQ=YEARLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20081114T000000Z
+DTSTART:20071114T010000Z
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REQUEST
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+END:VCALENDAR
+""",
+                ("mailto:user3 at example.com",)
+            ),
+
+            # Recurring component with one instance, master without attendee, instance with attendee - filtering match
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-3
+DTSTART:20071114T000000Z
+ORGANIZER:mailto:user1 at example.com
+RRULE:FREQ=YEARLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20081114T000000Z
+DTSTART:20071114T010000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REQUEST
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20081114T000000Z
+DTSTART:20071114T010000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                ("mailto:user2 at example.com",)
+            ),
+
+            # Recurring component with one instance, master without attendee, instance with attendee - no filtering match
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-4
+DTSTART:20071114T000000Z
+ORGANIZER:mailto:user1 at example.com
+RRULE:FREQ=YEARLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20081114T000000Z
+DTSTART:20071114T010000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:REQUEST
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+END:VCALENDAR
+""",
+                ("mailto:user3 at example.com",)
+            ),
+        )
+        
+        for original, filtered, attendees in data:
+            component = Component.fromString(original)
+            itipped = iTipGenerator.generateAttendeeRequest(component, attendees)
+            self.assertEqual(filtered, str(itipped).replace("\r", ""))
+
+    def test_cancel(self):
+        
+        data = (
+            # Simple component, with two attendees - cancel one
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:CANCEL
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
+""",
+                ("mailto:user2 at example.com",),
+                (None,),
+            ),
+
+            # Simple component, with two attendees - cancel two
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-2
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+ORGANIZER:mailto:user1 at example.com
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:CANCEL
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-2
+ATTENDEE:mailto:user3 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+SEQUENCE:2
+END:VEVENT
+END:VCALENDAR
+""",
+                ("mailto:user3 at example.com", "mailto:user2 at example.com",),
+                (None,)
+            ),
+
+            # Recurring component with no instance, one attendee - cancel instance
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-3
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+RRULE:FREQ=YEARLY
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:CANCEL
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-3
+RECURRENCE-ID:20081114T000000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
+""",
+                ("mailto:user2 at example.com",),
+                (datetime.datetime(2008, 11, 14, 0, 0, tzinfo=tzutc()), ),
+            ),
+
+            # Recurring component with one instance, each with one attendee - cancel instance
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-4
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+RRULE:FREQ=YEARLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-4
+RECURRENCE-ID:20081114T000000Z
+DTSTART:20071114T010000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:CANCEL
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-4
+RECURRENCE-ID:20081114T000000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
+""",
+                ("mailto:user2 at example.com",),
+                (datetime.datetime(2008, 11, 14, 0, 0, tzinfo=tzutc()), ),
+            ),
+
+            # Recurring component with one instance, each with one attendee - cancel master
+            (
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-5
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+RRULE:FREQ=YEARLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-5
+RECURRENCE-ID:20081114T000000Z
+DTSTART:20071114T010000Z
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+METHOD:CANCEL
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-5
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+SEQUENCE:1
+END:VEVENT
+END:VCALENDAR
+""",
+                ("mailto:user2 at example.com",),
+                (None, ),
+            ),
+        )
+        
+        for original, filtered, attendees, instances in data:
+            component = Component.fromString(original)
+            itipped = iTipGenerator.generateCancel(component, attendees, instances)
+            itipped = str(itipped).replace("\r", "")
+            itipped = "".join([line for line in itipped.splitlines(True) if not line.startswith("DTSTAMP:")])
+            self.assertEqual(filtered, itipped)

Modified: CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/test/test_options.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/test/test_options.py	2008-08-11 22:41:16 UTC (rev 2804)
+++ CalendarServer/branches/users/cdaboo/implicit-2799/twistedcaldav/test/test_options.py	2008-08-12 01:48:19 UTC (rev 2805)
@@ -18,12 +18,13 @@
 from twisted.web2.test.test_server import SimpleRequest
 
 import twistedcaldav.test.util
+from twistedcaldav.config import config
 
 class OPTIONS (twistedcaldav.test.util.TestCase):
     """
     OPTIONS request
     """
-    def test_dav_header_caldav(self):
+    def test_dav_header_full_caldav(self):
         """
         DAV header advertises CalDAV
         """
@@ -36,12 +37,34 @@
             self.assertIn("access-control", dav, "no DAV access-control header")
             self.assertIn("calendar-access", dav, "no DAV calendar-access header")
             self.assertIn("calendar-schedule", dav, "no DAV calendar-schedule header")
+            self.assertIn("calendar-auto-schedule", dav, "no DAV calendar-auto-schedule header")
             self.assertIn("calendar-availability", dav, "no DAV calendar-availability header")
 
+        config.Scheduling["CalDAV"]["OldDraftCompatability"] = True
         request = SimpleRequest(self.site, "OPTIONS", "/")
 
         return self.send(request, do_test)
 
+    def test_dav_header_implicit_caldav(self):
+        """
+        DAV header advertises CalDAV
+        """
+        def do_test(response):
+            response = IResponse(response)
+
+            dav = response.headers.getHeader("dav")
+            if not dav: self.fail("no DAV header: %s" % (response.headers,))
+            self.assertIn("1", dav, "no DAV level 1 header")
+            self.assertIn("access-control", dav, "no DAV access-control header")
+            self.assertIn("calendar-access", dav, "no DAV calendar-access header")
+            self.assertIn("calendar-auto-schedule", dav, "no DAV calendar-auto-schedule header")
+            self.assertIn("calendar-availability", dav, "no DAV calendar-availability header")
+
+        config.Scheduling["CalDAV"]["OldDraftCompatability"] = False
+        request = SimpleRequest(self.site, "OPTIONS", "/")
+
+        return self.send(request, do_test)
+
     def test_allow_header_caldav(self):
         """
         Allow header advertises MKCALENDAR
-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20080811/32deb07c/attachment-0001.html 


More information about the calendarserver-changes mailing list