Skip to main content

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_key signs browser sessions.
  • master_key encrypts SMTP passwords stored in the database.
  • Changing master_key without 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 in recipients.

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

maildispatch-0.1.0.tar.gz (40.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

maildispatch-0.1.0-py3-none-any.whl (36.4 kB view details)

Uploaded Python 3

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

Hashes for maildispatch-0.1.0.tar.gz
Algorithm Hash digest
SHA256 d41681be60667b900ec34d316e97052eb7bf80660705379bd689772f74569bee
MD5 59519dac3f9a00c1192cc70561de102a
BLAKE2b-256 283711d3e152090a2b5dc297876df1193831cc29db403e27ae1c0e54bc7c0774

See more details on using hashes here.

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

Hashes for maildispatch-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 4226c4b973238e9a01300ab7895de2836eb12f6e545b17e2f9189502d52cf596
MD5 6188ea1bd5665451be7437aceb9a93fa
BLAKE2b-256 094dafe90383d4ae042e809c44bfefb0f53e09e3bd3d090e3c5805515706a45d

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.1.0 This release

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page