[CalendarServer-changes] [3811] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Wed Mar 11 07:59:16 PDT 2009


Revision: 3811
          http://trac.macosforge.org/projects/calendarserver/changeset/3811
Author:   cdaboo at apple.com
Date:     2009-03-11 07:59:15 -0700 (Wed, 11 Mar 2009)
Log Message:
-----------
Improved attendee PUT merging behavior to treat the server data as the primary state and only merge
per-Attendee properties from the PUT data into that (whilst checking for invalid changes to only
date-time related properties). This should eliminate bogus valid-attendee-change errors.

Modified Paths:
--------------
    CalendarServer/trunk/run
    CalendarServer/trunk/twistedcaldav/ical.py
    CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py
    CalendarServer/trunk/twistedcaldav/scheduling/implicit.py
    CalendarServer/trunk/twistedcaldav/scheduling/itip.py
    CalendarServer/trunk/twistedcaldav/scheduling/test/test_icaldiff.py

Modified: CalendarServer/trunk/run
===================================================================
--- CalendarServer/trunk/run	2009-03-11 14:52:12 UTC (rev 3810)
+++ CalendarServer/trunk/run	2009-03-11 14:59:15 UTC (rev 3811)
@@ -707,7 +707,7 @@
 
 caldavtester="${top}/CalDAVTester";
 
-svn_get "CalDAVTester" "${caldavtester}" "${svn_uri_base}/CalDAVTester/trunk" 3795;
+svn_get "CalDAVTester" "${caldavtester}" "${svn_uri_base}/CalDAVTester/trunk" 3810;
 
 #
 # PyFlakes

Modified: CalendarServer/trunk/twistedcaldav/ical.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/ical.py	2009-03-11 14:52:12 UTC (rev 3810)
+++ CalendarServer/trunk/twistedcaldav/ical.py	2009-03-11 14:59:15 UTC (rev 3811)
@@ -926,13 +926,19 @@
                     return True
         return False
         
-    def deriveInstance(self, rid):
+    def deriveInstance(self, rid, allowCancelled=False):
         """
         Derive an instance from the master component that has the provided RECURRENCE-ID, but
-        with all other properties, components etc from the master.
+        with all other properties, components etc from the master. If the requested override is
+        currently marked as an EXDATE in the existing master, allow an option whereby the override
+        is added as STATUS:CANCELLED and the EXDATE removed.
 
         @param rid: recurrence-id value
         @type rid: L{datetime.datetime}
+        @param allowCancelled: whether to allow a STATUS:CANCELLED override
+        @type allowCancelled: C{bool}
+        
+        @return: L{Component} for newly derived instance, or None if not valid override
         """
         
         # Must have a master component
@@ -942,16 +948,26 @@
 
         # TODO: Check that the recurrence-id is a valid instance
         # For now we just check that there is no matching EXDATE
-        for exdate in self.properties("EXDATE"):
-            if exdate == rid:
-                return None
+        didCancel = False
+        for exdate in tuple(master.properties("EXDATE")):
+            for exdateValue in exdate.value():
+                if exdateValue == rid:
+                    if allowCancelled:
+                        exdate.value().remove(exdateValue)
+                        if len(exdate.value()) == 0:
+                            master.removeProperty(exdate)
+                        didCancel = True
+                        break
+                    else:
+                        # Cannot derive from an existing EXDATE
+                        return None
         
         # Create the derived instance
         newcomp = master.duplicate()
 
         # Strip out unwanted recurrence properties
         for property in tuple(newcomp.properties()):
-            if property.name() in ["RRULE", "RDATE", "EXRULE", "EXDATE", "RECURRENCE-ID"]:
+            if property.name() in ("RRULE", "RDATE", "EXRULE", "EXDATE", "RECURRENCE-ID",):
                 newcomp.removeProperty(property)
         
         # New DTSTART is the RECURRENCE-ID we are deriving but adjusted to the
@@ -974,7 +990,10 @@
         except KeyError:
             rid_params = {}
         newcomp.addProperty(Property("RECURRENCE-ID", dtstart.value(), params=rid_params))
-            
+        
+        if didCancel:
+            newcomp.addProperty(Property("STATUS", "CANCELLED"))
+
         return newcomp
         
     def resourceUID(self):

Modified: CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py	2009-03-11 14:52:12 UTC (rev 3810)
+++ CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py	2009-03-11 14:59:15 UTC (rev 3811)
@@ -14,7 +14,8 @@
 # limitations under the License.
 ##
 
-from twistedcaldav.dateops import normalizeToUTC, toString
+from twistedcaldav.dateops import normalizeToUTC, toString,\
+    normalizeStartEndDuration
 from twistedcaldav.ical import Component, Property
 from twistedcaldav.log import Logger
 from twistedcaldav.scheduling.cuaddress import normalizeCUAddr
@@ -220,80 +221,305 @@
         else:
             new_property.params()[parameter] = paramvalue
 
-    def attendeeDiff(self, attendee):
+    def attendeeMerge(self, attendee):
         """
         Merge the ATTENDEE specific changes with the organizer's view of the attendee's event.
         This will remove any attempt by the attendee to change things like the time or location.
        
         @param attendee: the value of the ATTENDEE property corresponding to the attendee making the change
         @type attendee: C{str}
+        
+        @return: C{tuple} of:
+            C{bool} - change is allowed
+            C{bool} - iTIP reply needs to be sent
+            C{list} - list of RECURRENCE-IDs changed
+            L{Component} - new calendar object to store
         """
         
         self.attendee = normalizeCUAddr(attendee)
 
-        # If smart merge is needed we have to do this before trying the diff
-        if self.smart_merge:
-            log.debug("attendeeDiff: doing smart Attendee diff/merge")
-            self._attendeeMerge()
+        self.newCalendar = self.calendar1.duplicate()
+        self.newMaster = self.newCalendar.masterComponent()
 
-        # Do straight comparison without alarms
-        self.originalCalendar1 = self.calendar1
-        self.originalCalendar2 = self.calendar2
-        self.calendar1 = self._attendeeDuplicateAndNormalize(self.calendar1)
-        self.calendar2 = self._attendeeDuplicateAndNormalize(self.calendar2)
+        changeCausesReply = False
+        changedRids = []
+        
+        # First get uid/rid map of components
+        def mapComponents(calendar):
+            map = {}
+            cancelledRids = set()
+            master = None
+            for component in calendar.subcomponents():
+                if component.name() == "VTIMEZONE":
+                    continue
+                name = component.name()
+                uid = component.propertyValue("UID")
+                rid = component.getRecurrenceIDUTC()
+                map[(name, uid, rid,)] = component
+                if component.propertyValue("STATUS") == "CANCELLED" and rid is not None:
+                    cancelledRids.add(rid)
+                if rid is None:
+                    master = component
+            
+            # Normalize each master by adding any STATUS:CANCELLED components as EXDATEs
+            exdates = set()
+            if master:
+                # Get all EXDATEs in UTC
+                for exdate in master.properties("EXDATE"):
+                    exdates.update([normalizeToUTC(value) for value in exdate.value()])
+               
+            return exdates, map
+        
+        exdates1, map1 = mapComponents(self.calendar1)
+        set1 = set(map1.keys())
+        exdates2, map2 = mapComponents(self.calendar2)
+        set2 = set(map2.keys())
 
-        if self.calendar1 == self.calendar2:
-            return True, True
+        # All the components in calendar1 must be in calendar2 unless they are CANCELLED
+        result = set1 - set2
+        for key in result:
+            _ignore_name, _ignore_uid, rid = key
+            component = map1[key]
+            if component.propertyValue("STATUS") != "CANCELLED":
+                # Attendee may decline by EXDATE'ing an instance - we need to handle that
+                if rid in exdates2:
+                    # Mark Attendee as DECLINED in the server instance
+                    if self._attendeeDecline(self.newCalendar.overriddenComponent(rid)):
+                        changeCausesReply = True
+                        changedRids.append(rid)
+                else:
+                    log.debug("attendeeMerge: Missing uncancelled component from first calendar: %s" % (key,))
+                    return False, False, (), None
+            else: 
+                if rid not in exdates2:
+                    log.debug("attendeeMerge: Missing EXDATE for cancelled components from first calendar: %s" % (key,))
+                    return False, False, (), None
+                else:
+                    # Remove the CANCELLED component from the new calendar and add an EXDATE
+                    overridden = self.newCalendar.overriddenComponent(rid)
+                    self.newCalendar.removeComponent(overridden)
+                    if self.newMaster:
+                        self.newMaster.addProperty(Property("EXDATE", (rid,)))
+        
+        # Derive a new component in the new calendar for each new one in set2
+        for key in set2 - set1:
+            
+            # First check if the attendee's copy is cancelled and properly EXDATE'd
+            # and skip it if so.
+            _ignore_name, _ignore_uid, rid = key
+            component2 = map2[key]
+            if component2.propertyValue("STATUS") == "CANCELLED":
+                if rid not in exdates1:
+                    log.debug("attendeeMerge: Cancelled component not found in first calendar (or no EXDATE): %s" % (key,))
+                    return False, False, (), None
+                else:
+                    # Derive new component with STATUS:CANCELLED and remove EXDATE
+                    newOverride = self.newCalendar.deriveInstance(rid, allowCancelled=True)
+                    if newOverride is None:
+                        log.debug("attendeeMerge: Could not derive instance for cancelled component: %s" % (key,))
+                        return False, False, (), None
+                    self.newCalendar.addComponent(newOverride)
+            else:
+                # Derive new component
+                newOverride = self.newCalendar.deriveInstance(rid)
+                if newOverride is None:
+                    log.debug("attendeeMerge: Could not derive instance for uncancelled component: %s" % (key,))
+                    return False, False, (), None
+                self.newCalendar.addComponent(newOverride)
 
-        # Need to look at each component and do special comparisons
+        # So now newCalendar has all the same components as set2. Check changes and do transfers.
         
         # Make sure the same VCALENDAR properties match
-        if not self._checkVCALENDARProperties():
-            self._logDiffError("attendeeDiff: VCALENDAR properties do not match")
+        if not self._checkVCALENDARProperties(self.newCalendar, self.calendar2):
+            self._logDiffError("attendeeMerge: VCALENDAR properties do not match")
+            return False, False, (), None
+
+        # Now we transfer per-Attendee
+        # data from calendar2 into newCalendar to sync up changes, whilst verifying that other
+        # key properties are unchanged
+        declines = []
+        for key in set2:
+            _ignore_name, _ignore_uid, rid = key
+            serverData = self.newCalendar.overriddenComponent(rid)
+            clientData = map2[key]
+            
+            allowed, reply = self._transferAttendeeData(serverData, clientData, declines)
+            if not allowed:
+                self._logDiffError("attendeeMerge: Mismatched calendar objects")
+                return False, False, (), None
+            changeCausesReply |= reply
+            if reply:
+                changedRids.append(rid)
+
+        # We need to derive instances for any declined using an EXDATE
+        for decline in sorted(declines):
+            overridden = self.newCalendar.overriddenComponent(decline)
+            if not overridden:
+                overridden = self.newCalendar.deriveInstance(decline)
+                if overridden:
+                    self.newCalendar.addComponent(overridden)
+                    if self._attendeeDecline(overridden):
+                        changeCausesReply = True
+                        changedRids.append(decline)
+                else:
+                    log.debug("Unable to override and instance to mark as DECLINED: %s" % (decline,))
+                    return False, False, (), None
+
+        return True, changeCausesReply, changedRids, self.newCalendar
+
+    def _checkVCALENDARProperties(self, serverData, clientData):
+
+        self._transferProperty("X-CALENDARSERVER-ACCESS", serverData, clientData)
+
+        # Get property differences in the VCALENDAR objects
+        propdiff = set(serverData.properties()) ^ set(clientData.properties())
+        
+        # Ignore certain properties
+        ignored = ("PRODID", "CALSCALE",)
+        propdiff = set([prop for prop in propdiff if prop.name() not in ignored])
+        
+        result = len(propdiff) == 0
+        if not result:
+            log.debug("VCALENDAR properties differ: %s" % (propdiff,))
+        return result
+
+    def _transferAttendeeData(self, serverComponent, clientComponent, declines):
+        
+        # First check validity of date-time related properties
+        if not self._checkInvalidChanges(serverComponent, clientComponent, declines):
             return False, False
         
-        # Make sure the same VTIMEZONE components appear
-        tzidRemapping = False
-        if not self._compareVTIMEZONEs():
-            # Not an error any more. Instead we need to merge back the original TZIDs
-            # into the event being written.
-            tzidRemapping = True
+        # Now look for items to transfer from one to the other.
+        # We care about the ATTENDEE's PARTSTAT, TRANSP, VALARMS, X-APPLE-NEEDS-REPLY,
+        # DTSTAMP, LAST-MODIFIED
         
-        # Compare each component instance from the new calendar with each derived
-        # component instance from the old one
-        result = self._compareComponents()
-        if not result[0]:
-            self._logDiffError("attendeeDiff: Mismatched calendar objects")
-        else:
-            # May need to do some rewriting
-            if tzidRemapping:
+        replyNeeded = False
+
+        # ATTENDEE/PARTSTAT/RSVP
+        serverAttendee = serverComponent.getAttendeeProperty((self.attendee,))
+        clientAttendee = clientComponent.getAttendeeProperty((self.attendee,))
+        if serverAttendee.params().get("PARTSTAT", ("NEEDS-ACTION",))[0] != clientAttendee.params().get("PARTSTAT", ("NEEDS-ACTION",))[0]:
+            serverAttendee.params()["PARTSTAT"] = clientAttendee.params().get("PARTSTAT", "NEEDS-ACTION")
+            replyNeeded = True
+        if serverAttendee.params().get("RSVP", ("FALSE",))[0] != clientAttendee.params().get("RSVP", ("FALSE",))[0]:
+            if clientAttendee.params().get("RSVP", ("FALSE",))[0] == "FALSE":
                 try:
-                    self._remapTZIDs()
-                    self._logDiffError("attendeeDiff: VTIMEZONEs re-mapped")
-                except ValueError, e:
-                    self._logDiffError("attendeeDiff: VTIMEZONE re-mapping failed: %s" % (str(e),))
-                    return False, False
+                    del serverAttendee.params()["RSVP"]
+                except KeyError:
+                    pass
+            else:
+                serverAttendee.params()["RSVP"] = ["TRUE",]
 
-        return result
-    
-    def _attendeeDuplicateAndNormalize(self, calendar):
-        calendar = calendar.duplicate()
-        calendar.normalizePropertyValueLists("EXDATE")
-        calendar.removePropertyParameters("ORGANIZER", ("SCHEDULE-STATUS",))
-        calendar.normalizeAll()
-        calendar.normalizeAttachments()
-        iTipGenerator.prepareSchedulingMessage(calendar, reply=True)
-        return calendar
+        # Transfer these properties from the client data
+        replyNeeded |= self._transferProperty("X-CALENDARSERVER-PRIVATE-COMMENT", serverComponent, clientComponent)
+        self._transferProperty("TRANSP", serverComponent, clientComponent)
+        self._transferProperty("DTSTAMP", serverComponent, clientComponent)
+        self._transferProperty("LAST-MODIFIED", serverComponent, clientComponent)
+        self._transferProperty("X-APPLE-NEEDS-REPLY", serverComponent, clientComponent)
+        
+        # Handle VALARMs
+        serverComponent.removeAlarms()
+        for comp in clientComponent.subcomponents():
+            if comp.name() == "VALARM":
+                serverComponent.addComponent(comp)
+        
+        return True, replyNeeded
+        
+    def _checkInvalidChanges(self, serverComponent, clientComponent, declines):
+        
+        # Properties we care about: DTSTART, DTEND, DURATION, RRULE, RDATE, EXDATE
+        
+        serverProps = self._getNormalizedDateTimeProperties(serverComponent)
+        clientProps = self._getNormalizedDateTimeProperties(clientComponent)
+        
+        # Need to special case EXDATEs as an Attendee can effectively DECLINE by adding an EXDATE
+        if serverProps[:-1] != clientProps[:-1]:
+            invalidChanges = []
+            propNames = ("DTSTART", "DTEND", "RRULE", "RDATE", "EXDATE")
+            invalidChanges = [propName for ctr, propName in enumerate(propNames) if serverProps[ctr] != clientProps[ctr]]
+            log.debug("Critical properties do not match: %s" % (", ".join(invalidChanges),))
+            return False
+        elif serverProps[-1] != clientProps[-1]:
+            # Bad if EXDATEs have been removed
+            missing = serverProps[-1] - clientProps[-1]
+            if missing:
+                log.debug("EXDATEs missing: %s" % (", ".join([toString(exdate) for exdate in missing]),))
+                return False
+            declines.extend(clientProps[-1] - serverProps[-1])
+            return True
+        else:
+            return True
+        
+    def _getNormalizedDateTimeProperties(self, component):
+        
+        # Basic time properties
+        dtstart = component.getProperty("DTSTART")
+        dtend = component.getProperty("DTEND")
+        duration = component.getProperty("DURATION")
+        
+        newdtstart, newdtend = normalizeStartEndDuration(
+            dtstart.value(),
+            dtend.value() if dtend is not None else None,
+            duration.value() if duration is not None else None,
+        )
+        
+        # Recurrence rules - we need to normalize the order of the value parts
+        newrrules = set()
+        rrules = component.properties("RRULE")
+        for rrule in rrules:
+            indexedTokens = {}
+            indexedTokens.update([valuePart.split("=") for valuePart in rrule.value().split(";")])
+            sortedValue = ";".join(["%s=%s" % (key, value,) for key, value in sorted(indexedTokens.iteritems(), key=lambda x:x[0])])
+            newrrules.add(sortedValue)
+        
+        # RDATEs
+        newrdates = set()
+        rdates = component.properties("RDATE")
+        for rdate in rdates:
+            newrdates.update([normalizeToUTC(value) for value in rdate.value()])
+        
+        # EXDATEs
+        newexdates = set()
+        exdates = component.properties("EXDATE")
+        for exdate in exdates:
+            newexdates.update([normalizeToUTC(value) for value in exdate.value()])
 
-    def _attendeeMerge(self):
+        return newdtstart, newdtend, newrrules, newrdates, newexdates
+
+    def _transferProperty(self, propName, serverComponent, clientComponent):
+
+        changed = False
+        serverProp = serverComponent.getProperty(propName)
+        clientProp = clientComponent.getProperty(propName)
+        if serverProp != clientProp:
+            if clientProp:
+                serverComponent.replaceProperty(Property(propName, clientProp.value()))
+            else:
+                serverComponent.removeProperty(serverProp)
+            changed = True
+        return changed
+
+
+    def _attendeeDecline(self, component):
         """
-        Merge changes to ATTENDEE properties in calendar1 into calendar2.
+        Marke attendee as DECLINED in the component.
+
+        @param component:
+        @type component:
         
-        NB At this point we are going to assume that the changes in calendar1 are only
-        other ATTENDEE PARTSTAT changes as this method should only get called when
-        If-Schedule-Tag-Match is present and does not generate an error for an Attendee.
+        @return: C{bool} indicating whether the PARTSTAT value was in fact changed
         """
-        self._doSmartMerge(self.attendee, False)
+        attendee = component.getAttendeeProperty((self.attendee,))
+        partstatChanged = attendee.params().get("PARTSTAT", ("NEEDS-ACTION",))[0] != "DECLINED"
+        attendee.params()["PARTSTAT"] = ["DECLINED",]
+        try:
+            del attendee.params()["RSVP"]
+        except KeyError:
+            pass
+        prop = component.getProperty("X-APPLE-NEEDS-REPLY")
+        if prop:
+            component.removeProperty(prop)
+        return partstatChanged
 
     def whatIsDifferent(self):
         """
@@ -351,281 +577,15 @@
             rids = None
         return props_changed, rids
 
-    def _checkVCALENDARProperties(self):
+    def _attendeeDuplicateAndNormalize(self, calendar):
+        calendar = calendar.duplicate()
+        calendar.normalizePropertyValueLists("EXDATE")
+        calendar.removePropertyParameters("ORGANIZER", ("SCHEDULE-STATUS",))
+        calendar.normalizeAll()
+        calendar.normalizeAttachments()
+        iTipGenerator.prepareSchedulingMessage(calendar, reply=True)
+        return calendar
 
-        # Get property differences in the VCALENDAR objects
-        propdiff = set(self.calendar1.properties()) ^ set(self.calendar2.properties())
-        
-        # Ignore certain properties
-        ignored = ("PRODID", "CALSCALE",)
-        propdiff = set([prop for prop in propdiff if prop.name() not in ignored])
-        
-        result = len(propdiff) == 0
-        if not result:
-            log.debug("VCALENDAR properties differ: %s" % (propdiff,))
-        return result
-
-    @staticmethod
-    def _extractTZIDs(calendar):
-
-        tzids = set()
-        for component in calendar.subcomponents():
-            if component.name() == "VTIMEZONE":
-                tzids.add(component.propertyValue("TZID"))
-        return tzids
-
-    def _compareVTIMEZONEs(self):
-
-        # FIXME: clients may re-write timezones so the best we can do is
-        # compare TZIDs. That is not ideal as a client could have an old version
-        # of a VTIMEZONE and thus could show events at different times than the
-        # organizer.
-        
-        tzids1 = self._extractTZIDs(self.calendar1)
-        tzids2 = self._extractTZIDs(self.calendar2)
-        result = tzids1 == tzids2
-        if not result:
-            log.debug("Different VTIMEZONES: %s %s" % (tzids1, tzids2))
-        return result
-
-    def _remapTZIDs(self):
-        """
-        Re-map TZIDs that changed between the existing calendar data and the new data
-        being written for the attendee.
-        """
-
-        # Do master component re-map first
-        old_master = self.originalCalendar1.masterComponent()
-        new_master = self.originalCalendar2.masterComponent()
-        self._remapTZIDsOnComponent(old_master, new_master)
-        
-        # Now do each corresponding overridden component
-        for newComponent in self.originalCalendar2.subcomponents():
-            
-            # Make sure we have an appropriate component
-            if newComponent.name() == "VTIMEZONE":
-                continue
-            rid = newComponent.getRecurrenceIDUTC()
-            if rid is None:
-                continue
-
-            # Find matching component in new calendar
-            oldComponent = self.originalCalendar1.overriddenComponent(rid)
-            if oldComponent is None:
-                # Derive a new instance from the new calendar and transfer attendee status
-                oldComponent = self.originalCalendar2.deriveInstance(rid)
-
-            if oldComponent:
-                self._remapTZIDsOnComponent(oldComponent, newComponent)
-        
-        
-        # Now manipulate the VTIMEZONE components in the calendar data
-        for newComponent in tuple(self.originalCalendar2.subcomponents()):
-            # Make sure we have an appropriate component
-            if newComponent.name() == "VTIMEZONE":
-                self.originalCalendar2.removeComponent(newComponent)
-                
-        # The following statement is required to force vobject to serialize the
-        # calendar data and in the process add any missing VTIMEZONEs as needed.
-        _ignore = str(self.originalCalendar2)
-        log.debug(_ignore)
-        
-    def _remapTZIDsOnComponent(self, oldComponent, newComponent):
-        """
-        Re-map TZIDs that changed between the existing calendar data and the new data
-        being written for the attendee.
-        """
-
-        # Look at each property that culd contain a TZID:
-        # DTSTART, DTEND, RDATE, EXDATE, RECURRENCE-ID, DUE.
-        # NB EXDATE/RDATE can occur multiple times - special case
-        checkPropertiesOneOff = (
-            "DTSTART",
-            "DTEND",
-            "RECURRENCE-ID",
-            "DUE",
-        )
-        checkPropertiesMultiple = (
-            "RDATE",
-            "EXDATE",
-        )
-        
-        for propName in checkPropertiesOneOff:
-            oldProp = oldComponent.getProperty(propName)
-            newProp = newComponent.getProperty(propName)
-            
-            # Special case behavior where DURATIOn is mapped to DTEND
-            if propName == "DTEND" and oldProp is None and newProp is not None:
-                oldProp = oldComponent.getProperty("DTSTART")
-
-            # Transfer tzinfo from old property value to the new one
-            if oldProp is not None and newProp is not None:
-                if "X-VOBJ-ORIGINAL-TZID" in oldProp.params():
-                    oldTZID = oldProp.paramValue("X-VOBJ-ORIGINAL-TZID")
-                    if "X-VOBJ-ORIGINAL-TZID" in newProp.params():
-                        newTZID = newProp.paramValue("X-VOBJ-ORIGINAL-TZID")
-                        
-                        if oldTZID != newTZID:
-                            newProp.params()["X-VOBJ-ORIGINAL-TZID"][0] = oldTZID
-                            newProp.setValue(newProp.value().replace(tzinfo=oldProp.value().tzinfo))
-                    else:
-                        raise ValueError("Cannot handle mismatched TZIDs on %s" % (propName,))
-                        
-        for propName in checkPropertiesMultiple:
-            oldProps = oldComponent.properties(propName)
-            newProps = newComponent.properties(propName)
-            oldTZID = None
-            oldTzinfo = None
-            for prop in oldProps:
-                if "X-VOBJ-ORIGINAL-TZID" in prop.params():
-                    if oldTZID and oldTZID != prop.paramValue("X-VOBJ-ORIGINAL-TZID"):
-                        raise ValueError("Cannot handle different TZIDs on multiple %s" % (propName,))
-                    else:
-                        oldTZID = prop.paramValue("X-VOBJ-ORIGINAL-TZID")
-                        oldTzinfo = prop.value()[0].tzinfo
-            for prop in newProps:
-                if "X-VOBJ-ORIGINAL-TZID" in prop.params():
-                    if oldTZID:
-                        prop.params()["X-VOBJ-ORIGINAL-TZID"][0] = oldTZID
-                        prop.setValue([item.replace(tzinfo=oldTzinfo) for item in prop.value()])
-                    else:
-                        raise ValueError("Cannot handle mismatched TZIDs on %s" % (propName,))
-                elif oldTZID:
-                    raise ValueError("Cannot handle mismatched TZIDs on %s" % (propName,))
-
-    def _compareComponents(self):
-        
-        # First get uid/rid map of components
-        def mapComponents(calendar):
-            map = {}
-            cancelledRids = set()
-            master = None
-            for component in calendar.subcomponents():
-                if component.name() == "VTIMEZONE":
-                    continue
-                name = component.name()
-                uid = component.propertyValue("UID")
-                rid = component.getRecurrenceIDUTC()
-                map[(name, uid, rid,)] = component
-                if component.propertyValue("STATUS") == "CANCELLED" and rid is not None:
-                    cancelledRids.add(rid)
-                if rid is None:
-                    master = component
-            
-            # Normalize each master by adding any STATUS:CANCELLED components as EXDATEs
-            exdates = set()
-            if master:
-                for rid in sorted(cancelledRids):
-                    master.addProperty(Property("EXDATE", [rid,]))
-                
-                # Get all EXDATEs in UTC
-                for exdate in master.properties("EXDATE"):
-                    exdates.update([normalizeToUTC(value) for value in exdate.value()])
-               
-            return exdates, map
-        
-        exdates1, map1 = mapComponents(self.calendar1)
-        set1 = set(map1.keys())
-        exdates2, map2 = mapComponents(self.calendar2)
-        set2 = set(map2.keys())
-
-        # All the components in calendar1 must be in calendar2 unless they are CANCELLED
-        result = set1 - set2
-        for key in result:
-            component = map1[key]
-            if component.propertyValue("STATUS") != "CANCELLED":
-                log.debug("Missing uncancelled component from first calendar: %s" % (key,))
-                return False, False
-            else: 
-                _ignore_name, _ignore_uid, rid = key
-                if rid not in exdates2:
-                    log.debug("Missing EXDATE for cancelled components from first calendar: %s" % (key,))
-                    return False, False
-                    
-
-        # Now verify that each component in set1 matches what is in set2
-        attendee_unchanged = True
-        for key, value in map1.iteritems():
-            component1 = value
-            component2 = map2.get(key)
-            if component2 is None:
-                continue
-
-            nomismatch, no_attendee_change = self._testComponents(component1, component2)
-            if not nomismatch:
-                return False, False
-            attendee_unchanged &= no_attendee_change
-        
-        # Now verify that each additional component in set2 matches a derived component in set1
-        for key in set2 - set1:
-            
-            # First check if the attendee's copy is cancelled and properly EXDATE'd
-            # and skip it if so.
-            component2 = map2[key]
-            if component2.propertyValue("STATUS") == "CANCELLED":
-                _ignore_name, _ignore_uid, rid = key
-                if rid not in exdates1:
-                    log.debug("Cancelled component not found in first calendar (or no EXDATE): %s" % (key,))
-                    return False, False
-                continue
-
-            # Now derive the organizer's expected instance and compare
-            component1 = self.calendar1.deriveInstance(key[2])
-            if component1 is None:
-                log.debug("_compareComponents: Could not derive instance: %s" % (key[2],))
-                return False, False
-            
-            nomismatch, no_attendee_change = self._testComponents(component1, component2)
-            if not nomismatch:
-                return False, False
-            attendee_unchanged &= no_attendee_change
-            
-        return True, attendee_unchanged
-
-    def _testComponents(self, comp1, comp2):
-        
-        assert isinstance(comp1, Component) and isinstance(comp2, Component)
-        
-        if comp1.name() != comp2.name():
-            log.debug("Component names are different: '%s' and '%s'" % (comp1.name(), comp2.name()))
-            return False, False
-        
-        # Only accept a change to this attendee's own ATTENDEE property
-        comp1.transformAllFromNative()
-        comp2.transformAllFromNative()
-        propdiff = set(comp1.properties()) ^ set(comp2.properties())
-        comp1.transformAllToNative()
-        comp2.transformAllToNative()
-        for prop in tuple(propdiff):
-            # These ones are OK to change
-            if prop.name() in (
-                "TRANSP",
-                "DTSTAMP",
-                "CREATED",
-                "LAST-MODIFIED",
-                "SEQUENCE",
-            ):
-                propdiff.remove(prop)
-                continue
-            
-            # These ones can change and trigger a reschedule
-            if ((prop.name() == "ATTENDEE" and prop.value() == self.attendee) or
-                prop.name() == "X-CALENDARSERVER-PRIVATE-COMMENT"):
-                continue
-
-            # Change that is not allowed
-            log.debug("Component properties are different (trigger is '%s'): %s" % (prop.name(), propdiff,))
-            return False, False
-
-        # Compare subcomponents.
-        # NB at this point we assume VALARMS have been removed.
-        result = set(comp1.subcomponents()) ^ set(comp2.subcomponents())
-        if result:
-            log.debug("Sub-components are different: %s" % (result,))
-            return False, False
-        
-        return True, len(propdiff) == 0
-
     def _diffComponents(self, comp1, comp2, changed, rids):
         
         assert isinstance(comp1, Component) and isinstance(comp2, Component)

Modified: CalendarServer/trunk/twistedcaldav/scheduling/implicit.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/implicit.py	2009-03-11 14:52:12 UTC (rev 3810)
+++ CalendarServer/trunk/twistedcaldav/scheduling/implicit.py	2009-03-11 14:59:15 UTC (rev 3811)
@@ -666,9 +666,11 @@
                     self.oldcalendar = None
 
                 # Determine whether the current change is allowed
-                change_allowed, no_itip = self.isAttendeeChangeInsignificant()
+                changeAllowed, doITipReply, changedRids, newCalendar = self.isAttendeeChangeInsignificant()
+                if changeAllowed:
+                    self.return_calendar = self.calendar = newCalendar
 
-                if not change_allowed:
+                if not changeAllowed:
                     if self.calendar.hasPropertyValueInAllComponents(Property("STATUS", "CANCELLED")):
                         log.debug("Attendee '%s' is creating CANCELLED event for mismatched UID: '%s' - removing entire event" % (self.attendee, self.uid,))
                         self.return_status = ImplicitScheduler.STATUS_ORPHANED_CANCELLED_EVENT
@@ -677,7 +679,7 @@
                         log.error("Attendee '%s' is not allowed to make an unauthorized change to an organized event: UID:%s" % (self.attendeePrincipal, self.uid,))
                         raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-attendee-change")))
 
-                if no_itip:
+                if not doITipReply:
                     log.debug("Implicit - attendee '%s' is updating UID: '%s' but change is not significant" % (self.attendee, self.uid))
                     returnValue(None)
             elif isinstance(self.organizerAddress, LocalCalendarUser):
@@ -747,7 +749,7 @@
             oldcalendar = self.organizer_calendar
             oldcalendar.attendeesView((self.attendee,))
         differ = iCalDiff(oldcalendar, self.calendar, self.do_smart_merge)
-        return differ.attendeeDiff(self.attendee)
+        return differ.attendeeMerge(self.attendee)
 
     def scheduleWithOrganizer(self):
 

Modified: CalendarServer/trunk/twistedcaldav/scheduling/itip.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/itip.py	2009-03-11 14:52:12 UTC (rev 3810)
+++ CalendarServer/trunk/twistedcaldav/scheduling/itip.py	2009-03-11 14:59:15 UTC (rev 3811)
@@ -124,18 +124,10 @@
                 if component.name() != "VTIMEZONE" and component.getRecurrenceIDUTC() is not None:
                     rid = component.getRecurrenceIDUTC()
                     if new_calendar.overriddenComponent(rid) is None:
-                        new_component = new_calendar.deriveInstance(rid)
+                        allowCancelled = component.propertyValue("STATUS") == "CANCELLED"
+                        new_component = new_calendar.deriveInstance(rid, allowCancelled=allowCancelled)
                         new_calendar.addComponent(new_component)
                         iTipProcessing.transferItems(calendar, master_valarms, private_comments, new_component)
-                        if component.propertyValue("STATUS") == "CANCELLED":
-                            new_component.replaceProperty(Property("STATUS", "CANCELLED"))
-                            for exdate in master_component.properties("EXDATE"):
-                                for value in exdate.value():
-                                    if value == rid:
-                                        exdate.value().remove(value)
-                                        if len(exdate.value()) == 0:
-                                            master_component.removeProperty(exdate)
-                                        break
             
             # Replace the entire object
             return new_calendar, props_changed, rids

Modified: CalendarServer/trunk/twistedcaldav/scheduling/test/test_icaldiff.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/test/test_icaldiff.py	2009-03-11 14:52:12 UTC (rev 3810)
+++ CalendarServer/trunk/twistedcaldav/scheduling/test/test_icaldiff.py	2009-03-11 14:59:15 UTC (rev 3811)
@@ -18,6 +18,7 @@
 from twistedcaldav.scheduling.icaldiff import iCalDiff
 import twistedcaldav.test.util
 from difflib import unified_diff
+from twistedcaldav.dateops import toString
 
 import itertools
 
@@ -474,7 +475,9 @@
             differ = iCalDiff(Component.fromString(calendar1), Component.fromString(calendar2), False)
             self.assertEqual(differ.organizerDiff(), result, msg=description)
 
-    def test_attendee_diff_simple(self):
+
+    def test_attendee_merge_simple(self):
+
         
         data = (
             (
@@ -506,7 +509,19 @@
 END:VCALENDAR
 """,
                 "mailto:user2 at example.com",
-                (True, True,)
+                (True, False, (), """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""")
             ),
             (
                 "#1.2 Simple component, PARTSTAT change",
@@ -537,7 +552,19 @@
 END:VCALENDAR
 """,
                 "mailto:user2 at example.com",
-                (True, False,)
+                (True, True, (None,), """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""")
             ),
             (
                 "#1.3 Simple component, bad change",
@@ -568,7 +595,7 @@
 END:VCALENDAR
 """,
                 "mailto:user2 at example.com",
-                (False, False,)
+                (False, False, (), None)
             ),
             (
                 "#1.4 Simple component, valarm change",
@@ -609,7 +636,24 @@
 END:VCALENDAR
 """,
                 "mailto:user2 at example.com",
-                (True, True,)
+                (True, False, (), """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test for Attendee
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""")
             ),
             (
                 "#1.5 Simple component, vcalendar props change ok",
@@ -651,7 +695,24 @@
 END:VCALENDAR
 """,
                 "mailto:user2 at example.com",
-                (True, True,)
+                (True, False, (), """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test for Attendee
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+END:VCALENDAR
+""")
             ),
             (
                 "#1.6 Simple component, vcalendar props change bad",
@@ -693,7 +754,7 @@
 END:VCALENDAR
 """,
                 "mailto:user2 at example.com",
-                (True, True,)
+                (False, False, (), None)
             ),
             (
                 "#1.7 Simple component, vtimezone no change",
@@ -760,7 +821,37 @@
 END:VCALENDAR
 """,
                 "mailto:user2 at example.com",
-                (True, True,)
+                (True, False, (), """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VTIMEZONE
+TZID:US-Eastern
+LAST-MODIFIED:20040110T032845Z
+BEGIN:STANDARD
+DTSTART:19901026T060000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19900404T010000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+END:VTIMEZONE
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART;TZID=US-Eastern:20080601T120000
+DTEND;TZID=US-Eastern:20080601T130000
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""")
             ),
             (
                 "#1.8 Simple component, vtimezone bad change",
@@ -827,7 +918,7 @@
 END:VCALENDAR
 """,
                 "mailto:user2 at example.com",
-                (False, False,)
+                (False, False, (), None)
             ),
             (
                 "#1.9 Simple component, vtimezone substitute",
@@ -894,7 +985,37 @@
 END:VCALENDAR
 """,
                 "mailto:user2 at example.com",
-                (True, True,)
+                (True, False, (), """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VTIMEZONE
+TZID:US-Eastern
+LAST-MODIFIED:20040110T032845Z
+BEGIN:STANDARD
+DTSTART:19901026T060000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19900404T010000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+END:VTIMEZONE
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART;TZID=US-Eastern:20080601T120000
+DTEND;TZID=US-Eastern:20080601T130000
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""")
             ),
             (
                 "#1.10 Simple component, vtimezone substitute",
@@ -961,15 +1082,52 @@
 END:VCALENDAR
 """,
                 "mailto:user2 at example.com",
-                (True, True,)
+                (True, False, (), """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VTIMEZONE
+TZID:US-Eastern
+LAST-MODIFIED:20040110T032845Z
+BEGIN:STANDARD
+DTSTART:19901026T060000
+RRULE:FREQ=YEARLY;BYDAY=-1SU;BYMONTH=10
+TZNAME:EST
+TZOFFSETFROM:-0400
+TZOFFSETTO:-0500
+END:STANDARD
+BEGIN:DAYLIGHT
+DTSTART:19900404T010000
+RRULE:FREQ=YEARLY;BYDAY=1SU;BYMONTH=4
+TZNAME:EDT
+TZOFFSETFROM:-0500
+TZOFFSETTO:-0400
+END:DAYLIGHT
+END:VTIMEZONE
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART;TZID=US-Eastern:20080601T120000
+DTEND;TZID=US-Eastern:20080601T130000
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""")
             ),
         )
 
         for description, calendar1, calendar2, attendee, result in data:
             differ = iCalDiff(Component.fromString(calendar1), Component.fromString(calendar2), False)
-            self.assertEqual(differ.attendeeDiff(attendee), result, msg=description)
+            diffResult = differ.attendeeMerge(attendee)
+            diffResult = (
+                diffResult[0],
+                diffResult[1],
+                tuple([toString(i) if i else None for i in diffResult[2]]),
+                str(diffResult[3]).replace("\r", "") if diffResult[3] else None,
+            )
+            self.assertEqual(diffResult, result, msg="%s: actual result: (%s)" % (description, ", ".join([str(i).replace("\r", "") for i in diffResult]),))
 
-    def test_attendee_diff_complex(self):
+    def test_attendee_merge_complex(self):
         
         data = (
             (
@@ -1019,7 +1177,28 @@
 END:VCALENDAR
 """,
                 "mailto:user2 at example.com",
-                (True, True,)
+                (True, False, (), """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T123000Z
+DTEND:20080602T130000Z
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""")
             ),
             (
                 "#1.2 Complex component, alarm change",
@@ -1078,7 +1257,33 @@
 END:VCALENDAR
 """,
                 "mailto:user2 at example.com",
-                (True, True,)
+                (True, False, (), """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+BEGIN:VALARM
+ACTION:DISPLAY
+DESCRIPTION:Test for Attendee
+TRIGGER;RELATED=START:-PT10M
+END:VALARM
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T123000Z
+DTEND:20080602T130000Z
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""")
             ),
             (
                 "#1.3 Complex component, missing override",
@@ -1119,7 +1324,7 @@
 END:VCALENDAR
 """,
                 "mailto:user2 at example.com",
-                (False, False,)
+                (False, False, (), None)
             ),
             (
                 "#1.4 Complex component, additional override no change ok",
@@ -1177,7 +1382,37 @@
 END:VCALENDAR
 """,
                 "mailto:user2 at example.com",
-                (True, True,)
+                (True, False, (), """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T123000Z
+DTEND:20080602T130000Z
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080604T120000Z
+DTSTART:20080604T120000Z
+DTEND:20080604T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""")
             ),
             (
                 "#1.5 Complex component, additional override change ok",
@@ -1235,7 +1470,37 @@
 END:VCALENDAR
 """,
                 "mailto:user2 at example.com",
-                (True, False,)
+                (True, True, ("20080602T120000Z", "20080604T120000Z",), """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080602T120000Z
+DTSTART:20080602T123000Z
+DTEND:20080602T130000Z
+ATTENDEE;PARTSTAT=ACCEPTED;RSVP=TRUE:mailto:user2 at example.com
+ORGANIZER:mailto:user1 at example.com
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080604T120000Z
+DTSTART:20080604T120000Z
+DTEND:20080604T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=DECLINED;RSVP=TRUE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""")
             ),
             (
                 "#1.6 Complex component, additional override bad",
@@ -1293,14 +1558,440 @@
 END:VCALENDAR
 """,
                 "mailto:user2 at example.com",
-                (False, False,)
+                (False, False, (), None)
             ),
         )
 
         for description, calendar1, calendar2, attendee, result in data:
             differ = iCalDiff(Component.fromString(calendar1), Component.fromString(calendar2), False)
-            self.assertEqual(differ.attendeeDiff(attendee), result, msg=description)
+            diffResult = differ.attendeeMerge(attendee)
+            diffResult = (
+                diffResult[0],
+                diffResult[1],
+                tuple([toString(i) if i else None for i in diffResult[2]]),
+                str(diffResult[3]).replace("\r", "") if diffResult[3] else None,
+            )
+            self.assertEqual(diffResult, result, msg="%s: actual result: (%s)" % (description, ", ".join([str(i).replace("\r", "") for i in diffResult]),))
 
+
+    def test_attendee_merge_exdate(self):
+        
+        data = (
+            (
+                "#1.1 Single component, one EXDATE",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user2 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user2 at example.com
+EXDATE:20080604T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+                "mailto:user2 at example.com",
+                (True, True, ("20080604T120000Z",), """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080604T120000Z
+DTSTART:20080604T120000Z
+DTEND:20080604T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=DECLINED:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""")
+            ),
+            (
+                "#1.2 Single component, two EXDATEs",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user2 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user2 at example.com
+EXDATE:20080604T120000Z,20080605T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+                "mailto:user2 at example.com",
+                (True, True, ("20080604T120000Z", "20080605T120000Z",), """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080604T120000Z
+DTSTART:20080604T120000Z
+DTEND:20080604T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=DECLINED:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080605T120000Z
+DTSTART:20080605T120000Z
+DTEND:20080605T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=DECLINED:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""")
+            ),
+            (
+                "#1.3 Two components, one EXDATE",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user2 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080604T120000Z
+DTSTART:20080604T130000Z
+DTEND:20080604T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user2 at example.com
+EXDATE:20080604T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+                "mailto:user2 at example.com",
+                (True, True, ("20080604T120000Z",), """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080604T120000Z
+DTSTART:20080604T130000Z
+DTEND:20080604T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=DECLINED:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""")
+            ),
+            (
+                "#1.4 Two components, two EXDATEs",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user2 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080604T120000Z
+DTSTART:20080604T130000Z
+DTEND:20080604T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user2 at example.com
+EXDATE:20080604T120000Z
+EXDATE:20080606T120000Z
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+                "mailto:user2 at example.com",
+                (True, True, ("20080604T120000Z", "20080606T120000Z",), """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION;RSVP=TRUE:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080604T120000Z
+DTSTART:20080604T130000Z
+DTEND:20080604T140000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=DECLINED:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080606T120000Z
+DTSTART:20080606T120000Z
+DTEND:20080606T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=DECLINED:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+END:VEVENT
+END:VCALENDAR
+""")
+            ),
+        )
+
+        for description, calendar1, calendar2, attendee, result in data:
+            differ = iCalDiff(Component.fromString(calendar1), Component.fromString(calendar2), False)
+            diffResult = differ.attendeeMerge(attendee)
+            diffResult = (
+                diffResult[0],
+                diffResult[1],
+                tuple([toString(i) if i else None for i in diffResult[2]]),
+                str(diffResult[3]).replace("\r", "") if diffResult[3] else None,
+            )
+            self.assertEqual(diffResult, result, msg="%s: actual result: (%s)" % (description, ", ".join([str(i).replace("\r", "") for i in diffResult]),))
+
+    def test_attendee_merge_cancelled(self):
+        
+        data = (
+            (
+                "#1.1 Remove EXDATE add CANCELLED",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user2 at example.com
+RRULE:FREQ=DAILY
+EXDATE:20080604T120000Z
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user2 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080604T120000Z
+DTEND:20080604T130000Z
+RECURRENCE-ID:20080604T120000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user2 at example.com
+STATUS:CANCELLED
+END:VEVENT
+END:VCALENDAR
+""",
+                "mailto:user2 at example.com",
+                (True, False, (), """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080604T120000Z
+DTSTART:20080604T120000Z
+DTEND:20080604T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user2 at example.com
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+STATUS:CANCELLED
+END:VEVENT
+END:VCALENDAR
+""")
+            ),
+            (
+                "#1.2 Removed CANCELLED add EXDATE",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user2 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080604T120000Z
+DTEND:20080604T130000Z
+RECURRENCE-ID:20080604T120000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user2 at example.com
+STATUS:CANCELLED
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ORGANIZER;CN="User 01":mailto:user1 at example.com
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user2 at example.com
+RRULE:FREQ=DAILY
+EXDATE:20080604T120000Z
+END:VEVENT
+END:VCALENDAR
+""",
+                "mailto:user2 at example.com",
+                (True, False, (), """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890
+DTSTART:20080601T120000Z
+DTEND:20080601T130000Z
+ATTENDEE:mailto:user1 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user2 at example.com
+EXDATE:20080604T120000Z
+ORGANIZER;CN=User 01:mailto:user1 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""")
+            ),
+        )
+
+        for description, calendar1, calendar2, attendee, result in data:
+            differ = iCalDiff(Component.fromString(calendar1), Component.fromString(calendar2), False)
+            diffResult = differ.attendeeMerge(attendee)
+            diffResult = (
+                diffResult[0],
+                diffResult[1],
+                tuple([toString(i) if i else None for i in diffResult[2]]),
+                str(diffResult[3]).replace("\r", "") if diffResult[3] else None,
+            )
+            self.assertEqual(diffResult, result, msg="%s: actual result: (%s)" % (description, ", ".join([str(i).replace("\r", "") for i in diffResult]),))
+
     def test_what_is_different(self):
         
         data1 = (
@@ -3050,638 +3741,3 @@
             strcal2 = str(cal2)
             strchanged = str(Component.fromString(changed_calendar))
             self.assertEqual(strchanged, strcal2, msg="%s mismatch:\n%s" % (description, "\n".join(unified_diff(strchanged.split("\n"), strcal2.split("\n")))))
-
-    def test_attendee_smart_merge(self):
-        
-        data1 = (
-            (
-                "#1.1 Simple component, no change",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-SUMMARY:Test
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ATTENDEE:mailto:user3 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-SUMMARY:Test
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ATTENDEE:mailto:user3 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-SUMMARY:Test
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ATTENDEE:mailto:user3 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-            ),
-            (
-                "#1.2 Simple component, client change only",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-SUMMARY:Test
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ATTENDEE:mailto:user3 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-SUMMARY:Test - 2
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=ACCEPTED:mailto:user2 at example.com
-ATTENDEE:mailto:user3 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-SUMMARY:Test - 2
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=ACCEPTED:mailto:user2 at example.com
-ATTENDEE:mailto:user3 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-            ),
-            (
-                "#1.3 Simple component, server change only",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-SUMMARY:Test
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ATTENDEE;PARTSTAT=ACCEPTED:mailto:user3 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-SUMMARY:Test
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ATTENDEE:mailto:user3 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-SUMMARY:Test
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ATTENDEE;PARTSTAT=ACCEPTED:mailto:user3 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-            ),
-            (
-                "#1.4 Simple component, both change",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-SUMMARY:Test
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ATTENDEE;PARTSTAT=ACCEPTED:mailto:user3 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-SUMMARY:Test - 2
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=ACCEPTED:mailto:user2 at example.com
-ATTENDEE:mailto:user3 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-SUMMARY:Test - 2
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=ACCEPTED:mailto:user2 at example.com
-ATTENDEE;PARTSTAT=ACCEPTED:mailto:user3 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-            ),
-        )
-        
-        data2 = (
-            (
-                "#2.1 Simple recurring component, no change",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-SUMMARY:Test
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ATTENDEE:mailto:user3 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T120000Z
-DTEND:20080602T130000Z
-SUMMARY:Test - 2
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ATTENDEE:mailto:user3 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-SUMMARY:Test
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ATTENDEE:mailto:user3 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T120000Z
-DTEND:20080602T130000Z
-SUMMARY:Test - 2
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ATTENDEE:mailto:user3 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-SUMMARY:Test
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ATTENDEE:mailto:user3 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T120000Z
-DTEND:20080602T130000Z
-SUMMARY:Test - 2
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ATTENDEE:mailto:user3 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-            ),
-            (
-                "#2.2 Simple recurring component, client instance change",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-SUMMARY:Test
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ATTENDEE:mailto:user3 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T120000Z
-DTEND:20080602T130000Z
-SUMMARY:Test - 2
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ATTENDEE:mailto:user3 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-SUMMARY:Test
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=ACCEPTED:mailto:user2 at example.com
-ATTENDEE:mailto:user3 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T120000Z
-DTEND:20080602T130000Z
-SUMMARY:Test - 2
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=ACCEPTED:mailto:user2 at example.com
-ATTENDEE:mailto:user3 at example.com
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080603T120000Z
-DTSTART:20080603T120000Z
-DTEND:20080603T130000Z
-SUMMARY:Test
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=DECLINED:mailto:user2 at example.com
-ATTENDEE:mailto:user3 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-SUMMARY:Test
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=ACCEPTED:mailto:user2 at example.com
-ATTENDEE:mailto:user3 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T120000Z
-DTEND:20080602T130000Z
-SUMMARY:Test - 2
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=ACCEPTED:mailto:user2 at example.com
-ATTENDEE:mailto:user3 at example.com
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080603T120000Z
-DTSTART:20080603T120000Z
-DTEND:20080603T130000Z
-SUMMARY:Test
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=DECLINED:mailto:user2 at example.com
-ATTENDEE:mailto:user3 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-            ),
-            (
-                "#2.3 Simple recurring component, server instance change",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-SUMMARY:Test
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ATTENDEE;PARTSTAT=ACCEPTED:mailto:user3 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T120000Z
-DTEND:20080602T130000Z
-SUMMARY:Test - 2
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ATTENDEE;PARTSTAT=ACCEPTED:mailto:user3 at example.com
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080603T120000Z
-DTSTART:20080603T120000Z
-DTEND:20080603T130000Z
-SUMMARY:Test
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ATTENDEE;PARTSTAT=DECLINED:mailto:user3 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-SUMMARY:Test
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=ACCEPTED:mailto:user2 at example.com
-ATTENDEE:mailto:user3 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T120000Z
-DTEND:20080602T130000Z
-SUMMARY:Test - 2
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=ACCEPTED:mailto:user2 at example.com
-ATTENDEE:mailto:user3 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-SUMMARY:Test
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=ACCEPTED:mailto:user2 at example.com
-ATTENDEE;PARTSTAT=ACCEPTED:mailto:user3 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T120000Z
-DTEND:20080602T130000Z
-SUMMARY:Test - 2
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=ACCEPTED:mailto:user2 at example.com
-ATTENDEE;PARTSTAT=ACCEPTED:mailto:user3 at example.com
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080603T120000Z
-DTSTART:20080603T120000Z
-DTEND:20080603T130000Z
-SUMMARY:Test
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=ACCEPTED:mailto:user2 at example.com
-ATTENDEE;PARTSTAT=DECLINED:mailto:user3 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-            ),
-            (
-                "#2.4 Simple recurring component, both instance change",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-SUMMARY:Test
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ATTENDEE;PARTSTAT=ACCEPTED:mailto:user3 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T120000Z
-DTEND:20080602T130000Z
-SUMMARY:Test - 2
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ATTENDEE;PARTSTAT=ACCEPTED:mailto:user3 at example.com
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080603T120000Z
-DTSTART:20080603T120000Z
-DTEND:20080603T130000Z
-SUMMARY:Test
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE:mailto:user2 at example.com
-ATTENDEE;PARTSTAT=DECLINED:mailto:user3 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-SUMMARY:Test
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=ACCEPTED:mailto:user2 at example.com
-ATTENDEE:mailto:user3 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T120000Z
-DTEND:20080602T130000Z
-SUMMARY:Test - 2
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=ACCEPTED:mailto:user2 at example.com
-ATTENDEE:mailto:user3 at example.com
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080604T120000Z
-DTSTART:20080604T120000Z
-DTEND:20080604T130000Z
-SUMMARY:Test
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=DECLINED:mailto:user2 at example.com
-ATTENDEE:mailto:user3 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-                """BEGIN:VCALENDAR
-VERSION:2.0
-PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
-BEGIN:VEVENT
-UID:12345-67890
-DTSTART:20080601T120000Z
-DTEND:20080601T130000Z
-SUMMARY:Test
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=ACCEPTED:mailto:user2 at example.com
-ATTENDEE;PARTSTAT=ACCEPTED:mailto:user3 at example.com
-RRULE:FREQ=DAILY
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080602T120000Z
-DTSTART:20080602T120000Z
-DTEND:20080602T130000Z
-SUMMARY:Test - 2
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=ACCEPTED:mailto:user2 at example.com
-ATTENDEE;PARTSTAT=ACCEPTED:mailto:user3 at example.com
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080604T120000Z
-DTSTART:20080604T120000Z
-DTEND:20080604T130000Z
-SUMMARY:Test
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=DECLINED:mailto:user2 at example.com
-ATTENDEE;PARTSTAT=ACCEPTED:mailto:user3 at example.com
-END:VEVENT
-BEGIN:VEVENT
-UID:12345-67890
-RECURRENCE-ID:20080603T120000Z
-DTSTART:20080603T120000Z
-DTEND:20080603T130000Z
-SUMMARY:Test
-ORGANIZER;CN="User 01":mailto:user1 at example.com
-ATTENDEE:mailto:user1 at example.com
-ATTENDEE;PARTSTAT=ACCEPTED:mailto:user2 at example.com
-ATTENDEE;PARTSTAT=DECLINED:mailto:user3 at example.com
-END:VEVENT
-END:VCALENDAR
-""",
-            ),
-        )
-        
-        for description, calendar1, calendar2, changed_calendar in itertools.chain(data1, data2,):
-            cal1 = Component.fromString(calendar1)
-            cal2 = Component.fromString(calendar2)
-
-            differ = iCalDiff(cal1, cal2, True)
-            differ.attendeeDiff("mailto:user2 at example.com")
-
-            strcal2 = str(cal2)
-            strchanged = str(Component.fromString(changed_calendar))
-            self.assertEqual(strchanged, strcal2, msg="%s mismatch:\n%s" % (description, "\n".join(unified_diff(strchanged.split("\n"), strcal2.split("\n")))))
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20090311/73b4efad/attachment-0001.html>


More information about the calendarserver-changes mailing list