Files
nuntius/README.md
T

171 lines
6.8 KiB
Markdown
Raw Permalink Normal View History

# Nuntius
A small contact form backend for Linux and FreeBSD servers, made to run
behind [Caddy](https://caddyserver.com/) on a modest server: Caddy ends the
TLS and serves the form from the same origin, nuntius carries the messages.
Latin *nuntius* means "messenger": the service carries messages from web
visitors to your inbox. It serves multiple JSON form endpoints (contact,
feedback, newsletter, generic) from a single static binary, delivers
submissions via SMTP, and optionally persists newsletter signups to an
append-only JSONL log.
## Features
- **Single static binary**: roughly 6 MB stripped; 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.
- **Four form kinds**: `contact`, `feedback`, `newsletter`, and `generic`.
Each gets its own endpoint, rate limit, CORS allowlist, and honeypot field.
- **No-JavaScript forms**: endpoints also accept plain `urlencoded` and
`multipart` posts and can answer `303 See Other` to a thank-you page, so an
ordinary HTML `<form>` works with no script at all.
- **SMTP delivery**: Go stdlib `net/smtp` with `PLAIN` auth over STARTTLS
(upgraded automatically when the server advertises it, credentials never
sent in plaintext), implicit TLS on port `465`, and an optional
`require_tls` policy that aborts delivery against servers without TLS.
- **Newsletter double opt-in**: a signup stays pending until the subscriber
clicks the confirmation link mailed to them; only confirmed addresses land
in the JSONL log, unconfirmed ones expire, and repeat signups are silently
skipped.
- **Optional submission archive**: a per-form switch logs every accepted
submission to an append-only JSONL file before the mail goes out, so a
failed SMTP round-trip loses nothing.
- **Optional submitter receipt**: `auto_reply` mails the sender a short
automated acknowledgement with the owner's address as `Reply-To`.
- **Telegram notifications**: a per-form `[forms.telegram]` channel posts the
summary into your chat beside the mail; the submission counts as delivered
when either channel gets through, so an SMTP outage does not silence the
bell.
- **Server-side validation**: every field is checked before anything is
delivered: name, email (RFC 5322), an allow-listed optional `service`
field, and message length. Every limit is a configuration key, and the
server builds each form's policy from `config.toml`.
- **Token-bucket rate limiting**: per IP, per form, configurable submissions
per hour, capped in memory, persisted across graceful restarts.
- **Honeypot field**: invisible to humans, required by bots. Silently accepts
and drops spam submissions.
- **CORS allowlists**: per form, explicit origins only; disallowed origins
receive `403 Forbidden`.
- **Bounded requests**: every timeout, the body cap and the graceful shutdown
deadline are configuration keys; oversized bodies get `413`.
- **Operational endpoints**: `GET /health` and `GET /metrics` with lifetime
counters per form, optionally guarded by a bearer token.
- **Structured logging**: `log/slog` with JSON output to stdout; credentials
are never logged.
- **TOML configuration**: single file, parsed by the first-party `interpres`
library. Unknown fields and unknown form types are rejected at startup so
typos fail loudly.
- **Environment variable expansion**: `${VAR_NAME}` and `$VAR_NAME` in the
TOML file keep SMTP credentials out of version control.
- **IPv6-first**: binds to `[::]` by default, with automatic IPv4
compatibility via dual-stack sockets.
- **Cross-platform**: pre-built binaries for Linux (amd64, arm64, loong64,
riscv64) and FreeBSD (amd64, arm64) on every release. The FreeBSD binaries
ship as cross compiles and are runtime untested.
## Install
Prebuilt binaries for Linux and FreeBSD are on the
[releases page](https://sourcedock.dev/petrbalvin/nuntius/releases). From
source:
```sh
git clone https://sourcedock.dev/petrbalvin/nuntius.git
cd nuntius
just build # produces bin/nuntius
```
## Quick start
```sh
# 1. Point the server at a writable config path and satisfy the secret
# referenced by the starter template.
export NUNTIUS_CONFIG=./config.toml
export NUNTIUS_SMTP_PASSWORD=admin
# 2. Build and run; the first start writes a three-form starter config.
just build
just run # listens on [::]:8080
# 3. Verify.
curl http://127.0.0.1:8080/health
```
Edit the generated `./config.toml` with real SMTP credentials and recipient
addresses before exposing the service. In production the default location is
`/etc/nuntius/config.toml`; see [`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md)
for the full setup. The full schema lives in
[`docs/CONFIGURATION.md`](docs/CONFIGURATION.md).
## Usage
Run the server:
```sh
export NUNTIUS_CONFIG=./config.toml # omitted: /etc/nuntius/config.toml
bin/nuntius # serves until SIGINT or SIGTERM
```
Validate a configuration without listening, as a systemd `ExecStartPre`
would:
```sh
NUNTIUS_CONFIG=./config.toml bin/nuntius --check-config
```
Post a submission:
```sh
curl -X POST http://127.0.0.1:8080/api/nuntius/contact \
-H "Content-Type: application/json" \
-H "Origin: https://example.com" \
-d '{"name":"Jane Doe","email":"jane@example.com","message":"Hello"}'
# → 200 {"ok": true}; the mail lands in the form's recipient address
```
A plain HTML form posts the same fields urlencoded and, with `redirect_url`
set, receives `303 See Other` to its thank-you page. The complete endpoint
reference is [`docs/API.md`](docs/API.md); the flags and exit codes are in
[`docs/CLI.md`](docs/CLI.md).
Nuntius speaks plain HTTP by design: put Caddy in front of it for TLS, a
same-origin form endpoint and client IPs you can trust. Two lines in the
Caddyfile are enough:
```caddyfile
handle_path /api/nuntius/* {
reverse_proxy 127.0.0.1:8080
}
```
Caddy overwrites `X-Forwarded-For` for untrusted clients, so with
`server.trust_proxy_headers = true` the rate limiter and the logs see the
real visitor. The full production setup, including the systemd unit, is in
[`docs/DEPLOYMENT.md`](docs/DEPLOYMENT.md).
## Development
```sh
just build # build
just test # the test suite with the coverage floor
just fmt # format
```
See [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) for the full workflow, and
[CONTRIBUTING.md](CONTRIBUTING.md) for how to contribute.
## Documentation
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md): components and data flow
- [docs/API.md](docs/API.md): the API reference
- [docs/CONFIGURATION.md](docs/CONFIGURATION.md): every configuration key
- [docs/CLI.md](docs/CLI.md): flags, exit codes, and the manual page
- [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md): production setup, Caddy, updates
- [SECURITY.md](SECURITY.md): how to report a vulnerability
## Licence
MIT, see [LICENSE](LICENSE).
Copyright © 2026 [Petr Balvín](https://petrbalvin.org)