hub v0.18.0: app-email passthrough POST /api/v1/mail → Resend SMTP

Raw-MIME passthrough (STARTTLS, AUTH LOGIN) — separate from the notify HTTP-API
alert path (which drops inline CID images). Per-customer token-bucket rate limit,
From-header allowlist backstop. Resend key stays hub-side. No new external dep.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-29 08:45:22 +02:00
parent 4b97855cdd
commit fa3c4f2657
8 changed files with 695 additions and 72 deletions
+22
View File
@@ -1,5 +1,27 @@
# Felhom Hub — Changelog
## v0.18.0 — App-email passthrough: POST /api/v1/mail → Resend SMTP (2026-06-29)
The hub can now relay a customer box's outbound app email to Resend, re-emitting the raw MIME
**unchanged** over SMTP. This is the hub leg of the app-email relay (apps → on-box shim → hub →
Resend); the Resend key stays hub-side. Implements `documentation/audits/SPIKE-smtp-app-relay-2026-06-28.md`.
- **New `internal/mailrelay/relay.go`:** `ResendSMTP` (a `Sender`) — STARTTLS to `smtp.resend.com:587`,
`AUTH LOGIN resend/<key>` (a small `net/smtp.Auth` LOGIN impl; stdlib ships PlainAuth only), then raw
`MAIL`/`RCPT`/`DATA`. **Raw passthrough** — NOT the `internal/notify` Resend **HTTP-API** path, which is
unchanged for the hub's own structured alerts and silently drops inline CID images (spike §4). No new
external dependency (stdlib `net/smtp`).
- **New `POST /api/v1/mail`** (`internal/api/mail.go`): authenticates the box (`checkAuthCustomer`), enforces
the **From-header** domain allowlist (backstop; reject 403), applies a **per-customer in-memory token-bucket
rate limit** (default 30/min → 429 so one box can't drain the shared Resend quota), then passes the raw
bytes through to Resend. Success→200, Resend failure→502 (the box's shim maps that to the app).
- **Config:** new `mail` section (`per_customer_per_minute`, `from_domains`); wired in `cmd/hub/main.go` only
when a Resend key is present (else the endpoint returns 503). The existing `notify/dispatcher.go` alert path
is untouched.
- **Tests:** passthrough byte-equality (raw bytes reach the sender unchanged, not parsed), From-reject +
companion red-proof, per-customer rate-limit + isolation + companion, send-failure→502, 401/503/400 paths,
token-bucket unit (injected clock), LOGIN auth + From-domain parsing.
## v0.17.0 — Resend key sourced from a Secret, out of git (2026-06-29)
Resend rotation + de-git hygiene. The hub's Resend API key was committed in plaintext in