Files
selfpost/docs/plans/panel-notifications.md
T
mixandClaude Opus 5.5 ce7b09a65c
test / test (push) Canceled after 0s
release: 2.3.0: the panel writes to its users
panel-notifications (docs/plans/panel-notifications.md, 10/10), at the
owner's word (2026-10-12, "Да").

  - Four things are reported by e-mail, each once: a sending domain's
    DNS record that does not match, a level-2 rate limit refusing mail,
    the TLS certificate about to expire, a message more than three days
    in the queue. Global users are told all of it, domain users about
    their own domains.
  - Sent from postmaster@<SELFPOST_HOSTNAME> and signed with a DKIM key
    of the server's own name, which the panel generates.
  - Account has the user's switch, Server > Settings the instance's and
    the two DNS records to publish, Server > Health their state.
  - An account e-mail may now be at a public mail host.
  - Migrations 0004 and 0005.

Upgrading: publish the two records Server > Settings shows for the
hostname (a DKIM key and "v=spf1 a -all"); until then notifications are
sent and may be filed as spam. Every user with an e-mail starts with
notifications on. non_smtpd_milters now names OpenDKIM.

Accepted and recorded in security.md: an application can have a message
signed as the server's own name by writing it into From: (roadmap
candidate from-header-binding).

Cut: CHANGELOG [Unreleased] -> [2.3.0]; image pin 2.3.0 in compose,
README, guide; version table, roadmap, plan status. No tag: the owner
tags when he decides to (2026-10-11), so there is no image of this
version until then.

On the test server, on f09b60a: go vet ./... ok, go test ./... ok
(23 packages), make e2e ok.

Full detail: CHANGELOG.md (2.3.0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 17:08:37 +03:00

120 lines
7.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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; 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)