Metadata-Version: 2.4
Name: maildispatch
Version: 0.1.0
Summary: Self-hosted mail dispatch service with queues, priorities, sender and recipient rate limits, and maglink authentication.
Author-email: sxwxs <sxwxs@msn.com>
License-Expression: MIT
Project-URL: Homepage, https://github.com/sxwxs/maildispatch
Project-URL: Repository, https://github.com/sxwxs/maildispatch.git
Project-URL: Issues, https://github.com/sxwxs/maildispatch/issues
Keywords: email,smtp,queue,mail,flask
Classifier: Development Status :: 4 - Beta
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Operating System :: OS Independent
Classifier: Topic :: Communications :: Email
Classifier: Topic :: Internet :: WWW/HTTP
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: Flask>=2.3
Requires-Dist: PyYAML>=6.0
Requires-Dist: SQLAlchemy>=2.0
Requires-Dist: cryptography>=41
Requires-Dist: maglink[flask]>=0.2.0
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == "dev"
Dynamic: license-file

# 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`](https://pypi.org/project/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:

```bash
pip install maildispatch
```

Create a configuration file:

```bash
maildispatch init -c maildispatch.yml
```

Edit the non-secret settings and provide secrets through the environment:

```bash
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:

```bash
maildispatch check -c maildispatch.yml
```

Run the web server and one worker for a small deployment:

```bash
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:

```bash
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

```text
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:

```bash
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:

```yaml
service:
  master_key:
    file: /run/secrets/maildispatch_master_key
```

### Service

```yaml
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

```yaml
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

```yaml
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

```yaml
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

```yaml
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

```yaml
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

```http
POST /api/v1/messages
Authorization: Bearer md_live_...
Idempotency-Key: invoice-123
Content-Type: application/json
```

```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:

```json
{
  "ok": true,
  "message_id": "msg_...",
  "status": "queued"
}
```

HTTP `202` means the message was durably queued, not delivered.

### Message status

```http
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

```bash
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:

```bash
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

```python
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:

```text
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

```bash
python -m venv .venv
. .venv/bin/activate
pip install -e ../maglink
pip install -e ".[dev]"
pytest -q
```

Run locally:

```bash
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.
