1.1 --- a/imiptools/__init__.py Sun Jul 26 01:48:20 2015 +0200
1.2 +++ b/imiptools/__init__.py Sun Jul 26 01:59:34 2015 +0200
1.3 @@ -20,11 +20,10 @@
1.4 """
1.5
1.6 from email import message_from_file
1.7 +from imiptools.client import Client
1.8 from imiptools.content import handle_itip_part
1.9 -from imiptools.data import get_addresses, get_uri, make_freebusy, to_part
1.10 -from imiptools.dates import get_timestamp
1.11 +from imiptools.data import get_address, get_addresses, get_uri
1.12 from imiptools.mail import Messenger
1.13 -from imiptools.profile import Preferences
1.14 import imip_store
1.15 import sys
1.16
1.17 @@ -75,6 +74,10 @@
1.18 msg = message_from_file(f)
1.19 senders = get_addresses(msg.get_all("Reply-To") or msg.get_all("From") or [])
1.20
1.21 + messenger = self.messenger
1.22 + store = self.get_store()
1.23 + publisher = self.get_publisher()
1.24 +
1.25 # Handle messages with iTIP parts.
1.26 # Typically, the details of recipients are of interest in handling
1.27 # messages.
1.28 @@ -82,161 +85,13 @@
1.29 if not outgoing_only:
1.30 original_recipients = original_recipients or get_addresses(get_all_values(msg, "To") or [])
1.31 for recipient in original_recipients:
1.32 - self.process_for_recipient(msg, recipient, senders, outgoing_only)
1.33 + Recipient(get_uri(recipient), messenger, store, publisher, self).process(msg, senders, outgoing_only)
1.34
1.35 # However, outgoing messages do not usually presume anything about the
1.36 # eventual recipients.
1.37
1.38 else:
1.39 - self.process_for_recipient(msg, None, senders, outgoing_only)
1.40 -
1.41 - def process_for_recipient(self, msg, recipient, senders, outgoing_only):
1.42 -
1.43 - """
1.44 - Process the given 'msg' for a single 'recipient', having the given
1.45 - 'senders', and with the given 'outgoing_only' status.
1.46 -
1.47 - Processing individually means that contributions to resulting messages
1.48 - may be constructed according to individual preferences.
1.49 - """
1.50 -
1.51 - store = self.get_store()
1.52 - publisher = self.get_publisher()
1.53 -
1.54 - handlers = dict([(name, cls(senders, recipient, self.messenger, store, publisher))
1.55 - for name, cls in self.handlers])
1.56 - handled = False
1.57 -
1.58 - for part in msg.walk():
1.59 - if part.get_content_type() in itip_content_types and \
1.60 - part.get_param("method"):
1.61 -
1.62 - handle_itip_part(part, handlers)
1.63 - handled = True
1.64 -
1.65 - # When processing outgoing messages, no replies or deliveries are
1.66 - # performed.
1.67 -
1.68 - if outgoing_only:
1.69 - return
1.70 -
1.71 - # Get responses from the handlers.
1.72 -
1.73 - all_responses = []
1.74 - for handler in handlers.values():
1.75 - all_responses += handler.get_results()
1.76 -
1.77 - # Pack any returned parts into messages.
1.78 -
1.79 - if all_responses:
1.80 - outgoing_parts = {}
1.81 - forwarded_parts = []
1.82 -
1.83 - for outgoing_recipients, part in all_responses:
1.84 - if outgoing_recipients:
1.85 - for outgoing_recipient in outgoing_recipients:
1.86 - if not outgoing_parts.has_key(outgoing_recipient):
1.87 - outgoing_parts[outgoing_recipient] = []
1.88 - outgoing_parts[outgoing_recipient].append(part)
1.89 - else:
1.90 - forwarded_parts.append(part)
1.91 -
1.92 - # Reply using any outgoing parts in a new message.
1.93 -
1.94 - if outgoing_parts:
1.95 -
1.96 - # Obtain free/busy details, if configured to do so.
1.97 -
1.98 - fb = self.can_provide_freebusy(handlers) and self.get_freebusy_for_recipient(recipient)
1.99 -
1.100 - for outgoing_recipient, parts in outgoing_parts.items():
1.101 -
1.102 - # Bundle free/busy messages, if configured to do so.
1.103 -
1.104 - if fb: parts.append(fb)
1.105 - message = self.messenger.make_outgoing_message(parts, [outgoing_recipient])
1.106 -
1.107 - if self.debug:
1.108 - print >>sys.stderr, "Outgoing parts for %s..." % outgoing_recipient
1.109 - print message
1.110 - else:
1.111 - self.messenger.sendmail([outgoing_recipient], message.as_string())
1.112 -
1.113 - # Forward messages to their recipients either wrapping the existing
1.114 - # message, accompanying it or replacing it.
1.115 -
1.116 - if forwarded_parts:
1.117 -
1.118 - # Determine whether to wrap, accompany or replace the message.
1.119 -
1.120 - preferences = Preferences(get_uri(recipient))
1.121 -
1.122 - incoming = preferences.get("incoming")
1.123 -
1.124 - if incoming == "message-only":
1.125 - messages = [msg]
1.126 - else:
1.127 - summary = self.messenger.make_summary_message(msg, forwarded_parts)
1.128 - if incoming == "summary-then-message":
1.129 - messages = [summary, msg]
1.130 - elif incoming == "message-then-summary":
1.131 - messages = [msg, summary]
1.132 - elif incoming == "summary-only":
1.133 - messages = [summary]
1.134 - else: # incoming == "summary-wraps-message":
1.135 - messages = [self.messenger.wrap_message(msg, forwarded_parts)]
1.136 -
1.137 - for message in messages:
1.138 - if self.debug:
1.139 - print >>sys.stderr, "Forwarded parts..."
1.140 - print message
1.141 - elif self.lmtp_socket:
1.142 - self.messenger.sendmail(recipient, message.as_string(), lmtp_socket=self.lmtp_socket)
1.143 -
1.144 - # Unhandled messages are delivered as they are.
1.145 -
1.146 - if not handled:
1.147 - if self.debug:
1.148 - print >>sys.stderr, "Unhandled parts..."
1.149 - print msg
1.150 - elif self.lmtp_socket:
1.151 - self.messenger.sendmail(recipient, msg.as_string(), lmtp_socket=self.lmtp_socket)
1.152 -
1.153 - def can_provide_freebusy(self, handlers):
1.154 -
1.155 - "Test for any free/busy information produced by 'handlers'."
1.156 -
1.157 - fbhandler = handlers.get("VFREEBUSY")
1.158 - if fbhandler:
1.159 - fbmethods = fbhandler.get_outgoing_methods()
1.160 - return not "REPLY" in fbmethods and not "PUBLISH" in fbmethods
1.161 - else:
1.162 - return False
1.163 -
1.164 - def get_freebusy_for_recipient(self, recipient):
1.165 -
1.166 - """
1.167 - Return a list of responses containing free/busy information for the
1.168 - given 'recipient'.
1.169 - """
1.170 -
1.171 - organiser = get_uri(recipient)
1.172 - preferences = Preferences(organiser)
1.173 -
1.174 - organiser_attr = self.messenger and {"SENT-BY" : get_uri(self.messenger.sender)} or {}
1.175 -
1.176 - if preferences.get("freebusy_sharing") == "share" and \
1.177 - preferences.get("freebusy_bundling") == "always":
1.178 -
1.179 - # Invent a unique identifier.
1.180 -
1.181 - utcnow = get_timestamp()
1.182 - uid = "imip-agent-%s-%s" % (utcnow, recipient)
1.183 -
1.184 - freebusy = self.get_store().get_freebusy(organiser)
1.185 - return to_part("PUBLISH", [make_freebusy(freebusy, uid, organiser, organiser_attr)])
1.186 -
1.187 - return None
1.188 + Recipient(None, messenger, store, publisher, self).process(msg, senders, outgoing_only)
1.189
1.190 def process_args(self, args, stream):
1.191
1.192 @@ -333,4 +188,139 @@
1.193 sys.exit(EX_TEMPFAIL)
1.194 sys.exit(0)
1.195
1.196 +class Recipient(Client):
1.197 +
1.198 + "A processor acting as a client on behalf of a recipient."
1.199 +
1.200 + def __init__(self, user, messenger, store, publisher, processor):
1.201 +
1.202 + """
1.203 + Initialise the recipient with the given 'user' identity, 'messenger',
1.204 + 'store', 'publisher' and 'processor'.
1.205 + """
1.206 +
1.207 + Client.__init__(self, user, messenger, store, publisher)
1.208 + self.processor = processor
1.209 +
1.210 + def process(self, msg, senders, outgoing_only):
1.211 +
1.212 + """
1.213 + Process the given 'msg' for a single recipient, having the given
1.214 + 'senders', and with the given 'outgoing_only' status.
1.215 +
1.216 + Processing individually means that contributions to resulting messages
1.217 + may be constructed according to individual preferences.
1.218 + """
1.219 +
1.220 + handlers = dict([(name, cls(senders, self.user and get_address(self.user),
1.221 + self.messenger, self.store, self.publisher))
1.222 + for name, cls in self.processor.handlers])
1.223 + handled = False
1.224 +
1.225 + for part in msg.walk():
1.226 + if part.get_content_type() in itip_content_types and \
1.227 + part.get_param("method"):
1.228 +
1.229 + handle_itip_part(part, handlers)
1.230 + handled = True
1.231 +
1.232 + # When processing outgoing messages, no replies or deliveries are
1.233 + # performed.
1.234 +
1.235 + if outgoing_only:
1.236 + return
1.237 +
1.238 + # Get responses from the handlers.
1.239 +
1.240 + all_responses = []
1.241 + for handler in handlers.values():
1.242 + all_responses += handler.get_results()
1.243 +
1.244 + # Pack any returned parts into messages.
1.245 +
1.246 + if all_responses:
1.247 + outgoing_parts = {}
1.248 + forwarded_parts = []
1.249 +
1.250 + for outgoing_recipients, part in all_responses:
1.251 + if outgoing_recipients:
1.252 + for outgoing_recipient in outgoing_recipients:
1.253 + if not outgoing_parts.has_key(outgoing_recipient):
1.254 + outgoing_parts[outgoing_recipient] = []
1.255 + outgoing_parts[outgoing_recipient].append(part)
1.256 + else:
1.257 + forwarded_parts.append(part)
1.258 +
1.259 + # Reply using any outgoing parts in a new message.
1.260 +
1.261 + if outgoing_parts:
1.262 +
1.263 + # Obtain free/busy details, if configured to do so.
1.264 +
1.265 + fb = self.can_provide_freebusy(handlers) and self.get_freebusy_part()
1.266 +
1.267 + for outgoing_recipient, parts in outgoing_parts.items():
1.268 +
1.269 + # Bundle free/busy messages, if configured to do so.
1.270 +
1.271 + if fb: parts.append(fb)
1.272 + message = self.messenger.make_outgoing_message(parts, [outgoing_recipient])
1.273 +
1.274 + if self.processor.debug:
1.275 + print >>sys.stderr, "Outgoing parts for %s..." % outgoing_recipient
1.276 + print message
1.277 + else:
1.278 + self.messenger.sendmail([outgoing_recipient], message.as_string())
1.279 +
1.280 + # Forward messages to their recipients either wrapping the existing
1.281 + # message, accompanying it or replacing it.
1.282 +
1.283 + if forwarded_parts:
1.284 +
1.285 + # Determine whether to wrap, accompany or replace the message.
1.286 +
1.287 + prefs = self.get_preferences()
1.288 +
1.289 + incoming = prefs.get("incoming")
1.290 +
1.291 + if incoming == "message-only":
1.292 + messages = [msg]
1.293 + else:
1.294 + summary = self.messenger.make_summary_message(msg, forwarded_parts)
1.295 + if incoming == "summary-then-message":
1.296 + messages = [summary, msg]
1.297 + elif incoming == "message-then-summary":
1.298 + messages = [msg, summary]
1.299 + elif incoming == "summary-only":
1.300 + messages = [summary]
1.301 + else: # incoming == "summary-wraps-message":
1.302 + messages = [self.messenger.wrap_message(msg, forwarded_parts)]
1.303 +
1.304 + for message in messages:
1.305 + if self.processor.debug:
1.306 + print >>sys.stderr, "Forwarded parts..."
1.307 + print message
1.308 + elif self.processor.lmtp_socket:
1.309 + self.messenger.sendmail(get_address(self.user), message.as_string(), lmtp_socket=self.processor.lmtp_socket)
1.310 +
1.311 + # Unhandled messages are delivered as they are.
1.312 +
1.313 + if not handled:
1.314 + if self.processor.debug:
1.315 + print >>sys.stderr, "Unhandled parts..."
1.316 + print msg
1.317 + elif self.processor.lmtp_socket:
1.318 + self.messenger.sendmail(get_address(self.user), msg.as_string(), lmtp_socket=self.processor.lmtp_socket)
1.319 +
1.320 + def can_provide_freebusy(self, handlers):
1.321 +
1.322 + "Test for any free/busy information produced by 'handlers'."
1.323 +
1.324 + fbhandler = handlers.get("VFREEBUSY")
1.325 + if fbhandler:
1.326 + fbmethods = fbhandler.get_outgoing_methods()
1.327 + return not "REPLY" in fbmethods and not "PUBLISH" in fbmethods
1.328 + else:
1.329 + return False
1.330 +
1.331 # vim: tabstop=4 expandtab shiftwidth=4
2.1 --- a/imiptools/client.py Sun Jul 26 01:48:20 2015 +0200
2.2 +++ b/imiptools/client.py Sun Jul 26 01:59:34 2015 +0200
2.3 @@ -20,11 +20,14 @@
2.4 """
2.5
2.6 from datetime import datetime
2.7 -from imiptools.data import get_address, get_uri, get_window_end, uri_dict, uri_items, uri_values
2.8 +from imiptools.data import get_address, get_uri, get_window_end, \
2.9 + make_freebusy, to_part, \
2.10 + uri_dict, uri_items, uri_values
2.11 +from imiptools.dates import format_datetime, get_default_timezone, \
2.12 + get_timestamp, to_timezone
2.13 from imiptools.period import update_freebusy
2.14 from imiptools.profile import Preferences
2.15 -from imiptools.dates import format_datetime, get_default_timezone, \
2.16 - to_timezone
2.17 +import imip_store
2.18
2.19 def update_attendees(obj, attendees, removed):
2.20
2.21 @@ -75,9 +78,16 @@
2.22
2.23 default_window_size = 100
2.24
2.25 - def __init__(self, user, messenger=None):
2.26 + def __init__(self, user, messenger=None, store=None, publisher=None):
2.27 self.user = user
2.28 self.messenger = messenger
2.29 + self.store = store or imip_store.FileStore()
2.30 +
2.31 + try:
2.32 + self.publisher = publisher or imip_store.FilePublisher()
2.33 + except OSError:
2.34 + self.publisher = None
2.35 +
2.36 self.preferences = None
2.37
2.38 def get_preferences(self):
2.39 @@ -113,6 +123,12 @@
2.40
2.41 # Common operations on calendar data.
2.42
2.43 + def is_participating(self, attr, as_organiser=False):
2.44 + return as_organiser or not attr or attr.get("PARTSTAT") != "DECLINED"
2.45 +
2.46 + def get_overriding_transparency(self, attr, as_organiser=False):
2.47 + return as_organiser and not (attr and attr.get("PARTSTAT")) and "ORG" or None
2.48 +
2.49 def update_participation(self, obj, partstat=None):
2.50
2.51 """
2.52 @@ -136,12 +152,35 @@
2.53 if self.messenger and self.messenger.sender != get_address(self.user):
2.54 attr["SENT-BY"] = get_uri(self.messenger.sender)
2.55
2.56 + # Free/busy operations.
2.57 +
2.58 + def get_freebusy_part(self):
2.59 +
2.60 + """
2.61 + Return a message part containing free/busy information for the user.
2.62 + """
2.63 +
2.64 + if self.is_sharing() and self.is_bundling():
2.65 +
2.66 + # Invent a unique identifier.
2.67 +
2.68 + utcnow = get_timestamp()
2.69 + uid = "imip-agent-%s-%s" % (utcnow, get_address(self.user))
2.70 +
2.71 + freebusy = self.store.get_freebusy(self.user)
2.72 +
2.73 + user_attr = {}
2.74 + self.update_sender(user_attr)
2.75 + return to_part("PUBLISH", [make_freebusy(freebusy, uid, self.user, user_attr)])
2.76 +
2.77 + return None
2.78 +
2.79 class ClientForObject(Client):
2.80
2.81 "A client maintaining a specific object."
2.82
2.83 - def __init__(self, obj, user, messenger=None):
2.84 - Client.__init__(self, user, messenger)
2.85 + def __init__(self, obj, user, messenger=None, store=None, publisher=None):
2.86 + Client.__init__(self, user, messenger, store, publisher)
2.87 self.set_object(obj)
2.88
2.89 def set_object(self, obj):
2.90 @@ -190,11 +229,7 @@
2.91 else:
2.92 self.remove_from_freebusy(freebusy)
2.93
2.94 - def is_participating(self, attr, as_organiser=False):
2.95 - return as_organiser or not attr or attr.get("PARTSTAT") != "DECLINED"
2.96 -
2.97 - def get_overriding_transparency(self, attr, as_organiser=False):
2.98 - return as_organiser and not (attr and attr.get("PARTSTAT")) and "ORG" or None
2.99 + # Object update methods.
2.100
2.101 def update_dtstamp(self):
2.102
3.1 --- a/imiptools/handlers/__init__.py Sun Jul 26 01:48:20 2015 +0200
3.2 +++ b/imiptools/handlers/__init__.py Sun Jul 26 01:59:34 2015 +0200
3.3 @@ -30,7 +30,6 @@
3.4 remove_additional_periods, remove_affected_period
3.5 from imiptools.profile import Preferences
3.6 from socket import gethostname
3.7 -import imip_store
3.8
3.9 # References to the Web interface.
3.10
3.11 @@ -62,7 +61,7 @@
3.12 default store and publisher objects.
3.13 """
3.14
3.15 - ClientForObject.__init__(self, None, recipient and get_uri(recipient), messenger)
3.16 + ClientForObject.__init__(self, None, recipient and get_uri(recipient), messenger, store, publisher)
3.17
3.18 self.senders = senders and set(map(get_address, senders))
3.19 self.recipient = recipient and get_address(recipient)
3.20 @@ -70,13 +69,6 @@
3.21 self.results = []
3.22 self.outgoing_methods = set()
3.23
3.24 - self.store = store or imip_store.FileStore()
3.25 -
3.26 - try:
3.27 - self.publisher = publisher or imip_store.FilePublisher()
3.28 - except OSError:
3.29 - self.publisher = None
3.30 -
3.31 def wrap(self, text, link=True):
3.32
3.33 "Wrap any valid message for passing to the recipient."