Skip to content

Commit

Permalink
First working release
Browse files Browse the repository at this point in the history
  • Loading branch information
SimoneLazzaris committed Mar 29, 2021
1 parent 9203dd1 commit 7de900f
Show file tree
Hide file tree
Showing 6 changed files with 365 additions and 0 deletions.
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
__pycache__
bin
data
immudb
lib
lib64
pyvenv.cfg
71 changes: 71 additions & 0 deletions db.py
Original file line number Diff line number Diff line change
@@ -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
16 changes: 16 additions & 0 deletions main.py
Original file line number Diff line number Diff line change
@@ -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()
193 changes: 193 additions & 0 deletions pop3.py
Original file line number Diff line number Diff line change
@@ -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
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
immudb-py==0.9.2
Twisted==21.2.0
76 changes: 76 additions & 0 deletions smtp.py
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 7de900f

Please sign in to comment.