From 49986a9f947f8b02f08e2ac8b3d893d3834691e2 Mon Sep 17 00:00:00 2001 From: drebs Date: Tue, 23 Oct 2012 14:21:19 -0200 Subject: Simple SMTP relay using Twisted API. --- src/leap/email/smtp/smtprelay.py | 126 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 src/leap/email/smtp/smtprelay.py (limited to 'src/leap/email/smtp/smtprelay.py') diff --git a/src/leap/email/smtp/smtprelay.py b/src/leap/email/smtp/smtprelay.py new file mode 100644 index 00000000..10020a54 --- /dev/null +++ b/src/leap/email/smtp/smtprelay.py @@ -0,0 +1,126 @@ +from zope.interface import implements +from twisted.mail import smtp +from twisted.internet.protocol import ServerFactory +from twisted.internet import reactor +from twisted.internet import defer +from email.Header import Header +from StringIO import StringIO + + +class SMTPFactory(ServerFactory): + """ + Factory for an SMTP server with encrypted relaying capabilities. + """ + + def buildProtocol(self, addr): + "Return a protocol suitable for the job." + smtpProtocol = smtp.SMTP(SMTPDelivery()) + smtpProtocol.factory = self + return smtpProtocol + + +class SMTPDelivery(object): + """ + Validate email addresses and handle message delivery. + """ + + implements(smtp.IMessageDelivery) + + def __init__(self): + self.gpgkey = '' + + 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 GPG public key for a recipient.""" + # for now just accept any receipient + print "Accepting mail for %s..." % user.dest + return lambda: EncryptedMessage(user, self.gpgkey) + + 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 + recipients. + """ + implements(smtp.IMessage) + + SMTP_HOSTNAME = "mail.riseup.net" + SMTP_PORT = 25 + + def __init__(self, user, gpgkey): + self.user = user + self.gpgkey = gpgkey + self.getSMTPInfo() + self.lines = [] + + def lineReceived(self, line): + """Store email DATA lines as they arrive.""" + self.lines.append(line) + + def eomReceived(self): + """Encrypt and send message.""" + print "Message data complete." + self.lines.append('') # add a trailing newline + return self.sendMail() + + def connectionLost(self): + print "Connection lost unexpectedly!" + # unexpected loss of connection; don't save + del(self.lines) + + def sendSuccess(self, r): + print r + reactor.stop() + + def sendError(self, e): + print e + reactor.stop() + + def sendMail(self): + self.lines = [self.lines[0]] + \ + ["From: %s" % self.user.orig.addrstr] + \ + ["To: %s" % self.user.dest.addrstr] + \ + self.lines[1:] + msg = '\n'.join(self.lines) + 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) + #reactor.run() + return d + + + # this will be replaced by some other mechanism of obtaining credentials + # for SMTP server. + def getSMTPInfo(self): + f = open('/var/tmp/smtp-info.txt', 'r') + self.smtp_host = f.readline().rstrip() + self.smtp_port = f.readline().rstrip() + self.smtp_username = f.readline().rstrip() + self.smtp_password = f.readline().rstrip() + f.close() + + +# run server +if __name__ == "__main__": + import sys + reactor.listenTCP(25, SMTPFactory()) + reactor.run() -- cgit v1.2.3 From 148f81aa9214eeae6a870912e252c119a4c6472c Mon Sep 17 00:00:00 2001 From: drebs Date: Mon, 29 Oct 2012 11:49:15 -0200 Subject: send encrypted mail --- src/leap/email/smtp/smtprelay.py | 62 +++++++++++++++++++++++++++++----------- 1 file changed, 46 insertions(+), 16 deletions(-) (limited to 'src/leap/email/smtp/smtprelay.py') diff --git a/src/leap/email/smtp/smtprelay.py b/src/leap/email/smtp/smtprelay.py index 10020a54..cbcff43a 100644 --- a/src/leap/email/smtp/smtprelay.py +++ b/src/leap/email/smtp/smtprelay.py @@ -5,6 +5,8 @@ from twisted.internet import reactor from twisted.internet import defer from email.Header import Header from StringIO import StringIO +import gnupg +import re class SMTPFactory(ServerFactory): @@ -26,9 +28,6 @@ class SMTPDelivery(object): implements(smtp.IMessageDelivery) - def __init__(self): - self.gpgkey = '' - def receivedHeader(self, helo, origin, recipients): myHostname, clientIP = helo headerValue = "by %s from %s with ESMTP ; %s" % ( @@ -40,7 +39,7 @@ class SMTPDelivery(object): """Assert existence of GPG public key for a recipient.""" # for now just accept any receipient print "Accepting mail for %s..." % user.dest - return lambda: EncryptedMessage(user, self.gpgkey) + return lambda: EncryptedMessage(user) def validateFrom(self, helo, originAddress): # accept mail from anywhere. To reject an address, raise @@ -50,19 +49,19 @@ class SMTPDelivery(object): class EncryptedMessage(): """ - Receive plaintext from client, encrypt it and send message to - recipients. + Receive plaintext from client, encrypt it and send message to a + recipient. """ implements(smtp.IMessage) SMTP_HOSTNAME = "mail.riseup.net" SMTP_PORT = 25 - def __init__(self, user, gpgkey): + def __init__(self, user): self.user = user - self.gpgkey = gpgkey self.getSMTPInfo() self.lines = [] + self.gpg = GPGWrapper() def lineReceived(self, line): """Store email DATA lines as they arrive.""" @@ -72,6 +71,9 @@ class EncryptedMessage(): """Encrypt and send message.""" print "Message data complete." self.lines.append('') # add a trailing newline + self.received = self.lines[0] + self.lines = self.lines[1:] + self.encrypt() return self.sendMail() def connectionLost(self): @@ -88,11 +90,11 @@ class EncryptedMessage(): reactor.stop() def sendMail(self): - self.lines = [self.lines[0]] + \ - ["From: %s" % self.user.orig.addrstr] + \ - ["To: %s" % self.user.dest.addrstr] + \ - self.lines[1:] - msg = '\n'.join(self.lines) + lines = [self.received] + \ + ["From: %s" % self.user.orig.addrstr] + \ + ["To: %s" % self.user.dest.addrstr] + \ + [self.cyphertext] + msg = '\n'.join(lines) d = defer.Deferred() factory = smtp.ESMTPSenderFactory(self.smtp_username, self.smtp_password, @@ -104,14 +106,16 @@ class EncryptedMessage(): reactor.connectTCP(self.SMTP_HOSTNAME, self.SMTP_PORT, factory) d.addCallback(self.sendSuccess) d.addErrback(self.sendError) - #reactor.run() return d + def encrypt(self): + fp = self.gpg.get_fingerprint(self.user.dest.addrstr) + self.cyphertext = str(self.gpg.encrypt('\n'.join(self.lines), [fp])) # this will be replaced by some other mechanism of obtaining credentials # for SMTP server. def getSMTPInfo(self): - f = open('/var/tmp/smtp-info.txt', 'r') + f = open('/media/smtp-info.txt', 'r') self.smtp_host = f.readline().rstrip() self.smtp_port = f.readline().rstrip() self.smtp_username = f.readline().rstrip() @@ -119,8 +123,34 @@ class EncryptedMessage(): f.close() +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" # this has to be changed based on OS + + def __init__(self): + self.gpg = gnupg.GPG(gnupghome=self.GNUPG_HOME, gpgbinary=self.GNUPG_BINARY) + + def get_fingerprint(self, email): + """ + Find user's fingerprint based on their email. + """ + for key in self.gpg.list_keys(): + for uid in key['uids']: + if re.search(email, uid): + return key['fingerprint'] + + def encrypt(self, data, recipient): + return self.gpg.encrypt(data, recipient) + + + # run server if __name__ == "__main__": import sys - reactor.listenTCP(25, SMTPFactory()) + reactor.listenTCP(2500, SMTPFactory()) reactor.run() -- cgit v1.2.3 From 196ec0e1eefbd50860771de0c8d2b488c30c6506 Mon Sep 17 00:00:00 2001 From: drebs Date: Wed, 31 Oct 2012 11:57:08 -0200 Subject: relay loops forever with twistd --- src/leap/email/smtp/smtprelay.py | 156 --------------------------------------- 1 file changed, 156 deletions(-) delete mode 100644 src/leap/email/smtp/smtprelay.py (limited to 'src/leap/email/smtp/smtprelay.py') diff --git a/src/leap/email/smtp/smtprelay.py b/src/leap/email/smtp/smtprelay.py deleted file mode 100644 index cbcff43a..00000000 --- a/src/leap/email/smtp/smtprelay.py +++ /dev/null @@ -1,156 +0,0 @@ -from zope.interface import implements -from twisted.mail import smtp -from twisted.internet.protocol import ServerFactory -from twisted.internet import reactor -from twisted.internet import defer -from email.Header import Header -from StringIO import StringIO -import gnupg -import re - - -class SMTPFactory(ServerFactory): - """ - Factory for an SMTP server with encrypted relaying capabilities. - """ - - def buildProtocol(self, addr): - "Return a protocol suitable for the job." - smtpProtocol = smtp.SMTP(SMTPDelivery()) - smtpProtocol.factory = self - return smtpProtocol - - -class SMTPDelivery(object): - """ - Validate email addresses and handle message delivery. - """ - - implements(smtp.IMessageDelivery) - - 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 GPG public key for a recipient.""" - # for now just accept any receipient - print "Accepting mail for %s..." % user.dest - return lambda: EncryptedMessage(user) - - 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.riseup.net" - SMTP_PORT = 25 - - def __init__(self, user): - self.user = user - self.getSMTPInfo() - self.lines = [] - 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.""" - print "Message data complete." - self.lines.append('') # add a trailing newline - self.received = self.lines[0] - self.lines = self.lines[1:] - self.encrypt() - return self.sendMail() - - def connectionLost(self): - print "Connection lost unexpectedly!" - # unexpected loss of connection; don't save - del(self.lines) - - def sendSuccess(self, r): - print r - reactor.stop() - - def sendError(self, e): - print e - reactor.stop() - - def sendMail(self): - lines = [self.received] + \ - ["From: %s" % self.user.orig.addrstr] + \ - ["To: %s" % self.user.dest.addrstr] + \ - [self.cyphertext] - msg = '\n'.join(lines) - 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): - fp = self.gpg.get_fingerprint(self.user.dest.addrstr) - self.cyphertext = str(self.gpg.encrypt('\n'.join(self.lines), [fp])) - - # this will be replaced by some other mechanism of obtaining credentials - # for SMTP server. - def getSMTPInfo(self): - f = open('/media/smtp-info.txt', 'r') - self.smtp_host = f.readline().rstrip() - self.smtp_port = f.readline().rstrip() - self.smtp_username = f.readline().rstrip() - self.smtp_password = f.readline().rstrip() - f.close() - - -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" # this has to be changed based on OS - - def __init__(self): - self.gpg = gnupg.GPG(gnupghome=self.GNUPG_HOME, gpgbinary=self.GNUPG_BINARY) - - def get_fingerprint(self, email): - """ - Find user's fingerprint based on their email. - """ - for key in self.gpg.list_keys(): - for uid in key['uids']: - if re.search(email, uid): - return key['fingerprint'] - - def encrypt(self, data, recipient): - return self.gpg.encrypt(data, recipient) - - - -# run server -if __name__ == "__main__": - import sys - reactor.listenTCP(2500, SMTPFactory()) - reactor.run() -- cgit v1.2.3 From c2af0a21cb1263ec354708b4623eca154a19d102 Mon Sep 17 00:00:00 2001 From: drebs Date: Wed, 23 Jan 2013 17:19:26 -0200 Subject: Add test for message encryption. --- src/leap/email/smtp/smtprelay.py | 203 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 203 insertions(+) create mode 100644 src/leap/email/smtp/smtprelay.py (limited to 'src/leap/email/smtp/smtprelay.py') diff --git a/src/leap/email/smtp/smtprelay.py b/src/leap/email/smtp/smtprelay.py new file mode 100644 index 00000000..f44aeb6f --- /dev/null +++ b/src/leap/email/smtp/smtprelay.py @@ -0,0 +1,203 @@ +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 + + +class SMTPFactory(ServerFactory): + """ + Factory for an SMTP server with encrypted relaying capabilities. + """ + + def __init__(self, gpg=None): + self._gpg = gpg + + def buildProtocol(self, addr): + "Return a protocol suitable for the job." + # TODO: use ESMTP here. + smtpProtocol = smtp.SMTP(SMTPDelivery(self._gpg)) + smtpProtocol.factory = self + return smtpProtocol + + +class SMTPDelivery(object): + """ + Validate email addresses and handle message delivery. + """ + + implements(smtp.IMessageDelivery) + + def __init__(self, gpg=None): + 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, gpg=self._gpg) + except LookupError: + raise smtp.SMTPBadRcpt(user) + + 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.riseup.net" + SMTP_PORT = 25 + + def __init__(self, user, gpg=None): + self.user = user + self.getSMTPInfo() + 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. + 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)) + + # this will be replaced by some other mechanism of obtaining credentials + # for SMTP server. + def getSMTPInfo(self): + f = open('/media/smtp-info.txt', 'r') + self.smtp_host = f.readline().rstrip() + self.smtp_port = f.readline().rstrip() + self.smtp_username = f.readline().rstrip() + self.smtp_password = f.readline().rstrip() + f.close() + + +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" # this has to be changed 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 +factory = SMTPFactory() + +# these enable the use of this service with twistd +application = service.Application("LEAP SMTP Relay") +service = internet.TCPServer(port, factory) +service.setServiceParent(application) -- cgit v1.2.3