Amalthea is a Rust-based tenant-scoped email routing platform with optional Python rule evaluation, a web admin dashboard, DKIM signing support, and multi-backend storage.
- Tenant-aware email routing for addresses like
local_part@tenant.domain.tld - Built-in support for:
- SQLite (default)
- PostgreSQL
- MongoDB (
mongodbfeature)
- Optional Python routing rules via
scripts/routing_rules.py - Admin dashboard with tenant and routing rule management
- HTTP health and Prometheus metrics endpoints
- Inbound SMTP listener via
mailin-embedded - Outbound SMTP relay support via
lettre - DKIM signing for outbound mail
Valid email addresses are parsed as:
role@domain.tldrole@tenant.domain.tld
Where:
roleis the local part before@tenantis an optional subdomain used as a tenant identifierdomain.tldis the registered domainrolemay include+metadata, e.g.support+ticket123@tenant.example.com
The parser accepts exactly 2 or 3 host labels and rejects deeper subdomains.
Incoming email is routed by:
- Parsing the address
- Validating the registered domain
- Looking up the tenant by subdomain
- Loading tenant routing rules from the configured database
- Matching the
local_partagainst rule patterns- exact match
- wildcard
* - glob patterns like
admin*
- Applying Python rule overrides if enabled
- Returning one of:
DiscardWebhookForwardStoreReject
src/main.rs— binary entrypointsrc/lib.rs— library root andrun()entrypointsrc/compile_config.rs— defaults and env var namessrc/parsers/email_address.rs— address parsing modelsrc/routing/mod.rs— routing enginesrc/db/— database pool and repositorysrc/dashboard/— web dashboard, templates, authsrc/smtp/— inbound SMTP listenersrc/mailer/— outbound SMTP relay + DKIM signingsrc/dkim/— DKIM configuration and signingsrc/python/— Python rule integrationscripts/routing_rules.py— optional Python routing rule filetemplates/— Tera HTML templatesstatic/— frontend assetssql/— backend-specific SQL queriesmigrations/— schema DDL
cp .env.example .env
cargo runThis starts:
- HTTP dashboard on
127.0.0.1:8080 - SMTP listener on
0.0.0.0:22000
cargo build --release- SQLite only (default)
- PostgreSQL:
cargo build --release --features postgres
- MongoDB:
cargo build --release --features mongodb
- Python routing rules:
cargo build --release --features "python-rules"
To enable both SQLite and Python rules:
cargo build --release --features "sqlite python-rules"docker compose up -d --buildThe compose file exposes:
8080:8080— HTTP dashboard22000:22000— SMTP listener
Optional database services are available under profiles:
postgresmongodb
GET /dashboardGET /dashboard/tenantsGET /dashboard/rulesGET /dashboard/healthGET /dashboard/metricsGET /login
API endpoints:
GET /api/tenantsPOST /api/tenantsDELETE /api/tenants/{id}GET /api/rulesPOST /api/rulesDELETE /api/rules/{id}POST /api/route-testPOST /api/mail/sendPOST /api/auth/loginPOST /api/auth/logout
Important environment variables:
DATABASE_TYPE—sqlite(default),postgres, ormongodbSQLITE_DB_PATH— default./amalthea.dbDATABASE_URL— PostgreSQL connection stringMONGODB_URL— MongoDB connection stringSERVER_BIND_HOST— default127.0.0.1SERVER_BIND_PORT— default8080SMTP_BIND_HOST— default0.0.0.0SMTP_SERVER_PORT— default22000ADMIN_USERNAME— defaultadminADMIN_PASSWORD— defaultchangemeJWT_SECRET— required for dashboard loginDKIM_DOMAIN— domain used for routing and signingDKIM_SELECTOR— DKIM selectorDKIM_PRIVATE_KEY_PATH— path to private keySMTP_RELAY_HOST— outbound relay hostSMTP_RELAY_PORT— relay portSMTP_RELAY_USERNAME— relay usernameSMTP_RELAY_PASSWORD— relay passwordSMTP_RELAY_FROM— default outbound From addressINTERNAL_API_KEY— required for/api/mail/send
Note: if
JWT_SECRETis not set, admin login is disabled by design.
Enable Python rule support with the python-rules Cargo feature.
The Python script should define:
route_email(email_address)
It must return a dict with one of:
{"action": "default"}{"action": "override", "target_type": "DISCARD"}{"action": "override", "target_type": "STORE"}{"action": "override", "target_type": "WEBHOOK", "target_value": "https://..."}{"action": "override", "target_type": "FORWARD", "target_value": "user@example.com"}{"action": "reject", "reason": "..."}
The default sample script is scripts/routing_rules.py.
Supported TargetType values in tenant routing rules:
DISCARDWEBHOOKFORWARDSTORE
Rule matching supports:
- exact local-part match
- wildcard
* - glob patterns via
glob::Pattern
src/dkim/mod.rssigns outbound mail usingopensslsrc/mailer/mod.rssends mail through the configured SMTP relay- If DKIM is not configured, mail is sent unsigned with a warning
- Outbound mail requires
SMTP_RELAY_HOST
Inbound SMTP is handled by src/smtp/mod.rs with mailin-embedded.
- Accepts mail for the configured registered domain
- Uses the same routing engine as the dashboard test endpoint
- Supports
STORE,FORWARD, andDISCARD WEBHOOKdelivery is currently logged and stored instead of dispatched- Plaintext only — no STARTTLS/TLS. If you're internet-facing on this listener, put a real MTA (Postfix/Exim/etc.) in front of it to terminate TLS; see DEPLOYMENT.md for a worked example.
SMTP_SERVER_PORT defaults to 22000, not 25 — real inbound MX delivery
needs something routing 25 → this port (a local MTA, DNAT, or
cap_net_bind_service), plus DNS-only (unproxied) mail records at whatever
provider you use. See DEPLOYMENT.md for a full
Hetzner + Postfix walkthrough, the TLS caveat above in more detail, and
alternative shapes (different VPS/DNS providers, no separate MTA,
outbound-only via relay, managed Postgres/Mongo, driving the API from
non-Rust services).
- Change default
ADMIN_USERNAME/ADMIN_PASSWORD - Always set
JWT_SECRETfor admin access - Do not expose the admin dashboard or SMTP listener without proper network controls
- Secure
DKIM_PRIVATE_KEY_PATH
cargo test
cargo test -- --nocapture
cargo clippy
cargo fmtMIT License. See LICENSE.