test / test (push) Canceled after 0s
The last stage of the 2026-10-12 audit; no cut. Doc drift: architecture.md (the sender check in § OpenDKIM, the limit: row's 48 hours, the inbound log and filter-list routes and who may open them, migrations to 0011 and every table, KitPage as production data, Reload configuration as a restore writes, the panel's roles counted), schema-migrations.md (tables and columns since v2), guide.md (a delivery's lines go at the next prune; INBOUND_JOURNAL_MILTER_SOCKET moved to architecture.md's internal list), development.md (the env guard, the deferral the e2e proves, its fixed sleeps), roadmap.md. Comments say what the code does now; TestBaselineIsTheOnlyMigration is TestChainStartsAtTheBaseline; the milter package doc names its three milters. Dead code gone: view.Facts, dmarc HostedSuggestion and ValidateHostedAddress, app UpdateMode (each with the test that alone kept it), the method checks the method-bound routes made dead; Settings.WithResult calls resultFlash. staticcheck 2026.1 in test.yml on both modules, clean: the e2e package builds without its tests now (a test and two constants moved out of non-test files), three findings fixed. The test moved, TestImageBuildPreservesLogrotateMode, had sat in a non-test file and never run; run, it failed on the entrypoint (docker run without a hostname) and now asks with --entrypoint stat. The test gaps no step named, each closed: follow() with mail.log gone past blindMin and made again; CheckRestore with a manifest that is not JSON; the origin check with a port; every /server/ pattern in web.go through server() and in the routes table (it found GET /server/health/fragment missing from the table - guarded, not tested); L9 as the owner decided it on 2026-10-12, the comment on senderSession.Headers and the test passing an h that holds the earlier messages' From:. Plan stage 6 ticked; roadmap audit row done, 81/81. Tested on the test server: go vet + go test of 27 packages, make e2e (all pass, 513 s) on this tree; staticcheck locally on both modules. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
102 lines
3.4 KiB
Go
102 lines
3.4 KiB
Go
package handlers
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/mixeme/selfpost/internal/dmarc"
|
|
"github.com/mixeme/selfpost/internal/store"
|
|
"github.com/mixeme/selfpost/internal/web/validate"
|
|
"github.com/mixeme/selfpost/internal/web/view"
|
|
)
|
|
|
|
// HandleDomainDMARC saves where a domain's aggregate reports (rua=) go. The
|
|
// form offers exactly three choices: "none", "hosted" (the address SelfPost
|
|
// derives for the domain, offered only while hosted reports are enabled) and
|
|
// "custom" (an address typed for the domain). What is stored is the address
|
|
// alone; "" is no reports.
|
|
func (h *Handlers) HandleDomainDMARC(w http.ResponseWriter, r *http.Request) {
|
|
d, ok := h.lookupDomain(w, r)
|
|
if !ok {
|
|
return
|
|
}
|
|
if err := r.ParseForm(); err != nil {
|
|
h.renderDomainSettings(w, r, http.StatusBadRequest, d, settingsView{Err: "Invalid form submission."})
|
|
return
|
|
}
|
|
|
|
rua := ""
|
|
mode := strings.TrimSpace(r.PostFormValue("dmarc_rua_mode"))
|
|
// A refusal shows the form again with the choice that was made.
|
|
refuse := func(status int, msg string) {
|
|
h.renderDomainSettings(w, r, status, d, settingsView{Err: msg, RuaMode: mode, RuaCustom: strings.TrimSpace(r.PostFormValue("dmarc_rua_email"))})
|
|
}
|
|
switch mode {
|
|
case ruaNone:
|
|
case ruaCustom:
|
|
email := strings.TrimSpace(r.PostFormValue("dmarc_rua_email"))
|
|
if err := validate.Email(email); err != nil {
|
|
refuse(http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
if email == "" {
|
|
refuse(http.StatusBadRequest, "Enter the report address or choose another option.")
|
|
return
|
|
}
|
|
rua = email
|
|
case ruaHosted:
|
|
if h.dmarc == nil || !h.dmarc.Enabled() {
|
|
refuse(http.StatusBadRequest, "SelfPost-hosted reports are not enabled on this server.")
|
|
return
|
|
}
|
|
rua = dmarc.HostedReportAddress(h.cfg.Hostname, d.Name)
|
|
default:
|
|
h.renderDomainSettings(w, r, http.StatusBadRequest, d, settingsView{Err: "Choose how aggregate reports are addressed for this domain."})
|
|
return
|
|
}
|
|
|
|
if err := h.store.SetDomainDMARCAddress(d.ID, rua); err != nil {
|
|
logf("panel: domain %d: save dmarc rua: %v", d.ID, err)
|
|
refuse(http.StatusInternalServerError, "Could not save DMARC settings. Please check the logs and try again.")
|
|
return
|
|
}
|
|
h.resyncDMARC("domain report address")
|
|
h.dns.Forget(d.Name)
|
|
http.Redirect(w, r, fmt.Sprintf("/outbound/domains/%d?dmarc=1", d.ID), http.StatusSeeOther)
|
|
}
|
|
|
|
// The three choices of a domain's report address, the values of the select.
|
|
const (
|
|
ruaNone = view.ReportNone
|
|
ruaHosted = view.ReportHosted
|
|
ruaCustom = view.ReportCustom
|
|
)
|
|
|
|
// reportChoice says which of the three choices a domain's stored address is:
|
|
// none for "", hosted for the address SelfPost derives for the domain (only
|
|
// while hosted reports are enabled — otherwise it is just an address), custom
|
|
// for anything else.
|
|
func (h *Handlers) reportChoice(d store.Domain) string {
|
|
hosted := h.hostedDMARCAddress(d.Name)
|
|
switch {
|
|
case d.DMARCRua == "":
|
|
return ruaNone
|
|
case hosted != "" && strings.EqualFold(d.DMARCRua, hosted):
|
|
return ruaHosted
|
|
}
|
|
return ruaCustom
|
|
}
|
|
|
|
// resyncDMARC rebuilds the ingest allow-list after something that changes
|
|
// where a domain's reports go. A failure is logged, not shown: the setting is
|
|
// saved, and the next resync picks it up.
|
|
func (h *Handlers) resyncDMARC(why string) {
|
|
if h.dmarc == nil || !h.dmarc.Enabled() {
|
|
return
|
|
}
|
|
if err := h.dmarc.Resync(); err != nil {
|
|
logf("panel: dmarc resync after %s change: %v", why, err)
|
|
}
|
|
}
|