1.1 --- a/imiptools/client.py Sun Oct 01 15:08:20 2017 +0200
1.2 +++ b/imiptools/client.py Sun Oct 01 16:38:10 2017 +0200
1.3 @@ -234,7 +234,7 @@
1.4
1.5 # Common operations on calendar data.
1.6
1.7 - def update_sender(self, attr):
1.8 + def update_sender_attr(self, attr):
1.9
1.10 "Update the SENT-BY attribute of the 'attr' sender metadata."
1.11
1.12 @@ -303,7 +303,7 @@
1.13 freebusy = freebusy or self.store.get_freebusy(self.user)
1.14
1.15 user_attr = {}
1.16 - self.update_sender(user_attr)
1.17 + self.update_sender_attr(user_attr)
1.18 return self.to_part("PUBLISH", [make_freebusy(freebusy, uid, self.user, user_attr)])
1.19
1.20 return None
1.21 @@ -373,8 +373,7 @@
1.22 # Get the parent event, add SENT-BY details to the organiser.
1.23
1.24 if not attendee or self.is_participating(attendee, obj=obj):
1.25 - organiser, organiser_attr = uri_item(obj.get_item("ORGANIZER"))
1.26 - self.update_sender(organiser_attr)
1.27 + self.update_sender(obj)
1.28 responses.append(self.object_to_part(method, obj))
1.29 methods.add(method)
1.30
1.31 @@ -396,8 +395,7 @@
1.32 obj = self.get_stored_object(self.uid, recurrenceid, section)
1.33
1.34 if not attendee or self.is_participating(attendee, obj=obj):
1.35 - organiser, organiser_attr = uri_item(obj.get_item("ORGANIZER"))
1.36 - self.update_sender(organiser_attr)
1.37 + self.update_sender(obj)
1.38 responses.append(self.object_to_part(rmethod, obj))
1.39 methods.add(rmethod)
1.40
1.41 @@ -524,6 +522,17 @@
1.42
1.43 # Common operations on calendar data.
1.44
1.45 + def update_sender(self, obj=None):
1.46 +
1.47 + """
1.48 + Update sender details in 'obj', or the current object if not indicated,
1.49 + modifying the organiser attributes.
1.50 + """
1.51 +
1.52 + obj = obj or self.obj
1.53 + organiser, organiser_attr = uri_item(obj.get_item("ORGANIZER"))
1.54 + self.update_sender_attr(organiser_attr)
1.55 +
1.56 def update_senders(self, obj=None):
1.57
1.58 """
1.59 @@ -534,6 +543,7 @@
1.60
1.61 obj = obj or self.obj
1.62 calendar_uri = self.messenger and get_uri(self.messenger.sender)
1.63 +
1.64 for attendee, attendee_attr in uri_items(obj.get_items("ATTENDEE")):
1.65 if attendee != self.user:
1.66 if attendee_attr.get("SENT-BY") == calendar_uri:
1.67 @@ -556,41 +566,6 @@
1.68
1.69 return None
1.70
1.71 - def get_rescheduled_parts(self, periods, method):
1.72 -
1.73 - """
1.74 - Return message parts describing rescheduled 'periods' affected by 'method'.
1.75 - """
1.76 -
1.77 - rescheduled_parts = []
1.78 -
1.79 - if periods:
1.80 -
1.81 - # Duplicate the core of the object without any period information.
1.82 -
1.83 - obj = self.obj.copy()
1.84 - obj.remove_all(["RRULE", "RDATE", "DTSTART", "DTEND", "DURATION"])
1.85 -
1.86 - for p in periods:
1.87 - if not p.origin:
1.88 - continue
1.89 -
1.90 - # Set specific recurrence information.
1.91 -
1.92 - obj.set_datetime("DTSTART", p.get_start())
1.93 - obj.set_datetime("DTEND", p.get_end())
1.94 -
1.95 - # Acquire the original recurrence identifier associated with
1.96 - # this period. This may differ where the start of the period has
1.97 - # changed.
1.98 -
1.99 - dt, attr = p.get_recurrenceid_item()
1.100 - obj["RECURRENCE-ID"] = [(format_datetime(dt), attr)]
1.101 -
1.102 - rescheduled_parts.append(self.object_to_part(method, obj))
1.103 -
1.104 - return rescheduled_parts
1.105 -
1.106 # Object update methods.
1.107
1.108 def update_recurrenceid(self):
1.109 @@ -610,12 +585,12 @@
1.110 obj = obj or self.obj
1.111 self.dtstamp = obj.update_dtstamp()
1.112
1.113 - def update_sequence(self, increment=False, obj=None):
1.114 + def update_sequence(self, obj=None):
1.115
1.116 "Update the SEQUENCE in the current object or any given object 'obj'."
1.117
1.118 obj = obj or self.obj
1.119 - obj.update_sequence(increment)
1.120 + obj.update_sequence(self.is_organiser())
1.121
1.122 def merge_attendance(self, attendees):
1.123
1.124 @@ -758,39 +733,42 @@
1.125 attendee_attr["PARTSTAT"] = partstat
1.126 if attendee_attr.has_key("RSVP"):
1.127 del attendee_attr["RSVP"]
1.128 - self.update_sender(attendee_attr)
1.129 + self.update_sender_attr(attendee_attr)
1.130 return attendee_attr
1.131
1.132 - # Communication methods.
1.133 + # General message generation methods.
1.134
1.135 - def send_message(self, parts, sender, obj, from_organiser, bcc_sender):
1.136 + def get_recipients(self, obj=None):
1.137
1.138 """
1.139 - Send the given 'parts' to the appropriate recipients, also sending a
1.140 - copy to the 'sender'. The 'obj' together with the 'from_organiser' value
1.141 - (which indicates whether the organiser is sending this message) are used
1.142 - to determine the recipients of the message.
1.143 + Return recipients for 'obj' (or the current object) dependent on the
1.144 + current user's role.
1.145 """
1.146
1.147 + obj = obj or self.obj
1.148 +
1.149 + organiser = get_uri(obj.get_value("ORGANIZER"))
1.150 + attendees = uri_values(obj.get_values("ATTENDEE"))
1.151 +
1.152 # As organiser, send an invitation to attendees, excluding oneself if
1.153 # also attending. The updated event will be saved by the outgoing
1.154 # handler.
1.155
1.156 - organiser = get_uri(obj.get_value("ORGANIZER"))
1.157 - attendees = uri_values(obj.get_values("ATTENDEE"))
1.158 -
1.159 - if from_organiser:
1.160 - recipients = [get_address(attendee) for attendee in attendees if attendee != self.user]
1.161 + if self.is_organiser():
1.162 + return [get_address(attendee) for attendee in attendees if attendee != self.user]
1.163 else:
1.164 - recipients = [get_address(organiser)]
1.165 + return [get_address(organiser)]
1.166 +
1.167 + def attach_freebusy(self, parts):
1.168
1.169 - # Since the outgoing handler updates this user's free/busy details,
1.170 - # the stored details will probably not have the updated details at
1.171 - # this point, so we update our copy for serialisation as the bundled
1.172 - # free/busy object.
1.173 + """
1.174 + Since the outgoing handler updates this user's free/busy details, the
1.175 + stored details will probably not have the updated details straight away,
1.176 + so we update our copy for serialisation as the bundled free/busy object.
1.177 + """
1.178
1.179 freebusy = self.store.get_freebusy(self.user).copy()
1.180 - self.update_freebusy(freebusy, self.user, from_organiser)
1.181 + self.update_freebusy(freebusy, self.user, self.is_organiser())
1.182
1.183 # Bundle free/busy information if appropriate.
1.184
1.185 @@ -798,36 +776,159 @@
1.186 if part:
1.187 parts.append(part)
1.188
1.189 - if recipients or bcc_sender:
1.190 - self._send_message(sender, recipients, parts, bcc_sender)
1.191 + def make_message(self, parts, recipients, bcc_sender=False):
1.192 +
1.193 + """
1.194 + Send the given 'parts' to the appropriate 'recipients', also sending a
1.195 + copy to the sender.
1.196 + """
1.197 +
1.198 + if not self.messenger:
1.199 + return None
1.200
1.201 - def _send_message(self, sender, recipients, parts, bcc_sender):
1.202 + # Update and attach bundled free/busy details.
1.203 +
1.204 + self.attach_freebusy(parts)
1.205 +
1.206 + if not bcc_sender:
1.207 + return self.messenger.make_outgoing_message(parts, recipients)
1.208 + else:
1.209 + sender = get_address(self.user)
1.210 + return self.messenger.make_outgoing_message(parts, recipients, outgoing_bcc=sender)
1.211 +
1.212 + def send_message(self, message, recipients, bcc_sender=False):
1.213
1.214 """
1.215 - Send a message, explicitly specifying the 'sender' as an outgoing BCC
1.216 - recipient since the generic calendar user will be the actual sender.
1.217 + Send 'message' to 'recipients', explicitly specifying the sender as an
1.218 + outgoing BCC recipient if 'bcc_sender' is set, since the generic
1.219 + calendar user will be the actual sender.
1.220 """
1.221
1.222 + if not recipients and not bcc_sender or not self.messenger:
1.223 + return
1.224 +
1.225 + if not bcc_sender:
1.226 + self.messenger.sendmail(recipients, message.as_string())
1.227 + else:
1.228 + self.messenger.sendmail(recipients, message.as_string(), outgoing_bcc=sender)
1.229 +
1.230 + def make_message_for_self(self, parts):
1.231 +
1.232 + "Send 'message' to the current user."
1.233 +
1.234 + if not self.messenger:
1.235 + return None
1.236 +
1.237 + sender = get_address(self.user)
1.238 + return self.messenger.make_outgoing_message(parts, [sender])
1.239 +
1.240 + def send_message_to_self(self, message):
1.241 +
1.242 + "Send 'message' to the current user."
1.243 +
1.244 if not self.messenger:
1.245 return
1.246
1.247 - if not bcc_sender:
1.248 - message = self.messenger.make_outgoing_message(parts, recipients)
1.249 - self.messenger.sendmail(recipients, message.as_string())
1.250 - else:
1.251 - message = self.messenger.make_outgoing_message(parts, recipients, outgoing_bcc=sender)
1.252 - self.messenger.sendmail(recipients, message.as_string(), outgoing_bcc=sender)
1.253 + self.messenger.sendmail([sender], message.as_string())
1.254 +
1.255 + # Specific message generation methods.
1.256 +
1.257 + def get_rescheduled_parts(self, periods, method):
1.258 +
1.259 + """
1.260 + Return message parts describing rescheduled 'periods' affected by 'method'.
1.261 + """
1.262 +
1.263 + rescheduled_parts = []
1.264 +
1.265 + if periods:
1.266 +
1.267 + # Duplicate the core of the object without any period information.
1.268 +
1.269 + obj = self.obj.copy()
1.270 + obj.remove_all(["RRULE", "RDATE", "DTSTART", "DTEND", "DURATION"])
1.271 +
1.272 + for p in periods:
1.273 + if not p.origin:
1.274 + continue
1.275 +
1.276 + # Set specific recurrence information.
1.277
1.278 - def send_message_to_self(self, parts):
1.279 + obj.set_datetime("DTSTART", p.get_start())
1.280 + obj.set_datetime("DTEND", p.get_end())
1.281 +
1.282 + # Acquire the original recurrence identifier associated with
1.283 + # this period. This may differ where the start of the period has
1.284 + # changed.
1.285 +
1.286 + dt, attr = p.get_recurrenceid_item()
1.287 + obj["RECURRENCE-ID"] = [(format_datetime(dt), attr)]
1.288 +
1.289 + rescheduled_parts.append(self.object_to_part(method, obj))
1.290 +
1.291 + return rescheduled_parts
1.292 +
1.293 + def make_update_message(self, recipients, to_unschedule=None, to_reschedule=None):
1.294 +
1.295 + """
1.296 + Prepare event updates from the organiser of an event for the given
1.297 + 'recipients', using the period collections 'to_unschedule' and
1.298 + 'to_reschedule'.
1.299 + """
1.300 +
1.301 + # Start with the parent object and augment it with the given
1.302 + # amendments providing cancelled and modified occurrence information.
1.303
1.304 - "Send a message composed of the given 'parts' to the given user."
1.305 + parts = [self.object_to_part("REQUEST", self.obj)]
1.306 + unscheduled_parts = self.get_rescheduled_parts(to_unschedule, "CANCEL")
1.307 + rescheduled_parts = self.get_rescheduled_parts(to_reschedule, "REQUEST")
1.308 +
1.309 + return self.make_message(parts + unscheduled_parts + rescheduled_parts,
1.310 + recipients)
1.311 +
1.312 + def make_self_update_message(self, to_unschedule=None, to_reschedule=None):
1.313 +
1.314 + """
1.315 + Prepare event updates to be sent from the organiser of an event to
1.316 + themself.
1.317 + """
1.318 +
1.319 + parts = [self.object_to_part("PUBLISH", self.obj)]
1.320 + unscheduled_parts = self.get_rescheduled_parts(to_unschedule, "CANCEL")
1.321 + rescheduled_parts = self.get_rescheduled_parts(to_reschedule, "PUBLISH")
1.322 + return self.make_message_for_self(parts + unscheduled_parts + rescheduled_parts)
1.323 +
1.324 + def make_cancel_object(self, to_cancel=None):
1.325
1.326 - if not self.messenger:
1.327 - return
1.328 + """
1.329 + Prepare an event cancellation object involving the participants in the
1.330 + 'to_cancel' list.
1.331 + """
1.332 +
1.333 + if to_cancel:
1.334 + obj = self.obj.copy()
1.335 + obj["ATTENDEE"] = to_cancel
1.336 + else:
1.337 + obj = self.obj
1.338 +
1.339 + return obj
1.340 +
1.341 + def make_cancel_message(self, recipients, obj):
1.342
1.343 - sender = get_address(self.user)
1.344 - message = self.messenger.make_outgoing_message(parts, [sender])
1.345 - self.messenger.sendmail([sender], message.as_string())
1.346 + """
1.347 + Prepare an event cancellation message to 'recipients' using the details
1.348 + in 'obj'.
1.349 + """
1.350 +
1.351 + parts = [self.object_to_part("CANCEL", obj)]
1.352 + return self.make_message(parts, recipients)
1.353 +
1.354 + def make_cancel_message_for_self(self, obj):
1.355 +
1.356 + "Prepare an event cancellation for the current user."
1.357 +
1.358 + parts = [self.object_to_part("CANCEL", obj)]
1.359 + return self.make_message_for_self(parts)
1.360
1.361 # Action methods.
1.362
1.363 @@ -842,9 +943,9 @@
1.364 return False
1.365
1.366 method = "DECLINECOUNTER"
1.367 - self.update_senders(obj=obj)
1.368 + self.update_senders(obj)
1.369 obj.update_dtstamp()
1.370 - obj.update_sequence(False)
1.371 + obj.update_sequence()
1.372 self._send_message(get_address(self.user), [get_address(attendee)], [self.object_to_part(method, obj)], True)
1.373 return True
1.374
1.375 @@ -870,9 +971,16 @@
1.376 self.update_senders()
1.377
1.378 self.update_dtstamp()
1.379 - self.update_sequence(False)
1.380 - self.send_message([self.object_to_part(changed and "COUNTER" or "REPLY", self.obj)],
1.381 - get_address(self.user), self.obj, False, True)
1.382 + self.update_sequence()
1.383 +
1.384 + parts = [self.object_to_part(changed and "COUNTER" or "REPLY", self.obj)]
1.385 +
1.386 + # Create and send the response.
1.387 +
1.388 + recipients = self.get_recipients()
1.389 + message = self.make_message(parts, recipients, bcc_sender=True)
1.390 + self.send_message(message, recipients, bcc_sender=True)
1.391 +
1.392 return True
1.393
1.394 def process_created_request(self, method, to_cancel=None,
1.395 @@ -896,62 +1004,44 @@
1.396 recurrence instances that are already stored.
1.397 """
1.398
1.399 - # Here, the organiser should be the current user.
1.400 -
1.401 - organiser, organiser_attr = uri_item(self.obj.get_item("ORGANIZER"))
1.402 -
1.403 - self.update_sender(organiser_attr)
1.404 + self.update_sender()
1.405 self.update_senders()
1.406 self.update_dtstamp()
1.407 - self.update_sequence(True)
1.408 + self.update_sequence()
1.409
1.410 if method == "REQUEST":
1.411
1.412 - # Start with the parent object and augment it with the given
1.413 - # amendments.
1.414 -
1.415 - parts = [self.object_to_part(method, self.obj)]
1.416 -
1.417 - # Add message parts with cancelled and modified occurrence
1.418 - # information.
1.419 -
1.420 - unscheduled_parts = self.get_rescheduled_parts(to_unschedule, "CANCEL")
1.421 - rescheduled_parts = self.get_rescheduled_parts(to_reschedule, "REQUEST")
1.422 -
1.423 # Send the updated event, along with a cancellation for each of the
1.424 # unscheduled occurrences.
1.425
1.426 - self.send_message(parts + unscheduled_parts + rescheduled_parts,
1.427 - get_address(organiser), self.obj, True, False)
1.428 + recipients = self.get_recipients()
1.429 + message = self.make_update_message(recipients, to_unschedule, to_reschedule)
1.430 + self.send_message(message, recipients)
1.431
1.432 # Since the organiser can update the SEQUENCE but this can leave any
1.433 # mail/calendar client lagging, issue a PUBLISH message to the
1.434 # user's address.
1.435
1.436 - parts = [self.object_to_part("PUBLISH", self.obj)]
1.437 - rescheduled_parts = self.get_rescheduled_parts(to_reschedule, "PUBLISH")
1.438 -
1.439 - self.send_message_to_self(parts + unscheduled_parts + rescheduled_parts)
1.440 + message = self.make_self_update_message(to_unschedule, to_reschedule)
1.441 + self.send_message_to_self(message)
1.442
1.443 # When cancelling, replace the attendees with those for whom the event
1.444 # is now cancelled.
1.445
1.446 if method == "CANCEL" or to_cancel:
1.447 - if to_cancel:
1.448 - obj = self.obj.copy()
1.449 - obj["ATTENDEE"] = to_cancel
1.450 - else:
1.451 - obj = self.obj
1.452
1.453 # Send a cancellation to all uninvited attendees.
1.454
1.455 - parts = [self.object_to_part("CANCEL", obj)]
1.456 - self.send_message(parts, get_address(organiser), obj, True, False)
1.457 + recipients = self.get_recipients(obj)
1.458 + obj = self.make_cancel_object(to_cancel)
1.459 + message = self.make_cancel_message(recipients, obj)
1.460 + self.send_message(message, recipients)
1.461
1.462 # Issue a CANCEL message to the user's address.
1.463
1.464 if method == "CANCEL":
1.465 - self.send_message_to_self(parts)
1.466 + message = self.make_cancel_message_for_self(obj)
1.467 + self.send_message_to_self(message)
1.468
1.469 return True
1.470