Roadmap item and plan: show first retry delay, backoff cap and queue lifetime on Mail queue and the delivery page, reading postconf -h once at panel start so a manual override is visible. Explanation only; not implemented yet. CHANGELOG updated. Co-Authored-By: Cursor Grok 4.6 <noreply@anthropic.com> Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
# Plan: queue-retries (Postfix retry policy in the panel)
|
||||
|
||||
**Status:** agreed
|
||||
**Date:** 2026-08-13
|
||||
**Version:** patch; no schema, no configuration surface.
|
||||
**Order:** small panel item; does not wait on inbound-relay. May follow or
|
||||
interleave with remaining [code-review.md](code-review.md) work after P0.
|
||||
|
||||
---
|
||||
|
||||
## Goal
|
||||
|
||||
Show the operator how Postfix retries deferred mail: first retry delay, later
|
||||
backoff cap, and how long a message stays in the queue before it bounces. The
|
||||
numbers come from this container's effective Postfix config, not from
|
||||
hard-coded copy.
|
||||
|
||||
## Scope
|
||||
|
||||
**In:**
|
||||
|
||||
- A static «How delivery retries work» card on `/mail-queue` (global
|
||||
administrator). Not inside the HTMX poll fragment — the snapshot is taken at
|
||||
panel start.
|
||||
- The same human-readable intervals in `/deliveries/{id}` history for
|
||||
`deferred` and `bounced` (domain administrators never see Mail queue).
|
||||
- Operator docs: [guide.md](../guide.md) Mail queue bullet;
|
||||
[architecture.md](../architecture.md) notes the one-shot `postconf -h` at
|
||||
panel start. [CHANGELOG.md](../../CHANGELOG.md) `### Added`.
|
||||
|
||||
**Out:**
|
||||
|
||||
- Changing Postfix retry parameters, or exposing them as panel settings
|
||||
([product.md](../product.md): Postfix is used as-is).
|
||||
- An `attempts` column on `send_log`, or «attempt 3 of N» — Postfix has no
|
||||
attempt budget; it is time-based.
|
||||
- Counting `status=deferred` lines in `mail.log` or reading `postcat`. Each
|
||||
attempt is already on the delivery page's log table; the journal stores only
|
||||
the last status.
|
||||
- Duplicating the card on Status (summary + link to Mail queue already exist).
|
||||
- Re-reading `postconf` on every HTTP request.
|
||||
|
||||
## Architecture
|
||||
|
||||
Acceptance is still synchronous SMTP. Delivery stays in Postfix's on-disk
|
||||
queue. SelfPost does not enqueue, retry, or deliver.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
smtpAccept["SMTP 250 accepted"] --> postfixQueue["Postfix queue"]
|
||||
postfixQueue -->|"deferred: backoff until queue lifetime"| retry["Retry MX"]
|
||||
retry -->|sent| done["sent"]
|
||||
retry -->|lifetime elapsed or 5xx| bounce["bounced"]
|
||||
postfixQueue --> mailQueuePage["/mail-queue policy card"]
|
||||
postfixQueue --> deliveryPage["/deliveries/id history copy"]
|
||||
```
|
||||
|
||||
[build/postfix-config.sh](../../build/postfix-config.sh) does not set
|
||||
`queue_run_delay`, `minimal_backoff_time`, `maximal_backoff_time`,
|
||||
`maximal_queue_lifetime`, `bounce_queue_lifetime`, or `delay_warning_time`.
|
||||
Debian/Postfix 3.x compiled-in defaults therefore apply unless the operator
|
||||
overrides them (`postconf -e` inside the container).
|
||||
|
||||
### Loading the numbers
|
||||
|
||||
Once, when the HTTP role starts ([cmd/panel/httpserver.go](../../cmd/panel/httpserver.go),
|
||||
after `postfix-config.sh` has run):
|
||||
|
||||
```
|
||||
postconf -h queue_run_delay minimal_backoff_time maximal_backoff_time maximal_queue_lifetime bounce_queue_lifetime delay_warning_time
|
||||
```
|
||||
|
||||
`postconf -h`, not a parse of `/etc/postfix/main.cf`: stock values are not
|
||||
written to the file. `postconf` is the effective config, including a manual
|
||||
override.
|
||||
|
||||
- Fixed argv, no user input — same pattern as
|
||||
[postfix.Queue](../../internal/postfix/queue.go) /
|
||||
[security.md](../security.md).
|
||||
- Cache on `handlers.Config` (via `web.Config`). The HTMX fragment does not
|
||||
call `postconf`.
|
||||
- A live `postconf -e` is visible after the next panel (or container) restart.
|
||||
While the process is up, the panel shows the start-up snapshot.
|
||||
- Parse Postfix time units (`300s`, `5d`, `1h`, a bare number is seconds) in
|
||||
`internal/postfix`. Format human strings (`5 minutes`, `5 days`,
|
||||
`about 1 hour 7 minutes`) in one place so the Mail queue card and
|
||||
`deliveryEvents` cannot drift.
|
||||
- If `postconf` is missing (unit tests on Windows, binary outside the
|
||||
container): log a warning, fall back to Postfix 3.x compiled-in defaults
|
||||
(`300s` / `4000s` / `5d` / `0`), and put a muted note on the card. Tests
|
||||
stub the lookup (as `queueIDs` in the log-tailer) or pass a fixture on
|
||||
`Config`. Do not fail panel start.
|
||||
|
||||
Typical stock values, for orientation only — the UI prints whatever
|
||||
`postconf` returned:
|
||||
|
||||
| Parameter | Stock | Meaning |
|
||||
|---|---|---|
|
||||
| `queue_run_delay` / `minimal_backoff_time` | `300s` | First retry and deferred-queue scan |
|
||||
| `maximal_backoff_time` | `4000s` | Cap on the doubling gap (~1 h 7 min) |
|
||||
| `maximal_queue_lifetime` | `5d` | Then bounce |
|
||||
| `delay_warning_time` | `0` | No delay warning to the sender |
|
||||
|
||||
## Panel copy
|
||||
|
||||
Mail queue card facts: first retry; later retries (doubling, capped);
|
||||
kept in queue; then bounced. Short prose: there is no fixed attempt count; a
|
||||
`deferred` message stays in this listing until it is delivered or the queue
|
||||
lifetime runs out.
|
||||
|
||||
`deliveryEvents(row, policy)`:
|
||||
|
||||
- `deferred`: retries, first after X, then with increasing gaps up to Y, for
|
||||
up to Z.
|
||||
- `bounced`: or Postfix gave up after Z in the queue.
|
||||
|
||||
## Tests
|
||||
|
||||
- Duration parser: `5d`, `300s`, `4000s`, `1h`, `0`, bare number.
|
||||
- `/mail-queue` handler: card shows the fixture policy's human strings, not a
|
||||
live `postconf`.
|
||||
- Delivery page / `deliveryEvents`: `deferred` and `bounced` contain those
|
||||
strings ([handlers_monitor_test.go](../../internal/web/handlers/handlers_monitor_test.go)).
|
||||
- [templates_test.go](../../internal/web/view/templates_test.go): pass the new
|
||||
fields if rendering `mail_queue` requires them.
|
||||
|
||||
`go test` / `go vet` on the touched packages.
|
||||
|
||||
## Done when
|
||||
|
||||
- `/mail-queue` states this Postfix's first retry, backoff cap, and queue
|
||||
lifetime.
|
||||
- A `deferred` / `bounced` delivery page uses the same intervals.
|
||||
- A manual `postconf -e maximal_queue_lifetime=2d` followed by a panel restart
|
||||
changes what the panel prints.
|
||||
- Guide and architecture describe the snapshot; CHANGELOG has an Added entry.
|
||||
|
||||
## Risks
|
||||
|
||||
- Showing compiled-in fallbacks when `postconf` failed would mislead if the
|
||||
operator had overridden them — mitigate with the muted note on the card.
|
||||
- Inventing a max-attempt count would be false; the copy must stay time-based.
|
||||
+24
-1
@@ -29,6 +29,7 @@ in `git log` and [CHANGELOG.md](../CHANGELOG.md).
|
||||
| ID | Topic | Status | Plan |
|
||||
|---|---|---|---|
|
||||
| code-review | Full-tree review follow-ups (authz, docs, GUI, tests) | **agreed** | [plans/code-review.md](plans/code-review.md) |
|
||||
| queue-retries | Postfix retry policy in the panel (queue lifetime, backoff) | **agreed** | [plans/queue-retries.md](plans/queue-retries.md) |
|
||||
| inbound-relay | Inbound relay (backup-MX / forwarding) | **agreed** | [plans/inbound-relay.md](plans/inbound-relay.md) |
|
||||
| contributing | `CONTRIBUTING.md` | candidate | — |
|
||||
| dmarc-reports | DMARC aggregate report ingestion and panel UI | candidate | [plans/dmarc-reports.md](plans/dmarc-reports.md) |
|
||||
@@ -37,7 +38,8 @@ in `git log` and [CHANGELOG.md](../CHANGELOG.md).
|
||||
|
||||
**Recommended order** (not binding): **code-review P0** first (shipped
|
||||
domain-admin send-log leak — a defect, not a feature), then the rest of that
|
||||
plan as listed, then **inbound-relay**. Candidates need explicit agreement
|
||||
plan as listed; **queue-retries** is a small panel item that can land in
|
||||
parallel after P0; then **inbound-relay**. Candidates need explicit agreement
|
||||
before they join the queue.
|
||||
|
||||
After a context reset, pick an item marked `agreed` or `in progress`, then work
|
||||
@@ -64,6 +66,27 @@ jumps the feature queue. Implementation models are in the plan (Opus / Sonnet
|
||||
|
||||
---
|
||||
|
||||
## queue-retries
|
||||
|
||||
**Goal:** show on Mail queue and on a delivery's history how this Postfix
|
||||
retries deferred mail — first delay, backoff cap, queue lifetime — reading
|
||||
the effective config (`postconf -h`) once at panel start so a manual
|
||||
override is visible.
|
||||
|
||||
**Boundary:** explanation only. Postfix stays as-is; no attempt counter, no
|
||||
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).
|
||||
|
||||
**Dependencies / risks:** `postconf` unavailable outside the container
|
||||
(fallback + muted note). Copy must stay time-based — Postfix has no max
|
||||
attempt count.
|
||||
**Version:** patch.
|
||||
|
||||
---
|
||||
|
||||
## inbound-relay
|
||||
|
||||
**Goal:** optional acceptance of mail on port 25 for explicitly configured
|
||||
|
||||
Reference in New Issue
Block a user