[CalendarServer-changes] [15309] CalendarServer/branches/release/CalendarServer-5.4-dev

source_changes at macosforge.org source_changes at macosforge.org
Fri Nov 13 09:49:04 PST 2015


Revision: 15309
          http://trac.calendarserver.org//changeset/15309
Author:   cdaboo at apple.com
Date:     2015-11-13 09:49:04 -0800 (Fri, 13 Nov 2015)
Log Message:
-----------
Enhanced X- property/parameter whitelisting.

Modified Paths:
--------------
    CalendarServer/branches/release/CalendarServer-5.4-dev/conf/caldavd-test.plist
    CalendarServer/branches/release/CalendarServer-5.4-dev/twistedcaldav/ical.py
    CalendarServer/branches/release/CalendarServer-5.4-dev/twistedcaldav/stdconfig.py
    CalendarServer/branches/release/CalendarServer-5.4-dev/txdav/caldav/datastore/scheduling/icaldiff.py
    CalendarServer/branches/release/CalendarServer-5.4-dev/txdav/caldav/datastore/scheduling/itip.py
    CalendarServer/branches/release/CalendarServer-5.4-dev/txdav/caldav/datastore/scheduling/processing.py
    CalendarServer/branches/release/CalendarServer-5.4-dev/txdav/caldav/datastore/scheduling/test/test_itip.py
    CalendarServer/branches/release/CalendarServer-5.4-dev/txdav/caldav/datastore/sql.py

Modified: CalendarServer/branches/release/CalendarServer-5.4-dev/conf/caldavd-test.plist
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.4-dev/conf/caldavd-test.plist	2015-11-13 17:46:32 UTC (rev 15308)
+++ CalendarServer/branches/release/CalendarServer-5.4-dev/conf/caldavd-test.plist	2015-11-13 17:49:04 UTC (rev 15309)
@@ -709,6 +709,40 @@
         <true/>
         <key>EnablePrivateComments</key>
         <true/>
+        <key>OrganizerPublicProperties</key>
+        <array>
+        	<string>X-APPLE-DROPBOX</string>
+        	<string>X-APPLE-STRUCTURED-LOCATION</string>
+        	<string>X-TEST-ORGANIZER-PROP1</string>
+        	<string>X-TEST-ORGANIZER-PROP2</string>
+        	<string>X-TEST-ORGANIZER-PROP3</string>
+        	<string>X-TEST-ORGANIZER-PROP4</string>
+        	<string>X-TEST-ORGANIZER-PROP5</string>
+        </array>
+        <key>OrganizerPublicParameters</key>
+        <array>
+        	<string>X-TEST-ORGANIZER-PARAM1</string>
+        	<string>X-TEST-ORGANIZER-PARAM2</string>
+        	<string>X-TEST-ORGANIZER-PARAM3</string>
+        	<string>X-TEST-ORGANIZER-PARAM4</string>
+        	<string>X-TEST-ORGANIZER-PARAM5</string>
+        </array>
+        <key>AttendeePublicProperties</key>
+        <array>
+        	<string>X-TEST-ALL-PROP1</string>
+        	<string>X-TEST-ALL-PROP2</string>
+        	<string>X-TEST-ALL-PROP3</string>
+        	<string>X-TEST-ALL-PROP4</string>
+        	<string>X-TEST-ALL-PROP5</string>
+        </array>
+        <key>AttendeePublicParameters</key>
+        <array>
+        	<string>X-TEST-ALL-PARAM1</string>
+        	<string>X-TEST-ALL-PARAM2</string>
+        	<string>X-TEST-ALL-PARAM3</string>
+        	<string>X-TEST-ALL-PARAM4</string>
+        	<string>X-TEST-ALL-PARAM5</string>
+        </array>
       </dict>
 
       <!-- iSchedule protocol options -->

Modified: CalendarServer/branches/release/CalendarServer-5.4-dev/twistedcaldav/ical.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.4-dev/twistedcaldav/ical.py	2015-11-13 17:46:32 UTC (rev 15308)
+++ CalendarServer/branches/release/CalendarServer-5.4-dev/twistedcaldav/ical.py	2015-11-13 17:49:04 UTC (rev 15309)
@@ -78,6 +78,11 @@
 PERUSER_UID = "X-CALENDARSERVER-PERUSER-UID"
 PERINSTANCE_COMPONENT = "X-CALENDARSERVER-PERINSTANCE"
 
+PRIVATE_COMMENT = "X-CALENDARSERVER-PRIVATE-COMMENT"
+ATTENDEE_COMMENT = "X-CALENDARSERVER-ATTENDEE-COMMENT"
+ATTENDEE_COMMENT_REF = "X-CALENDARSERVER-ATTENDEE-REF"
+DTSTAMP_PARAM = "X-CALENDARSERVER-DTSTAMP"
+
 # 2445 default values and parameters
 # Structure: propname: (<default value>, <parameter defaults dict>)
 
@@ -964,6 +969,16 @@
             self._markAsDirty()
 
 
+    def removeProperties(self, name):
+        """
+        remove all properties with name
+        @param name: the name of the properties to remove.
+        """
+        self._pycalendar.removeProperties(name)
+        self._pycalendar.finalise()
+        self._markAsDirty()
+
+
     def removeAllPropertiesWithName(self, pname):
         """
         Remove all properties with the given name from all components.
@@ -2866,24 +2881,25 @@
                 self.removeComponent(component)
 
 
-    def removeXProperties(self, keep_properties=(), remove_x_parameters=True, do_subcomponents=True):
+    def removeXProperties(self, keep_properties=(), keep_parameters=(), do_subcomponents=True):
         """
         Remove all X- properties except the specified ones
         """
 
         if do_subcomponents and self.name() == "VCALENDAR":
             for component in self.subcomponents():
-                component.removeXProperties(keep_properties, remove_x_parameters, do_subcomponents=False)
+                component.removeXProperties(keep_properties, keep_parameters, do_subcomponents=False)
         else:
             if self.name() in ignoredComponents:
                 return
             for p in tuple(self.properties()):
-                xpname = p.name().startswith("X-")
-                if xpname and p.name() not in keep_properties:
+                pname = p.name()
+                xpname = pname.startswith("X-")
+                if xpname and pname not in keep_properties:
                     self.removeProperty(p)
-                elif not xpname and remove_x_parameters:
+                elif not xpname:
                     for paramname in p.parameterNames():
-                        if paramname.startswith("X-"):
+                        if paramname.startswith("X-") and paramname not in keep_parameters:
                             p.removeParameter(paramname)
 
 
@@ -3425,8 +3441,8 @@
                     return True
         else:
             attendee_refs = set()
-            for prop in tuple(self.properties("X-CALENDARSERVER-ATTENDEE-COMMENT")):
-                ref = prop.parameterValue("X-CALENDARSERVER-ATTENDEE-REF")
+            for prop in tuple(self.properties(ATTENDEE_COMMENT)):
+                ref = prop.parameterValue(ATTENDEE_COMMENT_REF)
                 if ref in attendee_refs:
                     if doFix:
                         self.removeProperty(prop)

Modified: CalendarServer/branches/release/CalendarServer-5.4-dev/twistedcaldav/stdconfig.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.4-dev/twistedcaldav/stdconfig.py	2015-11-13 17:46:32 UTC (rev 15308)
+++ CalendarServer/branches/release/CalendarServer-5.4-dev/twistedcaldav/stdconfig.py	2015-11-13 17:49:04 UTC (rev 15309)
@@ -685,6 +685,14 @@
                 "X-APPLE-DROPBOX",
                 "X-APPLE-STRUCTURED-LOCATION",
             ],
+            "OrganizerPublicParameters"  : [     # Names of X- iCalendar parameters that are sent from ORGANIZER to ATTENDEE
+            ],
+            "AttendeePublicProperties"  : [     # Names of X- iCalendar properties that are sent from ATTENDEE to ORGANIZER
+                                                # These are also implicitly added to OrganizerPublicProperties
+            ],
+            "AttendeePublicParameters"  : [     # Names of X- iCalendar parameters that are sent from ATTENDEE to ORGANIZER
+                                                # These are also implicitly added to OrganizerPublicParameters
+            ],
         },
 
         "iSchedule": {

Modified: CalendarServer/branches/release/CalendarServer-5.4-dev/txdav/caldav/datastore/scheduling/icaldiff.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.4-dev/txdav/caldav/datastore/scheduling/icaldiff.py	2015-11-13 17:46:32 UTC (rev 15308)
+++ CalendarServer/branches/release/CalendarServer-5.4-dev/txdav/caldav/datastore/scheduling/icaldiff.py	2015-11-13 17:49:04 UTC (rev 15309)
@@ -23,7 +23,7 @@
 
 from twistedcaldav import accounting
 from twistedcaldav.config import config
-from twistedcaldav.ical import Component, Property
+from twistedcaldav.ical import Component, Property, PRIVATE_COMMENT, DTSTAMP_PARAM
 from txdav.caldav.datastore.scheduling.cuaddress import normalizeCUAddr
 from txdav.caldav.datastore.scheduling.itip import iTipGenerator
 
@@ -41,7 +41,6 @@
     change is being triggered by an Organizer or an Attendee.
     """
 
-
     def __init__(self, oldcalendar, newcalendar, smart_merge, forceTRANSP=False):
         """
         Note that this object will always duplicate the calendar objects when doing
@@ -83,7 +82,10 @@
                 "DTSTAMP",
                 "LAST-MODIFIED",
             ))
-            calendar.removeXProperties(keep_properties=config.Scheduling.CalDAV.OrganizerPublicProperties)
+            calendar.removeXProperties(keep_properties=(
+                config.Scheduling.CalDAV.OrganizerPublicProperties +
+                config.Scheduling.CalDAV.AttendeePublicProperties
+            ))
             calendar.removePropertyParameters("ATTENDEE", ("RSVP", "SCHEDULE-STATUS", "SCHEDULE-FORCE-SEND",))
             calendar.normalizeAll()
             return calendar
@@ -356,7 +358,7 @@
 
                     # If smart_merge is happening, then derive an instance in the new data as the change in the old
                     # data is valid and likely due to some other attendee changing their status.
-                    if  self.smart_merge:
+                    if self.smart_merge:
                         newOverride = self.newcalendar.deriveInstance(rid, allowCancelled=True)
                         if newOverride is None:
                             self._logDiffError("attendeeMerge: Could not derive instance for uncancelled component: %s" % (key,))
@@ -506,7 +508,7 @@
 
             # If PARTSTAT was changed by the attendee, add a timestamp if needed
             if config.Scheduling.Options.TimestampAttendeePartStatChanges:
-                serverAttendee.setParameter("X-CALENDARSERVER-DTSTAMP", PyCalendarDateTime.getNowUTC().getText())
+                serverAttendee.setParameter(DTSTAMP_PARAM, PyCalendarDateTime.getNowUTC().getText())
             serverAttendee.removeParameter("X-CALENDARSERVER-AUTO")
 
             replyNeeded = True
@@ -527,14 +529,26 @@
             else:
                 serverAttendee.setParameter("RSVP", "TRUE")
 
+        for pname in config.Scheduling.CalDAV.AttendeePublicParameters:
+            serverValue = serverAttendee.parameterValue(pname)
+            clientValue = clientAttendee.parameterValue(pname)
+            if serverValue != clientValue:
+                if clientValue is None:
+                    serverAttendee.removeParameter(pname)
+                else:
+                    serverAttendee.setParameter(pname, clientValue)
+                replyNeeded = True
+
         # 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("COMPLETED", serverComponent, clientComponent)
         for pname in config.Scheduling.CalDAV.PerAttendeeProperties:
             self._transferProperty(pname, serverComponent, clientComponent)
+        replyNeeded |= self._transferProperty(PRIVATE_COMMENT, serverComponent, clientComponent)
+        for pname in config.Scheduling.CalDAV.AttendeePublicProperties:
+            replyNeeded |= self._transferProperty(pname, serverComponent, clientComponent)
 
         # Dropbox - this now never returns false
         if config.EnableDropBox:
@@ -621,8 +635,8 @@
             duration = component.getProperty("DURATION")
 
             timeRange = PyCalendarPeriod(
-                start=dtstart.value()  if dtstart  is not None else None,
-                end=dtend.value()    if dtend    is not None else None,
+                start=dtstart.value() if dtstart is not None else None,
+                end=dtend.value() if dtend is not None else None,
                 duration=duration.value() if duration is not None else None,
             )
             newdue = None
@@ -633,7 +647,7 @@
 
             if dtstart or duration:
                 timeRange = PyCalendarPeriod(
-                    start=dtstart.value()  if dtstart  is not None else None,
+                    start=dtstart.value() if dtstart is not None else None,
                     duration=duration.value() if duration is not None else None,
                 )
             else:
@@ -838,7 +852,7 @@
                 "DTSTAMP",
                 "CREATED",
                 "LAST-MODIFIED",
-                "X-CALENDARSERVER-PRIVATE-COMMENT",
+                PRIVATE_COMMENT,
             ):
                 continue
             propsChanged.setdefault(prop.name(), set())

Modified: CalendarServer/branches/release/CalendarServer-5.4-dev/txdav/caldav/datastore/scheduling/itip.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.4-dev/txdav/caldav/datastore/scheduling/itip.py	2015-11-13 17:46:32 UTC (rev 15308)
+++ CalendarServer/branches/release/CalendarServer-5.4-dev/txdav/caldav/datastore/scheduling/itip.py	2015-11-13 17:49:04 UTC (rev 15309)
@@ -31,10 +31,13 @@
 
 from twistedcaldav.config import config
 from twistedcaldav.ical import Property, iCalendarProductID, Component, \
-    ignoredComponents
+    ignoredComponents, PRIVATE_COMMENT, ATTENDEE_COMMENT, ATTENDEE_COMMENT_REF, \
+    DTSTAMP_PARAM
 
 from pycalendar.datetime import PyCalendarDateTime
 
+from collections import namedtuple
+
 log = Logger()
 
 __all__ = [
@@ -124,13 +127,13 @@
         current_master = calendar.masterComponent()
         if current_master:
             valarms = [comp for comp in current_master.subcomponents() if comp.name() == "VALARM"]
-            private_comments = tuple(current_master.properties("X-CALENDARSERVER-PRIVATE-COMMENT"))
+            private_comments = tuple(current_master.properties(PRIVATE_COMMENT))
             transps = tuple(current_master.properties("TRANSP"))
             completeds = tuple(current_master.properties("COMPLETED"))
             organizer = current_master.getProperty("ORGANIZER")
             organizer_schedule_status = organizer.parameterValue("SCHEDULE-STATUS", None) if organizer else None
             attendee = current_master.getAttendeeProperty((recipient,))
-            attendee_dtstamp = attendee.parameterValue("X-CALENDARSERVER-DTSTAMP") if attendee else None
+            attendee_dtstamp = attendee.parameterValue(DTSTAMP_PARAM) if attendee else None
             other_props = {}
             for pname in config.Scheduling.CalDAV.PerAttendeeProperties:
                 props = tuple(current_master.properties(pname))
@@ -310,6 +313,11 @@
             return True, False, rids
 
 
+    # Tuple used to hold information about what an ATTENDEE changed in their REPLY
+    # "params" indicates which parameters in the ATTENDEE property changes
+    # "props" indicates which properties changed
+    ReplyChanges = namedtuple("ReplyChanges", ("params", "props"))
+
     @staticmethod
     def processReply(itip_message, calendar):
         """
@@ -340,13 +348,13 @@
         old_master = calendar.masterComponent()
         new_master = itip_message.masterComponent()
         attendees = set()
-        rids = set()
+        rids = []
         if new_master is not None and old_master is not None:
-            attendee, partstat, private_comment = iTipProcessing.updateAttendeeData(new_master, old_master)
+            attendee, reply_changes = iTipProcessing.updateAttendeeData(new_master, old_master)
             if attendee:
                 attendees.add(attendee)
-                if partstat or private_comment:
-                    rids.add(("", partstat, private_comment,))
+                if reply_changes is not None:
+                    rids.append(("", reply_changes,))
 
         # Make sure all overridden components in the organizer's copy have matching overridden components
         # in the iTIP message
@@ -392,11 +400,11 @@
                     log.error("Ignoring instance: %s in iTIP REPLY for: %s" % (rid, itip_message.resourceUID()))
                     continue
 
-            attendee, partstat, private_comment = iTipProcessing.updateAttendeeData(itip_component, match_component)
+            attendee, reply_changes = iTipProcessing.updateAttendeeData(itip_component, match_component)
             if attendee:
                 attendees.add(attendee)
-                if rids is not None and (partstat or private_comment):
-                    rids.add((rid.getText(), partstat, private_comment,))
+                if rids is not None and reply_changes is not None:
+                    rids.append((rid.getText(), reply_changes,))
 
         # Check for an invalid instance by itself
         len_attendees = len(attendees)
@@ -419,11 +427,13 @@
         @type from_component: L{Component}
         @param to_component: component to copy to
         @type to_component: L{Component}
+
+        @return: tuple of attendee property value and reply changes
+        @rtype: L{tuple} of L{str}, L{ReplyChanges}
         """
 
         # Track what changed
-        partstat_changed = False
-        private_comment_changed = False
+        reply_changes = iTipProcessing.ReplyChanges([], [])
 
         # Get REQUEST-STATUS as we need to write that into the saved ATTENDEE property
         reqstatus = tuple(from_component.properties("REQUEST-STATUS"))
@@ -436,7 +446,7 @@
         attendees = tuple(from_component.properties("ATTENDEE"))
         if len(attendees) != 1:
             log.error("There must be one and only one ATTENDEE property in a REPLY\n%s" % (str(from_component),))
-            return None, False, False
+            return None, None
 
         attendee = attendees[0]
         partstat = attendee.parameterValue("PARTSTAT", "NEEDS-ACTION")
@@ -444,28 +454,40 @@
         # Now find matching ATTENDEE in to_component
         existing_attendee = to_component.getAttendeeProperty((attendee.value(),))
         if existing_attendee:
+            # Look for change to partstat
             oldpartstat = existing_attendee.parameterValue("PARTSTAT", "NEEDS-ACTION")
             existing_attendee.setParameter("PARTSTAT", partstat)
             existing_attendee.setParameter("SCHEDULE-STATUS", reqstatus)
-            partstat_changed = (oldpartstat != partstat)
+            if oldpartstat != partstat:
+                reply_changes.params.append("PARTSTAT")
 
-            # Always delete RSVP on PARTSTAT change
-            if partstat_changed:
+                # Always delete RSVP on PARTSTAT change
                 try:
                     existing_attendee.removeParameter("RSVP")
                 except KeyError:
                     pass
 
+            # Look for change to X- parameters
+            for paramname in config.Scheduling.CalDAV.AttendeePublicParameters:
+                oldparam = existing_attendee.parameterValue(paramname)
+                newparam = attendee.parameterValue(paramname)
+                if oldparam != newparam:
+                    if newparam is None:
+                        existing_attendee.removeParameter(paramname)
+                    else:
+                        existing_attendee.setParameter(paramname, newparam)
+                    reply_changes.params.append(paramname)
+
             # Handle attendee comments
             if config.Scheduling.CalDAV.get("EnablePrivateComments", True):
                 # Look for X-CALENDARSERVER-PRIVATE-COMMENT property in iTIP component (State 1 in spec)
-                attendee_comment = tuple(from_component.properties("X-CALENDARSERVER-PRIVATE-COMMENT"))
+                attendee_comment = tuple(from_component.properties(PRIVATE_COMMENT))
                 attendee_comment = attendee_comment[0] if len(attendee_comment) else None
 
                 # Look for matching X-CALENDARSERVER-ATTENDEE-COMMENT property in existing data (State 2 in spec)
-                private_comments = tuple(to_component.properties("X-CALENDARSERVER-ATTENDEE-COMMENT"))
+                private_comments = tuple(to_component.properties(ATTENDEE_COMMENT))
                 for comment in private_comments:
-                    attendeeref = comment.parameterValue("X-CALENDARSERVER-ATTENDEE-REF")
+                    attendeeref = comment.parameterValue(ATTENDEE_COMMENT_REF)
                     if attendeeref == attendee.value():
                         private_comment = comment
                         break
@@ -484,22 +506,22 @@
                 # We now remove the private comment on the organizer's side if the attendee removed it
                 to_component.removeProperty(private_comment)
 
-                private_comment_changed = True
+                reply_changes.props.append(PRIVATE_COMMENT)
 
             elif attendee_comment is not None and private_comment is None:
 
                 # Add new property
                 private_comment = Property(
-                    "X-CALENDARSERVER-ATTENDEE-COMMENT",
+                    ATTENDEE_COMMENT,
                     attendee_comment.value(),
                     params={
-                        "X-CALENDARSERVER-ATTENDEE-REF": attendee.value(),
-                        "X-CALENDARSERVER-DTSTAMP": PyCalendarDateTime.getNowUTC().getText(),
+                        ATTENDEE_COMMENT_REF: attendee.value(),
+                        DTSTAMP_PARAM: PyCalendarDateTime.getNowUTC().getText(),
                     }
                 )
                 to_component.addProperty(private_comment)
 
-                private_comment_changed = True
+                reply_changes.props.append(PRIVATE_COMMENT)
 
             else:
                 # Only change if different
@@ -508,17 +530,31 @@
                     private_comment.removeAllParameters()
 
                     # Add default parameters
-                    private_comment.setParameter("X-CALENDARSERVER-ATTENDEE-REF", attendee.value())
-                    private_comment.setParameter("X-CALENDARSERVER-DTSTAMP", PyCalendarDateTime.getNowUTC().getText())
+                    private_comment.setParameter(ATTENDEE_COMMENT_REF, attendee.value())
+                    private_comment.setParameter(DTSTAMP_PARAM, PyCalendarDateTime.getNowUTC().getText())
 
                     # Set new value
                     private_comment.setValue(attendee_comment.value())
 
-                    private_comment_changed = True
+                    reply_changes.props.append(PRIVATE_COMMENT)
 
-        return attendee.value(), partstat_changed, private_comment_changed
+        for propname in config.Scheduling.CalDAV.AttendeePublicProperties:
+            # Copy any property in the incoming component to the existing one.
+            # We do not currently delete anything in the existing component.
+            # We also remove all properties that match the name of the incoming one
+            # (i.e. we do not allow multi-occurring properties.
+            copy_props = tuple(from_component.properties(propname))
+            if copy_props:
+                to_component.removeProperties(propname)
+                for prop in copy_props:
+                    to_component.addProperty(prop.duplicate())
+                reply_changes.props.append(propname)
 
+        if len(reply_changes.props) == 0 and len(reply_changes.params) == 0:
+            reply_changes = None
+        return attendee.value(), reply_changes
 
+
     @staticmethod
     def transferItems(from_calendar, to_component, valarms, private_comments, transps, completeds, organizer_schedule_status, attendee_dtstamp, other_props, recipient, remove_matched=False):
         """
@@ -558,13 +594,13 @@
         matched = from_calendar.overriddenComponent(rid)
         if matched:
             valarms = [comp for comp in matched.subcomponents() if comp.name() == "VALARM"]
-            private_comments = tuple(matched.properties("X-CALENDARSERVER-PRIVATE-COMMENT"))
+            private_comments = tuple(matched.properties(PRIVATE_COMMENT))
             transps = tuple(matched.properties("TRANSP"))
             completeds = tuple(matched.properties("COMPLETED"))
             organizer = matched.getProperty("ORGANIZER")
             organizer_schedule_status = organizer.parameterValue("SCHEDULE-STATUS", None) if organizer else None
             attendee = matched.getAttendeeProperty((recipient,))
-            attendee_dtstamp = attendee.parameterValue("X-CALENDARSERVER-DTSTAMP") if attendee else None
+            attendee_dtstamp = attendee.parameterValue(DTSTAMP_PARAM) if attendee else None
             other_props = {}
             for pname in config.Scheduling.CalDAV.PerAttendeeProperties:
                 props = tuple(matched.properties(pname))
@@ -631,7 +667,7 @@
         # into the new one. But first remove any of the stuff we want to copy from
         # the component being copied to.
         to_component.removeAlarms()
-        to_component.removeProperty("X-CALENDARSERVER-PRIVATE-COMMENT")
+        to_component.removeProperty(PRIVATE_COMMENT)
         to_component.removeProperty("TRANSP")
         to_component.removeProperty("COMPLETED")
         for propname in other_props.keys():
@@ -653,7 +689,7 @@
         attendee = to_component.getAttendeeProperty((recipient,))
 
         if attendee_dtstamp and attendee:
-            attendee.setParameter("X-CALENDARSERVER-DTSTAMP", attendee_dtstamp)
+            attendee.setParameter(DTSTAMP_PARAM, attendee_dtstamp)
 
         return False
 
@@ -928,7 +964,7 @@
         itip.removeAlarms()
 
         # Remove all but essential properties
-        itip.filterProperties(keep=(
+        keep_properties = (
             "UID",
             "RECURRENCE-ID",
             "SEQUENCE",
@@ -942,11 +978,13 @@
             "EXDATE",
             "ORGANIZER",
             "ATTENDEE",
-            "X-CALENDARSERVER-PRIVATE-COMMENT",
             "SUMMARY",
             "LOCATION",
             "DESCRIPTION",
-        ))
+            PRIVATE_COMMENT,
+        )
+        keep_properties += tuple(config.Scheduling.CalDAV.AttendeePublicProperties)
+        itip.filterProperties(keep=keep_properties)
 
         # Now set each ATTENDEE's PARTSTAT to DECLINED
         if force_decline:
@@ -982,14 +1020,16 @@
         # Component properties - remove all X- except for those specified
         if not reply:
             # Organizer properties that need to go to the Attendees
-            keep_properties = config.Scheduling.CalDAV.OrganizerPublicProperties
+            keep_properties = config.Scheduling.CalDAV.OrganizerPublicProperties + config.Scheduling.CalDAV.AttendeePublicProperties
+            keep_parameters = config.Scheduling.CalDAV.OrganizerPublicParameters + config.Scheduling.CalDAV.AttendeePublicParameters
         else:
             # Attendee properties that need to go to the Organizer
-            keep_properties = ("X-CALENDARSERVER-PRIVATE-COMMENT",)
-        itip.removeXProperties(keep_properties=keep_properties)
+            keep_properties = (PRIVATE_COMMENT,) + tuple(config.Scheduling.CalDAV.AttendeePublicProperties)
+            keep_parameters = config.Scheduling.CalDAV.AttendeePublicParameters
+        itip.removeXProperties(keep_properties=keep_properties, keep_parameters=keep_parameters)
 
         # Property Parameters
-        itip.removePropertyParameters("ATTENDEE", ("SCHEDULE-AGENT", "SCHEDULE-STATUS", "SCHEDULE-FORCE-SEND", "X-CALENDARSERVER-DTSTAMP",))
+        itip.removePropertyParameters("ATTENDEE", ("SCHEDULE-AGENT", "SCHEDULE-STATUS", "SCHEDULE-FORCE-SEND", DTSTAMP_PARAM,))
         itip.removePropertyParameters("ORGANIZER", ("SCHEDULE-AGENT", "SCHEDULE-STATUS", "SCHEDULE-FORCE-SEND",))
 
 

Modified: CalendarServer/branches/release/CalendarServer-5.4-dev/txdav/caldav/datastore/scheduling/processing.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.4-dev/txdav/caldav/datastore/scheduling/processing.py	2015-11-13 17:46:32 UTC (rev 15308)
+++ CalendarServer/branches/release/CalendarServer-5.4-dev/txdav/caldav/datastore/scheduling/processing.py	2015-11-13 17:49:04 UTC (rev 15309)
@@ -26,7 +26,7 @@
 
 from twistedcaldav import customxml, caldavxml
 from twistedcaldav.config import config
-from twistedcaldav.ical import Property
+from twistedcaldav.ical import Property, DTSTAMP_PARAM
 from twistedcaldav.instance import InvalidOverriddenInstanceError
 from twistedcaldav.memcachelock import MemcacheLock, MemcacheLockTimeoutError
 from twistedcaldav.memcacher import Memcacher
@@ -207,21 +207,24 @@
 
             # Build the schedule-changes XML element
             attendeeReplying, rids = processed
-            partstatChanged = False
+            refreshNeeded = False
             reply_details = (customxml.Attendee.fromString(attendeeReplying),)
 
-            for rid, partstatChanged, privateCommentChanged in sorted(rids):
+            for rid, reply_changes in sorted(rids):
                 recurrence = []
                 if rid == "":
                     recurrence.append(customxml.Master())
                 else:
                     recurrence.append(customxml.RecurrenceID.fromString(rid))
                 changes = []
-                if partstatChanged:
-                    changes.append(customxml.ChangedProperty(customxml.ChangedParameter(name="PARTSTAT"), name="ATTENDEE"))
-                    partstatChanged = True
-                if privateCommentChanged:
-                    changes.append(customxml.ChangedProperty(name="X-CALENDARSERVER-PRIVATE-COMMENT"))
+
+                for param in reply_changes.params:
+                    changes.append(customxml.ChangedProperty(customxml.ChangedParameter(name=param), name="ATTENDEE"))
+                    refreshNeeded = True
+
+                for prop in reply_changes.props:
+                    changes.append(customxml.ChangedProperty(name=prop))
+
                 recurrence.append(customxml.Changes(*changes))
                 reply_details += (customxml.Recurrence(*recurrence),)
 
@@ -235,7 +238,7 @@
             # Only update other attendees when the partstat was changed by the reply,
             # and only if the request does not indicate we should skip attendee refresh
             # (e.g. inbox item processing during migration from non-implicit server)
-            if partstatChanged and not self.noAttendeeRefresh:
+            if refreshNeeded and not self.noAttendeeRefresh:
                 # Check limit of attendees
                 if config.Scheduling.Options.AttendeeRefreshCountLimit == 0 or len(self.recipient_calendar.getAllUniqueAttendees()) <= config.Scheduling.Options.AttendeeRefreshCountLimit:
                     yield self.queueAttendeeUpdate((attendeeReplying, organizer,))
@@ -1130,7 +1133,7 @@
 
             if madeChanges:
                 attendee.setParameter("X-CALENDARSERVER-AUTO", PyCalendarDateTime.getNowUTC().getText())
-                attendee.removeParameter("X-CALENDARSERVER-DTSTAMP")
+                attendee.removeParameter(DTSTAMP_PARAM)
 
         return madeChanges
 

Modified: CalendarServer/branches/release/CalendarServer-5.4-dev/txdav/caldav/datastore/scheduling/test/test_itip.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.4-dev/txdav/caldav/datastore/scheduling/test/test_itip.py	2015-11-13 17:46:32 UTC (rev 15308)
+++ CalendarServer/branches/release/CalendarServer-5.4-dev/txdav/caldav/datastore/scheduling/test/test_itip.py	2015-11-13 17:49:04 UTC (rev 15309)
@@ -21,7 +21,7 @@
 from twisted.trial import unittest
 
 from twistedcaldav.stdconfig import config
-from twistedcaldav.ical import Component
+from twistedcaldav.ical import Component, diff_iCalStrs, normalize_iCalStr
 
 from txdav.caldav.datastore.scheduling.itip import iTipProcessing, iTipGenerator
 
@@ -34,6 +34,42 @@
     iCalendar support tests
     """
 
+    def setUp(self):
+        self.patch(config.Scheduling.CalDAV, "OrganizerPublicProperties", [
+            "X-APPLE-DROPBOX",
+            "X-APPLE-STRUCTURED-LOCATION",
+            "X-TEST-ORGANIZER-PROP1",
+            "X-TEST-ORGANIZER-PROP2",
+            "X-TEST-ORGANIZER-PROP3",
+            "X-TEST-ORGANIZER-PROP4",
+            "X-TEST-ORGANIZER-PROP5",
+        ])
+
+        self.patch(config.Scheduling.CalDAV, "OrganizerPublicParameters", [
+            "X-TEST-ORGANIZER-PARAM1",
+            "X-TEST-ORGANIZER-PARAM2",
+            "X-TEST-ORGANIZER-PARAM3",
+            "X-TEST-ORGANIZER-PARAM4",
+            "X-TEST-ORGANIZER-PARAM5",
+        ])
+
+        self.patch(config.Scheduling.CalDAV, "AttendeePublicProperties", [
+            "X-TEST-ALL-PROP1",
+            "X-TEST-ALL-PROP2",
+            "X-TEST-ALL-PROP3",
+            "X-TEST-ALL-PROP4",
+            "X-TEST-ALL-PROP5",
+        ])
+
+        self.patch(config.Scheduling.CalDAV, "AttendeePublicParameters", [
+            "X-TEST-ALL-PARAM1",
+            "X-TEST-ALL-PARAM2",
+            "X-TEST-ALL-PARAM3",
+            "X-TEST-ALL-PARAM4",
+            "X-TEST-ALL-PARAM5",
+        ])
+
+
     def test_processRequest(self):
         """
         Test iTIPProcessing.processRequest works properly for various scenarios.
@@ -1178,7 +1214,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                True, "mailto:user1 at example.com", (("", True, False),),
+                True, "mailto:user1 at example.com", (("", iTipProcessing.ReplyChanges(params=['PARTSTAT'], props=[])),),
             ),
             (
                 "#1.2 Simple component, accepted",
@@ -1219,7 +1255,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                True, "mailto:user1 at example.com", (("", True, False),),
+                True, "mailto:user1 at example.com", (("", iTipProcessing.ReplyChanges(params=['PARTSTAT'], props=[])),),
             ),
             (
                 "#1.3 Simple component, no change",
@@ -1327,7 +1363,10 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                True, "mailto:user1 at example.com", (("", True, False), ("20080801T120000Z", True, False),),
+                True, "mailto:user1 at example.com", (
+                    ("", iTipProcessing.ReplyChanges(params=['PARTSTAT'], props=[])),
+                    ("20080801T120000Z", iTipProcessing.ReplyChanges(params=['PARTSTAT'], props=[])),
+                ),
             ),
             (
                 "#2.2 Recurring component, change master only",
@@ -1388,7 +1427,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                True, "mailto:user1 at example.com", (("", True, False),),
+                True, "mailto:user1 at example.com", (("", iTipProcessing.ReplyChanges(params=['PARTSTAT'], props=[])),),
             ),
             (
                 "#2.3 Recurring component, change override only",
@@ -1450,7 +1489,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                True, "mailto:user1 at example.com", (("20080801T120000Z", True, False),),
+                True, "mailto:user1 at example.com", (("20080801T120000Z", iTipProcessing.ReplyChanges(params=['PARTSTAT'], props=[])),),
             ),
             (
                 "#3.1 Recurring component, change master/override, new override",
@@ -1532,7 +1571,11 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                True, "mailto:user1 at example.com", (("", True, False), ("20080801T120000Z", True, False), ("20080901T120000Z", True, False),),
+                True, "mailto:user1 at example.com", (
+                    ("", iTipProcessing.ReplyChanges(params=['PARTSTAT'], props=[])),
+                    ("20080801T120000Z", iTipProcessing.ReplyChanges(params=['PARTSTAT'], props=[])),
+                    ("20080901T120000Z", iTipProcessing.ReplyChanges(params=['PARTSTAT'], props=[])),
+                ),
             ),
             (
                 "#3.2 Recurring component, change master, new override",
@@ -1608,7 +1651,10 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                True, "mailto:user1 at example.com", (("", True, False), ("20080901T120000Z", True, False),),
+                True, "mailto:user1 at example.com", (
+                    ("", iTipProcessing.ReplyChanges(params=['PARTSTAT'], props=[])),
+                    ("20080901T120000Z", iTipProcessing.ReplyChanges(params=['PARTSTAT'], props=[])),
+                ),
             ),
             (
                 "#3.3 Recurring component, change override, new override",
@@ -1685,7 +1731,10 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                True, "mailto:user1 at example.com", (("20080801T120000Z", True, False), ("20080901T120000Z", True, False),),
+                True, "mailto:user1 at example.com", (
+                    ("20080801T120000Z", iTipProcessing.ReplyChanges(params=['PARTSTAT'], props=[])),
+                    ("20080901T120000Z", iTipProcessing.ReplyChanges(params=['PARTSTAT'], props=[])),
+                ),
             ),
             (
                 "#4.1 Recurring component, invalid override",
@@ -1882,7 +1931,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                True, "mailto:user1 at example.com", (("", True, False),),
+                True, "mailto:user1 at example.com", (("", iTipProcessing.ReplyChanges(params=['PARTSTAT'], props=[])),),
             ),
             (
                 "#6.2 Multiple REQUEST-STATUS",
@@ -1925,7 +1974,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                True, "mailto:user1 at example.com", (("", True, False),),
+                True, "mailto:user1 at example.com", (("", iTipProcessing.ReplyChanges(params=['PARTSTAT'], props=[])),),
             ),
             (
                 "#6.3 Bad REQUEST-STATUS",
@@ -1967,7 +2016,7 @@
 END:VEVENT
 END:VCALENDAR
 """,
-                True, "mailto:user1 at example.com", (("", True, False),),
+                True, "mailto:user1 at example.com", (("", iTipProcessing.ReplyChanges(params=['PARTSTAT'], props=[])),),
             ),
         )
 
@@ -1975,11 +2024,6 @@
             calendar = Component.fromString(calendar_txt)
             itipmsg = Component.fromString(itipmsg_txt)
             reply_success, reply_processed = iTipProcessing.processReply(itipmsg, calendar)
-#            if not description.startswith("#3.1"):
-#                continue
-#            print(description)
-#            print(str(calendar))
-#            print(str(result))
             self.assertEqual(
                 str(calendar).replace("\r", "").replace("\n ", ""),
                 str(result).replace("\n ", ""),
@@ -2010,6 +2054,663 @@
                 )
 
 
+    def test_processReply_XDash(self):
+        """
+        Test iTIPProcessing.processReply with X- property and parameter changes
+        """
+
+        data = (
+            (
+                "1.1 Simple Reply - with X- param",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user02 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHOD:REPLY
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071115T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE;X-TEST-ALL-PARAM1=p1;PARTSTAT=ACCEPTED:mailto:user02 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;X-TEST-ALL-PARAM1=p1;PARTSTAT=ACCEPTED;SCHEDULE-STATUS=2.0:mailto:user02 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                "mailto:user02 at example.com", (
+                    ("", iTipProcessing.ReplyChanges(params=['PARTSTAT', 'X-TEST-ALL-PARAM1'], props=[])),
+                ),
+            ),
+            (
+                "1.2 Simple Reply - with X- param update",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;X-TEST-ALL-PARAM1=p1;PARTSTAT=ACCEPTED;SCHEDULE-STATUS=2.0:mailto:user02 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHOD:REPLY
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071115T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE;X-TEST-ALL-PARAM1=p1-1;PARTSTAT=ACCEPTED:mailto:user02 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;X-TEST-ALL-PARAM1=p1-1;PARTSTAT=ACCEPTED;SCHEDULE-STATUS=2.0:mailto:user02 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                "mailto:user02 at example.com", (
+                    ("", iTipProcessing.ReplyChanges(params=['X-TEST-ALL-PARAM1'], props=[])),
+                ),
+            ),
+            (
+                "1.3 Simple Reply - with X- param remove",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;X-TEST-ALL-PARAM1=p1;PARTSTAT=ACCEPTED;SCHEDULE-STATUS=2.0:mailto:user02 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHOD:REPLY
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071115T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user02 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED;SCHEDULE-STATUS=2.0:mailto:user02 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                "mailto:user02 at example.com", (
+                    ("", iTipProcessing.ReplyChanges(params=['X-TEST-ALL-PARAM1'], props=[])),
+                ),
+            ),
+            (
+                "2.1 Simple Reply - with X- prop",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user02 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHOD:REPLY
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071115T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user02 at example.com
+X-TEST-ALL-PROP1:p1
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED;SCHEDULE-STATUS=2.0:mailto:user02 at example.com
+X-TEST-ALL-PROP1:p1
+END:VEVENT
+END:VCALENDAR
+""",
+                "mailto:user02 at example.com", (
+                    ("", iTipProcessing.ReplyChanges(params=['PARTSTAT'], props=["X-TEST-ALL-PROP1"])),
+                ),
+            ),
+            (
+                "2.2 Simple Reply - with X- prop update",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED;SCHEDULE-STATUS=2.0:mailto:user02 at example.com
+X-TEST-ALL-PROP1:p1
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHOD:REPLY
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071115T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user02 at example.com
+X-TEST-ALL-PROP1:p1-1
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED;SCHEDULE-STATUS=2.0:mailto:user02 at example.com
+X-TEST-ALL-PROP1:p1-1
+END:VEVENT
+END:VCALENDAR
+""",
+                "mailto:user02 at example.com", (
+                    ("", iTipProcessing.ReplyChanges(params=[], props=["X-TEST-ALL-PROP1"])),
+                ),
+            ),
+            (
+                "2.3 Simple Reply - with X- prop preserve",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED;SCHEDULE-STATUS=2.0:mailto:user02 at example.com
+X-TEST-ALL-PROP1:p1-1
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHOD:REPLY
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071115T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=DECLINED:mailto:user02 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=DECLINED;SCHEDULE-STATUS=2.0:mailto:user02 at example.com
+X-TEST-ALL-PROP1:p1-1
+END:VEVENT
+END:VCALENDAR
+""",
+                "mailto:user02 at example.com", (
+                    ("", iTipProcessing.ReplyChanges(params=["PARTSTAT"], props=[])),
+                ),
+            ),
+            (
+                "3.1 Recurrence Reply - with X- param",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user02 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHOD:REPLY
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20071116T000000Z
+DTSTART:20071116T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE;X-TEST-ALL-PARAM1=p1;PARTSTAT=ACCEPTED:mailto:user02 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user02 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20071116T000000Z
+DTSTART:20071116T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;X-TEST-ALL-PARAM1=p1;PARTSTAT=ACCEPTED;SCHEDULE-STATUS=2.0:mailto:user02 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                "mailto:user02 at example.com", (
+                    ("20071116T000000Z", iTipProcessing.ReplyChanges(params=['PARTSTAT', 'X-TEST-ALL-PARAM1'], props=[])),
+                ),
+            ),
+            (
+                "3.2 Recurrence Reply - with X- param update",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;X-TEST-ALL-PARAM1=p1;PARTSTAT=ACCEPTED;SCHEDULE-STATUS=2.0:mailto:user02 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHOD:REPLY
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20071116T000000Z
+DTSTART:20071116T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE;X-TEST-ALL-PARAM1=p1-1;PARTSTAT=ACCEPTED:mailto:user02 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;X-TEST-ALL-PARAM1=p1;PARTSTAT=ACCEPTED;SCHEDULE-STATUS=2.0:mailto:user02 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20071116T000000Z
+DTSTART:20071116T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;X-TEST-ALL-PARAM1=p1-1;PARTSTAT=ACCEPTED;SCHEDULE-STATUS=2.0:mailto:user02 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                "mailto:user02 at example.com", (
+                    ("20071116T000000Z", iTipProcessing.ReplyChanges(params=['X-TEST-ALL-PARAM1'], props=[])),
+                ),
+            ),
+            (
+                "3.3 Recurrence Reply - with X- param remove",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;X-TEST-ALL-PARAM1=p1;PARTSTAT=ACCEPTED;SCHEDULE-STATUS=2.0:mailto:user02 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHOD:REPLY
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20071116T000000Z
+DTSTART:20071116T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user02 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;X-TEST-ALL-PARAM1=p1;PARTSTAT=ACCEPTED;SCHEDULE-STATUS=2.0:mailto:user02 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20071116T000000Z
+DTSTART:20071116T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED;SCHEDULE-STATUS=2.0:mailto:user02 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                "mailto:user02 at example.com", (
+                    ("20071116T000000Z", iTipProcessing.ReplyChanges(params=['X-TEST-ALL-PARAM1'], props=[])),
+                ),
+            ),
+            (
+                "4.1 Recurrence Reply - with X- prop",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user02 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHOD:REPLY
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20071116T000000Z
+DTSTART:20071116T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user02 at example.com
+X-TEST-ALL-PROP1:p1
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=NEEDS-ACTION:mailto:user02 at example.com
+RRULE:FREQ=DAILY
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20071116T000000Z
+DTSTART:20071116T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED;SCHEDULE-STATUS=2.0:mailto:user02 at example.com
+X-TEST-ALL-PROP1:p1
+END:VEVENT
+END:VCALENDAR
+""",
+                "mailto:user02 at example.com", (
+                    ("20071116T000000Z", iTipProcessing.ReplyChanges(params=['PARTSTAT'], props=["X-TEST-ALL-PROP1"])),
+                ),
+            ),
+            (
+                "4.2 Recurrence Reply - with X- prop update",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED;SCHEDULE-STATUS=2.0:mailto:user02 at example.com
+RRULE:FREQ=DAILY
+X-TEST-ALL-PROP1:p1
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHOD:REPLY
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20071116T000000Z
+DTSTART:20071116T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED:mailto:user02 at example.com
+X-TEST-ALL-PROP1:p1-1
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED;SCHEDULE-STATUS=2.0:mailto:user02 at example.com
+RRULE:FREQ=DAILY
+X-TEST-ALL-PROP1:p1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20071116T000000Z
+DTSTART:20071116T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED;SCHEDULE-STATUS=2.0:mailto:user02 at example.com
+X-TEST-ALL-PROP1:p1-1
+END:VEVENT
+END:VCALENDAR
+""",
+                "mailto:user02 at example.com", (
+                    ("20071116T000000Z", iTipProcessing.ReplyChanges(params=[], props=["X-TEST-ALL-PROP1"])),
+                ),
+            ),
+            (
+                "4.3 Recurrence Reply - with X- prop preserve",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED;SCHEDULE-STATUS=2.0:mailto:user02 at example.com
+RRULE:FREQ=DAILY
+X-TEST-ALL-PROP1:p1
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+METHOD:REPLY
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20071116T000000Z
+DTSTART:20071116T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=DECLINED:mailto:user02 at example.com
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART:20071114T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=ACCEPTED;SCHEDULE-STATUS=2.0:mailto:user02 at example.com
+RRULE:FREQ=DAILY
+X-TEST-ALL-PROP1:p1
+END:VEVENT
+BEGIN:VEVENT
+UID:12345-67890-1
+RECURRENCE-ID:20071116T000000Z
+DTSTART:20071116T000000Z
+DTSTAMP:20071114T000000Z
+ORGANIZER:mailto:user01 at example.com
+ATTENDEE:mailto:user01 at example.com
+ATTENDEE;PARTSTAT=DECLINED;SCHEDULE-STATUS=2.0:mailto:user02 at example.com
+X-TEST-ALL-PROP1:p1
+END:VEVENT
+END:VCALENDAR
+""",
+                "mailto:user02 at example.com", (
+                    ("20071116T000000Z", iTipProcessing.ReplyChanges(params=["PARTSTAT"], props=[])),
+                ),
+            ),
+        )
+
+        for title, calendar_txt, itip_txt, changed_txt, attendee, rids in data:
+            calendar = Component.fromString(calendar_txt)
+            itip = Component.fromString(itip_txt)
+            changed = Component.fromString(changed_txt)
+
+            result, reply_processed = iTipProcessing.processReply(itip, calendar)
+            self.assertTrue(result)
+            self.assertEqual(normalize_iCalStr(changed), normalize_iCalStr(calendar), "Calendar mismatch: {}:\n{}".format(title, diff_iCalStrs(changed, calendar),))
+            reply_attendee, reply_rids, = reply_processed
+            self.assertEqual(
+                reply_attendee,
+                attendee,
+                msg=title
+            )
+            self.assertEqual(
+                tuple(sorted(list(reply_rids), key=lambda x: x[0])),
+                rids,
+                msg=title
+            )
+
+
     def test_sequenceComparison(self):
         """
         Test iTIPProcessing.sequenceComparison
@@ -2512,6 +3213,42 @@
     """
     data_dir = os.path.join(os.path.dirname(__file__), "data")
 
+    def setUp(self):
+        self.patch(config.Scheduling.CalDAV, "OrganizerPublicProperties", [
+            "X-APPLE-DROPBOX",
+            "X-APPLE-STRUCTURED-LOCATION",
+            "X-TEST-ORGANIZER-PROP1",
+            "X-TEST-ORGANIZER-PROP2",
+            "X-TEST-ORGANIZER-PROP3",
+            "X-TEST-ORGANIZER-PROP4",
+            "X-TEST-ORGANIZER-PROP5",
+        ])
+
+        self.patch(config.Scheduling.CalDAV, "OrganizerPublicParameters", [
+            "X-TEST-ORGANIZER-PARAM1",
+            "X-TEST-ORGANIZER-PARAM2",
+            "X-TEST-ORGANIZER-PARAM3",
+            "X-TEST-ORGANIZER-PARAM4",
+            "X-TEST-ORGANIZER-PARAM5",
+        ])
+
+        self.patch(config.Scheduling.CalDAV, "AttendeePublicProperties", [
+            "X-TEST-ALL-PROP1",
+            "X-TEST-ALL-PROP2",
+            "X-TEST-ALL-PROP3",
+            "X-TEST-ALL-PROP4",
+            "X-TEST-ALL-PROP5",
+        ])
+
+        self.patch(config.Scheduling.CalDAV, "AttendeePublicParameters", [
+            "X-TEST-ALL-PARAM1",
+            "X-TEST-ALL-PARAM2",
+            "X-TEST-ALL-PARAM3",
+            "X-TEST-ALL-PARAM4",
+            "X-TEST-ALL-PARAM5",
+        ])
+
+
     def test_request(self):
 
         data = (
@@ -3140,3 +3877,283 @@
         itipped = str(itipped).replace("\r", "")
         itipped = "".join([line for line in itipped.splitlines(True) if not line.startswith("DTSTAMP:")])
         self.assertEqual(filtered, itipped)
+
+
+    def test_prepareSchedulingMessage(self):
+        """
+        Make sure L{iTIPGenerator.prepareSchedulingMessage} correctly filters X-
+        properties and parameters.
+        """
+
+        data = (
+            (
+                "Nothing to filter",
+                """BEGIN:VCALENDAR
+METHOD:REQUEST
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART;TZID=UTC:20071114T000000
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+METHOD:REQUEST
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART;TZID=UTC:20071114T000000
+END:VEVENT
+END:VCALENDAR
+""",
+                False,
+            ),
+            (
+                "Filter X- property",
+                """BEGIN:VCALENDAR
+METHOD:REQUEST
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART;TZID=UTC:20071114T000000
+X-FOO:BAR
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+METHOD:REQUEST
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART;TZID=UTC:20071114T000000
+END:VEVENT
+END:VCALENDAR
+""",
+                False,
+            ),
+            (
+                "Filter X- param",
+                """BEGIN:VCALENDAR
+METHOD:REQUEST
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART;TZID=UTC:20071114T000000
+LOCATION;X-FOO=BAR:Home
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+METHOD:REQUEST
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART;TZID=UTC:20071114T000000
+LOCATION:Home
+END:VEVENT
+END:VCALENDAR
+""",
+                False,
+            ),
+            (
+                "Keep X- property",
+                """BEGIN:VCALENDAR
+METHOD:REQUEST
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART;TZID=UTC:20071114T000000
+X-TEST-ORGANIZER-PROP1:organizer
+X-TEST-ALL-PROP1:all
+X-FOO:BAR
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+METHOD:REQUEST
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART;TZID=UTC:20071114T000000
+X-TEST-ORGANIZER-PROP1:organizer
+X-TEST-ALL-PROP1:all
+END:VEVENT
+END:VCALENDAR
+""",
+                False,
+            ),
+            (
+                "Keep X- param",
+                """BEGIN:VCALENDAR
+METHOD:REQUEST
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART;TZID=UTC:20071114T000000
+LOCATION;X-TEST-ORGANIZER-PARAM1=o;X-TEST-ALL-PARAM1=a:Home
+X-TEST-ORGANIZER-PROP1;X-TEST-ORGANIZER-PARAM2=o;X-FOO=BAR:dropped-it
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+METHOD:REQUEST
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART;TZID=UTC:20071114T000000
+LOCATION;X-TEST-ORGANIZER-PARAM1=o;X-TEST-ALL-PARAM1=a:Home
+X-TEST-ORGANIZER-PROP1;X-TEST-ORGANIZER-PARAM2=o;X-FOO=BAR:dropped-it
+END:VEVENT
+END:VCALENDAR
+""",
+                False,
+            ),
+            (
+                "Nothing to filter - reply",
+                """BEGIN:VCALENDAR
+METHOD:REQUEST
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART;TZID=UTC:20071114T000000
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+METHOD:REQUEST
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART;TZID=UTC:20071114T000000
+END:VEVENT
+END:VCALENDAR
+""",
+                True,
+            ),
+            (
+                "Filter X- property - reply",
+                """BEGIN:VCALENDAR
+METHOD:REQUEST
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART;TZID=UTC:20071114T000000
+X-TEST-ORGANIZER-PROP1:organizer
+X-FOO:BAR
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+METHOD:REQUEST
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART;TZID=UTC:20071114T000000
+END:VEVENT
+END:VCALENDAR
+""",
+                True,
+            ),
+            (
+                "Filter X- param - reply",
+                """BEGIN:VCALENDAR
+METHOD:REQUEST
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART;TZID=UTC:20071114T000000
+LOCATION;X-TEST-ORGANIZER-PARAM1=o;X-FOO=BAR:Home
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+METHOD:REQUEST
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART;TZID=UTC:20071114T000000
+LOCATION:Home
+END:VEVENT
+END:VCALENDAR
+""",
+                True,
+            ),
+            (
+                "Keep X- property - reply",
+                """BEGIN:VCALENDAR
+METHOD:REQUEST
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART;TZID=UTC:20071114T000000
+X-TEST-ORGANIZER-PROP1:organizer
+X-TEST-ALL-PROP1:all
+X-FOO:BAR
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+METHOD:REQUEST
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART;TZID=UTC:20071114T000000
+X-TEST-ALL-PROP1:all
+END:VEVENT
+END:VCALENDAR
+""",
+                True,
+            ),
+            (
+                "Keep X- param - reply",
+                """BEGIN:VCALENDAR
+METHOD:REQUEST
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART;TZID=UTC:20071114T000000
+LOCATION;X-TEST-ORGANIZER-PARAM1=o;X-TEST-ALL-PARAM1=a:Home
+X-TEST-ALL-PROP1;X-TEST-ALL-PARAM2=a;X-FOO=BAR:oragnizer
+END:VEVENT
+END:VCALENDAR
+""",
+                """BEGIN:VCALENDAR
+METHOD:REQUEST
+VERSION:2.0
+PRODID:-//CALENDARSERVER.ORG//NONSGML Version 1//EN
+BEGIN:VEVENT
+UID:12345-67890-1
+DTSTART;TZID=UTC:20071114T000000
+LOCATION;X-TEST-ALL-PARAM1=a:Home
+X-TEST-ALL-PROP1;X-TEST-ALL-PARAM2=a;X-FOO=BAR:oragnizer
+END:VEVENT
+END:VCALENDAR
+""",
+                True,
+            ),
+        )
+
+        for title, original, processed, reply in data:
+            component = Component.fromString(original)
+            new_component = Component.fromString(processed)
+            iTipGenerator.prepareSchedulingMessage(component, reply)
+            self.assertEqual(normalize_iCalStr(new_component), normalize_iCalStr(component), "Failed {}:\n{}".format(title, diff_iCalStrs(new_component, component),))

Modified: CalendarServer/branches/release/CalendarServer-5.4-dev/txdav/caldav/datastore/sql.py
===================================================================
--- CalendarServer/branches/release/CalendarServer-5.4-dev/txdav/caldav/datastore/sql.py	2015-11-13 17:46:32 UTC (rev 15308)
+++ CalendarServer/branches/release/CalendarServer-5.4-dev/txdav/caldav/datastore/sql.py	2015-11-13 17:49:04 UTC (rev 15309)
@@ -53,7 +53,7 @@
 from twistedcaldav.datafilters.peruserdata import PerUserDataFilter
 from twistedcaldav.dateops import normalizeForIndex, datetimeMktime, \
     pyCalendarTodatetime, parseSQLDateToPyCalendar
-from twistedcaldav.ical import Component, InvalidICalendarDataError, Property
+from twistedcaldav.ical import Component, InvalidICalendarDataError, Property, ATTENDEE_COMMENT
 from twistedcaldav.instance import InvalidOverriddenInstanceError
 from twistedcaldav.memcacher import Memcacher
 
@@ -1785,7 +1785,7 @@
         if config.Scheduling.CalDAV.get("EnablePrivateComments", True):
             old_has_private_comments = not inserting and self.hasPrivateComment
             new_has_private_comments = component.hasPropertyInAnyComponent((
-                "X-CALENDARSERVER-ATTENDEE-COMMENT",
+                ATTENDEE_COMMENT,
             ))
 
             if old_has_private_comments and not new_has_private_comments and internal_state == ComponentUpdateState.NORMAL:
@@ -1793,7 +1793,7 @@
                 log.debug("Organizer private comment properties were entirely removed by the client. Restoring existing properties.")
                 old_calendar = (yield self.componentForUser())
                 component.transferProperties(old_calendar, (
-                    "X-CALENDARSERVER-ATTENDEE-COMMENT",
+                    ATTENDEE_COMMENT,
                 ))
 
             self.hasPrivateComment = new_has_private_comments
@@ -1803,7 +1803,7 @@
 
             # Look for properties with duplicate "X-CALENDARSERVER-ATTENDEE-REF" values in the same component
             if component.hasDuplicatePrivateComments(doFix=config.RemoveDuplicatePrivateComments) and internal_state == ComponentUpdateState.NORMAL:
-                raise DuplicatePrivateCommentsError("Duplicate X-CALENDARSERVER-ATTENDEE-COMMENT properties present.")
+                raise DuplicatePrivateCommentsError("Duplicate {} properties present.".format(ATTENDEE_COMMENT))
 
 
     @inlineCallbacks
-------------- next part --------------
An HTML attachment was scrubbed...
URL: <https://lists.macosforge.org/pipermail/calendarserver-changes/attachments/20151113/c6c78ed6/attachment-0001.html>


More information about the calendarserver-changes mailing list