Skip to content

matrixise/dsmtpd

Repository files navigation

dsmptd: A debugger SMTP server for Humans

Tests Status

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

Usage

$ 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

Installation

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

Command-Line Options

-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

Usage Examples

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

Features

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.

Exit Codes

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.

Contributing

Clone the repository:

git clone git://github.com/matrixise/dsmtpd.git
cd dsmtpd

Development

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 linter
  • make lint-fix - Auto-fix linting issues and format code with ruff
  • make format - Format code with ruff format
  • make typecheck - Run mypy type checking

Build and Release:

  • make build - Build distribution packages
  • make check-dist - Verify distribution package integrity

Cleanup:

  • make clean - Remove all build artifacts and virtual environment
  • make 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 ruff linter with auto-fix
  • Run ruff format for code formatting
  • Run mypy for 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.

Releasing

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

  1. Make sure CHANGES.rst's Unreleased section lists at least one bullet for this release. The workflow refuses to release with an empty Unreleased section.
  2. Go to Actions → Release, click Run workflow, choose patch / minor / major, and confirm.

The Release workflow will:

  • compute the new version from dsmtpd/__init__.py and the chosen part,
  • promote the Unreleased section of CHANGES.rst to Version X.Y.Z dated today (ISO format) and update dsmtpd/__init__.py,
  • run the full test suite (make test) and python -m build + twine check against the bumped tree — this is the last reversible point, so failure here aborts before any tag is pushed,
  • refuse to push if main has moved during the run,
  • commit Release version X.Y.Z (with the changelog bullets in the body) to main and create the tag vX.Y.Z, then push both atomically,
  • trigger the Publish to PyPI workflow 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

About

dsmtpd is a small SMTP server for debugging and the developers.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors