Has anyone come across a local dev/e2e-centric SMT...
# random
w
Has anyone come across a local dev/e2e-centric SMTP server? I’m trying to create a full sign in/registration e2e workflow - and the missing piece is getting the forgot password link. I can create a custom mailbox, or outgoing mail thing on my server - but that’s already missing the point.
Something like Papercut, but for Mac and/or Linux. It’s not hard to write something like this, but 🤷 I’m also a bit lazy
n
👍 1
w
I looked at mail hog briefly, but I thought that’s more of an outgoing mail server? Not incoming?
I’ve also briefly considered using the gnu mail/mailx utilities built-in, but I just don’t know enough about them yet
Errr… Unix?
Python also has a deprecated smtpd which could be something
n
MailHog is a local SMTP server that instead of sending your application's emails to the actual recipient, it stores them in memory and makes them available in a Web UI inbox. So I guess it could be either outgoing or incoming depending on how you look at it? :)
w
Oh, yeah, then that could definitely be good. Thanks! I’ll look closer
b
We use mailhog for the email (and pseudo-SMS) parts of our e2e tests, and it works fine. The web UI is simple enough to easily clicking links etc in playwright
w
Ah, great! I assume it has an API for that. I didn’t realize that python’s smtpd was so trivial to get started with.
python3 -m smtpd -c DebuggingServer -n localhost:2525
And that runs an SMTP server that prints to stdout. Pretty trivial to either pipe that to a shared file, or launch a FastAPI process that wraps the incoming messages into an API. Thanks for the help guys!
Copy code
from contextlib import asynccontextmanager
from threading import Lock
Copy code
from fastapi import FastAPI
from aiosmtpd.controller import Controller
Copy code
# Arguably the SMTP server should be either an asyncio task, or a separate process - but this is just for low-load e2e testing
# TODO: If the e2e tests run via multi-process, maybe it's worth making this a queue and being a bit smarter about it
lock = Lock()
mailbox: dict = {}
Copy code
@asynccontextmanager
async def lifespan(app: FastAPI):
    handler = CustomHandler()
    controller = Controller(handler, hostname="localhost", port=2525)
    controller.start()
    yield
    controller.stop()
Copy code
class CustomHandler:
    async def handle_DATA(self, server, session, envelope):
        mail_from = envelope.mail_from
        rcpt_tos = envelope.rcpt_tos
        data = envelope.content  # type: bytes
        print(f"{envelope}, {mail_from}, {rcpt_tos}, {data}")
        with lock:
            mailbox[mail_from] = {
                "from": mail_from,
                "to": rcpt_tos,
                "body": data.decode("utf-8"),
            }
        return "250 OK"
Copy code
app = FastAPI(lifespan=lifespan)
Copy code
@app.get("/")
async def root():
    with lock:
        return mailbox