Show this Postfix's retry policy on Mail queue and delivery history.
test / test (push) Waiting to run

Numbers come from a one-shot postconf -h at panel start so a manual override is visible after restart, without inventing an attempt count.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-17 22:12:03 +03:00
parent 46191172d6
commit 705239568b
17 changed files with 669 additions and 30 deletions
+12 -3
View File
@@ -78,7 +78,14 @@ by the panel. Socket `/run/opendkim/opendkim.sock`.
One process, three roles:
1. **HTTP server**`:8080` (`PANEL_HTTP_ADDR`); HTTPS terminated by reverse
proxy only.
proxy only. On start it runs `postconf -h` once for the deferred-mail retry
parameters (`queue_run_delay`, `minimal_backoff_time`,
`maximal_backoff_time`, `maximal_queue_lifetime`, `bounce_queue_lifetime`,
`delay_warning_time`) and caches the snapshot on the handlers config. The
Mail queue card and a delivery's `deferred` / `bounced` history print those
numbers; they never call `postconf` per request. If `postconf` is missing,
the panel logs a warning and uses Postfix 3.x compiled-in defaults
(`300s` / `4000s` / `5d` / `0`) with a muted note on the card.
2. **journal-milter** — unix socket `JOURNAL_MILTER_SOCKET`; records From/To/
Subject/SASL user at DATA; enforces level-2 rate limits; **fail-open**
(`default_action=accept`) so milter failure does not stop mail. Domain
@@ -173,7 +180,7 @@ below is a summary — HTMX fragment endpoints
| `/domains/{id}`, `/domains/{id}/*` | Assigned-domain detail for domain-admins; delete domain is **global** |
| `/domains/import` | **Global.** Domain import (`POST`; form on the Backup page) |
| `/deliveries`, `/deliveries/{id}` | Send log with filters; scoped to assigned domains for domain-admins |
| `/mail-queue`, `/mail-queue/*` | **Global.** Postfix queue view |
| `/mail-queue`, `/mail-queue/*` | **Global.** Postfix queue view; retry-policy card on the page (not the HTMX fragment) |
| `/system-log`, `/system-log/*` | **Global.** `mail.log` tail |
| `/reload` | **Global.** `POST` — reload OpenDKIM + Postfix maps |
| `/backup`, `/backup/*` | **Global.** Full backup download (page also hosts the import form) |
@@ -184,7 +191,9 @@ HTMX polling refreshes monitoring fragments (5 s while the operator is active on
the page, 30 s when the tab is visible but idle, none when hidden — scheduled in
`panel.js` via `data-poll`, not `hx-trigger="every …"`); polling does not extend
session idle timeout (only non-`HX-Request` GET and mutating requests count as
activity).
activity). The Mail queue retry-policy card is outside that fragment: it is the
start-up `postconf -h` snapshot (see [Panel binary](#panel-binary-cmdpanel)),
not a live re-read.
### Sessions
+11 -3
View File
@@ -319,7 +319,12 @@ the panel shows after manual edits under `/data`.
### Mail queue and System log
- **Mail queue** (`/mail-queue`) — live view of messages Postfix is still
trying to deliver or deferring.
trying to deliver or deferring. A card at the top states this instance's
retry policy — first retry delay, later backoff cap, how long a message
stays in the queue — from `postconf -h`, read once when the panel starts.
A `postconf -e` override inside the container is visible after the next
panel (or container) restart. There is no maximum attempt count: Postfix
retries until the message is delivered or the queue lifetime runs out.
- **System log** (`/system-log`) — tail of `/data/log/mail.log` (Postfix and
related daemon lines). The log rotates daily (14 files kept) with a
`postfix reload` after each rotation; a background loop checks every six
@@ -668,8 +673,11 @@ by a [level-2 rate limit](#rate-limiting--level-2-domain-and-application));
*Details* opens that row's own page (`/deliveries/{id}`). That page carries
the sending domain, the application it was submitted under, the Postfix
queue id and the journal id, beside the message's history — when it was
accepted and what Postfix later reported for the recipient — and, under
both, the `mail.log` lines for its queue id: the connection to the
accepted and what Postfix later reported for the recipient. A `deferred`
or `bounced` row includes this Postfix's retry intervals (first delay,
backoff cap, queue lifetime), the same numbers Mail queue shows; domain
administrators see them here because they cannot open Mail queue. Under
both sit the `mail.log` lines for its queue id: the connection to the
receiving server, the server's reply, and the status that reply was filed
as. Rows outlive `mail.log`, so an older message's lines may have rotated
away; the page says so. Retention is controlled by
+10 -10
View File
@@ -1,6 +1,6 @@
# Plan: queue-retries (Postfix retry policy in the panel)
**Status:** agreed
**Status:** implemented (2026-08-17); version cut `1.3.1` pending
**Date:** 2026-08-13
**Version:** patch; no schema, no configuration surface.
**Order:** small panel item; does not wait on inbound-relay.
@@ -145,12 +145,12 @@ lifetime runs out.
Target version cut: **`1.3.1`** (PATCH). One commit per step; see
[development.md](../development.md) § Plan checklists.
- [ ] `internal/postfix`: parse Postfix time units (`5d`, `300s`, bare seconds) + tests — **Opus**
- [ ] `internal/postfix`: one-shot `postconf -h` (six keys), fallback + warn — **Opus**
- [ ] Load policy at HTTP start in `cmd/panel/httpserver.go`; cache on handlers config — **Opus**
- [ ] Human-readable duration formatter (shared by Mail queue card and delivery history) — **Sonnet**
- [ ] «How delivery retries work» card on `/mail-queue` (outside HTMX fragment) — **Sonnet**
- [ ] `deliveryEvents(row, policy)` — intervals in deferred/bounced copy — **Sonnet**
- [ ] Handler and template tests (`handlers_monitor_test.go`, `templates_test.go`) — **Sonnet**
- [ ] [guide.md](../guide.md) and [architecture.md](../architecture.md) — **Sonnet**
- [ ] `go vet`, `go test` on touched packages — **Haiku**
- [x] `internal/postfix`: parse Postfix time units (`5d`, `300s`, bare seconds) + tests — **Opus**
- [x] `internal/postfix`: one-shot `postconf -h` (six keys), fallback + warn — **Opus**
- [x] Load policy at HTTP start in `cmd/panel/httpserver.go`; cache on handlers config — **Opus**
- [x] Human-readable duration formatter (shared by Mail queue card and delivery history) — **Sonnet**
- [x] «How delivery retries work» card on `/mail-queue` (outside HTMX fragment) — **Sonnet**
- [x] `deliveryEvents(row, policy)` — intervals in deferred/bounced copy — **Sonnet**
- [x] Handler and template tests (`handlers_monitor_test.go`, `templates_test.go`) — **Sonnet**
- [x] [guide.md](../guide.md) and [architecture.md](../architecture.md) — **Sonnet**
- [x] `go vet`, `go test` on touched packages — **Haiku**
+5 -2
View File
@@ -28,7 +28,7 @@ in `git log` and [CHANGELOG.md](../CHANGELOG.md).
| ID | Topic | Status | Progress | Plan |
|---|---|---|---|---|
| queue-retries | Postfix retry policy in the panel (queue lifetime, backoff) | **agreed** | 0/9 | [plans/queue-retries.md](plans/queue-retries.md) |
| queue-retries | Postfix retry policy in the panel (queue lifetime, backoff) | **agreed** | 9/9 | [plans/queue-retries.md](plans/queue-retries.md) |
| inbound-relay | Inbound relay (backup-MX / forwarding) | **agreed** | 0/15 | [plans/inbound-relay.md](plans/inbound-relay.md) |
| send-log-retention | Send-log retention days in panel Settings | candidate | 0/8 | [plans/send-log-retention.md](plans/send-log-retention.md) |
| domain-stats-auto-ratelimit | 30-day send stats + auto level-2 rate limit | candidate | 0/11 | [plans/domain-stats-auto-ratelimit.md](plans/domain-stats-auto-ratelimit.md) |
@@ -65,7 +65,10 @@ panel knobs for queue lifetime, no schema change. Domain administrators see
the intervals on `/deliveries/{id}` (they cannot open Mail queue).
**Done when:** see the criteria in
[plans/queue-retries.md](plans/queue-retries.md).
[plans/queue-retries.md](plans/queue-retries.md). Implementation is on
`main` (checklist 9/9); the `1.3.1` version cut is still pending because
`[Unreleased]` also holds unrelated work (including a breaking backup-layout
change).
**Dependencies / risks:** `postconf` unavailable outside the container
(fallback + muted note). Copy must stay time-based — Postfix has no max