docs: panel header, type scale, account e-mail and the fixes of a full review
test / test (push) Canceled after 0s

Header. The navbar carries the wordmark instead of the stamp - at nav
size the stamp dissolves into the brick bar - and under the bar runs the
stamp's paper margin with the perforation along its outer edge: round
holes, flat teeth, ink hairline, 8px under a 60px bar as on the mark.
docs/assets/selfpost-wordmark.svg and -ink are the stamp's own outlines;
the header comparison sheet is removed.

Type and tables. About 40 % of the text measured under 13px. Body is
16px with two tokens under it, 14px for supporting text and 13px for
upper-case mono labels and tags; Bulma's small sizes are overridden or
dropped (sp-small replaces is-size-7, is-small only on a progress bar).
Every table sits in a table-container, addresses break after the @, and
all screens fit without scrolling at 1280px and up.

Contrast. A sweep of every text node against its real ground found the
solid red button at 3:1, the muted grey at 4.1:1, the amber postmark and
Bulma's menu labels just under 4.5. All 33 screens now measure 4.5:1 or
better. The sweep is kept as panel/audit.js and its clean output joins
the evidence a restyle step must show.

Account and roles. A user's e-mail is its own field (users.email in the
2.0 baseline), edited under Account and seen by the global user on the
user form and list. The DMARC default report address is a user's
setting, not the server's: it moves to Account with its authorization
record, and a domain follows the default of a named user. Roles are
labelled by reach - global, domain - and each assignment list gains All,
a per-user flag that covers domains added later. Account gains
Notifications for the new roadmap candidate panel-notifications;
password-reset is added as a candidate too.

Also: the inbound spam filter is visible where a domain user looks (a
box on each inbound domain, a tag on the list, its socket on Health);
the health row is compact again and splits 6/3/2; a read-only value is
as tall as its Copy button; Preflight and Account lose boxes that only
filled space, and the plan's layout rule and contract now forbid them.

check.py gains the type floor, the table-container rule and the is-small
rule, and no longer counts min-width: 0 as setting a width.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
mix
2026-09-22 00:05:32 +03:00
co-authored by Claude Fable 5.1
parent 4921416384
commit d83d326217
72 changed files with 970 additions and 624 deletions
+54
View File
@@ -23,6 +23,60 @@ document is not part of the release.
### Added
- Panel redesign, account e-mail and notifications: a user's e-mail is its own
field (2.0 baseline `users.email`, today `dmarc_report_email`) — edited under
**Account**, seen and set by the global administrator on the user form and in
the users list; DMARC may *use* it, as one choice beside the hosted address,
another address or none (Settings and domain settings). **Account** gains a
Notifications box and **Settings** a switch for the new roadmap candidate
`panel-notifications`; `password-reset` is added as a candidate too. The
inbound spam filter becomes visible: a *Spam filter* box on every inbound
domain, a tag on the inbound domains list, its socket among the milter
sockets. Preflight loses a box that only filled space — the test form now
shares its box with its result — and the plan's layout rule and contract say
so: what stands beside a form must be real. The role is labelled by its reach,
`global` or `domain` — everyone in the panel is an administrator — and each
assignment list on the user form gains **All**, a per-user flag that covers
domains added later. The DMARC default report address is a **user's**
setting, not the server's: it moves from Server › Settings to **Account**
with its report-authorization record, and a domain follows the default of a
*named* user, so two people sharing a domain never pull it two ways.
- Panel redesign, contrast: a sweep of every text node against its real ground
found the solid red button at 3:1 (Bulma derives a solid button's text from
the fill's lightness; brick had been patched, red had not), the muted grey
at 4.1:1, the amber postmark at 4.0 and Bulma's menu labels at 4.2. Solid
buttons are white on colour, muted is `#5c6774`, the postmark uses the warn
token; all 33 screens now measure 4.5:1 or better. The sweep is kept as
`panel/audit.js` — text size, contrast, overflow, clipped values, missing
icons — and its clean output joins the evidence a restyle step must show.
- Panel redesign, header (owner's choice: variant A, with the perforation on a
light edge as on the mark): the navbar carries the **wordmark** instead of
the stamp — `docs/assets/selfpost-wordmark.svg` and `-ink`, the stamp's own
outlines, recorded in `docs/product.md` — and under the brick bar runs the
stamp's paper margin with the perforation along its outer edge: round holes,
flat teeth, the ink hairline (`panel/perf-edge.svg`). All 33 screens rebuilt;
the components page documents the shell; the comparison sheet is removed.
The six health cards now split into six, three or two columns, so one card
is never left alone on a row. Three corrections after the owner's review:
the paper edge is 8px under the 60px bar — it had been scaled from the hole
size to 16px, where the mark's margin is a tenth of the brick field; the
health row is compact again (71px: smaller icons, each line a block with its
own line height, a verdict never wraps and stretches the row); and a
read-only value is as tall as the Copy button joined to it. `check.py` no
longer counts `min-width: 0` as setting a width. Text sizes: the filter
controls in a box head and the navbar's dropdown entries are 15px like the
bar itself (they had Bulma's `is-small` and its .875rem dropdown); `check.py`
now rejects `is-small` on anything but a progress bar.
- Panel redesign, type scale: about 40 % of the panel's text measured under
13px (labels 11.7, table heads 10.5, fact labels 10.2, tags and help 11.3).
Body is now 16px with two tokens under it — `--sp-fs-s` 14px for supporting
text and `--sp-fs-xs` 13px for upper-case mono labels and tags; Bulma's
small sizes are overridden or dropped (`sp-small` replaces `is-size-7`), and
`check.py` fails any font size under the floor. Every table now sits in a
`table-container` (enforced), addresses in tables break after the `@`, the
inbound log folds the client IP under the sender; all screens fit without
scrolling at 1280px and up. Documented on the components page and in the
plan (§ Type and tables).
- Panel redesign, header comparison sheet
([docs/assets/panel-redesign/header/](docs/assets/panel-redesign/header/index.html)):
at nav size the stamp dissolves into the brick bar and the perforated edge
@@ -1,81 +0,0 @@
<!DOCTYPE html>
<html lang="en" data-theme="light">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Header variants · SelfPost</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bulma@1.0.4/css/bulma.min.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@tabler/icons-webfont@3/dist/tabler-icons.min.css">
<link rel="stylesheet" href="../shared/brand.css">
<link rel="stylesheet" href="../panel/theme.css">
<link rel="stylesheet" href="variants.css">
</head>
<body>
<div class="hv-note"><h1>Header: the mark, the name, the edge</h1>
<p class="sp-muted">At nav size the stamp cannot carry itself: its perforation is under a pixel and its brick field dissolves into the brick bar, leaving a thin frame. And the current edge is inverted — a real perforation is round holes with flat teeth, not round bumps.</p></div>
<div class="hv-label"><b>Now</b><span>the stamp at nav size, round bumps hanging off the bar</span></div>
<div class="hv-frame hv-0">
<nav class="navbar is-primary"><div class="container">
<div class="navbar-brand"><a class="navbar-item" href="#"><img src="../../selfpost-stamp-compact.svg" alt="SelfPost"></a></div>
<div class="navbar-menu is-active"><div class="navbar-start"><a class="navbar-item" href="#">Overview</a><a class="navbar-item sp-current" href="#">Outbound</a><a class="navbar-item" href="#">Inbound</a><a class="navbar-item" href="#">Server</a></div>
<div class="navbar-end"><a class="navbar-item" href="#"><i class="ti ti-user-circle"></i>&nbsp;admin</a></div></div>
</div></nav>
<div class="sp-perf"></div>
<div class="sp-subnav"><div class="container"><a href="#" aria-current="page"><i class="ti ti-world-upload"></i>Domains</a><a href="#"><i class="ti ti-list-details"></i>Log</a><a href="#"><i class="ti ti-stack-2"></i>Queue</a><a href="#"><i class="ti ti-report-analytics"></i>DMARC reports</a></div></div>
<div class="container"><div class="sp-head">
<div class="sp-postmark sp-fail"><span>DNS</span><b>FAIL</b><span>1 OF 3 RECORDS</span></div>
<div class="sp-grow"><p class="sp-kicker">Outbound domains /</p><h1 class="title is-3">example.org</h1><p class="sp-lead">1 284 messages in 30 days · peak 96 msg/h</p></div>
<div class="buttons"><a class="button" href="#"><i class="ti ti-refresh"></i>Re-check DNS</a><a class="button is-primary" href="#"><i class="ti ti-plus"></i>Add application</a></div>
</div></div>
</div>
<div class="hv-label"><b>A · name + real perforation</b><span>wordmark only; round holes of page colour bitten into the bar, the mark's own hole-to-tooth ratio</span></div>
<div class="hv-frame hv-a">
<nav class="navbar is-primary"><div class="container">
<div class="navbar-brand"><a class="navbar-item" href="#"><img class="hv-word" src="wordmark-cream.svg" alt="SelfPost"></a></div>
<div class="navbar-menu is-active"><div class="navbar-start"><a class="navbar-item" href="#">Overview</a><a class="navbar-item sp-current" href="#">Outbound</a><a class="navbar-item" href="#">Inbound</a><a class="navbar-item" href="#">Server</a></div>
<div class="navbar-end"><a class="navbar-item" href="#"><i class="ti ti-user-circle"></i>&nbsp;admin</a></div></div>
</div></nav>
<div class="hv-perf"></div>
<div class="sp-subnav"><div class="container"><a href="#" aria-current="page"><i class="ti ti-world-upload"></i>Domains</a><a href="#"><i class="ti ti-list-details"></i>Log</a><a href="#"><i class="ti ti-stack-2"></i>Queue</a><a href="#"><i class="ti ti-report-analytics"></i>DMARC reports</a></div></div>
<div class="container"><div class="sp-head">
<div class="sp-postmark sp-fail"><span>DNS</span><b>FAIL</b><span>1 OF 3 RECORDS</span></div>
<div class="sp-grow"><p class="sp-kicker">Outbound domains /</p><h1 class="title is-3">example.org</h1><p class="sp-lead">1 284 messages in 30 days · peak 96 msg/h</p></div>
<div class="buttons"><a class="button" href="#"><i class="ti ti-refresh"></i>Re-check DNS</a><a class="button is-primary" href="#"><i class="ti ti-plus"></i>Add application</a></div>
</div></div>
</div>
<div class="hv-label"><b>B · name, no edge</b><span>wordmark only, a plain bar; the stamp and its perforation stay on sign-in, setup and the favicon</span></div>
<div class="hv-frame hv-b">
<nav class="navbar is-primary"><div class="container">
<div class="navbar-brand"><a class="navbar-item" href="#"><img class="hv-word" src="wordmark-cream.svg" alt="SelfPost"></a></div>
<div class="navbar-menu is-active"><div class="navbar-start"><a class="navbar-item" href="#">Overview</a><a class="navbar-item sp-current" href="#">Outbound</a><a class="navbar-item" href="#">Inbound</a><a class="navbar-item" href="#">Server</a></div>
<div class="navbar-end"><a class="navbar-item" href="#"><i class="ti ti-user-circle"></i>&nbsp;admin</a></div></div>
</div></nav>
<div class="sp-subnav"><div class="container"><a href="#" aria-current="page"><i class="ti ti-world-upload"></i>Domains</a><a href="#"><i class="ti ti-list-details"></i>Log</a><a href="#"><i class="ti ti-stack-2"></i>Queue</a><a href="#"><i class="ti ti-report-analytics"></i>DMARC reports</a></div></div>
<div class="container"><div class="sp-head">
<div class="sp-postmark sp-fail"><span>DNS</span><b>FAIL</b><span>1 OF 3 RECORDS</span></div>
<div class="sp-grow"><p class="sp-kicker">Outbound domains /</p><h1 class="title is-3">example.org</h1><p class="sp-lead">1 284 messages in 30 days · peak 96 msg/h</p></div>
<div class="buttons"><a class="button" href="#"><i class="ti ti-refresh"></i>Re-check DNS</a><a class="button is-primary" href="#"><i class="ti ti-plus"></i>Add application</a></div>
</div></div>
</div>
<div class="hv-label"><b>C · the header is the stamp</b><span>stamp paper, ink hairline and the mark's perforation; brick only behind the name, as on the mark</span></div>
<div class="hv-frame hv-c">
<nav class="navbar"><div class="container">
<div class="navbar-brand"><a class="hv-brand" href="#"><img class="hv-word" src="wordmark-cream.svg" alt="SelfPost"></a></div>
<div class="navbar-menu is-active"><div class="navbar-start"><a class="navbar-item" href="#">Overview</a><a class="navbar-item hv-current" href="#">Outbound</a><a class="navbar-item" href="#">Inbound</a><a class="navbar-item" href="#">Server</a></div>
<div class="navbar-end"><a class="navbar-item" href="#"><i class="ti ti-user-circle"></i>&nbsp;admin</a></div></div>
</div></nav>
<div class="hv-perf"></div>
<div class="sp-subnav"><div class="container"><a href="#" aria-current="page"><i class="ti ti-world-upload"></i>Domains</a><a href="#"><i class="ti ti-list-details"></i>Log</a><a href="#"><i class="ti ti-stack-2"></i>Queue</a><a href="#"><i class="ti ti-report-analytics"></i>DMARC reports</a></div></div>
<div class="container"><div class="sp-head">
<div class="sp-postmark sp-fail"><span>DNS</span><b>FAIL</b><span>1 OF 3 RECORDS</span></div>
<div class="sp-grow"><p class="sp-kicker">Outbound domains /</p><h1 class="title is-3">example.org</h1><p class="sp-lead">1 284 messages in 30 days · peak 96 msg/h</p></div>
<div class="buttons"><a class="button" href="#"><i class="ti ti-refresh"></i>Re-check DNS</a><a class="button is-primary" href="#"><i class="ti ti-plus"></i>Add application</a></div>
</div></div>
</div>
</body>
</html>
@@ -1,4 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="7" viewBox="0 0 16 7">
<path d="M0 0H16V6H13A5 5 0 0 0 3 6H0Z" fill="#F3EDE1"/>
<path d="M0 6H3A5 5 0 0 1 13 6H16" fill="none" stroke="#12161C" stroke-width="0.7"/>
</svg>

Before

Width:  |  Height:  |  Size: 231 B

@@ -1,33 +0,0 @@
/* Three takes on the panel header, on top of panel/theme.css. A comparison
sheet, not part of the design contract: the chosen one moves into
panel/theme.css and build.py, the rest is deleted. */
body { display: block; }
.hv-note { max-width: 60rem; margin: 2rem auto 0; padding: 0 1.5rem; }
.hv-note h1 { font-size: 1.6rem; font-weight: 600; margin-bottom: .4rem; }
.hv-label { max-width: 1344px; margin: 2.6rem auto .6rem; padding: 0 1.5rem; display: flex; gap: 1rem; align-items: baseline; }
.hv-label b { font-family: var(--sp-mono); font-size: .8rem; letter-spacing: .1em; text-transform: uppercase; color: var(--sp-brick); }
.hv-label span { color: var(--sp-muted); }
.hv-frame { border-block: 1px solid var(--sp-rule); background: var(--sp-paper); padding-bottom: 1.5rem; }
.hv-frame .sp-head { margin: 1.6rem 0 0; }
.hv-word { width: auto; display: block; }
.navbar-brand img.hv-word { max-height: none; height: 1.45rem; }
/* A — brick bar, wordmark, the stamp's real edge: round holes of page colour
bitten into the bar, hole wider than tooth (r / pitch = 5 / 16, the mark's
2.2 / 7.03). */
.hv-a .hv-perf {
height: 5px; margin-top: -5px; position: relative; z-index: 31; /* above Bulma's navbar (30) */
background: radial-gradient(circle at 8px 5px, var(--sp-paper) 5px, transparent 5.6px);
background-size: 16px 5px; background-repeat: repeat-x;
}
/* C — the header is the stamp: its paper, its ink hairline, its perforation;
brick is only the block behind the name, as on the mark. */
.hv-c .navbar { background: var(--sp-stamp-paper); --bulma-navbar-height: 4rem; }
.hv-c .navbar-item, .hv-c .navbar-link { color: var(--sp-ink); font-weight: 500; }
.hv-c .navbar-link::after { border-color: var(--sp-brick); }
.hv-c .hv-brand { background: var(--sp-brick); padding: .7rem 1.1rem; margin-block: .55rem; margin-right: 1rem; display: flex; align-items: center; }
.hv-c .hv-current { box-shadow: inset 0 -3px 0 var(--sp-brick); color: var(--sp-brick); }
.hv-c .hv-perf { height: 7px; background: url("perf-edge.svg") repeat-x; background-size: 16px 7px; }
.hv-b .sp-subnav, .hv-a .sp-subnav, .hv-c .sp-subnav { margin-top: 0; }
+1 -1
View File
@@ -13,7 +13,7 @@
<h1>The accepted direction, every screen</h1>
<p class="lead">Direction <strong>C</strong> (Bulma) was picked as the base: the important things in view, details one
or two clicks away. It takes the postal signature of <strong>A</strong> — the postmark verdict, numbered mono section
heads, the stamp's perforated edge — and the icons of <strong>B</strong>. Thirty-three screens and a components page live in
heads, the stamp's perforated paper edge under a brick bar that carries the wordmark — and the icons of <strong>B</strong>. Thirty-three screens and a components page live in
<a href="panel/overview.html">panel/</a>. Serve this folder from the repository root: the pages load the panel's own
fonts and mark by relative path.</p>
+65 -14
View File
@@ -12,7 +12,7 @@
<body>
<nav class="navbar is-primary" aria-label="main navigation">
<div class="container">
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-stamp-compact.svg" alt="SelfPost"></a></div>
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
<div class="navbar-menu is-active">
<div class="navbar-start">
<a class="navbar-item" href="overview.html">Overview</a>
@@ -67,36 +67,87 @@
<div class="container sp-page">
<div class="sp-head">
<div class="sp-grow">
<p class="sp-kicker">Signed in as admin · global administrator</p>
<p class="sp-kicker">Signed in as admin · global</p>
<h1 class="title is-3">Account</h1>
<p class="sp-lead">Your sign-in to this panel. Applications keep their own logins and passwords and are not affected.</p>
<p class="sp-lead">Who you are to this panel, what it should tell you about, and where reports about your domains go. Applications keep their own logins and passwords and are not affected.</p>
</div>
</div>
<div class="columns">
<div class="column is-6">
<div class="column is-5">
<div class="box">
<div class="sp-box-head"><span class="sp-no">01</span><h2>Panel credentials</h2></div>
<div class="sp-box-head"><span class="sp-no">01</span><h2>Profile</h2></div>
<div class="sp-box-body">
<form class="sp-form" method="post" action="#">
<div class="field"><label class="label" for="username">Username</label><div class="control has-icons-left"><input class="input" id="username" name="username" value="admin" autocomplete="username" spellcheck="false" required><span class="icon is-left"><i class="ti ti-user"></i></span></div></div>
<div class="field"><label class="label" for="email">E-mail</label><div class="control has-icons-left"><input class="input" id="email" name="email" type="email" value="mix@example.org" autocomplete="email"><span class="icon is-left"><i class="ti ti-mail"></i></span></div>
<p class="help">Where the panel writes to you: the notifications on the right and a password-reset link. It can also receive DMARC reports — your choice, under <a href="#dmarc">DMARC reports</a>.</p></div>
<button type="submit" class="button is-primary">Save profile</button>
</form>
</div>
</div>
<div class="box">
<div class="sp-box-head"><span class="sp-no">02</span><h2>Password</h2></div>
<div class="sp-box-body">
<form class="sp-form" method="post" action="#">
<div class="field"><label class="label" for="username">Username</label><div class="control"><input class="input" id="username" name="username" value="admin" autocomplete="username" spellcheck="false" required></div></div>
<div class="field"><label class="label" for="cur">Current password</label><div class="control"><input class="input" id="cur" name="current_password" type="password" autocomplete="current-password" required></div></div>
<div class="sp-pair">
<div class="field"><label class="label" for="new">New password</label><div class="control"><input class="input" id="new" name="new_password" type="password" autocomplete="new-password"></div></div>
<div class="field"><label class="label" for="new2">Confirm new password</label><div class="control"><input class="input" id="new2" name="new_password_confirm" type="password" autocomplete="new-password"></div></div>
<div class="field"><label class="label" for="new">New password</label><div class="control"><input class="input" id="new" name="new_password" type="password" autocomplete="new-password" required></div></div>
<div class="field"><label class="label" for="new2">Repeat</label><div class="control"><input class="input" id="new2" name="new_password_confirm" type="password" autocomplete="new-password" required></div></div>
</div>
<p class="help mb-3">Leave both new-password fields empty to change the username only.</p>
<button type="submit" class="button is-primary">Save changes</button>
<p class="help mb-3">Changing it signs out every other session of this account; this one stays signed in.</p>
<button type="submit" class="button">Change password</button>
</form>
</div>
</div>
</div>
<div class="column is-6">
<div class="column is-7">
<div class="box">
<div class="sp-box-head"><span class="sp-no"><i class="ti ti-info-circle"></i></span><h2>What happens</h2></div>
<div class="sp-box-head"><span class="sp-no">03</span><h2>Notifications</h2><div class="sp-end">Sent to mix@example.org</div></div>
<div class="sp-box-body">
<p class="mb-2">Changing the password signs out every other session of this account. This one stays signed in.</p>
<p class="sp-muted">A domain administrator sees this same page, the Outbound and Inbound domains assigned to them, and nothing under Server.</p>
<form class="sp-form" method="post" action="#">
<p class="sp-kicker mb-2">Your domains</p>
<div class="field"><label class="checkbox"><input type="checkbox" name="ev" checked> A DNS record stops matching — DKIM, SPF, DMARC or an inbound MX</label></div>
<div class="field"><label class="checkbox"><input type="checkbox" name="ev" checked> A domain or an application reaches its rate limit and mail is being refused</label></div>
<div class="field"><label class="checkbox"><input type="checkbox" name="ev" checked> An inbound domain's upstream cannot be reached and mail is piling up</label></div>
<div class="field"><label class="checkbox"><input type="checkbox" name="ev"> DMARC reports start showing failures for a domain</label></div>
<div class="field"><label class="checkbox"><input type="checkbox" name="ev"> Mail is held in quarantine</label></div>
<p class="sp-kicker mt-4 mb-2">The server · global role</p>
<div class="field"><label class="checkbox"><input type="checkbox" name="ev" checked> The TLS certificate is within 14 days of expiry</label></div>
<div class="field"><label class="checkbox"><input type="checkbox" name="ev" checked> A process or a milter socket stops answering</label></div>
<div class="field"><label class="checkbox"><input type="checkbox" name="ev"> Messages sit in a queue for more than a day</label></div>
<p class="help mb-3">One message when a problem starts and one when it clears — never a stream. A domain administrator is told only about the domains assigned to them.</p>
<button type="submit" class="button is-primary">Save notifications</button>
</form>
</div>
</div>
<div class="box" id="dmarc">
<div class="sp-box-head"><span class="sp-no">04</span><h2>DMARC reports</h2><div class="sp-end"><span class="tag is-success is-light">authorized</span></div><a class="sp-help" href="help.html#dmarc" title="What the reports are and where they go"><i class="ti ti-help-circle"></i></a></div>
<div class="sp-record">
<form class="sp-form" method="post" action="#">
<div class="field"><label class="label" for="rua">My default report address</label>
<div class="control"><div class="select is-fullwidth"><select id="rua" name="dmarc_default">
<option>SelfPost hosted — read them under DMARC reports</option>
<option selected>My account e-mail — mix@example.org</option>
<option>Another address…</option>
<option>No reports</option>
</select></div></div>
<p class="help">Used by every outbound domain whose report address is set to <em>admin's default</em> — 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.</p></div>
<button type="submit" class="button is-primary">Save report address</button>
</form>
</div>
<div class="sp-record">
<div class="sp-record-head"><h3>Report authorization</h3><span class="tag is-success is-light">published</span><small>Needed because the address is on another domain than the senders</small></div>
<div class="sp-row">
<div class="field"><label class="label">Host / name</label>
<div class="field has-addons"><div class="control is-expanded"><input class="input" readonly value="*._report._dmarc.example.org"></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
<div class="field"><label class="label">Type</label><div class="control"><input class="input" readonly value="TXT"></div></div>
</div>
<div class="field"><label class="label">Value to publish</label>
<div class="field has-addons"><div class="control is-expanded"><input class="input" readonly value="v=DMARC1"></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
</div>
</div>
</div>
+55
View File
@@ -0,0 +1,55 @@
/* Rendered-page audit for the panel: what check.py cannot see because it needs
a browser. Open any page of the mockups (or of the running panel), paste this
file into the console, then:
await panelAudit(["overview.html", "out-domain.html", ...]) // same-origin URLs
await panelAudit() // the page you are on
It loads each page in a 1440px and a 1280px frame and reports
- text under the 13px floor (the postmark's imprint excepted),
- text under 4.5:1 against the ground it actually sits on,
- anything wider than its box, and tables that have to scroll,
- health-card values that got clipped.
An empty `problems` list is the pass. Attach the output to the step's evidence
(docs/plans/panel-redesign.md, Enforcement). */
async function panelAudit(urls = [location.href], widths = [1440, 1280]) {
const lum = (c) => {
const [r, g, b] = c.map((v) => ((v /= 255) <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4));
return 0.2126 * r + 0.7152 * g + 0.0722 * b;
};
const rgba = (s) => { const m = s.match(/[\d.]+/g).map(Number); return { rgb: m.slice(0, 3), a: m.length > 3 ? m[3] : 1 }; };
const ground = (el, win) => {
for (let e = el; e; e = e.parentElement) { const b = rgba(win.getComputedStyle(e).backgroundColor); if (b.a > 0.5) return b.rgb; }
return [255, 255, 255];
};
const problems = new Set();
let texts = 0;
for (const url of urls) for (const width of widths) {
const f = document.createElement("iframe");
f.width = width; f.height = 900; f.src = url;
document.body.appendChild(f);
await new Promise((r) => (f.onload = r));
await f.contentDocument.fonts.ready;
await new Promise((r) => setTimeout(r, 150));
const d = f.contentDocument, win = f.contentWindow, at = `${width} ${url.split("/").pop()}`;
const walker = d.createTreeWalker(d.body, NodeFilter.SHOW_TEXT);
for (let n; (n = walker.nextNode()); ) {
const text = n.textContent.trim(), el = n.parentElement;
if (!text || el.closest(".sp-log")) continue; // the log pane has its own dark ground and palette
texts++;
const cs = win.getComputedStyle(el), what = `${el.tagName.toLowerCase()}.${[...el.classList].join(".")} "${text.slice(0, 24)}"`;
if (parseFloat(cs.fontSize) < 12.9 && !el.closest(".sp-postmark")) problems.add(`${at}: ${parseFloat(cs.fontSize)}px ${what}`);
const fg = rgba(cs.color).rgb, bg = ground(el, win), a = lum(fg), b = lum(bg);
const ratio = (Math.max(a, b) + 0.05) / (Math.min(a, b) + 0.05);
const large = parseFloat(cs.fontSize) >= 24 || (parseFloat(cs.fontSize) >= 18.6 && +cs.fontWeight >= 600);
if (ratio < (large ? 3 : 4.5)) problems.add(`${at}: contrast ${ratio.toFixed(2)} ${what}`);
}
if (d.documentElement.scrollWidth > win.innerWidth) problems.add(`${at}: the page scrolls sideways`);
for (const e of d.querySelectorAll(".box, .sp-head, .sp-box-head")) if (e.scrollWidth > e.clientWidth + 1) problems.add(`${at}: overflow in .${e.className}`);
for (const e of d.querySelectorAll(".table-container")) if (e.scrollWidth > e.clientWidth + 1) problems.add(`${at}: a table scrolls by ${e.scrollWidth - e.clientWidth}px`);
for (const e of d.querySelectorAll(".sp-h-name, .sp-h-value, .sp-h-sub")) if (e.scrollWidth > e.clientWidth + 1) problems.add(`${at}: clipped "${e.textContent}"`);
for (const i of d.querySelectorAll("i.ti")) if (win.getComputedStyle(i, "::before").content === "none") problems.add(`${at}: missing icon ${i.className}`);
f.remove();
}
return { pages: urls.length, widths, textsChecked: texts, problems: [...problems] };
}
+1 -1
View File
@@ -12,7 +12,7 @@
<body>
<nav class="navbar is-primary" aria-label="main navigation">
<div class="container">
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-stamp-compact.svg" alt="SelfPost"></a></div>
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
<div class="navbar-menu is-active">
<div class="navbar-start">
<a class="navbar-item" href="overview.html">Overview</a>
+1 -1
View File
@@ -59,7 +59,7 @@ FOOT = ('<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">Li
def navbar(section, page):
out = ['<nav class="navbar is-primary" aria-label="main navigation">', ' <div class="container">',
' <div class="navbar-brand"><a class="navbar-item" href="overview.html">'
'<img src="../../selfpost-stamp-compact.svg" alt="SelfPost"></a></div>',
'<img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>',
' <div class="navbar-menu is-active">', ' <div class="navbar-start">']
for key, label, href, _icon, pages in NAV:
cur = " sp-current" if key == section else ""
+16 -4
View File
@@ -25,9 +25,9 @@ VOID = {"input", "img", "br", "hr", "meta", "link"}
BULMA = set("""
box button buttons checkbox column columns control field file file-cta file-input file-label
file-name has-addons has-icons-left has-name has-text-danger help icon input label menu menu-label
menu-list notification progress select table tag textarea title
menu-list notification progress select table table-container tag textarea title
is-3 is-4 is-5 is-6 is-7 is-9 is-active is-danger is-expanded is-fullwidth is-gapless is-half
is-hoverable is-left is-light is-primary is-size-7 is-small is-success is-text is-warning
is-hoverable is-left is-light is-primary is-small is-success is-text is-warning
mb-1 mb-2 mb-3 mb-4 ml-2 mt-4 mt-5
""".split())
# Classes the shell (build.py, later layout.html) owns; screens never write them.
@@ -111,7 +111,7 @@ def box_kind(box):
kinds.append(name)
break
else:
kinds.append("table" if k.tag == "table" else k.tag)
kinds.append("table" if k.tag == "table" or k.has("table-container") else k.tag)
out = []
for k in kinds:
if not out or out[-1].split("×")[0] != k:
@@ -159,7 +159,14 @@ def main():
errors.append(f"theme.css: !important in `{sel}`")
if re.search(r"(^|[\s,>+~])#[a-z]", sel):
errors.append(f"theme.css: id selector `{sel}` — style components, not one element on one page")
if re.search(r"(?<![-\w])(max-width|min-width|width)\s*:", body) and not any(sel.startswith(w) or w in sel for w in WIDTH_OK):
# The type floor: 13px. Icons (.ti) and the postmark imprint are not running text.
for value, unit in re.findall(r"font-size\s*:\s*([\d.]+)(rem|em|px)", body):
floor = {"rem": 0.8125, "em": 0.85, "px": 13}[unit]
if float(value) < floor and ".sp-postmark" not in sel and ".ti" not in sel:
errors.append(f"theme.css: `{sel}` sets font-size {value}{unit}, below the 13px floor — use --sp-fs-xs or --sp-fs-s")
# `min-width: 0` sizes nothing: it only lets a flex or grid child shrink so its text can be clipped.
sized = re.sub(r"min-width\s*:\s*0\s*(;|$)", "", body)
if re.search(r"(?<![-\w])(max-width|min-width|width)\s*:", sized) and not any(sel.startswith(w) or w in sel for w in WIDTH_OK):
errors.append(f"theme.css: `{sel}` sets a width; widths come from the page layout (WIDTH_OK)")
used, documented, outlines = set(), set(), {}
@@ -181,6 +188,8 @@ def main():
errors.append(f"{name}: `{c}` is not defined in theme.css")
elif c == "ti" or c.startswith("ti-"):
continue
elif c == "is-small" and "progress" not in n.classes:
errors.append(f"{name}: `is-small` on <{n.tag}> — controls keep the normal size; only a progress bar is small")
elif c not in BULMA:
errors.append(f"{name}: class `{c}` is neither in the Bulma subset nor an sp- component")
if bare:
@@ -193,6 +202,9 @@ def main():
marks = root.find("sp-postmark")
if name != "components" and (len(marks) > 1 or any(not m.parent.has("sp-head") for m in marks)):
errors.append(f"{name}: at most one postmark, inside page_head")
for tbl in (n for n in root.walk() if n.tag == "table"):
if not tbl.parent.has("table-container"):
errors.append(f"{name}: a <table> must sit in a table-container so a wide one can scroll")
for box in root.find("box"):
if not box.kids or not box.kids[0].has("sp-box-head"):
errors.append(f"{name}: a box must start with box_head")
@@ -12,7 +12,7 @@
<body>
<nav class="navbar is-primary" aria-label="main navigation">
<div class="container">
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-stamp-compact.svg" alt="SelfPost"></a></div>
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
<div class="navbar-menu is-active">
<div class="navbar-start">
<a class="navbar-item" href="overview.html">Overview</a>
@@ -106,16 +106,16 @@
<div class="box">
<div class="sp-box-head"><span class="sp-no">03</span><h2>status_tag</h2><div class="sp-end">One mapping, one function</div></div>
<table class="table is-fullwidth">
<div class="table-container"><table class="table is-fullwidth">
<thead><tr><th>Status</th><th>Tag</th><th>Bulma classes</th></tr></thead>
<tbody>
<tr><td>ok, published, running, sent</td><td><span class="tag is-success is-light">ok</span></td><td class="sp-mono is-size-7">tag is-success is-light</td></tr>
<tr><td>warn, deferred, quiet</td><td><span class="tag is-warning is-light">warn</span></td><td class="sp-mono is-size-7">tag is-warning is-light</td></tr>
<tr><td>error, fail, mismatch, bounced</td><td><span class="tag is-danger is-light">fail</span></td><td class="sp-mono is-size-7">tag is-danger is-light</td></tr>
<tr><td>unknown, off, neutral counts</td><td><span class="tag is-light">off</span></td><td class="sp-mono is-size-7">tag is-light</td></tr>
<tr><td>held: quarantine · role: global</td><td><span class="tag is-primary is-light">quarantine</span></td><td class="sp-mono is-size-7">tag is-primary is-light</td></tr>
<tr><td>ok, published, running, sent</td><td><span class="tag is-success is-light">ok</span></td><td class="sp-mono sp-small">tag is-success is-light</td></tr>
<tr><td>warn, deferred, quiet</td><td><span class="tag is-warning is-light">warn</span></td><td class="sp-mono sp-small">tag is-warning is-light</td></tr>
<tr><td>error, fail, mismatch, bounced</td><td><span class="tag is-danger is-light">fail</span></td><td class="sp-mono sp-small">tag is-danger is-light</td></tr>
<tr><td>unknown, off, neutral counts</td><td><span class="tag is-light">off</span></td><td class="sp-mono sp-small">tag is-light</td></tr>
<tr><td>held: quarantine · role: global</td><td><span class="tag is-primary is-light">quarantine</span></td><td class="sp-mono sp-small">tag is-primary is-light</td></tr>
</tbody>
</table>
</table></div>
</div>
</div>
@@ -139,9 +139,9 @@
<div class="box">
<div class="sp-box-head"><span class="sp-no">04</span><h2>health_card</h2><div class="sp-end">Overview only · each card links to its detail</div></div>
<div class="sp-health">
<a href="#"><span class="sp-h-icon"><i class="ti ti-server-cog"></i></span><span><span class="sp-h-name">Name</span><br><span class="sp-h-value">Verdict in words</span><br><span class="sp-h-sub">ok · one supporting fact</span></span></a>
<a href="#" class="sp-warn"><span class="sp-h-icon"><i class="ti ti-certificate"></i></span><span><span class="sp-h-name">Name</span><br><span class="sp-h-value">Expires in 12 days</span><br><span class="sp-h-sub">sp-warn</span></span></a>
<a href="#" class="sp-fail"><span class="sp-h-icon"><i class="ti ti-plug-connected-x"></i></span><span><span class="sp-h-name">Name</span><br><span class="sp-h-value">1 of 2 answering</span><br><span class="sp-h-sub">sp-fail</span></span></a>
<a href="#"><span class="sp-h-icon"><i class="ti ti-server-cog"></i></span><span><span class="sp-h-name">Name</span><span class="sp-h-value">Verdict in words</span><span class="sp-h-sub">ok · one supporting fact</span></span></a>
<a href="#" class="sp-warn"><span class="sp-h-icon"><i class="ti ti-certificate"></i></span><span><span class="sp-h-name">Name</span><span class="sp-h-value">Expires in 12 days</span><span class="sp-h-sub">sp-warn</span></span></a>
<a href="#" class="sp-fail"><span class="sp-h-icon"><i class="ti ti-plug-connected-x"></i></span><span><span class="sp-h-name">Name</span><span class="sp-h-value">1 of 2 answering</span><span class="sp-h-sub">sp-fail</span></span></a>
</div>
</div>
@@ -168,13 +168,13 @@
</div>
<div class="box">
<div class="sp-box-head"><span class="sp-no">07</span><h2>Table conventions</h2></div>
<table class="table is-fullwidth is-hoverable">
<div class="sp-box-head"><span class="sp-no">07</span><h2>Table conventions</h2><div class="sp-end">Always inside <code>table-container</code> · an address breaks after its @</div></div>
<div class="table-container"><table class="table is-fullwidth is-hoverable">
<thead><tr><th>Time, UTC</th><th>Name</th><th>sp-clip</th><th>Status</th><th></th></tr></thead>
<tbody>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-21 14:01:52</td><td><a href="#"><strong>the row's link</strong></a></td><td class="sp-clip">A long subject is clipped with an ellipsis instead of wrapping the row</td><td><span class="tag is-success is-light">sent</span></td><td class="sp-actions"><a href="#">Details</a><a class="has-text-danger" href="#">Delete</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 14:01:52</td><td><a href="#"><strong>the row's link</strong></a></td><td class="sp-clip">A long subject is clipped with an ellipsis instead of wrapping the row</td><td><span class="tag is-success is-light">sent</span></td><td class="sp-actions"><a href="#">Details</a><a class="has-text-danger" href="#">Delete</a></td></tr>
</tbody>
</table>
</table></div>
<div class="sp-box-foot"><span>Page 1 of 9</span><a href="#">Older →</a></div>
</div>
@@ -262,6 +262,31 @@
</div>
</div>
<div class="box">
<div class="sp-box-head"><span class="sp-no">13</span><h2>shell</h2><div class="sp-end">Owned by the layout — a screen never writes it</div></div>
<div class="sp-box-body">
<dl class="sp-facts">
<div><dt>Name</dt><dd>The wordmark, not the stamp: at nav size the stamp's perforation is under a pixel and its brick field dissolves into the bar. The stamp lives on sign-in, setup and the favicon.</dd></div>
<div><dt>Edge</dt><dd>Built like the mark: the brick field, then the stamp's paper margin, and the perforation along the outer edge of that margin — round holes, flat teeth, the ink hairline. One tile, <code>perf-edge.svg</code>.</dd></div>
<div><dt>Sibling strip</dt><dd>The pages of the current group, under the edge. Five or six entries; past that the group is split.</dd></div>
<div><dt>Menus</dt><dd>Bulma hover dropdowns, no JavaScript. The user menu holds Account, Help and Sign out.</dd></div>
</dl>
</div>
</div>
<div class="box">
<div class="sp-box-head"><span class="sp-no">14</span><h2>Type scale</h2><div class="sp-end">Three sizes · nothing to be read is under 13px</div></div>
<div class="table-container"><table class="table is-fullwidth">
<thead><tr><th>Size</th><th>Looks like</th><th>Used for</th></tr></thead>
<tbody>
<tr><td class="sp-mono sp-small">body · 16px</td><td>The quick brown fox, 203.0.113.25</td><td class="sp-muted">Text, table cells, inputs, selects, buttons, menus and their dropdowns</td></tr>
<tr><td class="sp-mono sp-small">--sp-fs-s · 14px</td><td class="sp-small">The quick brown fox, <span class="sp-mono">203.0.113.25</span></td><td class="sp-muted">Supporting text: labels, <code>help</code>, notes in a box head or foot, mono values (<code>sp-small</code>), logs, the footer</td></tr>
<tr><td class="sp-mono sp-small">--sp-fs-xs · 13px</td><td><span class="sp-kicker">Upper-case mono label</span> <span class="tag is-light">tag</span></td><td class="sp-muted">Upper-case mono micro-labels (kicker, table head, <code>dt</code>, box title) and tags — nothing else</td></tr>
</tbody>
</table></div>
<div class="sp-box-foot">Bulma's <code>is-small</code> and <code>is-size-7</code> are not used: a control is never small, and <code>sp-small</code> replaces the helper. The postmark's fine print is an imprint, not text.</div>
</div>
<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
</div>
</main>
@@ -12,7 +12,7 @@
<body>
<nav class="navbar is-primary" aria-label="main navigation">
<div class="container">
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-stamp-compact.svg" alt="SelfPost"></a></div>
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
<div class="navbar-menu is-active">
<div class="navbar-start">
<a class="navbar-item" href="overview.html">Overview</a>
@@ -79,26 +79,26 @@
<div class="column is-5">
<div class="box">
<div class="sp-box-head"><span class="sp-no">01</span><h2>Sources · 7 days</h2></div>
<table class="table is-fullwidth">
<div class="table-container"><table class="table is-fullwidth">
<thead><tr><th>Source</th><th>Pass</th><th>Fail</th><th>Disposition</th></tr></thead>
<tbody>
<tr><td class="sp-mono is-size-7">203.0.113.25 <span class="tag is-light">this relay</span></td><td>11 204</td><td>0</td><td><span class="tag is-success is-light">none</span></td></tr>
<tr><td class="sp-mono is-size-7">198.51.100.44</td><td>0</td><td>41</td><td><span class="tag is-warning is-light">none</span></td></tr>
<tr><td class="sp-mono sp-small">203.0.113.25 <span class="tag is-light">this relay</span></td><td>11 204</td><td>0</td><td><span class="tag is-success is-light">none</span></td></tr>
<tr><td class="sp-mono sp-small">198.51.100.44</td><td>0</td><td>41</td><td><span class="tag is-warning is-light">none</span></td></tr>
</tbody>
</table>
</table></div>
</div>
</div>
<div class="column is-7">
<div class="box">
<div class="sp-box-head"><span class="sp-no">02</span><h2>Reports</h2></div>
<table class="table is-fullwidth is-hoverable">
<div class="table-container"><table class="table is-fullwidth is-hoverable">
<thead><tr><th>Received, UTC</th><th>Reporter</th><th>Window</th><th>Pass</th><th>Fail</th><th></th></tr></thead>
<tbody>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-21 04:40</td><td class="sp-muted">Outlook.com</td><td>20 Sep</td><td>1 903</td><td><span class="tag is-warning is-light">7</span></td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-21 02:15</td><td class="sp-muted">google.com</td><td>20 Sep</td><td>6 410</td><td>0</td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-20 04:38</td><td class="sp-muted">Outlook.com</td><td>19 Sep</td><td>1 877</td><td><span class="tag is-warning is-light">34</span></td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 04:40</td><td class="sp-muted">Outlook.com</td><td>20 Sep</td><td>1 903</td><td><span class="tag is-warning is-light">7</span></td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 02:15</td><td class="sp-muted">google.com</td><td>20 Sep</td><td>6 410</td><td>0</td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-20 04:38</td><td class="sp-muted">Outlook.com</td><td>19 Sep</td><td>1 877</td><td><span class="tag is-warning is-light">34</span></td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
</tbody>
</table>
</table></div>
</div>
</div>
</div>
@@ -12,7 +12,7 @@
<body>
<nav class="navbar is-primary" aria-label="main navigation">
<div class="container">
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-stamp-compact.svg" alt="SelfPost"></a></div>
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
<div class="navbar-menu is-active">
<div class="navbar-start">
<a class="navbar-item" href="overview.html">Overview</a>
@@ -80,7 +80,7 @@
<div class="sp-box-body">
<dl class="sp-facts">
<div><dt>Reporter</dt><dd>Outlook.com</dd></div>
<div><dt>Report id</dt><dd class="sp-mono is-size-7">5f1c9a0e7b2d4e61a3</dd></div>
<div><dt>Report id</dt><dd class="sp-mono sp-small">5f1c9a0e7b2d4e61a3</dd></div>
<div><dt>Window</dt><dd>20 Sep 00:00 – 21 Sep 00:00 UTC</dd></div>
<div><dt>Received</dt><dd>2026-09-21 04:40 UTC</dd></div>
</dl>
@@ -94,7 +94,7 @@
<dl class="sp-facts">
<div><dt>p / sp / pct</dt><dd class="sp-mono">none / none / 100</dd></div>
<div><dt>adkim / aspf</dt><dd class="sp-mono">r / r</dd></div>
<div><dt>rua</dt><dd class="sp-mono is-size-7">dmarc@mail.example.org</dd></div>
<div><dt>rua</dt><dd class="sp-mono sp-small">dmarc@mail.example.org</dd></div>
</dl>
</div>
</div>
@@ -103,13 +103,13 @@
<div class="box">
<div class="sp-box-head"><span class="sp-no">03</span><h2>Records</h2><div class="sp-end">Parsed from the aggregate XML</div></div>
<table class="table is-fullwidth">
<div class="table-container"><table class="table is-fullwidth">
<thead><tr><th>Source</th><th>Count</th><th>Disposition</th><th>SPF</th><th>DKIM</th><th>Header from</th></tr></thead>
<tbody>
<tr><td class="sp-mono is-size-7">203.0.113.25 <span class="tag is-light">this relay</span></td><td>1 903</td><td><span class="tag is-success is-light">none</span></td><td><span class="tag is-success is-light">pass</span></td><td><span class="tag is-success is-light">pass</span></td><td>shop.example.org</td></tr>
<tr><td class="sp-mono is-size-7">198.51.100.44</td><td>7</td><td><span class="tag is-warning is-light">none</span></td><td><span class="tag is-danger is-light">fail</span></td><td><span class="tag is-danger is-light">fail</span></td><td>shop.example.org</td></tr>
<tr><td class="sp-mono sp-small">203.0.113.25 <span class="tag is-light">this relay</span></td><td>1 903</td><td><span class="tag is-success is-light">none</span></td><td><span class="tag is-success is-light">pass</span></td><td><span class="tag is-success is-light">pass</span></td><td>shop.example.org</td></tr>
<tr><td class="sp-mono sp-small">198.51.100.44</td><td>7</td><td><span class="tag is-warning is-light">none</span></td><td><span class="tag is-danger is-light">fail</span></td><td><span class="tag is-danger is-light">fail</span></td><td>shop.example.org</td></tr>
</tbody>
</table>
</table></div>
</div>
<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
+7 -7
View File
@@ -12,7 +12,7 @@
<body>
<nav class="navbar is-primary" aria-label="main navigation">
<div class="container">
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-stamp-compact.svg" alt="SelfPost"></a></div>
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
<div class="navbar-menu is-active">
<div class="navbar-start">
<a class="navbar-item" href="overview.html">Overview</a>
@@ -87,15 +87,15 @@
<div class="box">
<div class="sp-box-head"><span class="sp-no">02</span><h2>Recent reports</h2></div>
<table class="table is-fullwidth is-hoverable">
<div class="table-container"><table class="table is-fullwidth is-hoverable">
<thead><tr><th>Received, UTC</th><th>Domain</th><th>Reporter</th><th>Window</th><th>Pass</th><th>Fail</th><th></th></tr></thead>
<tbody>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-21 06:12</td><td><a href="dmarc-domain.html">example.org</a></td><td class="sp-muted">google.com</td><td>20 Sep</td><td>412</td><td>0</td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-21 04:40</td><td><a href="dmarc-domain.html">shop.example.org</a></td><td class="sp-muted">Outlook.com</td><td>20 Sep</td><td>1 903</td><td><span class="tag is-warning is-light">7</span></td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-20 23:58</td><td><a href="dmarc-domain.html">example.org</a></td><td class="sp-muted">Yahoo</td><td>19 Sep</td><td>38</td><td>0</td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-20 06:09</td><td><a href="dmarc-domain.html">notify.acme.io</a></td><td class="sp-muted">google.com</td><td>19 Sep</td><td>61</td><td>0</td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 06:12</td><td><a href="dmarc-domain.html">example.org</a></td><td class="sp-muted">google.com</td><td>20 Sep</td><td>412</td><td>0</td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 04:40</td><td><a href="dmarc-domain.html">shop.example.org</a></td><td class="sp-muted">Outlook.com</td><td>20 Sep</td><td>1 903</td><td><span class="tag is-warning is-light">7</span></td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-20 23:58</td><td><a href="dmarc-domain.html">example.org</a></td><td class="sp-muted">Yahoo</td><td>19 Sep</td><td>38</td><td>0</td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-20 06:09</td><td><a href="dmarc-domain.html">notify.acme.io</a></td><td class="sp-muted">google.com</td><td>19 Sep</td><td>61</td><td>0</td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
</tbody>
</table>
</table></div>
</div>
<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
+10 -9
View File
@@ -12,7 +12,7 @@
<body>
<nav class="navbar is-primary" aria-label="main navigation">
<div class="container">
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-stamp-compact.svg" alt="SelfPost"></a></div>
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
<div class="navbar-menu is-active">
<div class="navbar-start">
<a class="navbar-item" href="overview.html">Overview</a>
@@ -78,20 +78,20 @@
<div class="column is-6">
<div class="box" id="machine">
<div class="sp-box-head"><span class="sp-no">01</span><h2>Machine</h2><div class="sp-end"><span class="tag is-success is-light">ok</span></div></div>
<table class="table is-fullwidth">
<div class="table-container"><table class="table is-fullwidth">
<thead><tr><th>Resource</th><th>Usage</th><th>Detail</th></tr></thead>
<tbody>
<tr><td>CPU</td><td><progress class="progress is-small is-success mb-1" value="12" max="100"></progress>12 %</td><td class="sp-muted">2 cores, load 0.21</td></tr>
<tr><td>Memory</td><td><progress class="progress is-small is-success mb-1" value="41" max="100"></progress>41 %</td><td class="sp-muted">812 MB of 1.9 GB</td></tr>
<tr><td>Network</td><td class="sp-nowrap">↓ 18 kB/s · ↑ 44 kB/s</td><td class="sp-muted">eth0: 2.1 GB in, 6.4 GB out</td></tr>
</tbody>
</table>
</table></div>
</div>
</div>
<div class="column is-6">
<div class="box" id="processes">
<div class="sp-box-head"><span class="sp-no">02</span><h2>Processes</h2><div class="sp-end"><span class="tag is-success is-light">ok</span></div></div>
<table class="table is-fullwidth">
<div class="table-container"><table class="table is-fullwidth">
<thead><tr><th>Program</th><th>State</th><th>Detail</th></tr></thead>
<tbody>
<tr><td>postfix</td><td><span class="tag is-success is-light">running</span></td><td class="sp-muted">pid 41, uptime 18 days</td></tr>
@@ -100,7 +100,7 @@
<tr><td>panel</td><td><span class="tag is-success is-light">running</span></td><td class="sp-muted">pid 44, uptime 18 days</td></tr>
<tr><td>dmarc-ingest</td><td><span class="tag is-success is-light">running</span></td><td class="sp-muted">pid 47, uptime 18 days</td></tr>
</tbody>
</table>
</table></div>
</div>
</div>
</div>
@@ -122,13 +122,14 @@
<div class="column is-4">
<div class="box" id="sockets">
<div class="sp-box-head"><span class="sp-no">04</span><h2>Milter sockets</h2><div class="sp-end"><span class="tag is-success is-light">ok</span></div></div>
<table class="table is-fullwidth">
<div class="table-container"><table class="table is-fullwidth">
<thead><tr><th>Milter</th><th>State</th></tr></thead>
<tbody>
<tr><td>opendkim<br><span class="sp-muted sp-mono is-size-7">inet:127.0.0.1:8891</span></td><td><span class="tag is-success is-light">ok</span></td></tr>
<tr><td>journal-milter<br><span class="sp-muted sp-mono is-size-7">unix:/run/selfpost/journal.sock</span></td><td><span class="tag is-success is-light">ok</span></td></tr>
<tr><td>opendkim<br><span class="sp-muted sp-mono sp-small">inet:127.0.0.1:8891</span></td><td><span class="tag is-success is-light">ok</span></td></tr>
<tr><td>journal-milter<br><span class="sp-muted sp-mono sp-small">unix:/run/selfpost/journal.sock</span></td><td><span class="tag is-success is-light">ok</span></td></tr>
<tr><td>rspamd <span class="sp-muted">· inbound spam filter</span><br><span class="sp-muted sp-mono sp-small">inet:antispam:11332</span></td><td><span class="tag is-success is-light">ok</span></td></tr>
</tbody>
</table>
</table></div>
</div>
</div>
<div class="column is-4">
+1 -1
View File
@@ -12,7 +12,7 @@
<body>
<nav class="navbar is-primary" aria-label="main navigation">
<div class="container">
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-stamp-compact.svg" alt="SelfPost"></a></div>
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
<div class="navbar-menu is-active">
<div class="navbar-start">
<a class="navbar-item" href="overview.html">Overview</a>
@@ -12,7 +12,7 @@
<body>
<nav class="navbar is-primary" aria-label="main navigation">
<div class="container">
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-stamp-compact.svg" alt="SelfPost"></a></div>
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
<div class="navbar-menu is-active">
<div class="navbar-start">
<a class="navbar-item" href="overview.html">Overview</a>
@@ -12,7 +12,7 @@
<body>
<nav class="navbar is-primary" aria-label="main navigation">
<div class="container">
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-stamp-compact.svg" alt="SelfPost"></a></div>
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
<div class="navbar-menu is-active">
<div class="navbar-start">
<a class="navbar-item" href="overview.html">Overview</a>
@@ -85,6 +85,7 @@
<li><a class="is-active" href="#dns"><i class="ti ti-world-check"></i>MX record <span class="tag is-success is-light">ok</span></a></li>
<li><a href="#upstream"><i class="ti ti-arrow-forward-up"></i>Upstream</a></li>
<li><a href="#recipients"><i class="ti ti-address-book"></i>Recipients <span class="tag is-light">4</span></a></li>
<li><a href="#filter"><i class="ti ti-shield-check"></i>Spam filter <span class="tag is-success is-light">on</span></a></li>
</ul>
<p class="menu-label">See also</p>
<ul class="menu-list">
@@ -148,6 +149,18 @@ security@lists.example.org</textarea></div>
</div>
</div>
</div>
<div class="box" id="filter">
<div class="sp-box-head"><span class="sp-no">04</span><h2>Spam filter</h2><div class="sp-end"><span class="tag is-success is-light">on</span></div></div>
<div class="sp-box-body">
<dl class="sp-facts">
<div><dt>Engine</dt><dd>rspamd, for every inbound domain of this server</dd></div>
<div><dt>Suspicious mail</dt><dd>Held — <a href="in-quarantine.html">1 message in quarantine</a> for this domain</dd></div>
<div><dt>Spam</dt><dd>Refused at the door; see <em>reject</em> in the <a href="in-log.html">log</a></dd></div>
<div><dt>Allow and deny lists</dt><dd class="sp-muted">Kept by the global administrator, one pair for the whole server</dd></div>
</dl>
</div>
</div>
</div>
</div>
@@ -12,7 +12,7 @@
<body>
<nav class="navbar is-primary" aria-label="main navigation">
<div class="container">
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-stamp-compact.svg" alt="SelfPost"></a></div>
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
<div class="navbar-menu is-active">
<div class="navbar-start">
<a class="navbar-item" href="overview.html">Overview</a>
@@ -87,14 +87,14 @@
</div>
<div class="box">
<div class="sp-box-head"><span class="sp-no">02</span><h2>Domains</h2><div class="sp-end">2 domains · DNS cached a few minutes</div></div>
<table class="table is-fullwidth is-hoverable">
<div class="sp-box-head"><span class="sp-no">02</span><h2>Domains</h2><div class="sp-end">Spam filter <span class="tag is-success is-light">on</span> · 2 domains</div></div>
<div class="table-container"><table class="table is-fullwidth is-hoverable">
<thead><tr><th>Domain</th><th>DNS</th><th>Upstream</th><th>Recipients</th><th>TLS to upstream</th><th></th></tr></thead>
<tbody>
<tr><td><a href="in-domain.html"><strong>lists.example.org</strong></a></td><td><span class="tag is-success is-light">MX</span></td><td class="sp-mono sp-muted">mx.internal:25</td><td>4 addresses</td><td><span class="tag is-success is-light">required</span></td><td class="sp-actions"><a href="in-domain.html">Open</a><a class="has-text-danger" href="in-domain-delete.html">Delete</a></td></tr>
<tr><td><a href="in-domain.html"><strong>acme.io</strong></a></td><td><span class="tag is-success is-light">MX</span></td><td class="sp-mono sp-muted">10.0.4.12:2525</td><td>any address</td><td><span class="tag is-light">opportunistic</span></td><td class="sp-actions"><a href="in-domain.html">Open</a><a class="has-text-danger" href="in-domain-delete.html">Delete</a></td></tr>
</tbody>
</table>
</table></div>
<div class="sp-box-foot">The MX tag is green when at least one MX of the domain points at this server.</div>
</div>
@@ -12,7 +12,7 @@
<body>
<nav class="navbar is-primary" aria-label="main navigation">
<div class="container">
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-stamp-compact.svg" alt="SelfPost"></a></div>
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
<div class="navbar-menu is-active">
<div class="navbar-start">
<a class="navbar-item" href="overview.html">Overview</a>
@@ -67,7 +67,7 @@
<div class="container sp-page">
<div class="sp-head">
<div class="sp-grow">
<p class="sp-kicker">Inbound · global administrator</p>
<p class="sp-kicker">Inbound · global role</p>
<h1 class="title is-3">Filter lists</h1>
<p class="sp-lead">Correct the spam filter by hand: senders it must always let through, and senders it must always turn away. One pair of lists for the whole instance.</p>
</div>
@@ -102,26 +102,26 @@
<div class="column is-6">
<div class="box">
<div class="sp-box-head"><span class="sp-no">02</span><h2>Deny</h2><div class="sp-end">3 entries · wins over Allow</div></div>
<table class="table is-fullwidth is-hoverable">
<div class="table-container"><table class="table is-fullwidth is-hoverable">
<thead><tr><th>Value</th><th>Note</th><th>Added</th><th></th></tr></thead>
<tbody>
<tr><td class="sp-mono is-size-7">@bulk.example <span class="tag is-light">domain</span></td><td class="sp-muted">newsletter nobody subscribed to</td><td class="sp-muted sp-nowrap is-size-7">2026-09-02</td><td class="sp-actions"><a class="has-text-danger" href="#">Remove</a></td></tr>
<tr><td class="sp-mono is-size-7">198.51.100.0/24 <span class="tag is-light">ip</span></td><td class="sp-muted">dictionary attack on lists.example.org</td><td class="sp-muted sp-nowrap is-size-7">2026-08-19</td><td class="sp-actions"><a class="has-text-danger" href="#">Remove</a></td></tr>
<tr><td class="sp-mono is-size-7">win@lottery.example <span class="tag is-light">address</span></td><td class="sp-muted">—</td><td class="sp-muted sp-nowrap is-size-7">2026-07-30</td><td class="sp-actions"><a class="has-text-danger" href="#">Remove</a></td></tr>
<tr><td class="sp-mono sp-small">@bulk.example <span class="tag is-light">domain</span></td><td class="sp-muted">newsletter nobody subscribed to</td><td class="sp-muted sp-nowrap sp-small">2026-09-02</td><td class="sp-actions"><a class="has-text-danger" href="#">Remove</a></td></tr>
<tr><td class="sp-mono sp-small">198.51.100.0/24 <span class="tag is-light">ip</span></td><td class="sp-muted">dictionary attack on lists.example.org</td><td class="sp-muted sp-nowrap sp-small">2026-08-19</td><td class="sp-actions"><a class="has-text-danger" href="#">Remove</a></td></tr>
<tr><td class="sp-mono sp-small">win@lottery.example <span class="tag is-light">address</span></td><td class="sp-muted">—</td><td class="sp-muted sp-nowrap sp-small">2026-07-30</td><td class="sp-actions"><a class="has-text-danger" href="#">Remove</a></td></tr>
</tbody>
</table>
</table></div>
</div>
</div>
<div class="column is-6">
<div class="box">
<div class="sp-box-head"><span class="sp-no">03</span><h2>Allow</h2><div class="sp-end">2 entries</div></div>
<table class="table is-fullwidth is-hoverable">
<div class="table-container"><table class="table is-fullwidth is-hoverable">
<thead><tr><th>Value</th><th>Note</th><th>Added</th><th></th></tr></thead>
<tbody>
<tr><td class="sp-mono is-size-7">@partner.example <span class="tag is-light">domain</span></td><td class="sp-muted">invoices were scored as spam</td><td class="sp-muted sp-nowrap is-size-7">2026-09-11</td><td class="sp-actions"><a class="has-text-danger" href="#">Remove</a></td></tr>
<tr><td class="sp-mono is-size-7">noreply@github.example <span class="tag is-light">address</span></td><td class="sp-muted">security advisories</td><td class="sp-muted sp-nowrap is-size-7">2026-06-04</td><td class="sp-actions"><a class="has-text-danger" href="#">Remove</a></td></tr>
<tr><td class="sp-mono sp-small">@partner.example <span class="tag is-light">domain</span></td><td class="sp-muted">invoices were scored as spam</td><td class="sp-muted sp-nowrap sp-small">2026-09-11</td><td class="sp-actions"><a class="has-text-danger" href="#">Remove</a></td></tr>
<tr><td class="sp-mono sp-small">noreply@github.example <span class="tag is-light">address</span></td><td class="sp-muted">security advisories</td><td class="sp-muted sp-nowrap sp-small">2026-06-04</td><td class="sp-actions"><a class="has-text-danger" href="#">Remove</a></td></tr>
</tbody>
</table>
</table></div>
</div>
</div>
</div>
@@ -131,7 +131,7 @@
<div class="sp-box-body">
<dl class="sp-facts">
<div><dt>Applied by</dt><dd>rspamd, from map files</dd></div>
<div><dt>Files</dt><dd class="sp-mono is-size-7">/data/antispam/allow.map<br>/data/antispam/deny.map</dd></div>
<div><dt>Files</dt><dd class="sp-mono sp-small">/data/antispam/allow.map<br>/data/antispam/deny.map</dd></div>
<div><dt>Last written</dt><dd>2026-09-11 08:40 UTC</dd></div>
<div><dt>When the filter is off</dt><dd class="sp-muted">The lists are kept and editable, but nothing reads them until <code>INBOUND_ANTISPAM_MILTER</code> is set.</dd></div>
</dl>
+15 -15
View File
@@ -12,7 +12,7 @@
<body>
<nav class="navbar is-primary" aria-label="main navigation">
<div class="container">
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-stamp-compact.svg" alt="SelfPost"></a></div>
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
<div class="navbar-menu is-active">
<div class="navbar-start">
<a class="navbar-item" href="overview.html">Overview</a>
@@ -77,24 +77,24 @@
<div class="sp-box-head">
<span class="sp-no">01</span><h2>Messages</h2>
<form class="sp-end" method="get" action="#">
<div class="select is-small"><select name="domain"><option>All domains</option><option>lists.example.org</option><option>acme.io</option></select></div>
<div class="select is-small"><select name="decision"><option>Any decision</option><option>accept</option><option>reject</option><option>tempfail</option><option>quarantine</option></select></div>
<input class="input is-small" type="date" name="from" value="2026-09-14" aria-label="From date">
<input class="input is-small" type="date" name="to" value="2026-09-21" aria-label="To date">
<button type="submit" class="button is-small"><i class="ti ti-filter"></i>Filter</button>
<div class="select"><select name="domain"><option>All domains</option><option>lists.example.org</option><option>acme.io</option></select></div>
<div class="select"><select name="decision"><option>Any decision</option><option>accept</option><option>reject</option><option>tempfail</option><option>quarantine</option></select></div>
<input class="input" type="date" name="from" value="2026-09-14" aria-label="From date">
<input class="input" type="date" name="to" value="2026-09-21" aria-label="To date">
<button type="submit" class="button"><i class="ti ti-filter"></i>Filter</button>
</form>
</div>
<table class="table is-fullwidth is-hoverable">
<thead><tr><th>Time, UTC</th><th>Client</th><th>From</th><th>To</th><th>Subject</th><th>Decision</th><th>Why</th><th></th></tr></thead>
<div class="table-container"><table class="table is-fullwidth is-hoverable">
<thead><tr><th>Time, UTC</th><th>From · client</th><th>To</th><th>Subject</th><th>Decision</th><th>Why</th><th></th></tr></thead>
<tbody>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-21 14:00:18</td><td class="sp-mono is-size-7">192.0.2.77</td><td>t.berg@uni.example</td><td>dev@lists.example.org</td><td class="sp-clip">Re: [dev] patch series v3</td><td><span class="tag is-success is-light">accept</span></td><td class="sp-muted is-size-7">rspamd · score 0.4</td><td class="sp-actions"><a href="in-filter-lists.html">Deny sender</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-21 13:59:02</td><td class="sp-mono is-size-7">203.0.113.140</td><td>invoice@partner-billing.example</td><td>owner@lists.example.org</td><td class="sp-clip">Outstanding invoice — action required</td><td><span class="tag is-primary is-light">quarantine</span></td><td class="sp-muted is-size-7">rspamd · score 9.8 · PHISHING, FORGED_SENDER</td><td class="sp-actions"><a href="in-quarantine-message.html">Review</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-21 13:57:41</td><td class="sp-mono is-size-7">198.51.100.90</td><td>promo@bulk.example</td><td>sales@lists.example.org</td><td class="sp-clip sp-muted">—</td><td><span class="tag is-danger is-light">reject</span></td><td class="sp-muted is-size-7">postfix · 550 5.1.1 unknown recipient</td><td class="sp-actions"><a href="in-filter-lists.html">Deny sender</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-21 13:51:30</td><td class="sp-mono is-size-7">198.51.100.12</td><td>billing@partner.example</td><td>owner@lists.example.org</td><td class="sp-clip">Invoice 2026-0917</td><td><span class="tag is-danger is-light">reject</span></td><td class="sp-muted is-size-7">rspamd · score 16.2 · BAYES_SPAM</td><td class="sp-actions"><a href="in-filter-lists.html">Allow sender</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-21 13:44:02</td><td class="sp-mono is-size-7">192.0.2.31</td><td>hr@partner.example</td><td>jobs@acme.io</td><td class="sp-clip">Application: backend engineer</td><td><span class="tag is-warning is-light">tempfail</span></td><td class="sp-muted is-size-7">selfpost · upstream 10.0.4.12 timed out</td><td class="sp-actions"></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-21 13:30:55</td><td class="sp-mono is-size-7">192.0.2.200</td><td>noreply@github.example</td><td>security@lists.example.org</td><td class="sp-clip">[advisory] new report</td><td><span class="tag is-success is-light">accept</span></td><td class="sp-muted is-size-7">rspamd · allow list</td><td class="sp-actions"></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 14:00:18</td><td>t.berg@<wbr>uni.example<br><span class="sp-mono sp-muted sp-small">192.0.2.77</span></td><td>dev@<wbr>lists.example.org</td><td class="sp-clip">Re: [dev] patch series v3</td><td><span class="tag is-success is-light">accept</span></td><td class="sp-muted sp-small">rspamd · score 0.4</td><td class="sp-actions"><a href="in-filter-lists.html">Deny sender</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:59:02</td><td>invoice@<wbr>partner-billing.example<br><span class="sp-mono sp-muted sp-small">203.0.113.140</span></td><td>owner@<wbr>lists.example.org</td><td class="sp-clip">Outstanding invoice — action required</td><td><span class="tag is-primary is-light">quarantine</span></td><td class="sp-muted sp-small">rspamd · score 9.8 · PHISHING, FORGED_SENDER</td><td class="sp-actions"><a href="in-quarantine-message.html">Review</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:57:41</td><td>promo@<wbr>bulk.example<br><span class="sp-mono sp-muted sp-small">198.51.100.90</span></td><td>sales@<wbr>lists.example.org</td><td class="sp-clip sp-muted">—</td><td><span class="tag is-danger is-light">reject</span></td><td class="sp-muted sp-small">postfix · 550 5.1.1 unknown recipient</td><td class="sp-actions"><a href="in-filter-lists.html">Deny sender</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:51:30</td><td>billing@<wbr>partner.example<br><span class="sp-mono sp-muted sp-small">198.51.100.12</span></td><td>owner@<wbr>lists.example.org</td><td class="sp-clip">Invoice 2026-0917</td><td><span class="tag is-danger is-light">reject</span></td><td class="sp-muted sp-small">rspamd · score 16.2 · BAYES_SPAM</td><td class="sp-actions"><a href="in-filter-lists.html">Allow sender</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:44:02</td><td>hr@<wbr>partner.example<br><span class="sp-mono sp-muted sp-small">192.0.2.31</span></td><td>jobs@<wbr>acme.io</td><td class="sp-clip">Application: backend engineer</td><td><span class="tag is-warning is-light">tempfail</span></td><td class="sp-muted sp-small">selfpost · upstream 10.0.4.12 timed out</td><td class="sp-actions"></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:30:55</td><td>noreply@<wbr>github.example<br><span class="sp-mono sp-muted sp-small">192.0.2.200</span></td><td>security@<wbr>lists.example.org</td><td class="sp-clip">[advisory] new report</td><td><span class="tag is-success is-light">accept</span></td><td class="sp-muted sp-small">rspamd · allow list</td><td class="sp-actions"></td></tr>
</tbody>
</table>
</table></div>
<div class="sp-box-foot"><span>Page 1 of 38</span><a href="#">Older →</a><span class="sp-end">Allow / Deny sender opens <a href="in-filter-lists.html">Filter lists</a> with the entry filled in · global administrator only</span></div>
</div>
@@ -12,7 +12,7 @@
<body>
<nav class="navbar is-primary" aria-label="main navigation">
<div class="container">
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-stamp-compact.svg" alt="SelfPost"></a></div>
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
<div class="navbar-menu is-active">
<div class="navbar-start">
<a class="navbar-item" href="overview.html">Overview</a>
@@ -92,14 +92,14 @@
<div class="column is-5">
<div class="box">
<div class="sp-box-head"><span class="sp-no">02</span><h2>Why it was held</h2><div class="sp-end">rspamd · score 9.8</div></div>
<table class="table is-fullwidth">
<div class="table-container"><table class="table is-fullwidth">
<thead><tr><th>Symbol</th><th>Score</th></tr></thead>
<tbody>
<tr><td class="sp-mono is-size-7">PHISHING</td><td>5.0</td></tr>
<tr><td class="sp-mono is-size-7">FORGED_SENDER</td><td>3.0</td></tr>
<tr><td class="sp-mono is-size-7">HTML_ATTACHMENT</td><td>1.8</td></tr>
<tr><td class="sp-mono sp-small">PHISHING</td><td>5.0</td></tr>
<tr><td class="sp-mono sp-small">FORGED_SENDER</td><td>3.0</td></tr>
<tr><td class="sp-mono sp-small">HTML_ATTACHMENT</td><td>1.8</td></tr>
</tbody>
</table>
</table></div>
</div>
</div>
</div>
@@ -12,7 +12,7 @@
<body>
<nav class="navbar is-primary" aria-label="main navigation">
<div class="container">
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-stamp-compact.svg" alt="SelfPost"></a></div>
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
<div class="navbar-menu is-active">
<div class="navbar-start">
<a class="navbar-item" href="overview.html">Overview</a>
@@ -77,18 +77,18 @@
<div class="sp-box-head">
<span class="sp-no">01</span><h2>Held messages</h2>
<form class="sp-end" method="get" action="#">
<div class="select is-small"><select name="domain"><option>All domains</option><option>lists.example.org</option><option>acme.io</option></select></div>
<button type="submit" class="button is-small"><i class="ti ti-filter"></i>Filter</button>
<div class="select"><select name="domain"><option>All domains</option><option>lists.example.org</option><option>acme.io</option></select></div>
<button type="submit" class="button"><i class="ti ti-filter"></i>Filter</button>
</form>
</div>
<table class="table is-fullwidth is-hoverable">
<div class="table-container"><table class="table is-fullwidth is-hoverable">
<thead><tr><th>Held, UTC</th><th>From</th><th>To</th><th>Subject</th><th>Why</th><th>Size</th><th>Deleted in</th><th></th></tr></thead>
<tbody>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-21 13:59:02</td><td>invoice@partner-billing.example</td><td>owner@lists.example.org</td><td class="sp-clip"><a href="in-quarantine-message.html"><strong>Outstanding invoice — action required</strong></a></td><td class="sp-muted is-size-7">score 9.8 · PHISHING</td><td class="sp-muted sp-nowrap">48 kB</td><td class="sp-nowrap">14 days</td><td class="sp-actions"><a href="in-quarantine-message.html">Review</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-20 22:14:40</td><td>no-reply@shipping-update.example</td><td>jobs@acme.io</td><td class="sp-clip"><a href="in-quarantine-message.html"><strong>Your parcel could not be delivered</strong></a></td><td class="sp-muted is-size-7">score 8.1 · URL_SHORTENER</td><td class="sp-muted sp-nowrap">12 kB</td><td class="sp-nowrap">13 days</td><td class="sp-actions"><a href="in-quarantine-message.html">Review</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-09 07:02:11</td><td>m.ortega@supplier.example</td><td>owner@lists.example.org</td><td class="sp-clip"><a href="in-quarantine-message.html"><strong>Fwd: scanned contract (3 attachments)</strong></a></td><td class="sp-muted is-size-7">score 7.4 · MIME_ARCHIVE</td><td class="sp-muted sp-nowrap">2.9 MB</td><td class="sp-nowrap"><span class="tag is-warning is-light">2 days</span></td><td class="sp-actions"><a href="in-quarantine-message.html">Review</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:59:02</td><td>invoice@<wbr>partner-billing.example</td><td>owner@<wbr>lists.example.org</td><td class="sp-clip"><a href="in-quarantine-message.html"><strong>Outstanding invoice — action required</strong></a></td><td class="sp-muted sp-small">score 9.8 · PHISHING</td><td class="sp-muted sp-nowrap">48 kB</td><td class="sp-nowrap">14 days</td><td class="sp-actions"><a href="in-quarantine-message.html">Review</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-20 22:14:40</td><td>no-reply@<wbr>shipping-update.example</td><td>jobs@<wbr>acme.io</td><td class="sp-clip"><a href="in-quarantine-message.html"><strong>Your parcel could not be delivered</strong></a></td><td class="sp-muted sp-small">score 8.1 · URL_SHORTENER</td><td class="sp-muted sp-nowrap">12 kB</td><td class="sp-nowrap">13 days</td><td class="sp-actions"><a href="in-quarantine-message.html">Review</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-09 07:02:11</td><td>m.ortega@<wbr>supplier.example</td><td>owner@<wbr>lists.example.org</td><td class="sp-clip"><a href="in-quarantine-message.html"><strong>Fwd: scanned contract (3 attachments)</strong></a></td><td class="sp-muted sp-small">score 7.4 · MIME_ARCHIVE</td><td class="sp-muted sp-nowrap">2.9 MB</td><td class="sp-nowrap"><span class="tag is-warning is-light">2 days</span></td><td class="sp-actions"><a href="in-quarantine-message.html">Review</a></td></tr>
</tbody>
</table>
</table></div>
<div class="sp-box-foot"><span>3 messages · 3.0 MB of 500 MB</span><span class="sp-end">A domain administrator sees only their assigned inbound domains</span></div>
</div>
@@ -12,7 +12,7 @@
<body>
<nav class="navbar is-primary" aria-label="main navigation">
<div class="container">
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-stamp-compact.svg" alt="SelfPost"></a></div>
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
<div class="navbar-menu is-active">
<div class="navbar-start">
<a class="navbar-item" href="overview.html">Overview</a>
@@ -12,7 +12,7 @@
<body>
<nav class="navbar is-primary" aria-label="main navigation">
<div class="container">
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-stamp-compact.svg" alt="SelfPost"></a></div>
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
<div class="navbar-menu is-active">
<div class="navbar-start">
<a class="navbar-item" href="overview.html">Overview</a>
@@ -12,7 +12,7 @@
<body>
<nav class="navbar is-primary" aria-label="main navigation">
<div class="container">
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-stamp-compact.svg" alt="SelfPost"></a></div>
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
<div class="navbar-menu is-active">
<div class="navbar-start">
<a class="navbar-item" href="overview.html">Overview</a>
@@ -12,7 +12,7 @@
<body>
<nav class="navbar is-primary" aria-label="main navigation">
<div class="container">
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-stamp-compact.svg" alt="SelfPost"></a></div>
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
<div class="navbar-menu is-active">
<div class="navbar-start">
<a class="navbar-item" href="overview.html">Overview</a>
@@ -12,7 +12,7 @@
<body>
<nav class="navbar is-primary" aria-label="main navigation">
<div class="container">
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-stamp-compact.svg" alt="SelfPost"></a></div>
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
<div class="navbar-menu is-active">
<div class="navbar-start">
<a class="navbar-item" href="overview.html">Overview</a>
@@ -81,12 +81,13 @@
<form class="sp-form" method="post" action="#">
<div class="field"><label class="label" for="rua">Aggregate reports (rua=)</label>
<div class="control"><div class="select is-fullwidth"><select id="rua" name="dmarc_rua_mode">
<option selected>Same as Settings (dmarc@mail.example.org)</option>
<option selected>admin's default — mix@example.org</option>
<option>SelfPost hosted (example.org@dmarc.mail.example.org)</option>
<option>My default instead — (you are admin)</option>
<option>No aggregate reports</option>
<option>Custom address</option>
</select></div></div>
<p class="help">The default comes from <a href="settings.html">Settings</a>. Changing it changes the DMARC record to publish.</p></div>
<p class="help">A default belongs to a user and is set under their <a href="account.html#dmarc">Account</a>; 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.</p></div>
<button type="submit" class="button is-primary">Save report address</button>
</form>
</div>
@@ -12,7 +12,7 @@
<body>
<nav class="navbar is-primary" aria-label="main navigation">
<div class="container">
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-stamp-compact.svg" alt="SelfPost"></a></div>
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
<div class="navbar-menu is-active">
<div class="navbar-start">
<a class="navbar-item" href="overview.html">Overview</a>
@@ -132,7 +132,7 @@
</div>
<div class="sp-record">
<div class="sp-record-head"><h3>DMARC</h3><span class="tag is-success is-light">published</span><small>Report address <a href="out-domain-settings.html#reports">from Settings</a></small></div>
<div class="sp-record-head"><h3>DMARC</h3><span class="tag is-success is-light">published</span><small>Report address: <a href="out-domain-settings.html#reports">admin's default</a></small></div>
<div class="sp-row">
<div class="field"><label class="label">Host / name</label>
<div class="field has-addons"><div class="control is-expanded"><input class="input" readonly value="_dmarc.example.org"></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
@@ -146,7 +146,7 @@
<div class="box" id="apps">
<div class="sp-box-head"><span class="sp-no">02</span><h2>Applications</h2><div class="sp-end">SASL logins that may send as this domain</div><a class="sp-help" href="help.html#apps" title="What an application is"><i class="ti ti-help-circle"></i></a></div>
<table class="table is-fullwidth is-hoverable">
<div class="table-container"><table class="table is-fullwidth is-hoverable">
<thead><tr><th>Login</th><th>May send as</th><th>30 days</th><th>Limits</th><th></th></tr></thead>
<tbody>
<tr><td class="sp-mono"><strong>prod-server</strong></td><td>*@example.org</td><td class="sp-muted">1 102 msg · peak 96/h</td><td><span class="tag is-success is-light">200 / h</span></td>
@@ -154,7 +154,7 @@
<tr><td class="sp-mono"><strong>alerts</strong></td><td>alerts@example.org, noc@example.org</td><td class="sp-muted">182 msg · peak 14/h</td><td><span class="tag is-light">domain</span> <span class="tag is-success is-light">2 IPs</span></td>
<td class="sp-actions"><a href="out-app.html">Edit</a><a href="out-app-created.html">New password</a><a class="has-text-danger" href="#">Delete</a></td></tr>
</tbody>
</table>
</table></div>
<div class="sp-box-foot">New password and Delete ask for confirmation; the old password stops working at once.</div>
</div>
@@ -12,7 +12,7 @@
<body>
<nav class="navbar is-primary" aria-label="main navigation">
<div class="container">
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-stamp-compact.svg" alt="SelfPost"></a></div>
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
<div class="navbar-menu is-active">
<div class="navbar-start">
<a class="navbar-item" href="overview.html">Overview</a>
@@ -89,14 +89,14 @@
<div class="box">
<div class="sp-box-head"><span class="sp-no">02</span><h2>Domains</h2><div class="sp-end">3 domains · DNS cached a few minutes</div></div>
<table class="table is-fullwidth is-hoverable">
<div class="table-container"><table class="table is-fullwidth is-hoverable">
<thead><tr><th>Domain</th><th>DNS</th><th>Selector</th><th>Apps</th><th>30 days</th><th></th></tr></thead>
<tbody>
<tr><td><a href="out-domain.html"><strong>example.org</strong></a></td><td><span class="tag is-success is-light">DKIM</span> <span class="tag is-danger is-light">SPF</span> <span class="tag is-success is-light">DMARC</span></td><td class="sp-muted sp-mono">sp2026</td><td>2</td><td class="sp-muted">1 284 msg · peak 96/h</td><td class="sp-actions"><a href="out-domain.html">Open</a><a class="has-text-danger" href="out-domain-delete.html">Delete</a></td></tr>
<tr><td><a href="out-domain.html"><strong>shop.example.org</strong></a></td><td><span class="tag is-success is-light">DKIM</span> <span class="tag is-success is-light">SPF</span> <span class="tag is-success is-light">DMARC</span></td><td class="sp-muted sp-mono">sp2026</td><td>1</td><td class="sp-muted">18 920 msg · peak 410/h</td><td class="sp-actions"><a href="out-domain.html">Open</a><a class="has-text-danger" href="out-domain-delete.html">Delete</a></td></tr>
<tr><td><a href="out-domain.html"><strong>notify.acme.io</strong></a></td><td><span class="tag is-success is-light">DKIM</span> <span class="tag is-success is-light">SPF</span> <span class="tag is-warning is-light">DMARC</span></td><td class="sp-muted sp-mono">sp2025</td><td>3</td><td class="sp-muted">402 msg · peak 22/h</td><td class="sp-actions"><a href="out-domain.html">Open</a><a class="has-text-danger" href="out-domain-delete.html">Delete</a></td></tr>
</tbody>
</table>
</table></div>
<div class="sp-box-foot">Each tag is one check. Green — published and correct; amber — published but weak; red — missing or wrong.</div>
</div>
+13 -13
View File
@@ -12,7 +12,7 @@
<body>
<nav class="navbar is-primary" aria-label="main navigation">
<div class="container">
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-stamp-compact.svg" alt="SelfPost"></a></div>
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
<div class="navbar-menu is-active">
<div class="navbar-start">
<a class="navbar-item" href="overview.html">Overview</a>
@@ -77,23 +77,23 @@
<div class="sp-box-head">
<span class="sp-no">01</span><h2>Messages</h2>
<form class="sp-end" method="get" action="#">
<div class="select is-small"><select name="domain"><option>All domains</option><option>example.org</option><option>shop.example.org</option><option>notify.acme.io</option></select></div>
<div class="select is-small"><select name="app"><option>All applications</option><option>prod-server</option><option>alerts</option></select></div>
<div class="select is-small"><select name="status"><option>Any status</option><option>sent</option><option>deferred</option><option>bounced</option><option>rate-limited</option></select></div>
<button type="submit" class="button is-small"><i class="ti ti-filter"></i>Filter</button>
<div class="select"><select name="domain"><option>All domains</option><option>example.org</option><option>shop.example.org</option><option>notify.acme.io</option></select></div>
<div class="select"><select name="app"><option>All applications</option><option>prod-server</option><option>alerts</option></select></div>
<div class="select"><select name="status"><option>Any status</option><option>sent</option><option>deferred</option><option>bounced</option><option>rate-limited</option></select></div>
<button type="submit" class="button"><i class="ti ti-filter"></i>Filter</button>
</form>
</div>
<table class="table is-fullwidth is-hoverable">
<div class="table-container"><table class="table is-fullwidth is-hoverable">
<thead><tr><th>Time, UTC</th><th>From</th><th>To</th><th>Subject</th><th>Status</th><th></th></tr></thead>
<tbody>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-21 14:01:52</td><td>shop@shop.example.org</td><td>m.keller@gmx.de</td><td class="sp-clip">Your order #48213 has shipped</td><td><span class="tag is-success is-light">sent</span></td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-21 14:01:50</td><td>shop@shop.example.org</td><td>anna.lind@outlook.com</td><td class="sp-clip">Your order #48212 has shipped</td><td><span class="tag is-success is-light">sent</span></td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-21 13:58:07</td><td>alerts@example.org</td><td>noc@example.org</td><td class="sp-clip">[FIRING] disk usage above 90 % on db-2</td><td><span class="tag is-warning is-light">deferred</span></td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-21 13:41:19</td><td>no-reply@notify.acme.io</td><td>j.doe@nonexistent.example</td><td class="sp-clip">Reset your password</td><td><span class="tag is-danger is-light">bounced</span></td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-21 13:40:02</td><td>prod@example.org</td><td>billing@partner.example</td><td class="sp-clip">Invoice 2026-0917</td><td><span class="tag is-light">rate-limited</span></td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-21 13:12:44</td><td>shop@shop.example.org</td><td>p.novak@seznam.cz</td><td class="sp-clip">We received your return request</td><td><span class="tag is-success is-light">sent</span></td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 14:01:52</td><td>shop@<wbr>shop.example.org</td><td>m.keller@<wbr>gmx.de</td><td class="sp-clip">Your order #48213 has shipped</td><td><span class="tag is-success is-light">sent</span></td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 14:01:50</td><td>shop@<wbr>shop.example.org</td><td>anna.lind@<wbr>outlook.com</td><td class="sp-clip">Your order #48212 has shipped</td><td><span class="tag is-success is-light">sent</span></td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:58:07</td><td>alerts@<wbr>example.org</td><td>noc@<wbr>example.org</td><td class="sp-clip">[FIRING] disk usage above 90 % on db-2</td><td><span class="tag is-warning is-light">deferred</span></td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:41:19</td><td>no-reply@<wbr>notify.acme.io</td><td>j.doe@<wbr>nonexistent.example</td><td class="sp-clip">Reset your password</td><td><span class="tag is-danger is-light">bounced</span></td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:40:02</td><td>prod@<wbr>example.org</td><td>billing@<wbr>partner.example</td><td class="sp-clip">Invoice 2026-0917</td><td><span class="tag is-light">rate-limited</span></td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:12:44</td><td>shop@<wbr>shop.example.org</td><td>p.novak@<wbr>seznam.cz</td><td class="sp-clip">We received your return request</td><td><span class="tag is-success is-light">sent</span></td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
</tbody>
</table>
</table></div>
<div class="sp-box-foot"><span>Page 1 of 412</span><a href="#">Older →</a><span class="sp-end">New rows appear on their own</span></div>
</div>
@@ -12,7 +12,7 @@
<body>
<nav class="navbar is-primary" aria-label="main navigation">
<div class="container">
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-stamp-compact.svg" alt="SelfPost"></a></div>
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
<div class="navbar-menu is-active">
<div class="navbar-start">
<a class="navbar-item" href="overview.html">Overview</a>
@@ -12,7 +12,7 @@
<body>
<nav class="navbar is-primary" aria-label="main navigation">
<div class="container">
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-stamp-compact.svg" alt="SelfPost"></a></div>
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
<div class="navbar-menu is-active">
<div class="navbar-start">
<a class="navbar-item" href="overview.html">Overview</a>
@@ -75,12 +75,12 @@
<div class="box">
<div class="sp-box-head"><span class="sp-no">01</span><h2>Waiting</h2><div class="sp-end">1 message · 3.1 kB</div></div>
<table class="table is-fullwidth is-hoverable">
<div class="table-container"><table class="table is-fullwidth is-hoverable">
<thead><tr><th>Queue id</th><th>Arrived, UTC</th><th>From</th><th>To</th><th>Last reply</th><th></th></tr></thead>
<tbody>
<tr><td class="sp-mono">4XcB7k2Jm9z1</td><td class="sp-mono sp-muted sp-nowrap is-size-7">09-21 13:58:07</td><td>alerts@example.org</td><td>noc@example.org</td><td class="sp-muted">451 4.7.1 Greylisted, try again in 5 minutes</td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
<tr><td class="sp-mono">4XcB7k2Jm9z1</td><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:58:07</td><td>alerts@<wbr>example.org</td><td>noc@<wbr>example.org</td><td class="sp-muted">451 4.7.1 Greylisted, try again in 5 minutes</td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
</tbody>
</table>
</table></div>
</div>
<div class="box">
+14 -24
View File
@@ -4,13 +4,15 @@
{
"columns": [
{
"is-6": [
"box[Panel credentials]:body"
"is-5": [
"box[Profile]:body",
"box[Password]:body"
]
},
{
"is-6": [
"box[What happens]:body"
"is-7": [
"box[Notifications]:body",
"box[DMARC reports]:record×2"
]
}
]
@@ -165,7 +167,8 @@
]
}
]
}
},
"box[Spam filter]:body"
]
}
]
@@ -392,20 +395,7 @@
"preflight": [
"head+postmark+actions",
"box[Checks]:table",
{
"columns": [
{
"is-6": [
"box[Send a test e-mail]:body"
]
},
{
"is-6": [
"box[Not checked here]:body"
]
}
]
}
"box[Send a test e-mail]:body"
],
"settings": [
"head",
@@ -414,13 +404,13 @@
{
"columns": [
{
"is-7": [
"box[DMARC aggregate reports]:record×2"
"is-6": [
"box[Log retention]:body",
"box[Notifications]:body"
]
},
{
"is-5": [
"box[Log retention]:body",
"is-6": [
"box[Sending rate limits]:body"
]
}
@@ -454,7 +444,7 @@
"columns": [
{
"is-6": [
"box[Sign-in]:body"
"box[Account]:body"
]
},
{
+14 -14
View File
@@ -12,7 +12,7 @@
<body>
<nav class="navbar is-primary" aria-label="main navigation">
<div class="container">
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-stamp-compact.svg" alt="SelfPost"></a></div>
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
<div class="navbar-menu is-active">
<div class="navbar-start">
<a class="navbar-item sp-current" href="overview.html">Overview</a>
@@ -72,18 +72,18 @@
<h1 class="title is-3">Mail is flowing. One thing needs attention.</h1>
<p class="sp-lead">The TLS certificate expires in 12 days and has not renewed yet.</p>
</div>
<div class="sp-muted is-size-7">Updated 14:02:31 UTC<br>refreshes every 10 s</div>
<div class="sp-muted sp-small">Updated 14:02:31 UTC<br>refreshes every 10 s</div>
</div>
<div class="box">
<div class="sp-box-head"><span class="sp-no">01</span><h2>Server health</h2><div class="sp-end"><a href="health.html">All details</a></div><a class="sp-help" href="help.html#checks" title="What these checks mean"><i class="ti ti-help-circle"></i></a></div>
<div class="sp-health">
<a href="health.html#machine"><span class="sp-h-icon"><i class="ti ti-cpu"></i></span><span><span class="sp-h-name">Machine</span><br><span class="sp-h-value">CPU 12 % · RAM 41 %</span><br><span class="sp-h-sub">↓ 18 kB/s · ↑ 44 kB/s</span></span></a>
<a href="health.html#processes"><span class="sp-h-icon"><i class="ti ti-server-cog"></i></span><span><span class="sp-h-name">Processes</span><br><span class="sp-h-value">5 of 5 running</span><br><span class="sp-h-sub">up 18 days</span></span></a>
<a href="health.html#certificate" class="sp-warn"><span class="sp-h-icon"><i class="ti ti-certificate"></i></span><span><span class="sp-h-name">TLS certificate</span><br><span class="sp-h-value">Expires in 12 days</span><br><span class="sp-h-sub">renewal has not run</span></span></a>
<a href="out-queue.html"><span class="sp-h-icon"><i class="ti ti-stack-2"></i></span><span><span class="sp-h-name">Queues</span><br><span class="sp-h-value">Both empty</span><br><span class="sp-h-sub">outbound and inbound</span></span></a>
<a href="health.html#sockets"><span class="sp-h-icon"><i class="ti ti-plug-connected"></i></span><span><span class="sp-h-name">Milter sockets</span><br><span class="sp-h-value">2 of 2 answering</span><br><span class="sp-h-sub">opendkim, journal</span></span></a>
<a href="health.html#hostname"><span class="sp-h-icon"><i class="ti ti-arrows-exchange"></i></span><span><span class="sp-h-name">Reverse DNS</span><br><span class="sp-h-value">Forward = reverse</span><br><span class="sp-h-sub">203.0.113.25</span></span></a>
<a href="health.html#machine"><span class="sp-h-icon"><i class="ti ti-cpu"></i></span><span><span class="sp-h-name">Machine</span><span class="sp-h-value">CPU 12 %</span><span class="sp-h-sub">RAM 41 % · 62 kB/s</span></span></a>
<a href="health.html#processes"><span class="sp-h-icon"><i class="ti ti-server-cog"></i></span><span><span class="sp-h-name">Processes</span><span class="sp-h-value">5 of 5 running</span><span class="sp-h-sub">up 18 days</span></span></a>
<a href="health.html#certificate" class="sp-warn"><span class="sp-h-icon"><i class="ti ti-certificate"></i></span><span><span class="sp-h-name">TLS certificate</span><span class="sp-h-value">Expires in 12 days</span><span class="sp-h-sub">renewal has not run</span></span></a>
<a href="out-queue.html"><span class="sp-h-icon"><i class="ti ti-stack-2"></i></span><span><span class="sp-h-name">Queues</span><span class="sp-h-value">Both empty</span><span class="sp-h-sub">outbound and inbound</span></span></a>
<a href="health.html#sockets"><span class="sp-h-icon"><i class="ti ti-plug-connected"></i></span><span><span class="sp-h-name">Milter sockets</span><span class="sp-h-value">3 of 3 answering</span><span class="sp-h-sub">incl. the spam filter</span></span></a>
<a href="health.html#hostname"><span class="sp-h-icon"><i class="ti ti-arrows-exchange"></i></span><span><span class="sp-h-name">Reverse DNS</span><span class="sp-h-value">Forward = reverse</span><span class="sp-h-sub">203.0.113.25</span></span></a>
</div>
</div>
@@ -91,26 +91,26 @@
<div class="column is-7">
<div class="box">
<div class="sp-box-head"><span class="sp-no">02</span><h2>Outbound domains</h2><div class="sp-end"><a href="out-domains.html">Manage</a></div></div>
<table class="table is-fullwidth is-hoverable">
<div class="table-container"><table class="table is-fullwidth is-hoverable">
<thead><tr><th>Domain</th><th>DNS</th><th>Apps</th><th>30 days</th></tr></thead>
<tbody>
<tr><td><a href="out-domain.html"><strong>example.org</strong></a></td><td><span class="tag is-success is-light">DKIM</span> <span class="tag is-danger is-light">SPF</span> <span class="tag is-success is-light">DMARC</span></td><td>2</td><td class="sp-muted">1 284 msg</td></tr>
<tr><td><a href="out-domain.html"><strong>shop.example.org</strong></a></td><td><span class="tag is-success is-light">DKIM</span> <span class="tag is-success is-light">SPF</span> <span class="tag is-success is-light">DMARC</span></td><td>1</td><td class="sp-muted">18 920 msg</td></tr>
<tr><td><a href="out-domain.html"><strong>notify.acme.io</strong></a></td><td><span class="tag is-success is-light">DKIM</span> <span class="tag is-success is-light">SPF</span> <span class="tag is-warning is-light">DMARC</span></td><td>3</td><td class="sp-muted">402 msg</td></tr>
</tbody>
</table>
</table></div>
</div>
</div>
<div class="column is-5">
<div class="box">
<div class="sp-box-head"><span class="sp-no">03</span><h2>Inbound domains</h2><div class="sp-end"><a href="in-domains.html">Manage</a></div></div>
<table class="table is-fullwidth is-hoverable">
<div class="table-container"><table class="table is-fullwidth is-hoverable">
<thead><tr><th>Domain</th><th>MX</th><th>Upstream</th></tr></thead>
<tbody>
<tr><td><a href="in-domain.html"><strong>lists.example.org</strong></a></td><td><span class="tag is-success is-light">MX</span></td><td class="sp-muted sp-mono is-size-7">mx.internal:25</td></tr>
<tr><td><a href="in-domain.html"><strong>acme.io</strong></a></td><td><span class="tag is-success is-light">MX</span></td><td class="sp-muted sp-mono is-size-7">10.0.4.12:2525</td></tr>
<tr><td><a href="in-domain.html"><strong>lists.example.org</strong></a></td><td><span class="tag is-success is-light">MX</span></td><td class="sp-muted sp-mono sp-small">mx.internal:25</td></tr>
<tr><td><a href="in-domain.html"><strong>acme.io</strong></a></td><td><span class="tag is-success is-light">MX</span></td><td class="sp-muted sp-mono sp-small">10.0.4.12:2525</td></tr>
</tbody>
</table>
</table></div>
</div>
</div>
</div>
@@ -0,0 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="8" height="4" viewBox="0 0 8 4">
<!-- One tooth and one hole of the stamp's edge. Hole radius 2.5 on a pitch of 8:
the mark's 2.2 / 7.03. The hairline is the mark's 0.7 stroke at this scale. -->
<path d="M0 0H8V3H6.5A2.5 2.5 0 0 0 1.5 3H0Z" fill="#F3EDE1"/>
<path d="M0 3H1.5A2.5 2.5 0 0 1 6.5 3H8" fill="none" stroke="#12161C" stroke-width="0.75"/>
</svg>

After

Width:  |  Height:  |  Size: 409 B

+23 -25
View File
@@ -12,7 +12,7 @@
<body>
<nav class="navbar is-primary" aria-label="main navigation">
<div class="container">
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-stamp-compact.svg" alt="SelfPost"></a></div>
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
<div class="navbar-menu is-active">
<div class="navbar-start">
<a class="navbar-item" href="overview.html">Overview</a>
@@ -77,43 +77,41 @@
<div class="box">
<div class="sp-box-head"><span class="sp-no">01</span><h2>Checks</h2><div class="sp-end">Ran 14:02 UTC · 3.1 s</div></div>
<table class="table is-fullwidth">
<div class="table-container"><table class="table is-fullwidth">
<thead><tr><th>Check</th><th>Result</th><th>Found</th><th>What to do</th></tr></thead>
<tbody>
<tr><td><strong>Reverse DNS</strong></td><td><span class="tag is-success is-light">ok</span></td><td class="sp-mono is-size-7">203.0.113.25 ⇄ mail.example.org</td><td class="sp-muted">—</td></tr>
<tr><td><strong>TLS certificate</strong></td><td><span class="tag is-warning is-light">warn</span></td><td class="is-size-7">Valid for <span class="sp-mono">mail.example.org</span>, expires in 12 days</td><td>Renewal has not run. Check that port 80 reaches the ACME client and look for <code>acme</code> in the <a href="system-log.html">system log</a>.</td></tr>
<tr><td><strong>Port 25</strong></td><td><span class="tag is-success is-light">ok</span></td><td class="sp-mono is-size-7">220 mail.example.org ESMTP</td><td class="sp-muted">—</td></tr>
<tr><td><strong>Ports 465, 587</strong></td><td><span class="tag is-success is-light">ok</span></td><td class="sp-mono is-size-7">TLS handshake, 220 banner on both</td><td class="sp-muted">—</td></tr>
<tr><td><strong>HELO name</strong></td><td><span class="tag is-success is-light">ok</span></td><td class="sp-mono is-size-7">mail.example.org</td><td class="sp-muted">—</td></tr>
<tr><td><strong>Proxy headers</strong></td><td><span class="tag is-success is-light">ok</span></td><td class="is-size-7">Peer <span class="sp-mono">172.18.0.2</span> is trusted; <span class="sp-mono">X-Forwarded-Proto: https</span></td><td class="sp-muted">—</td></tr>
<tr><td><strong>DKIM signing</strong></td><td><span class="tag is-success is-light">ok</span></td><td class="sp-mono is-size-7">inet:127.0.0.1:8891 answers</td><td class="sp-muted">—</td></tr>
<tr><td class="sp-nowrap"><strong>Reverse DNS</strong></td><td><span class="tag is-success is-light">ok</span></td><td class="sp-mono sp-small">203.0.113.25 ⇄ mail.example.org</td><td class="sp-muted">—</td></tr>
<tr><td class="sp-nowrap"><strong>TLS certificate</strong></td><td><span class="tag is-warning is-light">warn</span></td><td class="sp-small">Valid for <span class="sp-mono">mail.example.org</span>, expires in 12 days</td><td>Renewal has not run. Check that port 80 reaches the ACME client and look for <code>acme</code> in the <a href="system-log.html">system log</a>.</td></tr>
<tr><td class="sp-nowrap"><strong>Port 25</strong></td><td><span class="tag is-success is-light">ok</span></td><td class="sp-mono sp-small">220 mail.example.org ESMTP</td><td class="sp-muted">—</td></tr>
<tr><td class="sp-nowrap"><strong>Ports 465, 587</strong></td><td><span class="tag is-success is-light">ok</span></td><td class="sp-mono sp-small">TLS handshake, 220 banner on both</td><td class="sp-muted">—</td></tr>
<tr><td class="sp-nowrap"><strong>HELO name</strong></td><td><span class="tag is-success is-light">ok</span></td><td class="sp-mono sp-small">mail.example.org</td><td class="sp-muted">—</td></tr>
<tr><td class="sp-nowrap"><strong>Proxy headers</strong></td><td><span class="tag is-success is-light">ok</span></td><td class="sp-small">Peer <span class="sp-mono">172.18.0.2</span> is trusted; <span class="sp-mono">X-Forwarded-Proto: https</span></td><td class="sp-muted">—</td></tr>
<tr><td class="sp-nowrap"><strong>DKIM signing</strong></td><td><span class="tag is-success is-light">ok</span></td><td class="sp-mono sp-small">inet:127.0.0.1:8891 answers</td><td class="sp-muted">—</td></tr>
</tbody>
</table>
<div class="sp-box-foot">Ports are dialled from inside the container at the public address; a firewall that treats inside and outside differently can make this pass while the world is still blocked.</div>
</table></div>
<div class="sp-box-foot"><span>Ports are dialled from inside the container at the public address; a firewall that treats inside and outside differently can make this pass while the world is still blocked. SPF, DKIM and DMARC are per domain — each <a href=\"out-domains.html\">outbound domain</a> shows its own.</span></div>
</div>
<div class="columns">
<div class="column is-6">
<div class="box">
<div class="sp-box-head"><span class="sp-no">02</span><h2>Send a test e-mail</h2></div>
<div class="sp-box-body">
<div class="box">
<div class="sp-box-head"><span class="sp-no">02</span><h2>Send a test e-mail</h2><div class="sp-end">Confirms the relay accepted it, not that it arrived</div></div>
<div class="sp-box-body">
<div class="columns">
<div class="column is-6">
<form class="sp-form" method="post" action="#">
<div class="field"><label class="label" for="rcpt">Recipient</label>
<div class="field has-addons">
<div class="control is-expanded has-icons-left"><input class="input" id="rcpt" name="recipient" type="email" placeholder="you@elsewhere.example" required><span class="icon is-left"><i class="ti ti-mail"></i></span></div>
<div class="control"><button type="submit" class="button is-primary"><i class="ti ti-send"></i>Send</button></div>
</div>
<p class="help">Sent from <span class="sp-mono">postmaster@mail.example.org</span> through the local Postfix. This confirms the relay accepted it — follow the delivery in the <a href="out-log.html">outbound log</a>.</p></div>
<p class="help">Sent from <span class="sp-mono">postmaster@mail.example.org</span> through the local Postfix.</p></div>
</form>
</div>
</div>
</div>
<div class="column is-6">
<div class="box">
<div class="sp-box-head"><span class="sp-no">03</span><h2>Not checked here</h2></div>
<div class="sp-box-body">
<p class="mb-2">SPF, DKIM and DMARC records are per domain — each <a href="out-domains.html">outbound domain</a> shows its own.</p>
<p class="sp-muted">Whether a receiver finally accepted the test message is not tracked; only that it left.</p>
<div class="column is-6">
<dl class="sp-facts">
<div><dt>Last test</dt><dd>14:03 UTC to <span class="sp-mono">mix@elsewhere.example</span></dd></div>
<div><dt>Result</dt><dd><span class="tag is-success is-light">queued</span> <span class="sp-mono sp-small">4XcC1d8Pq2z1</span></dd></div>
<div><dt>Then</dt><dd><a href="out-message.html">Follow it in the outbound log</a></dd></div>
</dl>
</div>
</div>
</div>
+14 -25
View File
@@ -12,7 +12,7 @@
<body>
<nav class="navbar is-primary" aria-label="main navigation">
<div class="container">
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-stamp-compact.svg" alt="SelfPost"></a></div>
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
<div class="navbar-menu is-active">
<div class="navbar-start">
<a class="navbar-item" href="overview.html">Overview</a>
@@ -69,36 +69,15 @@
<div class="sp-grow">
<p class="sp-kicker">Server</p>
<h1 class="title is-3">Settings</h1>
<p class="sp-lead">Defaults for the whole instance. Your own username and password are under <a href="account.html">Account</a>.</p>
<p class="sp-lead">What holds for the whole instance, whoever is signed in. Your own name, e-mail, notifications and DMARC report address are under <a href="account.html">Account</a>.</p>
</div>
</div>
<form class="sp-form" method="post" action="#">
<div class="columns">
<div class="column is-7">
<div class="column is-6">
<div class="box">
<div class="sp-box-head"><span class="sp-no">01</span><h2>DMARC aggregate reports</h2><div class="sp-end"><span class="tag is-success is-light">authorized</span></div></div>
<div class="sp-record">
<div class="field"><label class="label" for="rua">Default report address</label>
<div class="control has-icons-left"><input class="input" id="rua" name="dmarc_report_email" type="email" value="dmarc@mail.example.org"><span class="icon is-left"><i class="ti ti-mail"></i></span></div>
<p class="help">The default <code>rua=</code> for every outbound domain; a domain may override it. An address on <code>mail.example.org</code> is received and parsed by SelfPost itself.</p></div>
</div>
<div class="sp-record">
<div class="sp-record-head"><h3>Report authorization</h3><span class="tag is-success is-light">published</span><small>Needed when <code>rua=</code> points at another domain</small></div>
<div class="sp-row">
<div class="field"><label class="label">Host / name</label>
<div class="field has-addons"><div class="control is-expanded"><input class="input" readonly value="*._report._dmarc.mail.example.org"></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
<div class="field"><label class="label">Type</label><div class="control"><input class="input" readonly value="TXT"></div></div>
</div>
<div class="field"><label class="label">Value to publish</label>
<div class="field has-addons"><div class="control is-expanded"><input class="input" readonly value="v=DMARC1"></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
</div>
</div>
</div>
<div class="column is-5">
<div class="box">
<div class="sp-box-head"><span class="sp-no">02</span><h2>Log retention</h2></div>
<div class="sp-box-head"><span class="sp-no">01</span><h2>Log retention</h2></div>
<div class="sp-box-body">
<div class="field"><label class="label" for="ret">Keep outbound log rows, days</label>
<div class="control"><input class="input" id="ret" name="send_log_retention_days" type="number" min="7" max="365" value="30" required></div>
@@ -106,6 +85,16 @@
</div>
</div>
<div class="box">
<div class="sp-box-head"><span class="sp-no">02</span><h2>Notifications</h2></div>
<div class="sp-box-body">
<div class="field"><label class="checkbox"><input type="checkbox" name="notify" checked> Send notification e-mails</label></div>
<p class="help">Sent through this relay from <span class="sp-mono">postmaster@mail.example.org</span>. Each user picks the events under <a href="account.html">Account</a>; a user without an e-mail gets none.</p>
</div>
</div>
</div>
<div class="column is-6">
<div class="box" id="rate-limits">
<div class="sp-box-head"><span class="sp-no">03</span><h2>Sending rate limits</h2><div class="sp-end">Read-only</div></div>
<div class="sp-box-body">
@@ -1,36 +1,87 @@
<!-- title: Account | section: user -->
<div class="sp-head">
<div class="sp-grow">
<p class="sp-kicker">Signed in as admin · global administrator</p>
<p class="sp-kicker">Signed in as admin · global</p>
<h1 class="title is-3">Account</h1>
<p class="sp-lead">Your sign-in to this panel. Applications keep their own logins and passwords and are not affected.</p>
<p class="sp-lead">Who you are to this panel, what it should tell you about, and where reports about your domains go. Applications keep their own logins and passwords and are not affected.</p>
</div>
</div>
<div class="columns">
<div class="column is-6">
<div class="column is-5">
<div class="box">
<div class="sp-box-head"><span class="sp-no">01</span><h2>Panel credentials</h2></div>
<div class="sp-box-head"><span class="sp-no">01</span><h2>Profile</h2></div>
<div class="sp-box-body">
<form class="sp-form" method="post" action="#">
<div class="field"><label class="label" for="username">Username</label><div class="control has-icons-left"><input class="input" id="username" name="username" value="admin" autocomplete="username" spellcheck="false" required><span class="icon is-left"><i class="ti ti-user"></i></span></div></div>
<div class="field"><label class="label" for="email">E-mail</label><div class="control has-icons-left"><input class="input" id="email" name="email" type="email" value="mix@example.org" autocomplete="email"><span class="icon is-left"><i class="ti ti-mail"></i></span></div>
<p class="help">Where the panel writes to you: the notifications on the right and a password-reset link. It can also receive DMARC reports — your choice, under <a href="#dmarc">DMARC reports</a>.</p></div>
<button type="submit" class="button is-primary">Save profile</button>
</form>
</div>
</div>
<div class="box">
<div class="sp-box-head"><span class="sp-no">02</span><h2>Password</h2></div>
<div class="sp-box-body">
<form class="sp-form" method="post" action="#">
<div class="field"><label class="label" for="username">Username</label><div class="control"><input class="input" id="username" name="username" value="admin" autocomplete="username" spellcheck="false" required></div></div>
<div class="field"><label class="label" for="cur">Current password</label><div class="control"><input class="input" id="cur" name="current_password" type="password" autocomplete="current-password" required></div></div>
<div class="sp-pair">
<div class="field"><label class="label" for="new">New password</label><div class="control"><input class="input" id="new" name="new_password" type="password" autocomplete="new-password"></div></div>
<div class="field"><label class="label" for="new2">Confirm new password</label><div class="control"><input class="input" id="new2" name="new_password_confirm" type="password" autocomplete="new-password"></div></div>
<div class="field"><label class="label" for="new">New password</label><div class="control"><input class="input" id="new" name="new_password" type="password" autocomplete="new-password" required></div></div>
<div class="field"><label class="label" for="new2">Repeat</label><div class="control"><input class="input" id="new2" name="new_password_confirm" type="password" autocomplete="new-password" required></div></div>
</div>
<p class="help mb-3">Leave both new-password fields empty to change the username only.</p>
<button type="submit" class="button is-primary">Save changes</button>
<p class="help mb-3">Changing it signs out every other session of this account; this one stays signed in.</p>
<button type="submit" class="button">Change password</button>
</form>
</div>
</div>
</div>
<div class="column is-6">
<div class="column is-7">
<div class="box">
<div class="sp-box-head"><span class="sp-no"><i class="ti ti-info-circle"></i></span><h2>What happens</h2></div>
<div class="sp-box-head"><span class="sp-no">03</span><h2>Notifications</h2><div class="sp-end">Sent to mix@example.org</div></div>
<div class="sp-box-body">
<p class="mb-2">Changing the password signs out every other session of this account. This one stays signed in.</p>
<p class="sp-muted">A domain administrator sees this same page, the Outbound and Inbound domains assigned to them, and nothing under Server.</p>
<form class="sp-form" method="post" action="#">
<p class="sp-kicker mb-2">Your domains</p>
<div class="field"><label class="checkbox"><input type="checkbox" name="ev" checked> A DNS record stops matching — DKIM, SPF, DMARC or an inbound MX</label></div>
<div class="field"><label class="checkbox"><input type="checkbox" name="ev" checked> A domain or an application reaches its rate limit and mail is being refused</label></div>
<div class="field"><label class="checkbox"><input type="checkbox" name="ev" checked> An inbound domain's upstream cannot be reached and mail is piling up</label></div>
<div class="field"><label class="checkbox"><input type="checkbox" name="ev"> DMARC reports start showing failures for a domain</label></div>
<div class="field"><label class="checkbox"><input type="checkbox" name="ev"> Mail is held in quarantine</label></div>
<p class="sp-kicker mt-4 mb-2">The server · global role</p>
<div class="field"><label class="checkbox"><input type="checkbox" name="ev" checked> The TLS certificate is within 14 days of expiry</label></div>
<div class="field"><label class="checkbox"><input type="checkbox" name="ev" checked> A process or a milter socket stops answering</label></div>
<div class="field"><label class="checkbox"><input type="checkbox" name="ev"> Messages sit in a queue for more than a day</label></div>
<p class="help mb-3">One message when a problem starts and one when it clears — never a stream. A domain administrator is told only about the domains assigned to them.</p>
<button type="submit" class="button is-primary">Save notifications</button>
</form>
</div>
</div>
<div class="box" id="dmarc">
<div class="sp-box-head"><span class="sp-no">04</span><h2>DMARC reports</h2><div class="sp-end"><span class="tag is-success is-light">authorized</span></div><a class="sp-help" href="help.html#dmarc" title="What the reports are and where they go"><i class="ti ti-help-circle"></i></a></div>
<div class="sp-record">
<form class="sp-form" method="post" action="#">
<div class="field"><label class="label" for="rua">My default report address</label>
<div class="control"><div class="select is-fullwidth"><select id="rua" name="dmarc_default">
<option>SelfPost hosted — read them under DMARC reports</option>
<option selected>My account e-mail — mix@example.org</option>
<option>Another address…</option>
<option>No reports</option>
</select></div></div>
<p class="help">Used by every outbound domain whose report address is set to <em>admin's default</em> — 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.</p></div>
<button type="submit" class="button is-primary">Save report address</button>
</form>
</div>
<div class="sp-record">
<div class="sp-record-head"><h3>Report authorization</h3><span class="tag is-success is-light">published</span><small>Needed because the address is on another domain than the senders</small></div>
<div class="sp-row">
<div class="field"><label class="label">Host / name</label>
<div class="field has-addons"><div class="control is-expanded"><input class="input" readonly value="*._report._dmarc.example.org"></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
<div class="field"><label class="label">Type</label><div class="control"><input class="input" readonly value="TXT"></div></div>
</div>
<div class="field"><label class="label">Value to publish</label>
<div class="field has-addons"><div class="control is-expanded"><input class="input" readonly value="v=DMARC1"></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
</div>
</div>
</div>
@@ -40,16 +40,16 @@
<div class="box">
<div class="sp-box-head"><span class="sp-no">03</span><h2>status_tag</h2><div class="sp-end">One mapping, one function</div></div>
<table class="table is-fullwidth">
<div class="table-container"><table class="table is-fullwidth">
<thead><tr><th>Status</th><th>Tag</th><th>Bulma classes</th></tr></thead>
<tbody>
<tr><td>ok, published, running, sent</td><td><span class="tag is-success is-light">ok</span></td><td class="sp-mono is-size-7">tag is-success is-light</td></tr>
<tr><td>warn, deferred, quiet</td><td><span class="tag is-warning is-light">warn</span></td><td class="sp-mono is-size-7">tag is-warning is-light</td></tr>
<tr><td>error, fail, mismatch, bounced</td><td><span class="tag is-danger is-light">fail</span></td><td class="sp-mono is-size-7">tag is-danger is-light</td></tr>
<tr><td>unknown, off, neutral counts</td><td><span class="tag is-light">off</span></td><td class="sp-mono is-size-7">tag is-light</td></tr>
<tr><td>held: quarantine · role: global</td><td><span class="tag is-primary is-light">quarantine</span></td><td class="sp-mono is-size-7">tag is-primary is-light</td></tr>
<tr><td>ok, published, running, sent</td><td><span class="tag is-success is-light">ok</span></td><td class="sp-mono sp-small">tag is-success is-light</td></tr>
<tr><td>warn, deferred, quiet</td><td><span class="tag is-warning is-light">warn</span></td><td class="sp-mono sp-small">tag is-warning is-light</td></tr>
<tr><td>error, fail, mismatch, bounced</td><td><span class="tag is-danger is-light">fail</span></td><td class="sp-mono sp-small">tag is-danger is-light</td></tr>
<tr><td>unknown, off, neutral counts</td><td><span class="tag is-light">off</span></td><td class="sp-mono sp-small">tag is-light</td></tr>
<tr><td>held: quarantine · role: global</td><td><span class="tag is-primary is-light">quarantine</span></td><td class="sp-mono sp-small">tag is-primary is-light</td></tr>
</tbody>
</table>
</table></div>
</div>
</div>
@@ -73,9 +73,9 @@
<div class="box">
<div class="sp-box-head"><span class="sp-no">04</span><h2>health_card</h2><div class="sp-end">Overview only · each card links to its detail</div></div>
<div class="sp-health">
<a href="#"><span class="sp-h-icon"><i class="ti ti-server-cog"></i></span><span><span class="sp-h-name">Name</span><br><span class="sp-h-value">Verdict in words</span><br><span class="sp-h-sub">ok · one supporting fact</span></span></a>
<a href="#" class="sp-warn"><span class="sp-h-icon"><i class="ti ti-certificate"></i></span><span><span class="sp-h-name">Name</span><br><span class="sp-h-value">Expires in 12 days</span><br><span class="sp-h-sub">sp-warn</span></span></a>
<a href="#" class="sp-fail"><span class="sp-h-icon"><i class="ti ti-plug-connected-x"></i></span><span><span class="sp-h-name">Name</span><br><span class="sp-h-value">1 of 2 answering</span><br><span class="sp-h-sub">sp-fail</span></span></a>
<a href="#"><span class="sp-h-icon"><i class="ti ti-server-cog"></i></span><span><span class="sp-h-name">Name</span><span class="sp-h-value">Verdict in words</span><span class="sp-h-sub">ok · one supporting fact</span></span></a>
<a href="#" class="sp-warn"><span class="sp-h-icon"><i class="ti ti-certificate"></i></span><span><span class="sp-h-name">Name</span><span class="sp-h-value">Expires in 12 days</span><span class="sp-h-sub">sp-warn</span></span></a>
<a href="#" class="sp-fail"><span class="sp-h-icon"><i class="ti ti-plug-connected-x"></i></span><span><span class="sp-h-name">Name</span><span class="sp-h-value">1 of 2 answering</span><span class="sp-h-sub">sp-fail</span></span></a>
</div>
</div>
@@ -102,13 +102,13 @@
</div>
<div class="box">
<div class="sp-box-head"><span class="sp-no">07</span><h2>Table conventions</h2></div>
<table class="table is-fullwidth is-hoverable">
<div class="sp-box-head"><span class="sp-no">07</span><h2>Table conventions</h2><div class="sp-end">Always inside <code>table-container</code> · an address breaks after its @</div></div>
<div class="table-container"><table class="table is-fullwidth is-hoverable">
<thead><tr><th>Time, UTC</th><th>Name</th><th>sp-clip</th><th>Status</th><th></th></tr></thead>
<tbody>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-21 14:01:52</td><td><a href="#"><strong>the row's link</strong></a></td><td class="sp-clip">A long subject is clipped with an ellipsis instead of wrapping the row</td><td><span class="tag is-success is-light">sent</span></td><td class="sp-actions"><a href="#">Details</a><a class="has-text-danger" href="#">Delete</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 14:01:52</td><td><a href="#"><strong>the row's link</strong></a></td><td class="sp-clip">A long subject is clipped with an ellipsis instead of wrapping the row</td><td><span class="tag is-success is-light">sent</span></td><td class="sp-actions"><a href="#">Details</a><a class="has-text-danger" href="#">Delete</a></td></tr>
</tbody>
</table>
</table></div>
<div class="sp-box-foot"><span>Page 1 of 9</span><a href="#">Older →</a></div>
</div>
@@ -195,3 +195,28 @@
</div>
</div>
</div>
<div class="box">
<div class="sp-box-head"><span class="sp-no">13</span><h2>shell</h2><div class="sp-end">Owned by the layout — a screen never writes it</div></div>
<div class="sp-box-body">
<dl class="sp-facts">
<div><dt>Name</dt><dd>The wordmark, not the stamp: at nav size the stamp's perforation is under a pixel and its brick field dissolves into the bar. The stamp lives on sign-in, setup and the favicon.</dd></div>
<div><dt>Edge</dt><dd>Built like the mark: the brick field, then the stamp's paper margin, and the perforation along the outer edge of that margin — round holes, flat teeth, the ink hairline. One tile, <code>perf-edge.svg</code>.</dd></div>
<div><dt>Sibling strip</dt><dd>The pages of the current group, under the edge. Five or six entries; past that the group is split.</dd></div>
<div><dt>Menus</dt><dd>Bulma hover dropdowns, no JavaScript. The user menu holds Account, Help and Sign out.</dd></div>
</dl>
</div>
</div>
<div class="box">
<div class="sp-box-head"><span class="sp-no">14</span><h2>Type scale</h2><div class="sp-end">Three sizes · nothing to be read is under 13px</div></div>
<div class="table-container"><table class="table is-fullwidth">
<thead><tr><th>Size</th><th>Looks like</th><th>Used for</th></tr></thead>
<tbody>
<tr><td class="sp-mono sp-small">body · 16px</td><td>The quick brown fox, 203.0.113.25</td><td class="sp-muted">Text, table cells, inputs, selects, buttons, menus and their dropdowns</td></tr>
<tr><td class="sp-mono sp-small">--sp-fs-s · 14px</td><td class="sp-small">The quick brown fox, <span class="sp-mono">203.0.113.25</span></td><td class="sp-muted">Supporting text: labels, <code>help</code>, notes in a box head or foot, mono values (<code>sp-small</code>), logs, the footer</td></tr>
<tr><td class="sp-mono sp-small">--sp-fs-xs · 13px</td><td><span class="sp-kicker">Upper-case mono label</span> <span class="tag is-light">tag</span></td><td class="sp-muted">Upper-case mono micro-labels (kicker, table head, <code>dt</code>, box title) and tags — nothing else</td></tr>
</tbody>
</table></div>
<div class="sp-box-foot">Bulma's <code>is-small</code> and <code>is-size-7</code> are not used: a control is never small, and <code>sp-small</code> replaces the helper. The postmark's fine print is an imprint, not text.</div>
</div>
@@ -13,26 +13,26 @@
<div class="column is-5">
<div class="box">
<div class="sp-box-head"><span class="sp-no">01</span><h2>Sources · 7 days</h2></div>
<table class="table is-fullwidth">
<div class="table-container"><table class="table is-fullwidth">
<thead><tr><th>Source</th><th>Pass</th><th>Fail</th><th>Disposition</th></tr></thead>
<tbody>
<tr><td class="sp-mono is-size-7">203.0.113.25 <span class="tag is-light">this relay</span></td><td>11 204</td><td>0</td><td><span class="tag is-success is-light">none</span></td></tr>
<tr><td class="sp-mono is-size-7">198.51.100.44</td><td>0</td><td>41</td><td><span class="tag is-warning is-light">none</span></td></tr>
<tr><td class="sp-mono sp-small">203.0.113.25 <span class="tag is-light">this relay</span></td><td>11 204</td><td>0</td><td><span class="tag is-success is-light">none</span></td></tr>
<tr><td class="sp-mono sp-small">198.51.100.44</td><td>0</td><td>41</td><td><span class="tag is-warning is-light">none</span></td></tr>
</tbody>
</table>
</table></div>
</div>
</div>
<div class="column is-7">
<div class="box">
<div class="sp-box-head"><span class="sp-no">02</span><h2>Reports</h2></div>
<table class="table is-fullwidth is-hoverable">
<div class="table-container"><table class="table is-fullwidth is-hoverable">
<thead><tr><th>Received, UTC</th><th>Reporter</th><th>Window</th><th>Pass</th><th>Fail</th><th></th></tr></thead>
<tbody>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-21 04:40</td><td class="sp-muted">Outlook.com</td><td>20 Sep</td><td>1 903</td><td><span class="tag is-warning is-light">7</span></td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-21 02:15</td><td class="sp-muted">google.com</td><td>20 Sep</td><td>6 410</td><td>0</td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-20 04:38</td><td class="sp-muted">Outlook.com</td><td>19 Sep</td><td>1 877</td><td><span class="tag is-warning is-light">34</span></td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 04:40</td><td class="sp-muted">Outlook.com</td><td>20 Sep</td><td>1 903</td><td><span class="tag is-warning is-light">7</span></td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 02:15</td><td class="sp-muted">google.com</td><td>20 Sep</td><td>6 410</td><td>0</td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-20 04:38</td><td class="sp-muted">Outlook.com</td><td>19 Sep</td><td>1 877</td><td><span class="tag is-warning is-light">34</span></td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
</tbody>
</table>
</table></div>
</div>
</div>
</div>
@@ -14,7 +14,7 @@
<div class="sp-box-body">
<dl class="sp-facts">
<div><dt>Reporter</dt><dd>Outlook.com</dd></div>
<div><dt>Report id</dt><dd class="sp-mono is-size-7">5f1c9a0e7b2d4e61a3</dd></div>
<div><dt>Report id</dt><dd class="sp-mono sp-small">5f1c9a0e7b2d4e61a3</dd></div>
<div><dt>Window</dt><dd>20 Sep 00:00 – 21 Sep 00:00 UTC</dd></div>
<div><dt>Received</dt><dd>2026-09-21 04:40 UTC</dd></div>
</dl>
@@ -28,7 +28,7 @@
<dl class="sp-facts">
<div><dt>p / sp / pct</dt><dd class="sp-mono">none / none / 100</dd></div>
<div><dt>adkim / aspf</dt><dd class="sp-mono">r / r</dd></div>
<div><dt>rua</dt><dd class="sp-mono is-size-7">dmarc@mail.example.org</dd></div>
<div><dt>rua</dt><dd class="sp-mono sp-small">dmarc@mail.example.org</dd></div>
</dl>
</div>
</div>
@@ -37,11 +37,11 @@
<div class="box">
<div class="sp-box-head"><span class="sp-no">03</span><h2>Records</h2><div class="sp-end">Parsed from the aggregate XML</div></div>
<table class="table is-fullwidth">
<div class="table-container"><table class="table is-fullwidth">
<thead><tr><th>Source</th><th>Count</th><th>Disposition</th><th>SPF</th><th>DKIM</th><th>Header from</th></tr></thead>
<tbody>
<tr><td class="sp-mono is-size-7">203.0.113.25 <span class="tag is-light">this relay</span></td><td>1 903</td><td><span class="tag is-success is-light">none</span></td><td><span class="tag is-success is-light">pass</span></td><td><span class="tag is-success is-light">pass</span></td><td>shop.example.org</td></tr>
<tr><td class="sp-mono is-size-7">198.51.100.44</td><td>7</td><td><span class="tag is-warning is-light">none</span></td><td><span class="tag is-danger is-light">fail</span></td><td><span class="tag is-danger is-light">fail</span></td><td>shop.example.org</td></tr>
<tr><td class="sp-mono sp-small">203.0.113.25 <span class="tag is-light">this relay</span></td><td>1 903</td><td><span class="tag is-success is-light">none</span></td><td><span class="tag is-success is-light">pass</span></td><td><span class="tag is-success is-light">pass</span></td><td>shop.example.org</td></tr>
<tr><td class="sp-mono sp-small">198.51.100.44</td><td>7</td><td><span class="tag is-warning is-light">none</span></td><td><span class="tag is-danger is-light">fail</span></td><td><span class="tag is-danger is-light">fail</span></td><td>shop.example.org</td></tr>
</tbody>
</table>
</table></div>
</div>
@@ -21,13 +21,13 @@
<div class="box">
<div class="sp-box-head"><span class="sp-no">02</span><h2>Recent reports</h2></div>
<table class="table is-fullwidth is-hoverable">
<div class="table-container"><table class="table is-fullwidth is-hoverable">
<thead><tr><th>Received, UTC</th><th>Domain</th><th>Reporter</th><th>Window</th><th>Pass</th><th>Fail</th><th></th></tr></thead>
<tbody>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-21 06:12</td><td><a href="dmarc-domain.html">example.org</a></td><td class="sp-muted">google.com</td><td>20 Sep</td><td>412</td><td>0</td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-21 04:40</td><td><a href="dmarc-domain.html">shop.example.org</a></td><td class="sp-muted">Outlook.com</td><td>20 Sep</td><td>1 903</td><td><span class="tag is-warning is-light">7</span></td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-20 23:58</td><td><a href="dmarc-domain.html">example.org</a></td><td class="sp-muted">Yahoo</td><td>19 Sep</td><td>38</td><td>0</td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-20 06:09</td><td><a href="dmarc-domain.html">notify.acme.io</a></td><td class="sp-muted">google.com</td><td>19 Sep</td><td>61</td><td>0</td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 06:12</td><td><a href="dmarc-domain.html">example.org</a></td><td class="sp-muted">google.com</td><td>20 Sep</td><td>412</td><td>0</td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 04:40</td><td><a href="dmarc-domain.html">shop.example.org</a></td><td class="sp-muted">Outlook.com</td><td>20 Sep</td><td>1 903</td><td><span class="tag is-warning is-light">7</span></td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-20 23:58</td><td><a href="dmarc-domain.html">example.org</a></td><td class="sp-muted">Yahoo</td><td>19 Sep</td><td>38</td><td>0</td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-20 06:09</td><td><a href="dmarc-domain.html">notify.acme.io</a></td><td class="sp-muted">google.com</td><td>19 Sep</td><td>61</td><td>0</td><td class="sp-actions"><a href="dmarc-report.html">View</a></td></tr>
</tbody>
</table>
</table></div>
</div>
@@ -12,20 +12,20 @@
<div class="column is-6">
<div class="box" id="machine">
<div class="sp-box-head"><span class="sp-no">01</span><h2>Machine</h2><div class="sp-end"><span class="tag is-success is-light">ok</span></div></div>
<table class="table is-fullwidth">
<div class="table-container"><table class="table is-fullwidth">
<thead><tr><th>Resource</th><th>Usage</th><th>Detail</th></tr></thead>
<tbody>
<tr><td>CPU</td><td><progress class="progress is-small is-success mb-1" value="12" max="100"></progress>12 %</td><td class="sp-muted">2 cores, load 0.21</td></tr>
<tr><td>Memory</td><td><progress class="progress is-small is-success mb-1" value="41" max="100"></progress>41 %</td><td class="sp-muted">812 MB of 1.9 GB</td></tr>
<tr><td>Network</td><td class="sp-nowrap">↓ 18 kB/s · ↑ 44 kB/s</td><td class="sp-muted">eth0: 2.1 GB in, 6.4 GB out</td></tr>
</tbody>
</table>
</table></div>
</div>
</div>
<div class="column is-6">
<div class="box" id="processes">
<div class="sp-box-head"><span class="sp-no">02</span><h2>Processes</h2><div class="sp-end"><span class="tag is-success is-light">ok</span></div></div>
<table class="table is-fullwidth">
<div class="table-container"><table class="table is-fullwidth">
<thead><tr><th>Program</th><th>State</th><th>Detail</th></tr></thead>
<tbody>
<tr><td>postfix</td><td><span class="tag is-success is-light">running</span></td><td class="sp-muted">pid 41, uptime 18 days</td></tr>
@@ -34,7 +34,7 @@
<tr><td>panel</td><td><span class="tag is-success is-light">running</span></td><td class="sp-muted">pid 44, uptime 18 days</td></tr>
<tr><td>dmarc-ingest</td><td><span class="tag is-success is-light">running</span></td><td class="sp-muted">pid 47, uptime 18 days</td></tr>
</tbody>
</table>
</table></div>
</div>
</div>
</div>
@@ -56,13 +56,14 @@
<div class="column is-4">
<div class="box" id="sockets">
<div class="sp-box-head"><span class="sp-no">04</span><h2>Milter sockets</h2><div class="sp-end"><span class="tag is-success is-light">ok</span></div></div>
<table class="table is-fullwidth">
<div class="table-container"><table class="table is-fullwidth">
<thead><tr><th>Milter</th><th>State</th></tr></thead>
<tbody>
<tr><td>opendkim<br><span class="sp-muted sp-mono is-size-7">inet:127.0.0.1:8891</span></td><td><span class="tag is-success is-light">ok</span></td></tr>
<tr><td>journal-milter<br><span class="sp-muted sp-mono is-size-7">unix:/run/selfpost/journal.sock</span></td><td><span class="tag is-success is-light">ok</span></td></tr>
<tr><td>opendkim<br><span class="sp-muted sp-mono sp-small">inet:127.0.0.1:8891</span></td><td><span class="tag is-success is-light">ok</span></td></tr>
<tr><td>journal-milter<br><span class="sp-muted sp-mono sp-small">unix:/run/selfpost/journal.sock</span></td><td><span class="tag is-success is-light">ok</span></td></tr>
<tr><td>rspamd <span class="sp-muted">· inbound spam filter</span><br><span class="sp-muted sp-mono sp-small">inet:antispam:11332</span></td><td><span class="tag is-success is-light">ok</span></td></tr>
</tbody>
</table>
</table></div>
</div>
</div>
<div class="column is-4">
@@ -19,6 +19,7 @@
<li><a class="is-active" href="#dns"><i class="ti ti-world-check"></i>MX record <span class="tag is-success is-light">ok</span></a></li>
<li><a href="#upstream"><i class="ti ti-arrow-forward-up"></i>Upstream</a></li>
<li><a href="#recipients"><i class="ti ti-address-book"></i>Recipients <span class="tag is-light">4</span></a></li>
<li><a href="#filter"><i class="ti ti-shield-check"></i>Spam filter <span class="tag is-success is-light">on</span></a></li>
</ul>
<p class="menu-label">See also</p>
<ul class="menu-list">
@@ -82,5 +83,17 @@ security@lists.example.org</textarea></div>
</div>
</div>
</div>
<div class="box" id="filter">
<div class="sp-box-head"><span class="sp-no">04</span><h2>Spam filter</h2><div class="sp-end"><span class="tag is-success is-light">on</span></div></div>
<div class="sp-box-body">
<dl class="sp-facts">
<div><dt>Engine</dt><dd>rspamd, for every inbound domain of this server</dd></div>
<div><dt>Suspicious mail</dt><dd>Held — <a href="in-quarantine.html">1 message in quarantine</a> for this domain</dd></div>
<div><dt>Spam</dt><dd>Refused at the door; see <em>reject</em> in the <a href="in-log.html">log</a></dd></div>
<div><dt>Allow and deny lists</dt><dd class="sp-muted">Kept by the global administrator, one pair for the whole server</dd></div>
</dl>
</div>
</div>
</div>
</div>
@@ -21,13 +21,13 @@
</div>
<div class="box">
<div class="sp-box-head"><span class="sp-no">02</span><h2>Domains</h2><div class="sp-end">2 domains · DNS cached a few minutes</div></div>
<table class="table is-fullwidth is-hoverable">
<div class="sp-box-head"><span class="sp-no">02</span><h2>Domains</h2><div class="sp-end">Spam filter <span class="tag is-success is-light">on</span> · 2 domains</div></div>
<div class="table-container"><table class="table is-fullwidth is-hoverable">
<thead><tr><th>Domain</th><th>DNS</th><th>Upstream</th><th>Recipients</th><th>TLS to upstream</th><th></th></tr></thead>
<tbody>
<tr><td><a href="in-domain.html"><strong>lists.example.org</strong></a></td><td><span class="tag is-success is-light">MX</span></td><td class="sp-mono sp-muted">mx.internal:25</td><td>4 addresses</td><td><span class="tag is-success is-light">required</span></td><td class="sp-actions"><a href="in-domain.html">Open</a><a class="has-text-danger" href="in-domain-delete.html">Delete</a></td></tr>
<tr><td><a href="in-domain.html"><strong>acme.io</strong></a></td><td><span class="tag is-success is-light">MX</span></td><td class="sp-mono sp-muted">10.0.4.12:2525</td><td>any address</td><td><span class="tag is-light">opportunistic</span></td><td class="sp-actions"><a href="in-domain.html">Open</a><a class="has-text-danger" href="in-domain-delete.html">Delete</a></td></tr>
</tbody>
</table>
</table></div>
<div class="sp-box-foot">The MX tag is green when at least one MX of the domain points at this server.</div>
</div>
@@ -1,7 +1,7 @@
<!-- title: Filter lists | section: inbound | page: filter-lists -->
<div class="sp-head">
<div class="sp-grow">
<p class="sp-kicker">Inbound · global administrator</p>
<p class="sp-kicker">Inbound · global role</p>
<h1 class="title is-3">Filter lists</h1>
<p class="sp-lead">Correct the spam filter by hand: senders it must always let through, and senders it must always turn away. One pair of lists for the whole instance.</p>
</div>
@@ -36,26 +36,26 @@
<div class="column is-6">
<div class="box">
<div class="sp-box-head"><span class="sp-no">02</span><h2>Deny</h2><div class="sp-end">3 entries · wins over Allow</div></div>
<table class="table is-fullwidth is-hoverable">
<div class="table-container"><table class="table is-fullwidth is-hoverable">
<thead><tr><th>Value</th><th>Note</th><th>Added</th><th></th></tr></thead>
<tbody>
<tr><td class="sp-mono is-size-7">@bulk.example <span class="tag is-light">domain</span></td><td class="sp-muted">newsletter nobody subscribed to</td><td class="sp-muted sp-nowrap is-size-7">2026-09-02</td><td class="sp-actions"><a class="has-text-danger" href="#">Remove</a></td></tr>
<tr><td class="sp-mono is-size-7">198.51.100.0/24 <span class="tag is-light">ip</span></td><td class="sp-muted">dictionary attack on lists.example.org</td><td class="sp-muted sp-nowrap is-size-7">2026-08-19</td><td class="sp-actions"><a class="has-text-danger" href="#">Remove</a></td></tr>
<tr><td class="sp-mono is-size-7">win@lottery.example <span class="tag is-light">address</span></td><td class="sp-muted">—</td><td class="sp-muted sp-nowrap is-size-7">2026-07-30</td><td class="sp-actions"><a class="has-text-danger" href="#">Remove</a></td></tr>
<tr><td class="sp-mono sp-small">@bulk.example <span class="tag is-light">domain</span></td><td class="sp-muted">newsletter nobody subscribed to</td><td class="sp-muted sp-nowrap sp-small">2026-09-02</td><td class="sp-actions"><a class="has-text-danger" href="#">Remove</a></td></tr>
<tr><td class="sp-mono sp-small">198.51.100.0/24 <span class="tag is-light">ip</span></td><td class="sp-muted">dictionary attack on lists.example.org</td><td class="sp-muted sp-nowrap sp-small">2026-08-19</td><td class="sp-actions"><a class="has-text-danger" href="#">Remove</a></td></tr>
<tr><td class="sp-mono sp-small">win@lottery.example <span class="tag is-light">address</span></td><td class="sp-muted">—</td><td class="sp-muted sp-nowrap sp-small">2026-07-30</td><td class="sp-actions"><a class="has-text-danger" href="#">Remove</a></td></tr>
</tbody>
</table>
</table></div>
</div>
</div>
<div class="column is-6">
<div class="box">
<div class="sp-box-head"><span class="sp-no">03</span><h2>Allow</h2><div class="sp-end">2 entries</div></div>
<table class="table is-fullwidth is-hoverable">
<div class="table-container"><table class="table is-fullwidth is-hoverable">
<thead><tr><th>Value</th><th>Note</th><th>Added</th><th></th></tr></thead>
<tbody>
<tr><td class="sp-mono is-size-7">@partner.example <span class="tag is-light">domain</span></td><td class="sp-muted">invoices were scored as spam</td><td class="sp-muted sp-nowrap is-size-7">2026-09-11</td><td class="sp-actions"><a class="has-text-danger" href="#">Remove</a></td></tr>
<tr><td class="sp-mono is-size-7">noreply@github.example <span class="tag is-light">address</span></td><td class="sp-muted">security advisories</td><td class="sp-muted sp-nowrap is-size-7">2026-06-04</td><td class="sp-actions"><a class="has-text-danger" href="#">Remove</a></td></tr>
<tr><td class="sp-mono sp-small">@partner.example <span class="tag is-light">domain</span></td><td class="sp-muted">invoices were scored as spam</td><td class="sp-muted sp-nowrap sp-small">2026-09-11</td><td class="sp-actions"><a class="has-text-danger" href="#">Remove</a></td></tr>
<tr><td class="sp-mono sp-small">noreply@github.example <span class="tag is-light">address</span></td><td class="sp-muted">security advisories</td><td class="sp-muted sp-nowrap sp-small">2026-06-04</td><td class="sp-actions"><a class="has-text-danger" href="#">Remove</a></td></tr>
</tbody>
</table>
</table></div>
</div>
</div>
</div>
@@ -65,7 +65,7 @@
<div class="sp-box-body">
<dl class="sp-facts">
<div><dt>Applied by</dt><dd>rspamd, from map files</dd></div>
<div><dt>Files</dt><dd class="sp-mono is-size-7">/data/antispam/allow.map<br>/data/antispam/deny.map</dd></div>
<div><dt>Files</dt><dd class="sp-mono sp-small">/data/antispam/allow.map<br>/data/antispam/deny.map</dd></div>
<div><dt>Last written</dt><dd>2026-09-11 08:40 UTC</dd></div>
<div><dt>When the filter is off</dt><dd class="sp-muted">The lists are kept and editable, but nothing reads them until <code>INBOUND_ANTISPAM_MILTER</code> is set.</dd></div>
</dl>
@@ -11,23 +11,23 @@
<div class="sp-box-head">
<span class="sp-no">01</span><h2>Messages</h2>
<form class="sp-end" method="get" action="#">
<div class="select is-small"><select name="domain"><option>All domains</option><option>lists.example.org</option><option>acme.io</option></select></div>
<div class="select is-small"><select name="decision"><option>Any decision</option><option>accept</option><option>reject</option><option>tempfail</option><option>quarantine</option></select></div>
<input class="input is-small" type="date" name="from" value="2026-09-14" aria-label="From date">
<input class="input is-small" type="date" name="to" value="2026-09-21" aria-label="To date">
<button type="submit" class="button is-small"><i class="ti ti-filter"></i>Filter</button>
<div class="select"><select name="domain"><option>All domains</option><option>lists.example.org</option><option>acme.io</option></select></div>
<div class="select"><select name="decision"><option>Any decision</option><option>accept</option><option>reject</option><option>tempfail</option><option>quarantine</option></select></div>
<input class="input" type="date" name="from" value="2026-09-14" aria-label="From date">
<input class="input" type="date" name="to" value="2026-09-21" aria-label="To date">
<button type="submit" class="button"><i class="ti ti-filter"></i>Filter</button>
</form>
</div>
<table class="table is-fullwidth is-hoverable">
<thead><tr><th>Time, UTC</th><th>Client</th><th>From</th><th>To</th><th>Subject</th><th>Decision</th><th>Why</th><th></th></tr></thead>
<div class="table-container"><table class="table is-fullwidth is-hoverable">
<thead><tr><th>Time, UTC</th><th>From · client</th><th>To</th><th>Subject</th><th>Decision</th><th>Why</th><th></th></tr></thead>
<tbody>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-21 14:00:18</td><td class="sp-mono is-size-7">192.0.2.77</td><td>t.berg@uni.example</td><td>dev@lists.example.org</td><td class="sp-clip">Re: [dev] patch series v3</td><td><span class="tag is-success is-light">accept</span></td><td class="sp-muted is-size-7">rspamd · score 0.4</td><td class="sp-actions"><a href="in-filter-lists.html">Deny sender</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-21 13:59:02</td><td class="sp-mono is-size-7">203.0.113.140</td><td>invoice@partner-billing.example</td><td>owner@lists.example.org</td><td class="sp-clip">Outstanding invoice — action required</td><td><span class="tag is-primary is-light">quarantine</span></td><td class="sp-muted is-size-7">rspamd · score 9.8 · PHISHING, FORGED_SENDER</td><td class="sp-actions"><a href="in-quarantine-message.html">Review</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-21 13:57:41</td><td class="sp-mono is-size-7">198.51.100.90</td><td>promo@bulk.example</td><td>sales@lists.example.org</td><td class="sp-clip sp-muted">—</td><td><span class="tag is-danger is-light">reject</span></td><td class="sp-muted is-size-7">postfix · 550 5.1.1 unknown recipient</td><td class="sp-actions"><a href="in-filter-lists.html">Deny sender</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-21 13:51:30</td><td class="sp-mono is-size-7">198.51.100.12</td><td>billing@partner.example</td><td>owner@lists.example.org</td><td class="sp-clip">Invoice 2026-0917</td><td><span class="tag is-danger is-light">reject</span></td><td class="sp-muted is-size-7">rspamd · score 16.2 · BAYES_SPAM</td><td class="sp-actions"><a href="in-filter-lists.html">Allow sender</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-21 13:44:02</td><td class="sp-mono is-size-7">192.0.2.31</td><td>hr@partner.example</td><td>jobs@acme.io</td><td class="sp-clip">Application: backend engineer</td><td><span class="tag is-warning is-light">tempfail</span></td><td class="sp-muted is-size-7">selfpost · upstream 10.0.4.12 timed out</td><td class="sp-actions"></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-21 13:30:55</td><td class="sp-mono is-size-7">192.0.2.200</td><td>noreply@github.example</td><td>security@lists.example.org</td><td class="sp-clip">[advisory] new report</td><td><span class="tag is-success is-light">accept</span></td><td class="sp-muted is-size-7">rspamd · allow list</td><td class="sp-actions"></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 14:00:18</td><td>t.berg@<wbr>uni.example<br><span class="sp-mono sp-muted sp-small">192.0.2.77</span></td><td>dev@<wbr>lists.example.org</td><td class="sp-clip">Re: [dev] patch series v3</td><td><span class="tag is-success is-light">accept</span></td><td class="sp-muted sp-small">rspamd · score 0.4</td><td class="sp-actions"><a href="in-filter-lists.html">Deny sender</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:59:02</td><td>invoice@<wbr>partner-billing.example<br><span class="sp-mono sp-muted sp-small">203.0.113.140</span></td><td>owner@<wbr>lists.example.org</td><td class="sp-clip">Outstanding invoice — action required</td><td><span class="tag is-primary is-light">quarantine</span></td><td class="sp-muted sp-small">rspamd · score 9.8 · PHISHING, FORGED_SENDER</td><td class="sp-actions"><a href="in-quarantine-message.html">Review</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:57:41</td><td>promo@<wbr>bulk.example<br><span class="sp-mono sp-muted sp-small">198.51.100.90</span></td><td>sales@<wbr>lists.example.org</td><td class="sp-clip sp-muted">—</td><td><span class="tag is-danger is-light">reject</span></td><td class="sp-muted sp-small">postfix · 550 5.1.1 unknown recipient</td><td class="sp-actions"><a href="in-filter-lists.html">Deny sender</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:51:30</td><td>billing@<wbr>partner.example<br><span class="sp-mono sp-muted sp-small">198.51.100.12</span></td><td>owner@<wbr>lists.example.org</td><td class="sp-clip">Invoice 2026-0917</td><td><span class="tag is-danger is-light">reject</span></td><td class="sp-muted sp-small">rspamd · score 16.2 · BAYES_SPAM</td><td class="sp-actions"><a href="in-filter-lists.html">Allow sender</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:44:02</td><td>hr@<wbr>partner.example<br><span class="sp-mono sp-muted sp-small">192.0.2.31</span></td><td>jobs@<wbr>acme.io</td><td class="sp-clip">Application: backend engineer</td><td><span class="tag is-warning is-light">tempfail</span></td><td class="sp-muted sp-small">selfpost · upstream 10.0.4.12 timed out</td><td class="sp-actions"></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:30:55</td><td>noreply@<wbr>github.example<br><span class="sp-mono sp-muted sp-small">192.0.2.200</span></td><td>security@<wbr>lists.example.org</td><td class="sp-clip">[advisory] new report</td><td><span class="tag is-success is-light">accept</span></td><td class="sp-muted sp-small">rspamd · allow list</td><td class="sp-actions"></td></tr>
</tbody>
</table>
</table></div>
<div class="sp-box-foot"><span>Page 1 of 38</span><a href="#">Older →</a><span class="sp-end">Allow / Deny sender opens <a href="in-filter-lists.html">Filter lists</a> with the entry filled in · global administrator only</span></div>
</div>
@@ -26,14 +26,14 @@
<div class="column is-5">
<div class="box">
<div class="sp-box-head"><span class="sp-no">02</span><h2>Why it was held</h2><div class="sp-end">rspamd · score 9.8</div></div>
<table class="table is-fullwidth">
<div class="table-container"><table class="table is-fullwidth">
<thead><tr><th>Symbol</th><th>Score</th></tr></thead>
<tbody>
<tr><td class="sp-mono is-size-7">PHISHING</td><td>5.0</td></tr>
<tr><td class="sp-mono is-size-7">FORGED_SENDER</td><td>3.0</td></tr>
<tr><td class="sp-mono is-size-7">HTML_ATTACHMENT</td><td>1.8</td></tr>
<tr><td class="sp-mono sp-small">PHISHING</td><td>5.0</td></tr>
<tr><td class="sp-mono sp-small">FORGED_SENDER</td><td>3.0</td></tr>
<tr><td class="sp-mono sp-small">HTML_ATTACHMENT</td><td>1.8</td></tr>
</tbody>
</table>
</table></div>
</div>
</div>
</div>
@@ -11,17 +11,17 @@
<div class="sp-box-head">
<span class="sp-no">01</span><h2>Held messages</h2>
<form class="sp-end" method="get" action="#">
<div class="select is-small"><select name="domain"><option>All domains</option><option>lists.example.org</option><option>acme.io</option></select></div>
<button type="submit" class="button is-small"><i class="ti ti-filter"></i>Filter</button>
<div class="select"><select name="domain"><option>All domains</option><option>lists.example.org</option><option>acme.io</option></select></div>
<button type="submit" class="button"><i class="ti ti-filter"></i>Filter</button>
</form>
</div>
<table class="table is-fullwidth is-hoverable">
<div class="table-container"><table class="table is-fullwidth is-hoverable">
<thead><tr><th>Held, UTC</th><th>From</th><th>To</th><th>Subject</th><th>Why</th><th>Size</th><th>Deleted in</th><th></th></tr></thead>
<tbody>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-21 13:59:02</td><td>invoice@partner-billing.example</td><td>owner@lists.example.org</td><td class="sp-clip"><a href="in-quarantine-message.html"><strong>Outstanding invoice — action required</strong></a></td><td class="sp-muted is-size-7">score 9.8 · PHISHING</td><td class="sp-muted sp-nowrap">48 kB</td><td class="sp-nowrap">14 days</td><td class="sp-actions"><a href="in-quarantine-message.html">Review</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-20 22:14:40</td><td>no-reply@shipping-update.example</td><td>jobs@acme.io</td><td class="sp-clip"><a href="in-quarantine-message.html"><strong>Your parcel could not be delivered</strong></a></td><td class="sp-muted is-size-7">score 8.1 · URL_SHORTENER</td><td class="sp-muted sp-nowrap">12 kB</td><td class="sp-nowrap">13 days</td><td class="sp-actions"><a href="in-quarantine-message.html">Review</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-09 07:02:11</td><td>m.ortega@supplier.example</td><td>owner@lists.example.org</td><td class="sp-clip"><a href="in-quarantine-message.html"><strong>Fwd: scanned contract (3 attachments)</strong></a></td><td class="sp-muted is-size-7">score 7.4 · MIME_ARCHIVE</td><td class="sp-muted sp-nowrap">2.9 MB</td><td class="sp-nowrap"><span class="tag is-warning is-light">2 days</span></td><td class="sp-actions"><a href="in-quarantine-message.html">Review</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:59:02</td><td>invoice@<wbr>partner-billing.example</td><td>owner@<wbr>lists.example.org</td><td class="sp-clip"><a href="in-quarantine-message.html"><strong>Outstanding invoice — action required</strong></a></td><td class="sp-muted sp-small">score 9.8 · PHISHING</td><td class="sp-muted sp-nowrap">48 kB</td><td class="sp-nowrap">14 days</td><td class="sp-actions"><a href="in-quarantine-message.html">Review</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-20 22:14:40</td><td>no-reply@<wbr>shipping-update.example</td><td>jobs@<wbr>acme.io</td><td class="sp-clip"><a href="in-quarantine-message.html"><strong>Your parcel could not be delivered</strong></a></td><td class="sp-muted sp-small">score 8.1 · URL_SHORTENER</td><td class="sp-muted sp-nowrap">12 kB</td><td class="sp-nowrap">13 days</td><td class="sp-actions"><a href="in-quarantine-message.html">Review</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-09 07:02:11</td><td>m.ortega@<wbr>supplier.example</td><td>owner@<wbr>lists.example.org</td><td class="sp-clip"><a href="in-quarantine-message.html"><strong>Fwd: scanned contract (3 attachments)</strong></a></td><td class="sp-muted sp-small">score 7.4 · MIME_ARCHIVE</td><td class="sp-muted sp-nowrap">2.9 MB</td><td class="sp-nowrap"><span class="tag is-warning is-light">2 days</span></td><td class="sp-actions"><a href="in-quarantine-message.html">Review</a></td></tr>
</tbody>
</table>
</table></div>
<div class="sp-box-foot"><span>3 messages · 3.0 MB of 500 MB</span><span class="sp-end">A domain administrator sees only their assigned inbound domains</span></div>
</div>
@@ -15,12 +15,13 @@
<form class="sp-form" method="post" action="#">
<div class="field"><label class="label" for="rua">Aggregate reports (rua=)</label>
<div class="control"><div class="select is-fullwidth"><select id="rua" name="dmarc_rua_mode">
<option selected>Same as Settings (dmarc@mail.example.org)</option>
<option selected>admin's default — mix@example.org</option>
<option>SelfPost hosted (example.org@dmarc.mail.example.org)</option>
<option>My default instead — (you are admin)</option>
<option>No aggregate reports</option>
<option>Custom address</option>
</select></div></div>
<p class="help">The default comes from <a href="settings.html">Settings</a>. Changing it changes the DMARC record to publish.</p></div>
<p class="help">A default belongs to a user and is set under their <a href="account.html#dmarc">Account</a>; 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.</p></div>
<button type="submit" class="button is-primary">Save report address</button>
</form>
</div>
@@ -66,7 +66,7 @@
</div>
<div class="sp-record">
<div class="sp-record-head"><h3>DMARC</h3><span class="tag is-success is-light">published</span><small>Report address <a href="out-domain-settings.html#reports">from Settings</a></small></div>
<div class="sp-record-head"><h3>DMARC</h3><span class="tag is-success is-light">published</span><small>Report address: <a href="out-domain-settings.html#reports">admin's default</a></small></div>
<div class="sp-row">
<div class="field"><label class="label">Host / name</label>
<div class="field has-addons"><div class="control is-expanded"><input class="input" readonly value="_dmarc.example.org"></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
@@ -80,7 +80,7 @@
<div class="box" id="apps">
<div class="sp-box-head"><span class="sp-no">02</span><h2>Applications</h2><div class="sp-end">SASL logins that may send as this domain</div><a class="sp-help" href="help.html#apps" title="What an application is"><i class="ti ti-help-circle"></i></a></div>
<table class="table is-fullwidth is-hoverable">
<div class="table-container"><table class="table is-fullwidth is-hoverable">
<thead><tr><th>Login</th><th>May send as</th><th>30 days</th><th>Limits</th><th></th></tr></thead>
<tbody>
<tr><td class="sp-mono"><strong>prod-server</strong></td><td>*@example.org</td><td class="sp-muted">1 102 msg · peak 96/h</td><td><span class="tag is-success is-light">200 / h</span></td>
@@ -88,7 +88,7 @@
<tr><td class="sp-mono"><strong>alerts</strong></td><td>alerts@example.org, noc@example.org</td><td class="sp-muted">182 msg · peak 14/h</td><td><span class="tag is-light">domain</span> <span class="tag is-success is-light">2 IPs</span></td>
<td class="sp-actions"><a href="out-app.html">Edit</a><a href="out-app-created.html">New password</a><a class="has-text-danger" href="#">Delete</a></td></tr>
</tbody>
</table>
</table></div>
<div class="sp-box-foot">New password and Delete ask for confirmation; the old password stops working at once.</div>
</div>
@@ -23,13 +23,13 @@
<div class="box">
<div class="sp-box-head"><span class="sp-no">02</span><h2>Domains</h2><div class="sp-end">3 domains · DNS cached a few minutes</div></div>
<table class="table is-fullwidth is-hoverable">
<div class="table-container"><table class="table is-fullwidth is-hoverable">
<thead><tr><th>Domain</th><th>DNS</th><th>Selector</th><th>Apps</th><th>30 days</th><th></th></tr></thead>
<tbody>
<tr><td><a href="out-domain.html"><strong>example.org</strong></a></td><td><span class="tag is-success is-light">DKIM</span> <span class="tag is-danger is-light">SPF</span> <span class="tag is-success is-light">DMARC</span></td><td class="sp-muted sp-mono">sp2026</td><td>2</td><td class="sp-muted">1 284 msg · peak 96/h</td><td class="sp-actions"><a href="out-domain.html">Open</a><a class="has-text-danger" href="out-domain-delete.html">Delete</a></td></tr>
<tr><td><a href="out-domain.html"><strong>shop.example.org</strong></a></td><td><span class="tag is-success is-light">DKIM</span> <span class="tag is-success is-light">SPF</span> <span class="tag is-success is-light">DMARC</span></td><td class="sp-muted sp-mono">sp2026</td><td>1</td><td class="sp-muted">18 920 msg · peak 410/h</td><td class="sp-actions"><a href="out-domain.html">Open</a><a class="has-text-danger" href="out-domain-delete.html">Delete</a></td></tr>
<tr><td><a href="out-domain.html"><strong>notify.acme.io</strong></a></td><td><span class="tag is-success is-light">DKIM</span> <span class="tag is-success is-light">SPF</span> <span class="tag is-warning is-light">DMARC</span></td><td class="sp-muted sp-mono">sp2025</td><td>3</td><td class="sp-muted">402 msg · peak 22/h</td><td class="sp-actions"><a href="out-domain.html">Open</a><a class="has-text-danger" href="out-domain-delete.html">Delete</a></td></tr>
</tbody>
</table>
</table></div>
<div class="sp-box-foot">Each tag is one check. Green — published and correct; amber — published but weak; red — missing or wrong.</div>
</div>
@@ -11,22 +11,22 @@
<div class="sp-box-head">
<span class="sp-no">01</span><h2>Messages</h2>
<form class="sp-end" method="get" action="#">
<div class="select is-small"><select name="domain"><option>All domains</option><option>example.org</option><option>shop.example.org</option><option>notify.acme.io</option></select></div>
<div class="select is-small"><select name="app"><option>All applications</option><option>prod-server</option><option>alerts</option></select></div>
<div class="select is-small"><select name="status"><option>Any status</option><option>sent</option><option>deferred</option><option>bounced</option><option>rate-limited</option></select></div>
<button type="submit" class="button is-small"><i class="ti ti-filter"></i>Filter</button>
<div class="select"><select name="domain"><option>All domains</option><option>example.org</option><option>shop.example.org</option><option>notify.acme.io</option></select></div>
<div class="select"><select name="app"><option>All applications</option><option>prod-server</option><option>alerts</option></select></div>
<div class="select"><select name="status"><option>Any status</option><option>sent</option><option>deferred</option><option>bounced</option><option>rate-limited</option></select></div>
<button type="submit" class="button"><i class="ti ti-filter"></i>Filter</button>
</form>
</div>
<table class="table is-fullwidth is-hoverable">
<div class="table-container"><table class="table is-fullwidth is-hoverable">
<thead><tr><th>Time, UTC</th><th>From</th><th>To</th><th>Subject</th><th>Status</th><th></th></tr></thead>
<tbody>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-21 14:01:52</td><td>shop@shop.example.org</td><td>m.keller@gmx.de</td><td class="sp-clip">Your order #48213 has shipped</td><td><span class="tag is-success is-light">sent</span></td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-21 14:01:50</td><td>shop@shop.example.org</td><td>anna.lind@outlook.com</td><td class="sp-clip">Your order #48212 has shipped</td><td><span class="tag is-success is-light">sent</span></td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-21 13:58:07</td><td>alerts@example.org</td><td>noc@example.org</td><td class="sp-clip">[FIRING] disk usage above 90 % on db-2</td><td><span class="tag is-warning is-light">deferred</span></td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-21 13:41:19</td><td>no-reply@notify.acme.io</td><td>j.doe@nonexistent.example</td><td class="sp-clip">Reset your password</td><td><span class="tag is-danger is-light">bounced</span></td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-21 13:40:02</td><td>prod@example.org</td><td>billing@partner.example</td><td class="sp-clip">Invoice 2026-0917</td><td><span class="tag is-light">rate-limited</span></td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap is-size-7">09-21 13:12:44</td><td>shop@shop.example.org</td><td>p.novak@seznam.cz</td><td class="sp-clip">We received your return request</td><td><span class="tag is-success is-light">sent</span></td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 14:01:52</td><td>shop@<wbr>shop.example.org</td><td>m.keller@<wbr>gmx.de</td><td class="sp-clip">Your order #48213 has shipped</td><td><span class="tag is-success is-light">sent</span></td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 14:01:50</td><td>shop@<wbr>shop.example.org</td><td>anna.lind@<wbr>outlook.com</td><td class="sp-clip">Your order #48212 has shipped</td><td><span class="tag is-success is-light">sent</span></td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:58:07</td><td>alerts@<wbr>example.org</td><td>noc@<wbr>example.org</td><td class="sp-clip">[FIRING] disk usage above 90 % on db-2</td><td><span class="tag is-warning is-light">deferred</span></td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:41:19</td><td>no-reply@<wbr>notify.acme.io</td><td>j.doe@<wbr>nonexistent.example</td><td class="sp-clip">Reset your password</td><td><span class="tag is-danger is-light">bounced</span></td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:40:02</td><td>prod@<wbr>example.org</td><td>billing@<wbr>partner.example</td><td class="sp-clip">Invoice 2026-0917</td><td><span class="tag is-light">rate-limited</span></td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
<tr><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:12:44</td><td>shop@<wbr>shop.example.org</td><td>p.novak@<wbr>seznam.cz</td><td class="sp-clip">We received your return request</td><td><span class="tag is-success is-light">sent</span></td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
</tbody>
</table>
</table></div>
<div class="sp-box-foot"><span>Page 1 of 412</span><a href="#">Older →</a><span class="sp-end">New rows appear on their own</span></div>
</div>
@@ -9,12 +9,12 @@
<div class="box">
<div class="sp-box-head"><span class="sp-no">01</span><h2>Waiting</h2><div class="sp-end">1 message · 3.1 kB</div></div>
<table class="table is-fullwidth is-hoverable">
<div class="table-container"><table class="table is-fullwidth is-hoverable">
<thead><tr><th>Queue id</th><th>Arrived, UTC</th><th>From</th><th>To</th><th>Last reply</th><th></th></tr></thead>
<tbody>
<tr><td class="sp-mono">4XcB7k2Jm9z1</td><td class="sp-mono sp-muted sp-nowrap is-size-7">09-21 13:58:07</td><td>alerts@example.org</td><td>noc@example.org</td><td class="sp-muted">451 4.7.1 Greylisted, try again in 5 minutes</td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
<tr><td class="sp-mono">4XcB7k2Jm9z1</td><td class="sp-mono sp-muted sp-nowrap sp-small">09-21 13:58:07</td><td>alerts@<wbr>example.org</td><td>noc@<wbr>example.org</td><td class="sp-muted">451 4.7.1 Greylisted, try again in 5 minutes</td><td class="sp-actions"><a href="out-message.html">Details</a></td></tr>
</tbody>
</table>
</table></div>
</div>
<div class="box">
@@ -6,18 +6,18 @@
<h1 class="title is-3">Mail is flowing. One thing needs attention.</h1>
<p class="sp-lead">The TLS certificate expires in 12 days and has not renewed yet.</p>
</div>
<div class="sp-muted is-size-7">Updated 14:02:31 UTC<br>refreshes every 10 s</div>
<div class="sp-muted sp-small">Updated 14:02:31 UTC<br>refreshes every 10 s</div>
</div>
<div class="box">
<div class="sp-box-head"><span class="sp-no">01</span><h2>Server health</h2><div class="sp-end"><a href="health.html">All details</a></div><a class="sp-help" href="help.html#checks" title="What these checks mean"><i class="ti ti-help-circle"></i></a></div>
<div class="sp-health">
<a href="health.html#machine"><span class="sp-h-icon"><i class="ti ti-cpu"></i></span><span><span class="sp-h-name">Machine</span><br><span class="sp-h-value">CPU 12 % · RAM 41 %</span><br><span class="sp-h-sub">↓ 18 kB/s · ↑ 44 kB/s</span></span></a>
<a href="health.html#processes"><span class="sp-h-icon"><i class="ti ti-server-cog"></i></span><span><span class="sp-h-name">Processes</span><br><span class="sp-h-value">5 of 5 running</span><br><span class="sp-h-sub">up 18 days</span></span></a>
<a href="health.html#certificate" class="sp-warn"><span class="sp-h-icon"><i class="ti ti-certificate"></i></span><span><span class="sp-h-name">TLS certificate</span><br><span class="sp-h-value">Expires in 12 days</span><br><span class="sp-h-sub">renewal has not run</span></span></a>
<a href="out-queue.html"><span class="sp-h-icon"><i class="ti ti-stack-2"></i></span><span><span class="sp-h-name">Queues</span><br><span class="sp-h-value">Both empty</span><br><span class="sp-h-sub">outbound and inbound</span></span></a>
<a href="health.html#sockets"><span class="sp-h-icon"><i class="ti ti-plug-connected"></i></span><span><span class="sp-h-name">Milter sockets</span><br><span class="sp-h-value">2 of 2 answering</span><br><span class="sp-h-sub">opendkim, journal</span></span></a>
<a href="health.html#hostname"><span class="sp-h-icon"><i class="ti ti-arrows-exchange"></i></span><span><span class="sp-h-name">Reverse DNS</span><br><span class="sp-h-value">Forward = reverse</span><br><span class="sp-h-sub">203.0.113.25</span></span></a>
<a href="health.html#machine"><span class="sp-h-icon"><i class="ti ti-cpu"></i></span><span><span class="sp-h-name">Machine</span><span class="sp-h-value">CPU 12 %</span><span class="sp-h-sub">RAM 41 % · 62 kB/s</span></span></a>
<a href="health.html#processes"><span class="sp-h-icon"><i class="ti ti-server-cog"></i></span><span><span class="sp-h-name">Processes</span><span class="sp-h-value">5 of 5 running</span><span class="sp-h-sub">up 18 days</span></span></a>
<a href="health.html#certificate" class="sp-warn"><span class="sp-h-icon"><i class="ti ti-certificate"></i></span><span><span class="sp-h-name">TLS certificate</span><span class="sp-h-value">Expires in 12 days</span><span class="sp-h-sub">renewal has not run</span></span></a>
<a href="out-queue.html"><span class="sp-h-icon"><i class="ti ti-stack-2"></i></span><span><span class="sp-h-name">Queues</span><span class="sp-h-value">Both empty</span><span class="sp-h-sub">outbound and inbound</span></span></a>
<a href="health.html#sockets"><span class="sp-h-icon"><i class="ti ti-plug-connected"></i></span><span><span class="sp-h-name">Milter sockets</span><span class="sp-h-value">3 of 3 answering</span><span class="sp-h-sub">incl. the spam filter</span></span></a>
<a href="health.html#hostname"><span class="sp-h-icon"><i class="ti ti-arrows-exchange"></i></span><span><span class="sp-h-name">Reverse DNS</span><span class="sp-h-value">Forward = reverse</span><span class="sp-h-sub">203.0.113.25</span></span></a>
</div>
</div>
@@ -25,26 +25,26 @@
<div class="column is-7">
<div class="box">
<div class="sp-box-head"><span class="sp-no">02</span><h2>Outbound domains</h2><div class="sp-end"><a href="out-domains.html">Manage</a></div></div>
<table class="table is-fullwidth is-hoverable">
<div class="table-container"><table class="table is-fullwidth is-hoverable">
<thead><tr><th>Domain</th><th>DNS</th><th>Apps</th><th>30 days</th></tr></thead>
<tbody>
<tr><td><a href="out-domain.html"><strong>example.org</strong></a></td><td><span class="tag is-success is-light">DKIM</span> <span class="tag is-danger is-light">SPF</span> <span class="tag is-success is-light">DMARC</span></td><td>2</td><td class="sp-muted">1 284 msg</td></tr>
<tr><td><a href="out-domain.html"><strong>shop.example.org</strong></a></td><td><span class="tag is-success is-light">DKIM</span> <span class="tag is-success is-light">SPF</span> <span class="tag is-success is-light">DMARC</span></td><td>1</td><td class="sp-muted">18 920 msg</td></tr>
<tr><td><a href="out-domain.html"><strong>notify.acme.io</strong></a></td><td><span class="tag is-success is-light">DKIM</span> <span class="tag is-success is-light">SPF</span> <span class="tag is-warning is-light">DMARC</span></td><td>3</td><td class="sp-muted">402 msg</td></tr>
</tbody>
</table>
</table></div>
</div>
</div>
<div class="column is-5">
<div class="box">
<div class="sp-box-head"><span class="sp-no">03</span><h2>Inbound domains</h2><div class="sp-end"><a href="in-domains.html">Manage</a></div></div>
<table class="table is-fullwidth is-hoverable">
<div class="table-container"><table class="table is-fullwidth is-hoverable">
<thead><tr><th>Domain</th><th>MX</th><th>Upstream</th></tr></thead>
<tbody>
<tr><td><a href="in-domain.html"><strong>lists.example.org</strong></a></td><td><span class="tag is-success is-light">MX</span></td><td class="sp-muted sp-mono is-size-7">mx.internal:25</td></tr>
<tr><td><a href="in-domain.html"><strong>acme.io</strong></a></td><td><span class="tag is-success is-light">MX</span></td><td class="sp-muted sp-mono is-size-7">10.0.4.12:2525</td></tr>
<tr><td><a href="in-domain.html"><strong>lists.example.org</strong></a></td><td><span class="tag is-success is-light">MX</span></td><td class="sp-muted sp-mono sp-small">mx.internal:25</td></tr>
<tr><td><a href="in-domain.html"><strong>acme.io</strong></a></td><td><span class="tag is-success is-light">MX</span></td><td class="sp-muted sp-mono sp-small">10.0.4.12:2525</td></tr>
</tbody>
</table>
</table></div>
</div>
</div>
</div>
@@ -11,43 +11,41 @@
<div class="box">
<div class="sp-box-head"><span class="sp-no">01</span><h2>Checks</h2><div class="sp-end">Ran 14:02 UTC · 3.1 s</div></div>
<table class="table is-fullwidth">
<div class="table-container"><table class="table is-fullwidth">
<thead><tr><th>Check</th><th>Result</th><th>Found</th><th>What to do</th></tr></thead>
<tbody>
<tr><td><strong>Reverse DNS</strong></td><td><span class="tag is-success is-light">ok</span></td><td class="sp-mono is-size-7">203.0.113.25 ⇄ mail.example.org</td><td class="sp-muted">—</td></tr>
<tr><td><strong>TLS certificate</strong></td><td><span class="tag is-warning is-light">warn</span></td><td class="is-size-7">Valid for <span class="sp-mono">mail.example.org</span>, expires in 12 days</td><td>Renewal has not run. Check that port 80 reaches the ACME client and look for <code>acme</code> in the <a href="system-log.html">system log</a>.</td></tr>
<tr><td><strong>Port 25</strong></td><td><span class="tag is-success is-light">ok</span></td><td class="sp-mono is-size-7">220 mail.example.org ESMTP</td><td class="sp-muted">—</td></tr>
<tr><td><strong>Ports 465, 587</strong></td><td><span class="tag is-success is-light">ok</span></td><td class="sp-mono is-size-7">TLS handshake, 220 banner on both</td><td class="sp-muted">—</td></tr>
<tr><td><strong>HELO name</strong></td><td><span class="tag is-success is-light">ok</span></td><td class="sp-mono is-size-7">mail.example.org</td><td class="sp-muted">—</td></tr>
<tr><td><strong>Proxy headers</strong></td><td><span class="tag is-success is-light">ok</span></td><td class="is-size-7">Peer <span class="sp-mono">172.18.0.2</span> is trusted; <span class="sp-mono">X-Forwarded-Proto: https</span></td><td class="sp-muted">—</td></tr>
<tr><td><strong>DKIM signing</strong></td><td><span class="tag is-success is-light">ok</span></td><td class="sp-mono is-size-7">inet:127.0.0.1:8891 answers</td><td class="sp-muted">—</td></tr>
<tr><td class="sp-nowrap"><strong>Reverse DNS</strong></td><td><span class="tag is-success is-light">ok</span></td><td class="sp-mono sp-small">203.0.113.25 ⇄ mail.example.org</td><td class="sp-muted">—</td></tr>
<tr><td class="sp-nowrap"><strong>TLS certificate</strong></td><td><span class="tag is-warning is-light">warn</span></td><td class="sp-small">Valid for <span class="sp-mono">mail.example.org</span>, expires in 12 days</td><td>Renewal has not run. Check that port 80 reaches the ACME client and look for <code>acme</code> in the <a href="system-log.html">system log</a>.</td></tr>
<tr><td class="sp-nowrap"><strong>Port 25</strong></td><td><span class="tag is-success is-light">ok</span></td><td class="sp-mono sp-small">220 mail.example.org ESMTP</td><td class="sp-muted">—</td></tr>
<tr><td class="sp-nowrap"><strong>Ports 465, 587</strong></td><td><span class="tag is-success is-light">ok</span></td><td class="sp-mono sp-small">TLS handshake, 220 banner on both</td><td class="sp-muted">—</td></tr>
<tr><td class="sp-nowrap"><strong>HELO name</strong></td><td><span class="tag is-success is-light">ok</span></td><td class="sp-mono sp-small">mail.example.org</td><td class="sp-muted">—</td></tr>
<tr><td class="sp-nowrap"><strong>Proxy headers</strong></td><td><span class="tag is-success is-light">ok</span></td><td class="sp-small">Peer <span class="sp-mono">172.18.0.2</span> is trusted; <span class="sp-mono">X-Forwarded-Proto: https</span></td><td class="sp-muted">—</td></tr>
<tr><td class="sp-nowrap"><strong>DKIM signing</strong></td><td><span class="tag is-success is-light">ok</span></td><td class="sp-mono sp-small">inet:127.0.0.1:8891 answers</td><td class="sp-muted">—</td></tr>
</tbody>
</table>
<div class="sp-box-foot">Ports are dialled from inside the container at the public address; a firewall that treats inside and outside differently can make this pass while the world is still blocked.</div>
</table></div>
<div class="sp-box-foot"><span>Ports are dialled from inside the container at the public address; a firewall that treats inside and outside differently can make this pass while the world is still blocked. SPF, DKIM and DMARC are per domain — each <a href=\"out-domains.html\">outbound domain</a> shows its own.</span></div>
</div>
<div class="columns">
<div class="column is-6">
<div class="box">
<div class="sp-box-head"><span class="sp-no">02</span><h2>Send a test e-mail</h2></div>
<div class="sp-box-body">
<div class="box">
<div class="sp-box-head"><span class="sp-no">02</span><h2>Send a test e-mail</h2><div class="sp-end">Confirms the relay accepted it, not that it arrived</div></div>
<div class="sp-box-body">
<div class="columns">
<div class="column is-6">
<form class="sp-form" method="post" action="#">
<div class="field"><label class="label" for="rcpt">Recipient</label>
<div class="field has-addons">
<div class="control is-expanded has-icons-left"><input class="input" id="rcpt" name="recipient" type="email" placeholder="you@elsewhere.example" required><span class="icon is-left"><i class="ti ti-mail"></i></span></div>
<div class="control"><button type="submit" class="button is-primary"><i class="ti ti-send"></i>Send</button></div>
</div>
<p class="help">Sent from <span class="sp-mono">postmaster@mail.example.org</span> through the local Postfix. This confirms the relay accepted it — follow the delivery in the <a href="out-log.html">outbound log</a>.</p></div>
<p class="help">Sent from <span class="sp-mono">postmaster@mail.example.org</span> through the local Postfix.</p></div>
</form>
</div>
</div>
</div>
<div class="column is-6">
<div class="box">
<div class="sp-box-head"><span class="sp-no">03</span><h2>Not checked here</h2></div>
<div class="sp-box-body">
<p class="mb-2">SPF, DKIM and DMARC records are per domain — each <a href="out-domains.html">outbound domain</a> shows its own.</p>
<p class="sp-muted">Whether a receiver finally accepted the test message is not tracked; only that it left.</p>
<div class="column is-6">
<dl class="sp-facts">
<div><dt>Last test</dt><dd>14:03 UTC to <span class="sp-mono">mix@elsewhere.example</span></dd></div>
<div><dt>Result</dt><dd><span class="tag is-success is-light">queued</span> <span class="sp-mono sp-small">4XcC1d8Pq2z1</span></dd></div>
<div><dt>Then</dt><dd><a href="out-message.html">Follow it in the outbound log</a></dd></div>
</dl>
</div>
</div>
</div>
@@ -3,36 +3,15 @@
<div class="sp-grow">
<p class="sp-kicker">Server</p>
<h1 class="title is-3">Settings</h1>
<p class="sp-lead">Defaults for the whole instance. Your own username and password are under <a href="account.html">Account</a>.</p>
<p class="sp-lead">What holds for the whole instance, whoever is signed in. Your own name, e-mail, notifications and DMARC report address are under <a href="account.html">Account</a>.</p>
</div>
</div>
<form class="sp-form" method="post" action="#">
<div class="columns">
<div class="column is-7">
<div class="column is-6">
<div class="box">
<div class="sp-box-head"><span class="sp-no">01</span><h2>DMARC aggregate reports</h2><div class="sp-end"><span class="tag is-success is-light">authorized</span></div></div>
<div class="sp-record">
<div class="field"><label class="label" for="rua">Default report address</label>
<div class="control has-icons-left"><input class="input" id="rua" name="dmarc_report_email" type="email" value="dmarc@mail.example.org"><span class="icon is-left"><i class="ti ti-mail"></i></span></div>
<p class="help">The default <code>rua=</code> for every outbound domain; a domain may override it. An address on <code>mail.example.org</code> is received and parsed by SelfPost itself.</p></div>
</div>
<div class="sp-record">
<div class="sp-record-head"><h3>Report authorization</h3><span class="tag is-success is-light">published</span><small>Needed when <code>rua=</code> points at another domain</small></div>
<div class="sp-row">
<div class="field"><label class="label">Host / name</label>
<div class="field has-addons"><div class="control is-expanded"><input class="input" readonly value="*._report._dmarc.mail.example.org"></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
<div class="field"><label class="label">Type</label><div class="control"><input class="input" readonly value="TXT"></div></div>
</div>
<div class="field"><label class="label">Value to publish</label>
<div class="field has-addons"><div class="control is-expanded"><input class="input" readonly value="v=DMARC1"></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
</div>
</div>
</div>
<div class="column is-5">
<div class="box">
<div class="sp-box-head"><span class="sp-no">02</span><h2>Log retention</h2></div>
<div class="sp-box-head"><span class="sp-no">01</span><h2>Log retention</h2></div>
<div class="sp-box-body">
<div class="field"><label class="label" for="ret">Keep outbound log rows, days</label>
<div class="control"><input class="input" id="ret" name="send_log_retention_days" type="number" min="7" max="365" value="30" required></div>
@@ -40,6 +19,16 @@
</div>
</div>
<div class="box">
<div class="sp-box-head"><span class="sp-no">02</span><h2>Notifications</h2></div>
<div class="sp-box-body">
<div class="field"><label class="checkbox"><input type="checkbox" name="notify" checked> Send notification e-mails</label></div>
<p class="help">Sent through this relay from <span class="sp-mono">postmaster@mail.example.org</span>. Each user picks the events under <a href="account.html">Account</a>; a user without an e-mail gets none.</p>
</div>
</div>
</div>
<div class="column is-6">
<div class="box" id="rate-limits">
<div class="sp-box-head"><span class="sp-no">03</span><h2>Sending rate limits</h2><div class="sp-end">Read-only</div></div>
<div class="sp-box-body">
@@ -14,7 +14,7 @@
<p class="mb-3">Deleting the panel user <strong>acme-ops</strong> will:</p>
<ul class="sp-list-check">
<li><i class="ti ti-x"></i>end any signed-in session of this user at once;</li>
<li><i class="ti ti-x"></i>remove the assignments to <strong>notify.acme.io</strong> (outbound) and <strong>acme.io</strong> (inbound).</li>
<li><i class="ti ti-x"></i>remove the assignments to <strong>all outbound domains</strong> and to <strong>acme.io</strong> (inbound).</li>
</ul>
<p class="sp-muted mb-4">The domains, their applications and passwords, upstreams and recipients are not affected.</p>
<form method="post" action="users.html">
+12 -7
View File
@@ -12,14 +12,16 @@
<div class="columns">
<div class="column is-6">
<div class="box">
<div class="sp-box-head"><span class="sp-no">01</span><h2>Sign-in</h2></div>
<div class="sp-box-head"><span class="sp-no">01</span><h2>Account</h2></div>
<div class="sp-box-body">
<div class="field"><label class="label" for="username">Username</label><div class="control"><input class="input" id="username" name="username" value="acme-ops" autocomplete="username" spellcheck="false" required></div></div>
<div class="field"><label class="label" for="email">E-mail</label><div class="control"><input class="input" id="email" name="email" type="email" value="ops@acme.io" autocomplete="off"></div>
<p class="help">For notifications and a password-reset link. The user can change it under Account.</p></div>
<div class="field"><label class="label" for="password">Password</label><div class="control"><input class="input" id="password" name="password" type="password" autocomplete="new-password"></div>
<p class="help">Leave empty to keep the current one.</p></div>
<div class="field"><label class="label" for="role">Role</label>
<div class="control"><div class="select is-fullwidth"><select id="role" name="role"><option selected>Domain administrator</option><option>Global administrator</option></select></div></div>
<p class="help">A global administrator sees everything, including Server, and is the only one who adds or deletes domains. The only global administrator cannot be demoted or deleted.</p></div>
<div class="control"><div class="select is-fullwidth"><select id="role" name="role"><option selected>Domain — the domains assigned below</option><option>Global — the whole server</option></select></div></div>
<p class="help">Everyone here is an administrator; the role is the reach. <em>Global</em> also sees Server and is the only one who adds or deletes domains. The only global user cannot be changed to <em>domain</em> or deleted.</p></div>
</div>
</div>
</div>
@@ -27,17 +29,20 @@
<div class="box">
<div class="sp-box-head"><span class="sp-no">02</span><h2>Outbound domains</h2><div class="sp-end">DNS records, applications, log</div></div>
<div class="sp-box-body">
<div class="field"><label class="checkbox"><input type="checkbox" name="domain_ids"> example.org</label></div>
<div class="field"><label class="checkbox"><input type="checkbox" name="domain_ids"> shop.example.org</label></div>
<div class="field"><label class="checkbox"><input type="checkbox" name="domain_ids" checked> notify.acme.io</label></div>
<div class="field"><label class="checkbox"><input type="checkbox" name="all_domains" checked> <strong>All outbound domains</strong>, including ones added later</label></div>
<div class="field"><label class="checkbox"><input type="checkbox" name="domain_ids" checked disabled> example.org</label></div>
<div class="field"><label class="checkbox"><input type="checkbox" name="domain_ids" checked disabled> shop.example.org</label></div>
<div class="field"><label class="checkbox"><input type="checkbox" name="domain_ids" checked disabled> notify.acme.io</label></div>
<p class="help">With <em>All</em> ticked the list below it is ignored — and greyed out where scripts run.</p>
</div>
</div>
<div class="box">
<div class="sp-box-head"><span class="sp-no">03</span><h2>Inbound domains</h2><div class="sp-end">Upstream, recipients, log</div></div>
<div class="sp-box-body">
<div class="field"><label class="checkbox"><input type="checkbox" name="all_inbound_domains"> <strong>All inbound domains</strong>, including ones added later</label></div>
<div class="field"><label class="checkbox"><input type="checkbox" name="inbound_domain_ids"> lists.example.org</label></div>
<div class="field"><label class="checkbox"><input type="checkbox" name="inbound_domain_ids" checked> acme.io</label></div>
<p class="help">Granted separately from outbound: the same name on both lists is two assignments. A domain administrator needs at least one domain on either list.</p>
<p class="help">Granted separately from outbound: the same name on both lists is two assignments. The <em>domain</em> role needs at least one domain, or <em>All</em>, on either list.</p>
</div>
</div>
</div>
@@ -3,7 +3,7 @@
<div class="sp-grow">
<p class="sp-kicker">Server</p>
<h1 class="title is-3">Users</h1>
<p class="sp-lead">Who may sign in to this panel. A domain administrator sees only the domains assigned to them — outbound and inbound are granted separately.</p>
<p class="sp-lead">Who may sign in to this panel. Everyone here is an administrator; the role is the reach — <em>global</em> is the whole server, <em>domain</em> is the domains assigned, outbound and inbound separately.</p>
</div>
<a class="button is-primary" href="user.html"><i class="ti ti-user-plus"></i>Create user</a>
</div>
@@ -12,12 +12,12 @@
<div class="box">
<div class="sp-box-head"><span class="sp-no">01</span><h2>Panel users</h2><div class="sp-end">3 users</div></div>
<table class="table is-fullwidth is-hoverable">
<thead><tr><th>Username</th><th>Role</th><th>Outbound domains</th><th>Inbound domains</th><th></th></tr></thead>
<div class="table-container"><table class="table is-fullwidth is-hoverable">
<thead><tr><th>Username</th><th>E-mail</th><th>Role</th><th>Outbound domains</th><th>Inbound domains</th><th></th></tr></thead>
<tbody>
<tr><td><strong>admin</strong> <span class="tag is-light">you</span></td><td><span class="tag is-primary is-light">global</span></td><td class="sp-muted">All</td><td class="sp-muted">All</td><td class="sp-actions"><a href="user.html">Edit</a></td></tr>
<tr><td><strong>shop-team</strong></td><td><span class="tag is-light">domain admin</span></td><td class="sp-muted">shop.example.org</td><td class="sp-muted">—</td><td class="sp-actions"><a href="user.html">Edit</a></td></tr>
<tr><td><strong>acme-ops</strong></td><td><span class="tag is-light">domain admin</span></td><td class="sp-muted">notify.acme.io</td><td class="sp-muted">acme.io</td><td class="sp-actions"><a href="user.html">Edit</a></td></tr>
<tr><td><strong>admin</strong> <span class="tag is-light">you</span></td><td class="sp-muted">mix@example.org</td><td><span class="tag is-primary is-light">global</span></td><td class="sp-muted">All</td><td class="sp-muted">All</td><td class="sp-actions"><a href="user.html">Edit</a></td></tr>
<tr><td><strong>shop-team</strong></td><td class="sp-muted">team@shop.example.org</td><td><span class="tag is-light">domain</span></td><td class="sp-muted">shop.example.org</td><td class="sp-muted">—</td><td class="sp-actions"><a href="user.html">Edit</a></td></tr>
<tr><td><strong>acme-ops</strong></td><td class="sp-muted">ops@acme.io</td><td><span class="tag is-light">domain</span></td><td class="sp-muted">All</td><td class="sp-muted">acme.io</td><td class="sp-actions"><a href="user.html">Edit</a></td></tr>
</tbody>
</table>
</table></div>
</div>
@@ -12,7 +12,7 @@
<body>
<nav class="navbar is-primary" aria-label="main navigation">
<div class="container">
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-stamp-compact.svg" alt="SelfPost"></a></div>
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
<div class="navbar-menu is-active">
<div class="navbar-start">
<a class="navbar-item" href="overview.html">Overview</a>
+93 -40
View File
@@ -14,38 +14,68 @@
--bulma-warning-h: 36deg; --bulma-warning-s: 85%; --bulma-warning-l: 55%;
--bulma-danger-h: 4deg; --bulma-danger-s: 76%; --bulma-danger-l: 40%;
--bulma-radius: 6px; --bulma-radius-large: 10px;
--bulma-body-size: 15px;
--bulma-body-size: 16px;
/* The type scale. Three sizes carry all running text; nothing a person has
to read is below 13px. check.py enforces the floor.
body 16px text, inputs, buttons, menus, table cells
s 14px supporting text: labels, help, notes, footers, mono values, logs
xs 13px upper-case mono micro-labels and tags only */
--sp-fs-s: .875rem;
--sp-fs-xs: .8125rem;
--bulma-link-text: var(--sp-brick);
--bulma-link-text-hover: var(--sp-brick-dark);
}
.button.is-primary, .button.is-primary:hover { color: #fff; }
/* Bulma picks a button's text colour from the lightness of its fill; our brick
and our red are dark enough to land on the wrong side and get near-black
text (3:1). Solid buttons are white on colour; the light variants keep
Bulma's dark-on-tint. */
.button.is-primary:not(.is-light), .button.is-primary:not(.is-light):hover,
.button.is-danger:not(.is-light), .button.is-danger:not(.is-light):hover { color: #fff; }
a strong { color: inherit; }
html, body { background: var(--sp-paper); }
body { color: var(--sp-ink); min-height: 100vh; display: flex; flex-direction: column; }
main { flex: 1; }
code { background: var(--sp-well); color: var(--sp-ink); border-radius: 3px; padding: .1em .35em; font-size: .85em; }
code { background: var(--sp-well); color: var(--sp-ink); border-radius: 3px; padding: .1em .35em; font-size: .95em; }
.ti { font-size: 1.15em; vertical-align: -.12em; }
.button .ti { margin-right: .35rem; }
.sp-muted { color: var(--sp-muted); }
/* Bulma ships these at .75rem; on the scale they are s or xs. `sp-small`
replaces Bulma's `is-size-7` helper, which is .75rem and !important. */
.help, .label { font-size: var(--sp-fs-s); }
.tag, .menu-label { font-size: var(--sp-fs-xs); }
.menu-label { color: var(--sp-muted); } /* Bulma's grey is 4.2:1 on the paper */
.sp-small { font-size: var(--sp-fs-s); }
.sp-mono { font-family: var(--sp-mono); }
.sp-nowrap { white-space: nowrap; }
/* ---- navbar, the perforated edge under it, sibling pages below */
.navbar.is-primary { --bulma-navbar-height: 4rem; }
.navbar-brand img { max-height: 3rem; height: 3rem; }
/* The name, not the stamp: at this size the stamp's perforation is under a
pixel and its brick field dissolves into the bar. The stamp stays where it
has room — sign-in, setup, the favicon. */
.navbar-brand img { max-height: none; height: 1.45rem; }
.navbar.is-primary .navbar-item, .navbar.is-primary .navbar-link { color: var(--sp-on-brick); font-weight: 500; }
.navbar.is-primary .navbar-link::after { border-color: var(--sp-on-brick); }
.navbar.is-primary .navbar-link .ti { margin-right: .4rem; }
.navbar-dropdown { border-top: 0; box-shadow: 0 8px 24px rgb(18 22 28 / .14); }
/* Bulma shrinks dropdown items to .875rem; a menu entry is read as often as
the bar it hangs from, so it gets the bar's size. */
.navbar-dropdown { border-top: 0; box-shadow: 0 8px 24px rgb(18 22 28 / .14); font-size: 1rem; }
.navbar.is-primary .navbar-dropdown .navbar-item { color: var(--sp-ink); font-weight: 400; padding-right: 2rem; }
.navbar.is-primary .navbar-dropdown .navbar-item .ti { margin-right: .6rem; color: var(--sp-muted); }
.navbar.is-primary .navbar-dropdown .navbar-item.is-active { color: var(--sp-brick); font-weight: 600; background: var(--sp-brick-tint); }
.navbar.is-primary .navbar-dropdown .navbar-item.is-active .ti { color: var(--sp-brick); }
.navbar.is-primary .sp-current > .navbar-link, .navbar.is-primary a.navbar-item.sp-current { background: rgb(0 0 0 / .18); }
/* The stamp's own edge: a row of punched half-circles hanging off the bar. */
/* The header is built like the mark: a brick field, then the stamp's paper
margin, and the perforation runs along the outer edge of that margin — round
holes, flat teeth, the mark's ink hairline (perf-edge.svg is one tooth and
one hole at the mark's 2.2 / 7.03 ratio). On the mark the margin is one
pitch deep and a tenth of the brick field's height; here that is 8px under
a 60px bar — a little over a tenth, so a 2.5px hole still reads as round. */
.sp-perf {
height: 7px; background: radial-gradient(circle at 7px 0, var(--sp-brick) 5px, transparent 5.6px);
background-size: 14px 7px; background-repeat: repeat-x;
height: 8px;
background:
url("perf-edge.svg") repeat-x left bottom / 8px 4px,
linear-gradient(var(--sp-stamp-paper), var(--sp-stamp-paper)) no-repeat left top / 100% 4px;
}
.sp-subnav { border-bottom: 1px solid var(--sp-rule); margin-top: -1px; }
.sp-subnav .container { display: flex; gap: 1.8rem; overflow-x: auto; }
@@ -58,7 +88,7 @@ code { background: var(--sp-well); color: var(--sp-ink); border-radius: 3px; pad
.sp-page { padding-block: 1.8rem 2.5rem; }
.sp-head { display: flex; align-items: center; gap: 1.2rem; flex-wrap: wrap; margin-bottom: 1.5rem; }
.sp-head .sp-grow { flex: 1; min-width: 14rem; }
.sp-kicker { font-family: var(--sp-mono); font-size: .75rem; letter-spacing: .1em; text-transform: uppercase; color: var(--sp-muted); }
.sp-kicker { font-family: var(--sp-mono); font-size: var(--sp-fs-xs); letter-spacing: .1em; text-transform: uppercase; color: var(--sp-muted); }
.sp-kicker a { color: inherit; }
.sp-kicker a:hover { color: var(--sp-brick); }
.title { color: var(--sp-ink); font-weight: 600; }
@@ -69,24 +99,28 @@ code { background: var(--sp-well); color: var(--sp-ink); border-radius: 3px; pad
/* ---- postmark */
.sp-postmark {
flex: none; width: 5.4rem; height: 5.4rem; border-radius: 50%; transform: rotate(-9deg);
flex: none; width: 6rem; height: 6rem; border-radius: 50%; transform: rotate(-9deg);
border: 2px solid currentColor; outline: 1px solid currentColor; outline-offset: 3px; margin: .3rem .5rem;
display: grid; place-content: center; text-align: center; color: var(--sp-ok);
font-family: var(--sp-mono); font-weight: 600; letter-spacing: .08em; line-height: 1.15;
}
.sp-postmark.sp-warn { color: #a66a0e; }
.sp-postmark.sp-warn { color: var(--sp-warn); }
.sp-postmark.sp-fail { color: var(--sp-fail); }
.sp-postmark b { font-size: 1.2rem; display: block; }
.sp-postmark span { font-size: .52rem; display: block; }
.sp-postmark b { font-size: 1.3rem; display: block; }
/* Below the floor on purpose: an imprint, and the heading beside it says the same in words. */
.sp-postmark span { font-size: .6rem; display: block; }
/* ---- box with a numbered mono head */
.box { box-shadow: none; border: 1px solid var(--sp-rule); padding: 0; overflow: hidden; }
.box > .sp-box-head {
display: flex; align-items: center; gap: .7rem; padding: .7rem 1.25rem; border-bottom: 1px solid var(--sp-rule);
display: flex; flex-wrap: wrap; align-items: center; gap: .7rem; padding: .7rem 1.25rem; border-bottom: 1px solid var(--sp-rule);
}
.sp-box-head .sp-no { font-family: var(--sp-mono); font-size: .75rem; font-weight: 600; color: var(--sp-brick); }
.sp-box-head h2 { font-family: var(--sp-mono); font-size: .8rem; font-weight: 600; letter-spacing: .1em; text-transform: uppercase; margin: 0; }
.sp-box-head .sp-end { margin-left: auto; color: var(--sp-muted); font-size: .85rem; display: flex; gap: .6rem; align-items: center; }
.sp-box-head form.sp-end { justify-content: flex-end; }
/* Bulma's .input is 100% wide; in the filter row it takes a flex basis instead, so it sits beside the selects. */
.sp-box-head form.sp-end > .input { flex: 0 1 10.5rem; }
.sp-box-head .sp-no { font-family: var(--sp-mono); font-size: var(--sp-fs-xs); font-weight: 600; color: var(--sp-brick); }
.sp-box-head h2 { font-family: var(--sp-mono); font-size: var(--sp-fs-xs); font-weight: 600; letter-spacing: .1em; text-transform: uppercase; margin: 0; }
.sp-box-head .sp-end { margin-left: auto; color: var(--sp-muted); font-size: var(--sp-fs-s); display: flex; gap: .6rem; align-items: center; }
.sp-box-head .sp-help { color: var(--sp-muted); line-height: 1; }
.sp-box-head .sp-help:hover { color: var(--sp-brick); }
.sp-box-head .sp-end + .sp-help { margin-left: 0; }
@@ -97,47 +131,59 @@ code { background: var(--sp-well); color: var(--sp-ink); border-radius: 3px; pad
.notification .ti { margin-right: .5rem; }
.box > .sp-box-body { padding: 1.25rem; }
.box > .sp-box-body > :last-child { margin-bottom: 0; }
.box > .table { margin: 0; }
/* A table never sits bare in a box: the container lets a wide one scroll
sideways instead of being cut off by the box's rounded overflow. */
.box > .table-container, .box > .table-container > .table { margin: 0; }
.sp-box-foot .sp-end { margin-left: auto; }
.sp-box-head form.sp-end { margin-block: -.2rem; }
.box > .sp-box-foot {padding: .7rem 1.25rem; border-top: 1px solid var(--sp-rule); color: var(--sp-muted); font-size: .85rem; display: flex; gap: 1rem; align-items: center; }
/* A filter form in the head keeps full-size controls; the head gives up its
own padding to them instead of shrinking the text. */
.sp-box-head form.sp-end { margin-block: -.35rem; font-size: 1rem; }
.box > .sp-box-foot {padding: .7rem 1.25rem; border-top: 1px solid var(--sp-rule); color: var(--sp-muted); font-size: var(--sp-fs-s); display: flex; gap: 1rem; align-items: center; }
.box.sp-danger-zone { border-color: var(--sp-fail-rule); }
.box.sp-danger-zone .sp-box-head { background: var(--sp-fail-bg); border-color: var(--sp-fail-rule); }
.box.sp-credential { border-color: #e0c874; }
.box.sp-credential .sp-box-head { background: #fcf6e4; border-color: #e0c874; }
.table { background: transparent; }
.table thead th { font-family: var(--sp-mono); font-size: .7rem; font-weight: 400; letter-spacing: .08em; text-transform: uppercase; color: var(--sp-muted); }
.table td, .table th { vertical-align: middle; padding: .6rem .75rem; }
.box > .table td:first-child, .box > .table th:first-child { padding-left: 1.25rem; }
.box > .table td:last-child, .box > .table th:last-child { padding-right: 1.25rem; }
.table thead th { font-family: var(--sp-mono); font-size: var(--sp-fs-xs); font-weight: 400; letter-spacing: .08em; text-transform: uppercase; color: var(--sp-muted); }
/* Addresses in tables carry a <wbr> after the @ (one template function), so a
long one breaks where it reads naturally and its column can still shrink;
`overflow-wrap: anywhere` would do it too, but mid-word. */
.table td, .table th { vertical-align: middle; padding: .55rem .6rem; }
.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; }
.table td.sp-clip { max-width: 18rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.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); }
.sp-empty .ti { font-size: 2rem; display: block; margin-bottom: .4rem; color: var(--sp-rule-strong); }
/* ---- health cards */
.sp-health { display: grid; grid-template-columns: repeat(auto-fit, minmax(13rem, 1fr)); gap: 0; }
/* Six cards: six, three or two columns — never a count that leaves one card alone on a row. */
.sp-health { display: grid; grid-template-columns: repeat(6, 1fr); gap: 0; }
.sp-health > a {
display: flex; gap: .8rem; align-items: center; padding: .95rem 1.1rem; color: var(--sp-ink);
display: flex; gap: .7rem; align-items: center; padding: .75rem 1rem; color: var(--sp-ink); min-width: 0;
border-right: 1px solid var(--sp-rule); border-bottom: 1px solid var(--sp-rule); margin: 0 -1px -1px 0;
}
.sp-health > a:hover { background: var(--sp-paper); }
.sp-h-icon { flex: none; width: 2.6rem; height: 2.6rem; border-radius: 8px; display: grid; place-content: center; background: var(--sp-ok-bg); color: var(--sp-ok); }
.sp-h-icon .ti { font-size: 1.5rem; vertical-align: 0; }
.sp-health > a > span:last-child { min-width: 0; }
.sp-h-icon { flex: none; width: 2.1rem; height: 2.1rem; border-radius: 7px; display: grid; place-content: center; background: var(--sp-ok-bg); color: var(--sp-ok); }
.sp-h-icon .ti { font-size: 1.25rem; vertical-align: 0; }
.sp-health > a.sp-warn { background: var(--sp-warn-bg); }
.sp-health > a.sp-warn .sp-h-icon { background: #f6e2b8; color: #8a5510; }
.sp-health > a.sp-fail { background: var(--sp-fail-bg); }
.sp-health > a.sp-fail .sp-h-icon { background: #f5d3cc; color: var(--sp-fail); }
.sp-h-name { font-family: var(--sp-mono); font-size: .66rem; letter-spacing: .08em; text-transform: uppercase; color: var(--sp-muted); }
.sp-h-value { font-weight: 600; line-height: 1.25; }
.sp-h-sub { font-size: .8rem; color: var(--sp-muted); }
/* Each line is a block with its own tight line height; a verdict is two or
three words and never wraps — a wrapped card would stretch the whole row. */
.sp-h-name, .sp-h-value, .sp-h-sub { display: block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
.sp-h-name { font-family: var(--sp-mono); font-size: var(--sp-fs-xs); letter-spacing: .08em; text-transform: uppercase; color: var(--sp-muted); line-height: 1.3; }
.sp-h-value { font-weight: 600; line-height: 1.3; }
.sp-h-sub { font-size: var(--sp-fs-s); color: var(--sp-muted); line-height: 1.3; }
/* ---- facts, records, side menu */
.sp-facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); gap: 1rem 1.5rem; margin: 0; }
.sp-facts dt { font-family: var(--sp-mono); font-size: .68rem; letter-spacing: .08em; text-transform: uppercase; color: var(--sp-muted); }
.sp-facts dt { font-family: var(--sp-mono); font-size: var(--sp-fs-xs); letter-spacing: .08em; text-transform: uppercase; color: var(--sp-muted); }
.sp-facts dd { margin: 0; overflow-wrap: anywhere; }
.sp-facts dd.sp-big { font-size: 1.5rem; font-weight: 600; line-height: 1.2; }
.sp-side { position: sticky; top: 1.5rem; }
@@ -151,9 +197,13 @@ code { background: var(--sp-well); color: var(--sp-ink); border-radius: 3px; pad
.sp-record-head { display: flex; align-items: center; gap: .6rem; margin-bottom: .75rem; }
.sp-record-head h3 { font-size: 1rem; font-weight: 600; margin: 0; }
.sp-record-head small { margin-left: auto; color: var(--sp-muted); text-align: right; }
.sp-record .label, .sp-form .label { font-size: .78rem; font-weight: 500; color: var(--sp-ink-2); margin-bottom: .25rem; }
.sp-record .label, .sp-form .label { font-weight: 500; color: var(--sp-ink-2); margin-bottom: .25rem; }
.sp-record .label { font-weight: 400; color: var(--sp-muted); }
.input[readonly], .textarea[readonly] { font-family: var(--sp-mono); font-size: .82rem; background: var(--sp-well); box-shadow: none; }
.input[readonly], .textarea[readonly] { font-family: var(--sp-mono); font-size: var(--sp-fs-s); background: var(--sp-well); box-shadow: none; }
/* Bulma sizes a control from its own font; the smaller mono text made the
value shorter than the Copy button joined to it. */
.input[readonly] { height: 2.5rem; }
.textarea[readonly] { min-height: 2.5rem; }
.textarea[readonly] { resize: none; min-height: 0; }
.sp-record .field:not(:last-child) { margin-bottom: .6rem; }
.sp-row { display: grid; grid-template-columns: 1fr 7rem; gap: .75rem; }
@@ -162,7 +212,7 @@ code { background: var(--sp-well); color: var(--sp-ink); border-radius: 3px; pad
/* ---- logs, timeline, help */
.sp-log {
margin: 0; padding: 1rem 1.25rem; background: #191c21; color: #d9d5cc; border-radius: 0;
font-family: var(--sp-mono); font-size: .78rem; line-height: 1.6; overflow-x: auto; white-space: pre;
font-family: var(--sp-mono); font-size: var(--sp-fs-s); line-height: 1.6; overflow-x: auto; white-space: pre;
}
.sp-log .sp-w { color: #e0b15c; } .sp-log .sp-e { color: #ef8f82; } .sp-log .sp-d { color: #7f8894; }
.sp-timeline { list-style: none; margin: 0; padding: 0; }
@@ -172,8 +222,8 @@ code { background: var(--sp-well); color: var(--sp-ink); border-radius: 3px; pad
.sp-timeline li.sp-warn::before { background: #d9962b; }
.sp-timeline li.sp-fail::before { background: var(--sp-fail); }
.sp-timeline li.sp-pending::before { background: #fff; border: 2px solid var(--sp-rule-strong); }
.sp-timeline time { font-family: var(--sp-mono); font-size: .75rem; color: var(--sp-muted); display: block; }
.sp-route { display: flex; flex-wrap: wrap; align-items: center; gap: .6rem; font-family: var(--sp-mono); font-size: .9rem; margin-top: .3rem; }
.sp-timeline time { font-family: var(--sp-mono); font-size: var(--sp-fs-xs); color: var(--sp-muted); display: block; }
.sp-route { display: flex; flex-wrap: wrap; align-items: center; gap: .6rem; font-family: var(--sp-mono); font-size: var(--sp-fs-s); margin-top: .3rem; }
.sp-help-topic + .sp-help-topic { border-top: 1px solid var(--sp-rule); }
.sp-help-topic { padding: 1rem 1.25rem; }
.sp-help-topic h3 { font-weight: 600; margin-bottom: .3rem; }
@@ -189,14 +239,17 @@ code { background: var(--sp-well); color: var(--sp-ink); border-radius: 3px; pad
background: radial-gradient(circle at 0 7px, var(--sp-brick) 5px, transparent 5.6px); background-size: 7px 14px;
}
.sp-signin-brand img { width: 20rem; max-width: 100%; height: auto; }
.sp-signin-brand p { font-family: var(--sp-mono); letter-spacing: .18em; font-size: .8rem; color: var(--sp-on-brick); margin-top: 1.6rem; }
.sp-signin-brand p { font-family: var(--sp-mono); letter-spacing: .18em; font-size: var(--sp-fs-xs); color: var(--sp-on-brick); margin-top: 1.6rem; }
.sp-signin-form { display: flex; flex-direction: column; justify-content: center; padding: 3rem; }
.sp-signin-form form { width: 22rem; max-width: 100%; margin-inline: auto; }
.sp-foot { color: var(--sp-muted); font-size: .8rem; padding-block: 1.5rem 0; }
.sp-foot { color: var(--sp-muted); font-size: var(--sp-fs-s); padding-block: 1.5rem 0; }
.sp-foot a { color: inherit; text-decoration: underline; }
.sp-signin-form .sp-foot { text-align: center; margin-top: 2.5rem; }
@media (max-width: 1407px) { .sp-health { grid-template-columns: repeat(3, 1fr); } }
@media (max-width: 1023px) { .sp-box-head form.sp-end { flex-wrap: wrap; } }
@media (max-width: 768px) {
.sp-health { grid-template-columns: repeat(2, 1fr); }
.sp-row, .sp-pair { grid-template-columns: 1fr; }
.sp-record-head { flex-wrap: wrap; }
.sp-record-head small { margin-left: 0; text-align: left; flex-basis: 100%; }
@@ -12,7 +12,7 @@
<body>
<nav class="navbar is-primary" aria-label="main navigation">
<div class="container">
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-stamp-compact.svg" alt="SelfPost"></a></div>
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
<div class="navbar-menu is-active">
<div class="navbar-start">
<a class="navbar-item" href="overview.html">Overview</a>
@@ -80,7 +80,7 @@
<p class="mb-3">Deleting the panel user <strong>acme-ops</strong> will:</p>
<ul class="sp-list-check">
<li><i class="ti ti-x"></i>end any signed-in session of this user at once;</li>
<li><i class="ti ti-x"></i>remove the assignments to <strong>notify.acme.io</strong> (outbound) and <strong>acme.io</strong> (inbound).</li>
<li><i class="ti ti-x"></i>remove the assignments to <strong>all outbound domains</strong> and to <strong>acme.io</strong> (inbound).</li>
</ul>
<p class="sp-muted mb-4">The domains, their applications and passwords, upstreams and recipients are not affected.</p>
<form method="post" action="users.html">
+13 -8
View File
@@ -12,7 +12,7 @@
<body>
<nav class="navbar is-primary" aria-label="main navigation">
<div class="container">
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-stamp-compact.svg" alt="SelfPost"></a></div>
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
<div class="navbar-menu is-active">
<div class="navbar-start">
<a class="navbar-item" href="overview.html">Overview</a>
@@ -78,14 +78,16 @@
<div class="columns">
<div class="column is-6">
<div class="box">
<div class="sp-box-head"><span class="sp-no">01</span><h2>Sign-in</h2></div>
<div class="sp-box-head"><span class="sp-no">01</span><h2>Account</h2></div>
<div class="sp-box-body">
<div class="field"><label class="label" for="username">Username</label><div class="control"><input class="input" id="username" name="username" value="acme-ops" autocomplete="username" spellcheck="false" required></div></div>
<div class="field"><label class="label" for="email">E-mail</label><div class="control"><input class="input" id="email" name="email" type="email" value="ops@acme.io" autocomplete="off"></div>
<p class="help">For notifications and a password-reset link. The user can change it under Account.</p></div>
<div class="field"><label class="label" for="password">Password</label><div class="control"><input class="input" id="password" name="password" type="password" autocomplete="new-password"></div>
<p class="help">Leave empty to keep the current one.</p></div>
<div class="field"><label class="label" for="role">Role</label>
<div class="control"><div class="select is-fullwidth"><select id="role" name="role"><option selected>Domain administrator</option><option>Global administrator</option></select></div></div>
<p class="help">A global administrator sees everything, including Server, and is the only one who adds or deletes domains. The only global administrator cannot be demoted or deleted.</p></div>
<div class="control"><div class="select is-fullwidth"><select id="role" name="role"><option selected>Domain — the domains assigned below</option><option>Global — the whole server</option></select></div></div>
<p class="help">Everyone here is an administrator; the role is the reach. <em>Global</em> also sees Server and is the only one who adds or deletes domains. The only global user cannot be changed to <em>domain</em> or deleted.</p></div>
</div>
</div>
</div>
@@ -93,17 +95,20 @@
<div class="box">
<div class="sp-box-head"><span class="sp-no">02</span><h2>Outbound domains</h2><div class="sp-end">DNS records, applications, log</div></div>
<div class="sp-box-body">
<div class="field"><label class="checkbox"><input type="checkbox" name="domain_ids"> example.org</label></div>
<div class="field"><label class="checkbox"><input type="checkbox" name="domain_ids"> shop.example.org</label></div>
<div class="field"><label class="checkbox"><input type="checkbox" name="domain_ids" checked> notify.acme.io</label></div>
<div class="field"><label class="checkbox"><input type="checkbox" name="all_domains" checked> <strong>All outbound domains</strong>, including ones added later</label></div>
<div class="field"><label class="checkbox"><input type="checkbox" name="domain_ids" checked disabled> example.org</label></div>
<div class="field"><label class="checkbox"><input type="checkbox" name="domain_ids" checked disabled> shop.example.org</label></div>
<div class="field"><label class="checkbox"><input type="checkbox" name="domain_ids" checked disabled> notify.acme.io</label></div>
<p class="help">With <em>All</em> ticked the list below it is ignored — and greyed out where scripts run.</p>
</div>
</div>
<div class="box">
<div class="sp-box-head"><span class="sp-no">03</span><h2>Inbound domains</h2><div class="sp-end">Upstream, recipients, log</div></div>
<div class="sp-box-body">
<div class="field"><label class="checkbox"><input type="checkbox" name="all_inbound_domains"> <strong>All inbound domains</strong>, including ones added later</label></div>
<div class="field"><label class="checkbox"><input type="checkbox" name="inbound_domain_ids"> lists.example.org</label></div>
<div class="field"><label class="checkbox"><input type="checkbox" name="inbound_domain_ids" checked> acme.io</label></div>
<p class="help">Granted separately from outbound: the same name on both lists is two assignments. A domain administrator needs at least one domain on either list.</p>
<p class="help">Granted separately from outbound: the same name on both lists is two assignments. The <em>domain</em> role needs at least one domain, or <em>All</em>, on either list.</p>
</div>
</div>
</div>
+8 -8
View File
@@ -12,7 +12,7 @@
<body>
<nav class="navbar is-primary" aria-label="main navigation">
<div class="container">
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-stamp-compact.svg" alt="SelfPost"></a></div>
<div class="navbar-brand"><a class="navbar-item" href="overview.html"><img src="../../selfpost-wordmark.svg" alt="SelfPost"></a></div>
<div class="navbar-menu is-active">
<div class="navbar-start">
<a class="navbar-item" href="overview.html">Overview</a>
@@ -69,7 +69,7 @@
<div class="sp-grow">
<p class="sp-kicker">Server</p>
<h1 class="title is-3">Users</h1>
<p class="sp-lead">Who may sign in to this panel. A domain administrator sees only the domains assigned to them — outbound and inbound are granted separately.</p>
<p class="sp-lead">Who may sign in to this panel. Everyone here is an administrator; the role is the reach — <em>global</em> is the whole server, <em>domain</em> is the domains assigned, outbound and inbound separately.</p>
</div>
<a class="button is-primary" href="user.html"><i class="ti ti-user-plus"></i>Create user</a>
</div>
@@ -78,14 +78,14 @@
<div class="box">
<div class="sp-box-head"><span class="sp-no">01</span><h2>Panel users</h2><div class="sp-end">3 users</div></div>
<table class="table is-fullwidth is-hoverable">
<thead><tr><th>Username</th><th>Role</th><th>Outbound domains</th><th>Inbound domains</th><th></th></tr></thead>
<div class="table-container"><table class="table is-fullwidth is-hoverable">
<thead><tr><th>Username</th><th>E-mail</th><th>Role</th><th>Outbound domains</th><th>Inbound domains</th><th></th></tr></thead>
<tbody>
<tr><td><strong>admin</strong> <span class="tag is-light">you</span></td><td><span class="tag is-primary is-light">global</span></td><td class="sp-muted">All</td><td class="sp-muted">All</td><td class="sp-actions"><a href="user.html">Edit</a></td></tr>
<tr><td><strong>shop-team</strong></td><td><span class="tag is-light">domain admin</span></td><td class="sp-muted">shop.example.org</td><td class="sp-muted">—</td><td class="sp-actions"><a href="user.html">Edit</a></td></tr>
<tr><td><strong>acme-ops</strong></td><td><span class="tag is-light">domain admin</span></td><td class="sp-muted">notify.acme.io</td><td class="sp-muted">acme.io</td><td class="sp-actions"><a href="user.html">Edit</a></td></tr>
<tr><td><strong>admin</strong> <span class="tag is-light">you</span></td><td class="sp-muted">mix@example.org</td><td><span class="tag is-primary is-light">global</span></td><td class="sp-muted">All</td><td class="sp-muted">All</td><td class="sp-actions"><a href="user.html">Edit</a></td></tr>
<tr><td><strong>shop-team</strong></td><td class="sp-muted">team@shop.example.org</td><td><span class="tag is-light">domain</span></td><td class="sp-muted">shop.example.org</td><td class="sp-muted">—</td><td class="sp-actions"><a href="user.html">Edit</a></td></tr>
<tr><td><strong>acme-ops</strong></td><td class="sp-muted">ops@acme.io</td><td><span class="tag is-light">domain</span></td><td class="sp-muted">All</td><td class="sp-muted">acme.io</td><td class="sp-actions"><a href="user.html">Edit</a></td></tr>
</tbody>
</table>
</table></div>
</div>
<footer class="sp-foot">SelfPost 1.8.0 · © 2026 Mix · <a href="#">License (AGPL-3.0)</a> · <a href="#">Source</a> · No warranty</footer>
+1 -1
View File
@@ -31,7 +31,7 @@
--sp-sheet: #fff;
--sp-ink: #12161c;
--sp-ink-2: #2a313b;
--sp-muted: #6b7684;
--sp-muted: #5c6774; /* 5:1 on the paper and on the tinted cards; #6b7684 was 4.1:1 */
--sp-rule: #dedcd7;
--sp-rule-strong: #cbc8c1;
--sp-well: #efede9; /* code / read-only values */

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

+90 -15
View File
@@ -17,6 +17,7 @@ design itself is drawn and accepted:
| [panel/components.html](../assets/panel-redesign/panel/components.html) | every component in every state — the only parts a page may be built from |
| [panel/outlines.json](../assets/panel-redesign/panel/outlines.json) | the component skeleton of each screen, generated from the mockups |
| [panel/check.py](../assets/panel-redesign/panel/check.py) | the contract as a program; its rules are ported to Go tests below |
| [panel/audit.js](../assets/panel-redesign/panel/audit.js) | what only a browser can measure: text size, contrast, overflow, clipped values — pasted into the console, on the mockups or on the running panel |
When this file and a mockup disagree, the mockup wins and this file is fixed.
When an implementation and a mockup disagree, the implementation is wrong.
@@ -55,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 credentials half of `/settings` |
| User menu | Account · Help · Sign out | Account is the user's half of `/settings`: credentials, e-mail, DMARC default |
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
@@ -86,7 +87,7 @@ too, and that is granted separately from outbound.** Today every inbound
handler is `requireGlobal()` and `user_domains` references outbound domains
only.
| | Global administrator | Domain administrator |
| | `global` | `domain` |
|---|---|---|
| Overview, Server | yes | no (404) |
| Outbound: domain, applications, settings, log, DMARC | all domains | assigned outbound domains |
@@ -100,13 +101,53 @@ only.
- New table `user_inbound_domains (user_id, inbound_domain_id)`, mirroring
`user_domains`; both live in the 2.0 baseline. The same name on both lists
is two assignments — the two kinds of domain stay separate entities.
- The user form has two lists (mockup `user.html`); a domain administrator
needs at least one assignment on either.
- The user form has two lists (mockup `user.html`); the *domain* role needs at
least one assignment on either.
- **Each list has *All*, which includes domains added later** — a flag per
user and direction (`users.all_domains`, `users.all_inbound_domains`), not a
tick on every row: otherwise tomorrow's domain is silently unassigned. *All*
widens the list, not the role: adding and deleting domains, the queues and
Server stay with the global role. The form works without scripts — with
*All* ticked the server ignores the rows below it.
- **The role is a reach, not a rank.** Everyone in the panel is an
administrator, so the label is `global` or `domain` — in the users list, the
role select and the page kickers; never "admin".
- A group with nothing assigned is absent from that user's menu.
- **The spam filter is visible to whoever it affects.** Whether the instance
filters inbound mail is an environment setting, so a domain administrator
cannot look it up: every inbound domain page has a *Spam filter* box (on /
off, engine, what happens to suspicious mail, who keeps the lists), the
inbound domains list shows it in its head, and for the global administrator
the filter's socket is one of the milter sockets on Health and Overview.
- Authorization helpers come in pairs: `requireDomain(id)` and
`requireInboundDomain(id)`; lists and logs are filtered by assignment in the
query, not in the template.
## 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.
- 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`.
## Routes
**Decided 2026-09-21 (owner): paths are renamed to follow the new names and
@@ -135,8 +176,8 @@ list, an action under the thing it changes.
| system-log | `GET /server/log` | `/system-log` |
| 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` |
| account | `GET, POST /account` | the credentials half of `/settings` (today `/account` redirects there) |
| settings | `GET, POST /server/settings` | the instance half of `/settings`: log retention, rate limits; plus the notifications switch |
| account | `GET /account` · `POST /account/profile`, `/password`, `/notifications`, `/dmarc` | `/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`,
@@ -191,6 +232,8 @@ the first step of that feature's plan — not invented during implementation.
| inbound-antispam-panel (agreed, `1.10.0`) | journal = **Inbound › Log** (add client IP and engine to the mockup's columns; `quarantine` joins the decisions); lists = new **Inbound › Filter lists**, global-only because the lists are instance-wide | list layout: add form + two tables, `status_tag`, filter form in `box_head`; the log links *Allow / Deny sender* to the lists | `in-log`, `in-filter-lists` |
| inbound-quarantine (candidate) | new **Inbound › Quarantine**, list + detail with *Release* / *Discard* | list layout; detail like `out-message` with `facts`, `log_pane` for headers only, Release beside a `sp-danger-zone` Discard | `in-quarantine`, `in-quarantine-message` — the open questions of its plan (where mail lives, what release means, retention, RBAC) stay open; the mockup assumes SelfPost-held mail, release to upstream, 14 days, delegated like the rest of Inbound |
| preflight (candidate) | new **Server › Preflight**, next to Health: Health is what runs now, Preflight is the deeper on-demand installation check with a test e-mail form | `postmark` verdict, a table of checks with `status_tag` and a *What to do* column, one form box for the test e-mail | `preflight` |
| panel-notifications (candidate) | per-user event choice on **Account**; one switch under **Server › Settings** | checkboxes in a box, `help` | `account`, `settings` |
| password-reset (candidate) | a link on sign-in, two signed-out screens (request, set new password) | the signed-out layout | **no** |
| csrf-tokens | no screen; every form gains a hidden field — forms are rendered through one helper so the token cannot be forgotten | — | n/a |
| template-data-typing | no screen; the component partials are the natural first users of typed data | — | n/a |
| structured-logging, contributing, review follow-ups | none | — | n/a |
@@ -226,8 +269,31 @@ migration to the 1.x chain as planned; the 2.0 baseline then folds it in.
| `timeline`, `log_pane`, `empty_state`, `confirm_list`, `help_topic` | as on the components page |
| form rows | `sp-row` (wide + short), `sp-pair` (two equal) |
The shell — brick navbar with hover menus, perforated edge, sibling-page
strip, footer — belongs to `layout.html` alone.
The shell belongs to `layout.html` alone: a brick navbar with hover menus
that carries the **wordmark, not the stamp** (at nav size the stamp dissolves
into the bar; it stays on sign-in, setup and the favicon); under it the
stamp's paper margin with the perforation along its outer edge — round holes,
flat teeth, the mark's ink hairline, one tile `perf-edge.svg`; then the
sibling-page strip; and the footer.
## Type and tables
- **Three sizes.** Body 16px; `--sp-fs-s` 14px for supporting text (labels,
`help`, notes, mono values, logs); `--sp-fs-xs` 13px for upper-case mono
micro-labels and tags only. Nothing a person has to read is under 13px.
Bulma's own small sizes are overridden (`help`, `tag`, `label`,
`menu-label`, the navbar dropdown) or not used (`is-small` on a control,
`is-size-7` — `sp-small` instead). `check.py` enforces the floor.
- **Contrast.** Text is at least 4.5:1 against the ground it actually sits on
(3:1 for large text). Two traps are closed in the theme: Bulma picks a solid
button's text colour from the fill's lightness, and the brick and the red
land on the wrong side — solid buttons are white on colour; and the muted
grey is `#5c6774`, not the 4.1:1 `#6b7684` of the old panel.
- **A table sits in a `table-container`**, so a wide one scrolls inside its
box instead of being cut off. Addresses in table cells get a `<wbr>` after
the `@` from one template function, so a column can shrink without breaking a
name mid-word. Screens fit without scrolling at 1280px and up; below that a
dense log scrolls inside its box.
## Layout rules
@@ -239,7 +305,11 @@ A page is exactly one of:
- **signed out** — the split brand / form screen.
Width comes from the layout, never from the page. A narrow form does not get a
`max-width`; it gets a column and a neighbour that explains it.
`max-width`; it gets a column. **What stands beside it must be real** — the
form's own result, the facts it changes, a second form. A box written to fill
the space beside another is the same defect as a width tuned by hand, and the
first draft of Preflight had one. If nothing real belongs there, the form
shares its box with its result, or the page uses fewer columns.
## The contract
@@ -249,17 +319,20 @@ An implementer — human or model — **must not**:
that the components page does not show.
2. Add a component, a state or a layout. If a screen seems to need one, **stop
and ask**; do not approximate it with ad-hoc markup.
3. Put a selector in `panel.css` that names a page or an element id, uses
`!important`, or sets a width outside the `WIDTH_OK` list.
4. Write component markup by hand in a page template instead of calling the
3. Add a box whose only job is to fill space, or text that restates the page.
Every box answers a question the operator has on that page.
4. Put a selector in `panel.css` that names a page or an element id, uses
`!important`, sets a width outside the `WIDTH_OK` list, or a font size under
the 13px floor.
5. Write component markup by hand in a page template instead of calling the
partial.
5. Edit the vendored Bulma, or load a second CSS framework, icon set or any
6. Edit the vendored Bulma, or load a second CSS framework, icon set or any
JavaScript widget.
6. Change a mockup, `outlines.json`, `check.py` or a guard test in the same
7. Change a mockup, `outlines.json`, `check.py` or a guard test in the same
commit as `internal/web`, or at all without the owner's explicit
instruction. **The guards are not the implementer's to adjust.** A failing
guard means the implementation is wrong, not the guard.
7. Report a step as done on the strength of a description. Done means the
8. Report a step as done on the strength of a description. Done means the
evidence below exists.
**Changing the design** is allowed and has one route: mockup first. Edit the
@@ -291,6 +364,8 @@ or the guard tests.
Every checklist step that restyles pages ends with, in the commit:
- `go test ./internal/web/...` output in the commit message body (pass);
- the output of `panel/audit.js` run against the restyled pages of the running
container, with an empty `problems` list;
- a screenshot of each restyled page at 1440×900, saved next to its mockup's
name under `docs/assets/panel-redesign/evidence/`, rendered from the running
container with the e2e fixtures — not from the mockup;
+1
View File
@@ -24,6 +24,7 @@ delivers as the configured sending domain.
| | Text |
|---|---|
| Stamp subtitle | SELF-HOSTED SMTP RELAY |
| Wordmark | The stamp's own letter outlines without the stamp (`docs/assets/selfpost-wordmark.svg`, `-ink` for light grounds) — for places too small for the stamp, such as the panel header |
| Tagline | YOUR SMTP. YOUR RULES. |
| Hero line (README, GitHub) | Self-hosted SMTP relay with a web panel — one Docker image, no provider. |
+52
View File
@@ -36,6 +36,8 @@ in `git log` and [CHANGELOG.md](../CHANGELOG.md).
| template-data-typing | Typed structs for template data instead of `map[string]any` | candidate | — | — |
| structured-logging | `log/slog` with levels and fields | candidate | — | — |
| review-2026-08-followups | Remaining findings of the 2026-08-19 code review | candidate | — | — |
| panel-notifications | E-mail notifications about important events | candidate | — | — |
| password-reset | Password reset by e-mail | candidate | — | — |
| panel-redesign | Panel redesign (accepted mockups, design contract) | candidate | 3/21 | [plans/panel-redesign.md](plans/panel-redesign.md) |
| schema-squash | Squash SQLite migrations into a 2.x baseline | **2.x** | — | — |
@@ -295,6 +297,56 @@ and tagging it would only delete cross-platform test coverage.
---
## panel-notifications
**Goal:** the panel tells its administrators when something needs them instead
of waiting to be opened: a DNS record stops matching, a domain or application
hits its rate limit and mail is refused, an inbound upstream is unreachable,
DMARC failures appear, mail is held in quarantine; for the global
administrator also the TLS certificate nearing expiry, a process or milter
socket down, a queue older than a day.
**Boundary:** e-mail only, sent through the relay itself from
`postmaster@<hostname>`. Each user picks events under **Account**; a domain
administrator hears only about assigned domains. One message when a problem
starts and one when it clears — no streams, no digests in v1. One instance
switch under **Server › Settings**. No webhooks, no chat integrations.
**Done when:** an event from the list reaches the account e-mail of every user
who chose it and may see its subject, once on start and once on clear; a user
without an e-mail gets nothing; the switch turns it all off.
**Dependencies / risks:** the account e-mail (2.0 baseline, see
[plans/panel-redesign.md](plans/panel-redesign.md) § Account e-mail); a relay
that cannot send cannot report that it cannot send — server events need the
direct-to-MX path or stay panel-only; event state must survive restarts so a
reboot does not re-announce everything. Screens are drawn: `account`,
`settings`.
**Version:** `2.x` MINOR; `candidate`.
---
## password-reset
**Goal:** a user who forgot their password gets a one-time link at their
account e-mail instead of asking the global administrator to set a new one.
**Boundary:** a link on the sign-in page, a short-lived single-use token, the
same answer whether or not the username exists. The only global administrator
without a working e-mail still recovers from the container (today's path).
**Done when:** the flow works end to end, tokens expire and cannot be reused,
requests are rate-limited, and it passes a security review.
**Dependencies / risks:** the account e-mail; an account-takeover surface where
there was none — security review is part of the item, not an afterthought.
Not drawn yet.
**Version:** `2.x` MINOR; `candidate`.
---
## panel-redesign
**Goal:** a panel an operator opens a few times a year and still finds their