imip-agent

imiptools/__init__.py

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