IBM Plex WOFF2 files were shipped without the SIL OFL 1.1 text, NOTICE
told modifiers to edit layout.html for a Source URL that lives in
legal.go, and workflow_dispatch took GITHUB_REF_NAME as the version so a
run from main would publish ghcr.io/...:main.
Closes P5 of docs/plans/code-review.md. CHANGELOG updated.
Co-Authored-By: Cursor Grok 4.6 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
- internal/web/auth/ratelimit_test.go: the login/setup limiter's ceiling,
per-address scope, window reset and the sweep that keeps finished
buckets out of memory.
- internal/web/auth/handlers_test.go: sign-in (session issued, refusals
that do not reveal which usernames exist, a lockout a correct password
cannot bypass, the setup hint before the first administrator) and the
one-time setup link (creates the first global administrator and then
closes, rejects a wrong or expired token, refuses credentials the panel
would not accept later, rate-limited).
- internal/web/handlers/authz_test.go: every global-only route answers a
domain administrator — and a request with no principal — with 404, with
a positive control so the table cannot pass on a handler that always
404s. This is the check that would have caught the send-log leak.
- test/e2e: CoreDNS pinned to 1.14.6 instead of latest; the level-1
failure message quoted RATE_LIMIT_MESSAGES_PER_IP=5 while the stand
sets 50.
docs/plans/code-review.md P4 checked off except the optional backup-boot
test; CHANGELOG updated. No production code changed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
The deliveries_rows template wraps status in badge spans, but the e2e gate still looked for bare td cells, so send_verify_dkim_and_status timed out even when mail was delivered and logged. Add a handler regression test so go test ./... catches this drift without Docker.
Co-authored-by: Cursor <cursoragent@cursor.com>
Invert level-2 semantics so domain limits apply to every client IP and
application limits with trusted IPs raise the ceiling above the domain
(still capped by level 1). Panel shows L1, validates maxima, and documents
the model on Settings.
Co-authored-by: Cursor <cursoragent@cursor.com>
Pin compose and local trial to ghcr.io/mixeme/selfpost:1.0.0, close the
CHANGELOG cut, and retire implementation-plan / v1.x-closure-plan.
Includes the post-cut startup fixes needed for a green release e2e gate:
root-owned TLS copies for postfix check, maillog_file_prefixes for /data,
hostname gate and traversable /data, panel /healthz before setup, and
setup-token / TempDir reclaim via docker exec.
Co-Authored-By: Composer <noreply@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Move the delivery log from the ephemeral /var/log to /data/log/mail.log so
the lines that resolve a queued send-log row survive a container recreate.
postlogd writes it as postfix, the panel reads it through the selfpost group
(dir 2750, file 0640, normalised every start); backups exclude log/.
Close the residual gap with a queue sweep: rows queued for over two minutes
whose id postqueue -p no longer lists are marked bounced. The sweep waits
until the tailer has read the log to its end and does nothing when the queue
cannot be listed, so a message in flight is never touched.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Two scrapes had drifted from the pages they read, and the suite has not
been run since either page changed.
applicationID still looked for an application as a table row
(<td class="code">login</td> ... /applications/N/mode). Applications became
a list of blocks in d35b309, so the lookup had been failing for several
commits, including the one currently deployed — this is stale test, not a
regression. It now anchors on the login heading and takes the id from the
first action posted under it, whichever that is, so reordering a block's
controls will not break it again.
The level-2 rate-limit check looked for the application's login among the
send-log rows. 997af18 took that column off the log — the log identifies a
message and names the application only on a row's own page — so the check
now filters the log by application instead. That is the same attribution
through a server-side WHERE app_login rather than a substring match on
rendered HTML.
Verified on selfpost.example.com: make e2e green, all subtests pass.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Codeberg is being retired as the project's public site, so every reference now
points at GitHub. That includes the Go module path (codeberg.org/mix/selfpost →
github.com/mixeme/selfpost): leaving an import path on a host that is going
away would break `go get` and `go install`, so this is not only a docs change.
Touches go.mod, test/e2e/go.mod, all imports, Makefile MODULE, the -ldflags
version stamp in build/Dockerfile and docs/development.md, the licence headers
in the SVG/HTML assets, and README (no more primary/mirror pair).
Comments no longer cite the archived specification. "spec 7.6.1", "spec 5.1"
and friends pointed into docs/archive/specification-v1.0.md, which is marked as
not a source of truth; each is now a reference to the live document that owns
the subject — architecture.md (with section), product.md, security.md or the
README. The review only asked for the 7.x refs (code-review.md § 4), but 4/5/6/
8/9 had the same defect, so they went too. Comments only, no behaviour change.
Also closes the remaining review items: architecture.md gained a Code layers
section with the layer diagram (A2), and TestParseDelivery gained the exotic
mail.log cases (§ 3).
Fixes a bug that last test found: the delivery-line pattern matched status=
greedily, taking the *last* occurrence on the line. Postfix appends the remote
server's reply verbatim, so a rejection whose reply quoted "status=sent" was
filed as a delivered message in the send log. It now takes the first status=
after the recipient, which is the real field.
R7 (CONTRIBUTING.md) moved to roadmap 2.x — one developer, no external PR flow,
so the file would have no audience yet. R1 (compose image tag) and the git tag
stay in roadmap § v1.x as the release-commit steps.
gofmt/go vet clean on both modules; go test ./... green except the three known
Windows-only failures (file perms, backslash paths, renaming an open file).
Not exercised on the dev server — no Docker locally.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The PTR check reported a correctly published record as wrong. The lookups
went through the container's resolver (127.0.0.11) which forwards to the
host's systemd-resolved, and systemd-resolved synthesises the reverse
lookup of the machine's own addresses from the local hostname rather than
asking public DNS. On the production host that meant
203.0.113.10 -> provider-assigned-hostname (does not match)
while public DNS has had 203.0.113.10 -> selfpost.example.com all along.
These checks exist to report what a receiving mail server sees, so they
now dial recursive resolvers themselves, defaulting to 1.1.1.1, 8.8.8.8
and 9.9.9.9 and overridable with SELFPOST_DNS_RESOLVERS. The e2e stand
sets it to its CoreDNS, which the `dns:` directive alone no longer covers.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
/sendlog -> /deliveries, /queue -> /mail-queue, /logtail -> /system-log,
along with the HTMX polling fragments under each.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Separate test/e2e Go module drives the shipped deploy/docker-compose.yml
(plus a test-only override: self-signed cert, low ports, isolated compose
project) against a fake DNS zone (CoreDNS) and an smtp-sink MX, exactly as
an administrator and their applications would over HTTP/SMTP — covering the
class of failure unit tests can't see (container wiring). Positive path:
setup -> login -> domain -> DKIM record published into the fake zone ->
application -> SMTP AUTH send -> DKIM verified against the DNS-published
key -> send-log queued->sent. Negative: no-AUTH/unauthenticated relay,
sender/login mismatch, L1 (anvil) and L2 (panel) rate limits, journal-milter
fail-open, SELFPOST_HOSTNAME gate, session survives docker restart.
release.yml moves off qemu to a native per-arch build (amd64/arm64), each
gated by this suite before its tag is pushed and merged into the version
manifest.
Verified green on selfpost.example.com via `make e2e`; go vet/gofmt clean in
both modules.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>