dmarc-day, health-sockets: the reporter's day said; the inbound journal dialled

The owner, 2026-10-17: "Показ времени отчёта по UTC принимаем. Это не
проблема. Пояснения добавь. Все мелочи закрой." Designs 9b33b80, 2bf9056.

DMARC: a report's day stays the reporter's, in UTC (RFC 7489 § 7.2); the
report's Window now says so under its times (windowNote), and Settings'
Time zone box and Help (#settings, #dmarc) name it as the one exception to
the panel's zone. formatDMARCWindow counts a window that ends at midnight
as ending the day before, so a daily report is one day, not "20 Sep - 21
Sep" (TestDMARCWindowIsTheReportersDay, in Moscow's zone).

Health: with the inbound relay on, the inbound journal milter's socket is
dialled (InboundJournalSocket through web and handlers Config), fail-open
as the send log's, and Overview's card counts it; the send log's socket is
named journal-milter, as the mockup names it.

The confirmation link's sign-in state is drawn (confirm-email-signin.html);
the panel already renders it.

guide: /license and /server/components, the DMARC pages' paths and day,
selfpost-backup -o / -i / -version, the Inbound log's retention field,
PANEL_COOKIE_SECURE, the inbound journal's socket, the DMARC exception to
the zone. CHANGELOG [Unreleased].

(The e2e case negative_level1_ratelimit, listed as able to pass falsely,
was fixed on 2026-10-14 - the note was stale.)

go vet + go test (26 packages) and make e2e on the test server for this
tree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mix
2026-10-17 13:38:34 +03:00
co-authored by Claude Opus 5.5
parent 2bf9056d20
commit 0eaf69a830
13 changed files with 129 additions and 29 deletions
+3 -1
View File
@@ -35,7 +35,9 @@ type Config struct {
SignSocket string
SignLocalSocket string
JournalSocket string
SenderSocket string
// InboundJournalSocket is the inbound journal milter's (Inbound › Log).
InboundJournalSocket 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
@@ -222,12 +222,21 @@ func (h *Handlers) relayIPs() map[string]bool {
return ips
}
// formatDMARCWindow is the day, or days, a report covers, as its reporter
// counts them: in UTC (RFC 7489 § 7.2, date-range in UTC seconds), which the
// panel keeps whatever its own zone (owner, 2026-10-17). A window that ends
// at midnight ends the day before: a daily report from 00:00 to 00:00 is one
// day, not two.
func formatDMARCWindow(begin, end time.Time) string {
if begin.IsZero() {
return ""
}
if begin.Year() == end.Year() && begin.YearDay() == end.YearDay() {
return begin.UTC().Format("2 Jan")
begin, last := begin.UTC(), end.UTC()
if last.After(begin) && last.Equal(last.Truncate(24*time.Hour)) {
last = last.Add(-time.Second)
}
return begin.UTC().Format("2 Jan") + " – " + end.UTC().Format("2 Jan")
if begin.Year() == last.Year() && begin.YearDay() == last.YearDay() {
return begin.Format("2 Jan")
}
return begin.Format("2 Jan") + " – " + last.Format("2 Jan")
}
@@ -0,0 +1,26 @@
package handlers
import (
"testing"
"time"
)
// A report's window is its reporter's day or days, in UTC, whatever the
// panel's zone; a window that ends at midnight ends the day before.
func TestDMARCWindowIsTheReportersDay(t *testing.T) {
inZone(t, "Europe/Moscow")
day := func(d, h, m, s int) time.Time { return time.Date(2026, 9, d, h, m, s, 0, time.UTC) }
for _, c := range []struct {
begin, end time.Time
want string
}{
{day(20, 0, 0, 0), day(21, 0, 0, 0), "20 Sep"},
{day(20, 0, 0, 0), day(20, 23, 59, 59), "20 Sep"},
{day(20, 0, 0, 0), day(22, 0, 0, 0), "20 Sep – 21 Sep"},
{day(20, 12, 0, 0), day(21, 12, 0, 0), "20 Sep – 21 Sep"},
} {
if got := formatDMARCWindow(c.begin, c.end); got != c.want {
t.Errorf("%s – %s = %q, want %q", c.begin, c.end, got, c.want)
}
}
}
@@ -27,7 +27,8 @@ func socketsServer(t *testing.T) *Handlers {
h.cfg.SignLocalSocket = filepath.Join(dir, "sign-local.sock")
h.cfg.JournalSocket = filepath.Join(dir, "journal.sock")
h.cfg.SenderSocket = filepath.Join(dir, "sender.sock")
for _, path := range []string{h.cfg.SignSocket, h.cfg.SignLocalSocket, h.cfg.SenderSocket, h.cfg.JournalSocket} {
h.cfg.InboundJournalSocket = filepath.Join(dir, "inbound-journal.sock")
for _, path := range []string{h.cfg.SignSocket, h.cfg.SignLocalSocket, h.cfg.SenderSocket, h.cfg.JournalSocket, h.cfg.InboundJournalSocket} {
l, err := net.Listen("unix", path)
if err != nil {
t.Skipf("unix sockets unavailable here: %v", err)
@@ -57,8 +58,9 @@ func TestFilterOffAddsNoSocket(t *testing.T) {
h := socketsServer(t)
h.cfg.InboundEnabled = true
h.cfg.InboundAntispamMilter = "" // no filter attached
if got := socketsOf(t, h); len(got) != 4 {
t.Fatalf("%d sockets with the filter off, want 4: %+v", len(got), got)
// The relay's own journal is dialled; no filter is.
if got := socketsOf(t, h); len(got) != 5 || got[4].Name != "inbound-journal-milter" {
t.Fatalf("%d sockets with the filter off, want 5 (the inbound journal last): %+v", len(got), got)
}
// A milter address with the inbound relay off is not a filter either.
@@ -69,7 +71,7 @@ func TestFilterOffAddsNoSocket(t *testing.T) {
t.Fatalf("%d sockets with the inbound relay off, want 4: %+v", len(got), got)
}
card := socketCard(t, h)
if card.Value != "4 of 4 answering" || card.Sub != "dkim-signer, sender-check, send-log" || card.Level != "" {
if card.Value != "4 of 4 answering" || card.Sub != "dkim-signer, sender-check, journal-milter" || card.Level != "" {
t.Errorf("card = %+v", card)
}
}
@@ -85,7 +87,7 @@ func TestFilterOnAnsweringIsTheLastSocket(t *testing.T) {
h.cfg.InboundAntispamMilter, h.cfg.InboundAntispamAction = "inet:"+l.Addr().String(), "tempfail"
got := socketsOf(t, h)
if len(got) != 5 || got[4].Name != "Spam filter" || got[4].Note != "inbound" || got[4].Path != "inet:"+l.Addr().String() {
if len(got) != 6 || got[5].Name != "Spam filter" || got[5].Note != "inbound" || got[5].Path != "inet:"+l.Addr().String() {
t.Fatalf("sockets = %+v", got)
}
for _, s := range got {
@@ -94,7 +96,7 @@ func TestFilterOnAnsweringIsTheLastSocket(t *testing.T) {
}
}
card := socketCard(t, h)
if card.Value != "5 of 5 answering" || card.Sub != "incl. the spam filter" || card.Level != "" {
if card.Value != "6 of 6 answering" || card.Sub != "incl. the spam filter" || card.Level != "" {
t.Errorf("card = %+v", card)
}
@@ -134,10 +136,10 @@ func TestFilterDownIsAMissingSocket(t *testing.T) {
} {
h.cfg.InboundAntispamMilter, h.cfg.InboundAntispamAction = dead, tt.action
got := socketsOf(t, h)
if len(got) != 5 {
t.Fatalf("action %q: %d sockets, want 5", tt.action, len(got))
if len(got) != 6 {
t.Fatalf("action %q: %d sockets, want 6", tt.action, len(got))
}
f := got[4]
f := got[5]
refused := "Connection to " + l.Addr().String() + " refused. " + tt.detail
if f.Present || f.Status != tt.status || f.Detail != refused {
t.Errorf("action %q: filter = %+v, want status %q, detail %q", tt.action, f, tt.status, refused)
@@ -148,7 +150,7 @@ func TestFilterDownIsAMissingSocket(t *testing.T) {
t.Errorf("action %q: SocketStatus = %q, want %q", tt.action, c.SocketStatus, tt.status)
}
card := socketCard(t, h)
if card.Value != "4 of 5 answering" || card.Level != tt.level || !strings.HasSuffix(card.Sub, "Spam filter") {
if card.Value != "5 of 6 answering" || card.Level != tt.level || !strings.HasSuffix(card.Sub, "Spam filter") {
t.Errorf("action %q: card = %+v", tt.action, card)
}
@@ -162,7 +164,7 @@ func TestFilterDownIsAMissingSocket(t *testing.T) {
rec := httptest.NewRecorder()
h.HandleOverviewFragment(rec, reqAs("/overview/fragment", globalPrincipal))
page := rec.Body.String()
for _, want := range []string{`>` + tt.stamp + `<`, `Milter sockets</a>: 4 of 5 answering`} {
for _, want := range []string{`>` + tt.stamp + `<`, `Milter sockets</a>: 5 of 6 answering`} {
if !strings.Contains(page, want) {
t.Errorf("action %q: Overview is missing %q", tt.action, want)
}
+6 -1
View File
@@ -572,7 +572,12 @@ func (h *Handlers) healthChecks() healthReadings {
signerSocket(h.cfg.SignSocket, "applications"),
signerSocket(h.cfg.SignLocalSocket, "notifications"),
health.CheckSocket("sender-check", h.cfg.SenderSocket, true),
health.CheckSocket("send-log", h.cfg.JournalSocket, false),
health.CheckSocket("journal-milter", h.cfg.JournalSocket, false),
}
// The inbound journal writes Inbound › Log, fail-open as the outbound
// one: dialled while there is an inbound relay to journal.
if h.cfg.InboundEnabled {
sockets = append(sockets, health.CheckSocket("inbound-journal-milter", h.cfg.InboundJournalSocket, false))
}
if on, milter, action := h.inboundFilter(); on {
sockets = append(sockets, filterSocket(milter, action))