Files
selfpost/internal/web/handlers/handlers.go
T
mixandClaude Opus 5.5 98e09fef4a
test / test (push) Canceled after 0s
health: sockets dialled, the certificate Postfix serves, "deferred" only for the queue
Audit 2026-10-12, stage 2c, "Health says what is served"; the pages of
ea8b6bf's mockup.

Sockets (A-38). A unix socket's file outlives a killed OpenDKIM or panel
and refuses every connect() (unix(7)); Health said "Listening" from a
stat. CheckSocket and CheckMilter's unix case now dial: refused is
"Nothing listens on the ... socket: the program that made it has
stopped", EACCES is unknown (the panel may not connect), anything else
"does not answer". The probe closes before a byte, which no milter logs:
libmilter takes the EOF as SMFIC_EOF without a line (sendmail
8.17.1.9-2+deb12u2, the image's libmilter1.0.1, libmilter/comm.c
mi_rd_cmd(), engine.c mi_engine() - its source is now in dev/upstream),
OpenDKIM's mlfi_close() has no connection to free, go-milter v0.4.1
returns quietly on io.EOF (session.go, HandleMilterCommands()). The page
polls every 5 s while in view, so this mattered.

Certificate (A-65). Postfix serves build/postfix-config.sh's copy,
refreshed by cert-reload every TLS_RELOAD_INTERVAL_SECONDS. The panel now
reads smtpd_tls_cert_file from postconf -h at its start (as the retry
policy) and TLS_RELOAD_INTERVAL_SECONDS; CheckServedCertificate checks the
served copy and, when the mounted file differs byte for byte (cert-reload's
own cmp -s), names it as Renewal: its expiry and mtime, two facts on the
card. Waiting is fine for the interval: a nearing expiry of the served one
is then no warning (an expiry still an error); past interval + 1 h it is a
warning pointing at cert-reload's lines. The notifier sends no expiry
notice while a renewal waits on time.

Wording (D-P1, D-P3, and the same for the spam filter): Sender check
note, help topics sender-check, upstream, filter, certificate - word for
word as in the mockup; the filter row's detail too.

Tests: stale socket file (Linux) refused for CheckSocket and CheckMilter;
CheckServedCertificate (same, waiting, late, expired + waiting, no served
path); intervalText; the card's facts; the notifier quiet while a renewal
waits, speaking once it is late. Stand (2026-10-15, make e2e, exit 0):
new health_shows_what_is_served - the three sockets dialled ok by the
panel's user; a certificate mounted without its key is refused by
cert-reload ("do not belong together") and the card shows the served
expiry, "Renewal expires" with the mounted one's, "Renewal mounted", and
"Postfix still serves the certificate above."; a matching pair is then
served again. go vet and go test ./... pass on the server (24 packages).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 00:44:21 +03:00

109 lines
3.2 KiB
Go

// Package handlers implements the panel's authenticated HTTP handlers.
package handlers
import (
"log"
"time"
"github.com/mixeme/selfpost/internal/app"
"github.com/mixeme/selfpost/internal/dmarc"
"github.com/mixeme/selfpost/internal/dnscheck"
"github.com/mixeme/selfpost/internal/domain"
"github.com/mixeme/selfpost/internal/health"
"github.com/mixeme/selfpost/internal/inbound"
"github.com/mixeme/selfpost/internal/postfix"
"github.com/mixeme/selfpost/internal/store"
"github.com/mixeme/selfpost/internal/web/auth"
"github.com/mixeme/selfpost/internal/web/view"
)
// Config holds handler-specific panel configuration.
type Config struct {
Hostname string
SubmissionEnabled bool
MailLogPath string
DataDir string
DBPath string
DeployRoot string
Version string
TLSCertFile string
TLSServedCertFile string
TLSReloadInterval time.Duration
OpenDKIMSocket string
JournalSocket string
SenderSocket string
// Level-1 Postfix anvil backstop (env RATE_LIMIT_*), shown in the panel
// and used to cap domain/app level-2 ceilings (guide § Rate limiting).
RateLimitMessagesPerIP int
RateLimitWindowSeconds int
// RetryPolicy is this Postfix's deferred-mail timings, snapshotted once
// when the HTTP role starts (architecture.md). The Mail queue card and
// delivery history read it from here; they never call postconf.
RetryPolicy postfix.RetryPolicy
// InboundEnabled mirrors INBOUND_RELAY_ENABLE: the inbound panel and
// routes exist only when this is true.
InboundEnabled bool
// InboundAntispamMilter mirrors INBOUND_ANTISPAM_MILTER and
// InboundAntispamAction INBOUND_ANTISPAM_MILTER_ACTION ("accept",
// "tempfail", or "" for a value build/postfix-config.sh would refuse). The
// filter is on only when the inbound relay is on and the milter is set.
InboundAntispamMilter string
InboundAntispamAction string
// DMARCEnabled mirrors DMARC_REPORTS_ENABLE.
DMARCEnabled bool
// SendLogRetentionEnvDefault is SEND_LOG_RETENTION_DAYS at panel start; used
// as bootstrap and fallback when the settings row is missing or invalid.
SendLogRetentionEnvDefault int
// InboundSpamLogRetentionEnvDefault is INBOUND_SPAM_LOG_RETENTION_DAYS at
// panel start, the same bootstrap and fallback for the inbound journal.
InboundSpamLogRetentionEnvDefault int
}
// Handlers holds dependencies for authenticated panel routes.
type Handlers struct {
store *store.Store
domains *domain.Service
apps *app.Service
inbound *inbound.Service
lists *inbound.Lists
dmarc *dmarc.Service
cfg Config
view *view.Engine
dns *dnscheck.Checker
machine *health.MachineSampler
auth *auth.Module
}
// New builds authenticated panel handlers.
func New(
st *store.Store,
domains *domain.Service,
apps *app.Service,
inboundSvc *inbound.Service,
lists *inbound.Lists,
dmarcSvc *dmarc.Service,
cfg Config,
v *view.Engine,
dns *dnscheck.Checker,
machine *health.MachineSampler,
a *auth.Module,
) *Handlers {
return &Handlers{
store: st,
domains: domains,
apps: apps,
inbound: inboundSvc,
lists: lists,
dmarc: dmarcSvc,
cfg: cfg,
view: v,
dns: dns,
machine: machine,
auth: a,
}
}
func logf(format string, args ...any) {
log.Printf(format, args...)
}