https://github.com/klppl/postern
Self-hosted email gateway: microservices POST to an HTTP API, messages queue in SQLite and deliver via SMTP or MXroute's HTTP API. Single Go binary, no CGO, retries, per-key rate limits, templates, admin UI.
https://github.com/klppl/postern
email email-api email-gateway golang microservices mxroute self-hosted smtp sqlite transactional-email
Last synced: 3 days ago
JSON representation
Self-hosted email gateway: microservices POST to an HTTP API, messages queue in SQLite and deliver via SMTP or MXroute's HTTP API. Single Go binary, no CGO, retries, per-key rate limits, templates, admin UI.
- Host: GitHub
- URL: https://github.com/klppl/postern
- Owner: klppl
- License: other
- Created: 2026-04-26T20:56:50.000Z (3 months ago)
- Default Branch: main
- Last Pushed: 2026-07-10T20:36:59.000Z (17 days ago)
- Last Synced: 2026-07-10T22:13:41.476Z (16 days ago)
- Topics: email, email-api, email-gateway, golang, microservices, mxroute, self-hosted, smtp, sqlite, transactional-email
- Language: Go
- Size: 109 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
README
# Postern
Self-hosted email gateway. Your microservices POST to `/api/v1/send`; Postern
queues the message in SQLite, renders any template, and delivers it through a
provider — a standard SMTP relay or MXroute's HTTP SMTP API.
Single Go binary, no CGO (`modernc.org/sqlite`), no external broker. Migrations,
admin templates, and static assets are embedded — only `.env` and the DB file
are read at runtime.
## Features
- **HTTP send API** with `pn_`-prefixed API keys (stored hashed, shown once).
- **Recipients bound to the key** by default; per-key opt-in for request-supplied
`to`/`cc`/`bcc` (all-or-nothing, capped at 50). `from` is never overridable.
- **Queue + retry**: single worker drains the outbox, backoff 1m→5m→15m→1h→6h→24h,
dead after 6 attempts. 5xx SMTP errors dead-letter immediately.
- **Per-key rate limits** (minute/hour/day), persisted in SQLite.
- **Handlebars templates** (`{{var}}`) with subject/text/HTML variants; public or
per-key allow-listed.
- **Server-rendered admin UI** with signed-cookie sessions for SMTP config, keys,
templates, and message inspection.
- **Encryption at rest**: one master key AES-256-GCM-encrypts the SMTP password
and HMAC-signs cookies.
## Quick start
```bash
go build -o postern ./cmd/postern
cp .env.example .env # set POSTERN_MASTER_KEY (openssl rand -hex 32) + admin creds
./postern # open http://localhost:8080/admin/
```
Sign in, pick a delivery mode and fill in provider config, then create an API
key (the raw key is shown once).
## API
`POST /api/v1/send`, `Authorization: Bearer pn_...`, JSON body:
```json
{ "subject": "Welcome", "body": "Hi.", "body_html": "
Hi.
" }
```
or with a template:
```json
{ "template_name": "welcome", "variables": { "name": "Alex" } }
```
Add `to`/`cc`/`bcc` when the key allows recipient override. Responses:
`202` queued, `400` bad request, `401` bad key, `429` rate limited (honor
`Retry-After`). Delivery is async; check status in `/admin/messages/`.
`GET /healthz` → `{"ok":true}`.
## Configuration
Bootstrap and secrets are env vars; provider config and retention live in SQLite
and are edited in the admin UI (re-read on every send, so rotating credentials or
switching providers needs no restart).
| Variable | Required | Default | Notes |
| ------------------------- | -------- | ------------ | ------------------------------------------ |
| `POSTERN_MASTER_KEY` | yes | — | 32-byte hex/base64; encrypts secrets + signs cookies |
| `POSTERN_DB_PATH` | no | `postern.db` | SQLite file path |
| `POSTERN_LISTEN_ADDR` | no | `:8080` | `host:port` |
| `POSTERN_ADMIN_USERNAME` | no | — | Bootstraps the first admin user |
| `POSTERN_ADMIN_PASSWORD` | no | — | Same |
| `POSTERN_TLS_CERT` / `_KEY`| no | — | Built-in TLS (set both or neither) |
| `POSTERN_WORKER_INTERVAL` | no | `1s` | Outbox poll interval |
| `POSTERN_SHUTDOWN_GRACE` | no | `30s` | HTTP shutdown grace |
**Back up the master key** — losing it makes the stored SMTP password
unrecoverable.
## Deploy
Run behind a reverse proxy that terminates TLS (set
`POSTERN_LISTEN_ADDR=127.0.0.1:8080`), or use built-in TLS via the cert/key vars.
```bash
# systemd
sudo ./deploy/install.sh && sudo systemctl start postern
# docker (distroless, ~25 MB)
docker run -d -p 8080:8080 -v postern-data:/data \
-e POSTERN_MASTER_KEY=$(openssl rand -hex 32) \
-e POSTERN_ADMIN_USERNAME=admin -e POSTERN_ADMIN_PASSWORD=changeme postern
```
## Build
```bash
go test ./...
go build -trimpath -ldflags="-s -w" -o postern ./cmd/postern
```
Requires Go 1.22+. No CGO.
## License
[The Lagom License](LICENSE).