-
v0.1.0
StableTest / test (push) Successful in 2m1sRelease / gates (push) Successful in 1m57sRelease / build (amd64, freebsd) (push) Successful in 1m26sRelease / build (amd64, linux) (push) Successful in 1m30sRelease / build (arm64, freebsd) (push) Successful in 1m28sRelease / build (arm64, linux) (push) Successful in 1m49sRelease / build (loong64, linux) (push) Successful in 1m30sRelease / build (riscv64, linux) (push) Successful in 1m29sRelease / release (push) Successful in 41sreleased this
2026-09-28 22:33:07 +00:00 | 0 commits to main since this releaseAdded
- 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-partyinterpres
TOML parser. - Plain HTML form posts, no JavaScript required. Every endpoint accepts
application/x-www-form-urlencodedandmultipart/form-dataunder the
fixed field namesname,email,serviceandmessagebeside the JSON
contract, and a form withredirect_urlset answers every accepted
submission, honeypot hits included, with303 See Otherto that page.
Failures stay machine-readable JSON. Body-parse rejections report
invalid_bodyacross every content type. - Four form types:
contact,feedback,newsletterandgeneric. 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:
servicesputs an allow-list on the optionalservicepayload 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_nameand
require_messagemove 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 ofconfig.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 afterpending_ttl_seconds(72 hours by default), tokens are
256-bit random values stored as SHA-256 hashes, and replaying a used link
returns410 Gone. A repeat signup of an already recorded address gets the
same200 okresponse with no second mail and no duplicate record. - Abuse defence without CAPTCHA. A per-IP token bucket bounds each form
(capped atserver.rate_limit_max_bucketsdistinct 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 explicitserver.trust_proxy_headersopt-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 todata_dir/ratelimit-snapshot.json(atomic write,
mode0600) 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 = truewrites every
accepted submission todata_dir/archive-<name>.jsonlbefore 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 = truemails the submitter a short confirmation with the
owner's address asReply-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, optionaltimeout_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 /healthreports the listener and the form
count,GET /metricsreports lifetime counters per form and in total
(optionally guarded by aserver.metrics_tokenbearer credential),
--check-configvalidates the configuration file and exits nonzero on any
error (so a systemdExecStartPrecan refuse to start on a broken config),
and theNUNTIUS_CONFIGenvironment 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_secondsplussmtp.timeout_seconds. Every key is optional
with its default documented indocs/CONFIGURATION.md. - SMTP delivery with a TLS policy. PLAIN auth over STARTTLS, implicit TLS
on port465(encrypted from the first byte), thesmtp.require_tlsswitch
that aborts delivery when a STARTTLS port never upgrades, and a configurable
conversation timeout. Secrets stay out ofconfig.tomlthrough${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,SubjectorReply-Toare 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, formpath
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.
--versionprints the
tag the binary was built at, a pseudo-version naming the commit otherwise,
and(devel)outside version control, with+dirtyappended on a dirty
tree. Nothing is injected at build time. - Binaries for Linux and FreeBSD. Every release ships
linux/amd64,
linux/arm64,linux/loong64andlinux/riscv64, plusfreebsd/amd64and
freebsd/arm64as cross compiles; the FreeBSD binaries are runtime
untested.
Downloads
- A contact form backend in one static binary. nuntius serves JSON form