FMG-2: upgrade Go to 1.27.1, refresh dependencies and drop gotoolkit #8
2026-09-07
fix: close the go-smtp drain race and pin the shutdown budget by behaviour
All checks were successful
go-smtp registers each accepted session on a WaitGroup that Shutdown waits on from another goroutine, so an Accept still in flight when the drain begins is an Add concurrent with that Wait. Under -race, which is how make test runs, the suite failed 3/20 locally. Stop accepting and let Serve return before draining, which removes the overlap entirely: 0/25 with no data race reported. It also means a session accepted in that instant is now actually waited for. Closing the listener first makes Serve return net.ErrClosed and makes Shutdown's own close fail the same way, so both are filtered — without that, every clean shutdown logged a spurious "still in flight" warning. The timeout half of the earlier fix was pinned only by a constant assertion: a budget one millisecond over readTimeout, or reverting the warn back to a returned error, both left the whole suite green. The budget is now a Server field so a test can drive a real overrun, and TestServerReportsNoErrorWhenDrainOverrunsBudget asserts Start returns nil on it; the constant assertion now requires a 10s margin rather than a strict inequality. Both reverts fail these tests. Also: - Warn about config keys the decode ignored, instead of dropping them silently. A mistyped [[Recipient]] alongside a valid [CatchAll] used to start with recipients=0 and no signal at all. Still not fatal, so configurations carrying a stray key keep working. - Skip targets that parse but carry no scheme; url.Parse accepts almost any string, so "usable target" did not previously mean much. - Release the listener when Serve exits on its own, rather than leaking it to a library caller. - Document that a Server is single use. - Wait for the bind rather than a probe connection in tests, and report a Start failure instead of letting it look like a slow bind. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai>
fix: address review findings on the server lifecycle and config loading
All checks were successful
Start bound the listener inside ListenAndServe on a goroutine, so a stop that won the race found go-smtp's listener list still empty, closed nothing, and left Accept running forever — a SIGTERM during startup needed SIGKILL. Bind synchronously and serve the listener we own, and close it from Stop so a direct caller gets the same guarantee. shutdownTimeout equalled ReadTimeout, so draining an idle client (which only drops once its read deadline expires) raced its own budget and surfaced an intentional stop as "context deadline exceeded", which main turned into a non-zero exit. Give the drain room and log an overrun instead of failing. Also: - Validate config after decoding. TOML ignores unknown keys, so [[Recipient]] or Adresses previously started a server that answered 250 OK and forwarded nothing. - Drop cmd/sendmail's hand-rolled PLAIN client; net/smtp.PlainAuth already permits plaintext to a loopback host, as backend_test.go relies on. - Release the signal handler once the first signal lands, so a second Ctrl-C aborts a slow drain. - go.mod: express the language version, not a patch pin, so consumers on go1.27.0 with GOTOOLCHAIN=local still build. - Makefile: install goreleaser like golangci-lint, so check/build work on a clean checkout. - Tests: ephemeral ports throughout, no require inside an Eventually condition, and regression coverage for both lifecycle bugs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: multica-agent <github@multica.ai>
ci: use the goreleaser from the CI image instead of goreleaser-action
goreleaser-action v7.1.0 made cosign signature verification of its own download mandatory, and ci-base ships no cosign — the goreleaser-lint job failed on the v6.4.0 -> v7.2.3 bump. The action was redundant anyway: ci-base already installs goreleaser from the upstream apt repo, which is what `make build` has always used. Both workflows now call it directly, and `goreleaser check` moves behind a `make check` target so every CI job runs a make target. Co-authored-by: multica-agent <github@multica.ai> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
deps: upgrade Go to 1.27.1, refresh dependencies and drop gotoolkit
Replaces the gotoolkit helpers with the standard library and the libraries they wrapped: - gotoolkit/encoding TOML wrapper -> pelletier/go-toml/v2 directly, behind a new smtp2shoutrrr.LoadConfig shared by both commands. - gotoolkit/service + gotoolkit/model.Server -> signal.NotifyContext and a concrete *Server whose Start shuts the listener down gracefully when its context is cancelled. Also drops the unused ANONYMOUS SASL client from cmd/sendmail, bumps alpine, golangci-lint and the CI actions, and adds coverage for config loading and the server lifecycle. Closes FMG-2 Co-authored-by: multica-agent <github@multica.ai> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>