herald-smtp

Multi-language Documentation
SMTP email adapter for Herald. Herald forwards verification codes over HTTP to this service; herald-smtp sends email via SMTP. All SMTP credentials and sending logic live in this project only—Herald does not hold any SMTP credentials when using herald-smtp.
The HTTP server uses Fiber v3 and the matching v2 module lines of the Fiber-facing kit packages. Building from source requires Go 1.26 or later.
Core Features
- Herald HTTP Provider contract: Implements the same HTTP send contract as Herald's external provider; request/response align with provider-kit
HTTPSendRequest / HTTPSendResponse.
- Optional API Key auth: When
API_KEY is set, Herald must send X-API-Key; otherwise no auth required.
- Idempotency: Supports
Idempotency-Key (or body idempotency_key, maximum 256 bytes); requests with the same key and content share one SMTP send within a single process, and successful results are cached for the configured TTL.
- SMTP transport modes: Supports plaintext SMTP, STARTTLS, and implicit TLS with bounded send timeouts.
- Bounded SMTP concurrency: Limits simultaneous SMTP sends with
SMTP_MAX_CONCURRENT_SENDS; excess requests fail immediately with 429 rate_limited instead of building an unbounded queue.
- Graceful shutdown: On
SIGINT or SIGTERM, the server stops accepting new requests and waits up to SHUTDOWN_TIMEOUT_SECONDS. The effective timeout is never shorter than SMTP_TIMEOUT_SECONDS + 5 seconds.
v1 Compatibility
Version 1 establishes the stable HTTP and operational contract for /v1/send, /healthz, /readyz, the documented environment variables, and the JSON success/error envelopes. Backward-compatible fields and endpoints may be added in minor releases; removing or changing documented behavior requires a new major version.
A successful SMTP response means that the configured SMTP server accepted the message. It is not proof of final mailbox delivery. Readiness checks local SMTP client initialization only, and idempotency remains local to one process.
Architecture
sequenceDiagram
participant User
participant Stargate
participant Herald
participant HeraldSMTP as herald-smtp
participant SMTP as SMTP Server
User->>Stargate: Login (email)
Stargate->>Herald: Create challenge (channel=email, destination=email)
Herald->>HeraldSMTP: POST /v1/send (to, subject, body)
HeraldSMTP->>SMTP: SMTP send
SMTP-->>User: Email
HeraldSMTP-->>Herald: ok, message_id
Herald-->>Stargate: challenge_id, expires_in
- Stargate: ForwardAuth / login orchestration.
- Herald: OTP challenge creation and verification; calls herald-smtp for channel
email when HERALD_SMTP_API_URL is set.
- herald-smtp: HTTP adapter; sends email via SMTP; holds SMTP credentials only here.
Protocol
- POST /v1/send
Request: channel (e.g. email), to (email address), subject, body (or params.code), idempotency_key, optional template/params/locale.
Response: { "ok": true, "message_id": "...", "provider": "smtp" } or { "ok": false, "error_code": "...", "error_message": "..." }.
- GET /healthz: Liveness endpoint returning
{ "status": "healthy", "service": "herald-smtp" }. It does not test SMTP configuration or connectivity.
- GET /readyz: Readiness endpoint. It returns
200 after the SMTP client is initialized and 503 when SMTP configuration is missing or invalid.
Essential Configuration
| Variable |
Description |
Default |
Required |
PORT |
Listen port (with or without leading colon) |
:8084 |
No |
API_KEY |
If set, Herald must send X-API-Key |
`` |
No |
SMTP_HOST |
SMTP server host |
`` |
Yes (for send) |
SMTP_PORT |
SMTP server port |
587 |
No |
SMTP_USER |
SMTP username |
`` |
No (if server allows anonymous) |
SMTP_PASSWORD |
SMTP password |
`` |
No |
SMTP_FROM |
Sender email address |
`` |
Yes (for send) |
SMTP_FROM_NAME |
Optional sender display name |
`` |
No |
SMTP_USE_TLS |
Use implicit TLS (typically port 465) |
false |
No |
SMTP_USE_STARTTLS |
Use STARTTLS |
true |
No |
See the deployment guide for TLS modes, timeouts, request limits, idempotency limits, and the complete environment-variable reference.
Herald side
Configure Herald with HTTP provider for channel email (instead of built-in SMTP):
HERALD_SMTP_API_URL = base URL of herald-smtp (e.g. http://herald-smtp:8084)
- Optional:
HERALD_SMTP_API_KEY = same as herald-smtp API_KEY
When HERALD_SMTP_API_URL is set, Herald does not use built-in SMTP (no SMTP_HOST in Herald).
Quick Start
Build and run
export SMTP_HOST=smtp.example.com
export SMTP_PORT=587
export SMTP_FROM=noreply@example.com
export SMTP_USER=user
export SMTP_PASSWORD=secret
export API_KEY=replace-with-a-strong-random-value
go build -o herald-smtp .
./herald-smtp
In another terminal, verify liveness and send a test request:
curl -sS http://localhost:8084/healthz
curl -fsS http://localhost:8084/readyz
curl -sS -X POST http://localhost:8084/v1/send \
-H 'Content-Type: application/json' \
-H 'X-API-Key: replace-with-a-strong-random-value' \
-H 'Idempotency-Key: quickstart-001' \
-d '{"to":"recipient@example.com","subject":"Test","body":"Hello from herald-smtp"}'
Replace the example host, credentials, sender, recipient, and API key before use. A successful /healthz response only confirms that the process is running; it does not prove that SMTP sending works.
Run with Docker
docker pull ghcr.io/soulteary/herald-smtp:v1.0.0
docker run -d --name herald-smtp -p 8084:8084 --stop-timeout=40 \
-e SMTP_HOST=smtp.example.com \
-e SMTP_FROM=noreply@example.com \
-e SMTP_USER=user \
-e SMTP_PASSWORD=secret \
ghcr.io/soulteary/herald-smtp:v1.0.0
Optional: add -e API_KEY=your_shared_secret and set HERALD_SMTP_API_KEY to the same value on Herald.
For production, pin an exact release tag rather than latest. If SMTP_TIMEOUT_SECONDS makes the effective shutdown timeout exceed 40 seconds, increase --stop-timeout to match.
Scaling note: idempotency state is held in process memory. Multiple replicas do not share cached keys, so the same request routed to different replicas can be sent more than once. Use one replica unless the caller provides a shared idempotency layer.
Documentation
Testing
go test -race -cover ./...
License
See LICENSE for details. Notable release changes are recorded in CHANGELOG.md.