imip-agent

imiptools/__init__.py

560:ade19f50b58e
2015-05-18 Paul Boddie Produce recurring periods employing dates if they are involved. Handle missing DTSTART when encountering CANCEL messages.
     1 #!/usr/bin/env python     2      3 """     4 A processing framework for iMIP content.     5      6 Copyright (C) 2014, 2015 Paul Boddie <paul@boddie.org.uk>     7      8 This program is free software; you can redistribute it and/or modify it under     9 the terms of the GNU General Public License as published by the Free Software    10 Foundation; either version 3 of the License, or (at your option) any later    11 version.    12     13 This program is distributed in the hope that it will be useful, but WITHOUT    14 ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS    15 FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more    16 details.    17     18 You should have received a copy of the GNU General Public License along with    19 this program.  If not, see <http://www.gnu.org/licenses/>.    20 """    21     22 from email import message_from_file    23 from imiptools.content import handle_itip_part    24 from imiptools.data import get_addresses, get_uri, make_freebusy, to_part    25 from imiptools.dates import get_timestamp    26 from imiptools.mail import Messenger    27 from imiptools.profile import Preferences    28 import imip_store    29 import sys    30     31 # Postfix exit codes.    32     33 EX_TEMPFAIL     = 75    34     35 # Permitted iTIP content types.    36     37 itip_content_types = [    38     "text/calendar",                        # from RFC 6047    39     "text/x-vcalendar", "application/ics",  # other possibilities    40     ]    41     42 # Processing of incoming messages.    43     44 def get_all_values(msg, key):    45     l = []    46     for v in msg.get_all(key) or []:    47         l += [s.strip() for s in v.split(",")]    48     return l    49     50 class Processor:    51     52     "The processing framework."    53     54     def __init__(self, handlers, messenger=None):    55         self.handlers = handlers    56         self.messenger = messenger or Messenger()    57         self.lmtp_socket = None    58     59     def process(self, f, original_recipients, outgoing_only):    60     61         """    62         Process content from the stream 'f' accompanied by the given    63         'original_recipients'.    64         """    65     66         msg = message_from_file(f)    67         senders = get_addresses(msg.get_all("Reply-To") or msg.get_all("From") or [])    68     69         # Handle messages with iTIP parts.    70         # Typically, the details of recipients are of interest in handling    71         # messages.    72     73         if not outgoing_only:    74             original_recipients = original_recipients or get_addresses(get_all_values(msg, "To") or [])    75             for recipient in original_recipients:    76                 self.process_for_recipient(msg, recipient, senders, outgoing_only)    77     78         # However, outgoing messages do not usually presume anything about the    79         # eventual recipients.    80     81         else:    82             self.process_for_recipient(msg, None, senders, outgoing_only)    83     84     def process_for_recipient(self, msg, recipient, senders, outgoing_only):    85     86         """    87         Process the given 'msg' for a single 'recipient', having the given    88         'senders', and with the given 'outgoing_only' status.    89     90         Processing individually means that contributions to resulting messages    91         may be constructed according to individual preferences.    92         """    93     94         handlers = dict([(name, cls(senders, recipient, self.messenger)) for name, cls in self.handlers])    95         handled = False    96     97         for part in msg.walk():    98             if part.get_content_type() in itip_content_types and \    99                part.get_param("method"):   100    101                 handle_itip_part(part, handlers)   102                 handled = True   103    104         # When processing outgoing messages, no replies or deliveries are   105         # performed.   106    107         if outgoing_only:   108             return   109    110         # Get responses from the handlers.   111    112         all_responses = []   113         for handler in handlers.values():   114             all_responses += handler.get_results()   115    116         # Pack any returned parts into messages.   117    118         if all_responses:   119             outgoing_parts = {}   120             forwarded_parts = []   121    122             for outgoing_recipients, part in all_responses:   123                 if outgoing_recipients:   124                     for outgoing_recipient in outgoing_recipients:   125                         if not outgoing_parts.has_key(outgoing_recipient):   126                             outgoing_parts[outgoing_recipient] = []   127                         outgoing_parts[outgoing_recipient].append(part)   128                 else:   129                     forwarded_parts.append(part)   130    131             # Reply using any outgoing parts in a new message.   132    133             if outgoing_parts:   134    135                 # Obtain free/busy details, if configured to do so.   136    137                 fb = self.can_provide_freebusy(handlers) and self.get_freebusy_for_recipient(recipient)   138    139                 for outgoing_recipient, parts in outgoing_parts.items():   140    141                     # Bundle free/busy messages, if configured to do so.   142    143                     if fb: parts.append(fb)   144                     message = self.messenger.make_outgoing_message(parts, [outgoing_recipient])   145    146                     if "-d" in sys.argv:   147                         print >>sys.stderr, "Outgoing parts for %s..." % outgoing_recipient   148                         print message   149                     else:   150                         self.messenger.sendmail([outgoing_recipient], message.as_string())   151    152             # Forward messages to their recipients either wrapping the existing   153             # message, accompanying it or replacing it.   154    155             if forwarded_parts:   156    157                 # Determine whether to wrap, accompany or replace the message.   158    159                 preferences = Preferences(get_uri(recipient))   160    161                 incoming = preferences.get("incoming")   162    163                 if incoming == "message-only":   164                     messages = [msg]   165                 else:   166                     summary = self.messenger.make_summary_message(msg, forwarded_parts)   167                     if incoming == "summary-then-message":   168                         messages = [summary, msg]   169                     elif incoming == "message-then-summary":   170                         messages = [msg, summary]   171                     elif incoming == "summary-only":   172                         messages = [summary]   173                     else: # incoming == "summary-wraps-message":   174                         messages = [self.messenger.wrap_message(msg, forwarded_parts)]   175    176                 for message in messages:   177                     if "-d" in sys.argv:   178                         print >>sys.stderr, "Forwarded parts..."   179                         print message   180                     elif self.lmtp_socket:   181                         self.messenger.sendmail(recipient, message.as_string(), lmtp_socket=self.lmtp_socket)   182    183         # Unhandled messages are delivered as they are.   184    185         if not handled:   186             if "-d" in sys.argv:   187                 print >>sys.stderr, "Unhandled parts..."   188                 print msg   189             elif self.lmtp_socket:   190                 self.messenger.sendmail(recipient, msg.as_string(), lmtp_socket=self.lmtp_socket)   191    192     def can_provide_freebusy(self, handlers):   193    194         "Test for any free/busy information produced by 'handlers'."   195    196         fbhandler = handlers.get("VFREEBUSY")   197         if fbhandler:   198             fbmethods = fbhandler.get_outgoing_methods()   199             return not "REPLY" in fbmethods and not "PUBLISH" in fbmethods   200         else:   201             return False   202    203     def get_freebusy_for_recipient(self, recipient):   204    205         """   206         Return a list of responses containing free/busy information for the   207         given 'recipient'.   208         """   209    210         organiser = get_uri(recipient)   211         preferences = Preferences(organiser)   212    213         organiser_attr = self.messenger and {"SENT-BY" : get_uri(self.messenger.sender)} or {}   214    215         if preferences.get("freebusy_sharing") == "share" and \   216            preferences.get("freebusy_bundling") == "always":   217    218             # Invent a unique identifier.   219    220             utcnow = get_timestamp()   221             uid = "imip-agent-%s-%s" % (utcnow, recipient)   222    223             freebusy = imip_store.FileStore().get_freebusy(organiser)   224             return to_part("PUBLISH", [make_freebusy(freebusy, uid, organiser, organiser_attr)])   225    226         return None   227    228     def process_args(self, args, stream):   229    230         """   231         Interpret the given program arguments 'args' and process input from the   232         given 'stream'.   233         """   234    235         # Obtain the different kinds of recipients plus sender address.   236    237         original_recipients = []   238         recipients = []   239         senders = []   240         lmtp = []   241         outgoing_only = False   242    243         l = []   244    245         for arg in args:   246    247             # Detect outgoing processing mode.   248    249             if arg == "-O":   250                 outgoing_only = True   251    252             # Switch to collecting recipients.   253    254             if arg == "-o":   255                 l = original_recipients   256    257             # Switch to collecting senders.   258    259             elif arg == "-s":   260                 l = senders   261    262             # Switch to getting the LMTP socket.   263    264             elif arg == "-l":   265                 l = lmtp   266    267             # Ignore debugging options.   268    269             elif arg == "-d":   270                 pass   271             else:   272                 l.append(arg)   273    274         self.messenger.sender = senders and senders[0] or self.messenger.sender   275         self.lmtp_socket = lmtp and lmtp[0] or None   276         self.process(stream, original_recipients, outgoing_only)   277    278     def __call__(self):   279    280         """   281         Obtain arguments from the command line to initialise the processor   282         before invoking it.   283         """   284    285         args = sys.argv[1:]   286    287         if "-d" in args:   288             self.process_args(args, sys.stdin)   289         else:   290             try:   291                 self.process_args(args, sys.stdin)   292             except SystemExit, value:   293                 sys.exit(value)   294             except Exception, exc:   295                 if "-v" in args:   296                     raise   297                 type, value, tb = sys.exc_info()   298                 while tb.tb_next:   299                     tb = tb.tb_next   300                 f = tb.tb_frame   301                 co = f and f.f_code   302                 filename = co and co.co_filename   303                 print >>sys.stderr, "Exception %s at %d in %s" % (exc, tb.tb_lineno, filename)   304                 #import traceback   305                 #traceback.print_exc(file=open("/tmp/mail.log", "a"))   306                 sys.exit(EX_TEMPFAIL)   307         sys.exit(0)   308    309 # vim: tabstop=4 expandtab shiftwidth=4