summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md4
-rw-r--r--changes/feature_bounce_problematic_mails4
-rw-r--r--pkg/mx.conf.sample19
-rwxr-xr-xpkg/mx.tac14
-rw-r--r--src/leap/mx/mail_receiver.py140
5 files changed, 174 insertions, 7 deletions
diff --git a/README.md b/README.md
index c467496..03b0ade 100644
--- a/README.md
+++ b/README.md
@@ -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
diff --git a/pkg/mx.tac b/pkg/mx.tac
index c101de9..75d2405 100755
--- a/pkg/mx.tac
+++ b/pkg/mx.tac
@@ -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()