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()  | 
