210 lines
8.1 KiB
Markdown
210 lines
8.1 KiB
Markdown
# Nuntius — A Small, Opinionated Contact Form Backend
|
|
|
|
[](LICENSE)
|
|
[](https://go.dev)
|
|
[](https://kernel.org)
|
|
[](https://freebsd.org)
|
|
|
|
> Latin *nuntius* = "messenger" — the service that carries messages from visitors to you.
|
|
|
|
**nuntius** is a small, modular contact form backend written in Go. It serves multiple JSON endpoints (contact, feedback, newsletter, generic) over a single static binary, delivers submissions via SMTP, optionally persists newsletter signups to a JSONL log, and is designed to sit behind nginx on a tiny Linux server. Only the first-party `interpres` TOML module, no external third-party Go modules, ~6 MB binary.
|
|
|
|
## Features
|
|
|
|
- **Single static binary** — `go build -ldflags="-s -w"` produces ≈ 6 MB; no Node, no Python, no Redis, no Postgres, no third-party Go packages.
|
|
- **Four form kinds out of the box** — `contact`, `feedback`, `newsletter`, and `generic`. Each gets its own endpoint, rate limit, CORS allowlist, and honeypot field.
|
|
- **SMTP delivery** — Go stdlib `net/smtp` with `PLAIN` auth. Works with any provider that exposes SMTP.
|
|
- **Newsletter subscribers on disk** — append-only JSONL log under `data_dir/`. No database, no double opt-in. One `{email, ip, form, created_at}` line per signup.
|
|
- **Server-side validation** — `pkg/contactform.Validate` is reusable; name, email (RFC 5322), service allow-list (for `contact`), and message length.
|
|
- **Reusable package** — `pkg/contactform` exports typed `Request` / `Response` / `ErrorResponse` plus a `Validate` function you can drop into any Go project.
|
|
- **Structured logging** — `log/slog` with JSON output to stdout; plays well with journald, Loki, Datadog…
|
|
- **Graceful shutdown** — SIGINT/SIGTERM close in-flight requests within 15 s.
|
|
- **Rate limiting** — token-bucket per IP, configurable per hour, in-memory (no Redis required).
|
|
- **Honeypot field** — invisible to humans, required for spam bots. Silently accepts and drops.
|
|
- **Config validation** — unknown fields and unknown form types are rejected at startup, so typos fail loudly instead of silently disabling a form.
|
|
- **Env-var secrets** — `${VAR_NAME}` / `$VAR_NAME` expansion in the TOML file keeps SMTP passwords out of disk.
|
|
|
|
## Quick Start
|
|
|
|
```bash
|
|
# 1. Clone
|
|
git clone https://sourcedock.dev/petrbalvin/nuntius.git
|
|
cd nuntius
|
|
|
|
# 2. Build
|
|
just build
|
|
# → ./bin/nuntius
|
|
|
|
# 3. Run (auto-creates /etc/nuntius/config.toml with a 3-form template on first start)
|
|
sudo mkdir -p /etc/nuntius
|
|
just run
|
|
# → "nuntius starting" addr=:8080 forms=3
|
|
|
|
# 4. Edit the generated config to point at your SMTP and recipient
|
|
sudo $EDITOR /etc/nuntius/config.toml
|
|
just run # restart to pick up changes
|
|
```
|
|
|
|
A default `config.toml` is auto-generated at `/etc/nuntius/config.toml` on first run with three sample forms (contact, feedback, newsletter). Edit it before exposing the service.
|
|
|
|
The full schema lives in [`docs/configuration.md`](docs/configuration.md); the deploy guide is in [`docs/deployment.md`](docs/deployment.md).
|
|
|
|
## Usage
|
|
|
|
### Test the endpoints
|
|
|
|
```bash
|
|
# Contact form
|
|
curl -X POST http://localhost:8080/api/nuntius/contact \
|
|
-H "Content-Type: application/json" \
|
|
-H "Origin: https://petrbalvin.org" \
|
|
-d '{
|
|
"name": "Jane Doe",
|
|
"email": "jane@example.com",
|
|
"service": "architecture",
|
|
"message": "Hello, I would like to discuss an engagement."
|
|
}'
|
|
|
|
# Feedback (no service field)
|
|
curl -X POST http://localhost:8080/api/nuntius/feedback \
|
|
-H "Content-Type: application/json" \
|
|
-H "Origin: https://petrbalvin.org" \
|
|
-d '{
|
|
"name": "Jane Doe",
|
|
"email": "jane@example.com",
|
|
"message": "I love how the site uses Vue.js with zero runtime deps."
|
|
}'
|
|
|
|
# Newsletter (email only)
|
|
curl -X POST http://localhost:8080/api/nuntius/newsletter \
|
|
-H "Content-Type: application/json" \
|
|
-H "Origin: https://petrbalvin.org" \
|
|
-d '{ "email": "jane@example.com" }'
|
|
```
|
|
|
|
Successful response (all three):
|
|
|
|
```json
|
|
{ "ok": true }
|
|
```
|
|
|
|
Validation error (e.g. newsletter with bad email):
|
|
|
|
```json
|
|
{
|
|
"error": "validation",
|
|
"details": [
|
|
{ "field": "email", "message": "email is invalid" }
|
|
]
|
|
}
|
|
```
|
|
|
|
### Frontend integration
|
|
|
|
If you proxy `/api/nuntius/*` through nginx (recommended, see [`docs/deployment.md`](docs/deployment.md)), no frontend config is needed — the form posts to the same origin as the page. Otherwise, set `VITE_NUNTIUS_URL` to the upstream origin:
|
|
|
|
```js
|
|
await fetch(`${import.meta.env.VITE_NUNTIUS_URL || ''}/api/nuntius/contact`, {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
name: name.value,
|
|
email: email.value,
|
|
service: service.value,
|
|
message: message.value,
|
|
}),
|
|
});
|
|
```
|
|
|
|
### Use the package in your own project
|
|
|
|
`pkg/contactform` is a small, dependency-free Go package that you can import into any project:
|
|
|
|
```go
|
|
import "sourcedock.dev/petrbalvin/nuntius/pkg/contactform"
|
|
|
|
req := &contactform.Request{
|
|
Name: "Jane",
|
|
Email: "jane@example.com",
|
|
Service: "architecture",
|
|
Message: "Hello!",
|
|
}
|
|
|
|
if errs := contactform.Validate(req, "contact"); len(errs) > 0 {
|
|
// handle validation errors
|
|
}
|
|
```
|
|
|
|
Full library example: [`docs/library-usage.md`](docs/library-usage.md). Full HTTP API: [`docs/api-reference.md`](docs/api-reference.md).
|
|
|
|
## Configuration (TL;DR)
|
|
|
|
Default path: `/etc/nuntius/config.toml`. A starter file is written automatically on first start.
|
|
|
|
```toml
|
|
data_dir = "./data"
|
|
|
|
[server]
|
|
port = 8080
|
|
|
|
[[forms]]
|
|
name = "contact"
|
|
type = "contact"
|
|
path = "/api/nuntius/contact"
|
|
to = "you@example.com"
|
|
from = "noreply@example.com"
|
|
rate_limit_per_hour = 10
|
|
honeypot_field = "website"
|
|
allowed_origins = ["https://example.com"]
|
|
|
|
[forms.smtp]
|
|
host = "smtp.example.com"
|
|
port = 587
|
|
user = "noreply@example.com"
|
|
password = "${NUNTIUS_SMTP_PASSWORD}"
|
|
```
|
|
|
|
Form types and full schema: [`docs/configuration.md`](docs/configuration.md). Secrets via env vars, CORS, honeypot, rate limit, and validation rules are all documented there.
|
|
|
|
## Requirements
|
|
|
|
- **Go 1.26+** (uses Go 1.22+ `http.ServeMux` method patterns and `log/slog`)
|
|
- An **SMTP** account (Yandex, Mailgun, Postmark, your own Postfix, …)
|
|
- **No other runtime dependencies.** No Node, no Python, no Redis, no Postgres. Configuration is TOML parsed via the first-party [`interpres`](https://sourcedock.dev/petrbalvin/interpres) library; newsletter storage is an append-only JSONL file via `bufio`.
|
|
|
|
## Development
|
|
|
|
```bash
|
|
just install # go mod tidy + go mod download (no-op for this project, kept for symmetry)
|
|
just build # → ./bin/nuntius (with -s -w)
|
|
just test # go test ./...
|
|
just run # go run ./cmd/server
|
|
just uninstall # rm -rf ./bin
|
|
```
|
|
|
|
All four `just` recipes are mandatory per the project conventions and produce zero errors and zero warnings.
|
|
|
|
## Documentation
|
|
|
|
- [`docs/architecture.md`](docs/architecture.md) — layers, request flow, design decisions
|
|
- [`docs/configuration.md`](docs/configuration.md) — full config schema, form types, secrets, newsletter log
|
|
- [`docs/api-reference.md`](docs/api-reference.md) — HTTP verbs, status codes, `GET /health`
|
|
- [`docs/deployment.md`](docs/deployment.md) — production systemd, nginx, updates
|
|
- [`docs/security.md`](docs/security.md) — threat model, CORS, rate limiting, secret handling
|
|
- [`docs/library-usage.md`](docs/library-usage.md) — `pkg/contactform` API and standalone use
|
|
|
|
## Contributing
|
|
|
|
See [`CONTRIBUTING.md`](CONTRIBUTING.md) for development setup, code style, commit conventions, and the pull request flow.
|
|
|
|
## Security
|
|
|
|
For a full threat model, see [`docs/security.md`](docs/security.md). The short version: every form has its own per-IP rate limit, CORS allowlist, and honeypot field; SMTP credentials never touch the log; unknown config fields are rejected at startup.
|
|
|
|
If you find a security issue, please email **opensource@petrbalvin.org** rather than opening a public issue.
|
|
|
|
## License
|
|
|
|
MIT — see [LICENSE](./LICENSE).
|
|
Copyright © 2026 [Petr Balvín](https://petrbalvin.org)
|