diff options
-rw-r--r-- | README.md | 4 | ||||
-rw-r--r-- | changes/feature_bounce_problematic_mails | 4 | ||||
-rw-r--r-- | pkg/mx.conf.sample | 19 | ||||
-rwxr-xr-x | pkg/mx.tac | 14 | ||||
-rw-r--r-- | src/leap/mx/mail_receiver.py | 140 |
5 files changed, 174 insertions, 7 deletions
@@ -59,6 +59,9 @@ $ git clone https://github.com/leapcode/leap_mx.git ~~~ Although, **it is advised** to install inside a python virtualenv. +## [configuration](#configuration) ## +A sample config file can be found in pkg/mx.conf.sample + ## [running](#running) ## ========================= @@ -78,4 +81,3 @@ Our bugtracker is [here](https://leap.se/code/projects/eip/issue/new). Please use that for bug reports and feature requests instead of github's tracker. We're using github for code commenting and review between collaborators. - diff --git a/changes/feature_bounce_problematic_mails b/changes/feature_bounce_problematic_mails new file mode 100644 index 0000000..39f059a --- /dev/null +++ b/changes/feature_bounce_problematic_mails @@ -0,0 +1,4 @@ + o Bounce mails when there's a problematic situation for an email, + such as no public key. Fixes #4803. + o Properly log tracebacks for exceptions occuring in the mail + processing loop.
\ No newline at end of file diff --git a/pkg/mx.conf.sample b/pkg/mx.conf.sample new file mode 100644 index 0000000..c9ad0f8 --- /dev/null +++ b/pkg/mx.conf.sample @@ -0,0 +1,19 @@ +[mail1] +path=/path/to/Maildir/ +recursive=<whether to analyze the above path recursively or not (True/False)> + +[couchdb] +user=<couch user> +password=<password> +server=localhost +port=6666 + +[alias map] +port=4242 + +[check recipient] +port=2244 + +[bounce] +from=<address for the From: of the bounce email without domain> +subject=Delivery failure
\ No newline at end of file @@ -46,6 +46,15 @@ password = config.get("couchdb", "password") server = config.get("couchdb", "server") port = config.get("couchdb", "port") +bounce_from = "bounce" +bounce_subject = "Delivery failure" + +try: + bounce_from = config.get("bounce", "from") + bounce_subject = config.get("bounce", "subject") +except ConfigParser.NoSectionError: + pass # we use the defaults above + alias_port = config.getint("alias map", "port") check_recipient_port = config.getint("check recipient", "port") @@ -74,11 +83,12 @@ mail_couch_url_prefix = "http://%s:%s@%s:%s" % (user, port) directories = [] for section in config.sections(): - if section in ("couchdb", "alias map", "check recipient"): + if section in ("couchdb", "alias map", "check recipient", "bounce"): continue to_watch = config.get(section, "path") recursive = config.getboolean(section, "recursive") directories.append([to_watch, recursive]) -mr = MailReceiver(mail_couch_url_prefix, cdb, directories) +mr = MailReceiver(mail_couch_url_prefix, cdb, directories, bounce_from, + bounce_subject) mr.setServiceParent(application) diff --git a/src/leap/mx/mail_receiver.py b/src/leap/mx/mail_receiver.py index 3561d33..86ba914 100644 --- a/src/leap/mx/mail_receiver.py +++ b/src/leap/mx/mail_receiver.py @@ -18,6 +18,16 @@ """ MailReceiver service definition + +If there's a user facing problem when processing an email, it will be +bounced back to the sender. + +User facing problems could be: +- Unknown user (missing uuid) +- Public key not found + +Any other problem is a bug, which will be logged. Until the bug is +fixed, the email will stay in there waiting. """ import os @@ -28,9 +38,13 @@ import email.utils import socket from email import message_from_string +from email.MIMEMultipart import MIMEMultipart +from email.MIMEText import MIMEText +from email.Utils import formatdate +from email.header import decode_header from twisted.application.service import Service -from twisted.internet import inotify, defer, task +from twisted.internet import inotify, defer, task, reactor from twisted.python import filepath, log from leap.soledad.common.crypto import ( @@ -41,6 +55,77 @@ from leap.soledad.common.crypto import ( from leap.soledad.common.couch import CouchDatabase, CouchDocument from leap.keymanager import openpgp +BOUNCE_TEMPLATE = """ +Delivery to the following recipient failed: + {0} + +Reasons: + {1} + +Original message: + {2} +""".strip() + + +from twisted.internet import protocol +from twisted.internet.error import ProcessDone + + +class BouncerSubprocessProtocol(protocol.ProcessProtocol): + """ + Bouncer subprocess protocol that will feed the msg contents to be + bounced through stdin + """ + + def __init__(self, msg): + """ + Constructor for the BouncerSubprocessProtocol + + :param msg: Message to send to stdin when the process has + launched + :type msg: str + """ + self._msg = msg + self._outBuffer = "" + self._errBuffer = "" + self._d = None + + def connectionMade(self): + self._d = defer.Deferred() + + self.transport.write(self._msg) + self.transport.closeStdin() + + def outReceived(self, data): + self._outBuffer += data + + def errReceived(self, data): + self._errBuffer += data + + def processEnded(self, reason): + if reason.check(ProcessDone): + self._d.callback(self._outBuffer) + else: + self._d.errback(reason) + + +def async_check_output(args, msg): + """ + Async spawn a process and return a defer to be able to check the + output with a callback/errback + + :param args: the command to execute along with the params for it + :type args: list of str + :param msg: string that will be send to stdin of the process once + it's spawned + :type msg: str + + :rtype: defer.Deferred + """ + pprotocol = BouncerSubprocessProtocol(msg) + reactor.spawnProcess(pprotocol, args[0], args) + return pprotocol.d + class MailReceiver(Service): """ @@ -49,7 +134,8 @@ class MailReceiver(Service): INCOMING_KEY = 'incoming' - def __init__(self, mail_couch_url, users_cdb, directories): + def __init__(self, mail_couch_url, users_cdb, directories, bounce_from, + bounce_subject): """ Constructor @@ -61,6 +147,10 @@ class MailReceiver(Service): :type users_cdb: ConnectedCouchDB :param directories: list of directories to monitor :type directories: list of tuples (path: str, recursive: bool) + :param bounce_from: Email address of the bouncer + :type bounce_from: str + :param bounce_subject: Subject line used in the bounced mail + :type bounce_subject: str """ # Service doesn't define an __init__ self._mail_couch_url = mail_couch_url @@ -69,6 +159,9 @@ class MailReceiver(Service): self._domain = socket.gethostbyaddr(socket.gethostname())[0] self._processing_skipped = False + self._bounce_from = bounce_from + self._bounce_subject = bounce_subject + def startService(self): """ Starts the MailReceiver service @@ -228,6 +321,37 @@ class MailReceiver(Service): return uuid + @defer.inlineCallbacks + def _bounce_mail(self, orig_msg, filepath, reason): + """ + Bounces the email contained in orig_msg to it's sender and + removes it from the queue. + + :param orig_msg: Message that is going to be bounced + :type orig_msg: email.message.Message + :param filepath: Path for that message + :type filepath: twisted.python.filepath.FilePath + :param reason: Brief explanation about why it's being bounced + :type reason: str + """ + to = orig_msg.get("From") + + msg = MIMEMultipart() + msg['From'] = self._bounce_from + msg['To'] = to + msg['Date'] = formatdate(localtime=True) + msg['Subject'] = self._bounce_subject + + decoded_to = " ".join([x[0] for x in decode_header(to)]) + text = BOUNCE_TEMPLATE.format(decoded_to, + reason, + orig_msg.as_string()) + + msg.attach(MIMEText(text)) + + yield async_check_output(["/usr/sbin/sendmail", "-t"], msg.as_string()) + yield self._conditional_remove(True, filepath) + def sleep(self, secs): """ Async sleep for a defer. Use this when you want to wait for @@ -266,13 +390,13 @@ class MailReceiver(Service): fullpath = os.path.join(root, fname) fpath = filepath.FilePath(fullpath) yield self._step_process_mail_backend(fpath) - except Exception as e: + except Exception: log.msg("Error processing skipped mail: %r" % \ (fullpath,)) log.err() if not recursive: break - except Exception as e: + except Exception: log.msg("Error processing skipped mail") log.err() finally: @@ -299,6 +423,9 @@ class MailReceiver(Service): if uuid is None: log.msg("Don't know how to deliver mail %r, skipping..." % (filepath.path,)) + bounce_reason = "Missing UUID: There was a problem " \ + "locating the user in our database." + yield self._bounce_mail(msg, filepath, bounce_reason) defer.returnValue(None) log.msg("Mail owner: %s" % (uuid,)) @@ -309,6 +436,10 @@ class MailReceiver(Service): pubkey = yield self._users_cdb.getPubKey(uuid) if pubkey is None or len(pubkey) == 0: log.msg("No public key, stopping the processing chain") + bounce_reason = "Missing PubKey: There was a problem " \ + "locating the user's public key in our " \ + "database." + yield self._bounce_mail(msg, filepath, bounce_reason) defer.returnValue(None) log.msg("Encrypting message to %s's pubkey" % (uuid,)) @@ -340,3 +471,4 @@ class MailReceiver(Service): except Exception as e: log.msg("Something went wrong while processing {0!r}: {1!r}" .format(filepath, e)) + log.err() |