Rust mail server with React Email templates, persistent priority queue, per-request SMTP overrides, JWT auth, and a built-in Swagger UI.
- Dynamic config via env vars /
.env/ TOML — no code changes to rebrand or swap SMTP. - React Email templates compiled to HTML at build time, with full i18n (EN/FR shipped, any locale added by convention).
- Tailwind theming — brand colors, fonts, radius, logo, social links injected into every built-in template.
- Priority queue backed by Postgres (apalis). Jobs survive restarts and resume automatically.
- JWT + argon2 API keys — issue short-lived bearer tokens from long-lived keys.
- Per-request SMTP override for
/mail/send-custom— credentials held in memory only. - Swagger UI at
/swagger-uiwired with the bearer JWT auth scheme so you can test every route from the browser. - Dockerized, multi-arch (linux/amd64 + linux/arm64) CI/CD pipeline pushes to Docker Hub on tag.
| Concern | Tech |
|---|---|
| HTTP | axum + tokio + tower |
| SMTP | lettre |
| Templates | React Email (Bun) → HTML + minijinja |
| Queue | apalis + Postgres |
| Auth | jsonwebtoken + argon2 |
| Config | figment (env + TOML) + dotenvy |
| Docs | utoipa + utoipa-swagger-ui |
| Observability | tracing + tracing-subscriber |
| Tests | cargo test + testcontainers (planned) |
Mailify/
├── crates/
│ ├── mailify-core/ # domain types, errors
│ ├── mailify-config/ # figment loader, theming, dotenv
│ ├── mailify-templates/ # HTML loader + minijinja renderer
│ ├── mailify-smtp/ # lettre wrapper + per-request override
│ ├── mailify-queue/ # apalis jobs (priority, retry, resume)
│ ├── mailify-auth/ # JWT issuer + axum middleware
│ └── mailify-api/ # axum bin `mailify` + OpenAPI
├── templates-parser/ # React Email source (Bun)
├── docker/Dockerfile # multi-stage (bun-builder → rust-builder → distroless)
├── docker-compose.yml # postgres + mailpit + mailify
└── .github/workflows/ci.yml # fmt, clippy, test, docker push
cp .env.example .env
make up
# http://localhost:8080/swagger-ui → interactive API docs
# http://localhost:8025 → Mailpit inbox (SMTP sink)# 1. Compile templates (first run only / after edits)
make gen build-templates
# 2. Start Postgres + Mailpit
docker compose up -d postgres mailpit
# 3. Run server
make devOr run the full flow with make setup && make dev.
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /health |
— | Liveness probe |
| POST | /auth/token |
— | Exchange API key for JWT |
| GET | /config |
JWT | Sanitized runtime config (secrets redacted) |
| GET | /templates |
JWT | Template catalog (id, category, locales) |
| GET | /templates/{id}/preview |
JWT | Render template as HTML (or JSON with ?json=true) |
| POST | /templates/{id}/preview |
JWT | Render with a JSON body ({locale, vars, json}) |
| POST | /mail/send |
JWT | Queue email using a built-in template |
| POST | /mail/send-custom |
JWT | Queue email with caller-supplied HTML + optional SMTP |
| GET | /swagger-ui |
— | Swagger UI (bearer JWT auth wired) |
| GET | /api-docs/openapi.json |
— | OpenAPI 3 schema |
API keys are stored in config as <id> → argon2_hash. Generate a hash:
make hash-key KEY=super-secret-value
# MAILIFY_AUTH__API_KEYS__WEB=$argon2id$v=19$m=19456,t=2,p=1$...Add the generated line to .env, then call POST /auth/token with {"api_key_id":"web","api_key":"super-secret-value"}.
All config is runtime. Precedence (low → high):
- Built-in defaults (
AppConfig::default) - Optional TOML file at
MAILIFY_CONFIG .env.<MAILIFY_ENV>.local→.env.<MAILIFY_ENV>→.env.local→.env- Process env
Nested fields use __ separator:
MAILIFY_SMTP__HOST=smtp.sendgrid.net
MAILIFY_SMTP__TLS=tls
MAILIFY_THEME__COLORS__PRIMARY=#2563eb
MAILIFY_AUTH__API_KEYS__WEB=$argon2id$...
Disable dotenv: MAILIFY_DOTENV=false. See .env.example for the full surface.
Built-in template source lives at templates-parser/emails. Add a new template by appending an entry to scripts/templates.config.ts, then:
make build-templatesThis runs the generator, exports HTML via email export, decodes HTML entities inside {{ }}/{% %} spans, copies subject/text sidecars, and writes catalog.json. Output lands in templates-parser/out/<id>/<locale>.html — exactly what the Rust registry reads at boot.
Theming tokens are referenced as {{ theme.colors.primary }}, {{ theme.fonts.body }}, etc. Any variable under vars in the send payload is available as {{ vars.* }}.
Tag a release — CI builds multi-arch images and pushes to Docker Hub.
git tag v0.1.0
git push origin v0.1.0Required repo secrets: DOCKERHUB_USERNAME, DOCKERHUB_TOKEN.
Distroless runtime image is ~20 MB compressed, runs as non-root, exposes port 8080.
make test # cargo test --workspace
make check # cargo check --workspace --all-targets
make clippy # cargo clippy --workspace --all-targets -- -D warnings
make fmt # cargo fmt --allMIT.