# HG changeset patch # User Paul Boddie # Date 1506868690 -7200 # Node ID 2bd0846a380dc2c973f386b6280b38cc5fc5551c # Parent eeebb61f4473367ba03e1114dedf113041e8d341 Exposed distinct operations in the client functionality, also changing the behaviour of certain object data update methods. diff -r eeebb61f4473 -r 2bd0846a380d imiptools/client.py --- a/imiptools/client.py Sun Oct 01 15:08:20 2017 +0200 +++ b/imiptools/client.py Sun Oct 01 16:38:10 2017 +0200 @@ -234,7 +234,7 @@ # Common operations on calendar data. - def update_sender(self, attr): + def update_sender_attr(self, attr): "Update the SENT-BY attribute of the 'attr' sender metadata." @@ -303,7 +303,7 @@ freebusy = freebusy or self.store.get_freebusy(self.user) user_attr = {} - self.update_sender(user_attr) + self.update_sender_attr(user_attr) return self.to_part("PUBLISH", [make_freebusy(freebusy, uid, self.user, user_attr)]) return None @@ -373,8 +373,7 @@ # Get the parent event, add SENT-BY details to the organiser. if not attendee or self.is_participating(attendee, obj=obj): - organiser, organiser_attr = uri_item(obj.get_item("ORGANIZER")) - self.update_sender(organiser_attr) + self.update_sender(obj) responses.append(self.object_to_part(method, obj)) methods.add(method) @@ -396,8 +395,7 @@ obj = self.get_stored_object(self.uid, recurrenceid, section) if not attendee or self.is_participating(attendee, obj=obj): - organiser, organiser_attr = uri_item(obj.get_item("ORGANIZER")) - self.update_sender(organiser_attr) + self.update_sender(obj) responses.append(self.object_to_part(rmethod, obj)) methods.add(rmethod) @@ -524,6 +522,17 @@ # Common operations on calendar data. + def update_sender(self, obj=None): + + """ + Update sender details in 'obj', or the current object if not indicated, + modifying the organiser attributes. + """ + + obj = obj or self.obj + organiser, organiser_attr = uri_item(obj.get_item("ORGANIZER")) + self.update_sender_attr(organiser_attr) + def update_senders(self, obj=None): """ @@ -534,6 +543,7 @@ obj = obj or self.obj calendar_uri = self.messenger and get_uri(self.messenger.sender) + for attendee, attendee_attr in uri_items(obj.get_items("ATTENDEE")): if attendee != self.user: if attendee_attr.get("SENT-BY") == calendar_uri: @@ -556,41 +566,6 @@ return None - def get_rescheduled_parts(self, periods, method): - - """ - Return message parts describing rescheduled 'periods' affected by 'method'. - """ - - rescheduled_parts = [] - - if periods: - - # Duplicate the core of the object without any period information. - - obj = self.obj.copy() - obj.remove_all(["RRULE", "RDATE", "DTSTART", "DTEND", "DURATION"]) - - for p in periods: - if not p.origin: - continue - - # Set specific recurrence information. - - obj.set_datetime("DTSTART", p.get_start()) - obj.set_datetime("DTEND", p.get_end()) - - # Acquire the original recurrence identifier associated with - # this period. This may differ where the start of the period has - # changed. - - dt, attr = p.get_recurrenceid_item() - obj["RECURRENCE-ID"] = [(format_datetime(dt), attr)] - - rescheduled_parts.append(self.object_to_part(method, obj)) - - return rescheduled_parts - # Object update methods. def update_recurrenceid(self): @@ -610,12 +585,12 @@ obj = obj or self.obj self.dtstamp = obj.update_dtstamp() - def update_sequence(self, increment=False, obj=None): + def update_sequence(self, obj=None): "Update the SEQUENCE in the current object or any given object 'obj'." obj = obj or self.obj - obj.update_sequence(increment) + obj.update_sequence(self.is_organiser()) def merge_attendance(self, attendees): @@ -758,39 +733,42 @@ attendee_attr["PARTSTAT"] = partstat if attendee_attr.has_key("RSVP"): del attendee_attr["RSVP"] - self.update_sender(attendee_attr) + self.update_sender_attr(attendee_attr) return attendee_attr - # Communication methods. + # General message generation methods. - def send_message(self, parts, sender, obj, from_organiser, bcc_sender): + def get_recipients(self, obj=None): """ - Send the given 'parts' to the appropriate recipients, also sending a - copy to the 'sender'. The 'obj' together with the 'from_organiser' value - (which indicates whether the organiser is sending this message) are used - to determine the recipients of the message. + Return recipients for 'obj' (or the current object) dependent on the + current user's role. """ + obj = obj or self.obj + + organiser = get_uri(obj.get_value("ORGANIZER")) + attendees = uri_values(obj.get_values("ATTENDEE")) + # As organiser, send an invitation to attendees, excluding oneself if # also attending. The updated event will be saved by the outgoing # handler. - organiser = get_uri(obj.get_value("ORGANIZER")) - attendees = uri_values(obj.get_values("ATTENDEE")) - - if from_organiser: - recipients = [get_address(attendee) for attendee in attendees if attendee != self.user] + if self.is_organiser(): + return [get_address(attendee) for attendee in attendees if attendee != self.user] else: - recipients = [get_address(organiser)] + return [get_address(organiser)] + + def attach_freebusy(self, parts): - # Since the outgoing handler updates this user's free/busy details, - # the stored details will probably not have the updated details at - # this point, so we update our copy for serialisation as the bundled - # free/busy object. + """ + Since the outgoing handler updates this user's free/busy details, the + stored details will probably not have the updated details straight away, + so we update our copy for serialisation as the bundled free/busy object. + """ freebusy = self.store.get_freebusy(self.user).copy() - self.update_freebusy(freebusy, self.user, from_organiser) + self.update_freebusy(freebusy, self.user, self.is_organiser()) # Bundle free/busy information if appropriate. @@ -798,36 +776,159 @@ if part: parts.append(part) - if recipients or bcc_sender: - self._send_message(sender, recipients, parts, bcc_sender) + def make_message(self, parts, recipients, bcc_sender=False): + + """ + Send the given 'parts' to the appropriate 'recipients', also sending a + copy to the sender. + """ + + if not self.messenger: + return None - def _send_message(self, sender, recipients, parts, bcc_sender): + # Update and attach bundled free/busy details. + + self.attach_freebusy(parts) + + if not bcc_sender: + return self.messenger.make_outgoing_message(parts, recipients) + else: + sender = get_address(self.user) + return self.messenger.make_outgoing_message(parts, recipients, outgoing_bcc=sender) + + def send_message(self, message, recipients, bcc_sender=False): """ - Send a message, explicitly specifying the 'sender' as an outgoing BCC - recipient since the generic calendar user will be the actual sender. + Send 'message' to 'recipients', explicitly specifying the sender as an + outgoing BCC recipient if 'bcc_sender' is set, since the generic + calendar user will be the actual sender. """ + if not recipients and not bcc_sender or not self.messenger: + return + + if not bcc_sender: + self.messenger.sendmail(recipients, message.as_string()) + else: + self.messenger.sendmail(recipients, message.as_string(), outgoing_bcc=sender) + + def make_message_for_self(self, parts): + + "Send 'message' to the current user." + + if not self.messenger: + return None + + sender = get_address(self.user) + return self.messenger.make_outgoing_message(parts, [sender]) + + def send_message_to_self(self, message): + + "Send 'message' to the current user." + if not self.messenger: return - if not bcc_sender: - message = self.messenger.make_outgoing_message(parts, recipients) - self.messenger.sendmail(recipients, message.as_string()) - else: - message = self.messenger.make_outgoing_message(parts, recipients, outgoing_bcc=sender) - self.messenger.sendmail(recipients, message.as_string(), outgoing_bcc=sender) + self.messenger.sendmail([sender], message.as_string()) + + # Specific message generation methods. + + def get_rescheduled_parts(self, periods, method): + + """ + Return message parts describing rescheduled 'periods' affected by 'method'. + """ + + rescheduled_parts = [] + + if periods: + + # Duplicate the core of the object without any period information. + + obj = self.obj.copy() + obj.remove_all(["RRULE", "RDATE", "DTSTART", "DTEND", "DURATION"]) + + for p in periods: + if not p.origin: + continue + + # Set specific recurrence information. - def send_message_to_self(self, parts): + obj.set_datetime("DTSTART", p.get_start()) + obj.set_datetime("DTEND", p.get_end()) + + # Acquire the original recurrence identifier associated with + # this period. This may differ where the start of the period has + # changed. + + dt, attr = p.get_recurrenceid_item() + obj["RECURRENCE-ID"] = [(format_datetime(dt), attr)] + + rescheduled_parts.append(self.object_to_part(method, obj)) + + return rescheduled_parts + + def make_update_message(self, recipients, to_unschedule=None, to_reschedule=None): + + """ + Prepare event updates from the organiser of an event for the given + 'recipients', using the period collections 'to_unschedule' and + 'to_reschedule'. + """ + + # Start with the parent object and augment it with the given + # amendments providing cancelled and modified occurrence information. - "Send a message composed of the given 'parts' to the given user." + parts = [self.object_to_part("REQUEST", self.obj)] + unscheduled_parts = self.get_rescheduled_parts(to_unschedule, "CANCEL") + rescheduled_parts = self.get_rescheduled_parts(to_reschedule, "REQUEST") + + return self.make_message(parts + unscheduled_parts + rescheduled_parts, + recipients) + + def make_self_update_message(self, to_unschedule=None, to_reschedule=None): + + """ + Prepare event updates to be sent from the organiser of an event to + themself. + """ + + parts = [self.object_to_part("PUBLISH", self.obj)] + unscheduled_parts = self.get_rescheduled_parts(to_unschedule, "CANCEL") + rescheduled_parts = self.get_rescheduled_parts(to_reschedule, "PUBLISH") + return self.make_message_for_self(parts + unscheduled_parts + rescheduled_parts) + + def make_cancel_object(self, to_cancel=None): - if not self.messenger: - return + """ + Prepare an event cancellation object involving the participants in the + 'to_cancel' list. + """ + + if to_cancel: + obj = self.obj.copy() + obj["ATTENDEE"] = to_cancel + else: + obj = self.obj + + return obj + + def make_cancel_message(self, recipients, obj): - sender = get_address(self.user) - message = self.messenger.make_outgoing_message(parts, [sender]) - self.messenger.sendmail([sender], message.as_string()) + """ + Prepare an event cancellation message to 'recipients' using the details + in 'obj'. + """ + + parts = [self.object_to_part("CANCEL", obj)] + return self.make_message(parts, recipients) + + def make_cancel_message_for_self(self, obj): + + "Prepare an event cancellation for the current user." + + parts = [self.object_to_part("CANCEL", obj)] + return self.make_message_for_self(parts) # Action methods. @@ -842,9 +943,9 @@ return False method = "DECLINECOUNTER" - self.update_senders(obj=obj) + self.update_senders(obj) obj.update_dtstamp() - obj.update_sequence(False) + obj.update_sequence() self._send_message(get_address(self.user), [get_address(attendee)], [self.object_to_part(method, obj)], True) return True @@ -870,9 +971,16 @@ self.update_senders() self.update_dtstamp() - self.update_sequence(False) - self.send_message([self.object_to_part(changed and "COUNTER" or "REPLY", self.obj)], - get_address(self.user), self.obj, False, True) + self.update_sequence() + + parts = [self.object_to_part(changed and "COUNTER" or "REPLY", self.obj)] + + # Create and send the response. + + recipients = self.get_recipients() + message = self.make_message(parts, recipients, bcc_sender=True) + self.send_message(message, recipients, bcc_sender=True) + return True def process_created_request(self, method, to_cancel=None, @@ -896,62 +1004,44 @@ recurrence instances that are already stored. """ - # Here, the organiser should be the current user. - - organiser, organiser_attr = uri_item(self.obj.get_item("ORGANIZER")) - - self.update_sender(organiser_attr) + self.update_sender() self.update_senders() self.update_dtstamp() - self.update_sequence(True) + self.update_sequence() if method == "REQUEST": - # Start with the parent object and augment it with the given - # amendments. - - parts = [self.object_to_part(method, self.obj)] - - # Add message parts with cancelled and modified occurrence - # information. - - unscheduled_parts = self.get_rescheduled_parts(to_unschedule, "CANCEL") - rescheduled_parts = self.get_rescheduled_parts(to_reschedule, "REQUEST") - # Send the updated event, along with a cancellation for each of the # unscheduled occurrences. - self.send_message(parts + unscheduled_parts + rescheduled_parts, - get_address(organiser), self.obj, True, False) + recipients = self.get_recipients() + message = self.make_update_message(recipients, to_unschedule, to_reschedule) + self.send_message(message, recipients) # Since the organiser can update the SEQUENCE but this can leave any # mail/calendar client lagging, issue a PUBLISH message to the # user's address. - parts = [self.object_to_part("PUBLISH", self.obj)] - rescheduled_parts = self.get_rescheduled_parts(to_reschedule, "PUBLISH") - - self.send_message_to_self(parts + unscheduled_parts + rescheduled_parts) + message = self.make_self_update_message(to_unschedule, to_reschedule) + self.send_message_to_self(message) # When cancelling, replace the attendees with those for whom the event # is now cancelled. if method == "CANCEL" or to_cancel: - if to_cancel: - obj = self.obj.copy() - obj["ATTENDEE"] = to_cancel - else: - obj = self.obj # Send a cancellation to all uninvited attendees. - parts = [self.object_to_part("CANCEL", obj)] - self.send_message(parts, get_address(organiser), obj, True, False) + recipients = self.get_recipients(obj) + obj = self.make_cancel_object(to_cancel) + message = self.make_cancel_message(recipients, obj) + self.send_message(message, recipients) # Issue a CANCEL message to the user's address. if method == "CANCEL": - self.send_message_to_self(parts) + message = self.make_cancel_message_for_self(obj) + self.send_message_to_self(message) return True diff -r eeebb61f4473 -r 2bd0846a380d imiptools/handlers/common.py --- a/imiptools/handlers/common.py Sun Oct 01 15:08:20 2017 +0200 +++ b/imiptools/handlers/common.py Sun Oct 01 16:38:10 2017 +0200 @@ -88,7 +88,7 @@ # Indicate the actual sender of the reply. - self.update_sender(attendee_attr) + self.update_sender_attr(attendee_attr) dtstart = self.obj.get_datetime("DTSTART") dtend = self.obj.get_datetime("DTEND") @@ -132,7 +132,7 @@ # Indicate the actual sender of the message. attendee_attr = attendees[self.user] - self.update_sender(attendee_attr) + self.update_sender_attr(attendee_attr) # Make a new object with a minimal property selection. diff -r eeebb61f4473 -r 2bd0846a380d imiptools/handlers/resource.py --- a/imiptools/handlers/resource.py Sun Oct 01 15:08:20 2017 +0200 +++ b/imiptools/handlers/resource.py Sun Oct 01 16:38:10 2017 +0200 @@ -168,7 +168,7 @@ # Make a version of the object with just this attendee, update the # DTSTAMP in the response, and return the object for sending. - self.update_sender(attendee_attr) + self.update_sender_attr(attendee_attr) attendees = [(self.user, attendee_attr)] # Add delegates if delegating (RFC 5546 being inconsistent here since diff -r eeebb61f4473 -r 2bd0846a380d tests/test_handle.py --- a/tests/test_handle.py Sun Oct 01 15:08:20 2017 +0200 +++ b/tests/test_handle.py Sun Oct 01 16:38:10 2017 +0200 @@ -92,19 +92,15 @@ # NOTE: This is a simpler form of the code in imipweb.client. - organiser = get_address(self.obj.get_value("ORGANIZER")) - - self.update_sender(attendee_attr) + self.update_sender_attr(attendee_attr) self.obj["ATTENDEE"] = [(self.user, attendee_attr)] self.update_dtstamp() - self.update_sequence(False) + self.update_sequence() - message = self.messenger.make_outgoing_message( - [self.obj.to_part(method)], - [organiser], - outgoing_bcc=get_address(self.user) - ) + parts = [self.obj.to_part(method)] + recipients = self.get_recipients() + message = self.make_message(parts, recipients, bcc_sender=True) return message.as_string() # A simple main program that attempts to handle a stored request, writing the @@ -139,7 +135,8 @@ Alternatively, omit the UID and RECURRENCE-ID and provide event-only details on standard input to force the script to handle an event not already present in the -store. +store. The event details must be a fragment of a calendar file, not an entire +calendar file, nor a mail message containing calendar data. If 'counter' has been indicated, alternative start and end datetimes are also required. If a specific recurrence is being separated from an event, such