def processRequest(itip_message, calendar, recipient): """ Process a METHOD=REQUEST. We need to merge per-attendee properties such as TRANPS, COMPLETED etc with the data coming from the organizer. @param itip_message: the iTIP message to process. @type itip_message: L{Component} @param calendar: the calendar object to apply the REQUEST to @type calendar: L{Component} @param recipient: the attendee calendar user address to whom the message was sent @type recipient: C{str} @return: a C{tuple} of: calendar object ready to save, or C{None} (request should be ignored) a C{set} of recurrences that changed, or C{None} """ # Check sequencing if not iTipProcessing.sequenceComparison(itip_message, calendar): # Ignore out of sequence message return None, None # Special check: if the SCHEDULE-AGENT is being changed throw away all the existing data if calendar.getOrganizerScheduleAgent() != itip_message.getOrganizerScheduleAgent(): return (iTipProcessing.processNewRequest(itip_message, recipient, creating=True), {}) # Merge Organizer data with Attendee's own changes (VALARMs, Comment only for now). from txdav.caldav.datastore.scheduling.icaldiff import iCalDiff differ = iCalDiff(calendar, itip_message, False) rids = differ.whatIsDifferent() needs_action_rids, reschedule = differ.attendeeNeedsAction(rids) # Different behavior depending on whether a master component is present or not # Here we cache per-attendee data from the existing master that we need to use in any new # overridden components that the organizer added current_master = calendar.masterComponent() if current_master: valarms = [comp for comp in current_master.subcomponents() if comp.name() == "VALARM"] private_comments = current_master.properties("X-CALENDARSERVER-PRIVATE-COMMENT") transps = current_master.properties("TRANSP") completeds = 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 other_props = {} for pname in config.Scheduling.CalDAV.PerAttendeeProperties: props = tuple(current_master.properties(pname)) if props: other_props[pname] = props else: valarms = () private_comments = () transps = () completeds = () organizer_schedule_status = None attendee = None attendee_dtstamp = None other_props = {} if itip_message.masterComponent() is not None: # Get a new calendar object first new_calendar = iTipProcessing.processNewRequest(itip_message, recipient) # Copy over master alarms, comments etc master_component = new_calendar.masterComponent() transfer_partstat = None not in needs_action_rids and not reschedule seq_change = Component.compareComponentsForITIP(master_component, current_master, use_dtstamp=False) <= 0 iTipProcessing._transferItems(master_component, transfer_partstat and seq_change, valarms, private_comments, transps, completeds, organizer_schedule_status, attendee, attendee_dtstamp, other_props, recipient) # Now try to match recurrences in the new calendar for component in tuple(new_calendar.subcomponents()): if component.name() != "VTIMEZONE" and component.getRecurrenceIDUTC() is not None: iTipProcessing.transferItems(calendar, component, needs_action_rids, reschedule, valarms, private_comments, transps, completeds, organizer_schedule_status, attendee, attendee_dtstamp, other_props, recipient) # Now try to match recurrences from the old calendar for component in calendar.subcomponents(): if component.name() != "VTIMEZONE" and component.getRecurrenceIDUTC() is not None: rid = component.getRecurrenceIDUTC() if new_calendar.overriddenComponent(rid) is None: allowCancelled = component.propertyValue("STATUS") == "CANCELLED" hidden = component.hasProperty(Component.HIDDEN_INSTANCE_PROPERTY) new_component = new_calendar.deriveInstance(rid, allowCancelled=allowCancelled and not hidden) if new_component is not None: new_calendar.addComponent(new_component) iTipProcessing.transferItems(calendar, new_component, needs_action_rids, reschedule, valarms, private_comments, transps, completeds, organizer_schedule_status, attendee, attendee_dtstamp, other_props, recipient) if hidden: new_component.addProperty(Property(Component.HIDDEN_INSTANCE_PROPERTY, "T")) iTipProcessing.addTranspForNeedsAction(new_calendar.subcomponents(), recipient) # Replace the entire object return new_calendar, rids else: # Need existing tzids tzids = calendar.timezones() # Update existing instances for component in itip_message.subcomponents(): if component.name() == "VTIMEZONE": # May need to add a new VTIMEZONE if component.propertyValue("TZID") not in tzids: calendar.addComponent(component) else: component = component.duplicate() missingDeclined = iTipProcessing.transferItems(calendar, component, needs_action_rids, reschedule, valarms, private_comments, transps, completeds, organizer_schedule_status, attendee, attendee_dtstamp, other_props, recipient, remove_matched=True) if not missingDeclined: calendar.addComponent(component) iTipProcessing.addTranspForNeedsAction(calendar.subcomponents(), recipient) # Write back the modified object return calendar, rids
def sequenceComparison(itip, calendar): """ Check the iTIP SEQUENCE values for the incoming iTIP message against the existing calendar data to determine whether the iTIP message is old and should be ignored. @param itip: the iTIP message to process @type itip: L{Component} @param calendar: the existing calendar data to compare with @type calendar: L{Component} @return: C{True} if the itip message is new and should be processed, C{False} if no processing is needed @rtype: C{bool} """ # Master component comparison trumps all else itip_master = itip.masterComponent() cal_master = calendar.masterComponent() # If master component exists, compare all in iTIP and update if any are new if cal_master: for itip_component in itip.subcomponents(): if itip_component.name() in ignoredComponents: continue cal_component = calendar.overriddenComponent(itip_component.getRecurrenceIDUTC()) if cal_component is None: cal_component = cal_master # TODO: No DTSTAMP comparison because we do not track DTSTAMPs # Treat components the same as meaning so an update - in theory no harm in doing that if Component.compareComponentsForITIP(itip_component, cal_component, use_dtstamp=False) >= 0: return True return False elif itip_master: # Do comparison of each appropriate component if any one is new, process the itip for cal_component in calendar.subcomponents(): if cal_component.name() in ignoredComponents: continue itip_component = itip.overriddenComponent(cal_component.getRecurrenceIDUTC()) if itip_component is None: itip_component = itip_master # TODO: No DTSTAMP comparison because we do not track DTSTAMPs # Treat components the same as meaning so an update - in theory no harm in doing that if Component.compareComponentsForITIP(itip_component, cal_component, use_dtstamp=False) >= 0: return True return False else: # Do comparison of each matching component if any one is new, process the entire itip. # There is a race condition here, similar to REPLY, where we could reinstate an instance # that has been removed. Not much we can do about it without additional tracking. cal_rids = set() for cal_component in calendar.subcomponents(): if cal_component.name() in ignoredComponents: continue cal_rids.add(cal_component.getRecurrenceIDUTC()) itip_rids = set() for itip_component in itip.subcomponents(): if itip_component.name() in ignoredComponents: continue itip_rids.add(itip_component.getRecurrenceIDUTC()) # Compare ones that match for rid in cal_rids & itip_rids: cal_component = calendar.overriddenComponent(rid) itip_component = itip.overriddenComponent(rid) # TODO: No DTSTAMP comparison because we do not track DTSTAMPs # Treat components the same as meaning so an update - in theory no harm in doing that if Component.compareComponentsForITIP(itip_component, cal_component, use_dtstamp=False) >= 0: return True # If there are others in one set and not the other - always process, else no process return len(cal_rids ^ itip_rids) > 0
def transferItems(from_calendar, to_component, needs_action_rids, reschedule, valarms, private_comments, transps, completeds, organizer_schedule_status, attendee, attendee_dtstamp, other_props, recipient, remove_matched=False): """ Transfer properties from a calendar to a component by first trying to match the component in the original calendar and use the properties from that, or use the values provided as arguments (which have been derived from the original calendar's master component). @param from_calendar: the old calendar data to transfer items from @type from_calendar: L{Component} @param to_component: the new component to transfer items to @type to_component: L{Component} @param valarms: a C{list} of VALARM components from the old master to use @type valarms: C{list} @param private_comments: a C{list} of private comment properties from the old master to use @type private_comments: C{list} @param transps: a C{list} of TRANSP properties from the old master to use @type transps: C{list} @param completeds: a C{list} of COMPLETED properties from the old master to use @type completeds: C{list} @param organizer_schedule_status: a the SCHEDULE-STATUS value for the organizer from the old master to use @type organizer_schedule_status: C{str} @param attendee_dtstamp: an the ATTENDEE DTSTAMP parameter value from the old master to use @type attendee_dtstamp: C{str} @param other_props: other properties from the old master to use @type other_props: C{list} @param recipient: the calendar user address of the attendee whose data is being processed @type recipient: C{str} @param remove_matched: whether or not to remove the matching component rather than transfer items @type remove_matched: C{bool} @return: C{True} if an EXDATE match occurred requiring the incoming component to be removed. """ rid = to_component.getRecurrenceIDUTC() transfer_partstat = rid not in needs_action_rids and not reschedule # Is there a matching component matched = from_calendar.overriddenComponent(rid) if matched: valarms = [comp for comp in matched.subcomponents() if comp.name() == "VALARM"] private_comments = matched.properties("X-CALENDARSERVER-PRIVATE-COMMENT") transps = matched.properties("TRANSP") completeds = 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 other_props = {} for pname in config.Scheduling.CalDAV.PerAttendeeProperties: props = tuple(matched.properties(pname)) if props: other_props[pname] = props seq_change = Component.compareComponentsForITIP(to_component, matched, use_dtstamp=False) <= 0 iTipProcessing._transferItems(to_component, transfer_partstat and seq_change, valarms, private_comments, transps, completeds, organizer_schedule_status, attendee, attendee_dtstamp, other_props, recipient) # Check for incoming DECLINED to_attendee = to_component.getAttendeeProperty((recipient,)) if to_attendee and to_attendee.parameterValue("PARTSTAT", "NEEDS-ACTION") == "DECLINED": # If existing item has HIDDEN property copy that over if matched.hasProperty(Component.HIDDEN_INSTANCE_PROPERTY): to_component.addProperty(Property(Component.HIDDEN_INSTANCE_PROPERTY, "T")) # Remove the old one if remove_matched: from_calendar.removeComponent(matched) # Check to see if the new component is cancelled as that could mean we are copying in the wrong attendee state if to_component.propertyValue("STATUS") == "CANCELLED": if attendee and to_attendee: to_attendee.setParameter("PARTSTAT", attendee.parameterValue("PARTSTAT", "NEEDS-ACTION")) else: # Check for incoming DECLINED attendee = to_component.getAttendeeProperty((recipient,)) if attendee and attendee.parameterValue("PARTSTAT", "NEEDS-ACTION") == "DECLINED": return True master_component = from_calendar.masterComponent() seq_change = (Component.compareComponentsForITIP(to_component, master_component, use_dtstamp=False) <= 0) if master_component is not None else True iTipProcessing._transferItems(to_component, transfer_partstat and seq_change, valarms, private_comments, transps, completeds, organizer_schedule_status, attendee, attendee_dtstamp, other_props, recipient) return False