From 415334c2cca36b6d16008ef138ed0d1ed28bddb9 Mon Sep 17 00:00:00 2001 From: Mix Date: Wed, 14 Oct 2026 17:18:15 +0300 Subject: [PATCH] message page: a row the sweep closed says the panel closed it (stage 2b) The History step of every bounced row said the receiving server refused the message permanently or Postfix gave up, "the reason is in the delivery log below" - for a row the reconcile sweep closed (reason "reconcile") neither is known and the log has nothing (review of stage 1). Such a row's step is now "Closed by the panel - Postfix no longer held the message and mail.log never said what became of it ... rather than show a delivery it cannot evidence." The help topic log names the case (design df96690). Pictured for the owner with the real templates (dev/shots); the variant has no mockup screen of its own, as the refused one. Test: the page of a row the sweep closed says "Closed by the panel" and not "the receiving server refused". CHANGELOG [Unreleased], audit plan line ticked, roadmap 29/62. Evidence: the message-page tests of ./internal/web/handlers ok locally; the server run follows before the push. Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 5 +++++ docs/audit-2026-10-12.md | 2 +- docs/roadmap.md | 2 +- internal/web/handlers/handlers_monitor.go | 6 ++++++ internal/web/handlers/handlers_users_test.go | 18 ++++++++++++++++++ 5 files changed, 31 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d7ac733..1240cf2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -33,6 +33,11 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version the page shows it. A row refused before this says the line was not kept. A refusal's line is red for a permanent reply, amber for a temporary one (audit 2026-10-12, stage 2b). +- **A message the panel closed itself says so.** The history of a `bounced` + row read "the receiving server refused the message permanently, or + Postfix gave up ... The reason is in the delivery log below" also for a + row the reconcile sweep closed, where neither is known and the log has + nothing. Such a row's step is now "Closed by the panel", with why. - **The Sender check card counts connections and messages apart.** What was one column, "Messages deferred", was two things: connections Postfix turned away while the panel did not answer - refused before any message diff --git a/docs/audit-2026-10-12.md b/docs/audit-2026-10-12.md index 042d63e..60d3660 100644 --- a/docs/audit-2026-10-12.md +++ b/docs/audit-2026-10-12.md @@ -783,7 +783,7 @@ order - each stands on the one before. sentence stays only for a row whose line was truly never written or is older than this change, and says that. Done when the e2e shows the stored line on the page of each of the three refusals. - **Opus** -- [ ] **A message the sweep closed does not say the receiving server refused +- [x] **A message the sweep closed does not say the receiving server refused it** (review of stage 1, 2026-10-14). The history step of a `bounced` row reads "the receiving server refused the message permanently, or Postfix gave up ... The reason is in the delivery log below" diff --git a/docs/roadmap.md b/docs/roadmap.md index 9478831..1819bd7 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -42,7 +42,7 @@ in `git log` and [CHANGELOG.md](../CHANGELOG.md). | from-header-binding | Hold a message's `From:` header against the application that sends it | shipped `2.4.0` | 9/9 | [plans/from-header-binding.md](plans/from-header-binding.md) | | panel-redesign | Panel redesign (accepted mockups, design contract) | shipped `2.0.0` | 27/27 | [plans/panel-redesign.md](plans/panel-redesign.md) | | schema-squash | Squash SQLite migrations into a 2.x baseline | built in panel-redesign (stage 1) | — | [plans/panel-redesign.md](plans/panel-redesign.md) | -| audit-2026-10-12 | Follow-ups of the 2026-10-12 full-tree code audit, seven stages | in progress (stage 0 shipped `2.4.2`, stages 1+2 `2.5.0`; next stage 2b → `2.5.1`) | 28/62 | [audit-2026-10-12.md](audit-2026-10-12.md) | +| audit-2026-10-12 | Follow-ups of the 2026-10-12 full-tree code audit, seven stages | in progress (stage 0 shipped `2.4.2`, stages 1+2 `2.5.0`; next stage 2b → `2.5.1`) | 29/62 | [audit-2026-10-12.md](audit-2026-10-12.md) | **Recommended order** (not binding; owner, 2026-10-09: feature order follows development convenience): **panel-redesign** shipped as `2.0.0` (2026-10-11) and diff --git a/internal/web/handlers/handlers_monitor.go b/internal/web/handlers/handlers_monitor.go index 59134ec..223ceee 100644 --- a/internal/web/handlers/handlers_monitor.go +++ b/internal/web/handlers/handlers_monitor.go @@ -211,6 +211,12 @@ func deliveryEvents(row store.SendLogRow, policy postfix.RetryPolicy, sendsAs vi fmt.Sprintf("The receiving server could not take the message yet. Postfix retries: first after %s, then with increasing gaps up to %s, for up to %s. There is no fixed attempt count — a deferred message stays in the queue until it is delivered or that lifetime runs out.", policy.FirstRetry(), policy.BackoffCap(), policy.QueueLifetime()))) case store.StatusBounced: + if row.Reason == store.ReasonReconcile { + // Neither the receiving server nor Postfix said so: the panel + // closed the row (audit 2026-10-12, stage 2b). + return append(events, step(row.UpdatedAt, "error", "Closed by the panel", + "Postfix no longer held the message and mail.log never said what became of it - the log rotated away while the panel was down, or could not be read. The panel closed it as a failure rather than show a delivery it cannot evidence.")) + } return append(events, step(row.UpdatedAt, "error", "Bounced", fmt.Sprintf("Delivery failed for good: the receiving server refused the message permanently, or Postfix gave up after %s in the queue. The reason is in the delivery log below.", policy.QueueLifetime()))) diff --git a/internal/web/handlers/handlers_users_test.go b/internal/web/handlers/handlers_users_test.go index 6f546df..a804535 100644 --- a/internal/web/handlers/handlers_users_test.go +++ b/internal/web/handlers/handlers_users_test.go @@ -186,3 +186,21 @@ func TestARefusedMessageShowsPostfixsLine(t *testing.T) { } } } + +// A message the reconcile sweep closed does not say a receiving server +// refused it: neither did (audit 2026-10-12, stage 2b). +func TestAMessageTheSweepClosedSaysSo(t *testing.T) { + h, st := inboundHandlers(t) + root := userWith(t, st, "root", store.RoleGlobal, store.Reach{}) + if err := st.InsertQueued(store.SendLogEntry{QueueID: "GONEQ", Domain: "example.org", AppLogin: "a", To: "x@example.net", Subject: "lost"}); err != nil { + t.Fatal(err) + } + rows, _ := st.QuerySendLog(store.SendLogFilter{AllDomains: true}, 1, 0) + if _, err := st.CloseUnreported(rows[0].ID); err != nil { + t.Fatal(err) + } + body := send(h.HandleDelivery, &root, "GET", "/outbound/log/"+idStr(rows[0].ID), map[string]string{"id": idStr(rows[0].ID)}, nil).Body.String() + if !strings.Contains(body, "Closed by the panel") || strings.Contains(body, "the receiving server refused") { + t.Fatalf("the page of a row the sweep closed:\n%s", body) + } +}