imip-agent

imiptools/__init__.py

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