imip-agent

imiptools/__init__.py

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