[CalendarServer-changes] [3360] CalendarServer/trunk

source_changes at macosforge.org source_changes at macosforge.org
Mon Nov 10 17:38:43 PST 2008


Revision: 3360
          http://trac.macosforge.org/projects/calendarserver/changeset/3360
Author:   cdaboo at apple.com
Date:     2008-11-10 17:38:43 -0800 (Mon, 10 Nov 2008)
Log Message:
-----------
Merged Schedule-Tag support to trunk.

Modified Paths:
--------------
    CalendarServer/trunk/conf/caldavd-test.plist
    CalendarServer/trunk/conf/caldavd.plist
    CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.static.patch
    CalendarServer/trunk/twistedcaldav/__init__.py
    CalendarServer/trunk/twistedcaldav/caldavxml.py
    CalendarServer/trunk/twistedcaldav/config.py
    CalendarServer/trunk/twistedcaldav/customxml.py
    CalendarServer/trunk/twistedcaldav/ical.py
    CalendarServer/trunk/twistedcaldav/method/get.py
    CalendarServer/trunk/twistedcaldav/method/put_common.py
    CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py
    CalendarServer/trunk/twistedcaldav/scheduling/implicit.py
    CalendarServer/trunk/twistedcaldav/scheduling/itip.py
    CalendarServer/trunk/twistedcaldav/scheduling/processing.py
    CalendarServer/trunk/twistedcaldav/scheduling/test/test_icaldiff.py
    CalendarServer/trunk/twistedcaldav/static.py
    CalendarServer/trunk/twistedcaldav/test/test_collectioncontents.py

Modified: CalendarServer/trunk/conf/caldavd-test.plist
===================================================================
--- CalendarServer/trunk/conf/caldavd-test.plist	2008-11-11 01:35:30 UTC (rev 3359)
+++ CalendarServer/trunk/conf/caldavd-test.plist	2008-11-11 01:38:43 UTC (rev 3360)
@@ -486,6 +486,8 @@
         </array>
         <key>OldDraftCompatibility</key>
         <true/>
+        <key>ScheduleTagCompatibility</key>
+        <true/>
         <key>DefaultCalendarProvisioned</key>
         <true/>
         <key>EnablePrivateComments</key>

Modified: CalendarServer/trunk/conf/caldavd.plist
===================================================================
--- CalendarServer/trunk/conf/caldavd.plist	2008-11-11 01:35:30 UTC (rev 3359)
+++ CalendarServer/trunk/conf/caldavd.plist	2008-11-11 01:38:43 UTC (rev 3360)
@@ -359,6 +359,8 @@
         </array>
         <key>OldDraftCompatibility</key>
         <true/>
+        <key>ScheduleTagCompatibility</key>
+        <true/>
         <key>DefaultCalendarProvisioned</key>
         <true/>
         <key>EnablePrivateComments</key>

Modified: CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.static.patch
===================================================================
--- CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.static.patch	2008-11-11 01:35:30 UTC (rev 3359)
+++ CalendarServer/trunk/lib-patches/Twisted/twisted.web2.dav.static.patch	2008-11-11 01:38:43 UTC (rev 3360)
@@ -60,11 +60,12 @@
  
      def __repr__(self):
          return "<%s: %s>" % (self.__class__.__name__, self.fp.path)
-@@ -75,6 +81,12 @@
+@@ -75,6 +81,13 @@
      # WebDAV
      ##
  
 +    def etag(self):
++        if not self.fp.exists(): return None
 +        if self.hasDeadProperty(TwistedGETContentMD5):
 +            return http_headers.ETag(str(self.readDeadProperty(TwistedGETContentMD5)))
 +        else:
@@ -73,7 +74,7 @@
      def davComplianceClasses(self):
          return ("1", "access-control") # Add "2" when we have locking
  
-@@ -87,7 +99,6 @@
+@@ -87,7 +100,6 @@
          """
          See L{IDAVResource.isCollection}.
          """
@@ -81,7 +82,7 @@
          return self.fp.isdir()
  
      ##
-@@ -98,6 +109,50 @@
+@@ -98,6 +110,50 @@
          return succeed(davPrivilegeSet)
  
      ##
@@ -132,7 +133,7 @@
      # Workarounds for issues with File
      ##
  
-@@ -112,8 +167,12 @@
+@@ -112,8 +168,12 @@
          See L{IResource}C{.locateChild}.
          """
          # If getChild() finds a child resource, return it
@@ -147,7 +148,7 @@
          
          # If we're not backed by a directory, we have no children.
          # But check for existance first; we might be a collection resource
-@@ -132,7 +191,9 @@
+@@ -132,7 +192,9 @@
          return (self.createSimilarFile(self.fp.child(path).path), segments[1:])
  
      def createSimilarFile(self, path):

Modified: CalendarServer/trunk/twistedcaldav/__init__.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/__init__.py	2008-11-11 01:35:30 UTC (rev 3359)
+++ CalendarServer/trunk/twistedcaldav/__init__.py	2008-11-11 01:38:43 UTC (rev 3360)
@@ -66,8 +66,17 @@
 File.contentTypes = loadMimeTypes(("/etc/apache2/mime.types", "/etc/httpd/mime.types",))
 
 import twisted.web2.dav.davxml
+from twisted.web2.http_headers import DefaultHTTPHandler, last, singleHeader, tokenize
 import twistedcaldav.caldavxml
 import twistedcaldav.customxml
 
 twisted.web2.dav.davxml.registerElements(twistedcaldav.caldavxml)
 twisted.web2.dav.davxml.registerElements(twistedcaldav.customxml)
+
+DefaultHTTPHandler.updateParsers({
+    'If-Schedule-Tag-Match':(last, str),
+})
+DefaultHTTPHandler.updateGenerators({
+    'Schedule-Tag':(str, singleHeader),
+})
+

Modified: CalendarServer/trunk/twistedcaldav/caldavxml.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/caldavxml.py	2008-11-11 01:35:30 UTC (rev 3359)
+++ CalendarServer/trunk/twistedcaldav/caldavxml.py	2008-11-11 01:38:43 UTC (rev 3360)
@@ -1664,6 +1664,15 @@
 
     allowed_children = { (davxml.dav_namespace, "href"): (0, None) } # NB Minimum is zero because this is a property name
 
+class ScheduleTag (CalDAVTextElement):
+    """
+    Property on scheduling resources.
+    (CalDAV-schedule, section x.x.x)
+    """
+    name = "schedule-tag"
+    hidden = True
+    protected = True
+
 class ScheduleInbox (CalDAVEmptyElement):
     """
     Denotes the resource type of a calendar schedule Inbox.

Modified: CalendarServer/trunk/twistedcaldav/config.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/config.py	2008-11-11 01:35:30 UTC (rev 3359)
+++ CalendarServer/trunk/twistedcaldav/config.py	2008-11-11 01:38:43 UTC (rev 3360)
@@ -204,6 +204,7 @@
             "HTTPDomain"                 : "",    # Domain for http calendar user addresses on this server
             "AddressPatterns"            : [],    # Reg-ex patterns to match local calendar user addresses
             "OldDraftCompatibility"      : True,  # Whether to maintain compatibility with non-implicit mode
+            "ScheduleTagCompatibility"   : True,  # Whether to support older clients that do not use Schedule-Tag feature
             "DefaultCalendarProvisioned" : True,  # Whether the provisioned default calendar is marked as the scheduling default
             "EnablePrivateComments"      : True,  # Private comments from attendees to organizer
         },

Modified: CalendarServer/trunk/twistedcaldav/customxml.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/customxml.py	2008-11-11 01:35:30 UTC (rev 3359)
+++ CalendarServer/trunk/twistedcaldav/customxml.py	2008-11-11 01:38:43 UTC (rev 3360)
@@ -72,6 +72,16 @@
     name = "scheduling-object-resource"
     hidden = True
 
+class TwistedScheduleMatchETags(davxml.WebDAVElement):
+    """
+    List of ETags that can be used for a "weak" If-Match comparison.    
+    """
+    namespace = twisted_private_namespace
+    name = "scheduling-match-etags"
+    hidden = True
+
+    allowed_children = { (dav_namespace, "getetag"): (0, None) }
+
 class TwistedCalendarHasPrivateCommentsProperty (davxml.WebDAVEmptyElement):
     """
     Indicates that a calendar resource has private comments.

Modified: CalendarServer/trunk/twistedcaldav/ical.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/ical.py	2008-11-11 01:35:30 UTC (rev 3359)
+++ CalendarServer/trunk/twistedcaldav/ical.py	2008-11-11 01:38:43 UTC (rev 3360)
@@ -873,6 +873,10 @@
             return None
 
         # 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
         
         # Create the derived instance
         newcomp = master.duplicate()

Modified: CalendarServer/trunk/twistedcaldav/method/get.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/get.py	2008-11-11 01:35:30 UTC (rev 3359)
+++ CalendarServer/trunk/twistedcaldav/method/get.py	2008-11-11 01:38:43 UTC (rev 3360)
@@ -28,6 +28,7 @@
 from twisted.web2.stream import MemoryStream
 
 from twistedcaldav import caldavxml
+from twistedcaldav.caldavxml import ScheduleTag
 from twistedcaldav.customxml import TwistedCalendarAccessProperty
 from twistedcaldav.ical import Component
 
@@ -59,6 +60,14 @@
                 response.headers.setHeader("content-type", MimeType.fromString("text/calendar; charset=utf-8"))
                 returnValue(response)
 
+
     # Do normal GET behavior
     response = (yield super(CalDAVFile, self).http_GET(request))
+    
+    # Add Schedule-Tag header if property is present
+    if self.exists() and self.hasDeadProperty(ScheduleTag):
+        scheduletag = self.readDeadProperty(ScheduleTag)
+        if scheduletag:
+            response.headers.setHeader("Schedule-Tag", str(scheduletag))
+
     returnValue(response)

Modified: CalendarServer/trunk/twistedcaldav/method/put_common.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/method/put_common.py	2008-11-11 01:35:30 UTC (rev 3359)
+++ CalendarServer/trunk/twistedcaldav/method/put_common.py	2008-11-11 01:38:43 UTC (rev 3360)
@@ -22,6 +22,7 @@
 
 import os
 import types
+import uuid
 
 from twisted.internet import reactor
 from twisted.internet.defer import Deferred, inlineCallbacks, succeed
@@ -44,11 +45,12 @@
 from twisted.web2.stream import MemoryStream
 
 from twistedcaldav.config import config
-from twistedcaldav.caldavxml import NoUIDConflict
+from twistedcaldav.caldavxml import NoUIDConflict, ScheduleTag
 from twistedcaldav.caldavxml import NumberOfRecurrencesWithinLimits
 from twistedcaldav.caldavxml import caldav_namespace
 from twistedcaldav.customxml import calendarserver_namespace ,\
-    TwistedCalendarHasPrivateCommentsProperty, TwistedSchedulingObjectResource
+    TwistedCalendarHasPrivateCommentsProperty, TwistedSchedulingObjectResource,\
+    TwistedScheduleMatchETags
 from twistedcaldav.customxml import TwistedCalendarAccessProperty
 from twistedcaldav.fileops import copyToWithXAttrs, copyXAttrs
 from twistedcaldav.fileops import putWithXAttrs
@@ -203,6 +205,7 @@
         isiTIP=False,
         allowImplicitSchedule=True,
         internal_request=False,
+        processing_organizer=None,
     ):
         """
         Function that does common PUT/COPY/MOVE behavior.
@@ -222,6 +225,7 @@
         @param isiTIP:                True if relaxed calendar data validation is to be done, False otherwise.
         @param allowImplicitSchedule: True if implicit scheduling should be attempted, False otherwise.
         @param internal_request:   True if this request originates internally and needs to bypass scheduling authorization checks.
+        @param processing_organizer: True if implicit processing for an organizer, False if for an attendee, None if not implicit processing.
         """
         
         # Check that all arguments are valid
@@ -261,6 +265,7 @@
         self.isiTIP = isiTIP
         self.allowImplicitSchedule = allowImplicitSchedule
         self.internal_request = internal_request
+        self.processing_organizer = processing_organizer
         
         self.rollback = None
         self.access = None
@@ -273,6 +278,7 @@
 
         # Basic validation
         yield self.validCopyMoveOperation()
+        self.validIfScheduleMatch()
 
         if self.destinationcal:
             # Valid resource name check
@@ -366,6 +372,38 @@
                     log.debug(msg)
                     raise HTTPError(StatusResponse(responsecode.FORBIDDEN, msg))
 
+    def validIfScheduleMatch(self):
+        """
+        Check for If-ScheduleTag-Match header behavior.
+        """
+        
+        # Only when a direct request
+        self.schedule_tag_match = False
+        if not self.isiTIP and not self.internal_request:
+            header = self.request.headers.getHeader("If-Schedule-Tag-Match")
+            if header:
+                # Do "precondition" test
+                matched = False
+                if self.destination.exists() and self.destination.hasDeadProperty(ScheduleTag):
+                    scheduletag = self.destination.readDeadProperty(ScheduleTag)
+                    matched = (scheduletag == header)
+                if not matched:
+                    log.debug("If-Schedule-Tag-Match: header value '%s' does not match resource value '%s'" % (header, scheduletag,))
+                    raise HTTPError(responsecode.PRECONDITION_FAILED)
+                self.schedule_tag_match = True
+            
+            elif config.Scheduling["CalDAV"]["ScheduleTagCompatibility"]:
+                # Compatibility with old clients. Policy:
+                #
+                # 1. If If-Match header is not present, never do smart merge.
+                # 2. If If-Match is present and the specified ETag is considered a "weak" match to the
+                #    current Schedule-Tag, then do smart merge, else reject with a 412.
+                #
+                # Actually by the time we get here the pre-condition will already have been tested and found to be OK,
+                # so we can just always do smart merge now if If-Match is present.
+
+                self.schedule_tag_match = self.request.headers.getHeader("If-Match") is not None
+
     def validResourceName(self):
         """
         Make sure that the resource name for the new resource is valid.
@@ -606,7 +644,17 @@
 
     @inlineCallbacks
     def doImplicitScheduling(self):
+
+        # Get any existing schedule-tag property on the resource
+        if self.destination.exists() and self.destination.hasDeadProperty(ScheduleTag):
+            self.scheduletag = self.destination.readDeadProperty(ScheduleTag)
+            if self.scheduletag:
+                self.scheduletag = str(self.scheduletag)
+        else:
+            self.scheduletag = None
+
         data_changed = False
+        did_implicit_action = False
 
         # Do scheduling
         if not self.isiTIP:
@@ -650,15 +698,16 @@
                 ))
             
             if do_implicit_action and self.allowImplicitSchedule:
-                new_calendar = (yield scheduler.doImplicitScheduling())
+                new_calendar = (yield scheduler.doImplicitScheduling(self.schedule_tag_match))
                 if new_calendar:
                     self.calendar = new_calendar
                     self.calendardata = str(self.calendar)
                     data_changed = True
+                did_implicit_action = True
         else:
             is_scheduling_resource = False
             
-        returnValue((is_scheduling_resource, data_changed,))
+        returnValue((is_scheduling_resource, data_changed, did_implicit_action,))
 
     @inlineCallbacks
     def doStore(self, implicit):
@@ -836,7 +885,7 @@
             new_has_private_comments = self.preservePrivateComments()
     
             # Do scheduling
-            is_scheduling_resource, data_changed = (yield self.doImplicitScheduling())
+            is_scheduling_resource, data_changed, did_implicit_action = (yield self.doImplicitScheduling())
 
             # Initialize the rollback system
             self.setupRollback()
@@ -858,12 +907,64 @@
             # Do the actual put or copy
             response = (yield self.doStore(data_changed))
             
+            # Must not set ETag in response if data changed
+            if did_implicit_action:
+                def _removeEtag(request, response):
+                    response.headers.removeHeader('etag')
+                    return response
+                _removeEtag.handleErrors = True
 
+                self.request.addResponseFilter(_removeEtag, atEnd=True)
+
             # Check for scheduling object resource and write property
             if is_scheduling_resource:
                 self.destination.writeDeadProperty(TwistedSchedulingObjectResource())
-            elif not self.destinationcal:
+
+                # Need to figure out when to change the schedule tag:
+                #
+                # 1. If this is not an internal request then the resource is being explicitly changed
+                # 2. If it is an internal request for the Organizer, schedule tag never changes
+                # 3. If it is an internal request for an Attendee and the message being processed came
+                #    from the Organizer then the schedule tag changes.
+
+                change_scheduletag = True
+                if self.internal_request:
+                    # Check what kind of processing is going on
+                    if self.processing_organizer == True:
+                        # All auto-processed updates for an Organizer leave the tag unchanged
+                        change_scheduletag = False
+                    elif self.processing_organizer == False:
+                        # Auto-processed updates that are the result of an organizer "refresh' due
+                        # to another Attendee's REPLY should leave the tag unchanged
+                        change_scheduletag = not hasattr(self.request, "doing_attendee_refresh")
+
+                if change_scheduletag or self.scheduletag is None:
+                    self.scheduletag = str(uuid.uuid4())
+                self.destination.writeDeadProperty(ScheduleTag.fromString(self.scheduletag))
+
+                # Add a response header
+                response.headers.setHeader("Schedule-Tag", self.scheduletag)                
+
+                # Handle weak etag compatibility
+                if config.Scheduling["CalDAV"]["ScheduleTagCompatibility"]:
+                    if change_scheduletag:
+                        # Schedule-Tag change => weak ETag behavior must not happen
+                        etags = ()
+                    else:
+                        # Schedule-Tag did not change => add current ETag to list of those that can
+                        # be used in a weak pre-condition test
+                        if self.destination.hasDeadProperty(TwistedScheduleMatchETags):
+                            etags = self.destination.readDeadProperty(TwistedScheduleMatchETags).children
+                        else:
+                            etags = ()
+                    etags += (davxml.GETETag.fromString(self.destination.etag().tag),)
+                    self.destination.writeDeadProperty(TwistedScheduleMatchETags(*etags))
+                else:
+                    self.destination.removeDeadProperty(TwistedScheduleMatchETags)                
+            else:
                 self.destination.removeDeadProperty(TwistedSchedulingObjectResource)                
+                self.destination.removeDeadProperty(ScheduleTag)                
+                self.destination.removeDeadProperty(TwistedScheduleMatchETags)                
 
             # Check for existence of private comments and write property
             if config.Scheduling["CalDAV"].get("EnablePrivateComments", True):

Modified: CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py	2008-11-11 01:35:30 UTC (rev 3359)
+++ CalendarServer/trunk/twistedcaldav/scheduling/icaldiff.py	2008-11-11 01:38:43 UTC (rev 3360)
@@ -33,7 +33,7 @@
 
 class iCalDiff(object):
     
-    def __init__(self, calendar1, calendar2):
+    def __init__(self, calendar1, calendar2, smart_merge):
         """
         
         @param calendar1:
@@ -44,6 +44,7 @@
         
         self.calendar1 = calendar1
         self.calendar2 = calendar2
+        self.smart_merge = smart_merge
     
     def organizerDiff(self):
         """
@@ -51,6 +52,11 @@
         changed by an organizer. Basically any change except for anything related to a VALARM.
         """
         
+        # If smart merge is needed we have to do this before trying the diff
+        if self.smart_merge:
+            log.debug("organizerDiff: doing smart Organizer diff/merge")
+            self._organizerMerge()
+
         def duplicateAndNormalize(calendar):
             calendar = calendar.duplicate()
             calendar.removeAlarms()
@@ -64,7 +70,7 @@
             calendar.removePropertyParameters("ATTENDEE", ("RSVP", "SCHEDULE-AGENT", "SCHEDULE-STATUS",))
             calendar.removePropertyParametersByValue("ATTENDEE", (("PARTSTAT", "NEEDS-ACTION"),))
             return calendar
-            
+        
         # Normalize components for comparison
         self.calendar1 = duplicateAndNormalize(self.calendar1)
         self.calendar2 = duplicateAndNormalize(self.calendar2)
@@ -74,8 +80,137 @@
             self._logDiffError("organizerDiff: Mismatched calendar objects")
         return result
 
-    def attendeeMerge(self, attendee):
+    def _organizerMerge(self):
         """
+        Merge changes to ATTENDEE properties in calendar1 into calendar2.
+        """
+        organizer = self.calendar2.masterComponent().propertyValue("ORGANIZER")
+        self._doSmartMerge(organizer, True)
+
+    def _doSmartMerge(self, ignore_attendee, is_organizer):
+        """
+        Merge changes to ATTENDEE properties in calendar1 into calendar2.
+        """
+        
+        old_master = self.calendar1.masterComponent()
+        new_master = self.calendar2.masterComponent()
+        
+        # Do master merge first
+        self._tryComponentMerge(old_master, new_master, ignore_attendee, is_organizer)
+
+        # New check the matching components
+        for old_component in self.calendar1.subcomponents():
+            
+            # Make sure we have an appropriate component
+            if old_component.name() == "VTIMEZONE":
+                continue
+            rid = old_component.getRecurrenceIDUTC()
+            if rid is None:
+                continue
+
+            # Find matching component in new calendar
+            new_component = self.calendar2.overriddenComponent(rid)
+            if new_component is None:
+                # Determine whether the instance is still valid in the new calendar
+                if True:
+                    # Derive a new instance from the new calendar and transfer attendee status
+                    new_component = self.calendar2.deriveInstance(rid)
+                    self.calendar2.addComponent(new_component)
+                    self._tryComponentMerge(old_component, new_component, ignore_attendee, is_organizer)
+                else:
+                    # Ignore the old instance as it no longer exists
+                    pass
+            else:
+                self._tryComponentMerge(old_component, new_component, ignore_attendee, is_organizer)
+
+        # Check the new instances not in the old calendar
+        for new_component in self.calendar2.subcomponents():
+            
+            # Make sure we have an appropriate component
+            if new_component.name() == "VTIMEZONE":
+                continue
+            rid = new_component.getRecurrenceIDUTC()
+            if rid is None:
+                continue
+
+            # Find matching component in old calendar
+            old_component = self.calendar1.overriddenComponent(rid)
+            if old_component is None:
+                # Try to derive a new instance in the client and transfer attendee status
+                old_component = self.calendar1.deriveInstance(rid)
+                if old_component:
+                    self.calendar1.addComponent(old_component)
+                    self._tryComponentMerge(old_component, new_component, ignore_attendee, is_organizer)
+                else:
+                    # Ignore as we have no state for the new instance
+                    pass
+    
+    def _tryComponentMerge(self, old_comp, new_comp, ignore_attendee_value, is_organizer):
+        if not is_organizer or not self._organizerChangePreventsMerge(old_comp, new_comp):
+            self._transferAttendees(old_comp, new_comp, ignore_attendee_value)
+
+    def _organizerChangePreventsMerge(self, old_comp, new_comp):
+        """
+        Check whether a change from an Organizer needs a re-schedule which means that any
+        Attendee state changes on the server are no longer relevant.
+
+        @param old_comp: existing server calendar component
+        @type old_comp: L{Component}
+        @param new_comp: new calendar component
+        @type new_comp: L{Component}
+        @return: C{True} if changes in new component are such that old attendee state is not
+            relevant, C{False} otherwise
+        """
+
+        props_to_test = ("DTSTART", "DTEND", "DURATION", "RRULE", "RDATE", "EXDATE", "RECURRENCE-ID",)
+        
+        for prop in props_to_test:
+            # Change => no merge
+            if old_comp.getProperty(prop) != new_comp.getProperty(prop):
+                # Always overwrite as we have a big change going on
+                return True
+
+        return False
+    
+    def _transferAttendees(self, old_comp, new_comp, ignore_attendee_value):
+        """
+        Transfer Attendee PARTSTAT from old component to new component.
+
+        @param old_comp: existing server calendar component
+        @type old_comp: L{Component}
+        @param new_comp: new calendar component
+        @type new_comp: L{Component}
+        @param ignore_attendee_value: Attendee to ignore
+        @type ignore_attendee_value: C{str}
+        """
+
+        # Create map of ATTENDEEs in old component
+        old_attendees = {}
+        for attendee in old_comp.properties("ATTENDEE"):
+            value = attendee.value()
+            if value == ignore_attendee_value:
+                continue
+            old_attendees[value] = attendee
+
+        for new_attendee in new_comp.properties("ATTENDEE"):
+            value = new_attendee.value()
+            old_attendee = old_attendees.get(value)
+            if old_attendee:
+                self._transferParameter(old_attendee, new_attendee, "PARTSTAT")
+                self._transferParameter(old_attendee, new_attendee, "SCHEDULE-STATUS")
+    
+    def _transferParameter(self, old_property, new_property, parameter):
+        paramvalue = old_property.params().get(parameter)
+        if paramvalue is None:
+            try:
+                del new_property.params()[parameter]
+            except KeyError:
+                pass
+        else:
+            new_property.params()[parameter] = paramvalue
+
+    def attendeeDiff(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.
        
@@ -85,6 +220,11 @@
         
         self.attendee = 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()
+
         def duplicateAndNormalize(calendar):
             calendar = calendar.duplicate()
             calendar.normalizePropertyValueLists("EXDATE")
@@ -103,21 +243,31 @@
         
         # Make sure the same VCALENDAR properties match
         if not self._checkVCALENDARProperties():
-            self._logDiffError("attendeeMerge: VCALENDAR properties do not match")
+            self._logDiffError("attendeeDiff: VCALENDAR properties do not match")
             return False, False
         
         # Make sure the same VTIMEZONE components appear
         if not self._compareVTIMEZONEs():
-            self._logDiffError("attendeeMerge: VTIMEZONEs do not match")
+            self._logDiffError("attendeeDiff: VTIMEZONEs do not match")
             return False, False
         
         # 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("attendeeMerge: Mismatched calendar objects")
+            self._logDiffError("attendeeDiff: Mismatched calendar objects")
         return result
     
+    def _attendeeMerge(self):
+        """
+        Merge changes to ATTENDEE properties in calendar1 into calendar2.
+        
+        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.
+        """
+        self._doSmartMerge(self.attendee, False)
+
     def whatIsDifferent(self):
         """
         Compare the two calendar objects in their entirety and return a list of properties

Modified: CalendarServer/trunk/twistedcaldav/scheduling/implicit.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/implicit.py	2008-11-11 01:35:30 UTC (rev 3359)
+++ CalendarServer/trunk/twistedcaldav/scheduling/implicit.py	2008-11-11 01:38:43 UTC (rev 3360)
@@ -77,7 +77,7 @@
 
         # If action is remove we actually need to get state from the existing scheduling object resource
         if self.action == "remove":
-            # Also make sure that we return the new calendar being be written rather than the old one
+            # Also make sure that we return the new calendar being written rather than the old one
             # when the implicit action is executed
             self.return_calendar = calendar
             self.calendar = resource.iCalendar()
@@ -183,15 +183,18 @@
         returnValue(self.state is not None)
 
     @inlineCallbacks
-    def doImplicitScheduling(self):
+    def doImplicitScheduling(self, do_smart_merge=False):
         """
         Do implicit scheduling operation based on the data already set by call to checkImplicitScheduling.
 
+        @param do_smart_merge: if True, merge attendee data on disk with new data being stored,
+            else overwrite data on disk.
         @return: a new calendar object modified with scheduling information,
             or C{None} if nothing happened
         """
         
         # Setup some parameters
+        self.do_smart_merge = do_smart_merge
         self.except_attendees = ()
 
         # Determine what type of scheduling this is: Organizer triggered or Attendee triggered
@@ -236,7 +239,11 @@
         self.originatorPrincipal = self.organizerPrincipal
         self.originator = self.organizer
         
-        result = (yield self.processRequests())
+        self.request.doing_attendee_refresh = True
+        try:
+            result = (yield self.processRequests())
+        finally:
+            delattr(self.request, "doing_attendee_refresh")
 
         returnValue(result)
 
@@ -429,7 +436,7 @@
             self.oldcalendar = self.resource.iCalendar()
             
             # Significant change
-            no_change, self.changed_rids = self.isChangeInsignificant()
+            no_change, self.changed_rids = self.isOrganizerChangeInsignificant()
             if no_change:
                 # Nothing to do
                 log.debug("Implicit - organizer '%s' is modifying UID: '%s' but change is not significant" % (self.organizer, self.uid))
@@ -448,10 +455,10 @@
             
         yield self.scheduleWithAttendees()
 
-    def isChangeInsignificant(self):
+    def isOrganizerChangeInsignificant(self):
         
         rids = None
-        differ = iCalDiff(self.oldcalendar, self.calendar)
+        differ = iCalDiff(self.oldcalendar, self.calendar, self.do_smart_merge)
         no_change = differ.organizerDiff()
         if not no_change:
             _ignore_props, rids = differ.whatIsDifferent()
@@ -710,8 +717,8 @@
         if oldcalendar is None:
             oldcalendar = self.organizer_calendar
             oldcalendar.attendeesView((self.attendee,))
-        differ = iCalDiff(oldcalendar, self.calendar)
-        change_allowed, no_itip = differ.attendeeMerge(self.attendee)
+        differ = iCalDiff(oldcalendar, self.calendar, self.do_smart_merge)
+        change_allowed, no_itip = differ.attendeeDiff(self.attendee)
         if not change_allowed:
             log.error("Attendee '%s' is not allowed to make an unauthorized change to an organized event: UID:%s" % (self.attendeePrincipal, self.uid,))
             raise HTTPError(ErrorResponse(responsecode.FORBIDDEN, (caldav_namespace, "valid-attendee-change")))

Modified: CalendarServer/trunk/twistedcaldav/scheduling/itip.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/itip.py	2008-11-11 01:35:30 UTC (rev 3359)
+++ CalendarServer/trunk/twistedcaldav/scheduling/itip.py	2008-11-11 01:38:43 UTC (rev 3360)
@@ -90,7 +90,7 @@
         
         # Merge Organizer data with Attendee's own changes (VALARMs, Comment only for now).
         from twistedcaldav.scheduling.icaldiff import iCalDiff
-        props_changed, rids = iCalDiff(calendar, itip_message).whatIsDifferent()
+        props_changed, rids = iCalDiff(calendar, itip_message, False).whatIsDifferent()
 
         # Different behavior depending on whether a master component is present or not
         current_master = calendar.masterComponent()

Modified: CalendarServer/trunk/twistedcaldav/scheduling/processing.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/processing.py	2008-11-11 01:35:30 UTC (rev 3359)
+++ CalendarServer/trunk/twistedcaldav/scheduling/processing.py	2008-11-11 01:38:43 UTC (rev 3360)
@@ -331,6 +331,11 @@
                         customxml.Update(*update_details),
                     ),
                 )
+                
+                # Refresh from another Attendee should not have Inbox item
+                if hasattr(self.request, "doing_attendee_refresh"):
+                    autoprocessed = True
+
                 result = (True, autoprocessed, changes,)
                 
             else:
@@ -537,6 +542,7 @@
                      isiTIP = False,
                      allowImplicitSchedule = False,
                      internal_request = True,
+                     processing_organizer = self.isOrganizerReceivingMessage(),
                  ).run()
     
         returnValue(newchild)

Modified: CalendarServer/trunk/twistedcaldav/scheduling/test/test_icaldiff.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/scheduling/test/test_icaldiff.py	2008-11-11 01:35:30 UTC (rev 3359)
+++ CalendarServer/trunk/twistedcaldav/scheduling/test/test_icaldiff.py	2008-11-11 01:38:43 UTC (rev 3360)
@@ -13,10 +13,12 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 ##
-from twistedcaldav.scheduling.icaldiff import iCalDiff
 
 from twistedcaldav.ical import Component
+from twistedcaldav.scheduling.icaldiff import iCalDiff
 import twistedcaldav.test.util
+from difflib import unified_diff
+
 import itertools
 
 class ICalDiff (twistedcaldav.test.util.TestCase):
@@ -469,7 +471,7 @@
         )
 
         for description, calendar1, calendar2, result in data:
-            differ = iCalDiff(Component.fromString(calendar1), Component.fromString(calendar2))
+            differ = iCalDiff(Component.fromString(calendar1), Component.fromString(calendar2), False)
             self.assertEqual(differ.organizerDiff(), result, msg=description)
 
     def test_attendee_diff_simple(self):
@@ -830,8 +832,8 @@
         )
 
         for description, calendar1, calendar2, attendee, result in data:
-            differ = iCalDiff(Component.fromString(calendar1), Component.fromString(calendar2))
-            self.assertEqual(differ.attendeeMerge(attendee), result, msg=description)
+            differ = iCalDiff(Component.fromString(calendar1), Component.fromString(calendar2), False)
+            self.assertEqual(differ.attendeeDiff(attendee), result, msg=description)
 
     def test_attendee_diff_complex(self):
         
@@ -1162,8 +1164,8 @@
         )
 
         for description, calendar1, calendar2, attendee, result in data:
-            differ = iCalDiff(Component.fromString(calendar1), Component.fromString(calendar2))
-            self.assertEqual(differ.attendeeMerge(attendee), result, msg=description)
+            differ = iCalDiff(Component.fromString(calendar1), Component.fromString(calendar2), False)
+            self.assertEqual(differ.attendeeDiff(attendee), result, msg=description)
 
     def test_what_is_different(self):
         
@@ -2064,10 +2066,1455 @@
         )
         
         for description, calendar1, calendar2, changes, rids in itertools.chain(data1, data2, data3,):
-            differ = iCalDiff(Component.fromString(calendar1), Component.fromString(calendar2))
+            differ = iCalDiff(Component.fromString(calendar1), Component.fromString(calendar2), False)
             expected_changes = set(changes)
             expected_rids = set(rids) if rids else None
             got_changes, got_rids = differ.whatIsDifferent()
             self.assertEqual(got_changes, expected_changes, msg="%s expected changes: '%s', got: '%s'" % (description, expected_changes, got_changes,))
             self.assertEqual(got_rids, expected_rids, msg="%s expected R-IDs: '%s', got: '%s'" % (description, expected_rids, got_rids,))
         
+    def test_organizer_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: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: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;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: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
+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;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: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.5 Simple component, incompatible 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;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:20080601T130000Z
+DTEND:20080601T140000Z
+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:20080601T130000Z
+DTEND:20080601T140000Z
+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
+""",
+            ),
+        )
+        
+        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: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
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080603T120000Z
+DTSTART:20080603T120000Z
+DTEND:20080603T130000Z
+SUMMARY:Test - 3
+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
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080603T120000Z
+DTSTART:20080603T120000Z
+DTEND:20080603T130000Z
+SUMMARY:Test - 3
+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.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;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: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
+""",
+            ),
+            (
+                "#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;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: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
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080604T120000Z
+DTSTART:20080604T120000Z
+DTEND:20080604T130000Z
+SUMMARY:Test - 4
+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:20080604T120000Z
+DTSTART:20080604T120000Z
+DTEND:20080604T130000Z
+SUMMARY:Test - 4
+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.5 Simple recurring component, both instance incompatible 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;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: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
+BEGIN:VEVENT
+UID:12345-67890
+RECURRENCE-ID:20080604T120000Z
+DTSTART:20080604T130000Z
+DTEND:20080604T140000Z
+SUMMARY:Test - 4
+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:20080604T120000Z
+DTSTART:20080604T130000Z
+DTEND:20080604T140000Z
+SUMMARY:Test - 4
+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
+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
+""",
+            ),
+        )
+        
+        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.organizerDiff()
+
+            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")))))

Modified: CalendarServer/trunk/twistedcaldav/static.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/static.py	2008-11-11 01:35:30 UTC (rev 3359)
+++ CalendarServer/trunk/twistedcaldav/static.py	2008-11-11 01:38:43 UTC (rev 3360)
@@ -40,9 +40,10 @@
 import errno
 from urlparse import urlsplit
 
-from twisted.internet.defer import fail, succeed, inlineCallbacks, returnValue
+from twisted.internet.defer import fail, succeed, inlineCallbacks, returnValue,\
+    maybeDeferred
 from twisted.python.failure import Failure
-from twisted.web2 import responsecode
+from twisted.web2 import responsecode, http, http_headers
 from twisted.web2.http import HTTPError, StatusResponse
 from twisted.web2.dav import davxml
 from twisted.web2.dav.fileop import mkcollection, rmdir
@@ -56,7 +57,8 @@
 from twistedcaldav import customxml
 from twistedcaldav.caldavxml import caldav_namespace
 from twistedcaldav.config import config
-from twistedcaldav.customxml import TwistedCalendarAccessProperty
+from twistedcaldav.customxml import TwistedCalendarAccessProperty,\
+    TwistedScheduleMatchETags
 from twistedcaldav.extensions import DAVFile, DirectoryPrincipalPropertySearchMixIn
 from twistedcaldav.extensions import CachingXattrPropertyStore
 from twistedcaldav.freebusyurl import FreeBusyURLResource
@@ -92,6 +94,51 @@
         else:
             return super(CalDAVFile, self).__repr__()
 
+    def checkPreconditions(self, request):
+        """
+        We override the base class to handle the special implicit scheduling weak ETag behavior
+        for compatibility with old clients using If-Match.
+        """
+        
+        if config.Scheduling["CalDAV"]["ScheduleTagCompatibility"]:
+            
+            if self.exists() and self.hasDeadProperty(TwistedScheduleMatchETags):
+                etags = self.readDeadProperty(TwistedScheduleMatchETags).children
+                if len(etags) > 1:
+                    # This is almost verbatim from twisted.web2.static.checkPreconditions
+                    if request.method not in ("GET", "HEAD"):
+                        
+                        # Loop over each tag and succeed if any one matches, else re-raise last exception
+                        exists = self.exists()
+                        last_modified = self.lastModified()
+                        last_exception = None
+                        for etag in etags:
+                            try:
+                                http.checkPreconditions(
+                                    request,
+                                    entityExists = exists,
+                                    etag = http_headers.ETag(etag),
+                                    lastModified = last_modified,
+                                )
+                            except HTTPError, e:
+                                last_exception = e
+                            else:
+                                break
+                        else:
+                            if last_exception:
+                                raise last_exception
+            
+                    # Check per-method preconditions
+                    method = getattr(self, "preconditions_" + request.method, None)
+                    if method:
+                        response = maybeDeferred(method, request)
+                        response.addCallback(lambda _: request)
+                        return response
+                    else:
+                        return None
+
+        return super(CalDAVFile, self).checkPreconditions(request)
+
     def deadProperties(self):
         if not hasattr(self, "_dead_properties"):
             self._dead_properties = CachingXattrPropertyStore(self)

Modified: CalendarServer/trunk/twistedcaldav/test/test_collectioncontents.py
===================================================================
--- CalendarServer/trunk/twistedcaldav/test/test_collectioncontents.py	2008-11-11 01:35:30 UTC (rev 3359)
+++ CalendarServer/trunk/twistedcaldav/test/test_collectioncontents.py	2008-11-11 01:38:43 UTC (rev 3360)
@@ -50,7 +50,7 @@
 
         # Need to not do implicit behavior during these tests
         def _fakeDoImplicitScheduling(self):
-            return False, False
+            return False, False, False
         
         StoreCalendarObjectResource.doImplicitScheduling = _fakeDoImplicitScheduling
 
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <http://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20081110/733ae6a4/attachment-0001.html>


More information about the calendarserver-changes mailing list