MailDispatch
MailDispatch is a self-hosted email delivery service for applications that need a durable queue, multiple SMTP senders, priorities, rate limits, retries, idempotency, and an authenticated management UI.
It integrates directly with maglink for
passwordless management login and exposes an HTTP API that other maglink
applications can use to send authentication email.
Features
- Durable database-backed mail queue
- Separate HTTP server and worker processes
- Multiple SMTP sender accounts
- Implicit TLS, STARTTLS, and optionally plain SMTP
- Sender selection weights and concurrency limits
- Per-minute, per-hour, and per-day sender limits
- Separate recipient limits for normal and authentication mail
- Message priority and retry scheduling
- Worker leases and recovery after worker failure
- Per-recipient delivery state and delivery-attempt history
- API keys with scopes and sender restrictions
- Concurrent-safe idempotency keys
- Recipient policies: any, allowlist, or enabled service users
- maglink passwordless management authentication
- Web UI for queues, users, senders, and API keys
- SQLite by default; other SQLAlchemy databases can be configured
Installation
Publish/install maglink first, then install MailDispatch:
pip install maildispatch
Create a configuration file:
maildispatch init -c maildispatch.yml
Edit the non-secret settings and provide secrets through the environment:
export MAILDISPATCH_SESSION_SECRET="$(python -c 'import secrets; print(secrets.token_urlsafe(48))')"
export MAILDISPATCH_MASTER_KEY="$(python -c 'import secrets; print(secrets.token_urlsafe(48))')"
export MAILDISPATCH_SYSTEM_SMTP_PASSWORD="your-smtp-app-password"
Validate the configuration and initialize the database:
maildispatch check -c maildispatch.yml
Run the web server and one worker for a small deployment:
maildispatch run -c maildispatch.yml
Open the configured service.base_url, request a login email for the bootstrap
administrator, and enter the displayed device code on the email confirmation
page.
Production processes
For production, run the HTTP service and workers separately:
maildispatch serve -c /etc/maildispatch/maildispatch.yml
maildispatch worker -c /etc/maildispatch/maildispatch.yml
Run multiple worker processes when more throughput is required. All processes
must use the same database and the same master key. Place the HTTP service
behind an HTTPS reverse proxy and set service.base_url to the public HTTPS
origin.
CLI
maildispatch init Create an example configuration
maildispatch check Validate configuration and initialize the database
maildispatch serve Run only the HTTP management/API server
maildispatch worker Run only a queue worker
maildispatch run Run the HTTP server and one in-process worker
Common options:
maildispatch --log-level INFO serve -c /etc/maildispatch/maildispatch.yml
maildispatch --log-level DEBUG worker -c /etc/maildispatch/maildispatch.yml
Configuration
The generated maildispatch.yml is the reference configuration. Environment
variables can be embedded as ${VARIABLE_NAME}. A secret can also be loaded
from a file:
service:
master_key:
file: /run/secrets/maildispatch_master_key
Service
service:
title: MailDispatch
base_url: https://mail.example.com
host: 127.0.0.1
port: 5080
secret_key: ${MAILDISPATCH_SESSION_SECRET}
master_key: ${MAILDISPATCH_MASTER_KEY}
secret_keysigns browser sessions.master_keyencrypts SMTP passwords stored in the database.- Changing
master_keywithout re-encrypting sender passwords makes existing sender credentials unreadable.
Database
database:
url: sqlite:///data/maildispatch.db
Relative SQLite paths are resolved relative to the configuration file. For a multi-host deployment, use a shared SQLAlchemy-supported database and install its Python driver.
Bootstrap administrator and sender
bootstrap:
mode: initialize_only
admin_email: admin@example.com
initial_users:
- email: admin@example.com
role: admin
enabled: true
can_login: true
system_sender:
id: system
name: Example Mail
email: mailer@example.com
enabled: true
smtp:
host: smtp.example.com
port: 465
username: mailer@example.com
password: ${MAILDISPATCH_SYSTEM_SMTP_PASSWORD}
tls_mode: implicit_tls
rate_limit:
per_minute: 20
per_hour: 300
per_day: 3000
max_concurrency: 2
Bootstrap inserts missing initial records. It does not overwrite an existing sender on restart. Later changes should be made through the management UI/API or directly through an intentional migration.
Additional senders can be defined under bootstrap.initial_senders or added in
the UI.
Authentication
auth:
require_captcha: true
token_ttl_seconds: 900
session_ttl_seconds: 86400
sender_id: system
mail_priority: 1000
request_rate_limit:
count: 3
window_seconds: 900
confirm_max_attempts: 8
Authentication email uses the configured sender and receives authentication priority. The email link alone is insufficient: the user must enter the device code shown by the waiting browser.
Recipient policy
mail:
recipient_policy:
mode: service_users
recipients: []
Modes:
any: any syntactically valid recipient is accepted;service_users: recipient must be an enabled MailDispatch user with login permission;allowlist: recipient must appear inrecipients.
The policy is checked both when a message is queued and immediately before sending. A queued message is canceled if its recipient becomes disallowed.
When external applications use MailDispatch for maglink authentication, choose
a policy that includes all application users. The external application must
still enforce its own login eligibility through maglink's IdentityProvider.
Rate limits and retries
mail:
default_priority: 100
max_priority_for_api_keys: 500
recipient_rate_limit:
enabled: true
normal:
per_minute: 5
per_hour: 30
per_day: 100
authentication:
per_15_minutes: 3
per_day: 20
worker:
poll_interval_seconds: 1
lease_seconds: 120
retry:
max_attempts: 5
delays_seconds: [60, 300, 1800, 7200, 28800]
API clients cannot exceed max_priority_for_api_keys. Messages with
purpose="authentication" require the authentication scope and use the
configured authentication priority and recipient limits.
Management UI
The management page supports:
- queue summary and status filtering;
- message status, sender, attempts, and errors;
- retrying failed/canceled messages;
- canceling queued messages;
- creating and editing users;
- changing user email, role, enabled state, and login permission;
- creating and editing SMTP sender accounts;
- changing sender host, TLS mode, limits, weight, and enabled state;
- rotating sender SMTP passwords;
- creating and restricting API keys.
SMTP passwords are never returned to the browser. Leave the password field empty when editing a sender to keep the current password.
Send API
Create a message
POST /api/v1/messages
Authorization: Bearer md_live_...
Idempotency-Key: invoice-123
Content-Type: application/json
{
"sender_id": "system",
"to": ["user@example.com"],
"cc": [],
"bcc": [],
"subject": "Your invoice",
"text": "Your invoice is ready.",
"html": "<p>Your invoice is ready.</p>",
"reply_to": "support@example.com",
"priority": 100,
"purpose": "transactional",
"metadata": {"invoice_id": "inv_123"}
}
Successful response:
{
"ok": true,
"message_id": "msg_...",
"status": "queued"
}
HTTP 202 means the message was durably queued, not delivered.
Message status
GET /api/v1/messages/msg_...
Authorization: Bearer md_live_...
An API key can only read messages created by that key.
Idempotency
Send a stable Idempotency-Key for every logical message. Concurrent requests
using the same API key and key value return the same message instead of
creating duplicate deliveries.
Do not reuse a key for unrelated content.
API-key scopes
| Scope | Purpose |
|---|---|
mail:send |
Submit normal messages |
mail:authentication |
Submit messages with purpose="authentication" |
Authentication integrations need both scopes. Sender restrictions can further limit a key to selected sender IDs.
curl example
curl -X POST "https://mail.example.com/api/v1/messages" \
-H "Authorization: Bearer $MAILDISPATCH_API_KEY" \
-H "Idempotency-Key: welcome-user-123" \
-H "Content-Type: application/json" \
-d '{
"sender_id": "system",
"to": ["user@example.com"],
"subject": "Welcome",
"text": "Welcome to the application.",
"priority": 100,
"purpose": "transactional"
}'
Python example
The repository contains a dependency-free client:
export MAILDISPATCH_API_KEY="md_live_..."
export MAILDISPATCH_API_URL="https://mail.example.com/api/v1/messages"
python examples/send_email.py user@example.com \
--subject "Hello" \
--text "Sent through MailDispatch"
Use MailDispatch for maglink login
import os
from maglink import AuthCore, HttpMailer, SqliteStore
mailer = HttpMailer(
endpoint=os.environ["MAILDISPATCH_API_URL"],
api_key=os.environ["MAILDISPATCH_API_KEY"],
sender_id="system",
)
core = AuthCore(
store=SqliteStore("data/auth.db"),
mailer=mailer,
verify_url_base="https://app.example.com/api/auth/verify",
identity_provider=application_identity_provider,
login_sender_id="system",
)
The API key needs mail:send and mail:authentication. See
examples/maglink_flask_login.py and the maglink agent skill for a complete
implementation.
Operational behavior
Message states include:
queued
sending
waiting_rate_limit
retry_wait
sent
failed
canceled
A worker claims one message with a lease, selects an eligible sender, reserves rate-limit counters, records a delivery attempt, and sends through SMTP. Transient transport errors are retried according to the configured delays. Permanent errors or exhausted retries mark the message failed.
Monitor worker logs and the management UI for:
- messages stuck in retry states;
- authentication failures;
- sender authentication errors;
- disabled or rate-limited senders;
- repeated recipient-policy cancellations;
- queue growth and delivery latency.
Security checklist
- Serve the web UI and API only through HTTPS.
- Keep session secret, master key, SMTP passwords, and API keys outside Git.
- Use SMTP app passwords or dedicated credentials.
- Prefer implicit TLS or STARTTLS; plain SMTP is disabled by default.
- Give API keys only the required scopes and sender IDs.
- Rotate exposed API keys and SMTP credentials immediately.
- Back up the database and master key separately and securely.
- Protect the master key: it decrypts every stored SMTP password.
- Use a restrictive recipient policy when possible.
- Keep authentication recipient limits stricter than ordinary mail limits.
- Run workers under a dedicated operating-system account.
- Restrict database and configuration-file permissions.
- Do not expose the built-in development server directly to the internet.
Development
python -m venv .venv
. .venv/bin/activate
pip install -e ../maglink
pip install -e ".[dev]"
pytest -q
Run locally:
maildispatch init
maildispatch check
maildispatch run
Release order
MailDispatch depends on maglink[flask]. Publish the required maglink version
before publishing a MailDispatch release that depends on it.
License
MIT. See the LICENSE file.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file maildispatch-0.1.0.tar.gz.
File metadata
- Download URL: maildispatch-0.1.0.tar.gz
- Upload date:
- Size: 40.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
d41681be60667b900ec34d316e97052eb7bf80660705379bd689772f74569bee
|
|
| MD5 |
59519dac3f9a00c1192cc70561de102a
|
|
| BLAKE2b-256 |
283711d3e152090a2b5dc297876df1193831cc29db403e27ae1c0e54bc7c0774
|
File details
Details for the file maildispatch-0.1.0-py3-none-any.whl.
File metadata
- Download URL: maildispatch-0.1.0-py3-none-any.whl
- Upload date:
- Size: 36.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.12.3
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4226c4b973238e9a01300ab7895de2836eb12f6e545b17e2f9189502d52cf596
|
|
| MD5 |
6188ea1bd5665451be7437aceb9a93fa
|
|
| BLAKE2b-256 |
094dafe90383d4ae042e809c44bfefb0f53e09e3bd3d090e3c5805515706a45d
|