From 36a04fd7d9ebf5f0f5b223b8c0f1dca20a61bea7 Mon Sep 17 00:00:00 2001 From: Peter Surda Date: Tue, 19 Jul 2016 13:57:54 +0200 Subject: [PATCH] Add SMTP server - you can now use SMTP to send messages - uses bmaddr.lan domain - runs on 127.0.0.8425 if you set "smtpd" to True - mandatory authentication with smtpdusername and smtpdpassword --- src/bitmessagemain.py | 6 ++ src/class_smtpServer.py | 195 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 201 insertions(+) create mode 100644 src/class_smtpServer.py diff --git a/src/bitmessagemain.py b/src/bitmessagemain.py index 4818f97b..af024e92 100755 --- a/src/bitmessagemain.py +++ b/src/bitmessagemain.py @@ -39,6 +39,7 @@ from class_singleListener import singleListener from class_singleWorker import singleWorker from class_addressGenerator import addressGenerator from class_smtpDeliver import smtpDeliver +from class_smtpServer import smtpServer from debug import logger # Helper Functions @@ -191,6 +192,11 @@ class Main: smtpDeliveryThread = smtpDeliver() smtpDeliveryThread.start() + # SMTP daemon thread + if daemon and shared.safeConfigGetBoolean("bitmessagesettings", "smtpd"): + smtpServerThread = smtpServer() + smtpServerThread.start() + # Start the thread that calculates POWs objectProcessorThread = objectProcessor() objectProcessorThread.daemon = False # DON'T close the main program even the thread remains. This thread checks the shutdown variable after processing each object. diff --git a/src/class_smtpServer.py b/src/class_smtpServer.py new file mode 100644 index 00000000..2d8333ac --- /dev/null +++ b/src/class_smtpServer.py @@ -0,0 +1,195 @@ +import base64 +import email +from email.parser import Parser +from email.header import decode_header +import re +import signal +import smtpd +import threading +import time + +from addresses import decodeAddress +from debug import logger +from helper_sql import sqlExecute +from helper_threading import StoppableThread +from pyelliptic.openssl import OpenSSL +import shared + +SMTPDOMAIN = "bmaddr.lan" +LISTENPORT = 8425 + +class smtpServerChannel(smtpd.SMTPChannel): + def smtp_EHLO(self, arg): + if not arg: + self.push('501 Syntax: HELO hostname') + return + self.push('250-PyBitmessage %s' % shared.softwareVersion) + self.push('250 AUTH PLAIN') + + def smtp_AUTH(self, arg): + if not arg or arg[0:5] not in ["PLAIN"]: + self.push('501 Syntax: AUTH PLAIN') + return + authstring = arg[6:] + try: + decoded = base64.b64decode(authstring) + correctauth = "\x00" + shared.safeConfigGet("bitmessagesettings", "smtpdusername", "") + \ + "\x00" + shared.safeConfigGet("bitmessagesettings", "smtpdpassword", "") + logger.debug("authstring: %s / %s", correctauth, decoded) + if correctauth == decoded: + self.auth = True + self.push('235 2.7.0 Authentication successful') + else: + raise Error("Auth fail") + except: + self.push('501 Authentication fail') + + def smtp_DATA(self, arg): + if not hasattr(self, "auth") or not self.auth: + self.push ("530 Authentication required") + return + smtpd.SMTPChannel.smtp_DATA(self, arg) + + +class smtpServerPyBitmessage(smtpd.SMTPServer): + def handle_accept(self): + pair = self.accept() + if pair is not None: + conn, addr = pair +# print >> DEBUGSTREAM, 'Incoming connection from %s' % repr(addr) + self.channel = smtpServerChannel(self, conn, addr) + + def send(self, fromAddress, toAddress, subject, message): + status, addressVersionNumber, streamNumber, ripe = decodeAddress(toAddress) + ackdata = OpenSSL.rand(32) + t = () + sqlExecute( + '''INSERT INTO sent VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)''', + '', + toAddress, + ripe, + fromAddress, + subject, + message, + ackdata, + int(time.time()), # sentTime (this will never change) + int(time.time()), # lastActionTime + 0, # sleepTill time. This will get set when the POW gets done. + 'msgqueued', + 0, # retryNumber + 'sent', # folder + 2, # encodingtype + min(shared.config.getint('bitmessagesettings', 'ttl'), 86400 * 2) # not necessary to have a TTL higher than 2 days + ) + + shared.workerQueue.put(('sendmessage', toAddress)) + + def decode_header(self, hdr): + ret = [] + for h in decode_header(self.msg_headers[hdr]): + if h[1]: + ret.append(unicode(h[0], h[1])) + else: + ret.append(h[0].decode("utf-8", errors='replace')) + + return ret + + + def process_message(self, peer, mailfrom, rcpttos, data): +# print 'Receiving message from:', peer + p = re.compile(".*<([^>]+)>") + if not hasattr(self.channel, "auth") or not self.channel.auth: + logger.error("Missing or invalid auth") + return + try: + self.msg_headers = Parser().parsestr(data) + except: + logger.error("Invalid headers") + return + + try: + sender, domain = p.sub(r'\1', mailfrom).split("@") + if domain != SMTPDOMAIN: + raise Exception("Bad domain %s", domain) + if sender not in shared.config.sections(): + raise Exception("Nonexisting user %s", sender) + except Exception as err: + logger.debug("Bad envelope from %s: %s", mailfrom, repr(err)) + msg_from = self.decode_header("from") + try: + msg_from = p.sub(r'\1', self.decode_header("from")[0]) + sender, domain = msg_from.split("@") + if domain != SMTPDOMAIN: + raise Exception("Bad domain %s", domain) + if sender not in shared.config.sections(): + raise Exception("Nonexisting user %s", sender) + except Exception as err: + logger.error("Bad headers from %s: %s", msg_from, repr(err)) + return + + try: + msg_subject = self.decode_header('subject')[0] + except: + msg_subject = "Subject missing..." + + msg_tmp = email.message_from_string(data) + body = u'' + for part in msg_tmp.walk(): + if part and part.get_content_type() == "text/plain": + body += part.get_payload(decode=1).decode(part.get_content_charset('utf-8'), errors='replace') + + for to in rcpttos: + try: + rcpt, domain = p.sub(r'\1', to).split("@") + if domain != SMTPDOMAIN: + raise Exception("Bad domain %s", domain) + logger.debug("Sending %s to %s about %s", sender, rcpt, msg_subject) + self.send(sender, rcpt, msg_subject, body) + logger.info("Relayed %s to %s", sender, rcpt) + except Exception as err: + logger.error( "Bad to %s: %s", to, repr(err)) + continue + return + +class smtpServer(threading.Thread, StoppableThread): + def __init__(self, parent=None): + threading.Thread.__init__(self, name="smtpServerThread") + self.initStop() + self.server = smtpServerPyBitmessage(('127.0.0.1', LISTENPORT), None) + + def stopThread(self): + super(smtpServer, self).stopThread() + self.server.close() + return + s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +# for ip in ('127.0.0.1', shared.config.get('bitmessagesettings', 'onionbindip')): + for ip in ('127.0.0.1'): + try: + s.connect((ip, LISTENPORT)) + s.shutdown(socket.SHUT_RDWR) + s.close() + break + except: + pass + + def run(self): + asyncore.loop(1) + +def signals(signal, frame): + print "Got signal, terminating" + for thread in threading.enumerate(): + if thread.isAlive() and isinstance(thread, StoppableThread): + thread.stopThread() + +def runServer(): + print "Running SMTPd thread" + smtpThread = smtpServer() + smtpThread.start() + signal.signal(signal.SIGINT, signals) + signal.signal(signal.SIGTERM, signals) + print "Processing" + smtpThread.join() + print "The end" + +if __name__ == "__main__": + runServer()