[CalendarServer-changes] [1903] CalendarServer/branches/users/cdaboo/better-itip-1900/twistedcaldav

source_changes at macosforge.org source_changes at macosforge.org
Tue Sep 25 12:00:25 PDT 2007


Revision: 1903
          http://trac.macosforge.org/projects/calendarserver/changeset/1903
Author:   cdaboo at apple.com
Date:     2007-09-25 12:00:24 -0700 (Tue, 25 Sep 2007)

Log Message:
-----------
Merged branch forward.

Modified Paths:
--------------
    CalendarServer/branches/users/cdaboo/better-itip-1900/twistedcaldav/ical.py
    CalendarServer/branches/users/cdaboo/better-itip-1900/twistedcaldav/itip.py

Modified: CalendarServer/branches/users/cdaboo/better-itip-1900/twistedcaldav/ical.py
===================================================================
--- CalendarServer/branches/users/cdaboo/better-itip-1900/twistedcaldav/ical.py	2007-09-25 17:57:26 UTC (rev 1902)
+++ CalendarServer/branches/users/cdaboo/better-itip-1900/twistedcaldav/ical.py	2007-09-25 19:00:24 UTC (rev 1903)
@@ -46,7 +46,7 @@
 from twisted.web2.stream import IStream
 from twisted.web2.dav.util import allDataFromStream
 
-from twistedcaldav.dateops import normalizeToUTC, timeRangesOverlap
+from twistedcaldav.dateops import compareDateTime, normalizeToUTC, timeRangesOverlap
 from twistedcaldav.instance import InstanceList
 
 iCalendarProductID = "-//CALENDARSERVER.ORG//NONSGML Version 1//EN"
@@ -309,7 +309,7 @@
         
         return type
     
-    def mainComponent(self):
+    def mainComponent(self, allow_multiple=False):
         """
         Return the primary iCal component in this calendar.
         @return: the L{Component} of the primary type.
@@ -321,13 +321,51 @@
         for component in self.subcomponents():
             if component.name() == "VTIMEZONE":
                 continue
-            elif (result is not None):
+            elif not allow_multiple and (result is not None):
                 raise ValueError("Calendar contains more than one primary component: %r" % (self,))
             else:
                 result = component
+                if allow_multiple:
+                    break
         
         return result
     
+    def masterComponent(self):
+        """
+        Return the master iCal component in this calendar.
+        @return: the L{Component} for the master component,
+            or C{None} if there isn't one.
+        """
+        assert self.name() == "VCALENDAR", "Must be a VCALENDAR: %r" % (self,)
+        
+        for component in self.subcomponents():
+            if component.name() == "VTIMEZONE":
+                continue
+            if not component.hasProperty("RECURRENCE-ID"):
+                return component
+        
+        return None
+    
+    def overriddenComponent(self, recurrence_id):
+        """
+        Return the overridden iCal component in this calendar matching the supplied RECURRENCE-ID property.
+
+        @param recurrence_id: The RECURRENCE-ID property value to match.
+        @type recurrence_id: L{datetime.datetime} or L{datetime.date}
+        @return: the L{Component} for the overridden component,
+            or C{None} if there isn't one.
+        """
+        assert self.name() == "VCALENDAR", "Must be a VCALENDAR: %r" % (self,)
+        
+        for component in self.subcomponents():
+            if component.name() == "VTIMEZONE":
+                continue
+            rid = component.getRecurrenceIDUTC()
+            if rid and compareDateTime(rid, recurrence_id) == 0:
+                return component
+        
+        return None
+    
     def duplicate(self):
         """
         Duplicate this object and all its contents.
@@ -355,6 +393,13 @@
         self._vobject.add(component._vobject)
         component._parent = self
 
+    def removeComponent(self, component):
+        """
+        Removes a subcomponent from this component.
+        @param component: the L{Component} to remove.
+        """
+        self._vobject.remove(component._vobject)
+
     def hasProperty(self, name):
         """
         @param name: the name of the property whose existence is being tested.
@@ -690,8 +735,6 @@
         assert self.name() == "VCALENDAR", "Not a calendar: %r" % (self,)
 
         if not hasattr(self, "_resource_uid"):
-            has_timezone = False
-
             for subcomponent in self.subcomponents():
                 if subcomponent.name() != "VTIMEZONE":
                     self._resource_uid = subcomponent.propertyValue("UID")
@@ -982,6 +1025,37 @@
 
         return None
 
+    def getAttendeeProperties(self, match):
+        """
+        Get all the attendees matching a value in each component. Works on a VCALENDAR component only.
+        
+        @param match: a C{list} of calendar user address strings to try and match.
+        @return: the string value of the Organizer property, or None
+        """
+        
+        assert self.name() == "VCALENDAR", "Not a calendar: %r" % (self,)
+
+        # FIXME: we should really have a URL class and have it manage comparisons
+        # in a sensible fashion.
+        def _normalizeCUAddress(addr):
+            if addr.startswith("/") or addr.startswith("http:") or addr.startswith("https:"):
+                return addr.rstrip("/")
+            else:
+                return addr
+
+        # Need to normalize http/https cu addresses
+        test = set()
+        for item in match:
+           test.add(_normalizeCUAddress(item))
+        
+        # Extract appropriate sub-component if this is a VCALENDAR
+        results = []
+        for component in self.subcomponents():
+            if component.name() != "VTIMEZONE":
+                results.append(component.getAttendeeProperty(match))
+
+        return results
+
     def getMaskUID(self):
         """
         Get the X-CALENDARSEREVR-MASK-UID value. Works on either a VCALENDAR or on a component.

Modified: CalendarServer/branches/users/cdaboo/better-itip-1900/twistedcaldav/itip.py
===================================================================
--- CalendarServer/branches/users/cdaboo/better-itip-1900/twistedcaldav/itip.py	2007-09-25 17:57:26 UTC (rev 1902)
+++ CalendarServer/branches/users/cdaboo/better-itip-1900/twistedcaldav/itip.py	2007-09-25 19:00:24 UTC (rev 1903)
@@ -88,8 +88,6 @@
     Process a METHOD=REQUEST.
     This is a deferredGenerator function so use yield whenever we have a deferred.
 
-    TODO: ignore recurrence overrides
-    
     Steps:
     
       1. See if this updates existing ones in Inbox.
@@ -114,153 +112,162 @@
     """
     
     logging.info("[ITIP]: Auto-processing iTIP REQUEST 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 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()
 
-    # 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 = False
-        for i in info:
-            # For any that are older, delete them.
-            if compareSyncInfo(i, newinfo) < 0:
-                try:
-                    d = waitForDeferred(deleteResource(inbox, i[0]))
-                    yield d
-                    d.getResult()
-                    logging.info("[ITIP]: deleted iTIP message %s in Inbox that was older than the new one." % (i[0],))
-                except:
-                    log.err("Error while auto-processing iTIP: %s" % (failure.Failure(),))
-                    raise iTipException
-            else:
-                # For any that are newer or the same, mark the new one to be deleted.
-                delete_child = True
-
-        # Delete the new one if so marked.
-        if delete_child:
-            try:
-                d = waitForDeferred(deleteResource(inbox, child.fp.basename()))
-                yield d
-                d.getResult()
-                logging.info("[ITIP]: deleted new iTIP message %s in Inbox because it was older than existing ones." % (child.fp.basename(),))
-            except:
-                log.err("Error while auto-processing iTIP: %s" % (failure.Failure(),))
-                raise iTipException
-            yield None
-            return
-
-        # 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.
+    # Next we want to try and find a match to any components on existing calendars listed as contributing
+    # to free-busy as we will need to update those with the new one.
+    d = waitForDeferred(findCalendarMatch(request, principal, calendar))
+    yield d
+    calmatch, updatecal, calURL = d.getResult()
+    
+    if new_master:
+        # So we have a full update. That means we need to delete any existing events completely and
+        # replace with the ones provided so long as the new one is newer.
         
-        # Find the current recipients calendar-free-busy-set
-        fbset = waitForDeferred(principal.calendarFreeBusyURIs(request))
-        yield fbset
-        fbset = fbset.getResult()
-
-        # Find the first calendar in the list with a component matching the one we are processing
-        calmatch = None
-        for calURL in fbset:
-            updatecal = waitForDeferred(request.locateResource(calURL))
-            yield updatecal
-            updatecal = updatecal.getResult()
-            if updatecal is None or not updatecal.exists() or not isCalendarCollectionResource(updatecal):
-                # We will ignore missing calendars. If the recipient has failed to
-                # properly manage the free busy set that should not prevent us from working.
-                continue
-            calmatch = matchComponentInCalendar(updatecal, calendar, None)
-            if calmatch:
-                logging.info("[ITIP]: found calendar component %s matching new iTIP message in %s." % (calmatch[0], calURL))
-                break
-        
         # If we have a match then we need to check whether we are updating etc
-        d = waitForDeferred(checkForReply(request, principal, calendar))
-        yield d
-        doreply, replycal, accepted = d.getResult()
+        check_reply = False
         if calmatch:
-            # See whether the current component is older than any existing ones and throw it away if so
-            cal = updatecal.iCalendar(calmatch[0])
-            info = getSyncInfo(calmatch[0], cal)
+            # 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:
-                # Re-write existing resource with new one, if accepted, otherwise delete existing as the
-                # update to it was not accepted.
-                try:
-                    if accepted:
-                        newchild = waitForDeferred(writeResource(request, calURL, updatecal, calmatch[0], calendar))
+                # 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.
+            d = waitForDeferred(checkForReply(request, principal, calendar))
+            yield d
+            doreply, replycal, accepted = d.getResult()
+            
+            try:
+                if accepted:
+                    if calmatch:
+                        newchild = waitForDeferred(writeResource(request, calURL, updatecal, calmatch, calendar))
                         yield newchild
                         newchild = newchild.getResult()
-                        logging.info("[ITIP]: replaced calendar component %s with new iTIP message in %s." % (calmatch[0], calURL))
+                        logging.info("[ITIP]: replaced calendar component %s with new iTIP message in %s." % (calmatch, calURL))
                     else:
-                        d = waitForDeferred(deleteResource(updatecal, calmatch[0]))
+                        newchild = waitForDeferred(writeResource(request, calURL, updatecal, None, calendar))
+                        yield newchild
+                        newchild.getResult()
+                        logging.info("[ITIP]: added new calendar component in %s." % (calURL,))
+                else:
+                    if calmatch:
+                        d = waitForDeferred(deleteResource(updatecal, calmatch))
                         yield d
                         d.getResult()
-                        logging.info("[ITIP]: deleted calendar component %s in %s as update was not accepted." % (calmatch[0], calURL))
-                except:
-                    log.err("Error while auto-processing iTIP: %s" % (failure.Failure(),))
-                    raise iTipException
+                        logging.info("[ITIP]: deleted calendar component %s in %s as update was not accepted." % (calmatch, calURL))
+                        
+                # Send a reply if needed. 
+                if doreply:
+                    logging.info("[ITIP]: sending iTIP REPLY %s" % (("declined","accepted")[accepted],))
+                    newchild = waitForDeferred(writeReply(request, principal, replycal, inbox))
+                    yield newchild
+                    newchild = newchild.getResult()
+                    newInboxResource(child, newchild)
+                processed = "processed"
+            except:
+                log.err("Error while auto-processing iTIP: %s" % (failure.Failure(),))
+                raise iTipException
+            
+    else:
+        # So we have a partial update. That means we have to do partial updates to instances in
+        # the existing calendar component.
 
-            else:
-                # Delete new one in Inbox as it is old
-                try:
-                    d = waitForDeferred(deleteResource(inbox, child.fp.basename()))
-                    yield d
-                    d.getResult()
-                    logging.info("[ITIP]: deleted new iTIP message %s in Inbox because it was older than %s in %s." % (child.fp.basename(), calmatch[0], calURL))
-                except:
-                    log.err("Error while auto-processing iTIP: %s" % (failure.Failure(),))
-                    raise iTipException
-                yield None
-                return
+        # If we have a match then we need to check whether we are updating etc
+        check_reply = False
+        if calmatch:
+            # Check each component to see whether its new
+            cal = updatecal.iCalendar(calmatch)
+            old_master = cal.masterComponent()
+            processed = "older"
+            new_components = [component for component in calendar.subcomponents()]
+            for component in new_components:
+                if component.name() == "VTIMEZONE":
+                    continue
+                
+                newinfo = (None,) + getComponentSyncInfo(component)
+                old_component = findMatchingComponent(component, cal)
+                if old_component:
+                    info = (None,) + getComponentSyncInfo(old_component)
+                elif old_master:
+                    info = (None,) + getComponentSyncInfo(old_master)
+                else:
+                    info = None
+                    
+                if info is None or compareSyncInfo(info, newinfo) < 0:
+                    # Existing resource is older and will be replaced
+                    check_reply = True
+                    processed = "processed"
+                else:
+                    calendar.removeComponent(component)
         else:
-            # Write new resource into first calendar in f-b-set
-            if len(fbset) != 0 and accepted:
-                calURL = fbset[0]
-                updatecal = waitForDeferred(request.locateResource(calURL))
-                yield updatecal
-                updatecal = updatecal.getResult()
-                try:
-                    newchild = waitForDeferred(writeResource(request, calURL, updatecal, None, calendar))
-                    yield newchild
-                    newchild.getResult()
-                    logging.info("[ITIP]: added new calendar component in %s." % (calURL,))
-                except:
-                    log.err("Error while auto-processing iTIP: %s" % (failure.Failure(),))
-                    raise iTipException
-        
-        # If we get here we have a new iTIP message that we want to process. Any previous ones
-        # have been removed (so we won't run in to problems when we check that there is free time
-        # to book the new one). 
-        if doreply:
-            logging.info("[ITIP]: sending iTIP REPLY %s" % (("declined","accepted")[accepted],))
-            newchild = waitForDeferred(writeReply(request, principal, replycal, inbox))
-            yield newchild
-            newchild = newchild.getResult()
-            newInboxResource(child, newchild)
+            # We have a new request which we can reply to
+            check_reply = True
 
-        # Remove the now processed incoming request.
-        try:
-            d = waitForDeferred(deleteResource(inbox, child.fp.basename()))
+        if check_reply:
+            # Process the reply by determining PARTSTAT and sending the reply and booking the event.
+            d = waitForDeferred(checkForReply(request, principal, calendar))
             yield d
-            d.getResult()
-            logging.info("[ITIP]: deleted new iTIP message %s in Inbox because it has been processed." % (child.fp.basename(),))
-        except:
-            log.err("Error while auto-processing iTIP: %s" % (failure.Failure(),))
-            raise iTipException
-        yield None
-        return
-    else:
-        raise NotImplementedError
+            doreply, replycal, accepted = d.getResult()
+            
+            try:
+                if calmatch:
+                    # Merge the new instances with the old ones
+                    mergeComponents(calendar, cal)
+                    newchild = waitForDeferred(writeResource(request, calURL, updatecal, calmatch, cal))
+                    yield newchild
+                    newchild = newchild.getResult()
+                    logging.info("[ITIP]: merged calendar component %s with new iTIP message in %s." % (calmatch, calURL))
+                else:
+                    if accepted:
+                        newchild = waitForDeferred(writeResource(request, calURL, updatecal, None, calendar))
+                        yield newchild
+                        newchild.getResult()
+                        logging.info("[ITIP]: added new calendar component in %s." % (calURL,))
+                        
+                # Do reply if needed. 
+                if doreply:
+                    logging.info("[ITIP]: sending iTIP REPLY %s" % (("declined","accepted")[accepted],))
+                    newchild = waitForDeferred(writeReply(request, principal, replycal, inbox))
+                    yield newchild
+                    newchild = newchild.getResult()
+                    newInboxResource(child, newchild)
+                    
+                processed = "processed"
+            except:
+                log.err("Error while auto-processing iTIP: %s" % (failure.Failure(),))
+                raise iTipException
 
+    # Remove the now processed incoming request.
+    try:
+        d = waitForDeferred(deleteResource(inbox, child.fp.basename()))
+        yield d
+        d.getResult()
+        logging.info("[ITIP]: deleted new iTIP message %s in Inbox because it has been %s." %
+            (child.fp.basename(),
+             {"processed":"processed",
+              "older":    "ignored: older",
+              "ignored":  "ignored: no match"}[processed],))
+    except:
+        log.err("Error while auto-processing iTIP: %s" % (failure.Failure(),))
+        raise iTipException
+    yield None
+    return
+
 processRequest = deferredGenerator(processRequest)
 
 def processAdd(request, principal, inbox, calendar, child):
@@ -316,6 +323,7 @@
     """
     
     logging.info("[ITIP]: Auto-processing iTIP CANCEL for: %s" % (str(principal),))
+    processed = "ignored"
 
     # Get all component info for this iTIP message
     newinfo = getSyncInfo(child.fp.basename(), calendar)
@@ -333,103 +341,136 @@
             
     if not has_rid:
         # Compare the new one with each existing one.
-        delete_child = False
-        for i in info:
-            # For any that are older, delete them.
-            if compareSyncInfo(i, newinfo) < 0:
-                try:
-                    d = waitForDeferred(deleteResource(inbox, i[0]))
-                    yield d
-                    d.getResult()
-                    logging.info("[ITIP]: deleted iTIP message %s in Inbox that was older than the new one." % (i[0],))
-                except:
-                    log.err("Error while auto-processing iTIP: %s" % (failure.Failure(),))
-                    raise iTipException
-            else:
-                # For any that are newer or the same, mark the new one to be deleted.
-                delete_child = True
+        d = waitForDeferred(processOthersInInbox(info, newinfo, inbox, child))
+        yield d
+        delete_child = d.getResult()
 
-        # Delete the new one if so marked.
         if delete_child:
-            try:
-                d = waitForDeferred(deleteResource(inbox, child.fp.basename()))
-                yield d
-                d.getResult()
-                logging.info("[ITIP]: deleted new iTIP message %s in Inbox because it was older than existing ones." % (child.fp.basename(),))
-            except:
-                log.err("Error while auto-processing iTIP: %s" % (failure.Failure(),))
-                raise iTipException
             yield None
             return
 
         # Next we want to try and find a match to any components on existing calendars listed as contributing
         # to free-busy as we will need to update those with the new one.
+        d = waitForDeferred(findCalendarMatch(request, principal, calendar))
+        yield d
+        calmatch, updatecal, calURL = d.getResult()
         
-        # Find the current recipients calendar-free-busy-set
-        fbset = waitForDeferred(principal.calendarFreeBusyURIs(request))
-        yield fbset
-        fbset = fbset.getResult()
-
-        # Find the first calendar in the list with a component matching the one we are processing
-        calmatch = None
-        for calURL in fbset:
-            updatecal = waitForDeferred(request.locateResource(calURL))
-            yield updatecal
-            updatecal = updatecal.getResult()
-            if updatecal is None or not updatecal.exists() or not isCalendarCollectionResource(updatecal):
-                # We will ignore missing calendars. If the recipient has failed to
-                # properly manage the free busy set that should not prevent us from working.
-                continue
-            calmatch = matchComponentInCalendar(updatecal, calendar, None)
-            if calmatch:
-                logging.info("[ITIP]: found calendar component %s matching new iTIP message in %s." % (calmatch[0], calURL))
-                break
-        
         # 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[0])
-            info = getSyncInfo(calmatch[0], cal)
+            cal = updatecal.iCalendar(calmatch)
+            info = getSyncInfo(calmatch, cal)
             if compareSyncInfo(info, newinfo) < 0:
                 # Delete existing resource which has been cancelled
                 try:
-                    d = waitForDeferred(deleteResource(updatecal, calmatch[0],))
+                    d = waitForDeferred(deleteResource(updatecal, calmatch,))
                     yield d
                     d.getResult()
-                    logging.info("[ITIP]: delete calendar component %s in %s as it was cancelled." % (calmatch[0], calURL))
+                    logging.info("[ITIP]: delete calendar component %s in %s as it was cancelled." % (calmatch, calURL))
                 except:
                     log.err("Error while auto-processing iTIP: %s" % (failure.Failure(),))
                     raise iTipException
+                processed = "processed"
             else:
-                # Delete new one in Inbox as it is old
-                try:
-                    d = waitForDeferred(deleteResource(inbox, child.fp.basename()))
+                processed = "older"
+        else:
+            # Nothing to do except delete the inbox item as we have nothing to cancel.
+            processed = "ignored"
+    else:
+        # Try and find a match to any components on existing calendars listed as contributing
+        # to free-busy as we will need to update those with the new one.
+        d = waitForDeferred(findCalendarMatch(request, principal, calendar))
+        yield d
+        calmatch, updatecal, calURL = d.getResult()
+        
+        # If we have a match then we need to check whether we are updating etc
+        if calmatch:
+            # iTIP CANCEL can contain multiple components being cancelled in the RECURRENCE-ID case.
+            # So we need to iterate over each iTIP component.
+
+            # Get the existing calendar object
+            existing_calendar = updatecal.iCalendar(calmatch)
+            existing_master = existing_calendar.masterComponent()
+            exdates = []
+            max_sequence = None
+            if existing_master:
+                max_sequence = existing_master.propertyValue("SEQUENCE")
+            if max_sequence is None:
+                max_sequence = 0
+
+            for component in calendar.subcomponents():
+                if component.name() == "VTIMEZONE":
+                    continue
+            
+                # Find matching component in existing calendar
+                old_component = 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 compareComponents(old_component, component) < 0:
+                        # Exclude the cancelled instance
+                        exdates.append(component.getRecurrenceIDUTC())
+                        max_sequence = max(max_sequence, component.propertyValue("SEQUENCE"))
+                        
+                        # 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())
+                        max_sequence = max(max_sequence, component.propertyValue("SEQUENCE"))
+
+            # 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))
+                    seq = existing_master.getProperty("SEQUENCE")
+                    if seq:
+                        seq.setValue(max_sequence)
+                    else:
+                        existing_master.addProperty(Property("SEQUENCE", max_sequence))
+
+                # See if there are still components in the calendar - we might have deleted the last overridden instance
+                # in which case the calendar object is empty (except for VTIMEZONEs).
+                if existing_calendar.mainType() is None:
+                    # Delete the now empty calendar object
+                    d = waitForDeferred(deleteResource(updatecal, calmatch))
                     yield d
                     d.getResult()
-                    logging.info("[ITIP]: deleted new iTIP message %s in Inbox because it was older than %s in %s." % (child.fp.basename(), calmatch[0], calURL))
-                except:
-                    log.err("Error while auto-processing iTIP: %s" % (failure.Failure(),))
-                    raise iTipException
-                yield None
-                return
+                    logging.info("[ITIP]: deleted calendar component %s after cancellations from iTIP message in %s." % (calmatch, calURL))
+                else:
+                    # Update the existing calendar object
+                    newchild = waitForDeferred(writeResource(request, calURL, updatecal, calmatch, existing_calendar))
+                    yield newchild
+                    newchild = newchild.getResult()
+                    logging.info("[ITIP]: updated calendar component %s with cancellations from iTIP message in %s." % (calmatch, calURL))
+                processed = "processed"
+            else:
+                processed = "older"
         else:
-            # Nothing to do
-            pass
-        
-        # Remove the now processed incoming request.
-        try:
-            d = waitForDeferred(deleteResource(inbox, child.fp.basename()))
-            yield d
-            d.getResult()
-            logging.info("[ITIP]: deleted new iTIP message %s in Inbox because it has been processed." % (child.fp.basename(),))
-        except:
-            log.err("Error while auto-processing iTIP: %s" % (failure.Failure(),))
-            raise iTipException
-        yield None
-        return
-    else:
-        raise NotImplementedError
+            # Nothing to do except delete the inbox item as we have nothing to cancel.
+            processed = "ignored"
 
+    # Remove the now processed incoming request.
+    try:
+        d = waitForDeferred(deleteResource(inbox, child.fp.basename()))
+        yield d
+        d.getResult()
+        logging.info("[ITIP]: deleted new iTIP message %s in Inbox because it has been %s." %
+            (child.fp.basename(),
+             {"processed":"processed",
+              "older":    "ignored: older",
+              "ignored":  "ignored: no match"}[processed],))
+    except:
+        log.err("Error while auto-processing iTIP: %s" % (failure.Failure(),))
+        raise iTipException
+    yield None
+    return
+
 processCancel = deferredGenerator(processCancel)
 
 def checkForReply(request, principal, calendar):
@@ -438,6 +479,9 @@
     RSVP=TRUE. A reply will either be positive (accepted
     invitation) or negative (denied invitation). In addition we will modify calendar to reflect
     any new state (e.g. remove RSVP, set PARTSTAT to ACCEPTED or DECLINED).
+    
+    BTW The incoming iTIP message may contain multiple components so we need to iterate over all those.
+    At the moment we will treat a failure on one isntances as a DECLINE of the entire set.
 
     @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.
@@ -455,7 +499,7 @@
     
     # Extract UID from primary component as we want to ignore this one if we match it
     # in any calendars.
-    comp = calendar.mainComponent()
+    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
@@ -495,16 +539,17 @@
      
     # Extract the ATTENDEE property matching current recipient from the calendar data
     cuas = principal.calendarUserAddresses()
-    attendeeProp = calendar.getAttendeeProperty(cuas)
-    if attendeeProp is None:
+    attendeeProps = calendar.getAttendeeProperties(cuas)
+    if not attendeeProps:
         yield False, None, accepted
         return
 
     # Look for specific parameters
     rsvp = False
-    if "RSVP" in attendeeProp.params():
-        if attendeeProp.params()["RSVP"][0] == "TRUE":
-            rsvp = True
+    for attendeeProp in attendeeProps:
+        if "RSVP" in attendeeProp.params():
+            if attendeeProp.params()["RSVP"][0] == "TRUE":
+                rsvp = True
     
             # Now modify the original component
             del attendeeProp.params()["RSVP"]
@@ -513,10 +558,11 @@
         partstat = "ACCEPTED"
     else:
         partstat = "DECLINED"
-    if "PARTSTAT" in attendeeProp.params():
-        attendeeProp.params()["PARTSTAT"][0] = partstat
-    else:
-        attendeeProp.params()["PARTSTAT"] = [partstat]
+    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
     
@@ -530,16 +576,21 @@
     replycal.getProperty("PRODID").setValue(iCalendarProductID)
     
     # Add REQUEST-STATUS
-    if accepted:
-        replycal.mainComponent().addProperty(Property(name="REQUEST-STATUS", value="2.0; Success."))
-    else:
-        replycal.mainComponent().addProperty(Property(name="REQUEST-STATUS", value="4.0; Event conflict. Date/time is busy."))
+    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."))
 
     # Remove all attendees other than ourselves
-    attendees = tuple(replycal.mainComponent().properties("ATTENDEE"))
-    for attendee in attendees:
-        if (attendee.value() != attendeeProp.value()):
-            replycal.mainComponent().removeProperty(attendee)
+    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()):
+                replycal.mainComponent().removeProperty(attendee)
 
     yield rsvp, replycal, accepted
 
@@ -611,7 +662,8 @@
     itipper = True
     if collection.isCalendarCollection():
         method = calendar.getProperty("METHOD")
-        calendar.removeProperty(method)
+        if method:
+            calendar.removeProperty(method)
         itipper = False
     
     # Now write it to the resource
@@ -701,35 +753,162 @@
     
     return True
 
-def matchComponentInCalendar(collection, calendar, ignore):
+def processOthersInInbox(info, newinfo, inbox, child):
+    # Compare the new one with each existing one.
+    delete_child = False
+    for i in info:
+        # For any that are older, delete them.
+        if compareSyncInfo(i, newinfo) < 0:
+            try:
+                d = waitForDeferred(deleteResource(inbox, i[0]))
+                yield d
+                d.getResult()
+                logging.info("[ITIP]: deleted iTIP message %s in Inbox that was older than the new one." % (i[0],))
+            except:
+                log.err("Error while auto-processing iTIP: %s" % (failure.Failure(),))
+                raise iTipException
+        else:
+            # For any that are newer or the same, mark the new one to be deleted.
+            delete_child = True
+
+    # Delete the new one if so marked.
+    if delete_child:
+        try:
+            d = waitForDeferred(deleteResource(inbox, child.fp.basename()))
+            yield d
+            d.getResult()
+            logging.info("[ITIP]: deleted new iTIP message %s in Inbox because it was older than existing ones." % (child.fp.basename(),))
+        except:
+            log.err("Error while auto-processing iTIP: %s" % (failure.Failure(),))
+            raise iTipException
+    
+    yield delete_child
+
+processOthersInInbox = deferredGenerator(processOthersInInbox)    
+
+def findCalendarMatch(request, principal, calendar):
+    # Try and find a match to any components on existing calendars listed as contributing
+    # to free-busy as we will need to update those with the new one.
+    
+    # Find the current recipients calendar-free-busy-set
+    fbset = waitForDeferred(principal.calendarFreeBusyURIs(request))
+    yield fbset
+    fbset = fbset.getResult()
+
+    # Find the first calendar in the list with a component matching the one we are processing
+    calmatch = None
+    updatecal = None
+    calURL = None
+    for calURL in fbset:
+        updatecal = waitForDeferred(request.locateResource(calURL))
+        yield updatecal
+        updatecal = updatecal.getResult()
+        if updatecal is None or not updatecal.exists() or not isCalendarCollectionResource(updatecal):
+            # We will ignore missing calendars. If the recipient has failed to
+            # properly manage the free busy set that should not prevent us from working.
+            continue
+        calmatch = matchComponentInCalendar(updatecal, calendar)
+        if calmatch:
+            logging.info("[ITIP]: found calendar component %s matching new iTIP message in %s." % (calmatch, calURL))
+            break
+    
+    if calmatch is None and len(fbset):
+        calURL = fbset[0]
+        updatecal = waitForDeferred(request.locateResource(calURL))
+        yield updatecal
+        updatecal = updatecal.getResult()
+
+    yield calmatch, updatecal, calURL
+
+findCalendarMatch = deferredGenerator(findCalendarMatch)    
+
+def matchComponentInCalendar(collection, calendar):
     """
     See if the component in the provided iTIP calendar object matches any in the specified calendar
-    collectrion, excluding the resource provided.
+    collection.
     
     @param collection: L{CalDAVFile} for the calendar collection to examine.
     @param calendar: L{Component} for calendar to examine.
-    @param ignore: L{CalDAVFile} to ignore if found, or C{None} if none to ignore.
     @return: C{list} of resource names found.
     """
 
-    result = []
     try:
-        # Extract UID from primary component
-        comp = calendar.mainComponent()
+        # 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)
         
-        # Remove the one we want to ignore
-        if ignore is not None:
-            result = [name for name in result if name != ignore.fp.basename()]
+        # There can be only one
+        if len(result) > 0: 
+            return result[0]
+        else:
+            return None
     except ValueError:
-        return []
+        return None
+
+def findMatchingComponent(component, calendar):
+    """
+    See if any overridden component in the provided iTIP calendar object matches the specified component.
     
-    return result
+    @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(newcal, oldcal):
+    """
+    Merge the overridden instance components in newcal into old cal replacing any
+    matching components there.
+
+    @param newcal: the new overridden instances to use.
+    @type newcal: L{Component}
+    @param oldcal: the component to merge into.
+    @type oldcal: L{Component}
+    """
+    
+    # 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.
+    existing_master = oldcal.masterComponent()
+    max_sequence = None
+    if existing_master:
+        max_sequence = existing_master.propertyValue("SEQUENCE")
+    if max_sequence is None:
+        max_sequence = 0
+
+    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)
+        max_sequence = max(max_sequence, component.propertyValue("SEQUENCE"))
+
+    if existing_master:
+        seq = existing_master.getProperty("SEQUENCE")
+        if seq:
+            seq.setValue(max_sequence)
+        else:
+            existing_master.addProperty(Property("SEQUENCE", max_sequence))
+
 def getAllInfo(collection, calendar, ignore):
     """
     Find each component in the calendar collection that has a matching UID with
@@ -744,8 +923,9 @@
     """
     names = []
     try:
-        # Extract UID from primary component
-        comp = calendar.mainComponent()
+        # 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
@@ -774,18 +954,50 @@
     @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 primary component
-        comp = calendar.mainComponent()
-        uid = comp.propertyValue("UID")
-        seq = comp.propertyValue("SEQUENCE")
-        dtstamp = comp.propertyValue("DTSTAMP")
-        rid = comp.propertyValue("RECURRENCE-ID")
+        # 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)
         
     except ValueError:
         return (name, None, None, None, None)
     
     return (name, uid, seq, dtstamp, rid)
 
+def getComponentSyncInfo(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")
+        dtstamp = component.propertyValue("DTSTAMP")
+        rid = component.propertyValue("RECURRENCE-ID")
+        
+    except ValueError:
+        return (None, None, None, None)
+    
+    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: 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.
@@ -820,50 +1032,3 @@
         return -1
 
     return 0
-
-def updating(collection, names, calendar):
-    """
-    Check whether the specified calendar object is an iTIP message that is "newer" than the
-    others listed, or does not match the component type listed.
-    
-    @param collection: L{CalDAVFile} for the calendar collection to examine.
-    @param names: C{list} of C{str} for names of resources in the collection to check against.
-    @param calendar: L{Component} for calendar to check.
-    @return: C{True} if new component is an update and valid, C{False} otherwise.
-    """
-    
-    # First get useful sync-related info from existing component
-    uid, seq, dtstamp, rid = getSyncInfo(calendar)
-    
-    # Now get info from each named component and compare
-    for name in names:
-        cal = collection.iCalendar(name)
-        cuid, cseq, cdtstamp, crid = getSyncInfo(cal)
-        
-        # UIDs MUST match
-        assert uid == cuid
-        
-        # Look for sequence
-        if (cseq is not None) and (seq is not None):
-            if cseq > seq:
-                return False
-            if cseq < seq:
-                continue
-        elif (cseq is not None) and (seq is None):
-            return False
-        elif (cseq is None) and (seq is not None):
-            continue
-
-        # Look for DTSTAMP
-        if (cdtstamp is not None) and (dtstamp is not None):
-            if cdtstamp > dtstamp:
-                return False
-            if cdtstamp < dtstamp:
-                continue
-        elif (cdtstamp is not None) and (dtstamp is None):
-            return False
-        elif (cdtstamp is None) and (dtstamp is not None):
-            continue
-        
-    return True
-            

-------------- next part --------------
An HTML attachment was scrubbed...
URL: http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20070925/f5358b44/attachment.html


More information about the calendarserver-changes mailing list