From 7de900fb1cdd7ef2a35ed476e0fd23ff5bea28cb Mon Sep 17 00:00:00 2001 From: Simone Lazzaris Date: Mon, 29 Mar 2021 17:40:28 +0200 Subject: [PATCH] First working release --- .gitignore | 7 ++ db.py | 71 +++++++++++++++++ main.py | 16 ++++ pop3.py | 193 +++++++++++++++++++++++++++++++++++++++++++++++ requirements.txt | 2 + smtp.py | 76 +++++++++++++++++++ 6 files changed, 365 insertions(+) create mode 100644 .gitignore create mode 100644 db.py create mode 100644 main.py create mode 100644 pop3.py create mode 100644 requirements.txt create mode 100644 smtp.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..3c73035 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +__pycache__ +bin +data +immudb +lib +lib64 +pyvenv.cfg diff --git a/db.py b/db.py new file mode 100644 index 0000000..d8a9f35 --- /dev/null +++ b/db.py @@ -0,0 +1,71 @@ +import immudb.client +import time,string,random,re + +IMMUDB_USER="immudb" +IMMUDB_PASS="immudb" +IMMUDB_HOST="localhost" +IMMUDB_PORT="3322" + +def rndstring(size): + chars=string.ascii_uppercase + string.digits + return ''.join(random.choice(chars) for _ in range(size)) + + +class db: + def __init__(self): + self.cli=immudb.client.ImmudbClient("{}:{}".format(IMMUDB_HOST,IMMUDB_PORT)) + self.cli.login(IMMUDB_USER,IMMUDB_PASS) + + def validUser(self, username): + k="USER:{}".format(username).encode('utf8') + try: + u=self.cli.safeGet(k) + return u.verified + except: + return False + + def validLogin(self, username, password): + k="USER:{}".format(username).encode('utf8') + try: + u=self.cli.safeGet(k) + return u.verified and u.value==password + except: + return False + + def storeEmail(self, username, message): + uniq="{}.{}".format(time.time(), rndstring(8)) + k="MAIL:{}:{}:S{}".format(username, uniq,len(message)) + self.cli.safeSet(k.encode('utf8'), message) + + def listEmail(self, username): + prefix="MAIL:{}:".format(username).encode('utf8') + ret=[] + prev=None + rx=re.compile(r"MAIL:.*:(.+):S([0-9]+)") + while True: + sc=self.cli.scan(prev, prefix, False, 10) + if len(sc)==0: + break + for i in sc.keys(): + prev=i + m=rx.match(i.decode('utf8')) + if m!=None: + ret.append((m.group(1),int(m.group(2)))) + return ret + + def getEmail(self, username, idx): + prefix="MAIL:{}:".format(username).encode('utf8') + ret=[] + prev=None + rx=re.compile(r"MAIL:.*:(.+):S([0-9]+)") + curr=0 + while True: + sc=self.cli.scan(prev, prefix, False, 10) + if len(sc)==0: + break + for i in sc.keys(): + curr+=1 + prev=i + if curr==idx: + return sc[i] + return None diff --git a/main.py b/main.py new file mode 100644 index 0000000..9991bbd --- /dev/null +++ b/main.py @@ -0,0 +1,16 @@ +from twisted.internet import protocol, reactor, endpoints +import logging +import smtp +import pop3 +import db + +logging.basicConfig(level=logging.DEBUG) +immudb=db.db() + +smtp_endpoint=endpoints.serverFromString(reactor,"tcp:7125") +pop3_endpoint=endpoints.serverFromString(reactor,"tcp:7110") + +smtp_endpoint.listen(smtp.SMTPFactory(immudb)) +pop3_endpoint.listen(pop3.POP3Factory(immudb)) + +reactor.run() diff --git a/pop3.py b/pop3.py new file mode 100644 index 0000000..979b999 --- /dev/null +++ b/pop3.py @@ -0,0 +1,193 @@ +from twisted.internet import protocol, reactor, endpoints +from twisted.protocols.basic import LineReceiver +import logging +logging.basicConfig(level=logging.DEBUG) + +class POP3Protocol(LineReceiver): + def __init__(self): + self.user=None + self.auth=False + + def connectionMade(self): + logging.info("Incoming pop3 connection") + greeting="+OK mailsafe ready." + self.sendLine(greeting.encode('utf8')) + + def lineReceived(self, line): + parms=line.split() + if len(parms)==0: + logging.warn("Unknown command") + self.sendLine("-ERR Unknown command.".encode('utf8')) + return + cmd=parms[0].decode('utf8').upper() + if self.auth: + prefix="cmd_" + else: + prefix="auth_" + f=getattr(self, prefix+cmd, None) + if f==None: + logging.warn("Unknown command '%s'",cmd) + self.sendLine("-ERR Unknown command.".encode('utf8')) + return + logging.info("Received command '%s'",cmd) + f(line) + + def auth_CAPA(self, line): + self.sendLine("+OK".encode('utf8')) + for c in ["TOP", "UIDL", "USER", "IMPLEMENTATION mailsafe"]: + self.sendLine(c.encode('utf8')) + self.sendLine(".".encode('utf8')) + + def auth_QUIT(self, line): + greeting="+OK See you later alligator".encode('utf8') + self.sendLine(greeting) + self.transport.loseConnection() + + def auth_USER(self, line): + try: + usr=line.split()[1].decode('utf8') + self.user=usr + greeting="+OK Now enter your password".encode('utf8') + self.sendLine(greeting) + except: + err="-ERR I don't understand".encode('utf8') + self.sendLine(err) + + def auth_PASS(self, line): + if self.auth==None: + err="-ERR USER first.".encode('utf8') + self.sendLine(err) + return + try: + pwd=line.split()[1] + if self.db.validLogin(self.user, pwd): + self.auth=True + greeting="+OK You are now logged in".encode('utf8') + else: + greeting="-ERR user {} sus".format(self.user).encode('utf8') + self.sendLine(greeting) + except: + err="-ERR I don't understand".encode('utf8') + self.sendLine(err) + + + def cmd_QUIT(self, line): + greeting="+OK See you later alligator".encode('utf8') + self.sendLine(greeting) + self.transport.loseConnection() + + def cmd_NOOP(self, line): + greeting="+OK".encode('utf8') + self.sendLine(greeting) + + def cmd_RSET(self, line): + greeting="+OK".encode('utf8') + self.sendLine(greeting) + + def cmd_DELE(self, line): + msg="-ERR Messages are permanent".encode('utf8') + self.sendLine(msg) + + def cmd_STAT(self, line): + msglist=self.db.listEmail(self.user) + ttsize=0 + for i in msglist: + ttsize+=i[1] + statusLine="+OK {} {}".format(len(msglist),ttsize).encode('utf8') + self.sendLine(statusLine) + + def cmd_RETR(self, line): + try: + msgnum=int(line.split()[1]) + except: + self.sendLine("-ERR wrong".encode('utf8')) + return + msg=self.db.getEmail(self.user,msgnum) + if msg==None: + self.sendLine("-ERR not found".encode('utf8')) + else: + self.sendLine("+OK {} octets".format(len(msg)).encode('utf8')) + for l in msg.split(b"\r\n"): + self.sendLine(l) + self.sendLine(".".encode('utf8')) + + def cmd_TOP(self, line): + try: + msgnum=int(line.split()[1]) + lines=int(line.split()[2]) + except: + self.sendLine("-ERR wrong".encode('utf8')) + return + msg=self.db.getEmail(self.user,msgnum) + if msg==None: + self.sendLine("-ERR not found".encode('utf8')) + else: + self.sendLine("+OK {} octets".format(len(msg)).encode('utf8')) + for i,l in enumerate(msg.split(b"\r\n")): + if i>=lines: + break + self.sendLine(l) + self.sendLine(".".encode('utf8')) + + def cmd_LIST(self, line): + parms=line.split() + if len(parms)>1: + try: + msgnum=int(parms[1])-1 + except: + self.sendLine("-ERR wrong".encode('utf8')) + return + else: + msgnum=None + msglist=self.db.listEmail(self.user) + ttsize=0 + for i in msglist: + ttsize+=i[1] + if msgnum==None: + statusLine="+OK {} messages ({} octets)".format(len(msglist),ttsize).encode('utf8') + self.sendLine(statusLine) + i=0 + for m in msglist: + i=i+1 + self.sendLine("{} {}".format(i,m[1]).encode('utf8')) + self.sendLine(".".encode('utf8')) + elif len(msglist)>msgnum and msgnum>0: + self.sendLine("+OK {} {}".format(msgnum+1,msglist[msgnum][1]).encode('utf8')) + else: + self.sendLine("-ERR You what?".encode('utf8')) + + def cmd_UIDL(self, line): + parms=line.split() + if len(parms)>1: + try: + msgnum=int(parms[1])-1 + except: + self.sendLine("-ERR wrong".encode('utf8')) + return + else: + msgnum=None + msglist=self.db.listEmail(self.user) + if msgnum==None: + self.sendLine("+OK".encode('utf8')) + i=0 + for m in msglist: + i=i+1 + self.sendLine("{} {}".format(i,m[0]).encode('utf8')) + self.sendLine(".".encode('utf8')) + elif len(msglist)>msgnum and msgnum>0: + self.sendLine("+OK {} {}".format(msgnum+1,msglist[msgnum][0]).encode('utf8')) + else: + self.sendLine("-ERR You what?".encode('utf8')) + + +class POP3Factory(protocol.ServerFactory): + protocol=POP3Protocol + + def __init__(self, db, *a, **kw): + protocol.ServerFactory.__init__(self, *a, **kw) + self.db=db + + def buildProtocol(self, addr): + p = protocol.ServerFactory.buildProtocol(self, addr) + p.db = self.db + return p diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..d238af3 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +immudb-py==0.9.2 +Twisted==21.2.0 diff --git a/smtp.py b/smtp.py new file mode 100644 index 0000000..b54c3c0 --- /dev/null +++ b/smtp.py @@ -0,0 +1,76 @@ +from twisted.internet import protocol, reactor, endpoints, defer +from twisted.mail import smtp +from zope.interface import implementer +from twisted.internet import defer +import datetime +import time +from email import utils +from email.header import Header +import logging +import db + +@implementer(smtp.IMessageDelivery) +class ImmudbMessageDelivery: + def __init__(self, db): + self.db=db + + def receivedHeader(self, helo, origin, rcpt): + recv="from {hello0} [{hello1}] message to {rcpts}; {date}" + rcpts=",".join([str(r.dest) for r in rcpt]) + date = utils.format_datetime(datetime.datetime.now()) + hh=Header("Received") + hh.append(recv.format(hello0=helo[0].decode('ascii'), + hello1=helo[1].decode('ascii'), + rcpts=rcpts, date=date).encode('ascii')) + return hh.encode().encode('ascii') + + def validateFrom(self, helo, origin): + logging.info("validating from %s",origin) + # All addresses are accepted + return origin + + def validateTo(self, user): + # Only messages directed to configured users are accepted + logging.info("validating to %s",user) + if self.db.validUser(user.dest): + logging.info("ok") + return lambda: ImmudbMessage(self.db, user.dest) + raise smtp.SMTPBadRcpt(user) + +@implementer(smtp.IMessage) +class ImmudbMessage: + def __init__(self, db, rcpt): + self.msg = b'' + self.db=db + self.rcpt=str(rcpt) + + def lineReceived(self, line): + self.msg+=line+b"\r\n" + + def eomReceived(self): + logging.info("New message received for %s",self.rcpt) + self.db.storeEmail(self.rcpt, self.msg) + self.lines = None + return defer.succeed(None) + + def connectionLost(self): + # There was an error, throw away the stored lines + self.lines = None + + +class SMTPProtocol(smtp.ESMTP): + def connectionMade(self): + logging.info("smtp connection") + smtp.ESMTP.connectionMade(self) + +class SMTPFactory(protocol.ServerFactory): + protocol=SMTPProtocol + domain="lazzaris.net" + def __init__(self, db, *a, **kw): + smtp.SMTPFactory.__init__(self, *a, **kw) + self.delivery=ImmudbMessageDelivery(db) + + def buildProtocol(self, addr): + p = smtp.SMTPFactory.buildProtocol(self, addr) + p.delivery = self.delivery + return p