imip-agent

imiptools/__init__.py

749:3b0ac9430955
2015-09-18 Paul Boddie Added support for configurable default preferences.
     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 import config    24 from imiptools.client import Client    25 from imiptools.content import handle_itip_part    26 from imiptools.data import get_address, get_addresses, get_uri    27 from imiptools.mail import Messenger    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):    55         self.handlers = handlers    56         self.messenger = None    57         self.lmtp_socket = None    58         self.store_dir = None    59         self.publishing_dir = None    60         self.preferences_dir = None    61         self.debug = False    62     63     def get_store(self):    64         return imip_store.FileStore(self.store_dir)    65     66     def get_publisher(self):    67         return self.publishing_dir and imip_store.FilePublisher(self.publishing_dir) or None    68     69     def process(self, f, original_recipients, outgoing_only):    70     71         """    72         Process content from the stream 'f' accompanied by the given    73         'original_recipients'.    74         """    75     76         msg = message_from_file(f)    77         senders = get_addresses(get_all_values(msg, "Reply-To") or get_all_values(msg, "From") or [])    78     79         messenger = self.messenger    80         store = self.get_store()    81         publisher = self.get_publisher()    82         preferences_dir = self.preferences_dir    83     84         # Handle messages with iTIP parts.    85         # Typically, the details of recipients are of interest in handling    86         # messages.    87     88         if not outgoing_only:    89             original_recipients = original_recipients or get_addresses(get_all_values(msg, "To") or [])    90             for recipient in original_recipients:    91                 Recipient(get_uri(recipient), messenger, store, publisher, preferences_dir, self.handlers, self.debug).process(msg, senders, outgoing_only)    92     93         # However, outgoing messages do not usually presume anything about the    94         # eventual recipients.    95     96         else:    97             Recipient(None, messenger, store, publisher, preferences_dir, self.handlers, self.debug).process(msg, senders, outgoing_only)    98     99     def process_args(self, args, stream):   100    101         """   102         Interpret the given program arguments 'args' and process input from the   103         given 'stream'.   104         """   105    106         # Obtain the different kinds of recipients plus sender address.   107    108         original_recipients = []   109         recipients = []   110         senders = []   111         lmtp = []   112         store_dir = []   113         publishing_dir = []   114         preferences_dir = []   115         local_smtp = False   116         outgoing_only = False   117    118         l = []   119    120         for arg in args:   121    122             # Detect outgoing processing mode.   123    124             if arg == "-O":   125                 outgoing_only = True   126    127             # Switch to collecting recipients.   128    129             elif arg == "-o":   130                 l = original_recipients   131    132             # Switch to collecting senders.   133    134             elif arg == "-s":   135                 l = senders   136    137             # Switch to getting the LMTP socket.   138    139             elif arg == "-l":   140                 l = lmtp   141    142             # Detect sending to local users via SMTP.   143    144             elif arg == "-L":   145                 local_smtp = True   146    147             # Switch to getting the store directory.   148    149             elif arg == "-S":   150                 l = store_dir   151    152             # Switch to getting the publishing directory.   153    154             elif arg == "-P":   155                 l = publishing_dir   156    157             # Switch to getting the preferences directory.   158    159             elif arg == "-p":   160                 l = preferences_dir   161    162             # Ignore debugging options.   163    164             elif arg == "-d":   165                 self.debug = True   166             else:   167                 l.append(arg)   168    169         self.messenger = Messenger(lmtp_socket=lmtp and lmtp[0] or None, local_smtp=local_smtp, sender=senders and senders[0] or None)   170         self.store_dir = store_dir and store_dir[0] or None   171         self.publishing_dir = publishing_dir and publishing_dir[0] or None   172         self.preferences_dir = preferences_dir and preferences_dir[0] or None   173         self.process(stream, original_recipients, outgoing_only)   174    175     def __call__(self):   176    177         """   178         Obtain arguments from the command line to initialise the processor   179         before invoking it.   180         """   181    182         args = sys.argv[1:]   183    184         if "-d" in args:   185             self.process_args(args, sys.stdin)   186         else:   187             try:   188                 self.process_args(args, sys.stdin)   189             except SystemExit, value:   190                 sys.exit(value)   191             except Exception, exc:   192                 if "-v" in args:   193                     raise   194                 type, value, tb = sys.exc_info()   195                 while tb.tb_next:   196                     tb = tb.tb_next   197                 f = tb.tb_frame   198                 co = f and f.f_code   199                 filename = co and co.co_filename   200                 print >>sys.stderr, "Exception %s at %d in %s" % (exc, tb.tb_lineno, filename)   201                 #import traceback   202                 #traceback.print_exc(file=open("/tmp/mail.log", "a"))   203                 sys.exit(EX_TEMPFAIL)   204         sys.exit(0)   205    206 class Recipient(Client):   207    208     "A processor acting as a client on behalf of a recipient."   209    210     def __init__(self, user, messenger, store, publisher, preferences_dir, handlers, debug):   211    212         """   213         Initialise the recipient with the given 'user' identity, 'messenger',   214         'store', 'publisher', 'preferences_dir', 'handlers' and 'debug' status.   215         """   216    217         Client.__init__(self, user, messenger, store, publisher, preferences_dir)   218         self.handlers = handlers   219         self.debug = debug   220    221     def process(self, msg, senders, outgoing_only):   222    223         """   224         Process the given 'msg' for a single recipient, having the given   225         'senders', and with the given 'outgoing_only' status.   226    227         Processing individually means that contributions to resulting messages   228         may be constructed according to individual preferences.   229         """   230    231         handlers = dict([(name, cls(senders, self.user and get_address(self.user),   232                                     self.messenger, self.store, self.publisher,   233                                     self.preferences_dir))   234                          for name, cls in self.handlers])   235         handled = False   236    237         # Check for participating recipients. Non-participating recipients will   238         # have their messages left as being unhandled.   239    240         # Note that no user is set for outgoing messages, and so a check for   241         # their participation must be done in an outgoing handler once they are   242         # identified.   243    244         if outgoing_only or self.is_participating():   245    246             # Check for returned messages.   247    248             for part in msg.walk():   249                 if part.get_content_type() == "message/delivery-status":   250                     break   251             else:   252                 for part in msg.walk():   253                     if part.get_content_type() in itip_content_types and \   254                        part.get_param("method"):   255    256                         handle_itip_part(part, handlers)   257                         handled = True   258    259         # When processing outgoing messages, no replies or deliveries are   260         # performed.   261    262         if outgoing_only:   263             return   264    265         # Get responses from the handlers.   266    267         all_responses = []   268         for handler in handlers.values():   269             all_responses += handler.get_results()   270    271         # Pack any returned parts into messages.   272    273         if all_responses:   274             outgoing_parts = {}   275             forwarded_parts = []   276    277             for outgoing_recipients, part in all_responses:   278                 if outgoing_recipients:   279                     for outgoing_recipient in outgoing_recipients:   280                         if not outgoing_parts.has_key(outgoing_recipient):   281                             outgoing_parts[outgoing_recipient] = []   282                         outgoing_parts[outgoing_recipient].append(part)   283                 else:   284                     forwarded_parts.append(part)   285    286             # Reply using any outgoing parts in a new message.   287    288             if outgoing_parts:   289    290                 # Obtain free/busy details, if configured to do so.   291    292                 fb = self.can_provide_freebusy(handlers) and self.get_freebusy_part()   293    294                 for outgoing_recipient, parts in outgoing_parts.items():   295    296                     # Bundle free/busy messages, if configured to do so.   297    298                     if fb: parts.append(fb)   299                     message = self.messenger.make_outgoing_message(parts, [outgoing_recipient])   300    301                     if self.debug:   302                         print >>sys.stderr, "Outgoing parts for %s..." % outgoing_recipient   303                         print message   304                     else:   305                         self.messenger.sendmail([outgoing_recipient], message.as_string())   306    307             # Forward messages to their recipients either wrapping the existing   308             # message, accompanying it or replacing it.   309    310             if forwarded_parts:   311    312                 # Determine whether to wrap, accompany or replace the message.   313    314                 prefs = self.get_preferences()   315                 incoming = prefs.get("incoming", config.INCOMING_DEFAULT)   316    317                 if incoming == "message-only":   318                     messages = [msg]   319                 else:   320                     summary = self.messenger.make_summary_message(msg, forwarded_parts)   321                     if incoming == "summary-then-message":   322                         messages = [summary, msg]   323                     elif incoming == "message-then-summary":   324                         messages = [msg, summary]   325                     elif incoming == "summary-only":   326                         messages = [summary]   327                     else: # incoming == "summary-wraps-message":   328                         messages = [self.messenger.wrap_message(msg, forwarded_parts)]   329    330                 for message in messages:   331                     if self.debug:   332                         print >>sys.stderr, "Forwarded parts..."   333                         print message   334                     elif self.messenger.local_delivery():   335                         self.messenger.sendmail([get_address(self.user)], message.as_string())   336    337         # Unhandled messages are delivered as they are.   338    339         if not handled:   340             if self.debug:   341                 print >>sys.stderr, "Unhandled parts..."   342                 print msg   343             elif self.messenger.local_delivery():   344                 self.messenger.sendmail([get_address(self.user)], msg.as_string())   345    346     def can_provide_freebusy(self, handlers):   347    348         "Test for any free/busy information produced by 'handlers'."   349    350         fbhandler = handlers.get("VFREEBUSY")   351         if fbhandler:   352             fbmethods = fbhandler.get_outgoing_methods()   353             return not "REPLY" in fbmethods and not "PUBLISH" in fbmethods   354         else:   355             return False   356    357 # vim: tabstop=4 expandtab shiftwidth=4