Files
selfpost/internal/web/handlers/handlers_settings.go
T
mixandClaude Opus 5.5 56c5dfee40 inbound-antispam-panel: Inbound > Log and Inbound > Filter lists
Step 6/12 of docs/plans/inbound-antispam-panel.md. Written by a Sonnet
subagent from the mockups, reviewed and corrected by me.

GET /inbound/log - the journal a page at a time (50), filtered by
domain, decision and day; QueryInboundSpam / CountInboundSpam with the
viewer's inbound domains as the scope. The global role reads all of it
and gets "Deny sender" / "Allow sender" links into the lists; a user
with the domain role reads the rows of the inbound domains assigned to
them, without the links. The plan said global-only for the journal;
the accepted mockup and 2.0's delegation of Inbound say otherwise -
recorded in the plan as built.

GET, POST /inbound/filter-lists and POST .../{id}/delete - global role,
404 otherwise. Add form (prefilled from the query), the two lists with
Remove as the kit's row action, and "Where the lists go" with the
filter's state and when the maps were last written
(Lists.LastWritten). A refused or duplicate entry is 422 with the
message and what was typed.

Server > Settings: "Keep inbound log rows, days" with the relay on.
Help: Inbound log, Filter lists. Navigation: Log for everyone who sees
Inbound, Filter lists for the global role (Shell.ShowFilterLists).
Status tags accept / reject / tempfail join the kit's mapping.

Tests: page fixtures for both screens, held to outlines.json; view and
handler tests (both roles, filters, paging, relay off, add / duplicate
/ invalid / delete, map file content, settings field, help). e2e reads
the journal from the page instead of the database and adds and removes
a deny entry through the panel, reading deny_domain.map after each.

Seen: both pages and Settings rendered from their fixtures and compared
with the mockups side by side (headless Edge, 1360 px) - same layout
and wording.

Checked on the test server, uncommitted tree: go vet ./... ok,
go test ./... ok (22 packages), make e2e ok.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-11 23:50:34 +03:00

122 lines
4.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handlers
import (
"net/http"
"strconv"
"strings"
"github.com/mixeme/selfpost/internal/store"
"github.com/mixeme/selfpost/internal/web/view"
)
// Server › Settings holds what is true of the whole instance, whoever is
// signed in: log retention (the outbound log's, and the inbound log's while the
// inbound relay is on) and the read-only view of the rate limits. A
// user's own name, e-mail, password and DMARC default are on Account
// (handlers_account.go). Before placing a setting here, ask whose it is.
// HandleServerSettings shows and saves the instance settings (global role).
func (h *Handlers) HandleServerSettings(w http.ResponseWriter, r *http.Request) {
if _, ok := h.requireGlobal(w, r); !ok {
return
}
switch r.Method {
case http.MethodGet:
h.renderServerSettings(w, r, http.StatusOK, "", h.currentRetentions())
case http.MethodPost:
h.submitServerSettings(w, r)
default:
w.Header().Set("Allow", "GET, POST")
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
}
// retentions are the two journal retentions the Settings form shows: the
// outbound log's, and the inbound log's while the inbound relay is on.
type retentions struct {
outbound, inbound int
}
func (h *Handlers) currentRetentions() retentions {
cur := retentions{outbound: h.sendLogRetentionDays()}
if h.cfg.InboundEnabled {
cur.inbound = h.inboundSpamLogRetentionDays()
}
return cur
}
func (h *Handlers) renderServerSettings(w http.ResponseWriter, r *http.Request, status int, formErr string, form retentions) {
page := view.NewSettings(h.shellMeta(r), strconv.Itoa(form.outbound), view.FormatRate(h.l1Messages(), h.l1Window()))
// The inbound field exists while there is an inbound journal to keep.
if h.cfg.InboundEnabled {
page.WithInbound(strconv.Itoa(form.inbound))
}
flash := ""
if r.URL.Query().Has("saved") {
flash = "Settings saved."
}
page.WithResult(flash, formErr)
h.view.Render(w, status, "settings", page)
}
func (h *Handlers) submitServerSettings(w http.ResponseWriter, r *http.Request) {
current := h.currentRetentions()
if err := r.ParseForm(); err != nil {
h.renderServerSettings(w, r, http.StatusBadRequest, "Invalid form submission.", current)
return
}
days, err := strconv.Atoi(strings.TrimSpace(r.PostFormValue("send_log_retention_days")))
if err != nil {
h.renderServerSettings(w, r, http.StatusBadRequest, "Send log retention must be a whole number of days.", current)
return
}
form := retentions{outbound: days, inbound: current.inbound}
if err := store.ValidateSendLogRetentionDays(days); err != nil {
h.renderServerSettings(w, r, http.StatusBadRequest, err.Error(), form)
return
}
if h.cfg.InboundEnabled {
inboundDays, err := strconv.Atoi(strings.TrimSpace(r.PostFormValue("inbound_spam_log_retention_days")))
if err != nil {
h.renderServerSettings(w, r, http.StatusBadRequest, "Inbound log retention must be a whole number of days.", form)
return
}
form.inbound = inboundDays
if err := store.ValidateInboundSpamLogRetentionDays(inboundDays); err != nil {
h.renderServerSettings(w, r, http.StatusBadRequest, err.Error(), form)
return
}
}
if err := h.store.SetSendLogRetentionDays(days); err != nil {
logf("panel: settings: set send-log retention failed: %v", err)
h.renderServerSettings(w, r, http.StatusInternalServerError,
"Could not save send log retention. Please check the logs and try again.", form)
return
}
if h.cfg.InboundEnabled {
if err := h.store.SetInboundSpamLogRetentionDays(form.inbound); err != nil {
logf("panel: settings: set inbound-log retention failed: %v", err)
h.renderServerSettings(w, r, http.StatusInternalServerError,
"Could not save inbound log retention. Please check the logs and try again.", form)
return
}
logf("panel: instance settings updated (send-log retention: %d days, inbound-log retention: %d days)", days, form.inbound)
} else {
logf("panel: instance settings updated (send-log retention: %d days)", days)
}
http.Redirect(w, r, "/server/settings?saved=1", http.StatusSeeOther)
}
// sendLogRetentionDays returns the effective delivery-journal retention window.
func (h *Handlers) sendLogRetentionDays() int {
days, err := h.store.GetSendLogRetentionDays(h.cfg.SendLogRetentionEnvDefault)
if err != nil {
logf("panel: send-log retention: %v", err)
if h.cfg.SendLogRetentionEnvDefault > 0 {
return h.cfg.SendLogRetentionEnvDefault
}
return store.SendLogRetentionDaysDefault
}
return days
}