[CalendarServer-changes] [3203] CalendarServer/trunk/twistedcaldav
source_changes at macosforge.org
source_changes at macosforge.org
Tue Oct 21 13:59:50 PDT 2008
Revision: 3203
http://trac.macosforge.org/projects/calendarserver/changeset/3203
Author: cdaboo at apple.com
Date: 2008-10-21 13:59:49 -0700 (Tue, 21 Oct 2008)
Log Message:
-----------
Re-factored auto-accept processing so that all the iTIP behavior is now handled by the implicit iTIP
code. The auto-accept is done during implicit processing, with the reply messages sent via reactor.callLater.
Modified Paths:
--------------
CalendarServer/trunk/twistedcaldav/ical.py
CalendarServer/trunk/twistedcaldav/scheduling/caldav.py
CalendarServer/trunk/twistedcaldav/scheduling/implicit.py
CalendarServer/trunk/twistedcaldav/scheduling/itip.py
CalendarServer/trunk/twistedcaldav/scheduling/processing.py
Removed Paths:
-------------
CalendarServer/trunk/twistedcaldav/itip.py
Modified: CalendarServer/trunk/twistedcaldav/ical.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/ical.py 2008-10-21 20:50:21 UTC (rev 3202)
+++ CalendarServer/trunk/twistedcaldav/ical.py 2008-10-21 20:59:49 UTC (rev 3203)
@@ -1287,14 +1287,14 @@
@param propname: the property name
@type propname: C{str}
@param propvalue: the property value to test
- @type propvalue: C{str}
+ @type propvalue: C{str} or C{None}
"""
for component in self.subcomponents():
if component.name() == "VTIMEZONE":
continue
for property in component.properties(propname):
- if property.value() == propvalue:
+ if propvalue is None or property.value() == propvalue:
property.params()[paramname] = [paramvalue]
def hasPropertyInAnyComponent(self, properties):
Deleted: CalendarServer/trunk/twistedcaldav/itip.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/itip.py 2008-10-21 20:50:21 UTC (rev 3202)
+++ CalendarServer/trunk/twistedcaldav/itip.py 2008-10-21 20:59:49 UTC (rev 3203)
@@ -1,873 +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
-import md5
-import time
-
-from twisted.python.failure import Failure
-from twisted.internet.defer import inlineCallbacks, returnValue, maybeDeferred,\
- succeed
-from twisted.web2.dav import davxml
-from twisted.web2.dav.method.report import NumberOfMatchesWithinLimits
-from twisted.web2.dav.util import joinURL
-from twisted.web2.dav.fileop import delete
-from twisted.web2.dav.resource import AccessDeniedError
-
-from twistedcaldav import caldavxml
-from twistedcaldav.accounting import accountingEnabled, emitAccounting
-from twistedcaldav.log import Logger
-from twistedcaldav.ical import Property
-from twistedcaldav.memcachelock import MemcacheLock, MemcacheLockTimeoutError
-from twistedcaldav.method import report_common
-from twistedcaldav.resource import isCalendarCollectionResource
-
-log = Logger()
-
-__version__ = "0.0"
-
-__all__ = [
- "iTipProcessor",
- "iTipGenerator",
-]
-
-class iTipException(Exception):
- pass
-
-class iTipProcessor(object):
-
- @inlineCallbacks
- 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
-
- 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 = ""
-
- # Get a lock on the inbox first
- _lock = MemcacheLock("iTIPAutoProcess", inbox.fp.path, timeout=60.0, retry_interval=1.0, expire_time=300)
-
- try:
- yield _lock.acquire()
- yield f()
- yield _lock.release()
- except MemcacheLockTimeoutError:
- raise
- except Exception, e:
- log.error(e)
- yield _lock.clean()
- raise e
-
- @inlineCallbacks
- def processRequest(self):
- """
- Process a METHOD=REQUEST.
-
- 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, add to f-b-s calendar DECLINED
- 2. If so, add to f-b-s calendar ACCEPTED
- 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"
-
- # 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()
-
- # 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 new_master:
- # So we have a full update. That means we need to delete any existing events completely and
- # replace with the ones provided so long as the new one is newer.
-
- # If we have a match then we need to check whether we are updating etc
- check_reply = False
- if calmatch:
- # See whether the new component is older than any existing ones and throw it away if so
- newinfo = (None,) + self.getComponentSyncInfo(new_master)
- cal = updatecal.iCalendar(calmatch)
- info = self.getSyncInfo(calmatch, cal)
- if self.compareSyncInfo(info, newinfo) < 0:
- # Existing resource is older and will be replaced
- check_reply = True
- else:
- processed = "older"
- else:
- # We have a new request which we can reply to
- check_reply = True
-
- if check_reply:
- # Process the reply by determining PARTSTAT and sending the reply and booking the event.
- valid, accepted = yield self.checkForReply()
-
- if valid:
- try:
- if calmatch:
- log.info("Replaced calendar component %s with new iTIP message in %s (%s)." % (calmatch, calURL, accepted,))
- yield self.writeResource(calURL, updatecal, calmatch, self.calendar)
- else:
- log.info("Added new calendar component in %s (%s)." % (calURL, accepted,))
- yield self.writeResource(calURL, updatecal, None, self.calendar)
-
- processed = "processed"
- except:
- # FIXME: bare except
- log.err("Error while auto-processing iTIP: %s" % (Failure(),))
- raise iTipException()
- else:
- processed = "ignored"
-
- else:
- # So we have a partial update. That means we have to do partial updates to instances in
- # the existing calendar component.
-
- # If we have a match then we need to check whether we are updating etc
- check_reply = False
- if calmatch:
- # Check each component to see whether its new
- cal = updatecal.iCalendar(calmatch)
- old_master = cal.masterComponent()
- processed = "older"
- new_components = [component for component in self.calendar.subcomponents()]
- for component in new_components:
- if component.name() == "VTIMEZONE":
- continue
-
- newinfo = (None,) + self.getComponentSyncInfo(component)
- old_component = self.findMatchingComponent(component, cal)
- if old_component:
- info = (None,) + self.getComponentSyncInfo(old_component)
- elif old_master:
- info = (None,) + self.getComponentSyncInfo(old_master)
- else:
- info = None
-
- if info is None or self.compareSyncInfo(info, newinfo) < 0:
- # Existing resource is older and will be replaced
- check_reply = True
- processed = "processed"
- else:
- self.calendar.removeComponent(component)
- else:
- # 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.
- valid, accepted = yield self.checkForReply()
-
- if valid:
- try:
- if calmatch:
- # Merge the new instances with the old ones
- self.mergeComponents(self.calendar, cal)
- log.info("Merged calendar component %s with new iTIP message in %s (%s)." % (calmatch, calURL, accepted,))
- yield self.writeResource(calURL, updatecal, calmatch, cal)
- else:
- log.info("Added new calendar component in %s (%s)." % (calURL, accepted,))
- yield self.writeResource(calURL, updatecal, None, self.calendar)
-
- processed = "processed"
- except:
- # FIXME: bare except
- log.err("Error while auto-processing iTIP: %s" % (Failure(),))
- raise iTipException()
- else:
- processed = "ignored"
-
- # Remove the now processed incoming request.
- if self.inbox:
- yield self.deleteInboxResource({
- "processed":"processed",
- "older": "ignored: older",
- "ignored": "ignored: no match"
- }[processed])
-
- returnValue(None)
-
- def processAdd(self):
- """
- Process a METHOD=ADD.
- """
- log.info("Auto-processing iTIP ADD for: %s" % (str(self.principal),))
-
- raise NotImplementedError()
-
- @inlineCallbacks
- def processCancel(self):
- """
- Process a METHOD=CANCEL.
-
- Policy find all components that match UID, SEQ and R-ID and remove them.
-
- Steps:
-
- 1. See if this updates existing ones in Inbox.
- 2. Remove existing ones in Inbox.
- 3. See if this updates existing ones in free-busy-set calendars.
- 4. Remove existing ones in those calendars.
- 5. Remove the incoming request.
-
- NB Removal can be complex as we need to take RECURRENCE-ID into account - i.e a single
- instance may be cancelled. What we need to do for this is:
-
- 1. If the R-ID of iTIP component matches the R-ID of one in Inbox then it is an exact match, so
- delete the old one.
- 2. If the R-ID of iTIP does not match an R-ID in Inbox, then we are adding a cancellation as an override, so
- leave the new and existing ones in the Inbox.
- 3. If the R-ID of iTIP component matches the R-ID of an overridden component in an f-b-s calendar, then
- remove the overridden component from the f-b-s resource.
- 4. Add an EXDATE to the f-b-s resource to 'cancel' that instance.
-
- TODO: Yes, I am going to ignore RANGE= on RECURRENCE-ID for now...
- """
-
- log.info("Auto-processing iTIP CANCEL for: %s" % (str(self.principal),))
- processed = "ignored"
-
- # Get all component info for this iTIP message
- newinfo = self.getSyncInfo(self.childname, self.calendar)
-
- # First see if we have a recurrence id which will force extra work
- has_rid = False
- if newinfo[4] is not None:
- has_rid = True
- else:
- for i in self.getAllInfo(self.inbox, self.calendar, self.child):
- if i[4] is not None:
- has_rid = True
- break
-
- # Next we want to try and find a match to any components on existing calendars listed as contributing
- # to free-busy as we will need to update those with the new one.
- calmatch, updatecal, calURL = yield self.findCalendarMatch()
-
- 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:
- log.info("Delete calendar component %s in %s as it was cancelled." % (calmatch, calURL))
- yield self.deleteResource(updatecal, calmatch,)
- except:
- # FIXME: bare except
- log.err("Error while auto-processing iTIP: %s" % (Failure(),))
- raise iTipException()
- processed = "processed"
- else:
- processed = "older"
- else:
- # Nothing to do except delete the inbox item as we have nothing to cancel.
- processed = "ignored"
- else:
- # 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
-
- # 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
- log.info("Deleted calendar component %s after cancellations from iTIP message in %s." % (calmatch, calURL))
- yield self.deleteResource(updatecal, calmatch)
- else:
- # Update the existing calendar object
- log.info("Updated calendar component %s with cancellations from iTIP message in %s." % (calmatch, calURL))
- yield self.writeResource(calURL, updatecal, calmatch, existing_calendar)
- processed = "processed"
- 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.
- if self.inbox:
- yield self.deleteInboxResource({
- "processed":"processed",
- "older": "ignored: older",
- "ignored": "ignored: no match"
- }[processed])
-
- returnValue(None)
-
- @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).
-
- 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.
-
- @return: a C{tuple} of C{bool} indicating whether a valid iTIP was received, and C{str} new partstat.
- """
-
- # 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)
-
- # 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
-
- if not accepted:
- break
-
- # 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, "",))
-
- if accepted:
- partstat = "ACCEPTED"
- else:
- partstat = "DECLINED"
-
- # Make sure declined events are TRANSPARENT on the calendar
- self.calendar.replacePropertyInAllComponents(Property("TRANSP", "TRANSPARENT"))
-
- for attendeeProp in attendeeProps:
- attendeeProp.params()["PARTSTAT"] = [partstat]
-
- returnValue((True, partstat,))
-
- @inlineCallbacks
- def writeReply(self, replycal):
- """
- Write an iTIP message reply into the specified Inbox.
-
- @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)
-
- 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)
-
- 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(newchild)
-
- @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"
-
- # 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,
- internal_request=True,
- ).run()
-
- returnValue(newchild)
-
- 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]))))
-
- @inlineCallbacks
- def deleteInboxResource(self, processed_state):
- # Remove the now processed incoming request.
- try:
- log.info("Deleting new iTIP message %s in Inbox because it has been %s." %
- (self.childname, processed_state,))
- yield self.deleteResource(self.inbox, self.childname)
- except:
- # FIXME: bare except
- log.err("Error while auto-processing iTIP: %s" % (Failure(),))
- raise iTipException()
-
- 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)
-
- # Sometimes the resource might already be gone...
- if delchild is None:
- log.warn("Nothing to delete: %s in %s is missing." % (name, collection))
- return succeed(None)
-
- 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
-
- @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 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
-
- @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.
-
- # Find the current recipients calendar-free-busy-set
- calendars = yield self.getCalendarsToMatch()
-
- # 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)
-
- returnValue((calmatch, updatecal, calURL))
-
- def getCalendarsToMatch(self):
- # Determine the set of calendar URIs for a principal need to be searched.
-
- # Find the current recipients calendar-free-busy-set
- return self.principal.calendarFreeBusyURIs(self.request)
-
- def matchComponentInCalendar(self, collection, calendar):
- """
- See if the component in the provided iTIP calendar object matches any in the specified calendar
- collection.
-
- @param collection: L{CalDAVFile} for the calendar collection to examine.
- @param calendar: L{Component} for calendar to examine.
- @return: C{list} of resource names found.
- """
-
- try:
- # Extract UID from primary component (note we allow multiple components to be present
- # because CANCEL requests can have multiple components).
- comp = calendar.mainComponent(allow_multiple=True)
- uid = comp.propertyValue("UID")
-
- # Now use calendar collection index to find all other resources with the same UID
- index = collection.index()
- result = index.resourceNamesForUID(uid)
-
- # There can be only one
- if len(result) > 0:
- return result[0]
- else:
- return None
- except ValueError:
- return None
-
- def findMatchingComponent(self, component, calendar):
- """
- See if any overridden component in the provided iTIP calendar object matches the specified component.
-
- @param component: the component to try and match.
- @type component: L{Component}
- @param calendar: the calendar to find a match in.
- @type calendar: L{Component}
- @return: L{Component} for matching component,
- or C{None} if not found.
- """
-
- # Extract RECURRENCE-ID value from component
- rid = component.getRecurrenceIDUTC()
-
- # Return the one that matches in the calendar
- return calendar.overriddenComponent(rid)
-
- def mergeComponents(self, newcal, oldcal):
- """
- Merge the overridden instance components in newcal into oldcal replacing any
- matching components there.
-
- @param newcal: the new overridden instances to use.
- @type newcal: L{Component}
- @param oldcal: the component to merge into.
- @type oldcal: L{Component}
- """
-
- # 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
-
- 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 []
-
- # Now get info for each name
- result = []
- for name in names:
- cal = collection.iCalendar(name)
- result.append(self.getSyncInfo(name, cal))
-
- 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)
-
- 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)
-
- def compareComponents(self, 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: 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
- 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]:
- return 1
- if info1[3] < info2[3]:
- 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
Modified: CalendarServer/trunk/twistedcaldav/scheduling/caldav.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/caldav.py 2008-10-21 20:50:21 UTC (rev 3202)
+++ CalendarServer/trunk/twistedcaldav/scheduling/caldav.py 2008-10-21 20:59:49 UTC (rev 3203)
@@ -14,7 +14,6 @@
# limitations under the License.
##
-from twisted.internet import reactor
from twisted.internet.defer import inlineCallbacks, returnValue
from twisted.python.failure import Failure
@@ -30,7 +29,6 @@
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
@@ -96,7 +94,6 @@
organizerProp = self.scheduler.calendar.getOrganizerProperty()
uid = self.scheduler.calendar.resourceUID()
- autoresponses = []
for recipient in self.recipients:
#
@@ -123,22 +120,10 @@
if self.freebusy:
yield self.generateFreeBusyResponse(recipient, self.responses, organizerProp, uid)
else:
- yield self.generateResponse(recipient, self.responses, autoresponses)
+ yield self.generateResponse(recipient, self.responses)
- # 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(2.0, itip.handleRequest, *(self.scheduler.request, principal, inbox, self.scheduler.calendar.duplicate(), child))
-
@inlineCallbacks
- def generateResponse(self, recipient, responses, autoresponses):
+ def generateResponse(self, recipient, responses):
# 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"
@@ -147,25 +132,20 @@
childURL = joinURL(recipient.inboxURL, name)
child = (yield self.scheduler.request.locateResource(childURL))
- # Do implicit scheduling message processing - but not for auto-accept principals.
- # Auto-accepts will be processed internally by the server a little later on.
- if not recipient.principal.autoSchedule():
- try:
- processor = ImplicitProcessor()
- processed, autoprocessed, changes = (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)
- else:
- processed = autoprocessed = False
- changes = None
+ # Do implicit scheduling message processing.
+ try:
+ processor = ImplicitProcessor()
+ processed, autoprocessed, changes = (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
@@ -205,10 +185,6 @@
# Store CS:schedule-changes property if present
if changes:
child.writeDeadProperty(changes)
-
- # Look for auto-schedule option
- if recipient.principal.autoSchedule():
- autoresponses.append((recipient.principal, recipient.inbox, child))
returnValue(True)
Modified: CalendarServer/trunk/twistedcaldav/scheduling/implicit.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/implicit.py 2008-10-21 20:50:21 UTC (rev 3202)
+++ CalendarServer/trunk/twistedcaldav/scheduling/implicit.py 2008-10-21 20:59:49 UTC (rev 3203)
@@ -129,6 +129,27 @@
returnValue(result)
@inlineCallbacks
+ def sendAttendeeReply(self, request, resource, calendar, attendee):
+
+ self.request = request
+ self.resource = resource
+ self.calendar = calendar
+ self.calendar_owner = None
+ self.deleting = False
+ self.internal_request = True
+ self.changed_rids = None
+
+ # Get some useful information from the calendar
+ yield self.extractCalendarData()
+
+ self.attendee = attendee.cuaddr
+ self.attendeePrincipal = attendee.principal
+
+ result = (yield self.scheduleWithOrganizer())
+
+ returnValue(result)
+
+ @inlineCallbacks
def extractCalendarData(self):
# Get the originator who is the authenticated user
Modified: CalendarServer/trunk/twistedcaldav/scheduling/itip.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/itip.py 2008-10-21 20:50:21 UTC (rev 3202)
+++ CalendarServer/trunk/twistedcaldav/scheduling/itip.py 2008-10-21 20:59:49 UTC (rev 3203)
@@ -51,7 +51,7 @@
class iTipProcessing(object):
@staticmethod
- def processNewRequest(itip_message, recipient=None):
+ def processNewRequest(itip_message, recipient=None, autoprocessing=False):
"""
Process a METHOD=REQUEST for a brand new calendar object.
@@ -67,13 +67,13 @@
if method:
calendar.removeProperty(method)
- if config.Scheduling["CalDAV"]["OldDraftCompatability"] and recipient:
+ if config.Scheduling["CalDAV"]["OldDraftCompatability"] and recipient and not autoprocessing:
iTipProcessing.fixForiCal3(calendar.subcomponents(), recipient)
return calendar
@staticmethod
- def processRequest(itip_message, calendar, recipient):
+ def processRequest(itip_message, calendar, recipient, autoprocessing=False):
"""
Process a METHOD=REQUEST.
@@ -104,7 +104,7 @@
if itip_message.masterComponent() is not None:
# Get a new calendar object first
- new_calendar = iTipProcessing.processNewRequest(itip_message, recipient)
+ new_calendar = iTipProcessing.processNewRequest(itip_message, recipient, autoprocessing)
# Copy over master alarms, comments
master_component = new_calendar.masterComponent()
@@ -135,7 +135,7 @@
component = component.duplicate()
iTipProcessing.transferItems(calendar, master_valarms, private_comments, component, remove_matched=True)
calendar.addComponent(component)
- if config.Scheduling["CalDAV"]["OldDraftCompatability"] and recipient:
+ if config.Scheduling["CalDAV"]["OldDraftCompatability"] and recipient and not autoprocessing:
iTipProcessing.fixForiCal3((component,), recipient)
# Write back the modified object
Modified: CalendarServer/trunk/twistedcaldav/scheduling/processing.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/processing.py 2008-10-21 20:50:21 UTC (rev 3202)
+++ CalendarServer/trunk/twistedcaldav/scheduling/processing.py 2008-10-21 20:59:49 UTC (rev 3203)
@@ -14,15 +14,19 @@
# limitations under the License.
##
+from hashlib import md5
from twisted.internet.defer import inlineCallbacks, returnValue, succeed
+from twisted.web2.dav.fileop import delete
+from twisted.web2.dav.util import joinURL
+from twistedcaldav import customxml, caldavxml
+from twistedcaldav.caldavxml import caldav_namespace
+from twistedcaldav.ical import Property
from twistedcaldav.log import Logger
from twistedcaldav.method import report_common
-from twisted.web2.dav.fileop import delete
+from twistedcaldav.method.report import NumberOfMatchesWithinLimits
from twistedcaldav.scheduling.itip import iTipProcessing
-from hashlib import md5
-from twisted.web2.dav.util import joinURL
-from twistedcaldav.caldavxml import caldav_namespace
-from twistedcaldav import customxml
+from twisted.internet import reactor
+import datetime
import time
__all__ = [
@@ -226,6 +230,9 @@
elif self.method == "ADD":
# TODO: implement ADD
result = (False, False, None)
+ else:
+ # NB We should never get here as we will have rejected unsupported METHODs earlier.
+ result = (True, True, None,)
returnValue(result)
@@ -242,13 +249,27 @@
default = (yield self.request.locateResource(defaultURL))
else:
default = None
+
+ # Must have a calendar if auto-replying
+ if default is None and self.recipient.principal.autoSchedule():
+ log.error("No default calendar for auto-replying recipient: '%s'." % (self.recipient.cuaddr,))
+ raise ImplicitProcessorException("5.3;No scheduling support for user")
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, self.recipient.cuaddr)
+ autoprocessed = self.recipient.principal.autoSchedule()
+ new_calendar = iTipProcessing.processNewRequest(self.message, self.recipient.cuaddr, autoprocessing=autoprocessed)
name = md5(str(new_calendar) + str(time.time()) + default.fp.path).hexdigest() + ".ics"
- yield self.writeCalendarResource(defaultURL, default, name, new_calendar)
+ # Handle auto-reply behavior
+ if autoprocessed:
+ send_reply, partstat = (yield self.checkAttendeeAutoReply(new_calendar))
+
+ new_resource = (yield self.writeCalendarResource(defaultURL, default, name, new_calendar))
+
+ if autoprocessed and send_reply:
+ reactor.callLater(2.0, self.sendAttendeeAutoReply, *(new_calendar, new_resource, partstat))
+
# Build the schedule-changes XML element
changes = customxml.ScheduleChanges(
customxml.DTStamp(),
@@ -256,18 +277,26 @@
customxml.Create(),
),
)
- result = (True, False, changes,)
+ result = (True, autoprocessed, changes,)
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, None,)
else:
# Processing update to existing event
- new_calendar, props_changed, rids = iTipProcessing.processRequest(self.message, self.recipient_calendar, self.recipient.cuaddr)
+ autoprocessed = self.recipient.principal.autoSchedule()
+ new_calendar, props_changed, rids = iTipProcessing.processRequest(self.message, self.recipient_calendar, self.recipient.cuaddr, autoprocessing=autoprocessed)
if new_calendar:
+ # Handle auto-reply behavior
+ if autoprocessed:
+ send_reply, partstat = (yield self.checkAttendeeAutoReply(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)
+ new_resource = (yield self.writeCalendarResource(self.recipient_calendar_collection_uri, self.recipient_calendar_collection, self.recipient_calendar_name, new_calendar))
+
+ if autoprocessed and send_reply:
+ reactor.callLater(2.0, self.sendAttendeeAutoReply, *(new_calendar, new_resource, partstat))
# Build the schedule-changes XML element
changes = ()
@@ -302,7 +331,7 @@
customxml.Update(*update_details),
),
)
- result = (True, False, changes,)
+ result = (True, autoprocessed, changes,)
else:
# Request needs to be ignored
@@ -320,6 +349,10 @@
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, None)
else:
+ # Need to check for auto-respond attendees. These need to suppress the inbox message
+ # if the cancel is processed.
+ autoprocessed = self.recipient.principal.autoSchedule()
+
# Check to see if this is a cancel of the entire event
processed_message, delete_original, rids = iTipProcessing.processCancel(self.message, self.recipient_calendar)
if processed_message:
@@ -336,7 +369,7 @@
customxml.Cancel(),
),
)
- result = (True, False, changes,)
+ result = (True, autoprocessed, changes,)
else:
@@ -354,14 +387,124 @@
),
),
)
- result = (True, False, changes)
+ result = (True, autoprocessed, changes)
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, None)
returnValue(result)
+ def sendAttendeeAutoReply(self, calendar, resource, partstat):
+ """
+ Auto-process the calendar option to generate automatic accept/decline status and
+ send a reply if needed.
+
+ @param calendar: calendar data to examine
+ @type calendar: L{Component}
+
+ @return: L{Component} for the new calendar data to write
+ """
+
+ # Send out a reply
+ log.debug("ImplicitProcessing - recipient '%s' processing UID: '%s' - auto-reply: %s" % (self.recipient.cuaddr, self.uid, partstat))
+ from twistedcaldav.scheduling.implicit import ImplicitScheduler
+ scheduler = ImplicitScheduler()
+ scheduler.sendAttendeeReply(self.request, resource, calendar, self.recipient)
+
@inlineCallbacks
+ def checkAttendeeAutoReply(self, calendar):
+ """
+ Check whether a reply to the given iTIP message is needed. We will not process a reply
+ 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. set PARTSTAT to ACCEPTED or DECLINED).
+
+ 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.
+
+ @return: C{bool} indicating whether changes were made.
+ """
+
+ log.debug("ImplicitProcessing - recipient '%s' processing UID: '%s' - checking for auto-reply" % (self.recipient.cuaddr, self.uid))
+
+ # 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)
+ instance_states = dict([(instance, True) for instance in instances.instances.itervalues()])
+
+ # 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")
+
+ # 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]):
+ instance_states[instance] = False
+ except NumberOfMatchesWithinLimits:
+ instance_states[instance] = False
+ log.info("Exceeded number of matches whilst trying to find free-time.")
+
+ # If everything is declined we can exit now
+ if all([not state for state in instance_states.itervalues()]):
+ break
+
+ # TODO: here we should do per-instance ACCEPT/DECLINE behavior
+ # For now we will assume overall ACCEPT/DECLINE
+
+ # Collect all the accepted and declined states
+ accepted = all(instance_states.itervalues())
+
+ # Extract the ATTENDEE property matching current recipient from the calendar data
+ cuas = self.recipient.principal.calendarUserAddresses()
+ attendeeProps = calendar.getAttendeeProperties(cuas)
+ if not attendeeProps:
+ returnValue((False, "",))
+
+ if accepted:
+ partstat = "ACCEPTED"
+ else:
+ partstat = "DECLINED"
+
+ # Make sure declined events are TRANSPARENT on the calendar
+ calendar.replacePropertyInAllComponents(Property("TRANSP", "TRANSPARENT"))
+
+ made_changes = False
+ for attendeeProp in attendeeProps:
+ if attendeeProp.params().get("PARTSTAT", ("NEEDS-ACTION",))[0] != partstat:
+ attendeeProp.params()["PARTSTAT"] = [partstat]
+ made_changes = True
+
+ # Fake a SCHEDULE-STATUS on the ORGANIZER property
+ if made_changes:
+ calendar.setParameterToValueForPropertyWithValue("SCHEDULE-STATUS", "2.0;Success", "ORGANIZER", None)
+
+ returnValue((made_changes, partstat,))
+
+ def _getCalendarsToMatch(self):
+ # Determine the set of calendar URIs for a principal need to be searched.
+
+ # Find the current recipients calendar-free-busy-set
+ return self.recipient.principal.calendarFreeBusyURIs(self.request)
+
+ @inlineCallbacks
def writeCalendarResource(self, collURL, collection, name, calendar):
"""
Write out the calendar resource (iTIP) message to the specified calendar, either over-writing the named
-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20081021/3aeb70cf/attachment-0001.html
More information about the calendarserver-changes
mailing list