Roadmap
This page is the canonical public roadmap. The narrative lives here; the execution surface (open issues, in-flight work) lives in GitHub Milestones.
Posthorn ships in deliberately small, scoped releases. v1.0 absorbs what was originally sequenced as v1.1, v1.2, and v1.3 into a single release because the surface area is small enough that splitting them produced more carve-outs than coherent product moments. v2 is the next planned milestone — the stateful-platform shift.
v1.0 — first public release
Section titled “v1.0 — first public release”Status: released. See the GitHub Releases page for the version history.
Scope: three ingress shapes, five outbound transports, full spam-protection stack, idempotency, dry-run, Prometheus metrics, hand-rolled SMTP listener for internal apps.
| Ingress | Detail |
|---|---|
| HTTP form (default) | application/x-www-form-urlencoded, multipart/form-data; honeypot, Origin/Referer fail-closed, optional CSRF tokens, rate limit |
| HTTP API mode | Bearer auth, JSON body, idempotency keys, per-request to_override, per-key rate limit |
| SMTP listener | AUTH PLAIN / client-cert, STARTTLS-required, sender allowlist + recipient allowlist or cap, size cap |
| Transport | Detail |
|---|---|
| Postmark | HTTP API, server-token auth |
| Resend | HTTP API, bearer auth |
| Mailgun | HTTP API, basic auth, multipart body, US/EU regions |
| AWS SES | HTTP API, Signature Version 4 (SigV4) auth, region-scoped |
| Outbound SMTP | Any STARTTLS-capable relay; stdlib net/smtp |
Operational essentials: /healthz, /metrics (Prometheus exposition), dry-run mode, IP-stripping, named trusted_proxies presets (Cloudflare).
What you can do today:
- Drop in a self-hosted contact form on any host that blocks outbound SMTP
- Run Posthorn as a Docker sidecar next to your blog / static site / app stack
- Hit it from a Cloudflare Worker for transactional sends with safe retries
- Point Ghost / Gitea / Mastodon / Authentik at Posthorn’s SMTP listener and let them keep speaking SMTP
- Reverse-proxy from whatever front door you already use (Caddy, nginx, Traefik, Cloudflare)
Spam protection
Section titled “Spam protection”A focused release that stacks additive defenses on top of the v1.0 form-mode protections (honeypot, Origin/Referer fail-closed, rate limit, CSRF). Each item is independently shippable and backwards-compatible — existing configs keep working unchanged. No platform-shape changes, no storage layer required. (Milestone names describe themes, not version numbers; this lands in a future minor release.)
The order below reflects a concrete, validated workload: live contact-form spam against a production deployment with honeypot + Origin + rate-limit all correctly configured still lands, because form-blaster bots (Xrumer / GSA SER) render the page for a valid Referer, skip the empty honeypot, and trickle under the burst cap. The items that actually beat that pattern lead the release; the ones that the observed spam would have walked straight past are deprioritized.
Leads the release — beats the observed pattern. The order was re-cut on 2026-07-04 against real spam captured on a production deployment: distributed reply-bait from burner Gmail addresses, in four different languages, zero-to-one links, one line to a wall of text. There is no content signature, so the checks that win are the content-agnostic ones that key on identity and bot behavior. The three checks below are merged to main and ship in the next minor release.
- ✅ Reputation-based spam check (merged) — optional StopForumSpam lookup on the submitter email and IP before send; fail-open so an outage never blocks legitimate mail. It catches the sender, not the message, so it works across every language and length the spam takes, and the repeat offender (the same burner address hitting a form for days) is caught on every attempt after the first. (#44)
- ✅ Proof-of-browser token (merged) — a JS-gated submit token that blocks form-blasters which POST directly without executing JavaScript. Also content-agnostic, no third-party dependency, no UX friction; the cheap kill for the bots reputation hasn’t catalogued yet. (#45)
- ✅ Cloudflare Turnstile verification (merged) — optional per-endpoint captcha for high-spam targets; the durable backstop that stops even headless-browser bots. siteverify call on submit, byte-identical 200 response on failure. (#33)
- CSRF minimum-age check — extends the existing CSRF TTL with a floor; bots that POST within milliseconds of token issuance get rejected. (#32)
Ships alongside the checks — operator trust and robustness (added 2026-07-03):
- Blocked-spam visibility — per-endpoint
on_spam = "discard" | "log", plusaction = "tag"on the fuzzy checks (reputation) to deliver with a[SPAM?]subject prefix instead of rejecting. A confidence-threshold check can false-positive, and without visibility that erodes trust invisibly: operators need to audit what got blocked. A batched email digest of blocked submissions is deferred to a follow-up release with its own design pass. (#61) - Single-use tokens — CSRF and proof-of-browser tokens die after first successful use (bounded in-memory cache of consumed tokens). Without this, one harvested token replays for the full TTL window; with it, the minimum-age and proof-of-browser checks hold against harvest-and-replay. (#62)
origin_policy = "strict"— opt-in: a present-but-mismatchedOriginheader rejects immediately, with no Referer fallback. Default stays lenient for compatibility with privacy extensions. (#63)
Deprioritized — ships when a real deployment surfaces the matching pattern:
- Content-shape validation —
max_linksURL cap andmin_message_length. Deprioritized after the real spam turned out to have no content signature: link counts of zero to one, lengths from one line to a wall of text, so a URL cap catches nothing and a length floor would reject legitimate short inquiries. Kept as a bounded primitive if a link-heavy pattern ever surfaces. (#34) - Multiple honeypot fields —
honeypotaccepts an array; any field being filled trips rejection. Marginal against bots that skip hidden fields entirely. (#31) - Operator-provided email-domain blocklist — disposable-provider domains from a file. Deprioritized: the observed spam came from mainstream providers (gmail), so a disposable-domain list scored zero against it. (#35)
- Per-day rate-limit cap — stacks on the existing token bucket for long-running low-rate abuse. Deprioritized: the observed spam is a distributed trickle from varied IPs, which a per-identity daily cap doesn’t touch. (#36)
Ordering reflects validated workload over upvotes — an item that a real deployment proves it needs (or proves it doesn’t) moves accordingly.
v2 — platform maturity
Section titled “v2 — platform maturity”The architectural shift that unlocks operating Posthorn as a real mail platform: durable state, lifecycle awareness, deliverability hygiene.
Storage layer:
- SQLite submission log — every submission persisted, queryable
- Retry queue across restarts — failed sends survive a container restart and retry later
Mail-platform features built on storage:
- Suppression list — auto on hard bounces and spam complaints; admin API for inspection and manual entries. Refuses to send to suppressed addresses (returns 200 with
{"suppressed": true}so callers treat it as final, not transient). - Durable idempotency — the v1.0 in-memory cache becomes restart-survivable
- Lifecycle event callbacks — delivery / bounce / open / click events forwarded to the originating caller with a Hash-based Message Authentication Code (HMAC) signed webhook
- Automatic unsubscribe link injection — per-recipient signed tokens, hosted unsubscribe endpoint, RFC 8058 one-click headers (which Gmail and Yahoo now require for senders >5000/day)
- HTML body support
- File attachments — multipart form uploads forwarded to the transport
- Multiple outputs per endpoint — email + webhook + log fan-out for the same submission
- Multi-tenant SMTP routing — sender-driven transport selection (envelope
MAIL FROMpicks the tenant’s provider account, with recipient as an optional secondary match; one SMTP listener serves multiple tenants with separate sender identities). See the design discussion in issue #30.
The Transport.Send interface stays the synchronous primitive; the queue wraps it. v1.0 deployments keep working unchanged.
Smaller items on the queue
Section titled “Smaller items on the queue”Operator feedback from the v1.0 launch surfaced a few quality-of-life items that don’t justify a v2 wait. These ship as patch releases or a 1.1 feature bump:
- Auto-generate ephemeral self-signed TLS certs when
require_tls = trueand notls_cert/tls_keyis provided. For internal-network deployments where TLS-on-the-wire is wanted but certificate file management isn’t. Requires explicit startup logging that the cert is ephemeral and not trustworthy for external clients. Issue #27. - Friendlier validation errors with deployment-pattern hints. Errors that surface common-config-shape examples (“for internal Docker networks, use auth_required = none; for production, use smtp-auth with require_tls = true”). Polish, but the validation surface is where new operators first encounter Posthorn’s mental model. Issue #28.
v3 — speculative
Section titled “v3 — speculative”No date. Depends entirely on community traction:
- Admin UI — embedded web app for browsing submissions, viewing logs, retrying failed sends (requires v2’s SQLite storage)
- Proof-of-work spam challenge — computational cost imposed on submitters; defeats botnet spam from many low-rate IPs that the v1.0 rate limiter can’t catch
- PGP encryption — encrypt outbound mail to recipient public keys at the gateway
How to influence the order
Section titled “How to influence the order”If you want to push something forward:
- Open a feature request with a concrete use case. Pitches like “wouldn’t it be cool if…” get less weight than “I want to use Posthorn for X, but Y is missing.”
- Comment on the relevant milestone or existing issue with your use case. Adoption signal beats pure upvotes.
- Start a discussion for half-formed ideas where the right framing isn’t clear yet.
What’s deliberately out of scope
Section titled “What’s deliberately out of scope”These come up in conversation and are worth naming explicitly:
| Feature | Why not |
|---|---|
| Acting as a “real” MTA (MX records, inbound mail) | Posthorn is an authenticated relay, not a mail server. The SMTP listener accepts mail from known internal clients only — see SMTP ingress. |
| Markdown body rendering | Templates are plain text. v2 may add markdown with explicit escaping. |
| GUI installer / first-run wizard | The TOML config is the source of truth; no UI in front of it. |
| Replacing transactional providers entirely (running our own outbound SMTP fleet) | That’s “build a postfix replacement” territory; out of scope forever. The point of Posthorn is to use your provider, not become one. |
| Batch send API | Deferred without a target version. The api-mode audience surveyed didn’t have a real workload for it; we’ll reconsider when an operator surfaces with concrete batch volume that round-trip overhead actually constrains. |