-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
9203dd1
commit 7de900f
Showing
6 changed files
with
365 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
__pycache__ | ||
bin | ||
data | ||
immudb | ||
lib | ||
lib64 | ||
pyvenv.cfg |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
immudb-py==0.9.2 | ||
Twisted==21.2.0 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |