From fc53ad61eeb5876b3485c6e4164851dd2676d933 Mon Sep 17 00:00:00 2001 From: Mix Date: Sat, 10 Oct 2026 12:38:35 +0300 Subject: [PATCH] design: a domain's report address is one of three choices; row actions are POSTs Two decisions of the owner, 2026-10-10. The report address. "Three choices: no reports, the SelfPost address, a typed address. No reports by default. A button that fills the typed address with one's own e-mail from the profile." This replaces the 2026-09-21 design in which a domain followed a named user's default: a co-administrator of a domain was shown another user's login name, and three explicit choices are simpler. - src/out-domain-settings.html: the box is a select of the three choices and the address field with a "Use my e-mail" button. - src/out-domain.html: the DMARC record's note names the address, with no user in it. - src/account.html: the DMARC reports box is gone; Account is Profile and Password, one box per column. - src/settings.html: the lead no longer points at Account for the report address. - docs/plans/panel-redesign.md: Account e-mail rewritten, the Routes row of account without /account/dmarc. docs/schema-migrations.md: the columns that go. Row actions. New password and Delete in a row of the Applications table are POSTs, so each is a form with a button that looks like the links beside it - as Sign out became one. - src/out-domain.html: Edit / New password / Delete per row, the box foot about confirmations is back; the login cell does not wrap. - theme.css: .table td.sp-actions form sits inline with the links' spacing, and its button takes the link's colour and type. - src/components.html shows the pattern once. check.py: 0 violations. outlines.json: only account changed (two equal columns, one box each). Co-Authored-By: Claude Opus 5.5 --- docs/assets/panel-redesign/panel/account.html | 37 ++----------- .../panel-redesign/panel/components.html | 2 +- .../panel/out-domain-settings.html | 12 ++--- .../panel-redesign/panel/out-domain.html | 12 ++--- .../assets/panel-redesign/panel/outlines.json | 9 ++-- .../assets/panel-redesign/panel/settings.html | 2 +- .../panel-redesign/panel/src/account.html | 37 ++----------- .../panel-redesign/panel/src/components.html | 2 +- .../panel/src/out-domain-settings.html | 12 ++--- .../panel-redesign/panel/src/out-domain.html | 12 ++--- .../panel-redesign/panel/src/settings.html | 2 +- docs/assets/panel-redesign/panel/theme.css | 9 +++- docs/plans/panel-redesign.md | 53 ++++++++++--------- docs/schema-migrations.md | 27 ++++------ 14 files changed, 89 insertions(+), 139 deletions(-) diff --git a/docs/assets/panel-redesign/panel/account.html b/docs/assets/panel-redesign/panel/account.html index 1dabf75..464cb49 100644 --- a/docs/assets/panel-redesign/panel/account.html +++ b/docs/assets/panel-redesign/panel/account.html @@ -69,24 +69,26 @@

Signed in as admin · global

Account

-

Who you are to this panel and where reports about your domains go. Applications keep their own logins and passwords and are not affected.

+

Who you are to this panel. Applications keep their own logins and passwords and are not affected.

-
+
01

Profile

-

Where the panel writes to you. It can also receive DMARC reports — your choice, under DMARC reports.

+

Your own address. A domain's report address can be filled with it from Domain settings.

+
+
02

Password

@@ -102,35 +104,6 @@
- -
-
-
03

DMARC reports

authorized
-
-
-
-
-

Used by every outbound domain whose report address is set to admin's default — 2 of your 3 now. Changing it changes the DMARC record those domains must publish. The hosted address is parsed by SelfPost; any other just receives the raw XML.

- -
-
-
-

Report authorization

publishedNeeded because the address is on another domain than the senders
-
-
-
-
-
-
-
-
-
-
diff --git a/docs/assets/panel-redesign/panel/components.html b/docs/assets/panel-redesign/panel/components.html index 2bae557..702e20e 100644 --- a/docs/assets/panel-redesign/panel/components.html +++ b/docs/assets/panel-redesign/panel/components.html @@ -172,7 +172,7 @@
- +
Time, UTCNamesp-clipStatus
09-21 14:01:52the row's linkA long subject is clipped with an ellipsis instead of wrapping the rowsentDetailsDelete
09-21 14:01:52the row's linkA long subject is clipped with an ellipsis instead of wrapping the rowsentDetails
Page 1 of 9Older →
diff --git a/docs/assets/panel-redesign/panel/out-domain-settings.html b/docs/assets/panel-redesign/panel/out-domain-settings.html index fdfd0bf..d061ab3 100644 --- a/docs/assets/panel-redesign/panel/out-domain-settings.html +++ b/docs/assets/panel-redesign/panel/out-domain-settings.html @@ -81,13 +81,13 @@
-

A default belongs to a user and is set under their Account; a domain follows one named user, so two people sharing a domain never pull it two ways. Changing this changes the DMARC record to publish.

+

No reports leaves rua= out of the DMARC record. SelfPost hosted sends them to an address on this server, which parses them for DMARC reports. A specific address is any mailbox, and it receives the raw XML. Changing this changes the DMARC record to publish.

+
+
diff --git a/docs/assets/panel-redesign/panel/out-domain.html b/docs/assets/panel-redesign/panel/out-domain.html index 741f967..c9f29f9 100644 --- a/docs/assets/panel-redesign/panel/out-domain.html +++ b/docs/assets/panel-redesign/panel/out-domain.html @@ -132,14 +132,14 @@
-

DMARC

publishedReport address: admin's default
+

DMARC

publishedReport address: example.org@dmarc.mail.example.org
-
+

p=none does not affect delivery. Tighten to quarantine, then reject, once the reports look clean.

@@ -149,10 +149,10 @@
- - - - + + + +
LoginMay send as30 daysLimits
prod-server*@example.org1 102 msg · peak 96/h200 / hEditNew passwordDelete
alertsalerts@example.org, noc@example.org182 msg · peak 14/hdomain 2 IPsEditNew passwordDelete
prod-server*@example.org1 102 msg · peak 96/h200 / hEdit
alertsalerts@example.org, noc@example.org182 msg · peak 14/hdomain 2 IPsEdit
New password and Delete ask for confirmation; the old password stops working at once.
diff --git a/docs/assets/panel-redesign/panel/outlines.json b/docs/assets/panel-redesign/panel/outlines.json index 6e9fe53..458642f 100644 --- a/docs/assets/panel-redesign/panel/outlines.json +++ b/docs/assets/panel-redesign/panel/outlines.json @@ -4,14 +4,13 @@ { "columns": [ { - "is-5": [ - "box[Profile]:body", - "box[Password]:body" + "is-6": [ + "box[Profile]:body" ] }, { - "is-7": [ - "box[DMARC reports]:record×2" + "is-6": [ + "box[Password]:body" ] } ] diff --git a/docs/assets/panel-redesign/panel/settings.html b/docs/assets/panel-redesign/panel/settings.html index a930424..08616cd 100644 --- a/docs/assets/panel-redesign/panel/settings.html +++ b/docs/assets/panel-redesign/panel/settings.html @@ -69,7 +69,7 @@

Server

Settings

-

What holds for the whole instance, whoever is signed in. Your own name, e-mail and DMARC report address are under Account.

+

What holds for the whole instance, whoever is signed in. Your own name, e-mail and password are under Account.

diff --git a/docs/assets/panel-redesign/panel/src/account.html b/docs/assets/panel-redesign/panel/src/account.html index 89f13df..9cff4ff 100644 --- a/docs/assets/panel-redesign/panel/src/account.html +++ b/docs/assets/panel-redesign/panel/src/account.html @@ -3,24 +3,26 @@

Signed in as admin · global

Account

-

Who you are to this panel and where reports about your domains go. Applications keep their own logins and passwords and are not affected.

+

Who you are to this panel. Applications keep their own logins and passwords and are not affected.

-
+
01

Profile

-

Where the panel writes to you. It can also receive DMARC reports — your choice, under DMARC reports.

+

Your own address. A domain's report address can be filled with it from Domain settings.

+
+
02

Password

@@ -36,33 +38,4 @@
- -
-
-
03

DMARC reports

authorized
-
-
-
-
-

Used by every outbound domain whose report address is set to admin's default — 2 of your 3 now. Changing it changes the DMARC record those domains must publish. The hosted address is parsed by SelfPost; any other just receives the raw XML.

- -
-
-
-

Report authorization

publishedNeeded because the address is on another domain than the senders
-
-
-
-
-
-
-
-
-
-
diff --git a/docs/assets/panel-redesign/panel/src/components.html b/docs/assets/panel-redesign/panel/src/components.html index 7a6b717..20c5f4a 100644 --- a/docs/assets/panel-redesign/panel/src/components.html +++ b/docs/assets/panel-redesign/panel/src/components.html @@ -106,7 +106,7 @@
- +
Time, UTCNamesp-clipStatus
09-21 14:01:52the row's linkA long subject is clipped with an ellipsis instead of wrapping the rowsentDetailsDelete
09-21 14:01:52the row's linkA long subject is clipped with an ellipsis instead of wrapping the rowsentDetails
Page 1 of 9Older →
diff --git a/docs/assets/panel-redesign/panel/src/out-domain-settings.html b/docs/assets/panel-redesign/panel/src/out-domain-settings.html index 7fa44cb..48ef7f1 100644 --- a/docs/assets/panel-redesign/panel/src/out-domain-settings.html +++ b/docs/assets/panel-redesign/panel/src/out-domain-settings.html @@ -15,13 +15,13 @@
-

A default belongs to a user and is set under their Account; a domain follows one named user, so two people sharing a domain never pull it two ways. Changing this changes the DMARC record to publish.

+

No reports leaves rua= out of the DMARC record. SelfPost hosted sends them to an address on this server, which parses them for DMARC reports. A specific address is any mailbox, and it receives the raw XML. Changing this changes the DMARC record to publish.

+
+
diff --git a/docs/assets/panel-redesign/panel/src/out-domain.html b/docs/assets/panel-redesign/panel/src/out-domain.html index 57a6133..4267107 100644 --- a/docs/assets/panel-redesign/panel/src/out-domain.html +++ b/docs/assets/panel-redesign/panel/src/out-domain.html @@ -66,14 +66,14 @@
-

DMARC

publishedReport address: admin's default
+

DMARC

publishedReport address: example.org@dmarc.mail.example.org
-
+

p=none does not affect delivery. Tighten to quarantine, then reject, once the reports look clean.

@@ -83,10 +83,10 @@
- - - - + + + +
LoginMay send as30 daysLimits
prod-server*@example.org1 102 msg · peak 96/h200 / hEditNew passwordDelete
alertsalerts@example.org, noc@example.org182 msg · peak 14/hdomain 2 IPsEditNew passwordDelete
prod-server*@example.org1 102 msg · peak 96/h200 / hEdit
alertsalerts@example.org, noc@example.org182 msg · peak 14/hdomain 2 IPsEdit
New password and Delete ask for confirmation; the old password stops working at once.
diff --git a/docs/assets/panel-redesign/panel/src/settings.html b/docs/assets/panel-redesign/panel/src/settings.html index 9d51a12..dda6d38 100644 --- a/docs/assets/panel-redesign/panel/src/settings.html +++ b/docs/assets/panel-redesign/panel/src/settings.html @@ -3,7 +3,7 @@

Server

Settings

-

What holds for the whole instance, whoever is signed in. Your own name, e-mail and DMARC report address are under Account.

+

What holds for the whole instance, whoever is signed in. Your own name, e-mail and password are under Account.

diff --git a/docs/assets/panel-redesign/panel/theme.css b/docs/assets/panel-redesign/panel/theme.css index b07b489..7fd471d 100644 --- a/docs/assets/panel-redesign/panel/theme.css +++ b/docs/assets/panel-redesign/panel/theme.css @@ -161,7 +161,14 @@ code { background: var(--sp-well); color: var(--sp-ink); border-radius: 3px; pad .box > .table-container td:first-child, .box > .table-container th:first-child { padding-left: 1.25rem; } .box > .table-container td:last-child, .box > .table-container th:last-child { padding-right: 1.25rem; } .table td.sp-actions { text-align: right; white-space: nowrap; } -.table td.sp-actions a { margin-left: .9rem; } +/* An action that changes state (New password, Delete) is a POST, so it is a form + with a button, not a link. The form sits inline like the links beside it and + the button is handed the link's colour and type; Bulma already resets the + button and shares the pointer and the focus ring between a and button, and a + link here has no hover colour of its own. */ +.table td.sp-actions a, .table td.sp-actions form { margin-left: .9rem; } +.table td.sp-actions form { display: inline; } +.table td.sp-actions button { color: var(--bulma-link-text); font: inherit; } .table td.sp-clip { max-width: 13rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .tag { font-family: var(--sp-mono); font-weight: 600; letter-spacing: .04em; } .sp-empty { padding: 2.2rem 1.25rem; text-align: center; color: var(--sp-muted); } diff --git a/docs/plans/panel-redesign.md b/docs/plans/panel-redesign.md index 9ebe3f1..ad1d2b2 100644 --- a/docs/plans/panel-redesign.md +++ b/docs/plans/panel-redesign.md @@ -56,7 +56,7 @@ Navigation is grouped by mail direction. Page titles carry the group | Outbound | Domains · Log · Queue · DMARC reports | `/domains`, `/deliveries`, `/mail-queue`, `/dmarc` | | Inbound | Domains · Log · Queue | `/inbound`; Log and Queue are new | | Server | Health · System log · Backup · Users · Settings | Health is new (the tables that left `/status`) | -| User menu | Account · Help · Sign out | Account is the user's half of `/settings`: credentials, e-mail, DMARC default | +| User menu | Account · Help · Sign out | Account is the user's half of `/settings`: credentials and e-mail | A hidden page leaves no gap in the menu. The inbound and DMARC feature flags stay as in [web.go](../../internal/web/web.go); who sees what changes — see @@ -125,28 +125,33 @@ only. ## Account e-mail -**Decided 2026-09-21 (owner): a user's e-mail is theirs, not a DMARC field.** -Today the column is `users.dmarc_report_email` and the only place it shows is -the DMARC card, so it reads as if it existed for that alone. +**Decided 2026-10-10 (owner): a domain's DMARC report address is one of three +choices, and nothing else exists.** This replaces the 2026-09-21 design, where +a domain followed a named user's default: a co-administrator of a domain was +shown another user's login name, and three explicit choices are simpler. -- The 2.0 baseline has `users.email`. It is what the panel writes to: event - notifications ([panel-notifications](../roadmap.md#panel-notifications)) and - a reset link ([password-reset](../roadmap.md#password-reset)). -- **Account** edits it; the global administrator sees and sets it on the user - form and sees it in the users list. -- DMARC only *may* use it. **The default report address is a user's setting, - not the server's** (owner, 2026-09-21): it lives on **Account** with its - report-authorization record, as a choice — SelfPost hosted, *my account - e-mail*, another address, none. Historically it was the administrator's - profile field, later mirrored into an instance setting only the global user - could edit; Server › Settings keeps nothing of it. -- A domain can have several users, so "the default" must be someone's. A - domain's report address is *the default of a named user* (shown as - `admin's default — mix@example.org`), the hosted address, a custom one or - none. A new domain follows whoever created it; any user of the domain may - switch it to their own default. If that user is deleted the domain falls - to *none* and its DMARC check says so. Baseline: `users.dmarc_default_mode`, - `users.dmarc_default_address`, `domains.dmarc_rua_user_id`. +- A domain's aggregate-report address (`rua=`) is exactly one of: **no + reports** (the state of a new domain), **the SelfPost hosted address** + (offered only while hosted reports are enabled on the server), or **an + address typed for this domain**. There is no per-user default, no domain + following a user, and no keep / inherit. Changing the choice changes the + DMARC record to publish. +- It is set in **Domain settings** and nowhere else. Beside the typed-address + field there is a button that fills it with the signed-in user's profile + e-mail (`panel.js`, `data-fill`): it only fills, saving is the form's own + button, and it is absent when the profile has no e-mail. Without scripts the + field is typed in. +- The 2.0 baseline has `users.email`: the user's own address, edited on + **Account**, set by the global administrator on the user form and shown in + the users list. It is what the panel writes to — event notifications + ([panel-notifications](../roadmap.md#panel-notifications)) and a reset link + ([password-reset](../roadmap.md#password-reset)) — and what the fill button + offers. **Account** is Profile and Password; Server › Settings keeps nothing + of DMARC. +- Baseline: `domains.dmarc_rua` alone (`''` = no reports). The hosted choice is + the address SelfPost derives for the domain and the server's hostname, so it + is stored as that address and read back as hosted when it equals it. Deleting + a user changes no domain's address. ## Routes @@ -177,7 +182,7 @@ list, an action under the thing it changes. | backup | `GET, POST /server/backup` · `POST /server/backup/import` | `/backup` · `POST /domains/import` | | users · user · user-delete | `/server/users` · `…/new` · `…/{uid}` · `…/{uid}/delete` | `/users/…` | | settings | `GET, POST /server/settings` | the instance half of `/settings`: log retention, rate limits | -| account | `GET /account` · `POST /account/profile`, `/password`, `/dmarc` | `/settings`: credentials and the DMARC default (today `/account` redirects there) | +| account | `GET /account` · `POST /account/profile`, `/password` | `/settings`: credentials and the DMARC default (today `/account` redirects there) | | help · components | `GET /help` · `GET /server/components` | `/help` · — | Unchanged: `/healthz`, `/license`, `/static/`, `/setup/{token}`, `/login`, @@ -505,7 +510,7 @@ only when both are done. Every stage-2 step ends with the evidence of **Stage 1 — data and routes** (no template work; parallel with stage 0) -- [x] Schema from zero: single `0001_init.sql` baseline with `users.email`, the DMARC default fields, `user_inbound_domains`, the two `all_*` flags; store tests; `schema-migrations.md` rewritten — **Opus** +- [x] Schema from zero: single `0001_init.sql` baseline with `users.email`, `user_inbound_domains`, the two `all_*` flags; store tests; `schema-migrations.md` rewritten — **Opus** - [x] Inbound delegation: `requireInboundDomain`, lists and log filtered in the query, two-list user form data, per-route 404 tests for both roles including another tenant's id — **Opus** - [x] Routes renamed per § Routes: new paths, one fragment name, old paths removed, subtree `requireGlobal()` for `/server/`; links in the old templates retargeted so the panel works on every commit — **Opus** - [x] The application form as one POST; `out-app-created` as the response with `Cache-Control: no-store` — **Opus** diff --git a/docs/schema-migrations.md b/docs/schema-migrations.md index c859f34..acd0ec0 100644 --- a/docs/schema-migrations.md +++ b/docs/schema-migrations.md @@ -83,9 +83,9 @@ for operators upgrading from older 2.x images. | Table | Role | |-------|------| | `settings` | Key/value settings of the instance — what is true whoever is signed in (send-log retention). Nothing that belongs to a user | -| `users` | Panel logins. `role` is `global` or `domain`; `email` is the user's own address; `dmarc_default_mode` / `dmarc_default_address` are their default DMARC report address; `all_domains` / `all_inbound_domains` widen a `domain` user to every domain of that direction | +| `users` | Panel logins. `role` is `global` or `domain`; `email` is the user's own address; `all_domains` / `all_inbound_domains` widen a `domain` user to every domain of that direction | | `sessions` | Panel login sessions (token hash, owning user by id and deleted with them, sliding idle expiry) | -| `domains` | Sending (outbound) domains. `dmarc_rua` / `dmarc_rua_user_id` say where aggregate reports go | +| `domains` | Sending (outbound) domains. `dmarc_rua` is where aggregate reports go (`''` = no reports) | | `user_domains` | Outbound domains assigned to a `domain` user | | `applications` | SASL applications per domain, with the client-IP restriction (`auth_ip_restrict`, `auth_allowed_ips`) | | `application_addresses` | Explicit From addresses (`list` mode) | @@ -108,27 +108,20 @@ and index of this list. |---|---|---| | `admin` created in 0001, dropped in 0005 | never exists | the reason for the squash | | `users.role` = `global` / `domain_admin` | `global` / `domain` | the role is a reach, not a rank — everyone in the panel is an administrator | -| `users.dmarc_report_email` | `users.email` | a user's e-mail is theirs; it is what the panel writes to, and DMARC only *may* use it | -| instance setting `dmarc_report_email` (mirror of the global user's field) | `users.dmarc_default_mode`, `users.dmarc_default_address` | the default report address is a user's setting, not the server's | -| `domains.dmarc_rua` NULL = inherit the instance default, `''` = none | `domains.dmarc_rua_user_id` set = follows that user's default; otherwise `dmarc_rua` is the address, `''` = none | a domain can have several users, so "the default" has to be someone's | +| `users.dmarc_report_email` | `users.email` | a user's e-mail is theirs; it is what the panel writes to, and a domain's report address can be filled with it | +| instance setting `dmarc_report_email` (mirror of the global user's field); `domains.dmarc_rua` NULL = inherit the instance default | `domains.dmarc_rua` alone: the address, `''` = no reports | a domain's report address is one of three explicit choices — no reports, hosted, a typed address; nothing is inherited and no user's default exists (2026-10-10) | | — | `user_inbound_domains` | inbound is delegated per user, separately from outbound | | — | `users.all_domains`, `users.all_inbound_domains` | "All" includes domains added later | | `rate_limits.allowed_ips` (unused since 1.9.0) | gone | client IPs belong to the application (`auth_allowed_ips`) | ### Where a domain's DMARC reports go -Resolved in one place, `Store.DomainDMARCRua`: - -1. `domains.dmarc_rua_user_id` is set → the domain follows that user's default - (`DMARCDefault.Resolve`): `hosted` → SelfPost's own mailbox for the domain - (no reports while ingest is off), `account` → `users.email`, `custom` → - `users.dmarc_default_address`, `none` → no reports. -2. Otherwise `domains.dmarc_rua` is the address itself; `''` means a - policy-only record. - -A new domain follows whoever created it. Deleting the followed user sets -`dmarc_rua_user_id` to NULL (`ON DELETE SET NULL`), which leaves the domain at -"no reports" — never at another user's default. +`domains.dmarc_rua` is the address itself and the only thing stored; `''` means +no reports, and it is what a new domain has. The choice shown in Domain +settings is derived from it: `''` is no reports, the address SelfPost derives +for the domain and the server's hostname (`dmarc.HostedReportAddress`, offered +while hosted reports are enabled) is hosted, anything else is an address typed +for the domain. Nothing refers to a user: deleting one changes no domain. ---