#!/usr/bin/python import os, subprocess, sys, textwrap, traceback, urllib, urlparse # Example of configuration of postfix to deliver to ERP5: # - Add the following lines to master.cf: # erp5 unix - n n - - pipe # flags=FR user=erp5 argv=/path_to/sendMailToERP5 --ingestion_map=... # recipient=${recipient} # - Tell smtpd service to use the new filter, by adding: # -o content_filter=erp5: class HTTPError(IOError): def __init__(self, errcode, errmsg, result): self.__dict__.update(errcode=errcode, errmsg=errmsg, result=result) def __str__(self): return '%s %s' % (self.errcode, self.errmsg) class urlopen(urllib.FancyURLopener, object): """Open a network object denoted by a URL for reading Raise a HTTPError exception if HTTP error code is not 200. """ def __new__(cls, *args, **kw): self = object.__new__(cls) self.__init__() return self.open(*args, **kw) def http_error(self, url, fp, errcode, errmsg, headers, data=None): raise HTTPError(errcode, errmsg, self.http_error_default(url, fp, errcode, errmsg, headers)) class Message(object): def __init__(self, *args): for arg in args: k, v = arg.split('=', 1) if not k.islower(): raise ValueError old = getattr(self, k, None) if old is not None: if type(old) is list: old.append(v) continue v = [old, v] setattr(self, k, v) recipient_list = self.__dict__.pop('recipient', []) if isinstance(recipient_list, basestring): recipient_list = [recipient_list] self.recipient_list = recipient_list def __call__(self, portal=None, **kw): # A filter should not deliver to more than one place, otherwise we can't # avoid duplicate (or lost) mails in case of failure. # So this method must not be modified to allow delivery to several # destinations. Additional deliveries (even if locally), must be done # by the ERP5 instance itself, by activity. if portal == 'UNAVAILABLE': print 'Message rejected' sys.exit(os.EX_UNAVAILABLE) if portal == 'SENDMAIL': print 'Deliver message locally ...' os.execl('/usr/sbin/sendmail', 'sendmail', '-G', '-i', *self.recipient_list) if portal is not None: scheme, netloc, path, query, fragment = urlparse.urlsplit(portal) if query or fragment: raise ValueError user, host = urllib.splituser(netloc) if user is None: password = None else: user, password = urllib.splitpasswd(user) user = kw.pop('user', user) if user is not None: password = kw.pop('password', password) if password is not None: user = '%s:%s' % (user, password) host = '%s@%s' % (user, host) url = urlparse.urlunsplit((scheme, host, path.rstrip('/'), '', '')) + \ '/portal_contributions/newContent' kw['data'] = sys.stdin.read() try: result = urlopen(url, urllib.urlencode(kw)) except HTTPError, e: if e.errcode >= 300: raise result = e.result result.read() # ERP5 does not return useful information print 'Message ingested' else: print 'Message dropped' class SimpleIngestionMap(object): """Simple implementation of ingestion map, using a Python file as database This class maps recipients to parameters for portal_contributions/newContent """ def __init__(self, ingestion_map_filename): fd = file(ingestion_map_filename) g = {} try: exec fd in g finally: fd.close() self._map = g['ingestion_map'] def __call__(self, message, **kw): for recipient in message.recipient_list: recipient = self._map.get(recipient) if recipient: kw.update(recipient) break return message(**kw) def getOptionParser(): from optparse import IndentedHelpFormatter, OptionGroup, OptionParser class Formatter(IndentedHelpFormatter): """Subclass IndentedHelpFormatter to preserve line breaks in description""" def format_description(self, description): return ''.join(IndentedHelpFormatter.format_description(self, x) for x in description.split('\n')) parser = OptionParser(usage="%prog [options] [=]...", formatter=Formatter(), description="""Positional \ arguments defines variables that are used by ingestion maps to determine \ options to send to ERP5. Currently, only 'recipient' key is used. This tool can be used directly to deliver mails from postfix to ERP5, \ by using it as a filter (cf documentation of /etc/postfix/master.cf).""") _ = parser.add_option _("--portal", help="URL of ERP5 instance to connect to, or one of the" " following special values: 'UNAVAILABLE' returns the mail" " to the sender; 'SENDMAIL' injects it back into MTA") _("--user", help="use this user to connect to ERP5") _("--password", help="use this password to connect to ERP5") _("--file_name", help="ERP5 requires a file name to guess content type") _("--container_path", help="define where to contribute the content" " (by default, it is guessed by ERP5)") #_("--portal_type", default="Mail Message") group = OptionGroup(parser, "Ingestion map", """Above options can be \ overridden according to recipients, using a Python module as ingestion map \ database. The module must define an 'ingestion_map' variable implementing \ 'get(recipient) -> option_dict'. Example: ingestion_map = { 'foo@bar.com': dict(user='foo', password='12345'), 'patches@prj1.org': dict(file_name='prj1.patch'), 'spam@example.invalid': dict(portal=None), # drop }""") group.add_option("--ingestion_map", help="get options from this file," " according to recipients") parser.add_option_group(group) _ = group.add_option parser.set_defaults(file_name="unnamed.eml") return parser def main(): parser = getOptionParser() options, args = parser.parse_args() message = Message(*args) default = {} for option in parser.option_list: dest = option.dest if dest not in (None, 'ingestion_map'): value = getattr(options, dest) if value is not None: default[dest] = value if options.ingestion_map: SimpleIngestionMap(options.ingestion_map)(message, **default) else: message(**default) if __name__ == '__main__': try: main() except SystemExit: raise except: traceback.print_exc() sys.exit(os.EX_TEMPFAIL)