# HG changeset patch # User Paul Boddie # Date 1437868774 -7200 # Node ID ef9992ae1c6e8c78996f62df31639e43e8692a15 # Parent 1c4f5c708f20bca567eb831103fea451d50a5ed0 Added store and publisher details to the common client initialisation, fixing the manager client after its conversion from handler to client. Consolidated free/busy message part generation. Introduced a recipient abstraction to handle the processing for each recipient of a message. diff -r 1c4f5c708f20 -r ef9992ae1c6e imiptools/__init__.py --- a/imiptools/__init__.py Sun Jul 26 01:48:20 2015 +0200 +++ b/imiptools/__init__.py Sun Jul 26 01:59:34 2015 +0200 @@ -20,11 +20,10 @@ """ from email import message_from_file +from imiptools.client import Client from imiptools.content import handle_itip_part -from imiptools.data import get_addresses, get_uri, make_freebusy, to_part -from imiptools.dates import get_timestamp +from imiptools.data import get_address, get_addresses, get_uri from imiptools.mail import Messenger -from imiptools.profile import Preferences import imip_store import sys @@ -75,6 +74,10 @@ msg = message_from_file(f) senders = get_addresses(msg.get_all("Reply-To") or msg.get_all("From") or []) + messenger = self.messenger + store = self.get_store() + publisher = self.get_publisher() + # Handle messages with iTIP parts. # Typically, the details of recipients are of interest in handling # messages. @@ -82,161 +85,13 @@ if not outgoing_only: original_recipients = original_recipients or get_addresses(get_all_values(msg, "To") or []) for recipient in original_recipients: - self.process_for_recipient(msg, recipient, senders, outgoing_only) + Recipient(get_uri(recipient), messenger, store, publisher, self).process(msg, senders, outgoing_only) # However, outgoing messages do not usually presume anything about the # eventual recipients. else: - self.process_for_recipient(msg, None, senders, outgoing_only) - - def process_for_recipient(self, msg, recipient, senders, outgoing_only): - - """ - Process the given 'msg' for a single 'recipient', having the given - 'senders', and with the given 'outgoing_only' status. - - Processing individually means that contributions to resulting messages - may be constructed according to individual preferences. - """ - - store = self.get_store() - publisher = self.get_publisher() - - handlers = dict([(name, cls(senders, recipient, self.messenger, store, publisher)) - for name, cls in self.handlers]) - handled = False - - for part in msg.walk(): - if part.get_content_type() in itip_content_types and \ - part.get_param("method"): - - handle_itip_part(part, handlers) - handled = True - - # When processing outgoing messages, no replies or deliveries are - # performed. - - if outgoing_only: - return - - # Get responses from the handlers. - - all_responses = [] - for handler in handlers.values(): - all_responses += handler.get_results() - - # Pack any returned parts into messages. - - if all_responses: - outgoing_parts = {} - forwarded_parts = [] - - for outgoing_recipients, part in all_responses: - if outgoing_recipients: - for outgoing_recipient in outgoing_recipients: - if not outgoing_parts.has_key(outgoing_recipient): - outgoing_parts[outgoing_recipient] = [] - outgoing_parts[outgoing_recipient].append(part) - else: - forwarded_parts.append(part) - - # Reply using any outgoing parts in a new message. - - if outgoing_parts: - - # Obtain free/busy details, if configured to do so. - - fb = self.can_provide_freebusy(handlers) and self.get_freebusy_for_recipient(recipient) - - for outgoing_recipient, parts in outgoing_parts.items(): - - # Bundle free/busy messages, if configured to do so. - - if fb: parts.append(fb) - message = self.messenger.make_outgoing_message(parts, [outgoing_recipient]) - - if self.debug: - print >>sys.stderr, "Outgoing parts for %s..." % outgoing_recipient - print message - else: - self.messenger.sendmail([outgoing_recipient], message.as_string()) - - # Forward messages to their recipients either wrapping the existing - # message, accompanying it or replacing it. - - if forwarded_parts: - - # Determine whether to wrap, accompany or replace the message. - - preferences = Preferences(get_uri(recipient)) - - incoming = preferences.get("incoming") - - if incoming == "message-only": - messages = [msg] - else: - summary = self.messenger.make_summary_message(msg, forwarded_parts) - if incoming == "summary-then-message": - messages = [summary, msg] - elif incoming == "message-then-summary": - messages = [msg, summary] - elif incoming == "summary-only": - messages = [summary] - else: # incoming == "summary-wraps-message": - messages = [self.messenger.wrap_message(msg, forwarded_parts)] - - for message in messages: - if self.debug: - print >>sys.stderr, "Forwarded parts..." - print message - elif self.lmtp_socket: - self.messenger.sendmail(recipient, message.as_string(), lmtp_socket=self.lmtp_socket) - - # Unhandled messages are delivered as they are. - - if not handled: - if self.debug: - print >>sys.stderr, "Unhandled parts..." - print msg - elif self.lmtp_socket: - self.messenger.sendmail(recipient, msg.as_string(), lmtp_socket=self.lmtp_socket) - - def can_provide_freebusy(self, handlers): - - "Test for any free/busy information produced by 'handlers'." - - fbhandler = handlers.get("VFREEBUSY") - if fbhandler: - fbmethods = fbhandler.get_outgoing_methods() - return not "REPLY" in fbmethods and not "PUBLISH" in fbmethods - else: - return False - - def get_freebusy_for_recipient(self, recipient): - - """ - Return a list of responses containing free/busy information for the - given 'recipient'. - """ - - organiser = get_uri(recipient) - preferences = Preferences(organiser) - - organiser_attr = self.messenger and {"SENT-BY" : get_uri(self.messenger.sender)} or {} - - if preferences.get("freebusy_sharing") == "share" and \ - preferences.get("freebusy_bundling") == "always": - - # Invent a unique identifier. - - utcnow = get_timestamp() - uid = "imip-agent-%s-%s" % (utcnow, recipient) - - freebusy = self.get_store().get_freebusy(organiser) - return to_part("PUBLISH", [make_freebusy(freebusy, uid, organiser, organiser_attr)]) - - return None + Recipient(None, messenger, store, publisher, self).process(msg, senders, outgoing_only) def process_args(self, args, stream): @@ -333,4 +188,139 @@ sys.exit(EX_TEMPFAIL) sys.exit(0) +class Recipient(Client): + + "A processor acting as a client on behalf of a recipient." + + def __init__(self, user, messenger, store, publisher, processor): + + """ + Initialise the recipient with the given 'user' identity, 'messenger', + 'store', 'publisher' and 'processor'. + """ + + Client.__init__(self, user, messenger, store, publisher) + self.processor = processor + + def process(self, msg, senders, outgoing_only): + + """ + Process the given 'msg' for a single recipient, having the given + 'senders', and with the given 'outgoing_only' status. + + Processing individually means that contributions to resulting messages + may be constructed according to individual preferences. + """ + + handlers = dict([(name, cls(senders, self.user and get_address(self.user), + self.messenger, self.store, self.publisher)) + for name, cls in self.processor.handlers]) + handled = False + + for part in msg.walk(): + if part.get_content_type() in itip_content_types and \ + part.get_param("method"): + + handle_itip_part(part, handlers) + handled = True + + # When processing outgoing messages, no replies or deliveries are + # performed. + + if outgoing_only: + return + + # Get responses from the handlers. + + all_responses = [] + for handler in handlers.values(): + all_responses += handler.get_results() + + # Pack any returned parts into messages. + + if all_responses: + outgoing_parts = {} + forwarded_parts = [] + + for outgoing_recipients, part in all_responses: + if outgoing_recipients: + for outgoing_recipient in outgoing_recipients: + if not outgoing_parts.has_key(outgoing_recipient): + outgoing_parts[outgoing_recipient] = [] + outgoing_parts[outgoing_recipient].append(part) + else: + forwarded_parts.append(part) + + # Reply using any outgoing parts in a new message. + + if outgoing_parts: + + # Obtain free/busy details, if configured to do so. + + fb = self.can_provide_freebusy(handlers) and self.get_freebusy_part() + + for outgoing_recipient, parts in outgoing_parts.items(): + + # Bundle free/busy messages, if configured to do so. + + if fb: parts.append(fb) + message = self.messenger.make_outgoing_message(parts, [outgoing_recipient]) + + if self.processor.debug: + print >>sys.stderr, "Outgoing parts for %s..." % outgoing_recipient + print message + else: + self.messenger.sendmail([outgoing_recipient], message.as_string()) + + # Forward messages to their recipients either wrapping the existing + # message, accompanying it or replacing it. + + if forwarded_parts: + + # Determine whether to wrap, accompany or replace the message. + + prefs = self.get_preferences() + + incoming = prefs.get("incoming") + + if incoming == "message-only": + messages = [msg] + else: + summary = self.messenger.make_summary_message(msg, forwarded_parts) + if incoming == "summary-then-message": + messages = [summary, msg] + elif incoming == "message-then-summary": + messages = [msg, summary] + elif incoming == "summary-only": + messages = [summary] + else: # incoming == "summary-wraps-message": + messages = [self.messenger.wrap_message(msg, forwarded_parts)] + + for message in messages: + if self.processor.debug: + print >>sys.stderr, "Forwarded parts..." + print message + elif self.processor.lmtp_socket: + self.messenger.sendmail(get_address(self.user), message.as_string(), lmtp_socket=self.processor.lmtp_socket) + + # Unhandled messages are delivered as they are. + + if not handled: + if self.processor.debug: + print >>sys.stderr, "Unhandled parts..." + print msg + elif self.processor.lmtp_socket: + self.messenger.sendmail(get_address(self.user), msg.as_string(), lmtp_socket=self.processor.lmtp_socket) + + def can_provide_freebusy(self, handlers): + + "Test for any free/busy information produced by 'handlers'." + + fbhandler = handlers.get("VFREEBUSY") + if fbhandler: + fbmethods = fbhandler.get_outgoing_methods() + return not "REPLY" in fbmethods and not "PUBLISH" in fbmethods + else: + return False + # vim: tabstop=4 expandtab shiftwidth=4 diff -r 1c4f5c708f20 -r ef9992ae1c6e imiptools/client.py --- a/imiptools/client.py Sun Jul 26 01:48:20 2015 +0200 +++ b/imiptools/client.py Sun Jul 26 01:59:34 2015 +0200 @@ -20,11 +20,14 @@ """ from datetime import datetime -from imiptools.data import get_address, get_uri, get_window_end, uri_dict, uri_items, uri_values +from imiptools.data import get_address, get_uri, get_window_end, \ + make_freebusy, to_part, \ + uri_dict, uri_items, uri_values +from imiptools.dates import format_datetime, get_default_timezone, \ + get_timestamp, to_timezone from imiptools.period import update_freebusy from imiptools.profile import Preferences -from imiptools.dates import format_datetime, get_default_timezone, \ - to_timezone +import imip_store def update_attendees(obj, attendees, removed): @@ -75,9 +78,16 @@ default_window_size = 100 - def __init__(self, user, messenger=None): + def __init__(self, user, messenger=None, store=None, publisher=None): self.user = user self.messenger = messenger + self.store = store or imip_store.FileStore() + + try: + self.publisher = publisher or imip_store.FilePublisher() + except OSError: + self.publisher = None + self.preferences = None def get_preferences(self): @@ -113,6 +123,12 @@ # Common operations on calendar data. + def is_participating(self, attr, as_organiser=False): + return as_organiser or not attr or attr.get("PARTSTAT") != "DECLINED" + + def get_overriding_transparency(self, attr, as_organiser=False): + return as_organiser and not (attr and attr.get("PARTSTAT")) and "ORG" or None + def update_participation(self, obj, partstat=None): """ @@ -136,12 +152,35 @@ if self.messenger and self.messenger.sender != get_address(self.user): attr["SENT-BY"] = get_uri(self.messenger.sender) + # Free/busy operations. + + def get_freebusy_part(self): + + """ + Return a message part containing free/busy information for the user. + """ + + if self.is_sharing() and self.is_bundling(): + + # Invent a unique identifier. + + utcnow = get_timestamp() + uid = "imip-agent-%s-%s" % (utcnow, get_address(self.user)) + + freebusy = self.store.get_freebusy(self.user) + + user_attr = {} + self.update_sender(user_attr) + return to_part("PUBLISH", [make_freebusy(freebusy, uid, self.user, user_attr)]) + + return None + class ClientForObject(Client): "A client maintaining a specific object." - def __init__(self, obj, user, messenger=None): - Client.__init__(self, user, messenger) + def __init__(self, obj, user, messenger=None, store=None, publisher=None): + Client.__init__(self, user, messenger, store, publisher) self.set_object(obj) def set_object(self, obj): @@ -190,11 +229,7 @@ else: self.remove_from_freebusy(freebusy) - def is_participating(self, attr, as_organiser=False): - return as_organiser or not attr or attr.get("PARTSTAT") != "DECLINED" - - def get_overriding_transparency(self, attr, as_organiser=False): - return as_organiser and not (attr and attr.get("PARTSTAT")) and "ORG" or None + # Object update methods. def update_dtstamp(self): diff -r 1c4f5c708f20 -r ef9992ae1c6e imiptools/handlers/__init__.py --- a/imiptools/handlers/__init__.py Sun Jul 26 01:48:20 2015 +0200 +++ b/imiptools/handlers/__init__.py Sun Jul 26 01:59:34 2015 +0200 @@ -30,7 +30,6 @@ remove_additional_periods, remove_affected_period from imiptools.profile import Preferences from socket import gethostname -import imip_store # References to the Web interface. @@ -62,7 +61,7 @@ default store and publisher objects. """ - ClientForObject.__init__(self, None, recipient and get_uri(recipient), messenger) + ClientForObject.__init__(self, None, recipient and get_uri(recipient), messenger, store, publisher) self.senders = senders and set(map(get_address, senders)) self.recipient = recipient and get_address(recipient) @@ -70,13 +69,6 @@ self.results = [] self.outgoing_methods = set() - self.store = store or imip_store.FileStore() - - try: - self.publisher = publisher or imip_store.FilePublisher() - except OSError: - self.publisher = None - def wrap(self, text, link=True): "Wrap any valid message for passing to the recipient." diff -r 1c4f5c708f20 -r ef9992ae1c6e imipweb/client.py --- a/imipweb/client.py Sun Jul 26 01:48:20 2015 +0200 +++ b/imipweb/client.py Sun Jul 26 01:59:34 2015 +0200 @@ -58,30 +58,20 @@ # Bundle free/busy information if appropriate. - if self.is_sharing() and self.is_bundling(): - - # Invent a unique identifier. - - utcnow = get_timestamp() - uid = "imip-agent-%s-%s" % (utcnow, get_address(self.user)) - - freebusy = self.store.get_freebusy(self.user) + part = self.get_freebusy_part() + if part: + parts.append(part) # 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. + freebusy = self.store.get_freebusy(self.user) + self.update_freebusy(freebusy, self.obj.get_periods(self.get_tzid(), self.get_window_end())) - user_attr = {} - self.update_sender(user_attr) - - parts.append(to_part("PUBLISH", [ - make_freebusy(freebusy, uid, self.user, user_attr) - ])) - # Explicitly specify the outgoing BCC recipient since we are sending as # the generic calendar user.