dmarc-day, health-sockets: the reporter's day said; the inbound journal dialled
The owner, 2026-10-17: "Показ времени отчёта по UTC принимаем. Это не проблема. Пояснения добавь. Все мелочи закрой." Designs9b33b80,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:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user