[CalendarServer-changes] [2880] CalendarServer/trunk
source_changes at macosforge.org
source_changes at macosforge.org
Tue Aug 26 13:09:01 PDT 2008
Revision: 2880
http://trac.macosforge.org/projects/calendarserver/changeset/2880
Author: cdaboo at apple.com
Date: 2008-08-26 13:09:00 -0700 (Tue, 26 Aug 2008)
Log Message:
-----------
Merge of implicit scheduling branch to trunk.
Modified Paths:
--------------
CalendarServer/trunk/conf/caldavd-test.plist
CalendarServer/trunk/conf/caldavd.plist
CalendarServer/trunk/twistedcaldav/__init__.py
CalendarServer/trunk/twistedcaldav/caldavxml.py
CalendarServer/trunk/twistedcaldav/config.py
CalendarServer/trunk/twistedcaldav/customxml.py
CalendarServer/trunk/twistedcaldav/directory/calendar.py
CalendarServer/trunk/twistedcaldav/directory/principal.py
CalendarServer/trunk/twistedcaldav/extensions.py
CalendarServer/trunk/twistedcaldav/fileops.py
CalendarServer/trunk/twistedcaldav/ical.py
CalendarServer/trunk/twistedcaldav/itip.py
CalendarServer/trunk/twistedcaldav/log.py
CalendarServer/trunk/twistedcaldav/method/copymove.py
CalendarServer/trunk/twistedcaldav/method/delete.py
CalendarServer/trunk/twistedcaldav/method/get.py
CalendarServer/trunk/twistedcaldav/method/mkcalendar.py
CalendarServer/trunk/twistedcaldav/method/propfind.py
CalendarServer/trunk/twistedcaldav/method/put.py
CalendarServer/trunk/twistedcaldav/method/put_common.py
CalendarServer/trunk/twistedcaldav/method/report.py
CalendarServer/trunk/twistedcaldav/method/report_calquery.py
CalendarServer/trunk/twistedcaldav/method/report_common.py
CalendarServer/trunk/twistedcaldav/method/report_freebusy.py
CalendarServer/trunk/twistedcaldav/method/report_multiget.py
CalendarServer/trunk/twistedcaldav/resource.py
CalendarServer/trunk/twistedcaldav/schedule.py
CalendarServer/trunk/twistedcaldav/static.py
CalendarServer/trunk/twistedcaldav/tap.py
CalendarServer/trunk/twistedcaldav/test/test_icalendar.py
CalendarServer/trunk/twistedcaldav/test/test_options.py
CalendarServer/trunk/twistedcaldav/timezoneservice.py
Added Paths:
-----------
CalendarServer/trunk/conf/servertoserver-test.xml
CalendarServer/trunk/conf/servertoserver.dtd
CalendarServer/trunk/lib-patches/vobject/src.vobject.base.patch
CalendarServer/trunk/twistedcaldav/freebusyurl.py
CalendarServer/trunk/twistedcaldav/scheduling/
CalendarServer/trunk/twistedcaldav/scheduling/__init__.py
CalendarServer/trunk/twistedcaldav/scheduling/addressmapping.py
CalendarServer/trunk/twistedcaldav/scheduling/caldav.py
CalendarServer/trunk/twistedcaldav/scheduling/cuaddress.py
CalendarServer/trunk/twistedcaldav/scheduling/delivery.py
CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py
CalendarServer/trunk/twistedcaldav/scheduling/imip.py
CalendarServer/trunk/twistedcaldav/scheduling/implicit.py
CalendarServer/trunk/twistedcaldav/scheduling/ischedule.py
CalendarServer/trunk/twistedcaldav/scheduling/ischeduleservers.py
CalendarServer/trunk/twistedcaldav/scheduling/itip.py
CalendarServer/trunk/twistedcaldav/scheduling/processing.py
CalendarServer/trunk/twistedcaldav/scheduling/scheduler.py
CalendarServer/trunk/twistedcaldav/scheduling/test/
CalendarServer/trunk/twistedcaldav/scheduling/test/__init__.py
CalendarServer/trunk/twistedcaldav/scheduling/test/test_icaldiff.py
CalendarServer/trunk/twistedcaldav/scheduling/test/test_implicit.py
CalendarServer/trunk/twistedcaldav/scheduling/test/test_itip.py
CalendarServer/trunk/twistedcaldav/test/test_imip.py
Removed Paths:
-------------
CalendarServer/trunk/twistedcaldav/scheduling/__init__.py
CalendarServer/trunk/twistedcaldav/scheduling/addressmapping.py
CalendarServer/trunk/twistedcaldav/scheduling/caldav.py
CalendarServer/trunk/twistedcaldav/scheduling/cuaddress.py
CalendarServer/trunk/twistedcaldav/scheduling/delivery.py
CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py
CalendarServer/trunk/twistedcaldav/scheduling/imip.py
CalendarServer/trunk/twistedcaldav/scheduling/implicit.py
CalendarServer/trunk/twistedcaldav/scheduling/ischedule.py
CalendarServer/trunk/twistedcaldav/scheduling/ischeduleservers.py
CalendarServer/trunk/twistedcaldav/scheduling/itip.py
CalendarServer/trunk/twistedcaldav/scheduling/processing.py
CalendarServer/trunk/twistedcaldav/scheduling/scheduler.py
CalendarServer/trunk/twistedcaldav/scheduling/test/
CalendarServer/trunk/twistedcaldav/scheduling/test/__init__.py
CalendarServer/trunk/twistedcaldav/scheduling/test/test_icaldiff.py
CalendarServer/trunk/twistedcaldav/scheduling/test/test_implicit.py
CalendarServer/trunk/twistedcaldav/scheduling/test/test_itip.py
Property Changed:
----------------
CalendarServer/trunk/twistedcaldav/test/
Modified: CalendarServer/trunk/conf/caldavd-test.plist
===================================================================
--- CalendarServer/trunk/conf/caldavd-test.plist 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/conf/caldavd-test.plist 2008-08-26 20:09:00 UTC (rev 2880)
@@ -379,7 +379,73 @@
<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/>
+ <key>DefaultCalendarProvisioned</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>TimePeriod</key>
+ <integer>14</integer>
+ <key>AnonymousAccess</key>
+ <false/>
+ </dict>
+
<!--
Twisted
-->
Modified: CalendarServer/trunk/conf/caldavd.plist
===================================================================
--- CalendarServer/trunk/conf/caldavd.plist 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/conf/caldavd.plist 2008-08-26 20:09:00 UTC (rev 2880)
@@ -348,6 +348,72 @@
<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/>
+ <key>DefaultCalendarProvisioned</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>TimePeriod</key>
+ <integer>14</integer>
+ <key>AnonymousAccess</key>
+ <false/>
+ </dict>
+
</dict>
</plist>
Copied: CalendarServer/trunk/conf/servertoserver-test.xml (from rev 2879, CalendarServer/branches/users/cdaboo/implicit-2867/conf/servertoserver-test.xml)
===================================================================
--- CalendarServer/trunk/conf/servertoserver-test.xml (rev 0)
+++ CalendarServer/trunk/conf/servertoserver-test.xml 2008-08-26 20:09:00 UTC (rev 2880)
@@ -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/trunk/conf/servertoserver.dtd (from rev 2879, CalendarServer/branches/users/cdaboo/implicit-2867/conf/servertoserver.dtd)
===================================================================
--- CalendarServer/trunk/conf/servertoserver.dtd (rev 0)
+++ CalendarServer/trunk/conf/servertoserver.dtd 2008-08-26 20:09:00 UTC (rev 2880)
@@ -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
Copied: CalendarServer/trunk/lib-patches/vobject/src.vobject.base.patch (from rev 2879, CalendarServer/branches/users/cdaboo/implicit-2867/lib-patches/vobject/src.vobject.base.patch)
===================================================================
--- CalendarServer/trunk/lib-patches/vobject/src.vobject.base.patch (rev 0)
+++ CalendarServer/trunk/lib-patches/vobject/src.vobject.base.patch 2008-08-26 20:09:00 UTC (rev 2880)
@@ -0,0 +1,24 @@
+Index: src/vobject/base.py
+===================================================================
+--- src/vobject/base.py (revision 192)
++++ src/vobject/base.py (working copy)
+@@ -273,6 +273,8 @@
+ self.value = copy.copy(copyit.value)
+ self.encoded = self.encoded
+ self.params = copy.copy(copyit.params)
++ for k,v in self.params.items():
++ self.params[k] = copy.copy(v)
+ self.singletonparams = copy.copy(copyit.singletonparams)
+ self.lineNumber = copyit.lineNumber
+
+@@ -912,7 +914,9 @@
+ s.write(str(obj.group + '.'))
+ if DEBUG: logger.debug("Serializing line" + str(obj))
+ s.write(str(obj.name.upper()))
+- for key, paramvals in obj.params.iteritems():
++ keys = sorted(obj.params.iterkeys())
++ for key in keys:
++ paramvals = obj.params[key]
+ s.write(';' + str(key) + '=' + ','.join(map(dquoteEscape, paramvals)).encode("utf-8"))
+ if isinstance(obj.value, unicode):
+ strout = obj.value.encode("utf-8")
Modified: CalendarServer/trunk/twistedcaldav/__init__.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/__init__.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/__init__.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -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/trunk/twistedcaldav/caldavxml.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/caldavxml.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/caldavxml.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -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/trunk/twistedcaldav/config.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/config.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/config.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -175,7 +175,49 @@
"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
+ "DefaultCalendarProvisioned" : True, # Whether the provisioned default calendar is marked as the scheduling default
+ },
+
+ "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 +310,9 @@
class Config (object):
+ """
+ @DynamicAttrs
+ """
def __init__(self, defaults):
self.setDefaults(defaults)
self._data = copy.deepcopy(self._defaults)
Modified: CalendarServer/trunk/twistedcaldav/customxml.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/customxml.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/customxml.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -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/trunk/twistedcaldav/directory/calendar.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/calendar.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/directory/calendar.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -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,28 @@
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)))
+ inbox.processFreeBusyCalendar(childURL, True)
+ # Default calendar may need to be marked as the default for scheduling
+ if config.Scheduling["CalDAV"]["DefaultCalendarProvisioned"]:
+ inbox.writeDeadProperty(caldavxml.ScheduleDefaultCalendarURL(davxml.HRef(childURL)))
+
return self
d = child.createCalendarCollection()
- d.addCallback(setupChild)
+ d.addCallback(setupFreeBusy)
return d
def provisionChild(self, name):
@@ -323,6 +336,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/trunk/twistedcaldav/directory/principal.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/directory/principal.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/directory/principal.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -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/trunk/twistedcaldav/extensions.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/extensions.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/extensions.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -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/trunk/twistedcaldav/fileops.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/fileops.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/fileops.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -13,15 +13,13 @@
# 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.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 +28,6 @@
def __init__(self, fp):
self.fp = fp
- at deferredGenerator
def putWithXAttrs(stream, filepath):
"""
Write a file to a possibly existing path and preserve any xattrs at that path.
@@ -50,20 +47,21 @@
xold = None
# First do the actual file copy
- d = waitForDeferred(put(stream, filepath))
- yield d
- response = d.getResult()
+ def _gotResponse(response):
+
+ # Restore original xattrs.
+ if props:
+ xnew = xattrPropertyStore(FakeXAttrResource(filepath))
+ for prop in props:
+ xnew.set(prop)
+ xnew = None
+
+ return response
- # Restore original xattrs.
- if props:
- xnew = xattrPropertyStore(FakeXAttrResource(filepath))
- for prop in props:
- xnew.set(prop)
- xnew = None
+ d = put(stream, filepath)
+ d.addCallback(_gotResponse)
+ return d
- yield response
-
- at deferredGenerator
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 +75,16 @@
"""
# First do the actual file copy
- d = waitForDeferred(copy(source_filepath, destination_filepath, destination_uri, "0"))
- yield d
- response = d.getResult()
-
- # Now copy over xattrs.
- copyXAttrs(source_filepath, destination_filepath)
+ def _gotResponse(response):
- yield response
+ # Now copy over xattrs.
+ copyXAttrs(source_filepath, destination_filepath)
+
+ return response
+
+ d = copy(source_filepath, destination_filepath, destination_uri, "0")
+ d.addCallback(_gotResponse)
+ return d
def copyToWithXAttrs(from_fp, to_fp):
"""
Copied: CalendarServer/trunk/twistedcaldav/freebusyurl.py (from rev 2879, CalendarServer/branches/users/cdaboo/implicit-2867/twistedcaldav/freebusyurl.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/freebusyurl.py (rev 0)
+++ CalendarServer/trunk/twistedcaldav/freebusyurl.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -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/trunk/twistedcaldav/ical.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/ical.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/ical.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -29,26 +29,18 @@
"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 types import ListType
+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()
@@ -92,12 +84,16 @@
def __str__ (self): return self._vobject.serialize()
def __repr__(self): return "<%s: %r: %r>" % (self.__class__.__name__, self.name(), self.value())
- def __hash__(self): return hash((self.name(), self.value()))
+ def __hash__(self):
+ if type(self.value()) is ListType:
+ return hash((self.name(), tuple(self.value())))
+ else:
+ return hash((self.name(), self.value()))
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):
@@ -698,6 +694,22 @@
return result
+ def timezones(self):
+ """
+ Returns the set of TZID's for each VTIMEZONE component.
+
+ @return: a set of strings, one for each unique TZID value.
+ """
+
+ assert self.name() == "VCALENDAR", "Not a calendar: %r" % (self,)
+
+ results = set()
+ for component in self.subcomponents():
+ if component.name() == "VTIMEZONE":
+ results.add(component.propertyValue("TZID"))
+
+ return results
+
def expand(self, start, end):
"""
Expand the components into a set of new components, one for each
@@ -739,7 +751,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)
@@ -795,6 +807,59 @@
instances.expandTimeRanges(componentSet, limit)
return instances
+ def getComponentInstances(self):
+ """
+ Get the R-ID value for each component.
+
+ @return: a tuple of recurrence-ids
+ """
+
+ # 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.getComponentInstances()
+ return result
+ else:
+ rid = self.getRecurrenceIDUTC()
+ return (rid,)
+
+ 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.
@@ -1022,6 +1087,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.
@@ -1062,6 +1153,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.
@@ -1137,6 +1249,133 @@
return None
+ def setParameterToValueForPropertyWithValue(self, paramname, paramvalue, propname, propvalue):
+ """
+ Add or change the parameter to the specified value on the property having the specified value.
+
+ @param paramname: the parameter name
+ @type paramname: C{str}
+ @param paramvalue: the parameter value to set
+ @type paramvalue: C{str}
+ @param propname: the property name
+ @type propname: C{str}
+ @param propvalue: the property value to test
+ @type propvalue: C{str}
+ """
+
+ for component in self.subcomponents():
+ if component.name() == "VTIMEZONE":
+ continue
+ for property in component.properties(propname):
+ if property.value() == propvalue:
+ property.params()[paramname] = [paramvalue]
+
+ def addPropertyToAllComponents(self, property):
+ """
+ Add a property to all top-level components except VTIMEZONE.
+
+ @param property: the property to add
+ @type property: L{Property}
+ """
+
+ for component in self.subcomponents():
+ if component.name() == "VTIMEZONE":
+ continue
+ component.addProperty(property)
+
+ 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
##
@@ -1301,9 +1540,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/trunk/twistedcaldav/itip.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/itip.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/itip.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -47,7 +47,6 @@
from twistedcaldav.log import Logger
from twistedcaldav.ical import Property, iCalendarProductID
from twistedcaldav.method import report_common
-from twistedcaldav.method.put_common import storeCalendarObjectResource
from twistedcaldav.resource import isCalendarCollectionResource
log = Logger()
@@ -55,893 +54,846 @@
__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
- check_reply = True
+ if new_master:
+ # So we have a full update. That means we need to delete any existing events completely and
+ # replace with the ones provided so long as the new one is newer.
+
+ # If we have a match then we need to check whether we are updating etc
+ check_reply = False
+ if calmatch:
+ # See whether the new component is older than any existing ones and throw it away if so
+ newinfo = (None,) + self.getComponentSyncInfo(new_master)
+ cal = updatecal.iCalendar(calmatch)
+ info = self.getSyncInfo(calmatch, cal)
+ if self.compareSyncInfo(info, newinfo) < 0:
+ # Existing resource is older and will be replaced
+ check_reply = True
+ else:
+ processed = "older"
else:
- processed = "older"
+ # We have a new request which we can reply to
+ check_reply = True
+
+ if check_reply:
+ # Process the reply by determining PARTSTAT and sending the reply and booking the event.
+ 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:
- # 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:
- if calmatch:
- newchild = yield writeResource(request, calURL, updatecal, calmatch, calendar)
- log.info("Replaced calendar component %s with new iTIP message in %s." % (calmatch, calURL))
+ # 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:
- 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))
+ info = None
- # 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
+ if info is None or self.compareSyncInfo(info, newinfo) < 0:
+ # Existing resource is older and will be replaced
+ check_reply = True
+ processed = "processed"
+ else:
+ self.calendar.removeComponent(component)
+ else:
+ # We have a new request which we can reply to
+ check_reply = True
+
+ if check_reply:
+ # Process the reply by determining PARTSTAT and sending the reply and booking the event.
+ doreply, replycal, accepted = yield self.checkForReply()
- 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
+ try:
+ if calmatch:
+ # 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:
+ 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)
+
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.
-
- # 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 getCalendarsToMatch(self):
+ # Determine the set of calendar URIs for a principal need to be searched.
- rid = component.getRecurrenceIDUTC()
- old_component = oldcal.overriddenComponent(rid)
- if old_component:
- oldcal.removeComponent(old_component)
- oldcal.addComponent(component)
+ # Find the current recipients calendar-free-busy-set
+ return self.principal.calendarFreeBusyURIs(self.request)
-def getAllInfo(collection, calendar, ignore):
- """
- Find each component in the calendar collection that has a matching UID with
- the supplied component, and get useful synchronization details from it, ignoring
- the one with the supplied resource name.
-
- @param collection: the L{CalDAVFile} for the calendar collection.
- @param calendar: the L{Component} for the component being compared with.
- @param ignore: the C{str} containing the name of a resource to ignore,
- or C{None} if none to ignore.
- @return: C{list} of synchronization information for each resource found.
- """
- names = []
- try:
- # Extract UID from primary component (note we allow multiple components to be present
- # because CANCEL requests can have multiple components).
- comp = calendar.mainComponent(allow_multiple=True)
- uid = comp.propertyValue("UID")
+ def matchComponentInCalendar(self, collection, calendar):
+ """
+ See if the component in the provided iTIP calendar object matches any in the specified calendar
+ collection.
- # Now use calendar collection index to find all other resources with the same UID
- index = collection.index()
- names = index.resourceNamesForUID(uid)
+ @param collection: L{CalDAVFile} for the calendar collection to examine.
+ @param calendar: L{Component} for calendar to examine.
+ @return: C{list} of resource names found.
+ """
+
+ try:
+ # Extract UID from primary component (note we allow multiple components to be present
+ # because CANCEL requests can have multiple components).
+ comp = calendar.mainComponent(allow_multiple=True)
+ uid = comp.propertyValue("UID")
+
+ # Now use calendar collection index to find all other resources with the same UID
+ index = collection.index()
+ result = index.resourceNamesForUID(uid)
+
+ # There can be only one
+ if len(result) > 0:
+ return result[0]
+ else:
+ return None
+ except ValueError:
+ return None
+
+ def findMatchingComponent(self, component, calendar):
+ """
+ See if any overridden component in the provided iTIP calendar object matches the specified component.
- # Remove the one we want to ignore
- if ignore is not None:
- names = [name for name in names if name != ignore.fp.basename()]
- except ValueError:
- return []
+ @param component: the component to try and match.
+ @type component: L{Component}
+ @param calendar: the calendar to find a match in.
+ @type calendar: L{Component}
+ @return: L{Component} for matching component,
+ or C{None} if not found.
+ """
- # Now get info for each name
- result = []
- for name in names:
- cal = collection.iCalendar(name)
- result.append(getSyncInfo(name, cal))
-
- return result
+ # Extract RECURRENCE-ID value from component
+ rid = component.getRecurrenceIDUTC()
+
+ # Return the one that matches in the calendar
+ return calendar.overriddenComponent(rid)
-def getSyncInfo(name, calendar):
- """
- Get property value details needed to synchronize iTIP components.
+ def mergeComponents(self, newcal, oldcal):
+ """
+ Merge the overridden instance components in newcal into oldcal replacing any
+ matching components there.
- @param calendar: L{Component} for calendar to check.
- @return: C{tuple} of (uid, seq, dtstamp, r-id) some of which may be C{None} if property does not exist
- """
- try:
- # Extract components from primary component (note we allow multiple components to be present
- # because CANCEL requests can have multiple components).
- comp = calendar.mainComponent(allow_multiple=True)
- uid, seq, dtstamp, rid = getComponentSyncInfo(comp)
+ @param newcal: the new overridden instances to use.
+ @type newcal: L{Component}
+ @param oldcal: the component to merge into.
+ @type oldcal: L{Component}
+ """
- except ValueError:
- return (name, None, None, None, None)
+ # FIXME: going to ignore VTIMEZONE - i.e. will assume that the component being added
+ # use a TZID that is already specified in the old component set.
- return (name, uid, seq, dtstamp, rid)
-
-def getComponentSyncInfo(component):
- """
- Get property value details needed to synchronize iTIP components.
+ # We will update the SEQUENCE on the master to the highest value of the current one on the master
+ # or the ones in the components we are changing.
- @param component: L{Component} to check.
- @return: C{tuple} of (uid, seq, dtstamp, r-id) some of which may be C{None} if property does not exist
- """
- try:
- # Extract items from component
- uid = component.propertyValue("UID")
- seq = component.propertyValue("SEQUENCE")
- if seq:
- seq = int(seq)
- dtstamp = component.propertyValue("DTSTAMP")
- rid = component.propertyValue("RECURRENCE-ID")
+ for component in newcal.subcomponents():
+ if component.name() == "VTIMEZONE":
+ continue
+
+ rid = component.getRecurrenceIDUTC()
+ old_component = oldcal.overriddenComponent(rid)
+ if old_component:
+ oldcal.removeComponent(old_component)
+ oldcal.addComponent(component)
+
+ def getAllInfo(self, collection, calendar, ignore):
+ """
+ Find each component in the calendar collection that has a matching UID with
+ the supplied component, and get useful synchronization details from it, ignoring
+ the one with the supplied resource name.
+
+ @param collection: the L{CalDAVFile} for the calendar collection.
+ @param calendar: the L{Component} for the component being compared with.
+ @param ignore: the C{str} containing the name of a resource to ignore,
+ or C{None} if none to ignore.
+ @return: C{list} of synchronization information for each resource found.
+ """
+ names = []
+ try:
+ # Extract UID from primary component (note we allow multiple components to be present
+ # because CANCEL requests can have multiple components).
+ comp = calendar.mainComponent(allow_multiple=True)
+ uid = comp.propertyValue("UID")
+
+ # Now use calendar collection index to find all other resources with the same UID
+ index = collection.index()
+ names = index.resourceNamesForUID(uid)
+
+ # Remove the one we want to ignore
+ if ignore is not None:
+ names = [name for name in names if name != ignore.fp.basename()]
+ except ValueError:
+ return []
- except ValueError:
- return (None, None, None, None)
+ # Now get info for each name
+ result = []
+ for name in names:
+ cal = collection.iCalendar(name)
+ result.append(self.getSyncInfo(name, cal))
- return (uid, seq, dtstamp, rid)
-
-def compareComponents(component1, component2):
- """
- Compare synchronization information for two components to see if they match according to iTIP.
-
- @param component1: first component to check.
- @type component1: L{Component}
- @param component2: second component to check.
- @type component2: L{Component}
+ return result
+
+ def getSyncInfo(self, name, calendar):
+ """
+ Get property value details needed to synchronize iTIP components.
+
+ @param calendar: L{Component} for calendar to check.
+ @return: C{tuple} of (uid, seq, dtstamp, r-id) some of which may be C{None} if property does not exist
+ """
+ try:
+ # Extract components from primary component (note we allow multiple components to be present
+ # because CANCEL requests can have multiple components).
+ comp = calendar.mainComponent(allow_multiple=True)
+ uid, seq, dtstamp, rid = self.getComponentSyncInfo(comp)
+
+ except ValueError:
+ return (name, None, None, None, None)
+
+ return (name, uid, seq, dtstamp, rid)
- @return: 0, 1, -1 as per compareSyncInfo.
- """
- info1 = (None,) + getComponentSyncInfo(component1)
- info2 = (None,) + getComponentSyncInfo(component2)
- return compareSyncInfo(info1, info2)
-
-def compareSyncInfo(info1, info2):
- """
- Compare two synchronization information records.
+ def getComponentSyncInfo(self, component):
+ """
+ Get property value details needed to synchronize iTIP components.
+
+ @param component: L{Component} to check.
+ @return: C{tuple} of (uid, seq, dtstamp, r-id) some of which may be C{None} if property does not exist
+ """
+ try:
+ # Extract items from component
+ uid = component.propertyValue("UID")
+ seq = component.propertyValue("SEQUENCE")
+ if seq:
+ seq = int(seq)
+ dtstamp = component.propertyValue("DTSTAMP")
+ rid = component.propertyValue("RECURRENCE-ID")
+
+ except ValueError:
+ return (None, None, None, None)
+
+ return (uid, seq, dtstamp, rid)
- @param info1: a C{tuple} as returned by L{getSyncInfo}.
- @param info2: a C{tuple} as returned by L{getSyncInfo}.
- @return: 1 if info1 > info2, 0 if info1 == info2, -1 if info1 < info2
- """
- # UIDs MUST match
- assert info1[1] == info2[1]
+ def compareComponents(self, component1, component2):
+ """
+ Compare synchronization information for two components to see if they match according to iTIP.
- # Look for sequence
- if (info1[2] is not None) and (info2[2] is not None):
- if info1[2] > info2[2]:
+ @param component1: first component to check.
+ @type component1: L{Component}
+ @param component2: second component to check.
+ @type component2: L{Component}
+
+ @return: 0, 1, -1 as per compareSyncInfo.
+ """
+ info1 = (None,) + self.getComponentSyncInfo(component1)
+ info2 = (None,) + self.getComponentSyncInfo(component2)
+ return self.compareSyncInfo(info1, info2)
+
+ def compareSyncInfo(self, info1, info2):
+ """
+ Compare two synchronization information records.
+
+ @param info1: a C{tuple} as returned by L{getSyncInfo}.
+ @param info2: a C{tuple} as returned by L{getSyncInfo}.
+ @return: 1 if info1 > info2, 0 if info1 == info2, -1 if info1 < info2
+ """
+ # UIDs MUST match
+ assert info1[1] == info2[1]
+
+ # Look for sequence
+ if (info1[2] is not None) and (info2[2] is not None):
+ if info1[2] > info2[2]:
+ return 1
+ if info1[2] < info2[2]:
+ return -1
+ elif (info1[2] is not None) and (info2[2] is None):
return 1
- if info1[2] < info2[2]:
+ elif (info1[2] is None) and (info2[2] is not None):
return -1
- elif (info1[2] is not None) and (info2[2] is None):
- return 1
- elif (info1[2] is None) and (info2[2] is not None):
- return -1
-
- # Look for DTSTAMP
- if (info1[3] is not None) and (info2[3] is not None):
- if info1[3] > info2[3]:
+
+ # Look for DTSTAMP
+ if (info1[3] is not None) and (info2[3] is not None):
+ if info1[3] > info2[3]:
+ return 1
+ if info1[3] < info2[3]:
+ return -1
+ elif (info1[3] is not None) and (info2[3] is None):
return 1
- if info1[3] < info2[3]:
+ elif (info1[3] is None) and (info2[3] is not None):
return -1
- elif (info1[3] is not None) and (info2[3] is None):
- return 1
- elif (info1[3] is None) and (info2[3] is not None):
- return -1
-
- return 0
+
+ return 0
Modified: CalendarServer/trunk/twistedcaldav/log.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/log.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/log.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -59,6 +59,14 @@
from twisted.python import log
+from StringIO import StringIO
+
+from twisted.internet.defer import succeed
+
+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,84 @@
"""
return cmpLogLevels(self.level(), level) <= 0
+ 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.
+ def _gotData(data):
+ iostr.write(data)
+
+ request.stream = MemoryStream(data)
+ request.stream.doStartReading = None
+
+ self.emit(level, iostr.getvalue(), **kwargs)
+
+ d = allDataFromStream(request.stream)
+ d.addCallback(_gotData)
+ return d
+
+ else:
+ return succeed(None)
+
+ 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.
+ def _gotData(data):
+ iostr.write(data)
+
+ response.stream = MemoryStream(data)
+ response.stream.doStartReading = None
+
+ self.emit(level, iostr.getvalue(), **kwargs)
+
+ d = allDataFromStream(response.stream)
+ d.addCallback(_gotData)
+ return d
+
class LoggingMixIn (object):
"""
Mix-in class for logging methods.
Modified: CalendarServer/trunk/twistedcaldav/method/copymove.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/copymove.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/method/copymove.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -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/trunk/twistedcaldav/method/delete.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/delete.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/method/delete.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -20,35 +20,54 @@
__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 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/trunk/twistedcaldav/method/get.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/get.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/method/get.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -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/trunk/twistedcaldav/method/mkcalendar.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/mkcalendar.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/method/mkcalendar.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -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/trunk/twistedcaldav/method/propfind.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/propfind.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/method/propfind.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -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/trunk/twistedcaldav/method/put.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/put.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/method/put.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -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/trunk/twistedcaldav/method/put_common.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/put_common.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/method/put_common.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -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, succeed
+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,230 @@
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,
+ allowImplicitSchedule=True,
+ ):
+ """
+ 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.
+ @param allowImplicitSchedule: True if implicit scheduling should be attempted, 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.allowImplicitSchedule = allowImplicitSchedule
+
+ self.rollback = None
+ self.access = None
- def validResourceName():
+ 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:
+ return self.validAccess()
+ else:
+ return succeed(None)
+
+ def validResourceName(self):
+ """
Make sure that the resource name for the new resource is valid.
"""
result = True
message = ""
- filename = destination.fp.basename()
+ filename = self.destination.fp.basename()
if filename.startswith("."):
result = False
message = "File name %s not allowed in calendar collection" % (filename,)
return result, message
- def validContentType():
+ def validContentType(self):
"""
Make sure that the content-type of the source resource is text/calendar.
This test is only needed when the source is not in a calendar collection.
"""
result = True
message = ""
- content_type = source.contentType()
+ content_type = self.source.contentType()
if not ((content_type.mediaType == "text") and (content_type.mediaSubtype == "calendar")):
result = False
message = "MIME type %s not allowed in calendar collection" % (content_type,)
return result, message
- def validCalendarDataCheck():
+ def validCalendarDataCheck(self):
"""
Check that the calendar data is valid iCalendar.
- @return: tuple: (True/False if the calendra data is valid,
+ @return: tuple: (True/False if the calendar data is valid,
log message string).
"""
result = True
message = ""
- if calendar is None:
+ if self.calendar is None:
result = False
message = "Empty resource not allowed in calendar collection"
else:
try:
- calendar.validCalendarForCalDAV()
+ self.calendar.validCalendarForCalDAV()
except ValueError, e:
result = False
message = "Invalid calendar data: %s" % (e,)
return result, message
- def validCalDAVDataCheck():
+ def validCalDAVDataCheck(self):
"""
Check that the calendar data is valid as a CalDAV calendar object resource.
@return: tuple: (True/False if the calendar data is valid,
@@ -241,17 +368,17 @@
result = True
message = ""
try:
- if isiTIP:
- calendar.validateComponentsForCalDAV(True)
+ if self.isiTIP:
+ self.calendar.validateComponentsForCalDAV(True)
else:
- calendar.validateForCalDAV()
+ self.calendar.validateForCalDAV()
except ValueError, e:
result = False
message = "Calendar data does not conform to CalDAV requirements: %s" % (e,)
return result, message
- def validSizeCheck():
+ def validSizeCheck(self):
"""
Make sure that the content-type of the source resource is text/calendar.
This test is only needed when the source is not in a calendar collection.
@@ -259,53 +386,53 @@
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():
+ 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()
-
- authz = destinationparent.currentPrincipal(request)
- if davxml.Principal(parent_owner) != authz and access != Component.ACCESS_PUBLIC:
- raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (calendarserver_namespace, "valid-access-restriction-change")))
+ def _callback(parent_owner):
+
+ 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")))
+
+ return None
- yield access, calendardata
+ d = self.destinationparent.owner(self.request)
+ d.addCallback(_callback)
+ return d
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)
+
+ return succeed(None)
- 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 +442,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 +459,296 @@
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 and self.allowImplicitSchedule:
+ scheduler = ImplicitScheduler()
+ new_calendar = (yield scheduler.doImplicitScheduling(self.request, self.destination, self.calendar, False))
+ if new_calendar:
+ self.calendar = new_calendar
+ self.calendardata = str(self.calendar)
- 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/trunk/twistedcaldav/method/report.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/report.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/method/report.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -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/trunk/twistedcaldav/method/report_calquery.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/report_calquery.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/method/report_calquery.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -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/trunk/twistedcaldav/method/report_common.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/report_common.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/method/report_common.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -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/trunk/twistedcaldav/method/report_freebusy.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/report_freebusy.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/method/report_freebusy.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -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,7 @@
fbinfo = ([], [], [])
matchcount = [0]
+
def generateFreeBusyInfo(calresource, uri): #@UnusedVariable
"""
Run a free busy report on the specified calendar collection
@@ -62,18 +64,19 @@
@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)
+
+ def _gotResult(result):
+ matchcount[0] = result
+ return True
+ d = report_common.generateFreeBusyInfo(request, calresource, fbinfo, timerange, matchcount[0])
+ d.addCallback(_gotResult)
+ return d
+
# 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 +88,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/trunk/twistedcaldav/method/report_multiget.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/report_multiget.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/method/report_multiget.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -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/trunk/twistedcaldav/resource.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/resource.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/resource.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -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
@@ -40,7 +39,8 @@
from twisted.web2.dav.davxml import dav_namespace
from twisted.web2.dav.http import ErrorResponse
from twisted.web2.dav.resource import TwistedACLInheritable
-from twisted.web2.dav.util import joinURL, parentForURL, unimplemented
+from twisted.web2.dav.util import joinURL, parentForURL, unimplemented,\
+ normalizeURL
from twisted.web2.http import HTTPError, RedirectResponse, StatusResponse, Response
from twisted.web2.http_headers import MimeType
from twisted.web2.iweb import IResponse
@@ -60,6 +60,8 @@
from twistedcaldav.ical import Component as iComponent
from twistedcaldav.log import LoggingMixIn
+from urlparse import urlsplit
+
if twistedcaldav.__version__:
serverVersion = twisted.web2.server.VERSION + " TwistedCalDAV/" + twistedcaldav.__version__
else:
@@ -68,7 +70,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:
@@ -237,6 +242,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,)
@@ -271,8 +277,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)
@@ -284,20 +307,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(
@@ -327,36 +346,46 @@
)
acls = davxml.ACL(ace, *acls.children)
- yield acls
+ returnValue(acls)
- @deferredGenerator
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()
- if parent and isinstance(parent, CalDAVResource):
- d = waitForDeferred(parent.owner(request))
- yield d
- yield d.getResult()
- else:
- yield None
+
+ def _gotParent(parent):
+ if parent and isinstance(parent, CalDAVResource):
+ return parent.owner(request)
- @deferredGenerator
+ d = self.locateParent(request, request.urlForResource(self))
+ d.addCallback(_gotParent)
+ return d
+
+ def ownerPrincipal(self, request):
+ """
+ Return the DAV:owner property value (MUST be a DAV:href or None).
+ """
+ def _gotParent(parent):
+ if parent and isinstance(parent, CalDAVResource):
+ return parent.ownerPrincipal(request)
+
+ d = self.locateParent(request, request.urlForResource(self))
+ d.addCallback(_gotParent)
+ return d
+
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
+ def _gotOwner(owner):
+ return davxml.Principal(owner) == self.currentPrincipal(request)
+ d = self.owner(request)
+ d.addCallback(_gotOwner)
+ return d
+
##
# CalDAV
##
@@ -449,6 +478,65 @@
"""
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)
+
+ # Also check the default calendar setting and remove it if the default is deleted
+ default = (yield inbox.readProperty((caldav_namespace, "schedule-default-calendar-URL"), request))
+ if default and len(default.children) == 1:
+ defaultURL = normalizeURL(str(default.children[0]))
+ if normalizeURL(request.path) == defaultURL:
+ yield inbox.writeProperty(caldavxml.ScheduleDefaultCalendarURL())
+
+ @inlineCallbacks
+ def movedCalendar(self, request, destination, destination_uri):
+ """
+ Calendar has been moved. 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:
+ (_ignore_scheme, _ignore_host, destination_path, _ignore_query, _ignore_fragment) = urlsplit(normalizeURL(destination_uri))
+
+ inbox = (yield request.locateResource(inboxURL))
+ inbox.processFreeBusyCalendar(request.path, False)
+ inbox.processFreeBusyCalendar(destination_uri, destination.isCalendarOpaque())
+
+ # Also check the default calendar setting and remove it if the default is deleted
+ default = (yield inbox.readProperty((caldav_namespace, "schedule-default-calendar-URL"), request))
+ if default and len(default.children) == 1:
+ defaultURL = normalizeURL(str(default.children[0]))
+ if normalizeURL(request.path) == defaultURL:
+ yield inbox.writeProperty(caldavxml.ScheduleDefaultCalendarURL(davxml.HRef(destination_path)))
+
+ 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/trunk/twistedcaldav/schedule.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/schedule.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/schedule.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -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.dav.util import joinURL, normalizeURL
+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)
@@ -140,6 +137,7 @@
elif property.qname() == (caldav_namespace, "calendar-free-busy-set"):
# Verify that the calendars added in the PROPPATCH are valid. We do not check
# whether existing items in the property are still valid - only new ones.
+ property.children = [davxml.HRef(normalizeURL(str(href))) for href in property.children]
new_calendars = set([str(href) for href in property.children])
if not self.hasDeadProperty(property):
old_calendars = set()
@@ -147,7 +145,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 +153,38 @@
(caldav_namespace, "valid-calendar-url")
))
+ elif property.qname() == (caldav_namespace, "schedule-default-calendar-URL"):
+ # Verify that the calendar added in the PROPPATCH is valid.
+ property.children = [davxml.HRef(normalizeURL(str(href))) for href in property.children]
+ 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):
+ uri = normalizeURL(uri)
+
+ if not self.hasDeadProperty((caldav_namespace, "calendar-free-busy-set")):
+ fbset = set()
+ else:
+ fbset = set([normalizeURL(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 +297,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
+ result = (yield scheduler.doSchedulingViaPOST())
+ returnValue(result.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
+ result = (yield scheduler.doSchedulingViaPOST())
+ returnValue(result.response())
Copied: CalendarServer/trunk/twistedcaldav/scheduling (from rev 2879, CalendarServer/branches/users/cdaboo/implicit-2867/twistedcaldav/scheduling)
Deleted: CalendarServer/trunk/twistedcaldav/scheduling/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2867/twistedcaldav/scheduling/__init__.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/scheduling/__init__.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -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/trunk/twistedcaldav/scheduling/__init__.py (from rev 2879, CalendarServer/branches/users/cdaboo/implicit-2867/twistedcaldav/scheduling/__init__.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/__init__.py (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/__init__.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -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/trunk/twistedcaldav/scheduling/addressmapping.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2867/twistedcaldav/scheduling/addressmapping.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/scheduling/addressmapping.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -1,94 +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)
-
- def isCalendarUserInMyDomain(self, cuaddr):
-
- # Check whether it is a possible local address
- def _gotResult(serviceType):
- return serviceType == DeliveryService.serviceType_caldav
-
- d = self.getCalendarUserServiceType(cuaddr)
- d.addCallback(_gotResult)
- return d
-
-mapper = ScheduleAddressMapper()
Copied: CalendarServer/trunk/twistedcaldav/scheduling/addressmapping.py (from rev 2879, CalendarServer/branches/users/cdaboo/implicit-2867/twistedcaldav/scheduling/addressmapping.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/addressmapping.py (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/addressmapping.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -0,0 +1,94 @@
+##
+# 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)
+
+ def isCalendarUserInMyDomain(self, cuaddr):
+
+ # Check whether it is a possible local address
+ def _gotResult(serviceType):
+ return serviceType == DeliveryService.serviceType_caldav
+
+ d = self.getCalendarUserServiceType(cuaddr)
+ d.addCallback(_gotResult)
+ return d
+
+mapper = ScheduleAddressMapper()
Deleted: CalendarServer/trunk/twistedcaldav/scheduling/caldav.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2867/twistedcaldav/scheduling/caldav.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/scheduling/caldav.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -1,286 +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
-from twistedcaldav.scheduling.processing import ImplicitProcessor,\
- ImplicitProcessorException
-
-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))
-
- # Do implicit scheduling message processing
- try:
- processor = ImplicitProcessor()
- processed, autoprocessed = (yield processor.doImplicitProcessing(
- self.scheduler.request,
- self.scheduler.calendar,
- self.scheduler.originator,
- recipient
- ))
- except ImplicitProcessorException, e:
- 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=e.msg)
- returnValue(False)
-
- if autoprocessed:
- # No need to write the inbox item as it has already been auto-processed
- responses.add(recipient.cuaddr, responsecode.OK, reqstatus="2.0;Success")
- returnValue(True)
- else:
- # Copy calendar to inbox
- 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.ScheduleProcessed() if processed else caldavxml.ScheduleUnprocessed()))
-
- # Look for auto-schedule option
- if not processed and 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/trunk/twistedcaldav/scheduling/caldav.py (from rev 2879, CalendarServer/branches/users/cdaboo/implicit-2867/twistedcaldav/scheduling/caldav.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/caldav.py (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/caldav.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -0,0 +1,286 @@
+##
+# 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
+from twistedcaldav.scheduling.processing import ImplicitProcessor,\
+ ImplicitProcessorException
+
+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))
+
+ # Do implicit scheduling message processing
+ try:
+ processor = ImplicitProcessor()
+ processed, autoprocessed = (yield processor.doImplicitProcessing(
+ self.scheduler.request,
+ self.scheduler.calendar,
+ self.scheduler.originator,
+ recipient
+ ))
+ except ImplicitProcessorException, e:
+ 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=e.msg)
+ returnValue(False)
+
+ if autoprocessed:
+ # No need to write the inbox item as it has already been auto-processed
+ responses.add(recipient.cuaddr, responsecode.OK, reqstatus="2.0;Success")
+ returnValue(True)
+ else:
+ # Copy calendar to inbox
+ 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.ScheduleProcessed() if processed else caldavxml.ScheduleUnprocessed()))
+
+ # Look for auto-schedule option
+ if not processed and 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/trunk/twistedcaldav/scheduling/cuaddress.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2867/twistedcaldav/scheduling/cuaddress.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/scheduling/cuaddress.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -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/trunk/twistedcaldav/scheduling/cuaddress.py (from rev 2879, CalendarServer/branches/users/cdaboo/implicit-2867/twistedcaldav/scheduling/cuaddress.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/cuaddress.py (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/cuaddress.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -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/trunk/twistedcaldav/scheduling/delivery.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2867/twistedcaldav/scheduling/delivery.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/scheduling/delivery.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -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/trunk/twistedcaldav/scheduling/delivery.py (from rev 2879, CalendarServer/branches/users/cdaboo/implicit-2867/twistedcaldav/scheduling/delivery.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/delivery.py (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/delivery.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -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/trunk/twistedcaldav/scheduling/icaldiff.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2867/twistedcaldav/scheduling/icaldiff.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -1,215 +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
-from twistedcaldav.scheduling.itip import iTipGenerator
-
-"""
-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.removeXProperties()
- self.calendar1.attendeesView((attendee,))
- iTipGenerator.prepareSchedulingMessage(self.calendar1)
-
- self.calendar2 = self.calendar2.duplicate()
- self.calendar2.removeXProperties()
- iTipGenerator.prepareSchedulingMessage(self.calendar2)
-
- 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,))
- 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",
- "SEQUENCE",
- ):
- 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/trunk/twistedcaldav/scheduling/icaldiff.py (from rev 2879, CalendarServer/branches/users/cdaboo/implicit-2867/twistedcaldav/scheduling/icaldiff.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -0,0 +1,215 @@
+##
+# 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
+from twistedcaldav.scheduling.itip import iTipGenerator
+
+"""
+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.removeXProperties()
+ self.calendar1.attendeesView((attendee,))
+ iTipGenerator.prepareSchedulingMessage(self.calendar1)
+
+ self.calendar2 = self.calendar2.duplicate()
+ self.calendar2.removeXProperties()
+ iTipGenerator.prepareSchedulingMessage(self.calendar2)
+
+ 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,))
+ 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",
+ "SEQUENCE",
+ ):
+ 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/trunk/twistedcaldav/scheduling/imip.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2867/twistedcaldav/scheduling/imip.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/scheduling/imip.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -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/trunk/twistedcaldav/scheduling/imip.py (from rev 2879, CalendarServer/branches/users/cdaboo/implicit-2867/twistedcaldav/scheduling/imip.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/imip.py (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/imip.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -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/trunk/twistedcaldav/scheduling/implicit.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2867/twistedcaldav/scheduling/implicit.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/scheduling/implicit.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -1,474 +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.scheduling.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
-from twistedcaldav import caldavxml
-
-__all__ = [
- "ImplicitScheduler",
-]
-
-log = Logger()
-
-# TODO:
-#
-# Handle the case where a PUT removes the ORGANIZER property. That should be equivalent to cancelling the entire meeting.
-# Support SCHEDULE-AGENT property
-# Support SCHEDULE-STATUS property
-# Support live calendars
-# Support Schedule-Reply header
-#
-
-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,
- or C{None} if nothing happened
- """
-
- self.request = request
- self.resource = resource
- self.calendar = calendar
- self.calendar_owner = (yield self.resource.owner(self.request))
- self.deleting = deleting
- self.internal_request = False
- self.except_attendees = ()
-
- # 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()
- else:
- returnValue(None)
-
- returnValue(self.calendar)
-
- def refreshAllAttendeesExceptSome(self, request, resource, calendar, attendees):
- """
-
- @param request:
- @type request:
- @param attendee:
- @type attendee:
- @param calendar:
- @type calendar:
- """
-
- self.request = request
- self.resource = resource
- self.calendar = calendar
- self.calendar_owner = None
- self.deleting = False
- self.internal_request = True
- self.except_attendees = attendees
-
- # Get some useful information from the calendar
- self.extractCalendarData()
- self.organizerPrincipal = self.resource.principalForCalendarUserAddress(self.organizer)
-
- return self.processRequests()
-
- 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
-
- 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 = ()
-
- return 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.
- """
-
- # Several possibilities for when CANCELs need to be sent:
- #
- # Remove ATTENDEE property
- # Add EXDATE
- # Remove overridden component
- # Remove RDATE
- # Truncate RRULE
- # Change RRULE
-
- # TODO: the later three will be ignored for now.
-
- oldAttendeesByInstance = self.oldcalendar.getAttendeesByInstance()
-
- mappedOld = set(oldAttendeesByInstance)
- mappedNew = set(self.attendeesByInstance)
-
- # Get missing instances
- oldInstances = set(self.oldcalendar.getComponentInstances())
- newInstances = set(self.calendar.getComponentInstances())
- removedInstances = oldInstances - newInstances
-
- # Also look for new EXDATEs
- oldexdates = set()
- for property in self.oldcalendar.masterComponent().properties("EXDATE"):
- oldexdates.update(property.value())
- newexdates = set()
- for property in self.calendar.masterComponent().properties("EXDATE"):
- newexdates.update(property.value())
-
- addedexdates = newexdates - oldexdates
-
- # Now figure out the attendees that need to be sent CANCELs
- self.cancelledAttendees = set()
-
- for item in mappedOld:
- if item not in mappedNew:
-
- # Several possibilities:
- #
- # 1. removed from master component - always a CANCEL
- # 2. removed from overridden component - always a CANCEL
- # 3. removed overridden component - only CANCEL if not in master or exdate added
-
- new_attendee, rid = item
-
- # 1. & 2.
- if rid is None or rid not in removedInstances:
- self.cancelledAttendees.add(item)
- else:
- # 3.
- if (new_attendee, None) not in mappedNew or rid in addedexdates:
- self.cancelledAttendees.add(item)
-
- master_attendees = self.oldcalendar.masterComponent().getAttendeesByInstance()
- for attendee, _ignore in master_attendees:
- for exdate in addedexdates:
- # Don't remove the master attendee's when an EXDATE is added for a removed overridden component
- # as the set of attendees in the override may be different from the master set, but the override
- # will have been accounted for by the previous attendee/instance logic.
- if exdate not in removedInstances:
- self.cancelledAttendees.add((attendee, exdate))
-
- @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.internal_request))
- 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
-
- # Don't send message to specified attendees
- if attendee in self.except_attendees:
- 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.internal_request))
- self.handleSchedulingResponse(response, True)
-
- def handleSchedulingResponse(self, response, is_organizer):
-
- # Map each recipient in the response to a status code
- responses = {}
- for item in response.responses:
- assert isinstance(item, caldavxml.Response), "Wrong element in response"
- recipient = str(item.children[0].children[0])
- status = str(item.children[1])
- responses[recipient] = status
-
- # Now apply to each ATTENDEE/ORGANIZER in the original data
- self.calendar.setParameterToValueForPropertyWithValue(
- "SCHEDULE-STATUS",
- status,
- "ATTENDEE" if is_organizer else "ORGANIZER",
- recipient)
-
- @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
-
- def scheduleWithOrganizer(self):
-
- itipmsg = iTipGenerator.generateAttendeeReply(self.calendar, self.attendee)
-
- # Send scheduling message
- return self.sendToOrganizer("REPLY", itipmsg)
-
- def scheduleCancelWithOrganizer(self):
-
- itipmsg = iTipGenerator.generateAttendeeReply(self.calendar, self.attendee, True)
-
- # Send scheduling message
- return self.sendToOrganizer("CANCEL", itipmsg)
-
- 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
- def _gotResponse(response):
- self.handleSchedulingResponse(response, False)
-
- log.info("Implicit %s - attendee: '%s' to organizer: '%s', UID: '%s'" % (action, self.attendee, self.organizer, self.uid,))
- d = scheduler.doSchedulingViaPUT(self.attendee, (self.organizer,), itipmsg, self.internal_request)
- d.addCallback(_gotResponse)
- return d
Copied: CalendarServer/trunk/twistedcaldav/scheduling/implicit.py (from rev 2879, CalendarServer/branches/users/cdaboo/implicit-2867/twistedcaldav/scheduling/implicit.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/implicit.py (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/implicit.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -0,0 +1,474 @@
+#
+# 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.scheduling.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
+from twistedcaldav import caldavxml
+
+__all__ = [
+ "ImplicitScheduler",
+]
+
+log = Logger()
+
+# TODO:
+#
+# Handle the case where a PUT removes the ORGANIZER property. That should be equivalent to cancelling the entire meeting.
+# Support SCHEDULE-AGENT property
+# Support SCHEDULE-STATUS property
+# Support live calendars
+# Support Schedule-Reply header
+#
+
+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,
+ or C{None} if nothing happened
+ """
+
+ self.request = request
+ self.resource = resource
+ self.calendar = calendar
+ self.calendar_owner = (yield self.resource.owner(self.request))
+ self.deleting = deleting
+ self.internal_request = False
+ self.except_attendees = ()
+
+ # 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()
+ else:
+ returnValue(None)
+
+ returnValue(self.calendar)
+
+ def refreshAllAttendeesExceptSome(self, request, resource, calendar, attendees):
+ """
+
+ @param request:
+ @type request:
+ @param attendee:
+ @type attendee:
+ @param calendar:
+ @type calendar:
+ """
+
+ self.request = request
+ self.resource = resource
+ self.calendar = calendar
+ self.calendar_owner = None
+ self.deleting = False
+ self.internal_request = True
+ self.except_attendees = attendees
+
+ # Get some useful information from the calendar
+ self.extractCalendarData()
+ self.organizerPrincipal = self.resource.principalForCalendarUserAddress(self.organizer)
+
+ return self.processRequests()
+
+ 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
+
+ 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 = ()
+
+ return 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.
+ """
+
+ # Several possibilities for when CANCELs need to be sent:
+ #
+ # Remove ATTENDEE property
+ # Add EXDATE
+ # Remove overridden component
+ # Remove RDATE
+ # Truncate RRULE
+ # Change RRULE
+
+ # TODO: the later three will be ignored for now.
+
+ oldAttendeesByInstance = self.oldcalendar.getAttendeesByInstance()
+
+ mappedOld = set(oldAttendeesByInstance)
+ mappedNew = set(self.attendeesByInstance)
+
+ # Get missing instances
+ oldInstances = set(self.oldcalendar.getComponentInstances())
+ newInstances = set(self.calendar.getComponentInstances())
+ removedInstances = oldInstances - newInstances
+
+ # Also look for new EXDATEs
+ oldexdates = set()
+ for property in self.oldcalendar.masterComponent().properties("EXDATE"):
+ oldexdates.update(property.value())
+ newexdates = set()
+ for property in self.calendar.masterComponent().properties("EXDATE"):
+ newexdates.update(property.value())
+
+ addedexdates = newexdates - oldexdates
+
+ # Now figure out the attendees that need to be sent CANCELs
+ self.cancelledAttendees = set()
+
+ for item in mappedOld:
+ if item not in mappedNew:
+
+ # Several possibilities:
+ #
+ # 1. removed from master component - always a CANCEL
+ # 2. removed from overridden component - always a CANCEL
+ # 3. removed overridden component - only CANCEL if not in master or exdate added
+
+ new_attendee, rid = item
+
+ # 1. & 2.
+ if rid is None or rid not in removedInstances:
+ self.cancelledAttendees.add(item)
+ else:
+ # 3.
+ if (new_attendee, None) not in mappedNew or rid in addedexdates:
+ self.cancelledAttendees.add(item)
+
+ master_attendees = self.oldcalendar.masterComponent().getAttendeesByInstance()
+ for attendee, _ignore in master_attendees:
+ for exdate in addedexdates:
+ # Don't remove the master attendee's when an EXDATE is added for a removed overridden component
+ # as the set of attendees in the override may be different from the master set, but the override
+ # will have been accounted for by the previous attendee/instance logic.
+ if exdate not in removedInstances:
+ self.cancelledAttendees.add((attendee, exdate))
+
+ @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.internal_request))
+ 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
+
+ # Don't send message to specified attendees
+ if attendee in self.except_attendees:
+ 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.internal_request))
+ self.handleSchedulingResponse(response, True)
+
+ def handleSchedulingResponse(self, response, is_organizer):
+
+ # Map each recipient in the response to a status code
+ responses = {}
+ for item in response.responses:
+ assert isinstance(item, caldavxml.Response), "Wrong element in response"
+ recipient = str(item.children[0].children[0])
+ status = str(item.children[1])
+ responses[recipient] = status
+
+ # Now apply to each ATTENDEE/ORGANIZER in the original data
+ self.calendar.setParameterToValueForPropertyWithValue(
+ "SCHEDULE-STATUS",
+ status,
+ "ATTENDEE" if is_organizer else "ORGANIZER",
+ recipient)
+
+ @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
+
+ def scheduleWithOrganizer(self):
+
+ itipmsg = iTipGenerator.generateAttendeeReply(self.calendar, self.attendee)
+
+ # Send scheduling message
+ return self.sendToOrganizer("REPLY", itipmsg)
+
+ def scheduleCancelWithOrganizer(self):
+
+ itipmsg = iTipGenerator.generateAttendeeReply(self.calendar, self.attendee, True)
+
+ # Send scheduling message
+ return self.sendToOrganizer("CANCEL", itipmsg)
+
+ 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
+ def _gotResponse(response):
+ self.handleSchedulingResponse(response, False)
+
+ log.info("Implicit %s - attendee: '%s' to organizer: '%s', UID: '%s'" % (action, self.attendee, self.organizer, self.uid,))
+ d = scheduler.doSchedulingViaPUT(self.attendee, (self.organizer,), itipmsg, self.internal_request)
+ d.addCallback(_gotResponse)
+ return d
Deleted: CalendarServer/trunk/twistedcaldav/scheduling/ischedule.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2867/twistedcaldav/scheduling/ischedule.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -1,183 +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)
-
- 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())
-
- return 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/trunk/twistedcaldav/scheduling/ischedule.py (from rev 2879, CalendarServer/branches/users/cdaboo/implicit-2867/twistedcaldav/scheduling/ischedule.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischedule.py (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischedule.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -0,0 +1,183 @@
+##
+# 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)
+
+ 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())
+
+ return 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/trunk/twistedcaldav/scheduling/ischeduleservers.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2867/twistedcaldav/scheduling/ischeduleservers.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischeduleservers.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -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/trunk/twistedcaldav/scheduling/ischeduleservers.py (from rev 2879, CalendarServer/branches/users/cdaboo/implicit-2867/twistedcaldav/scheduling/ischeduleservers.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/ischeduleservers.py (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/ischeduleservers.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -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/trunk/twistedcaldav/scheduling/itip.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2867/twistedcaldav/scheduling/itip.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/scheduling/itip.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -1,448 +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.
-##
-
-"""
-iTIP (RFC2446) processing.
-"""
-
-#
-# This is currently used for handling auto-replies to schedule requests arriving
-# in an inbox. It is called in a delayed fashion via reactor.callLater.
-#
-# We assume that all the components/calendars we deal with have been determined
-# as being 'valid for CalDAV/iTIP', i.e. they contain UIDs, single component
-# types, etc.
-#
-# The logic for component matching needs a lot more work as it currently does not
-# know how to deal with overridden instances.
-#
-
-import datetime
-
-from twistedcaldav.log import Logger
-from twistedcaldav.ical import Property, iCalendarProductID, Component
-
-from vobject.icalendar import utc
-
-log = Logger()
-
-__version__ = "0.0"
-
-__all__ = [
- "iTipProcessor",
- "iTipGenerator",
-]
-
-class iTipProcessing(object):
-
- @staticmethod
- def processNewRequest(itip_message):
- """
- Process a METHOD=REQUEST for a brand new calendar object.
-
- @param itip_message: the iTIP message calendar object to process.
- @type itip_message:
-
- @return: calendar object ready to save
- """
- assert itip_message.propertyValue("METHOD") == "REQUEST", "iTIP message must have METHOD:REQUEST"
-
- calendar = itip_message.duplicate()
- method = calendar.getProperty("METHOD")
- if method:
- calendar.removeProperty(method)
-
- return calendar
-
- @staticmethod
- def processRequest(itip_message, calendar):
- """
- Process a METHOD=REQUEST.
-
- @param itip_message: the iTIP message calendar object to process.
- @type itip_message:
- @param calendar: the calendar object to apply the REQUEST to
- @type calendar:
-
- @return: calendar object ready to save, or C{None} (request should be ignored)
- """
-
- # Merge Organizer data with Attendee's own changes (VALARMs only for now).
-
- # Different behavior depending on whether a master component is present or not
- current_master = calendar.masterComponent()
- if current_master:
- master_valarms = [comp for comp in current_master.subcomponents() if comp.name() == "VALARM"]
- else:
- master_valarms = ()
-
- if itip_message.masterComponent() is not None:
-
- # Get a new calendar object first
- new_calendar = iTipProcessing.processNewRequest(itip_message)
-
- # Copy over master alarms
- master_component = new_calendar.masterComponent()
- for alarm in master_valarms:
- master_component.addComponent(alarm)
-
- # Now try to match recurrences
- for component in new_calendar.subcomponents():
- if component.name() != "VTIMEZONE" and component.getRecurrenceIDUTC() is not None:
- iTipProcessing.transferAlarms(calendar, master_valarms, component)
-
- # Replace the entire object
- return new_calendar
-
- else:
- # Need existing tzids
- tzids = calendar.timezones()
-
- # Update existing instances
- for component in itip_message.subcomponents():
- if component.name() == "VTIMEZONE":
- # May need to add a new VTIMEZONE
- if component.propertyValue("TZID") not in tzids:
- calendar.addComponent(component)
- else:
- iTipProcessing.transferAlarms(calendar, master_valarms, component, remove_matched=True)
- calendar.addComponent(component)
-
- # Write back the modified object
- return calendar
-
- @staticmethod
- def processCancel(itip_message, calendar):
- """
- Process a METHOD=CANCEL.
-
- TODO: Yes, I am going to ignore RANGE= on RECURRENCE-ID for now...
-
- @param itip_message: the iTIP message calendar object to process.
- @type itip_message:
- @param calendar: the calendar object to apply the CANCEL to
- @type calendar:
-
- @return: C{tuple} of:
- C{bool} : C{True} if processed, C{False} if scheduling message should be ignored
- C{bool} : C{True} if calendar object should be deleted, C{False} otherwise
- """
-
- assert itip_message.propertyValue("METHOD") == "CANCEL", "iTIP message must have METHOD:CANCEL"
- assert itip_message.resourceUID() == calendar.resourceUID(), "UIDs must be the same to process iTIP message"
-
- # Check to see if this is a cancel of the entire event
- if itip_message.masterComponent() is not None:
- return True, True
-
- # 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 master object if it exists
- calendar_master = calendar.masterComponent()
- exdates = []
-
- # Look at each component in the iTIP message
- for component in itip_message.subcomponents():
- if component.name() == "VTIMEZONE":
- continue
-
- # Extract RECURRENCE-ID value from component
- rid = component.getRecurrenceIDUTC()
-
- # Get the one that matches in the calendar
- overridden = calendar.overriddenComponent(rid)
-
- if overridden:
- # We are cancelling an overridden component.
-
- # Exclude the cancelled instance
- exdates.append(component.getRecurrenceIDUTC())
-
- # Remove the existing component.
- calendar.removeComponent(overridden)
- elif calendar_master:
- # We are trying to CANCEL a non-overridden instance.
-
- # Exclude the cancelled instance
- exdates.append(component.getRecurrenceIDUTC())
-
- # If we have any EXDATEs lets add them to the existing calendar object.
- if exdates and calendar_master:
- calendar_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 calendar.mainType() is None:
- # Delete the now empty calendar object
- return True, True
- else:
- return True, False
-
- @staticmethod
- def processReply(itip_message, calendar):
- """
- Process a METHOD=REPLY.
-
- TODO: Yes, I am going to ignore RANGE= on RECURRENCE-ID for now...
-
- @param itip_message: the iTIP message calendar object to process.
- @type itip_message:
- @param calendar: the calendar object to apply the REPLY to
- @type calendar:
-
- @return: C{True} if processed, C{False} if scheduling message should be ignored
- """
-
- assert itip_message.propertyValue("METHOD") == "REPLY", "iTIP message must have METHOD:REPLY"
- assert itip_message.resourceUID() == calendar.resourceUID(), "UIDs must be the same to process iTIP message"
-
- # Take each component in the reply and update the corresponding component
- # in the organizer's copy (possibly generating new ones) so that the ATTENDEE
- # PARTSTATs match up.
-
- # Do the master first
- old_master = calendar.masterComponent()
- new_master = itip_message.masterComponent()
- attendees = set()
- if new_master:
- attendees.add(iTipProcessing.updateAttendeePartStat(new_master, old_master))
-
- # Now do all overridden ones
- for itip_component in itip_message.subcomponents():
-
- # Make sure we have an appropriate component
- if itip_component.name() == "VTIMEZONE":
- continue
- rid = itip_component.getRecurrenceIDUTC()
- if rid is None:
- continue
-
- # Find matching component in organizer's copy
- match_component = calendar.overriddenComponent(rid)
- if match_component is None:
- # Attendee is overriding an instance themselves - we need to create a derived one
- # for the Organizer
- match_component = calendar.deriveInstance(rid)
- calendar.addComponent(match_component)
-
- attendees.add(iTipProcessing.updateAttendeePartStat(itip_component, match_component))
-
- return True, attendees
-
- @staticmethod
- def updateAttendeePartStat(from_component, to_component):
- """
- Copy the PARTSTAT of the Attendee in the from_component to the matching ATTENDEE
- in the to_component. Ignore if no match found.
-
- @param from_component:
- @type from_component:
- @param to_component:
- @type to_component:
- """
-
- # Get attendee in from_component - there MUST be only one
- attendees = tuple(from_component.properties("ATTENDEE"))
- assert len(attendees) == 1, "There must be one and only one ATTENDEE property in a REPLY"
- attendee = attendees[0]
- partstat = attendee.params().get("PARTSTAT", ("NEEDS-ACTION",))[0]
-
- # Now find matching ATTENDEE in to_component
- existing_attendee = to_component.getAttendeeProperty((attendee.value(),))
- if existing_attendee:
- existing_attendee.params().setdefault("PARTSTAT", [partstat])[0] = partstat
-
- return attendee.value()
-
- @staticmethod
- def transferAlarms(from_calendar, master_valarms, to_component, remove_matched=False):
-
- rid = to_component.getRecurrenceIDUTC()
-
- # Is there a matching component
- matched = from_calendar.overriddenComponent(rid)
- if matched:
- # Copy over VALARMs from existing component
- [to_component.addComponent(comp) for comp in matched.subcomponents() if comp.name() == "VALARM"]
-
- # Remove the old one
- if remove_matched:
- from_calendar.removeComponent(matched)
-
- else:
- # It is a new override - copy any valarms on the existing master component
- # into the new one.
- for alarm in master_valarms:
- # Just copy in the new override
- to_component.addComponent(alarm)
-
-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,)
-
- tzids = set()
- 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)
-
- tzids.update(comp.timezoneIDs())
-
- # Now include any referenced tzids
- for comp in original.subcomponents():
- if comp.name() == "VTIMEZONE":
- tzid = comp.propertyValue("TZID")
- if tzid in tzids:
- itip.addComponent(comp)
-
- 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)
-
- # Strip out unwanted bits
- iTipGenerator.prepareSchedulingMessage(itip)
-
- 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",
- "X-CALENDARSERVER-PRIVATE-COMMENT",
- ))
-
- # 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"]
-
- # Add REQUEST-STATUS to each top-level component
- itip.addPropertyToAllComponents(Property("REQUEST-STATUS", "2.0;Success"))
- return itip
-
- @staticmethod
- def prepareSchedulingMessage(itip):
- """
- Remove properties and parameters that should not be sent in an iTIP message
- """
-
- # Component properties
- def stripSubComponents(component, strip):
-
- for subcomponent in tuple(component.subcomponents()):
- if subcomponent.name() in strip:
- component.removeComponent(subcomponent)
-
- # Component properties
- def stripComponentProperties(component, properties):
-
- for property in tuple(component.properties()):
- if property.name() in properties:
- component.removeProperty(property)
-
- # Property parameters
- def stripPropertyParameters(properties, parameters):
-
- for property in properties:
- for parameter in parameters:
- try:
- del property.params()[parameter]
- except KeyError:
- pass
-
- # Top-level properties
- stripComponentProperties(itip, ("X-CALENDARSERVER-ACCESS",))
-
- # Component properties
- for component in itip.subcomponents():
- stripSubComponents(component, ("VALARM",))
- stripComponentProperties(component, (
- "X-CALENDARSERVER-PRIVATE-COMMENT",
- "X-CALENDARSERVER-ATTENDEE-COMMENT",
- ))
- stripPropertyParameters(component.properties("ATTENDEE"), (
- "SCHEDULE-AGENT",
- "SCHEDULE-STATUS",
- ))
- stripPropertyParameters(component.properties("ORGANIZER"), (
- "SCHEDULE-STATUS",
- ))
Copied: CalendarServer/trunk/twistedcaldav/scheduling/itip.py (from rev 2879, CalendarServer/branches/users/cdaboo/implicit-2867/twistedcaldav/scheduling/itip.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/itip.py (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/itip.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -0,0 +1,448 @@
+##
+# 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.
+##
+
+"""
+iTIP (RFC2446) processing.
+"""
+
+#
+# This is currently used for handling auto-replies to schedule requests arriving
+# in an inbox. It is called in a delayed fashion via reactor.callLater.
+#
+# We assume that all the components/calendars we deal with have been determined
+# as being 'valid for CalDAV/iTIP', i.e. they contain UIDs, single component
+# types, etc.
+#
+# The logic for component matching needs a lot more work as it currently does not
+# know how to deal with overridden instances.
+#
+
+import datetime
+
+from twistedcaldav.log import Logger
+from twistedcaldav.ical import Property, iCalendarProductID, Component
+
+from vobject.icalendar import utc
+
+log = Logger()
+
+__version__ = "0.0"
+
+__all__ = [
+ "iTipProcessor",
+ "iTipGenerator",
+]
+
+class iTipProcessing(object):
+
+ @staticmethod
+ def processNewRequest(itip_message):
+ """
+ Process a METHOD=REQUEST for a brand new calendar object.
+
+ @param itip_message: the iTIP message calendar object to process.
+ @type itip_message:
+
+ @return: calendar object ready to save
+ """
+ assert itip_message.propertyValue("METHOD") == "REQUEST", "iTIP message must have METHOD:REQUEST"
+
+ calendar = itip_message.duplicate()
+ method = calendar.getProperty("METHOD")
+ if method:
+ calendar.removeProperty(method)
+
+ return calendar
+
+ @staticmethod
+ def processRequest(itip_message, calendar):
+ """
+ Process a METHOD=REQUEST.
+
+ @param itip_message: the iTIP message calendar object to process.
+ @type itip_message:
+ @param calendar: the calendar object to apply the REQUEST to
+ @type calendar:
+
+ @return: calendar object ready to save, or C{None} (request should be ignored)
+ """
+
+ # Merge Organizer data with Attendee's own changes (VALARMs only for now).
+
+ # Different behavior depending on whether a master component is present or not
+ current_master = calendar.masterComponent()
+ if current_master:
+ master_valarms = [comp for comp in current_master.subcomponents() if comp.name() == "VALARM"]
+ else:
+ master_valarms = ()
+
+ if itip_message.masterComponent() is not None:
+
+ # Get a new calendar object first
+ new_calendar = iTipProcessing.processNewRequest(itip_message)
+
+ # Copy over master alarms
+ master_component = new_calendar.masterComponent()
+ for alarm in master_valarms:
+ master_component.addComponent(alarm)
+
+ # Now try to match recurrences
+ for component in new_calendar.subcomponents():
+ if component.name() != "VTIMEZONE" and component.getRecurrenceIDUTC() is not None:
+ iTipProcessing.transferAlarms(calendar, master_valarms, component)
+
+ # Replace the entire object
+ return new_calendar
+
+ else:
+ # Need existing tzids
+ tzids = calendar.timezones()
+
+ # Update existing instances
+ for component in itip_message.subcomponents():
+ if component.name() == "VTIMEZONE":
+ # May need to add a new VTIMEZONE
+ if component.propertyValue("TZID") not in tzids:
+ calendar.addComponent(component)
+ else:
+ iTipProcessing.transferAlarms(calendar, master_valarms, component, remove_matched=True)
+ calendar.addComponent(component)
+
+ # Write back the modified object
+ return calendar
+
+ @staticmethod
+ def processCancel(itip_message, calendar):
+ """
+ Process a METHOD=CANCEL.
+
+ TODO: Yes, I am going to ignore RANGE= on RECURRENCE-ID for now...
+
+ @param itip_message: the iTIP message calendar object to process.
+ @type itip_message:
+ @param calendar: the calendar object to apply the CANCEL to
+ @type calendar:
+
+ @return: C{tuple} of:
+ C{bool} : C{True} if processed, C{False} if scheduling message should be ignored
+ C{bool} : C{True} if calendar object should be deleted, C{False} otherwise
+ """
+
+ assert itip_message.propertyValue("METHOD") == "CANCEL", "iTIP message must have METHOD:CANCEL"
+ assert itip_message.resourceUID() == calendar.resourceUID(), "UIDs must be the same to process iTIP message"
+
+ # Check to see if this is a cancel of the entire event
+ if itip_message.masterComponent() is not None:
+ return True, True
+
+ # 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 master object if it exists
+ calendar_master = calendar.masterComponent()
+ exdates = []
+
+ # Look at each component in the iTIP message
+ for component in itip_message.subcomponents():
+ if component.name() == "VTIMEZONE":
+ continue
+
+ # Extract RECURRENCE-ID value from component
+ rid = component.getRecurrenceIDUTC()
+
+ # Get the one that matches in the calendar
+ overridden = calendar.overriddenComponent(rid)
+
+ if overridden:
+ # We are cancelling an overridden component.
+
+ # Exclude the cancelled instance
+ exdates.append(component.getRecurrenceIDUTC())
+
+ # Remove the existing component.
+ calendar.removeComponent(overridden)
+ elif calendar_master:
+ # We are trying to CANCEL a non-overridden instance.
+
+ # Exclude the cancelled instance
+ exdates.append(component.getRecurrenceIDUTC())
+
+ # If we have any EXDATEs lets add them to the existing calendar object.
+ if exdates and calendar_master:
+ calendar_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 calendar.mainType() is None:
+ # Delete the now empty calendar object
+ return True, True
+ else:
+ return True, False
+
+ @staticmethod
+ def processReply(itip_message, calendar):
+ """
+ Process a METHOD=REPLY.
+
+ TODO: Yes, I am going to ignore RANGE= on RECURRENCE-ID for now...
+
+ @param itip_message: the iTIP message calendar object to process.
+ @type itip_message:
+ @param calendar: the calendar object to apply the REPLY to
+ @type calendar:
+
+ @return: C{True} if processed, C{False} if scheduling message should be ignored
+ """
+
+ assert itip_message.propertyValue("METHOD") == "REPLY", "iTIP message must have METHOD:REPLY"
+ assert itip_message.resourceUID() == calendar.resourceUID(), "UIDs must be the same to process iTIP message"
+
+ # Take each component in the reply and update the corresponding component
+ # in the organizer's copy (possibly generating new ones) so that the ATTENDEE
+ # PARTSTATs match up.
+
+ # Do the master first
+ old_master = calendar.masterComponent()
+ new_master = itip_message.masterComponent()
+ attendees = set()
+ if new_master:
+ attendees.add(iTipProcessing.updateAttendeePartStat(new_master, old_master))
+
+ # Now do all overridden ones
+ for itip_component in itip_message.subcomponents():
+
+ # Make sure we have an appropriate component
+ if itip_component.name() == "VTIMEZONE":
+ continue
+ rid = itip_component.getRecurrenceIDUTC()
+ if rid is None:
+ continue
+
+ # Find matching component in organizer's copy
+ match_component = calendar.overriddenComponent(rid)
+ if match_component is None:
+ # Attendee is overriding an instance themselves - we need to create a derived one
+ # for the Organizer
+ match_component = calendar.deriveInstance(rid)
+ calendar.addComponent(match_component)
+
+ attendees.add(iTipProcessing.updateAttendeePartStat(itip_component, match_component))
+
+ return True, attendees
+
+ @staticmethod
+ def updateAttendeePartStat(from_component, to_component):
+ """
+ Copy the PARTSTAT of the Attendee in the from_component to the matching ATTENDEE
+ in the to_component. Ignore if no match found.
+
+ @param from_component:
+ @type from_component:
+ @param to_component:
+ @type to_component:
+ """
+
+ # Get attendee in from_component - there MUST be only one
+ attendees = tuple(from_component.properties("ATTENDEE"))
+ assert len(attendees) == 1, "There must be one and only one ATTENDEE property in a REPLY"
+ attendee = attendees[0]
+ partstat = attendee.params().get("PARTSTAT", ("NEEDS-ACTION",))[0]
+
+ # Now find matching ATTENDEE in to_component
+ existing_attendee = to_component.getAttendeeProperty((attendee.value(),))
+ if existing_attendee:
+ existing_attendee.params().setdefault("PARTSTAT", [partstat])[0] = partstat
+
+ return attendee.value()
+
+ @staticmethod
+ def transferAlarms(from_calendar, master_valarms, to_component, remove_matched=False):
+
+ rid = to_component.getRecurrenceIDUTC()
+
+ # Is there a matching component
+ matched = from_calendar.overriddenComponent(rid)
+ if matched:
+ # Copy over VALARMs from existing component
+ [to_component.addComponent(comp) for comp in matched.subcomponents() if comp.name() == "VALARM"]
+
+ # Remove the old one
+ if remove_matched:
+ from_calendar.removeComponent(matched)
+
+ else:
+ # It is a new override - copy any valarms on the existing master component
+ # into the new one.
+ for alarm in master_valarms:
+ # Just copy in the new override
+ to_component.addComponent(alarm)
+
+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,)
+
+ tzids = set()
+ 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)
+
+ tzids.update(comp.timezoneIDs())
+
+ # Now include any referenced tzids
+ for comp in original.subcomponents():
+ if comp.name() == "VTIMEZONE":
+ tzid = comp.propertyValue("TZID")
+ if tzid in tzids:
+ itip.addComponent(comp)
+
+ 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)
+
+ # Strip out unwanted bits
+ iTipGenerator.prepareSchedulingMessage(itip)
+
+ 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",
+ "X-CALENDARSERVER-PRIVATE-COMMENT",
+ ))
+
+ # 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"]
+
+ # Add REQUEST-STATUS to each top-level component
+ itip.addPropertyToAllComponents(Property("REQUEST-STATUS", "2.0;Success"))
+ return itip
+
+ @staticmethod
+ def prepareSchedulingMessage(itip):
+ """
+ Remove properties and parameters that should not be sent in an iTIP message
+ """
+
+ # Component properties
+ def stripSubComponents(component, strip):
+
+ for subcomponent in tuple(component.subcomponents()):
+ if subcomponent.name() in strip:
+ component.removeComponent(subcomponent)
+
+ # Component properties
+ def stripComponentProperties(component, properties):
+
+ for property in tuple(component.properties()):
+ if property.name() in properties:
+ component.removeProperty(property)
+
+ # Property parameters
+ def stripPropertyParameters(properties, parameters):
+
+ for property in properties:
+ for parameter in parameters:
+ try:
+ del property.params()[parameter]
+ except KeyError:
+ pass
+
+ # Top-level properties
+ stripComponentProperties(itip, ("X-CALENDARSERVER-ACCESS",))
+
+ # Component properties
+ for component in itip.subcomponents():
+ stripSubComponents(component, ("VALARM",))
+ stripComponentProperties(component, (
+ "X-CALENDARSERVER-PRIVATE-COMMENT",
+ "X-CALENDARSERVER-ATTENDEE-COMMENT",
+ ))
+ stripPropertyParameters(component.properties("ATTENDEE"), (
+ "SCHEDULE-AGENT",
+ "SCHEDULE-STATUS",
+ ))
+ stripPropertyParameters(component.properties("ORGANIZER"), (
+ "SCHEDULE-STATUS",
+ ))
Deleted: CalendarServer/trunk/twistedcaldav/scheduling/processing.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2867/twistedcaldav/scheduling/processing.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/scheduling/processing.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -1,332 +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 twistedcaldav.log import Logger
-from twistedcaldav.method import report_common
-from twisted.web2.dav.fileop import delete
-from twistedcaldav.scheduling.itip import iTipProcessing
-from hashlib import md5
-from twisted.web2.dav.util import joinURL
-from twistedcaldav.caldavxml import caldav_namespace
-import time
-
-__all__ = [
- "ImplicitProcessor",
-]
-
-log = Logger()
-
-class ImplicitProcessorException(Exception):
-
- def __init__(self, msg):
- self.msg = msg
-
-class ImplicitProcessor(object):
-
- def __init__(self):
- pass
-
- @inlineCallbacks
- def doImplicitProcessing(self, request, message, originator, recipient):
- """
- Do implicit processing of a scheduling message, and possibly also auto-process it
- if the recipient has auto-accept on.
-
- @param message:
- @type message:
- @param originator:
- @type originator:
- @param recipient:
- @type recipient:
-
- @return: a C{tuple} of (C{bool}, C{bool}) indicating whether the message was processed, and if it was whether
- auto-processing has taken place.
- """
-
- self.request = request
- self.message = message
- self.originator = originator
- self.recipient = recipient
-
- # TODO: for now going to assume that the originator is local - i.e. the scheduling message sent
- # represents the actual organizer's view.
-
- # First see whether this is the organizer or attendee sending the message
- self.extractCalendarData()
-
- if self.isOrganizerReceivingMessage():
- result = (yield self.doImplicitOrganizer())
- elif self.isAttendeeReceivingMessage():
- result = (yield self.doImplicitAttendee())
- else:
- log.error("METHOD:%s not supported for implicit scheduling." % (self.method,))
- raise ImplicitProcessorException("3.14;Unsupported capability")
-
- returnValue(result)
-
- def extractCalendarData(self):
-
- # Some other useful things
- self.method = self.message.propertyValue("METHOD")
- self.uid = self.message.resourceUID()
-
- def isOrganizerReceivingMessage(self):
- return self.method in ("REPLY", "REFRESH")
-
- def isAttendeeReceivingMessage(self):
- return self.method in ("REQUEST", "ADD", "CANCEL")
-
- @inlineCallbacks
- def getRecipientsCopy(self):
- """
- Get the Recipient's copy of the event being processed.
- """
-
- self.recipient_calendar = None
- self.recipient_calendar_collection = None
- self.recipient_calendar_name = None
- if self.recipient.principal:
- # Get Recipient's calendar-home
- calendar_home = self.recipient.principal.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.recipient_calendar = collection.iCalendar(rname)
- self.recipient_calendar_name = rname
- self.recipient_calendar_collection = collection
- self.recipient_calendar_collection_uri = uri
- 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)
-
- @inlineCallbacks
- def doImplicitOrganizer(self):
-
- # Locate the organizer's copy of the event.
- yield self.getRecipientsCopy()
- if self.recipient_calendar is None:
- log.debug("ImplicitProcessing - originator '%s' to recipient '%s' ignoring UID: '%s' - organizer has no copy" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
- returnValue((True, True,))
-
- # Handle new items differently than existing ones.
- if self.method == "REPLY":
- result = (yield self.doImplicitOrganizerUpdate())
- elif self.method == "REFRESH":
- # With implicit we ignore refreshes.
- # TODO: for iMIP etc we do need to handle them
- result = (True, True,)
-
- returnValue(result)
-
- @inlineCallbacks
- def doImplicitOrganizerUpdate(self):
-
- # Check to see if this is a valid reply
- result, processed_attendees = iTipProcessing.processReply(self.message, self.recipient_calendar)
- if result:
-
- # Update the attendee's copy of the event
- log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:REPLY, UID: '%s' - updating event" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
- recipient_calendar_resource = (yield self.writeCalendarResource(self.recipient_calendar_collection_uri, self.recipient_calendar_collection, self.recipient_calendar_name, self.recipient_calendar))
- result = (True, False,)
-
- self.updateAllAttendeesExceptSome(recipient_calendar_resource, processed_attendees)
-
- else:
- # Ignore scheduling message
- result = (True, True,)
-
- returnValue(result)
-
- def updateAllAttendeesExceptSome(self, resource, attendees):
- """
- Send an update out to all attendees except the specified ones, to refresh the others due to a change
- by that one.
-
- @param attendee: cu-addresses of attendees not to send to
- @type attendee: C{set}
- """
-
- from twistedcaldav.scheduling.implicit import ImplicitScheduler
- scheduler = ImplicitScheduler()
- scheduler.refreshAllAttendeesExceptSome(self.request, resource, self.recipient_calendar, attendees)
-
- @inlineCallbacks
- def doImplicitAttendee(self):
-
- # Locate the attendee's copy of the event if it exists.
- yield self.getRecipientsCopy()
- self.new_resource = self.recipient_calendar is None
-
- # Handle new items differently than existing ones.
- if self.new_resource and self.method == "CANCEL":
- result = (True, True,)
- else:
- result = (yield self.doImplicitAttendeeUpdate())
-
- returnValue(result)
-
- @inlineCallbacks
- def doImplicitAttendeeUpdate(self):
-
- # Different based on method
- if self.method == "REQUEST":
- result = (yield self.doImplicitAttendeRequest())
- elif self.method == "CANCEL":
- result = (yield self.doImplicitAttendeCancel())
- elif self.method == "ADD":
- # TODO: implement ADD
- result = (False, False,)
-
- returnValue(result)
-
- @inlineCallbacks
- def doImplicitAttendeRequest(self):
-
- # If there is no existing copy, then look for default calendar and copy it here
- if self.new_resource:
-
- # Check for default calendar
- default = (yield self.recipient.inbox.readProperty((caldav_namespace, "schedule-default-calendar-URL"), self.request))
- if len(default.children) == 1:
- defaultURL = str(default.children[0])
- default = (yield self.request.locateResource(defaultURL))
- else:
- default = None
-
- if default:
- log.debug("ImplicitProcessing - originator '%s' to recipient '%s' ignoring METHOD:REQUEST, UID: '%s' - new processed" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
- new_calendar = iTipProcessing.processNewRequest(self.message)
- name = md5(str(new_calendar) + str(time.time()) + default.fp.path).hexdigest() + ".ics"
- yield self.writeCalendarResource(defaultURL, default, name, new_calendar)
- result = (True, False,)
- else:
- log.debug("ImplicitProcessing - originator '%s' to recipient '%s' ignoring METHOD:REQUEST, UID: '%s' - new not processed" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
- result = (False, False,)
- else:
- # Processing update to existing event
- new_calendar = iTipProcessing.processRequest(self.message, self.recipient_calendar)
- if new_calendar:
-
- # Update the attendee's copy of the event
- log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:REQUEST, UID: '%s' - updating event" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
- yield self.writeCalendarResource(self.recipient_calendar_collection_uri, self.recipient_calendar_collection, self.recipient_calendar_name, new_calendar)
- result = (True, False,)
-
- else:
- # Request needs to be ignored
- log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:REQUEST, UID: '%s' - ignoring" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
- result = (True, True,)
-
- returnValue(result)
-
-
- @inlineCallbacks
- def doImplicitAttendeCancel(self):
-
- # If there is no existing copy, then ignore
- if self.recipient_calendar is None:
- log.debug("ImplicitProcessing - originator '%s' to recipient '%s' ignoring METHOD:CANCEL, UID: '%s' - attendee has no copy" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
- result = (True, True,)
- else:
- # Check to see if this is a cancel of the entire event
- processed_message, delete_original = iTipProcessing.processCancel(self.message, self.recipient_calendar)
- if processed_message:
- if delete_original:
-
- # Delete the attendee's copy of the event
- log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:CANCEL, UID: '%s' - deleting entire event" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
- yield self.deleteCalendarResource(self.recipient_calendar_collection, self.recipient_calendar_name)
- result = (True, False,)
-
- else:
-
- # Update the attendee's copy of the event
- log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:CANCEL, UID: '%s' - updating event" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
- yield self.writeCalendarResource(self.recipient_calendar_collection_uri, self.recipient_calendar_collection, self.recipient_calendar_name, self.recipient_calendar)
- result = (True, False,)
- else:
- log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:CANCEL, UID: '%s' - ignoring" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
- result = (True, True,)
-
- returnValue(result)
-
- @inlineCallbacks
- def writeCalendarResource(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: L{Deferred} -> L{CalDAVFile}
- """
-
- # Create a new name if one was not provided
- if name is None:
- name = md5(str(calendar) + str(time.time()) + collection.fp.path).hexdigest() + ".ics"
-
- # Get a resource for the new item
- newchildURL = joinURL(collURL, name)
- newchild = yield self.request.locateResource(newchildURL)
-
- # 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 = False,
- allowImplicitSchedule = False
- ).run()
-
- returnValue(newchild)
-
- @inlineCallbacks
- def deleteCalendarResource(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())
-
- yield delete("", delchild.fp, "0")
-
- # Change CTag on the parent calendar collection
- yield collection.updateCTag()
Copied: CalendarServer/trunk/twistedcaldav/scheduling/processing.py (from rev 2879, CalendarServer/branches/users/cdaboo/implicit-2867/twistedcaldav/scheduling/processing.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/processing.py (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/processing.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -0,0 +1,332 @@
+#
+# 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 twistedcaldav.log import Logger
+from twistedcaldav.method import report_common
+from twisted.web2.dav.fileop import delete
+from twistedcaldav.scheduling.itip import iTipProcessing
+from hashlib import md5
+from twisted.web2.dav.util import joinURL
+from twistedcaldav.caldavxml import caldav_namespace
+import time
+
+__all__ = [
+ "ImplicitProcessor",
+]
+
+log = Logger()
+
+class ImplicitProcessorException(Exception):
+
+ def __init__(self, msg):
+ self.msg = msg
+
+class ImplicitProcessor(object):
+
+ def __init__(self):
+ pass
+
+ @inlineCallbacks
+ def doImplicitProcessing(self, request, message, originator, recipient):
+ """
+ Do implicit processing of a scheduling message, and possibly also auto-process it
+ if the recipient has auto-accept on.
+
+ @param message:
+ @type message:
+ @param originator:
+ @type originator:
+ @param recipient:
+ @type recipient:
+
+ @return: a C{tuple} of (C{bool}, C{bool}) indicating whether the message was processed, and if it was whether
+ auto-processing has taken place.
+ """
+
+ self.request = request
+ self.message = message
+ self.originator = originator
+ self.recipient = recipient
+
+ # TODO: for now going to assume that the originator is local - i.e. the scheduling message sent
+ # represents the actual organizer's view.
+
+ # First see whether this is the organizer or attendee sending the message
+ self.extractCalendarData()
+
+ if self.isOrganizerReceivingMessage():
+ result = (yield self.doImplicitOrganizer())
+ elif self.isAttendeeReceivingMessage():
+ result = (yield self.doImplicitAttendee())
+ else:
+ log.error("METHOD:%s not supported for implicit scheduling." % (self.method,))
+ raise ImplicitProcessorException("3.14;Unsupported capability")
+
+ returnValue(result)
+
+ def extractCalendarData(self):
+
+ # Some other useful things
+ self.method = self.message.propertyValue("METHOD")
+ self.uid = self.message.resourceUID()
+
+ def isOrganizerReceivingMessage(self):
+ return self.method in ("REPLY", "REFRESH")
+
+ def isAttendeeReceivingMessage(self):
+ return self.method in ("REQUEST", "ADD", "CANCEL")
+
+ @inlineCallbacks
+ def getRecipientsCopy(self):
+ """
+ Get the Recipient's copy of the event being processed.
+ """
+
+ self.recipient_calendar = None
+ self.recipient_calendar_collection = None
+ self.recipient_calendar_name = None
+ if self.recipient.principal:
+ # Get Recipient's calendar-home
+ calendar_home = self.recipient.principal.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.recipient_calendar = collection.iCalendar(rname)
+ self.recipient_calendar_name = rname
+ self.recipient_calendar_collection = collection
+ self.recipient_calendar_collection_uri = uri
+ 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)
+
+ @inlineCallbacks
+ def doImplicitOrganizer(self):
+
+ # Locate the organizer's copy of the event.
+ yield self.getRecipientsCopy()
+ if self.recipient_calendar is None:
+ log.debug("ImplicitProcessing - originator '%s' to recipient '%s' ignoring UID: '%s' - organizer has no copy" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
+ returnValue((True, True,))
+
+ # Handle new items differently than existing ones.
+ if self.method == "REPLY":
+ result = (yield self.doImplicitOrganizerUpdate())
+ elif self.method == "REFRESH":
+ # With implicit we ignore refreshes.
+ # TODO: for iMIP etc we do need to handle them
+ result = (True, True,)
+
+ returnValue(result)
+
+ @inlineCallbacks
+ def doImplicitOrganizerUpdate(self):
+
+ # Check to see if this is a valid reply
+ result, processed_attendees = iTipProcessing.processReply(self.message, self.recipient_calendar)
+ if result:
+
+ # Update the attendee's copy of the event
+ log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:REPLY, UID: '%s' - updating event" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
+ recipient_calendar_resource = (yield self.writeCalendarResource(self.recipient_calendar_collection_uri, self.recipient_calendar_collection, self.recipient_calendar_name, self.recipient_calendar))
+ result = (True, False,)
+
+ self.updateAllAttendeesExceptSome(recipient_calendar_resource, processed_attendees)
+
+ else:
+ # Ignore scheduling message
+ result = (True, True,)
+
+ returnValue(result)
+
+ def updateAllAttendeesExceptSome(self, resource, attendees):
+ """
+ Send an update out to all attendees except the specified ones, to refresh the others due to a change
+ by that one.
+
+ @param attendee: cu-addresses of attendees not to send to
+ @type attendee: C{set}
+ """
+
+ from twistedcaldav.scheduling.implicit import ImplicitScheduler
+ scheduler = ImplicitScheduler()
+ scheduler.refreshAllAttendeesExceptSome(self.request, resource, self.recipient_calendar, attendees)
+
+ @inlineCallbacks
+ def doImplicitAttendee(self):
+
+ # Locate the attendee's copy of the event if it exists.
+ yield self.getRecipientsCopy()
+ self.new_resource = self.recipient_calendar is None
+
+ # Handle new items differently than existing ones.
+ if self.new_resource and self.method == "CANCEL":
+ result = (True, True,)
+ else:
+ result = (yield self.doImplicitAttendeeUpdate())
+
+ returnValue(result)
+
+ @inlineCallbacks
+ def doImplicitAttendeeUpdate(self):
+
+ # Different based on method
+ if self.method == "REQUEST":
+ result = (yield self.doImplicitAttendeRequest())
+ elif self.method == "CANCEL":
+ result = (yield self.doImplicitAttendeCancel())
+ elif self.method == "ADD":
+ # TODO: implement ADD
+ result = (False, False,)
+
+ returnValue(result)
+
+ @inlineCallbacks
+ def doImplicitAttendeRequest(self):
+
+ # If there is no existing copy, then look for default calendar and copy it here
+ if self.new_resource:
+
+ # Check for default calendar
+ default = (yield self.recipient.inbox.readProperty((caldav_namespace, "schedule-default-calendar-URL"), self.request))
+ if len(default.children) == 1:
+ defaultURL = str(default.children[0])
+ default = (yield self.request.locateResource(defaultURL))
+ else:
+ default = None
+
+ if default:
+ log.debug("ImplicitProcessing - originator '%s' to recipient '%s' ignoring METHOD:REQUEST, UID: '%s' - new processed" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
+ new_calendar = iTipProcessing.processNewRequest(self.message)
+ name = md5(str(new_calendar) + str(time.time()) + default.fp.path).hexdigest() + ".ics"
+ yield self.writeCalendarResource(defaultURL, default, name, new_calendar)
+ result = (True, False,)
+ else:
+ log.debug("ImplicitProcessing - originator '%s' to recipient '%s' ignoring METHOD:REQUEST, UID: '%s' - new not processed" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
+ result = (False, False,)
+ else:
+ # Processing update to existing event
+ new_calendar = iTipProcessing.processRequest(self.message, self.recipient_calendar)
+ if new_calendar:
+
+ # Update the attendee's copy of the event
+ log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:REQUEST, UID: '%s' - updating event" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
+ yield self.writeCalendarResource(self.recipient_calendar_collection_uri, self.recipient_calendar_collection, self.recipient_calendar_name, new_calendar)
+ result = (True, False,)
+
+ else:
+ # Request needs to be ignored
+ log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:REQUEST, UID: '%s' - ignoring" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
+ result = (True, True,)
+
+ returnValue(result)
+
+
+ @inlineCallbacks
+ def doImplicitAttendeCancel(self):
+
+ # If there is no existing copy, then ignore
+ if self.recipient_calendar is None:
+ log.debug("ImplicitProcessing - originator '%s' to recipient '%s' ignoring METHOD:CANCEL, UID: '%s' - attendee has no copy" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
+ result = (True, True,)
+ else:
+ # Check to see if this is a cancel of the entire event
+ processed_message, delete_original = iTipProcessing.processCancel(self.message, self.recipient_calendar)
+ if processed_message:
+ if delete_original:
+
+ # Delete the attendee's copy of the event
+ log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:CANCEL, UID: '%s' - deleting entire event" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
+ yield self.deleteCalendarResource(self.recipient_calendar_collection, self.recipient_calendar_name)
+ result = (True, False,)
+
+ else:
+
+ # Update the attendee's copy of the event
+ log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:CANCEL, UID: '%s' - updating event" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
+ yield self.writeCalendarResource(self.recipient_calendar_collection_uri, self.recipient_calendar_collection, self.recipient_calendar_name, self.recipient_calendar)
+ result = (True, False,)
+ else:
+ log.debug("ImplicitProcessing - originator '%s' to recipient '%s' processing METHOD:CANCEL, UID: '%s' - ignoring" % (self.originator.cuaddr, self.recipient.cuaddr, self.uid))
+ result = (True, True,)
+
+ returnValue(result)
+
+ @inlineCallbacks
+ def writeCalendarResource(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: L{Deferred} -> L{CalDAVFile}
+ """
+
+ # Create a new name if one was not provided
+ if name is None:
+ name = md5(str(calendar) + str(time.time()) + collection.fp.path).hexdigest() + ".ics"
+
+ # Get a resource for the new item
+ newchildURL = joinURL(collURL, name)
+ newchild = yield self.request.locateResource(newchildURL)
+
+ # 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 = False,
+ allowImplicitSchedule = False
+ ).run()
+
+ returnValue(newchild)
+
+ @inlineCallbacks
+ def deleteCalendarResource(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())
+
+ yield delete("", delchild.fp, "0")
+
+ # Change CTag on the parent calendar collection
+ yield collection.updateCTag()
Deleted: CalendarServer/trunk/twistedcaldav/scheduling/scheduler.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2867/twistedcaldav/scheduling/scheduler.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/scheduling/scheduler.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -1,828 +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"
- self.internal_request = False
-
- @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()
-
- result = (yield self.doScheduling())
- returnValue(result)
-
- def doSchedulingViaPUT(self, originator, recipients, calendar, internal_request=False):
- """
- 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
- self.internal_request = internal_request
-
- return self.doScheduling()
-
- @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
- result = (yield self.generateSchedulingResponse())
-
- returnValue(result)
-
- 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)
-
- def generateLocalSchedulingResponses(self, recipients, responses, freebusy):
- """
- Generate scheduling responses for CalDAV recipients.
- """
-
- # Create the scheduler and run it.
- requestor = ScheduleViaCalDAV(self, recipients, responses, freebusy)
- return requestor.generateSchedulingResponses()
-
- def generateRemoteSchedulingResponses(self, recipients, responses, freebusy):
- """
- Generate scheduling responses for remote recipients.
- """
-
- # Create the scheduler and run it.
- requestor = ScheduleViaISchedule(self, recipients, responses, freebusy)
- return requestor.generateSchedulingResponses()
-
- def generateIMIPSchedulingResponses(self, recipients, responses, freebusy):
- """
- Generate scheduling responses for iMIP recipients.
- """
-
- # Create the scheduler and run it.
- requestor = ScheduleViaIMip(self, recipients, responses, freebusy)
- return requestor.generateSchedulingResponses()
-
-class CalDAVScheduler(Scheduler):
-
- def __init__(self, request, resource):
- super(CalDAVScheduler, self).__init__(request, resource)
- self.doingPOST = False
-
- def doSchedulingViaPOST(self):
- """
- The Scheduling POST operation on an Outbox.
- """
- self.doingPOST = True
- return super(CalDAVScheduler, self).doSchedulingViaPOST()
-
- def checkAuthorization(self):
- # Must have an authenticated user
- if not self.internal_request and 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, but not if this is a server
- # generated request
- if not self.internal_request:
- 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/trunk/twistedcaldav/scheduling/scheduler.py (from rev 2879, CalendarServer/branches/users/cdaboo/implicit-2867/twistedcaldav/scheduling/scheduler.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/scheduler.py (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/scheduler.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -0,0 +1,828 @@
+##
+# 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"
+ self.internal_request = False
+
+ @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()
+
+ result = (yield self.doScheduling())
+ returnValue(result)
+
+ def doSchedulingViaPUT(self, originator, recipients, calendar, internal_request=False):
+ """
+ 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
+ self.internal_request = internal_request
+
+ return self.doScheduling()
+
+ @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
+ result = (yield self.generateSchedulingResponse())
+
+ returnValue(result)
+
+ 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)
+
+ def generateLocalSchedulingResponses(self, recipients, responses, freebusy):
+ """
+ Generate scheduling responses for CalDAV recipients.
+ """
+
+ # Create the scheduler and run it.
+ requestor = ScheduleViaCalDAV(self, recipients, responses, freebusy)
+ return requestor.generateSchedulingResponses()
+
+ def generateRemoteSchedulingResponses(self, recipients, responses, freebusy):
+ """
+ Generate scheduling responses for remote recipients.
+ """
+
+ # Create the scheduler and run it.
+ requestor = ScheduleViaISchedule(self, recipients, responses, freebusy)
+ return requestor.generateSchedulingResponses()
+
+ def generateIMIPSchedulingResponses(self, recipients, responses, freebusy):
+ """
+ Generate scheduling responses for iMIP recipients.
+ """
+
+ # Create the scheduler and run it.
+ requestor = ScheduleViaIMip(self, recipients, responses, freebusy)
+ return requestor.generateSchedulingResponses()
+
+class CalDAVScheduler(Scheduler):
+
+ def __init__(self, request, resource):
+ super(CalDAVScheduler, self).__init__(request, resource)
+ self.doingPOST = False
+
+ def doSchedulingViaPOST(self):
+ """
+ The Scheduling POST operation on an Outbox.
+ """
+ self.doingPOST = True
+ return super(CalDAVScheduler, self).doSchedulingViaPOST()
+
+ def checkAuthorization(self):
+ # Must have an authenticated user
+ if not self.internal_request and 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, but not if this is a server
+ # generated request
+ if not self.internal_request:
+ 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/trunk/twistedcaldav/scheduling/test (from rev 2879, CalendarServer/branches/users/cdaboo/implicit-2867/twistedcaldav/scheduling/test)
Deleted: CalendarServer/trunk/twistedcaldav/scheduling/test/__init__.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2867/twistedcaldav/scheduling/test/__init__.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/scheduling/test/__init__.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -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/trunk/twistedcaldav/scheduling/test/__init__.py (from rev 2879, CalendarServer/branches/users/cdaboo/implicit-2867/twistedcaldav/scheduling/test/__init__.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/test/__init__.py (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/test/__init__.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -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/trunk/twistedcaldav/scheduling/test/test_icaldiff.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2867/twistedcaldav/scheduling/test/test_icaldiff.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/scheduling/test/test_icaldiff.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -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.attendeeMerge(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.attendeeMerge(attendee), result, msg=description)
Copied: CalendarServer/trunk/twistedcaldav/scheduling/test/test_icaldiff.py (from rev 2879, CalendarServer/branches/users/cdaboo/implicit-2867/twistedcaldav/scheduling/test/test_icaldiff.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/test/test_icaldiff.py (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/test/test_icaldiff.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -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.attendeeMerge(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.attendeeMerge(attendee), result, msg=description)
Deleted: CalendarServer/trunk/twistedcaldav/scheduling/test/test_implicit.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2867/twistedcaldav/scheduling/test/test_implicit.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/scheduling/test/test_implicit.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -1,762 +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
-from twistedcaldav.scheduling.implicit import ImplicitScheduler
-from dateutil.tz import tzutc
-import datetime
-
-class Implicit (twistedcaldav.test.util.TestCase):
- """
- iCalendar support tests
- """
-
- def test_removed_attendees(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
-""",
- (),
- ),
- (
- "#1.2 Simple component, one removal",
- """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
-END:VEVENT
-END:VCALENDAR
-""",
- (("mailto:user2 at example.com", None),),
- ),
- (
- "#1.3 Simple component, two removals",
- """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
-ATTENDEE:mailto:user3 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
-END:VEVENT
-END:VCALENDAR
-""",
- (
- ("mailto:user2 at example.com", None),
- ("mailto:user3 at example.com", None),
- ),
- ),
- (
- "#2.1 Simple recurring component, two removals",
- """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
-ATTENDEE:mailto:user3 at example.com
-RRULE:FREQ=MONTHLY
-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
-RRULE:FREQ=MONTHLY
-END:VEVENT
-END:VCALENDAR
-""",
- (
- ("mailto:user2 at example.com", None),
- ("mailto:user3 at example.com", None),
- ),
- ),
- (
- "#2.2 Simple recurring component, add exdate",
- """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
-ATTENDEE:mailto:user3 at example.com
-RRULE:FREQ=MONTHLY
-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
-ATTENDEE:mailto:user3 at example.com
-RRULE:FREQ=MONTHLY
-EXDATE:20080801T120000Z
-END:VEVENT
-END:VCALENDAR
-""",
- (
- ("mailto:user1 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
- ("mailto:user2 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
- ("mailto:user3 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
- ),
- ),
- (
- "#2.3 Simple recurring component, add multiple comma exdates",
- """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
-ATTENDEE:mailto:user3 at example.com
-RRULE:FREQ=MONTHLY
-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
-ATTENDEE:mailto:user3 at example.com
-RRULE:FREQ=MONTHLY
-EXDATE:20080801T120000Z,20080901T120000Z
-END:VEVENT
-END:VCALENDAR
-""",
- (
- ("mailto:user1 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
- ("mailto:user2 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
- ("mailto:user3 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
- ("mailto:user1 at example.com", datetime.datetime(2008, 9, 1, 12, 0, 0, tzinfo=tzutc())),
- ("mailto:user2 at example.com", datetime.datetime(2008, 9, 1, 12, 0, 0, tzinfo=tzutc())),
- ("mailto:user3 at example.com", datetime.datetime(2008, 9, 1, 12, 0, 0, tzinfo=tzutc())),
- ),
- ),
- (
- "#2.3 Simple recurring component, add multiple comma/property exdates",
- """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
-ATTENDEE:mailto:user3 at example.com
-RRULE:FREQ=MONTHLY
-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
-ATTENDEE:mailto:user3 at example.com
-RRULE:FREQ=MONTHLY
-EXDATE:20080801T120000Z,20080901T120000Z
-EXDATE:20081201T120000Z
-END:VEVENT
-END:VCALENDAR
-""",
- (
- ("mailto:user1 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
- ("mailto:user2 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
- ("mailto:user3 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
- ("mailto:user1 at example.com", datetime.datetime(2008, 9, 1, 12, 0, 0, tzinfo=tzutc())),
- ("mailto:user2 at example.com", datetime.datetime(2008, 9, 1, 12, 0, 0, tzinfo=tzutc())),
- ("mailto:user3 at example.com", datetime.datetime(2008, 9, 1, 12, 0, 0, tzinfo=tzutc())),
- ("mailto:user1 at example.com", datetime.datetime(2008, 12, 1, 12, 0, 0, tzinfo=tzutc())),
- ("mailto:user2 at example.com", datetime.datetime(2008, 12, 1, 12, 0, 0, tzinfo=tzutc())),
- ("mailto:user3 at example.com", datetime.datetime(2008, 12, 1, 12, 0, 0, tzinfo=tzutc())),
- ),
- ),
- (
- "#3.1 Complex recurring component with same attendees, 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
-ATTENDEE:mailto:user3 at example.com
-RRULE:FREQ=MONTHLY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080801T120000Z
-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
-ATTENDEE:mailto:user3 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
-ATTENDEE:mailto:user3 at example.com
-RRULE:FREQ=MONTHLY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080801T120000Z
-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
-ATTENDEE:mailto:user3 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
- (),
- ),
- (
- "#3.2 Complex recurring component with same attendees, change master/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:mailto:user2 at example.com
-ATTENDEE:mailto:user3 at example.com
-RRULE:FREQ=MONTHLY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080801T120000Z
-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
-ATTENDEE:mailto:user3 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
-RRULE:FREQ=MONTHLY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080801T120000Z
-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
-""",
- (
- ("mailto:user3 at example.com", None),
- ("mailto:user3 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
- ),
- ),
- (
- "#3.3 Complex recurring component with same attendees, change 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:mailto:user2 at example.com
-ATTENDEE:mailto:user3 at example.com
-RRULE:FREQ=MONTHLY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080801T120000Z
-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
-ATTENDEE:mailto:user3 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
-ATTENDEE:mailto:user3 at example.com
-RRULE:FREQ=MONTHLY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080801T120000Z
-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
-""",
- (
- ("mailto:user3 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
- ),
- ),
- (
- "#3.4 Complex recurring component with same attendees, change master",
- """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
-ATTENDEE:mailto:user3 at example.com
-RRULE:FREQ=MONTHLY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080801T120000Z
-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
-ATTENDEE:mailto:user3 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
-RRULE:FREQ=MONTHLY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080801T120000Z
-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
-ATTENDEE:mailto:user3 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
- (
- ("mailto:user3 at example.com", None),
- ),
- ),
- (
- "#3.5 Complex recurring component with same attendees, remove override - no exdate",
- """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
-ATTENDEE:mailto:user3 at example.com
-RRULE:FREQ=MONTHLY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080801T120000Z
-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
-ATTENDEE:mailto:user3 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
-ATTENDEE:mailto:user3 at example.com
-RRULE:FREQ=MONTHLY
-END:VEVENT
-END:VCALENDAR
-""",
- (),
- ),
- (
- "#3.6 Complex recurring component with same attendees, remove override - exdate",
- """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
-ATTENDEE:mailto:user3 at example.com
-RRULE:FREQ=MONTHLY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080801T120000Z
-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
-ATTENDEE:mailto:user3 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
-ATTENDEE:mailto:user3 at example.com
-RRULE:FREQ=MONTHLY
-EXDATE:20080801T120000Z
-END:VEVENT
-END:VCALENDAR
-""",
- (
- ("mailto:user1 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
- ("mailto:user2 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
- ("mailto:user3 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
- ),
- ),
- (
- "#4.1 Complex recurring component with different attendees, change master/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:mailto:user2 at example.com
-ATTENDEE:mailto:user3 at example.com
-RRULE:FREQ=MONTHLY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080801T120000Z
-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
-ATTENDEE:mailto:user4 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
-RRULE:FREQ=MONTHLY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080801T120000Z
-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
-""",
- (
- ("mailto:user3 at example.com", None),
- ("mailto:user4 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
- ),
- ),
- (
- "#4.2 Complex recurring component with different attendees, remove override - no exdate",
- """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
-ATTENDEE:mailto:user3 at example.com
-RRULE:FREQ=MONTHLY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080801T120000Z
-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
-ATTENDEE:mailto:user4 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
-ATTENDEE:mailto:user3 at example.com
-RRULE:FREQ=MONTHLY
-END:VEVENT
-END:VCALENDAR
-""",
- (
- ("mailto:user4 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
- ),
- ),
- (
- "#4.3 Complex recurring component with different attendees, remove override - exdate",
- """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
-ATTENDEE:mailto:user3 at example.com
-RRULE:FREQ=MONTHLY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080801T120000Z
-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
-ATTENDEE:mailto:user4 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
-ATTENDEE:mailto:user3 at example.com
-RRULE:FREQ=MONTHLY
-EXDATE:20080801T120000Z
-END:VEVENT
-END:VCALENDAR
-""",
- (
- ("mailto:user1 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
- ("mailto:user2 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
- ("mailto:user4 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
- ),
- ),
- )
-
- for description, calendar1, calendar2, result in data:
- scheduler = ImplicitScheduler()
- scheduler.oldcalendar = Component.fromString(calendar1)
- scheduler.calendar = Component.fromString(calendar2)
- scheduler.extractCalendarData()
- scheduler.findRemovedAttendees()
-# if not description.startswith("#4.3"):
-# continue
-# print description
-# print scheduler.cancelledAttendees
-# print set(result)
- self.assertEqual(scheduler.cancelledAttendees, set(result), msg=description)
-
Copied: CalendarServer/trunk/twistedcaldav/scheduling/test/test_implicit.py (from rev 2879, CalendarServer/branches/users/cdaboo/implicit-2867/twistedcaldav/scheduling/test/test_implicit.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/test/test_implicit.py (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/test/test_implicit.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -0,0 +1,762 @@
+##
+# 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
+from twistedcaldav.scheduling.implicit import ImplicitScheduler
+from dateutil.tz import tzutc
+import datetime
+
+class Implicit (twistedcaldav.test.util.TestCase):
+ """
+ iCalendar support tests
+ """
+
+ def test_removed_attendees(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
+""",
+ (),
+ ),
+ (
+ "#1.2 Simple component, one removal",
+ """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
+END:VEVENT
+END:VCALENDAR
+""",
+ (("mailto:user2 at example.com", None),),
+ ),
+ (
+ "#1.3 Simple component, two removals",
+ """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
+ATTENDEE:mailto:user3 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
+END:VEVENT
+END:VCALENDAR
+""",
+ (
+ ("mailto:user2 at example.com", None),
+ ("mailto:user3 at example.com", None),
+ ),
+ ),
+ (
+ "#2.1 Simple recurring component, two removals",
+ """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
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+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
+RRULE:FREQ=MONTHLY
+END:VEVENT
+END:VCALENDAR
+""",
+ (
+ ("mailto:user2 at example.com", None),
+ ("mailto:user3 at example.com", None),
+ ),
+ ),
+ (
+ "#2.2 Simple recurring component, add exdate",
+ """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
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+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
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+EXDATE:20080801T120000Z
+END:VEVENT
+END:VCALENDAR
+""",
+ (
+ ("mailto:user1 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
+ ("mailto:user2 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
+ ("mailto:user3 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
+ ),
+ ),
+ (
+ "#2.3 Simple recurring component, add multiple comma exdates",
+ """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
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+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
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+EXDATE:20080801T120000Z,20080901T120000Z
+END:VEVENT
+END:VCALENDAR
+""",
+ (
+ ("mailto:user1 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
+ ("mailto:user2 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
+ ("mailto:user3 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
+ ("mailto:user1 at example.com", datetime.datetime(2008, 9, 1, 12, 0, 0, tzinfo=tzutc())),
+ ("mailto:user2 at example.com", datetime.datetime(2008, 9, 1, 12, 0, 0, tzinfo=tzutc())),
+ ("mailto:user3 at example.com", datetime.datetime(2008, 9, 1, 12, 0, 0, tzinfo=tzutc())),
+ ),
+ ),
+ (
+ "#2.3 Simple recurring component, add multiple comma/property exdates",
+ """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
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+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
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+EXDATE:20080801T120000Z,20080901T120000Z
+EXDATE:20081201T120000Z
+END:VEVENT
+END:VCALENDAR
+""",
+ (
+ ("mailto:user1 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
+ ("mailto:user2 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
+ ("mailto:user3 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
+ ("mailto:user1 at example.com", datetime.datetime(2008, 9, 1, 12, 0, 0, tzinfo=tzutc())),
+ ("mailto:user2 at example.com", datetime.datetime(2008, 9, 1, 12, 0, 0, tzinfo=tzutc())),
+ ("mailto:user3 at example.com", datetime.datetime(2008, 9, 1, 12, 0, 0, tzinfo=tzutc())),
+ ("mailto:user1 at example.com", datetime.datetime(2008, 12, 1, 12, 0, 0, tzinfo=tzutc())),
+ ("mailto:user2 at example.com", datetime.datetime(2008, 12, 1, 12, 0, 0, tzinfo=tzutc())),
+ ("mailto:user3 at example.com", datetime.datetime(2008, 12, 1, 12, 0, 0, tzinfo=tzutc())),
+ ),
+ ),
+ (
+ "#3.1 Complex recurring component with same attendees, 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
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+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
+ATTENDEE:mailto:user3 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
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+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
+ATTENDEE:mailto:user3 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ (),
+ ),
+ (
+ "#3.2 Complex recurring component with same attendees, change master/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:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+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
+ATTENDEE:mailto:user3 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
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+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
+""",
+ (
+ ("mailto:user3 at example.com", None),
+ ("mailto:user3 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
+ ),
+ ),
+ (
+ "#3.3 Complex recurring component with same attendees, change 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:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+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
+ATTENDEE:mailto:user3 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
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+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
+""",
+ (
+ ("mailto:user3 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
+ ),
+ ),
+ (
+ "#3.4 Complex recurring component with same attendees, change master",
+ """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
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+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
+ATTENDEE:mailto:user3 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
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+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
+ATTENDEE:mailto:user3 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ (
+ ("mailto:user3 at example.com", None),
+ ),
+ ),
+ (
+ "#3.5 Complex recurring component with same attendees, remove override - no exdate",
+ """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
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+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
+ATTENDEE:mailto:user3 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
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+END:VCALENDAR
+""",
+ (),
+ ),
+ (
+ "#3.6 Complex recurring component with same attendees, remove override - exdate",
+ """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
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+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
+ATTENDEE:mailto:user3 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
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+EXDATE:20080801T120000Z
+END:VEVENT
+END:VCALENDAR
+""",
+ (
+ ("mailto:user1 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
+ ("mailto:user2 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
+ ("mailto:user3 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
+ ),
+ ),
+ (
+ "#4.1 Complex recurring component with different attendees, change master/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:mailto:user2 at example.com
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+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
+ATTENDEE:mailto:user4 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
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+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
+""",
+ (
+ ("mailto:user3 at example.com", None),
+ ("mailto:user4 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
+ ),
+ ),
+ (
+ "#4.2 Complex recurring component with different attendees, remove override - no exdate",
+ """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
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+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
+ATTENDEE:mailto:user4 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
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+END:VCALENDAR
+""",
+ (
+ ("mailto:user4 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
+ ),
+ ),
+ (
+ "#4.3 Complex recurring component with different attendees, remove override - exdate",
+ """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
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+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
+ATTENDEE:mailto:user4 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
+ATTENDEE:mailto:user3 at example.com
+RRULE:FREQ=MONTHLY
+EXDATE:20080801T120000Z
+END:VEVENT
+END:VCALENDAR
+""",
+ (
+ ("mailto:user1 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
+ ("mailto:user2 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
+ ("mailto:user4 at example.com", datetime.datetime(2008, 8, 1, 12, 0, 0, tzinfo=tzutc())),
+ ),
+ ),
+ )
+
+ for description, calendar1, calendar2, result in data:
+ scheduler = ImplicitScheduler()
+ scheduler.oldcalendar = Component.fromString(calendar1)
+ scheduler.calendar = Component.fromString(calendar2)
+ scheduler.extractCalendarData()
+ scheduler.findRemovedAttendees()
+# if not description.startswith("#4.3"):
+# continue
+# print description
+# print scheduler.cancelledAttendees
+# print set(result)
+ self.assertEqual(scheduler.cancelledAttendees, set(result), msg=description)
+
Deleted: CalendarServer/trunk/twistedcaldav/scheduling/test/test_itip.py
===================================================================
--- CalendarServer/branches/users/cdaboo/implicit-2867/twistedcaldav/scheduling/test/test_itip.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/scheduling/test/test_itip.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -1,1075 +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 dateutil.tz import tzutc
-from twistedcaldav.ical import Component
-from twistedcaldav.scheduling.itip import iTipProcessing, iTipGenerator
-import datetime
-import os
-import twistedcaldav.test.util
-
-class iTIPProcessing (twistedcaldav.test.util.TestCase):
- """
- iCalendar support tests
- """
-
- def test_update_attendee_partstat(self):
-
- data = (
- (
- "#1.1 Simple component, accepted",
- """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
-METHOD:REPLY
-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;PARTSTAT=ACCEPTED: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
-ATTENDEE;PARTSTAT=ACCEPTED:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
- ),
- (
- "#1.2 Simple component, accepted",
- """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;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
- """BEGIN:VCALENDAR
-METHOD:REPLY
-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;PARTSTAT=ACCEPTED: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
-ATTENDEE;PARTSTAT=ACCEPTED:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
- ),
- (
- "#1.3 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
-METHOD:REPLY
-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
-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
-ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
- ),
- (
- "#2.1 Recurring component, change master/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;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-RRULE:FREQ=MONTHLY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080801T120000Z
-DTSTART:20080801T123000Z
-DTEND:20080801T133000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
- """BEGIN:VCALENDAR
-METHOD:REPLY
-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;PARTSTAT=ACCEPTED:mailto:user1 at example.com
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080801T120000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE;PARTSTAT=DECLINED: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
-ATTENDEE;PARTSTAT=ACCEPTED:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=MONTHLY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080801T120000Z
-DTSTART:20080801T123000Z
-DTEND:20080801T133000Z
-ATTENDEE;PARTSTAT=DECLINED:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
- ),
- (
- "#2.2 Recurring component, change master only",
- """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;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-RRULE:FREQ=MONTHLY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080801T120000Z
-DTSTART:20080801T123000Z
-DTEND:20080801T133000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
- """BEGIN:VCALENDAR
-METHOD:REPLY
-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;PARTSTAT=ACCEPTED: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
-ATTENDEE;PARTSTAT=ACCEPTED:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=MONTHLY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080801T120000Z
-DTSTART:20080801T123000Z
-DTEND:20080801T133000Z
-ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
- ),
- (
- "#2.3 Recurring component, change override only",
- """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;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-RRULE:FREQ=MONTHLY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080801T120000Z
-DTSTART:20080801T123000Z
-DTEND:20080801T133000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
- """BEGIN:VCALENDAR
-METHOD:REPLY
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080801T120000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE;PARTSTAT=DECLINED: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
-ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=MONTHLY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080801T120000Z
-DTSTART:20080801T123000Z
-DTEND:20080801T133000Z
-ATTENDEE;PARTSTAT=DECLINED:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
- ),
- (
- "#3.1 Recurring component, change master/override, new 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;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-RRULE:FREQ=MONTHLY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080801T120000Z
-DTSTART:20080801T123000Z
-DTEND:20080801T133000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
- """BEGIN:VCALENDAR
-METHOD:REPLY
-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;PARTSTAT=ACCEPTED:mailto:user1 at example.com
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080801T120000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE;PARTSTAT=DECLINED:mailto:user1 at example.com
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080901T120000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE;PARTSTAT=TENTATIVE: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
-ATTENDEE;PARTSTAT=ACCEPTED:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=MONTHLY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080801T120000Z
-DTSTART:20080801T123000Z
-DTEND:20080801T133000Z
-ATTENDEE;PARTSTAT=DECLINED:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080901T120000Z
-DTSTART:20080901T120000Z
-DTEND:20080901T130000Z
-ATTENDEE;PARTSTAT=TENTATIVE:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
- ),
- (
- "#3.2 Recurring component, change master, new 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;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-RRULE:FREQ=MONTHLY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080801T120000Z
-DTSTART:20080801T123000Z
-DTEND:20080801T133000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
- """BEGIN:VCALENDAR
-METHOD:REPLY
-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;PARTSTAT=ACCEPTED:mailto:user1 at example.com
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080901T120000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE;PARTSTAT=TENTATIVE: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
-ATTENDEE;PARTSTAT=ACCEPTED:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=MONTHLY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080801T120000Z
-DTSTART:20080801T123000Z
-DTEND:20080801T133000Z
-ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080901T120000Z
-DTSTART:20080901T120000Z
-DTEND:20080901T130000Z
-ATTENDEE;PARTSTAT=TENTATIVE:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
- ),
- (
- "#3.3 Recurring component, change override, new 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;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-RRULE:FREQ=MONTHLY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080801T120000Z
-DTSTART:20080801T123000Z
-DTEND:20080801T133000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
- """BEGIN:VCALENDAR
-METHOD:REPLY
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080801T120000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE;PARTSTAT=DECLINED:mailto:user1 at example.com
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080901T120000Z
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE;PARTSTAT=TENTATIVE: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
-ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-RRULE:FREQ=MONTHLY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080801T120000Z
-DTSTART:20080801T123000Z
-DTEND:20080801T133000Z
-ATTENDEE;PARTSTAT=DECLINED:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080901T120000Z
-DTSTART:20080901T120000Z
-DTEND:20080901T130000Z
-ATTENDEE;PARTSTAT=TENTATIVE:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
-ORGANIZER;CN=User 01:mailto:user1 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
- ),
- )
-
- for description, calendar_txt, itipmsg_txt, result in data:
- calendar = Component.fromString(calendar_txt)
- itipmsg = Component.fromString(itipmsg_txt)
- iTipProcessing.processReply(itipmsg, calendar)
-# if not description.startswith("#3.1"):
-# continue
-# print description
-# print str(calendar)
-# print str(result)
- self.assertEqual(str(calendar).replace("\r", ""), str(result), msg=description)
-
-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)
Copied: CalendarServer/trunk/twistedcaldav/scheduling/test/test_itip.py (from rev 2879, CalendarServer/branches/users/cdaboo/implicit-2867/twistedcaldav/scheduling/test/test_itip.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/test/test_itip.py (rev 0)
+++ CalendarServer/trunk/twistedcaldav/scheduling/test/test_itip.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -0,0 +1,1075 @@
+##
+# 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 dateutil.tz import tzutc
+from twistedcaldav.ical import Component
+from twistedcaldav.scheduling.itip import iTipProcessing, iTipGenerator
+import datetime
+import os
+import twistedcaldav.test.util
+
+class iTIPProcessing (twistedcaldav.test.util.TestCase):
+ """
+ iCalendar support tests
+ """
+
+ def test_update_attendee_partstat(self):
+
+ data = (
+ (
+ "#1.1 Simple component, accepted",
+ """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
+METHOD:REPLY
+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;PARTSTAT=ACCEPTED: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
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ (
+ "#1.2 Simple component, accepted",
+ """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;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+METHOD:REPLY
+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;PARTSTAT=ACCEPTED: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
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ (
+ "#1.3 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
+METHOD:REPLY
+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
+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
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
+ATTENDEE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ (
+ "#2.1 Recurring component, change master/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;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+DTSTART:20080801T123000Z
+DTEND:20080801T133000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+METHOD:REPLY
+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;PARTSTAT=ACCEPTED:mailto:user1 at example.com
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE;PARTSTAT=DECLINED: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
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+DTSTART:20080801T123000Z
+DTEND:20080801T133000Z
+ATTENDEE;PARTSTAT=DECLINED:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ (
+ "#2.2 Recurring component, change master only",
+ """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;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+DTSTART:20080801T123000Z
+DTEND:20080801T133000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+METHOD:REPLY
+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;PARTSTAT=ACCEPTED: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
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+DTSTART:20080801T123000Z
+DTEND:20080801T133000Z
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ (
+ "#2.3 Recurring component, change override only",
+ """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;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+DTSTART:20080801T123000Z
+DTEND:20080801T133000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+METHOD:REPLY
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE;PARTSTAT=DECLINED: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
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+DTSTART:20080801T123000Z
+DTEND:20080801T133000Z
+ATTENDEE;PARTSTAT=DECLINED:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ (
+ "#3.1 Recurring component, change master/override, new 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;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+DTSTART:20080801T123000Z
+DTEND:20080801T133000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+METHOD:REPLY
+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;PARTSTAT=ACCEPTED:mailto:user1 at example.com
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE;PARTSTAT=DECLINED:mailto:user1 at example.com
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080901T120000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE;PARTSTAT=TENTATIVE: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
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+DTSTART:20080801T123000Z
+DTEND:20080801T133000Z
+ATTENDEE;PARTSTAT=DECLINED:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080901T120000Z
+DTSTART:20080901T120000Z
+DTEND:20080901T130000Z
+ATTENDEE;PARTSTAT=TENTATIVE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ (
+ "#3.2 Recurring component, change master, new 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;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+DTSTART:20080801T123000Z
+DTEND:20080801T133000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+METHOD:REPLY
+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;PARTSTAT=ACCEPTED:mailto:user1 at example.com
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080901T120000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE;PARTSTAT=TENTATIVE: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
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+DTSTART:20080801T123000Z
+DTEND:20080801T133000Z
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080901T120000Z
+DTSTART:20080901T120000Z
+DTEND:20080901T130000Z
+ATTENDEE;PARTSTAT=TENTATIVE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ (
+ "#3.3 Recurring component, change override, new 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;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+DTSTART:20080801T123000Z
+DTEND:20080801T133000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+METHOD:REPLY
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE;PARTSTAT=DECLINED:mailto:user1 at example.com
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080901T120000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE;PARTSTAT=TENTATIVE: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
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=MONTHLY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080801T120000Z
+DTSTART:20080801T123000Z
+DTEND:20080801T133000Z
+ATTENDEE;PARTSTAT=DECLINED:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080901T120000Z
+DTSTART:20080901T120000Z
+DTEND:20080901T130000Z
+ATTENDEE;PARTSTAT=TENTATIVE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ )
+
+ for description, calendar_txt, itipmsg_txt, result in data:
+ calendar = Component.fromString(calendar_txt)
+ itipmsg = Component.fromString(itipmsg_txt)
+ iTipProcessing.processReply(itipmsg, calendar)
+# if not description.startswith("#3.1"):
+# continue
+# print description
+# print str(calendar)
+# print str(result)
+ self.assertEqual(str(calendar).replace("\r", ""), str(result), msg=description)
+
+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/trunk/twistedcaldav/static.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/static.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/static.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -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/trunk/twistedcaldav/tap.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/tap.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/tap.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -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/trunk/twistedcaldav/test
___________________________________________________________________
Name: svn:ignore
+ oddata
s2s-data
Modified: CalendarServer/trunk/twistedcaldav/test/test_icalendar.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_icalendar.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/test/test_icalendar.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -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, Property
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,1096 @@
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_set_parameter_value(self):
+ data = (
+ # ATTENDEE - no existing parameter
+ (
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+ORGANIZER:mailto:user01 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;SCHEDULE-STATUS="2.0;OK":mailto:user02 at example.com
+ORGANIZER:mailto:user01 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ (
+ "SCHEDULE-STATUS",
+ "2.0;OK",
+ "ATTENDEE",
+ "mailto:user02 at example.com",
+ ),
+ ),
+ # ATTENDEE - existing parameter
+ (
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;SCHEDULE-STATUS="5.0;BAD":mailto:user02 at example.com
+ORGANIZER:mailto:user01 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;SCHEDULE-STATUS="2.0;OK":mailto:user02 at example.com
+ORGANIZER:mailto:user01 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ (
+ "SCHEDULE-STATUS",
+ "2.0;OK",
+ "ATTENDEE",
+ "mailto:user02 at example.com",
+ ),
+ ),
+ # ORGANIZER - no existing parameter
+ (
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+ORGANIZER:mailto:user01 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+ORGANIZER;SCHEDULE-STATUS="2.0;OK":mailto:user01 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ (
+ "SCHEDULE-STATUS",
+ "2.0;OK",
+ "ORGANIZER",
+ "mailto:user01 at example.com",
+ ),
+ ),
+ # ORGANIZER - existing parameter
+ (
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+ORGANIZER;SCHEDULE-STATUS="5.0;BAD":mailto:user01 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE:mailto:user02 at example.com
+ORGANIZER;SCHEDULE-STATUS="2.0;OK":mailto:user01 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+ (
+ "SCHEDULE-STATUS",
+ "2.0;OK",
+ "ORGANIZER",
+ "mailto:user01 at example.com",
+ ),
+ ),
+ )
+
+ for original, result, args in data:
+ component = Component.fromString(original)
+ component.setParameterToValueForPropertyWithValue(*args)
+ self.assertEqual(result, str(component).replace("\r", ""))
+
+ def test_add_property(self):
+ data = (
+ # Simple component
+ (
+ """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
+REQUEST-STATUS:2.0\;Success
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ # Complex component
+ (
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20071115T000000Z
+DTSTART:20071115T020000Z
+END:VEVENT
+END:VCALENDAR
+""",
+ """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//PYVOBJECT//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+REQUEST-STATUS:2.0\;Success
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20071115T000000Z
+DTSTART:20071115T020000Z
+REQUEST-STATUS:2.0\;Success
+END:VEVENT
+END:VCALENDAR
+""",
+ ),
+ )
+
+ for original, result in data:
+ component = Component.fromString(original)
+ component.addPropertyToAllComponents(Property("REQUEST-STATUS", "2.0;Success"))
+ self.assertEqual(result, str(component).replace("\r", ""))
+
+ 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/trunk/twistedcaldav/test/test_imip.py (from rev 2879, CalendarServer/branches/users/cdaboo/implicit-2867/twistedcaldav/test/test_imip.py)
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_imip.py (rev 0)
+++ CalendarServer/trunk/twistedcaldav/test/test_imip.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -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)
+
Modified: CalendarServer/trunk/twistedcaldav/test/test_options.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_options.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/test/test_options.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -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
Modified: CalendarServer/trunk/twistedcaldav/timezoneservice.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/timezoneservice.py 2008-08-26 19:20:26 UTC (rev 2879)
+++ CalendarServer/trunk/twistedcaldav/timezoneservice.py 2008-08-26 20:09:00 UTC (rev 2880)
@@ -23,7 +23,6 @@
"TimezoneServiceResource",
]
-from twisted.internet.defer import inlineCallbacks, returnValue
from twisted.web2 import responsecode
from twisted.web2.dav import davxml
from twisted.web2.dav.http import ErrorResponse
@@ -105,36 +104,38 @@
# GET and POST do the same thing
return self.http_POST(request)
- @inlineCallbacks
def http_POST(self, request):
"""
The timezone service POST method.
"""
# Check authentication and access controls
- yield self.authorize(request, (davxml.Read(),))
-
- if not request.args:
- # Do normal GET behavior
- response = yield self.render(request)
- returnValue(response)
-
- method = request.args.get("method", ("",))
- if len(method) != 1:
- raise HTTPError(ErrorResponse(responsecode.BAD_REQUEST, (calendarserver_namespace, "valid-method")))
- method = method[0]
+ def _gotResult(_):
- action = {
- "list" : self.doPOSTList,
- "get" : self.doPOSTGet,
- "expand" : self.doPOSTExpand,
- }.get(method, None)
-
- if action is None:
- raise HTTPError(ErrorResponse(responsecode.BAD_REQUEST, (calendarserver_namespace, "supported-method")))
+ if not request.args:
+ # Do normal GET behavior
+ return self.render(request)
+
+ method = request.args.get("method", ("",))
+ if len(method) != 1:
+ raise HTTPError(ErrorResponse(responsecode.BAD_REQUEST, (calendarserver_namespace, "valid-method")))
+ method = method[0]
+
+ action = {
+ "list" : self.doPOSTList,
+ "get" : self.doPOSTGet,
+ "expand" : self.doPOSTExpand,
+ }.get(method, None)
+
+ if action is None:
+ raise HTTPError(ErrorResponse(responsecode.BAD_REQUEST, (calendarserver_namespace, "supported-method")))
+
+ return action(request)
+
+ d = self.authorize(request, (davxml.Read(),))
+ d.addCallback(_gotResult)
+ return d
- returnValue(action(request))
-
def doPOSTList(self, request):
"""
Return a list of all timezones known to the server.
-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20080826/1cf5e524/attachment-0001.html
More information about the calendarserver-changes
mailing list