audit 4: cards ordered by number, a notification owed to each recipient
test / test (push) Canceled after 0s
test / test (push) Canceled after 0s
The cards layout (owner, 2026-10-13; the mockup first, b90a2f0): box_open
writes data-order for a box with InCards and a number, and panel.css orders
by it up to twelve - Health's eight and the kit's three.
TestCardsBoxesCarryTheirNumberAsTheirOrder reads the rendered pages.
Notifications (owner, 2026-10-14): migration 0011, notify_addressees. open
sends a subject's first message to every recipient and records who was
handed it; followUp, on every check while the subject is open and still
wrong, hands it to the owed and untold who are still recipients; closed
sends the end to the told (all recipients for a subject from before the
table). Tests: a failed copy goes out at the next check, once; the hourly
ceiling holds one back; a user added mid-subject hears nothing of it.
Also in the roadmap, from the owner today: dns-record-collapse,
notification-mail-recheck, settings-save-row (agreed, design first).
Tested on the test server: go vet + go test of 25 packages, make e2e (all
pass, 644 s) on this tree.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -393,8 +393,15 @@ is sent; the operator's view is guide § Notifications.
|
||||
deleted domain's row goes with it.
|
||||
- **Said means sent.** A problem that arises while no user can be told, or
|
||||
while Postfix does not take the message, stays unsaid and is told at a
|
||||
later check. Its ending, with nobody left to tell, is simply over. If one
|
||||
recipient's copy fails and another's goes out, the subject is said.
|
||||
later check. Its ending, with nobody left to tell, is simply over.
|
||||
- **Owed to each recipient** (since `2.5.4`, `notify_addressees`): the
|
||||
subject's first message records who it was addressed to and who was
|
||||
handed a copy; while the subject is open, every later check hands it to
|
||||
those not told yet - a copy that failed, or that the ceiling held back -
|
||||
who are still recipients. A user who comes to hear of a subject only while
|
||||
it is open is not mailed about it, and its ending goes to those who were
|
||||
told. A subject opened before `2.5.4` has no such record, and its ending
|
||||
goes to every recipient, as it did.
|
||||
- **The notifier's own mail is not a subject.** A notification waiting for a
|
||||
mailbox that does not answer, or its bounce coming back to
|
||||
`postmaster@<hostname>` - which receives no mail - is left out of the
|
||||
|
||||
@@ -1283,7 +1283,7 @@ command whose leftover is the owner's alone.
|
||||
`mi_rd_cmd`, go-milter v0.4.1 `HandleMilterCommands` on `io.EOF` - the
|
||||
comment on CheckSocket cites them). The socket nobody listens on is
|
||||
`milter_test.go`.
|
||||
- [ ] **The `cards` layout takes its phone order from the box's number**
|
||||
- [x] **The `cards` layout takes its phone order from the box's number**
|
||||
(owner, 2026-10-13, from the Not done list of from-header-binding). The
|
||||
order on a phone is written in `panel.css` for four boxes to a stack
|
||||
(`.sp-cards > .column:first-child > .box:nth-child(1..4)`); a fifth box
|
||||
@@ -1293,7 +1293,15 @@ command whose leftover is the owner's alone.
|
||||
checks that the numbers of a rendered `cards` page run in order. A change
|
||||
to a component: the mockup's components page and `theme.css` first
|
||||
(`design:` commit). - **Sonnet**
|
||||
- [ ] **A notification is owed to each recipient, not to the subject**
|
||||
*Done 2026-10-15:* an attribute, not a class: the mockup's check
|
||||
(`check.py`) refuses a class `theme.css` defines and no page uses, and a
|
||||
class per number up to twelve would be eight such. `data-order="N"` is
|
||||
not a style either, so the CSP holds; `theme.css` and `panel.css` have
|
||||
`.sp-cards [data-order="1..12"]`. `box_open` writes it for a box with
|
||||
`InCards` (Health's eight, the kit's three) and a number.
|
||||
`TestCardsBoxesCarryTheirNumberAsTheirOrder` reads the rendered pages:
|
||||
each box's data-order is its number, and the stylesheet has its rule.
|
||||
- [x] **A notification is owed to each recipient, not to the subject**
|
||||
(owner, 2026-10-14; from the reviews of panel-notifications, not found by
|
||||
the audit). `Notifier.send` ([notify.go](../internal/notify/notify.go))
|
||||
counts a subject as said when one recipient's copy was handed to Postfix;
|
||||
@@ -1307,6 +1315,17 @@ command whose leftover is the owner's alone.
|
||||
open is not mailed about it. "Cleared" goes to those who were told of the
|
||||
problem. Tests: one copy fails and goes out at the next check, once; the
|
||||
ceiling; a user added mid-subject. - **Opus**
|
||||
*Done 2026-10-15:* migration `0011`, `notify_addressees (key, address,
|
||||
told_at)`, gone with the subject's row. `open` sends the first message
|
||||
to every recipient and records who was handed it; `followUp`, on every
|
||||
check while the subject is open and still wrong (DNS bad, the
|
||||
certificate at its level, the queue still stuck, a limit within its
|
||||
day), hands the current message to the owed and untold who are still
|
||||
recipients; `closed` sends the end to the told (all recipients for a
|
||||
subject opened before the table). A cleared copy that fails is not
|
||||
retried - the subject is gone with it, as before. Tests: a failed copy
|
||||
goes out at the next check, once; the ceiling holds one back; a user
|
||||
added mid-subject hears nothing of it, its end included.
|
||||
- [ ] **A DNS outage is "unknown", not "wrong"** (M14, M15; owner,
|
||||
2026-10-14 - the two findings had no step). Each resolver gets a time
|
||||
budget of its own, so a silent first resolver no longer leaves the second
|
||||
|
||||
@@ -115,5 +115,5 @@ to the owner before any code ([panel-redesign.md](panel-redesign.md)
|
||||
- [x] Panel: Account, Settings, Health card, help
|
||||
- [x] guide.md, architecture.md, security.md, schema-migrations.md
|
||||
- [x] `go vet`, `go test`, e2e: a level-2 refusal reaches a mailbox as a signed notification (on the test server: vet ok, 23 packages ok, e2e ok; "once" and "clears" are held by the unit tests of `internal/notify`)
|
||||
- [x] Technical review — **Opus** (2026-10-12; no Critical. Fixed: refusals counted by the registered domain and only under a limit, with an index of their own; account e-mails may be at public mail hosts; "said" means sent; DNS checks must be 45 minutes apart; the notifier's own mail is not a queue subject and a cut or unreadable listing is no listing; the tables must name the selector the panel shows; an unanswered `a`/`mx` lookup is not an SPF mismatch; a pass stops between domains at shutdown; days are rounded as a person counts. Left as found: a recipient whose copy failed while another's went out is not retried; a certificate that lives under 14 days is reported at each renewal)
|
||||
- [x] Technical review — **Opus** (2026-10-12; no Critical. Fixed: refusals counted by the registered domain and only under a limit, with an index of their own; account e-mails may be at public mail hosts; "said" means sent; DNS checks must be 45 minutes apart; the notifier's own mail is not a queue subject and a cut or unreadable listing is no listing; the tables must name the selector the panel shows; an unanswered `a`/`mx` lookup is not an SPF mismatch; a pass stops between domains at shutdown; days are rounded as a person counts. Left as found: a recipient whose copy failed while another's went out is not retried (since 2.5.4 it is: audit 2026-10-12, stage 4); a certificate that lives under 14 days is reported at each renewal)
|
||||
- [x] Security review — **Fable** (2026-10-12; no Critical. Fixed: the limit notification was built on the domain a client writes in `MAIL FROM` - a mail cannon and a way to write to another tenant's users; a ceiling of 60 messages an hour; [security.md](../security.md) corrected on who can submit locally and on what the account e-mail now is. Recorded as accepted, with a roadmap candidate: an application can have a message signed as the server by writing its name into `From:` - the header is not held against the login, for sending domains either)
|
||||
|
||||
+59
-1
@@ -38,11 +38,14 @@ in `git log` and [CHANGELOG.md](../CHANGELOG.md).
|
||||
| delivery-log-storage | Keep a message's `mail.log` lines with its send-log row | shipped `2.2.0` | 8/8 | [plans/delivery-log-storage.md](plans/delivery-log-storage.md) |
|
||||
| password-reset | Password reset by e-mail | agreed (2026-10-13) | — | — |
|
||||
| local-time | The panel shows time, and counts its days, in a time zone other than UTC | agreed (2026-10-13; design questions open) | — | — |
|
||||
| notification-mail-recheck | The Notification mail records on Server › Settings can be checked again from where they are shown | agreed (2026-10-15) | — | — |
|
||||
| settings-save-row | Server › Settings: the Save button no longer stands alone under a long right column | agreed (2026-10-15) | — | — |
|
||||
| dns-record-collapse | A DNS record that is right is shown folded: its name and its tag, the values on request | agreed (2026-10-15) | — | — |
|
||||
| phone-desktop-mode | A phone browser in "desktop site" mode gets the phone navigation | agreed (2026-10-13: after stages 1 and 2 of audit-2026-10-12) | — | — |
|
||||
| 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`, 2b `2.5.1`, 2c `2.5.2`, 3 `2.5.3`; next stage 4 → `2.5.4`) | 62/81 | [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`, 2b `2.5.1`, 2c `2.5.2`, 3 `2.5.3`; next stage 4 → `2.5.4`) | 64/81 | [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
|
||||
@@ -697,3 +700,58 @@ joins the index only when the owner says so.
|
||||
[product.md](product.md). Notes and open questions:
|
||||
[plans/inbound-quarantine.md](plans/inbound-quarantine.md); the screens
|
||||
`in-quarantine` and `in-quarantine-message` are drawn and stay unbuilt.
|
||||
|
||||
## dns-record-collapse
|
||||
|
||||
**Problem (owner, 2026-10-15):** a DNS record block - the DKIM, SPF and
|
||||
DMARC records on an outbound domain's page, the server's records on
|
||||
Server › Settings, the MX of an inbound domain - shows the host, the type
|
||||
and the value to publish whether or not the record is already right. When
|
||||
DNS holds the record as it should be, the values are of no use on the page:
|
||||
they only push the records that need work further down.
|
||||
|
||||
**Goal:** a record whose check is *ok* is drawn folded - its name and its
|
||||
tag, one line - and opens on request to show the host, type and value (to
|
||||
copy them again, or to compare). A record that is missing, wrong or not
|
||||
checked yet stays open, as it is now, with *In DNS now* where it differs.
|
||||
|
||||
**Notes:** the block is the one `dns_record` partial (components.html), so
|
||||
the change is made once; it is a design change, mockup first (the kit's
|
||||
dns_record section and the four pages that draw records), wording
|
||||
included. No JavaScript needed for the folding itself: `<details>` opens
|
||||
and closes without it under the panel's CSP. Whether a fold remembers being
|
||||
opened across the polls of a page, and whether *Re-check DNS* folds a record
|
||||
that has just turned right, are for the mockup.
|
||||
|
||||
## notification-mail-recheck
|
||||
|
||||
**Problem (owner, 2026-10-15):** Server › Settings shows the DNS records of
|
||||
the server's own name in its *Notification mail* box (DKIM and SPF, with
|
||||
their tags), and nothing on that page says how to check them again after
|
||||
publishing. The way exists elsewhere: *Re-check DNS* on Server › Health
|
||||
re-runs the server's checks, these records' included
|
||||
(`HandleHealthRecheck` → `serverMail(true)`), and Health has a Notification
|
||||
mail card of its own - but from Settings it cannot be found.
|
||||
|
||||
**Goal:** the records can be checked again from where they are shown - a
|
||||
*Re-check DNS* action in the box's head, or at least the box saying where
|
||||
the check is and how long a result is kept. Design change, mockup first;
|
||||
decide together with [dns-record-collapse](#dns-record-collapse) (a record
|
||||
that turns right on the re-check folds).
|
||||
|
||||
## settings-save-row
|
||||
|
||||
**Problem (owner, 2026-10-15, a screenshot of `selfpost.mixfed.ru`):** on
|
||||
Server › Settings the *Save settings* button sits under the two columns, at
|
||||
the left, after a long gap: the left column (retention, sending rate limits)
|
||||
is short, the right one (the notifications switch, then *Notification mail*
|
||||
with its records open) is long. The button saves the retention fields and
|
||||
the switch; the records under the switch are not fields, and they are what
|
||||
pushes the button down.
|
||||
|
||||
**Goal:** the button reads as the save of the fields it saves. Either the
|
||||
gap goes by itself when [dns-record-collapse](#dns-record-collapse) folds
|
||||
the records that are right, or the page changes - the records out of the
|
||||
form's columns, or the button nearer its fields. Decide with the mockup,
|
||||
after dns-record-collapse, on a page whose records are missing as well as
|
||||
on one whose records are right.
|
||||
|
||||
@@ -17,8 +17,8 @@ When you add or review a migration, update this file in the same change.
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Chain head | `user_version = 10` |
|
||||
| Files | `0001_init.sql` (the 2.0 baseline), `0002_inbound_spam_log.sql`, `0003_send_log_lines.sql`, `0004_notifications.sql`, `0005_send_log_rejected_index.sql`, `0006_sender_check.sql`, `0007_delivery_log_days.sql`, `0008_tenant_boundary.sql`, `0009_refusal_line.sql`, `0010_send_log_indexes.sql` |
|
||||
| Chain head | `user_version = 11` |
|
||||
| Files | `0001_init.sql` (the 2.0 baseline), `0002_inbound_spam_log.sql`, `0003_send_log_lines.sql`, `0004_notifications.sql`, `0005_send_log_rejected_index.sql`, `0006_sender_check.sql`, `0007_delivery_log_days.sql`, `0008_tenant_boundary.sql`, `0009_refusal_line.sql`, `0010_send_log_indexes.sql`, `0011_notify_addressees.sql` |
|
||||
| File mark | `PRAGMA application_id = 0x53503230` ("SP20") |
|
||||
| Database file | `/data/selfpost.db` (bind mount) |
|
||||
| Compatibility | 2.x MINOR releases must boot a `2.0.0` data directory. **Nothing is carried over from 1.x.** |
|
||||
@@ -68,7 +68,8 @@ delivery-log-storage (`2.2.0`), `0004_notifications.sql` and
|
||||
`0006_sender_check.sql` of from-header-binding (`2.4.0`), and
|
||||
`0007_delivery_log_days.sql` and `0008_tenant_boundary.sql` of the 2026-10-12
|
||||
audit (`2.5.0`), `0009_refusal_line.sql` (`2.5.1`) and
|
||||
`0010_send_log_indexes.sql` (`2.5.3`).
|
||||
`0010_send_log_indexes.sql` (`2.5.3`) and `0011_notify_addressees.sql`
|
||||
(`2.5.4`).
|
||||
|
||||
---
|
||||
|
||||
@@ -86,6 +87,7 @@ audit (`2.5.0`), `0009_refusal_line.sql` (`2.5.1`) and
|
||||
| 8 | `0008_tenant_boundary.sql` | 2.5.0 | DDL + data | `idx_applications_login_nocase`: an application login is unique without regard to case (audit 2026-10-12, H4). `send_log.domain_id` and `inbound_spam_log.inbound_domain_id` (`ON DELETE SET NULL`), each with an index, filled once by name from the registry as it was then: a row belongs to a domain by id, and a domain deleted and added again starts with an empty log (L12) |
|
||||
| 9 | `0009_refusal_line.sql` | 2.5.1 | DDL | `send_log.refusal_line`: Postfix's line about a refusal at MAIL FROM (a level-2 limit, a client address off the list), which has no queue id; the log tailer puts it on the row; `send_log.recovered`, a refusal row the tailer wrote from `mail.log` because the panel stopped before writing it; `sender_check_days.messages` (messages deferred mid-message, counted apart from the connections turned away) and `sender_check_days.estimated` (a day whose downtime is an upper bound) (audit 2026-10-12, stage 2b) |
|
||||
| 10 | `0010_send_log_indexes.sql` | 2.5.3 | DDL | Four indexes of `send_log`, each for a question that walked the table or sorted a domain's log (audit 2026-10-12, M2, stage 3): `idx_send_log_open`, partial, the queued and deferred rows by time (the tailer's sweep every 5 min); `idx_send_log_domain_id` and `idx_send_log_app_login`, a page of one domain's or one application's log in id order; `idx_send_log_refusal_line`, partial, whether a refusal line is already on a row. `TestSendLogQueriesUseTheirIndexes` checks the plans of these and of the other by-time and by-scope statements the store runs |
|
||||
| 11 | `0011_notify_addressees.sql` | 2.5.4 | DDL | `notify_addressees (key, address, told_at)`: who a subject's notification is owed to and who was handed it, so a copy that failed or was held back by the hourly ceiling goes out at a later check and a subject's end goes to those told (audit 2026-10-12, stage 4); deleted with its `notify_state` row |
|
||||
|
||||
**Kind:** *DDL* — schema only; *data* — `INSERT`/`UPDATE` that must stay correct
|
||||
for operators upgrading from older 2.x images.
|
||||
|
||||
Reference in New Issue
Block a user