feat: contact form backend for linux and freebsd servers
Test / test (push) Successful in 2m1s
Release / gates (push) Successful in 1m57s
Release / build (amd64, freebsd) (push) Successful in 1m26s
Release / build (amd64, linux) (push) Successful in 1m30s
Release / build (arm64, freebsd) (push) Successful in 1m28s
Release / build (arm64, linux) (push) Successful in 1m49s
Release / build (loong64, linux) (push) Successful in 1m30s
Release / build (riscv64, linux) (push) Successful in 1m29s
Release / release (push) Successful in 41s

Assisted-by: GLM 5.3 Flash
This commit is contained in:
2026-09-29 00:32:56 +02:00
commit 3a38f00dc0
49 changed files with 10769 additions and 0 deletions
+145
View File
@@ -0,0 +1,145 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
//go:build linux || freebsd
// Command nuntius runs the contact form backend server.
//
// Configuration is loaded from a TOML file (default: /etc/nuntius/config.toml).
// The TOML file may reference environment variables for secrets using
// ${VAR_NAME} or $VAR_NAME syntax.
package main
import (
"context"
"errors"
"flag"
"fmt"
"log/slog"
"net"
"net/http"
"os"
"os/signal"
"strconv"
"syscall"
"time"
"sourcedock.dev/petrbalvin/nuntius/internal/config"
"sourcedock.dev/petrbalvin/nuntius/internal/handler"
"sourcedock.dev/petrbalvin/nuntius/internal/version"
)
func main() {
showVersion := flag.Bool("version", false, "Print version and exit")
checkConfig := flag.Bool("check-config", false, "Validate the configuration file and exit without listening")
flag.Parse()
if *showVersion {
fmt.Printf("%s %s\n", version.Name, version.Version())
return
}
logger := slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{Level: slog.LevelInfo}))
slog.SetDefault(logger)
cfgPath := config.ConfigPath()
if *checkConfig {
if _, err := config.Load(cfgPath); err != nil {
fmt.Fprintf(os.Stderr, "nuntius: configuration %s is invalid: %v\n", cfgPath, err)
os.Exit(1)
}
fmt.Printf("configuration OK: %s\n", cfgPath)
return
}
cfg, err := config.Load(cfgPath)
if err != nil {
logger.Error("config load failed", "path", cfgPath, "err", err)
os.Exit(1)
}
h := handler.New(cfg)
mux := http.NewServeMux()
h.Register(mux)
srv := &http.Server{
// The bind host and every timeout are configuration: an omitted
// key resolves to the value this release has always used, so the
// zero-config behaviour is unchanged.
Addr: net.JoinHostPort(cfg.Server.Bind, strconv.Itoa(cfg.Server.Port)),
Handler: withRequestLog(mux, logger, cfg.Server.TrustProxyHeaders),
ReadHeaderTimeout: cfg.Server.ReadHeaderTimeout(),
ReadTimeout: cfg.Server.ReadTimeout(),
WriteTimeout: cfg.Server.WriteTimeout(),
IdleTimeout: cfg.Server.IdleTimeout(),
}
logger.Info("nuntius starting",
"version", version.Name+" "+version.Version(),
"addr", srv.Addr,
"config", cfgPath,
"forms", len(cfg.Forms),
)
for _, f := range cfg.Forms {
logger.Info("form registered",
"name", f.Name,
"path", f.Path,
"to", f.To,
"smtp", f.SMTP.Host,
)
}
// Graceful shutdown.
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
go func() {
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
logger.Error("server error", "err", err)
os.Exit(1)
}
}()
<-ctx.Done()
logger.Info("shutdown signal received")
shutdownCtx, cancel := context.WithTimeout(context.Background(), cfg.Server.ShutdownTimeout())
defer cancel()
if err := srv.Shutdown(shutdownCtx); err != nil {
logger.Error("shutdown error", "err", err)
h.PersistState()
os.Exit(1)
}
h.PersistState()
h.Close()
logger.Info("nuntius stopped cleanly")
}
// withRequestLog logs each HTTP request method, path, status, and duration.
// The logged IP follows the same trust rules as rate limiting: proxy
// headers only when cfg says a trusted proxy is in front.
func withRequestLog(next http.Handler, logger *slog.Logger, trustProxy bool) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
rw := &statusRecorder{ResponseWriter: w, status: http.StatusOK}
next.ServeHTTP(rw, r)
logger.Info("request",
"method", r.Method,
"path", r.URL.Path,
"status", rw.status,
"duration_ms", time.Since(start).Milliseconds(),
"ip", handler.ClientIP(r, trustProxy),
)
})
}
type statusRecorder struct {
http.ResponseWriter
status int
}
func (r *statusRecorder) WriteHeader(code int) {
r.status = code
r.ResponseWriter.WriteHeader(code)
}
+52
View File
@@ -0,0 +1,52 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: MIT
//go:build linux || freebsd
package main
import (
"io"
"log/slog"
"net/http"
"net/http/httptest"
"testing"
)
func TestStatusRecorder(t *testing.T) {
rec := httptest.NewRecorder()
sr := &statusRecorder{ResponseWriter: rec, status: http.StatusOK}
// Default status before WriteHeader.
if sr.status != http.StatusOK {
t.Errorf("initial status = %d, want %d", sr.status, http.StatusOK)
}
sr.WriteHeader(http.StatusNotFound)
if sr.status != http.StatusNotFound {
t.Errorf("status after WriteHeader = %d, want %d", sr.status, http.StatusNotFound)
}
// rec should also have the status set.
if rec.Code != http.StatusNotFound {
t.Errorf("rec.Code = %d, want %d", rec.Code, http.StatusNotFound)
}
}
func TestWithRequestLog(t *testing.T) {
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
logger := slog.New(slog.NewTextHandler(io.Discard, nil))
wrapped := withRequestLog(handler, logger, false)
req := httptest.NewRequest(http.MethodGet, "/test", nil)
req.RemoteAddr = "192.168.1.1:12345"
rec := httptest.NewRecorder()
wrapped.ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("status = %d, want 200", rec.Code)
}
}