summaryrefslogtreecommitdiff
path: root/src/leap/mail/smtp/smtprelay.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/leap/mail/smtp/smtprelay.py')
-rw-r--r--src/leap/mail/smtp/smtprelay.py246
1 files changed, 246 insertions, 0 deletions
diff --git a/src/leap/mail/smtp/smtprelay.py b/src/leap/mail/smtp/smtprelay.py
new file mode 100644
index 0000000..6479873
--- /dev/null
+++ b/src/leap/mail/smtp/smtprelay.py
@@ -0,0 +1,246 @@
+"""
+LEAP SMTP encrypted relay.
+"""
+
+import re
+import gnupg
+from zope.interface import implements
+from StringIO import StringIO
+from twisted.mail import smtp
+from twisted.internet.protocol import ServerFactory
+from twisted.internet import reactor
+from twisted.internet import defer
+from twisted.application import internet, service
+from twisted.python import log
+from email.Header import Header
+from leap import soledad
+
+
+class SMTPInfoNotAvailable(Exception):
+ pass
+
+
+class SMTPFactory(ServerFactory):
+ """
+ Factory for an SMTP server with encrypted relaying capabilities.
+ """
+
+ def __init__(self, soledad, gpg=None):
+ self._soledad = soledad
+ self._gpg = gpg
+
+ def buildProtocol(self, addr):
+ "Return a protocol suitable for the job."
+ # TODO: use ESMTP here.
+ smtpProtocol = smtp.SMTP(SMTPDelivery(self._soledad, self._gpg))
+ smtpProtocol.factory = self
+ return smtpProtocol
+
+
+class SMTPDelivery(object):
+ """
+ Validate email addresses and handle message delivery.
+ """
+
+ implements(smtp.IMessageDelivery)
+
+ def __init__(self, soledad, gpg=None):
+ self._soledad = soledad
+ if gpg:
+ self._gpg = gpg
+ else:
+ self._gpg = GPGWrapper()
+
+ def receivedHeader(self, helo, origin, recipients):
+ myHostname, clientIP = helo
+ headerValue = "by %s from %s with ESMTP ; %s" % (
+ myHostname, clientIP, smtp.rfc822date())
+ # email.Header.Header used for automatic wrapping of long lines
+ return "Received: %s" % Header(headerValue)
+
+ def validateTo(self, user):
+ """Assert existence of and trust on recipient's GPG public key."""
+ # try to find recipient's public key
+ try:
+ # this will raise an exception if key is not found
+ trust = self._gpg.find_key(user.dest.addrstr)['trust']
+ # if key is not ultimatelly trusted, then the message will not
+ # be encrypted. So, we check for this below
+ #if trust != 'u':
+ # raise smtp.SMTPBadRcpt(user)
+ log.msg("Accepting mail for %s..." % user.dest)
+ return lambda: EncryptedMessage(user, soledad=self._soledad,
+ gpg=self._gpg)
+ except LookupError:
+ # if key was not found, check config to see if will send anyway.
+ if self.encrypted_only:
+ raise smtp.SMTPBadRcpt(user)
+ # TODO: send signal to cli/gui that this user's key was not found?
+ log.msg("Warning: will send an unencrypted message (because "
+ "encrypted_only' is set to False).")
+
+ def validateFrom(self, helo, originAddress):
+ # accept mail from anywhere. To reject an address, raise
+ # smtp.SMTPBadSender here.
+ return originAddress
+
+
+class EncryptedMessage():
+ """
+ Receive plaintext from client, encrypt it and send message to a
+ recipient.
+ """
+ implements(smtp.IMessage)
+
+ SMTP_HOSTNAME = "mail.leap.se"
+ SMTP_PORT = 25
+
+ def __init__(self, user, soledad, gpg=None):
+ self.user = user
+ self._soledad = soledad
+ self.fetchConfig()
+ self.lines = []
+ if gpg:
+ self._gpg = gpg
+ else:
+ self._gpg = GPGWrapper()
+
+ def lineReceived(self, line):
+ """Store email DATA lines as they arrive."""
+ self.lines.append(line)
+
+ def eomReceived(self):
+ """Encrypt and send message."""
+ log.msg("Message data complete.")
+ self.lines.append('') # add a trailing newline
+ self.parseMessage()
+ try:
+ self.encrypt()
+ return self.sendMessage()
+ except LookupError:
+ return None
+
+ def parseMessage(self):
+ """Separate message headers from body."""
+ sep = self.lines.index('')
+ self.headers = self.lines[:sep]
+ self.body = self.lines[sep + 1:]
+
+ def connectionLost(self):
+ log.msg("Connection lost unexpectedly!")
+ log.err()
+ # unexpected loss of connection; don't save
+ self.lines = []
+
+ def sendSuccess(self, r):
+ log.msg(r)
+
+ def sendError(self, e):
+ log.msg(e)
+ log.err()
+
+ def prepareHeader(self):
+ self.headers.insert(1, "From: %s" % self.user.orig.addrstr)
+ self.headers.insert(2, "To: %s" % self.user.dest.addrstr)
+ self.headers.append('')
+
+ def sendMessage(self):
+ self.prepareHeader()
+ msg = '\n'.join(self.headers + [self.cyphertext])
+ d = defer.Deferred()
+ factory = smtp.ESMTPSenderFactory(self.smtp_username,
+ self.smtp_password,
+ self.smtp_username,
+ self.user.dest.addrstr,
+ StringIO(msg),
+ d)
+ # the next call is TSL-powered!
+ reactor.connectTCP(self.SMTP_HOSTNAME, self.SMTP_PORT, factory)
+ d.addCallback(self.sendSuccess)
+ d.addErrback(self.sendError)
+ return d
+
+ def encrypt(self, always_trust=True):
+ # TODO: do not "always trust" here.
+ try:
+ fp = self._gpg.find_key(self.user.dest.addrstr)['fingerprint']
+ log.msg("Encrypting to %s" % fp)
+ self.cyphertext = str(
+ self._gpg.encrypt(
+ '\n'.join(self.body), [fp], always_trust=always_trust))
+ except LookupError:
+ if self.encrypted_only:
+ raise
+ log.msg("Warning: sending unencrypted mail (because "
+ "'encrypted_only' is set to False).")
+
+ # this will be replaced by some other mechanism of obtaining credentials
+ # for SMTP server.
+ def fetchConfig(self):
+ # TODO: Soledad/LEAP bootstrap should store the SMTP info on local db,
+ # so this relay can load it when it needs.
+ if not self._soledad:
+ # TODO: uncomment below exception when integration with Soledad is
+ # smooth.
+ #raise SMTPInfoNotAvailable()
+ # TODO: remove dummy settings below when soledad bootstrap is
+ # working.
+ self.smtp_host = ''
+ self.smtp_port = ''
+ self.smtp_username = ''
+ self.smtp_password = ''
+ self.encrypted_only = True
+ else:
+ self.smtp_config = self._soledad.get_doc('smtp_relay_config')
+ for confname in [
+ 'smtp_host', 'smtp_port', 'smtp_username',
+ 'smtp_password', 'encrypted_only',
+ ]:
+ setattr(self, confname, doc.content[confname])
+
+
+class GPGWrapper():
+ """
+ This is a temporary class for handling GPG requests, and should be
+ replaced by a more general class used throughout the project.
+ """
+
+ GNUPG_HOME = "~/.config/leap/gnupg"
+ GNUPG_BINARY = "/usr/bin/gpg" # TODO: change this based on OS
+
+ def __init__(self, gpghome=GNUPG_HOME, gpgbinary=GNUPG_BINARY):
+ self.gpg = gnupg.GPG(gnupghome=gpghome, gpgbinary=gpgbinary)
+
+ def find_key(self, email):
+ """
+ Find user's key based on their email.
+ """
+ for key in self.gpg.list_keys():
+ for uid in key['uids']:
+ if re.search(email, uid):
+ return key
+ raise LookupError("GnuPG public key for %s not found!" % email)
+
+ def encrypt(self, data, recipient, always_trust=True):
+ # TODO: do not 'always_trust'.
+ return self.gpg.encrypt(data, recipient, always_trust=always_trust)
+
+ def decrypt(self, data):
+ return self.gpg.decrypt(data)
+
+ def import_keys(self, data):
+ return self.gpg.import_keys(data)
+
+
+# service configuration
+port = 25
+user_email = 'user@leap.se' # TODO: replace for real mail from gui/cli
+
+# instantiate soledad for client app storage and sync
+s = soledad.Soledad(user_email)
+factory = SMTPFactory(s)
+
+# enable the use of this service with twistd
+application = service.Application("LEAP SMTP Relay")
+service = internet.TCPServer(port, factory)
+service.setServiceParent(application)