panel-notifications: guide, architecture, security; refusals looked for every minute

guide.md gains Notifications: the four things, when each is said and said
again, who is told, the two switches, and the two DNS records of the server's
own name that make the mail arrive. architecture.md: the notifier as the
fifth role, its readings, state, recipients and sending; the server's own key
in OpenDKIM's tables; non_smtpd_milters. security.md: the panel submits mail
of its own - how, to whom, and what text from outside can reach a body.

That text - a queued message's sender, a DNS finding - is reduced to
printable ASCII and cut before it is printed. Refused mail is looked for
every minute instead of every five: it is one indexed query, and a refusal is
mail not being sent now. The image's note on the panel's postdrop membership
no longer calls it read-side only.

go test ./internal/notify passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mix
2026-10-12 16:00:06 +03:00
co-authored by Claude Opus 5.5
parent 50fd6b859f
commit c9027381e5
8 changed files with 149 additions and 14 deletions
+52 -1
View File
@@ -156,9 +156,17 @@ smtp/inet` removes Debian's stock smtpd). Outbound delivery still uses the
Per-domain keys under `/data/opendkim/keys`; `KeyTable` / `SigningTable` maintained
by the panel. Socket `/run/opendkim/opendkim.sock`.
The server's own name (`SELFPOST_HOSTNAME`) has a key and a line in both
tables too, without being a row of the registry
(`domain.Service.SetServerName`): the panel's notifications are sent as
`postmaster@` that name and signed with it. No application can send as it -
the sender map is built from the registry. A sending domain of the same name
shares its key and selector with the server, and deleting that domain leaves
the key in place.
### Panel binary ([cmd/panel](../cmd/panel))
One process, four roles:
One process, five roles (six with the inbound relay on):
1. **HTTP server** — `:8080` (`PANEL_HTTP_ADDR`); HTTPS terminated by reverse
proxy only. On start it runs `postconf -h` once for the deferred-mail retry
@@ -188,8 +196,51 @@ One process, four roles:
page (§ Log tailer, *Stored lines*).
4. **rate-limit recalc** — every six hours (and on demand from Domain settings and
the application form), recomputes level-2 **Auto** rate limits from send-log statistics.
5. **notifier** — writes to the panel's users when something needs them
(§ Notifications below).
Milter chain in Postfix: OpenDKIM (tempfail) then journal (accept on failure).
Mail submitted locally with `sendmail(1)` — the notifier's — passes OpenDKIM
alone (`non_smtpd_milters`): it is signed, and it is not an application's
mail to be journalled or limited. Bounces Postfix generates pass no milter.
### Notifications
[internal/notify](../internal/notify/notify.go) reports four things to the
users of the panel: a sending domain's DNS record that stopped matching, a
level-2 rate limit that refused mail, the TLS certificate about to expire, a
message more than three days in the queue. The rules of when a message goes
out are the owner's and are written in
[plans/panel-notifications.md](plans/panel-notifications.md) § When a message
is sent; the operator's view is guide § Notifications.
- **Readings.** Once an hour, starting three minutes after the process does:
every sending domain's records through a DNS checker of the notifier's own,
forced past the cache (`cmd/panel/notifier.go` builds the same query a
domain's page does); the certificate file; `postqueue -j` for the arrival
time of each queued message. Once a minute: the send log's `rejected` rows
of the last day, per domain and application.
- **State** is a row per subject in `notify_state` (`dns:<domain>`, `cert`,
`queue`, `limit:<domain>:<login>`), which is what makes each thing be said
once and a restart say nothing again. A DNS subject goes *pending* on the
first bad check and *open*, with a message, on the second; back the same
way. The certificate's row remembers which threshold was said for which
expiry. A deleted domain's row goes with it.
- **No information is not a finding.** A lookup that did not answer, a queue
that cannot be listed, a certificate that cannot be read move no state.
- **Recipients** come from the store at the moment of sending: global users
for everything, domain users for subjects that are a domain's; only users
with an e-mail and their own switch on. One message per recipient, so users
do not see each other's addresses.
- **The instance switch** (settings key `notifications`, on unless turned
off) stops the checks themselves, not only the mail: no state moves while
it is off.
- **Sending** is `sendmail -i -f postmaster@<hostname> -- <address>` as the
unprivileged panel user, which is a member of `postdrop` and so can write
the maildrop directory without the setgid bit `no-new-privileges` forbids.
A message that could not be handed over leaves the state where it was and
is tried at the next check. The text names the page to open and has no
link: the panel does not know its own address.
### Log tailer and `mail.log` rotation