dsmtpd is a small tool to help the developer without an smtp server
Python Support: Python 3.10, 3.11, 3.12, 3.13, 3.14
$ dsmtpd -p 1025 -i 127.0.0.1 2013-01-13 14:00:07,346 INFO: Starting SMTP server at 127.0.0.1:1025
For the installation, we recommend to use a virtualenv, it's the easy way if you want to discover this package:
virtualenv ~/.envs/dsmtpd source ~/.envs/dsmtpd/bin/activate pip install dsmtpd
-p PORT, --port PORT- Specify the port to listen on. Default: 1025
-i INTERFACE, --interface INTERFACE- Specify the network interface to bind to. Default: 127.0.0.1 (loopback)
-d DIRECTORY, --directory DIRECTORY- Specify a Maildir directory to save incoming emails. Default: current directory
-s SIZE, --max-size SIZE- Maximum message size in bytes. Use 0 for no limit. Default: 33554432 (32 MiB)
--disable-smtputf8- Disable SMTPUTF8 extension for legacy SMTP client compatibility. Default: enabled
--version- Show program version and exit
-h, --help- Show help message and exit
Start server with default settings (port 1025, localhost):
dsmtpd
Start server on custom port:
dsmtpd -p 2525
Bind to all interfaces:
dsmtpd -i 0.0.0.0 -p 25
Save emails to specific Maildir:
dsmtpd -d /path/to/maildir
Limit message size to 10 MB:
dsmtpd --max-size 10485760
Disable UTF-8 support for legacy clients:
dsmtpd --disable-smtputf8
Send a test email with swaks:
swaks --from sender@example.com --to recipient@example.com --server localhost --port 1025
SMTPUTF8 Support
dsmtpd has built-in support for SMTPUTF8 (RFC 6531), which allows email addresses and content to contain UTF-8 characters. This feature is enabled by default and requires no configuration.
SMTPUTF8 enables:
- Email addresses with international characters (e.g.,
用户@例え.jp) - UTF-8 encoded message headers and body content
- Full Unicode support in SMTP transactions
Example usage with UTF-8 email addresses:
swaks --from user@example.com --to 用户@例え.jp --server localhost --port 1025
This functionality is provided by the underlying aiosmtpd library and works transparently with all standard SMTP clients that support the SMTPUTF8 extension.
Disabling SMTPUTF8
If you need to test compatibility with legacy SMTP clients or reproduce encoding issues, you can disable SMTPUTF8:
dsmtpd --disable-smtputf8
When disabled, the server will not advertise SMTPUTF8 support and will only accept ASCII email addresses and content.
dsmtpd uses specific exit codes to indicate the result of its execution.
| Code | Meaning | Example |
|---|---|---|
| 0 | Success | Normal shutdown (e.g. user pressed
Ctrl+C) or clean termination. |
| 2 | Invalid Maildir directory | The given path exists but does not contain
the required subfolders: tmp, new,
and cur. |
Clone the repository:
git clone git://github.com/matrixise/dsmtpd.git cd dsmtpd
The project includes a Makefile to simplify development tasks. It automatically manages a virtual environment and dependencies using Python from asdf or mise.
Quick Start
Set up your development environment:
make install-dev
This creates a .venv virtual environment and installs all development dependencies.
Available Make Targets
Development Workflow:
make install-dev- Set up development environment (creates venv and installs dependencies)make test- Run tests with pytest (automatically installs dependencies if needed)make lint- Check code quality with ruff lintermake lint-fix- Auto-fix linting issues and format code with ruffmake format- Format code with ruff formatmake typecheck- Run mypy type checking
Build and Release:
make build- Build distribution packagesmake check-dist- Verify distribution package integrity
Cleanup:
make clean- Remove all build artifacts and virtual environmentmake clean-build- Remove only build artifacts (dist/, build/)make clean-venv- Remove only the virtual environment
Workflow Tips
The Makefile uses smart dependency tracking. Running make test multiple times will only
reinstall dependencies if requirements-dev.txt or setup.cfg have changed, making
repeated test runs much faster.
To force a fresh installation of dependencies:
make install-dev
Running Tests
After setting up the development environment:
make test
Or directly with pytest:
.venv/bin/pytest
Code Quality & Pre-commit Hooks
The project uses prek to simplify pre-commit hook setup.
After installing development dependencies, set up pre-commit hooks:
prek install
This automatically installs git hooks that will:
- Run
rufflinter with auto-fix - Run
ruff formatfor code formatting - Run
mypyfor type checking
You can also run quality checks manually:
make lint # Check code quality make lint-fix # Auto-fix linting issues make format # Format code make typecheck # Run mypy type checking
The pre-commit hooks ensure code quality before commits, catching issues early and maintaining consistent code standards across all contributions.
The release process is automated end-to-end via the Release and
Publish to PyPI GitHub Actions workflows.
Versioning policy
The project follows Semantic Versioning:
PATCH(e.g. 1.2.0 → 1.2.1) for bug fixes and internal/build-only changes that are invisible to users (e.g. dropping unnecessary build dependencies).MINOR(e.g. 1.2.0 → 1.3.0) for new, backward-compatible features.MAJOR(e.g. 1.2.0 → 2.0.0) for breaking changes.
Maintaining the changelog
Add a bullet to the Unreleased section at the top of CHANGES.rst
whenever a PR worth mentioning to users is merged. The release workflow will
promote this section to Version X.Y.Z automatically when you cut the
release.
Cutting a release
- Make sure
CHANGES.rst'sUnreleasedsection lists at least one bullet for this release. The workflow refuses to release with an emptyUnreleasedsection. - Go to Actions → Release,
click Run workflow, choose
patch/minor/major, and confirm.
The Release workflow will:
- compute the new version from
dsmtpd/__init__.pyand the chosen part, - promote the
Unreleasedsection ofCHANGES.rsttoVersion X.Y.Zdated today (ISO format) and updatedsmtpd/__init__.py, - run the full test suite (
make test) andpython -m build+twine checkagainst the bumped tree — this is the last reversible point, so failure here aborts before any tag is pushed, - refuse to push if
mainhas moved during the run, - commit
Release version X.Y.Z(with the changelog bullets in the body) tomainand create the tagvX.Y.Z, then push both atomically, - trigger the
Publish to PyPIworkflow on the new tag and wait for it to finish — if the publish job fails, the release job fails too, so a green release run means the package is on PyPI.
The Publish to PyPI workflow runs the test matrix on the tagged commit,
builds the package, uploads to TestPyPI, uploads to PyPI (skipped
automatically for tags containing pre-release suffixes such as -rc1), and
creates the GitHub Release with auto-generated notes.
Two Release runs cannot execute in parallel — they are serialized via
a concurrency group.
Local dry run
You can preview what the bump would do without committing or pushing anything:
make bump PART=patch git diff # then undo if you only wanted to preview: git checkout -- dsmtpd/__init__.py CHANGES.rst
Manual fallback
If GitHub Actions is unavailable, the manual recipe still works: edit
dsmtpd/__init__.py and CHANGES.rst yourself (or run
make bump PART=... locally), commit as Release version X.Y.Z, then
git push origin main && git tag vX.Y.Z && git push origin vX.Y.Z.
Publish to PyPI will pick up the tag.
Copyright 2013 (c) by Stephane Wirtel