imip-agent

imiptools/__init__.py

1068:f4bf00639b35
2016-03-06 Paul Boddie Introduced useful conceptual methods. Removed superfluous import.
     1 #!/usr/bin/env python     2      3 """     4 A processing framework for iMIP content.     5      6 Copyright (C) 2014, 2015, 2016 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, os    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, outgoing_only=False):    55         self.handlers = handlers    56         self.outgoing_only = outgoing_only    57         self.messenger = None    58         self.lmtp_socket = None    59         self.store_dir = None    60         self.publishing_dir = None    61         self.journal_dir = None    62         self.preferences_dir = None    63         self.debug = False    64     65     def get_store(self):    66         return imip_store.FileStore(self.store_dir)    67     68     def get_publisher(self):    69         return self.publishing_dir and imip_store.FilePublisher(self.publishing_dir) or None    70     71     def get_journal(self):    72         return imip_store.FileJournal(self.journal_dir)    73     74     def process(self, f, original_recipients):    75     76         """    77         Process content from the stream 'f' accompanied by the given    78         'original_recipients'.    79         """    80     81         msg = message_from_file(f)    82         senders = get_addresses(get_all_values(msg, "Reply-To") or get_all_values(msg, "From") or [])    83     84         messenger = self.messenger    85         store = self.get_store()    86         publisher = self.get_publisher()    87         journal = self.get_journal()    88         preferences_dir = self.preferences_dir    89     90         # Handle messages with iTIP parts.    91         # Typically, the details of recipients are of interest in handling    92         # messages.    93     94         if not self.outgoing_only:    95             original_recipients = original_recipients or get_addresses(get_all_values(msg, "To") or [])    96             for recipient in original_recipients:    97                 Recipient(get_uri(recipient), messenger, store, publisher, journal, preferences_dir, self.handlers, self.outgoing_only, self.debug    98                          ).process(msg, senders)    99    100         # However, outgoing messages do not usually presume anything about the   101         # eventual recipients and focus on the sender instead. If possible, the   102         # sender is identified, but since this may be the calendar system (and   103         # the actual sender is defined in the object), and since the recipient   104         # may be in a Bcc header that is not available here, it may be left as   105         # None and deduced from the object content later.    106    107         else:   108             senders = [sender for sender in get_addresses(get_all_values(msg, "From") or []) if sender != config.MESSAGE_SENDER]   109             Recipient(senders and senders[0] or None, messenger, store, publisher, journal, preferences_dir, self.handlers, self.outgoing_only, self.debug   110                      ).process(msg, senders)   111    112     def process_args(self, args, stream):   113    114         """   115         Interpret the given program arguments 'args' and process input from the   116         given 'stream'.   117         """   118    119         # Obtain the different kinds of recipients plus sender address.   120    121         original_recipients = []   122         recipients = []   123         senders = []   124         lmtp = []   125         store_dir = []   126         publishing_dir = []   127         preferences_dir = []   128         journal_dir = []   129         local_smtp = False   130    131         l = []   132    133         for arg in args:   134    135             # Switch to collecting recipients.   136    137             if arg == "-o":   138                 l = original_recipients   139    140             # Switch to collecting senders.   141    142             elif arg == "-s":   143                 l = senders   144    145             # Switch to getting the LMTP socket.   146    147             elif arg == "-l":   148                 l = lmtp   149    150             # Detect sending to local users via SMTP.   151    152             elif arg == "-L":   153                 local_smtp = True   154    155             # Switch to getting the store directory.   156    157             elif arg == "-S":   158                 l = store_dir   159    160             # Switch to getting the publishing directory.   161    162             elif arg == "-P":   163                 l = publishing_dir   164    165             # Switch to getting the preferences directory.   166    167             elif arg == "-p":   168                 l = preferences_dir   169    170             # Switch to getting the journal directory.   171    172             elif arg == "-j":   173                 l = journal_dir   174    175             # Ignore debugging options.   176    177             elif arg == "-d":   178                 self.debug = True   179             else:   180                 l.append(arg)   181    182         self.messenger = Messenger(lmtp_socket=lmtp and lmtp[0] or None, local_smtp=local_smtp, sender=senders and senders[0] or None)   183         self.store_dir = store_dir and store_dir[0] or None   184         self.publishing_dir = publishing_dir and publishing_dir[0] or None   185         self.preferences_dir = preferences_dir and preferences_dir[0] or None   186         self.journal_dir = journal_dir and journal_dir[0] or None   187         self.process(stream, original_recipients)   188    189     def __call__(self):   190    191         """   192         Obtain arguments from the command line to initialise the processor   193         before invoking it.   194         """   195    196         args = sys.argv[1:]   197    198         if "--help" in args:   199             print >>sys.stderr, """\   200 Usage: %s [ -o <recipient> ... ] [-s <sender> ... ] [ -l <socket> | -L ] \\   201          [ -S <store directory> ] [ -P <publishing directory> ] \\   202          [ -p <preferences directory> ] [ -j <journal directory> ] [ -d ]   203    204 Address options:   205    206 -o  Indicate the original recipients of the message, overriding any found in   207     the message headers   208 -s  Indicate the senders of the message, overriding any found in the message   209     headers   210    211 Delivery options:   212    213 -l  The socket filename for LMTP communication with a mailbox solution,   214     selecting the LMTP delivery method   215 -L  Selects the local SMTP delivery method, requiring a suitable mail system   216     configuration   217    218 (Where a program needs to deliver messages, one of the above options must be   219 specified.)   220    221 Configuration options:   222    223 -j  Indicates the location of quota-related journal information   224 -P  Indicates the location of published free/busy resources   225 -p  Indicates the location of user preference directories   226 -S  Indicates the location of the calendar data store containing user storage   227     directories   228    229 Output options:   230    231 -d  Run in debug mode, producing informative output describing the behaviour   232     of the program   233 """ % os.path.split(sys.argv[0])[-1]   234         elif "-d" in args:   235             self.process_args(args, sys.stdin)   236         else:   237             try:   238                 self.process_args(args, sys.stdin)   239             except SystemExit, value:   240                 sys.exit(value)   241             except Exception, exc:   242                 if "-v" in args:   243                     raise   244                 type, value, tb = sys.exc_info()   245                 while tb.tb_next:   246                     tb = tb.tb_next   247                 f = tb.tb_frame   248                 co = f and f.f_code   249                 filename = co and co.co_filename   250                 print >>sys.stderr, "Exception %s at %d in %s" % (exc, tb.tb_lineno, filename)   251                 #import traceback   252                 #traceback.print_exc(file=open("/tmp/mail.log", "a"))   253                 sys.exit(EX_TEMPFAIL)   254         sys.exit(0)   255    256 class Recipient(Client):   257    258     "A processor acting as a client on behalf of a recipient."   259    260     def __init__(self, user, messenger, store, publisher, journal, preferences_dir,   261                  handlers, outgoing_only, debug):   262    263         """   264         Initialise the recipient with the given 'user' identity, 'messenger',   265         'store', 'publisher', 'journal', 'preferences_dir', 'handlers',   266         'outgoing_only' and 'debug' status.   267         """   268    269         Client.__init__(self, user, messenger, store, publisher, journal, preferences_dir)   270         self.handlers = handlers   271         self.outgoing_only = outgoing_only   272         self.debug = debug   273    274     def process(self, msg, senders):   275    276         """   277         Process the given 'msg' for a single recipient, having the given   278         'senders'.   279    280         Processing individually means that contributions to resulting messages   281         may be constructed according to individual preferences.   282         """   283    284         handlers = dict([(name, cls(senders, self.user and get_address(self.user),   285                                     self.messenger, self.store, self.publisher,   286                                     self.journal, self.preferences_dir))   287                          for name, cls in self.handlers])   288         handled = False   289    290         # Check for participating recipients. Non-participating recipients will   291         # have their messages left as being unhandled.   292    293         if self.outgoing_only or self.is_participating():   294    295             # Check for returned messages.   296    297             for part in msg.walk():   298                 if part.get_content_type() == "message/delivery-status":   299                     break   300             else:   301                 for part in msg.walk():   302                     if part.get_content_type() in itip_content_types and \   303                        part.get_param("method"):   304    305                         handle_itip_part(part, handlers)   306                         handled = True   307    308         # When processing outgoing messages, no replies or deliveries are   309         # performed.   310    311         if self.outgoing_only:   312             return   313    314         # Get responses from the handlers.   315    316         all_responses = []   317         for handler in handlers.values():   318             all_responses += handler.get_results()   319    320         # Pack any returned parts into messages.   321    322         if all_responses:   323             outgoing_parts = {}   324             forwarded_parts = []   325    326             for outgoing_recipients, part in all_responses:   327                 if outgoing_recipients:   328                     for outgoing_recipient in outgoing_recipients:   329                         if not outgoing_parts.has_key(outgoing_recipient):   330                             outgoing_parts[outgoing_recipient] = []   331                         outgoing_parts[outgoing_recipient].append(part)   332                 else:   333                     forwarded_parts.append(part)   334    335             # Reply using any outgoing parts in a new message.   336    337             if outgoing_parts:   338    339                 # Obtain free/busy details, if configured to do so.   340    341                 fb = self.can_provide_freebusy(handlers) and self.get_freebusy_part()   342    343                 for outgoing_recipient, parts in outgoing_parts.items():   344    345                     # Bundle free/busy messages, if configured to do so.   346    347                     if fb: parts.append(fb)   348                     message = self.messenger.make_outgoing_message(parts, [outgoing_recipient])   349    350                     if self.debug:   351                         print >>sys.stderr, "Outgoing parts for %s..." % outgoing_recipient   352                         print message   353                     else:   354                         self.messenger.sendmail([outgoing_recipient], message.as_string())   355    356             # Forward messages to their recipients either wrapping the existing   357             # message, accompanying it or replacing it.   358    359             if forwarded_parts:   360    361                 # Determine whether to wrap, accompany or replace the message.   362    363                 prefs = self.get_preferences()   364                 incoming = prefs.get("incoming", config.INCOMING_DEFAULT)   365    366                 if incoming == "message-only":   367                     messages = [msg]   368                 else:   369                     summary = self.messenger.make_summary_message(msg, forwarded_parts)   370                     if incoming == "summary-then-message":   371                         messages = [summary, msg]   372                     elif incoming == "message-then-summary":   373                         messages = [msg, summary]   374                     elif incoming == "summary-only":   375                         messages = [summary]   376                     else: # incoming == "summary-wraps-message":   377                         messages = [self.messenger.wrap_message(msg, forwarded_parts)]   378    379                 for message in messages:   380                     if self.debug:   381                         print >>sys.stderr, "Forwarded parts..."   382                         print message   383                     elif self.messenger.local_delivery():   384                         self.messenger.sendmail([get_address(self.user)], message.as_string())   385    386         # Unhandled messages are delivered as they are.   387    388         if not handled:   389             if self.debug:   390                 print >>sys.stderr, "Unhandled parts..."   391                 print msg   392             elif self.messenger.local_delivery():   393                 self.messenger.sendmail([get_address(self.user)], msg.as_string())   394    395     def can_provide_freebusy(self, handlers):   396    397         "Test for any free/busy information produced by 'handlers'."   398    399         fbhandler = handlers.get("VFREEBUSY")   400         if fbhandler:   401             fbmethods = fbhandler.get_outgoing_methods()   402             return not "REPLY" in fbmethods and not "PUBLISH" in fbmethods   403         else:   404             return False   405    406 # vim: tabstop=4 expandtab shiftwidth=4