aiosmtpd
An asyncio-based SMTP and LMTP server implementation for Python, built to mock mail servers in tests and run lightweight custom relays.
Repository Health
Technical Analysis
aiosmtpd reimplements Python’s deprecated stdlib smtpd.py on top of asyncio, giving developers a modern, coroutine-friendly SMTP and LMTP server they can embed directly in an application or a test suite. At its core is a Controller class that spins up an event loop (or attaches to a running one) and a SMTP protocol class that implements the RFC 5321/6152/3207 command set, including STARTTLS, AUTH, and pluggable size limits, alongside pipelining and SMTPUTF8 support.
Rather than baking in a single delivery mechanism, aiosmtpd separates protocol handling from message processing through a Handler interface: implementations receive hook callbacks (handle_RCPT, handle_DATA, handle_AUTH, etc.) and decide what happens to a message, whether that’s queuing to a Mailbox, proxying to a real MTA, or simply asserting on the envelope in a test. This makes it the standard choice in the Python ecosystem for verifying that application code sends the mail it claims to, without touching a real mail server, while remaining capable enough to run a small production-grade relay or LMTP endpoint.
What You Get
- A
Controllerclass that manages the asyncio event loop and socket lifecycle for you — start/stop a server in a background thread with one call - A full
SMTPprotocol implementation covering HELO/EHLO, MAIL/RCPT/DATA, STARTTLS, AUTH (PLAIN/LOGIN), pipelining, and SMTPUTF8 - An LMTP server variant (
aiosmtpd.lmtp.LMTP) for talking to local mail delivery agents - Built-in
Handlerimplementations (Debugging,Mailbox,AsyncMessage) plus a documented hook protocol for writing custom handlers - A
main.py/aiosmtpdconsole-script CLI for running a standalone server from the command line - PROXY protocol (v1/v2) support for accepting connections behind a TCP load balancer
Common Use Cases
- Spinning up a throwaway SMTP server inside
pytest/unittestsuites to assert on the exact envelope and message an application sends - Building a custom mail relay or filtering MTA that inspects, rewrites, or rejects messages via a custom
Handler - Running a local development mail catcher so outgoing mail from an app never reaches real inboxes
- Implementing an LMTP endpoint for local mail delivery pipelines
- Prototyping SMTP authentication or STARTTLS flows without standing up Postfix/Exim
Under The Hood
Architecture
aiosmtpd is organized as a thin, well-separated stack: controller.py owns process/event-loop lifecycle (starting the loop in a background thread, binding TCP/Unix sockets, waiting for readiness with a configurable timeout), smtp.py implements the actual asyncio.StreamReaderProtocol subclass that drives the SMTP state machine command-by-command (smtp_HELO, smtp_MAIL, smtp_DATA, etc.), and handlers.py defines the Handler hook contract plus several ready-made handlers (Debugging, Mailbox, AsyncMessage). lmtp.py extends the same protocol class for LMTP with minimal overrides, and proxy_protocol.py adds an optional decoding layer for PROXY-protocol-wrapped connections in front of a load balancer. This separation means swapping in a custom Handler changes only message disposition, never protocol handling, and swapping Controller subclasses (UnixSocketController, UnthreadedController) changes only how the loop is hosted.
Tech Stack
The library targets CPython and PyPy 3.9+ and depends on very little beyond the standard library — atpublic for explicit __all__ management via a @public decorator, and attrs for a couple of structured value types. The test suite pulls in pytest, pytest-cov, pytest-mock, coverage, and trustme (for generating throwaway TLS certificates to exercise STARTTLS/AUTH paths). Packaging uses classic setuptools/setup.cfg, and the project is exercised across Linux, Windows, MacOS, and Cygwin in CI via tox, with pytype for static type checking and flake8/bandit for style and security linting.
Code Quality
The project enforces 100% branch coverage (fail_under = 100 in its coverage config) across an extensive aiosmtpd/tests/ suite (test_smtp.py, test_smtps.py, test_starttls.py, test_lmtp.py, test_proxyprotocol.py, test_handlers.py, and more), run via tox across every supported interpreter combination with coverage, diff-coverage, and profiling variants. Code carries type hints throughout (py.typed marker present) and is checked with pytype; style is enforced with flake8/flake8-bugbear, and bandit runs as a security linter in CI. Error handling is explicit and deliberate — protocol-level failures raise typed exceptions (TLSSetupException) or degrade to documented SMTP error responses rather than being silently swallowed.
What Makes It Unique
aiosmtpd’s distinguishing choice is treating the SMTP/LMTP protocol implementation and message handling as fully decoupled concerns behind a small, well-documented Handler hook API, which is what makes it equally suitable as a production-grade relay component and as a disposable test fixture — a duality most SMTP testing tools don’t attempt. Its PROXY protocol support and explicit IPv6-preferring Controller design also reflect attention to real deployment topologies (load balancers, containers) rather than only the toy-server case, and its adoption of asyncio as a first-class rewrite of the deprecated stdlib smtpd module keeps it aligned with modern Python concurrency instead of the retired asyncore/asynchat APIs.