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.
---