Phase 13. Two new packages and one new screen.
internal/health owns the shared status vocabulary (ok/warn/error/unknown)
and the local checks: supervisord's process table, TLS certificate expiry
and the two milter sockets. Each check reports a problem as a status rather
than an error, so one broken component costs a line and not the page.
internal/dnscheck does the read-only lookups: forward-confirmed reverse DNS
for SELFPOST_HOSTNAME, and per-domain DKIM (compared against the key this
server actually signs with), SPF and DMARC. Every check is bounded by a
timeout and cached, and the resolver sits behind an interface so the tests
drive every branch without touching the network. The SPF check is
deliberately shallow: it looks for a mechanism literally covering the
server's address and does not follow include:/redirect=, so a record that
authorises us through an include is reported as "cannot tell" rather than
as a failure.
/status renders both, with the local checks in an HTMX-polled fragment and
the DNS lookups behind a Re-check button, and becomes the panel's landing
page: / now redirects there and the domain list lives at /domains. The
Reload button moves onto /status, where it reads as what it is — a
drift-recovery for the daemons — with text explaining what it regenerates.
A template test fails on any remaining href="/" so a stale link cannot
silently land on the wrong screen.
Also fixes a defect this made visible: the panel could never read the mail
queue in the documented deployment. postqueue relies on its setgid-postdrop
bit, which the compose file's no-new-privileges disables, so the Queue
screen always said "Could not read the mail queue" — including in the
released 1.0.0 image. The panel user is now a real member of postdrop,
which needs no setgid transition.
Verified in a container on the dev server against real DNS: PTR matching
(selfpost.mixfed.ru) and not matching (mixfed.ru), DKIM absent and
mismatched, SPF absent and via include:, DMARC p=quarantine/p=reject/absent,
and a resolver timeout degrading to "unknown" without hanging the page.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Phase 12 (UI/UX). The navigation bar now renders once from layout.html
instead of being copied into each content template, so it is present on
every authenticated page — including the domain page and its delete
confirmation, which had no links at all — and the current page is
highlighted via .Active rather than quietly dropping out of the list.
New /account page changes the administrator's username and/or password:
the current password is required and the attempt is throttled on the same
limiter as the login form, so this route cannot be used to brute-force
past that limit. A password change invalidates every other session while
keeping the one performing it; a rename carries that session over.
Backup and domain import move from a card in the middle of the domain
list to their own /backup page, one card each; the handlers themselves
are unchanged, only the page the import form renders its errors on.
The domain page gains a "Sending server settings" card (server, port,
encryption) so a client can be configured without reading the docs; 587
is listed only when SUBMISSION_ENABLE is true for this deployment, which
is a deploy-time flag the panel cannot verify at runtime.
Client-side (static/panel.js, no libraries): Copy buttons on the values
that get carried elsewhere (DKIM record, new application credentials,
server name), and the Addresses field is hidden while the address mode is
wildcard, where the server ignores it.
Verified in a container on the dev server: setup, login, every page's
nav and active item, domain and application creation, all account-form
paths including cross-session invalidation, import errors, full backup
download. gofmt/vet/test/docker build green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three HTMX-polled monitoring screens (spec 7.2.11-13): send log with
server-side domain/application filters and pagination, Postfix queue
(postqueue -p), and a mail.log tail. Fragment endpoints return HTML
snippets, not JSON (spec 7.1); all output is auto-escaped via
html/template (spec 7.6.7).
Adds store.QuerySendLog/CountSendLog/ListApplicationLogins,
postfix.Queue(), and logtail.TailLines (a point-in-time reverse read,
independent of the background follow loop). Verified on the dev server:
gofmt/vet/test green, docker build green, container e2e (filters,
60-row pagination, <script> escaping, real postqueue/mail.log output,
existing Reload button unaffected).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add/list/delete of sending domains with per-domain DKIM keys and the
OpenDKIM tables that drive signing (spec 6, 7.2.2-4, 7.2.10).
internal/domain:
- Pure-Go RSA-2048 keygen; PKCS#1 PEM written atomically at 0640; the
published DNS TXT record is derived from the key on disk (single source
of truth) rather than persisted. No os/exec for key generation.
- KeyTable/SigningTable fully regenerated from the registry on every
add/delete (idempotent), written atomically; SigningTable via refile:
with *@domain, KeyTable with absolute key paths. Table writer refuses
any unsafe character as a backstop (spec 7.6.4).
- Reload without root: the unprivileged panel signals OpenDKIM through
supervisord (`supervisorctl signal USR1 opendkim`, fixed args, no
shell, no user input — spec 7.6.3). An existing key is reused, never
overwritten, so re-adding a domain keeps its published DNS valid.
- Service orchestrates registry -> key -> table rebuild -> reload, with
rollback of the row if a downstream step fails; delete cascades apps
via the DB FK and removes the key + table entries.
Infra:
- Shared `selfpost` group bridges panel (writes keys) and opendkim
(reads them); /data/opendkim is setgid so panel-created files inherit
the group, keys are 0640, RequireSafeKeys is disabled by design.
- opendkim.conf moves from verify-only (Mode v) to signing (Mode s).
- entrypoint.sh normalises the DKIM tree on every start (ownership,
setgid, perms, empty tables before opendkim starts) — self-healing
after a restore.
- supervisord control socket opened to the `selfpost` group so the panel
can request the reload.
web/store:
- Strict domain-name validation (whitelist [a-z0-9.-], DNS shape, >=2
labels), lower-case normalisation (spec 7.6.2).
- Domain queries with application counts; delete relies on ON DELETE
CASCADE. Dashboard lists domains + add form; domain page shows the
DKIM record; a dedicated confirm page warns about the app cascade
before deletion (spec 7.2.4); manual reload button (spec 7.2.12,
OpenDKIM side; Postfix reload lands in Phase 5).
- Authenticated routes moved to a sub-mux using Go 1.22 method/wildcard
patterns.
Tests: validateDomain, DKIM keygen/record roundtrip, table rendering +
injection-safety, key reuse, store cascade. Verified on the dev server:
gofmt/vet/test green, image builds, container e2e (add/delete a domain,
DKIM record shown, OpenDKIM reads panel keys and reloads, keys and
tables persist across a restart).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>