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>
120 lines
7.1 KiB
Markdown
120 lines
7.1 KiB
Markdown
# Plan: panel-notifications
|
||
|
||
**Status:** shipped in `2.3.0` (2026-10-12); agreed the same day - the design questions were closed with the owner and he took it into work ("Делаем")
|
||
**Date:** 2026-10-12
|
||
**Version:** `2.3.0` MINOR (one migration; opt-out by a switch).
|
||
|
||
---
|
||
|
||
## Goal
|
||
|
||
The panel writes to its administrators when something needs them, instead of
|
||
waiting to be opened.
|
||
|
||
## Decisions (owner, 2026-10-12)
|
||
|
||
### Events of the first version
|
||
|
||
Four, each of them something the panel can already tell and a person has to
|
||
act on:
|
||
|
||
| Event | Who is told |
|
||
|---|---|
|
||
| A sending domain's DNS record stopped matching (SPF, DKIM, DMARC) | global users, and `domain` users the domain is assigned to |
|
||
| A level-2 rate limit refused mail | the same, for the domain of the limit |
|
||
| The TLS certificate is about to expire | global users |
|
||
| A message has been in the queue for more than **three days** | global users |
|
||
|
||
Three days, not one (owner: "one day is not that much"): Postfix keeps a
|
||
message five, so two are left to act before it bounces. The threshold is a
|
||
constant, not a setting.
|
||
|
||
Left for later: a process or milter socket down (a relay in that state often
|
||
cannot send), an inbound upstream unreachable, DMARC failures appearing
|
||
(noisy). Mail held in quarantine is not an event: quarantine is an idea
|
||
outside the roadmap.
|
||
|
||
### Sender and path
|
||
|
||
From `postmaster@<SELFPOST_HOSTNAME>`, submitted locally. Not from one of the
|
||
sending domains (owner): with several domains the choice is arbitrary, and a
|
||
message about the certificate has no business coming from a domain.
|
||
|
||
These messages have to arrive (owner: "we need the best deliverability for
|
||
them"), so the server's own name gets what a sending domain has:
|
||
|
||
- a **DKIM key for the hostname**, generated by the panel, with which
|
||
notifications are signed - no domain has to be chosen for it. An SPF record
|
||
alone would pass DMARC, but not after the user's mailbox forwards the
|
||
message on, and large receivers treat unsigned mail more strictly;
|
||
- an **SPF record** for the hostname, `v=spf1 a -all`.
|
||
|
||
The records themselves are under **Server › Settings** (owner: "the DNS
|
||
records go to Settings, Health has only the status"), in a box of their own
|
||
drawn with the record component of a domain's DNS page: host, value to
|
||
publish, copy buttons, state. **Server › Health** has a card of its own,
|
||
*Notification mail*, with the state of each record and a link to Settings.
|
||
A DMARC record for the hostname is not asked for: the name inherits its
|
||
parent domain's policy, and with SPF and DKIM aligned it passes any.
|
||
Without the records notifications are still sent, and the card warns.
|
||
|
||
Notifications are the panel's own mail: they are not rows of the Outbound log
|
||
and count against no level-2 limit.
|
||
|
||
A relay that cannot send cannot say so; none of the four events is "the relay
|
||
is down", and that is why.
|
||
|
||
### Who chooses what
|
||
|
||
- One switch for the instance under **Server › Settings**.
|
||
- One checkbox per user under **Account**, "send me notifications" - on when
|
||
the profile has an e-mail. No choice per event: four events, none of them
|
||
noise. Per-event choice comes when there are more and some are.
|
||
- A user without an e-mail gets nothing.
|
||
|
||
### When a message is sent
|
||
|
||
A background check once an hour for DNS, certificate and queue.
|
||
|
||
| Event | Sent | "Cleared" message |
|
||
|---|---|---|
|
||
| DNS mismatch | after two checks in a row found it (it has held for an hour). A resolver that did not answer is not a mismatch | after two good checks in a row |
|
||
| Certificate | 14 days before expiry, and again 3 days before if it is still the same certificate | none |
|
||
| Level-2 limit refused mail | at the first refusal of a sending domain's applications, then at most once a day for that domain (one subject per domain, as built: what the refused row says of its domain is the client's own word, so the registry is asked) | none |
|
||
| Queue older than three days | when such a message appears | one, when none is left |
|
||
|
||
Event state is kept in the database, so a restart announces nothing a second
|
||
time.
|
||
|
||
## To settle while building
|
||
|
||
- How the panel submits: `sendmail` as the unprivileged panel user, and what
|
||
that needs of the image.
|
||
- Whether a notification carries a link to the panel - there is no setting
|
||
for the panel's own address.
|
||
- Where the refusal of a level-2 limit is noticed: in the journal milter as
|
||
it refuses, or from the `rejected` rows of the send log on the hourly check.
|
||
- Schema: the user's checkbox, the instance switch, the event state table.
|
||
|
||
## Screens
|
||
|
||
Not drawn. The Notifications box was taken out of the `account` and
|
||
`settings` mockups on 2026-10-10 (owner: 2.0 ships no box for a feature that
|
||
is not built). Drawing them again - one checkbox on Account, one switch on
|
||
Server › Settings, the SPF check on Server › Health - is the first step, shown
|
||
to the owner before any code ([panel-redesign.md](panel-redesign.md)
|
||
§ Enforcement).
|
||
|
||
## Implementation checklist
|
||
|
||
- [x] Mockups: Account checkbox, Settings switch and records, Health status card, help topic; accepted by the owner 2026-10-12 (`design:` commit)
|
||
- [x] Migration and store: user flag, instance switch, event state
|
||
- [x] Sending: local submission from `postmaster@<hostname>`, message text (`sendmail` as the panel user; signed through `non_smtpd_milters` with a key of the server's own name; no link in the text - the panel does not know its own address)
|
||
- [x] Background checker: DNS, certificate, queue; the rules of *When a message is sent*
|
||
- [x] Level-2 refusal event (from the `rejected` rows of the send log, looked at every minute)
|
||
- [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 (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)
|