Files
nuntius/CHANGELOG.md
T

114 lines
6.8 KiB
Markdown
Raw Normal View History

# Changelog
All notable changes to **nuntius** are documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
2026-09-29 00:52:01 +02:00
## [development]
### Added
-
## [0.1.0] - 2026-09-29
### Added
- **A contact form backend in one static binary.** nuntius serves JSON form
endpoints, delivers the submissions by SMTP and writes structured JSON logs.
No Node, no Python, no Redis, no Postgres, and no third-party Go module
beyond the first-party [`interpres`](https://sourcedock.dev/petrbalvin/interpres)
TOML parser.
- **Plain HTML form posts, no JavaScript required.** Every endpoint accepts
`application/x-www-form-urlencoded` and `multipart/form-data` under the
fixed field names `name`, `email`, `service` and `message` beside the JSON
contract, and a form with `redirect_url` set answers every accepted
submission, honeypot hits included, with `303 See Other` to that page.
Failures stay machine-readable JSON. Body-parse rejections report
`invalid_body` across every content type.
- **Four form types**: `contact`, `feedback`, `newsletter` and `generic`. Each
form has its own endpoint, rate limit, CORS allowlist, honeypot field and
SMTP credentials, so one process serves many independent forms.
- **Declarative validation policy.** A form's rules are configuration:
`services` puts an allow-list on the optional `service` payload field of any
form type (the empty value always passes, the `*` entry accepts any value,
an explicit empty list accepts only the empty value), `require_name` and
`require_message` move the free-text fields in and out of the checks, and
`min_name_runes`, `max_name_runes`, `min_message_runes`, `max_message_runes`
size them. A new form shape is a matter of `config.toml`, never of Go code.
- **Newsletter double opt-in.** A signup stays pending and the subscriber
receives a confirmation mail with a single-use link
(`GET <form path>/confirm?token=...`); only a redeemed link writes the
address into the JSONL log and notifies the owner. Unconfirmed entries
expire after `pending_ttl_seconds` (72 hours by default), tokens are
256-bit random values stored as SHA-256 hashes, and replaying a used link
returns `410 Gone`. A repeat signup of an already recorded address gets the
same `200 ok` response with no second mail and no duplicate record.
- **Abuse defence without CAPTCHA.** A per-IP token bucket bounds each form
(capped at `server.rate_limit_max_buckets` distinct IPs, unknown IPs denied
while full), a configurable honeypot field silently drops bot submissions,
per-form CORS closes cross-site posts, and proxy headers are trusted only
with the explicit `server.trust_proxy_headers` opt-in, so a direct caller
cannot rotate identities to dodge the per-IP limit.
- **Rate-limit state survives graceful restarts.** On shutdown the per-IP
buckets are snapshotted to `data_dir/ratelimit-snapshot.json` (atomic write,
mode `0600`) and restored at startup; stale entries are dropped and a
missing or corrupt file behaves like an empty one.
- **Optional submission archive.** A form with `archive = true` writes every
accepted submission to `data_dir/archive-<name>.jsonl` before the mail is
attempted, so a failed SMTP round-trip loses nothing; a failed append
blocks the send, so a retry cannot split the mail from its record.
Newsletter forms reject the key because they persist through the double
opt-in log already.
- **Optional automated receipt to the submitter.** A form with
`auto_reply = true` mails the submitter a short confirmation with the
owner's address as `Reply-To`; the receipt is best effort and its failure
never turns an accepted submission into an error. Newsletter forms reject
the key because their subscribers already receive the confirmation mail.
- **Telegram notifications.** A per-form `[forms.telegram]` channel
(`bot_token`, `chat_id`, optional `timeout_seconds`) posts the submission
summary into the owner's chat beside the mail. The submission counts as
delivered when either channel gets through: an SMTP outage does not
silence the bell, and only when both fail does the caller see
`send_failed`. The notification is one-way; newsletter forms reject the
key because their double opt-in flow is mail-native.
- **Operational surface.** `GET /health` reports the listener and the form
count, `GET /metrics` reports lifetime counters per form and in total
(optionally guarded by a `server.metrics_token` bearer credential),
`--check-config` validates the configuration file and exits nonzero on any
error (so a systemd `ExecStartPre` can refuse to start on a broken config),
and the `NUNTIUS_CONFIG` environment variable moves the configuration file
out of `/etc/nuntius/` for development.
- **Every operational limit is a configuration key.** `server.bind`,
`server.read_header_timeout_seconds`, `server.read_timeout_seconds`,
`server.write_timeout_seconds`, `server.idle_timeout_seconds`,
`server.shutdown_timeout_seconds`, `server.max_body_bytes`,
`server.rate_limit_max_buckets`, `server.rate_limit_cleanup_seconds`,
`server.rate_limit_max_bucket_age_seconds`, and per form
`pending_ttl_seconds` plus `smtp.timeout_seconds`. Every key is optional
with its default documented in `docs/CONFIGURATION.md`.
- **SMTP delivery with a TLS policy.** PLAIN auth over STARTTLS, implicit TLS
on port `465` (encrypted from the first byte), the `smtp.require_tls` switch
that aborts delivery when a STARTTLS port never upgrades, and a configurable
conversation timeout. Secrets stay out of `config.toml` through `${VAR}`
expansion, and an undefined variable aborts startup instead of surfacing
later as a failed SMTP auth.
- **Messages are hardened at composition.** CR/LF sequences reaching `From`,
`To`, `Subject` or `Reply-To` are replaced with spaces inside the SMTP
builder itself, and the MIME boundary is random per message with a failure
aborting the send rather than falling back to a fixed boundary.
- **Configuration fails loudly at startup.** Unknown TOML fields and unknown
form types are rejected, ports and limits are range-checked, form `path`
values are validated as static route patterns so a mistyped path cannot
widen into a wildcard, and form names are restricted to `[a-zA-Z0-9_-]` so
a crafted name cannot escape the data directory.
- **The release identity comes from the toolchain.** `--version` prints the
tag the binary was built at, a pseudo-version naming the commit otherwise,
and `(devel)` outside version control, with `+dirty` appended on a dirty
tree. Nothing is injected at build time.
- **Binaries for Linux and FreeBSD.** Every release ships `linux/amd64`,
`linux/arm64`, `linux/loong64` and `linux/riscv64`, plus `freebsd/amd64` and
`freebsd/arm64` as cross compiles; the FreeBSD binaries are runtime
untested.