design: notifications on Account, Server > Settings and Server > Health

Accepted by the owner on 2026-10-12 from screenshots, after two changes of
his: DKIM for the server's own name as well as SPF ("we need the best
deliverability for these messages"), and the records themselves under
Settings with only their state on Health.

- account: a checkbox "Send me notifications" in the Profile box, under the
  e-mail it writes to. No box of its own, no choice per event.
- settings: box 03 Notifications with the instance switch; box 04
  Notification mail with the two DNS records of the server's own name, DKIM
  and SPF, drawn with the record component of a domain's DNS page. Sending
  rate limits move to the left column as box 02.
- health: card 06 Notification mail - the state of each record and a link to
  Settings - beside Configuration, now 07, each half the width.
- help: a Notifications topic (what is sent, when, to whom, what makes it
  arrive); Settings and Your account each gain a clause.

outlines.json rewritten with check.py --write: the structure of settings and
health changed. build.py: 35 pages; check.py: 32 screens, 52 sp- classes,
0 violations. The plan records both of his changes; step 1 of 10.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
mix
2026-10-12 15:45:06 +03:00
co-authored by Claude Opus 5.5
parent eff8010d39
commit 87fa0a4381
11 changed files with 172 additions and 28 deletions
@@ -82,6 +82,8 @@
<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="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> <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">Your own address. A domain's report address can be filled with it from Domain settings.</p></div> <p class="help">Your own address. A domain's report address can be filled with it from Domain settings.</p></div>
<div class="field"><label class="checkbox"><input type="checkbox" name="notify" checked> Send me notifications</label>
<p class="help">To this address, when something needs you: a DNS record of a domain stops matching, a rate limit refuses mail, the TLS certificate is about to expire, a message has waited in the queue for three days. <a href="help.html#notifications">What is sent and when</a></p></div>
<button type="submit" class="button is-primary">Save profile</button> <button type="submit" class="button is-primary">Save profile</button>
</form> </form>
</div> </div>
+24 -5
View File
@@ -145,11 +145,30 @@
</div> </div>
</div> </div>
<div class="box" id="configuration"> <div class="columns">
<div class="sp-box-head"><span class="sp-no">06</span><h2>Configuration</h2><div class="sp-end">Safe to run at any time</div></div> <div class="column is-6">
<div class="sp-box-body"> <div class="box" id="notification-mail">
<p class="mb-3">Regenerates the OpenDKIM and Postfix configuration from the database and reloads both daemons. Use it after editing the files by hand, after restoring a backup, or when the running configuration looks out of step with the domain and application lists. It does not touch the queue or the TLS certificate.</p> <div class="sp-box-head"><span class="sp-no">06</span><h2>Notification mail</h2><div class="sp-end"><span class="tag is-warning is-light">warn</span></div><a class="sp-help" href="help.html#notifications" title="What the panel sends and what makes it arrive"><i class="ti ti-help-circle"></i></a></div>
<form method="post" action="#"><button type="submit" class="button"><i class="ti ti-reload"></i>Reload configuration</button></form> <div class="table-container"><table class="table is-fullwidth">
<thead><tr><th>Record</th><th>State</th></tr></thead>
<tbody>
<tr><td>DKIM<br><span class="sp-muted sp-mono sp-small">sp2026._domainkey.mail.example.org</span></td><td><span class="tag is-success is-light">published</span></td></tr>
<tr><td>SPF<br><span class="sp-muted sp-mono sp-small">mail.example.org</span></td><td><span class="tag is-warning is-light">missing</span></td></tr>
</tbody>
</table></div>
<div class="sp-box-body">
<p class="sp-muted">Sent from <span class="sp-mono">postmaster@mail.example.org</span>. What to publish is under <a href="settings.html#notification-mail">Settings</a>.</p>
</div>
</div>
</div>
<div class="column is-6">
<div class="box" id="configuration">
<div class="sp-box-head"><span class="sp-no">07</span><h2>Configuration</h2><div class="sp-end">Safe to run at any time</div></div>
<div class="sp-box-body">
<p class="mb-3">Regenerates the OpenDKIM and Postfix configuration from the database and reloads both daemons. Use it after editing the files by hand, after restoring a backup, or when the running configuration looks out of step with the domain and application lists. It does not touch the queue or the TLS certificate.</p>
<form method="post" action="#"><button type="submit" class="button"><i class="ti ti-reload"></i>Reload configuration</button></form>
</div>
</div>
</div> </div>
</div> </div>
+4 -2
View File
@@ -105,6 +105,7 @@
<li><a href="#backup"><i class="ti ti-archive"></i>Backup and moving a domain</a></li> <li><a href="#backup"><i class="ti ti-archive"></i>Backup and moving a domain</a></li>
<li><a href="#users"><i class="ti ti-users"></i>Users and roles</a></li> <li><a href="#users"><i class="ti ti-users"></i>Users and roles</a></li>
<li><a href="#settings"><i class="ti ti-settings"></i>Settings</a></li> <li><a href="#settings"><i class="ti ti-settings"></i>Settings</a></li>
<li><a href="#notifications"><i class="ti ti-mail"></i>Notifications</a></li>
</ul> </ul>
<p class="menu-label">Account</p> <p class="menu-label">Account</p>
<ul class="menu-list"> <ul class="menu-list">
@@ -141,11 +142,12 @@
<div class="sp-box-head"><span class="sp-no">04</span><h2>Server</h2></div> <div class="sp-box-head"><span class="sp-no">04</span><h2>Server</h2></div>
<div class="sp-help-topic" id="backup"><h3>Backup and moving a domain</h3><p>Full backup downloads one archive of the whole instance: <code>data/</code> (the database, DKIM keys and application credentials), <code>docker-compose.yml</code>, <code>.env</code> and <code>certs/</code>. The Postfix queue is not in it. The backup is taken while the server is running, and Postfix keeps moving messages through the queue, so a copy could hold a message twice or miss it; mail still waiting for delivery is therefore not carried over, and a restored server starts with an empty queue. To keep the queue, stop the container and archive the project directory instead. The private key in <code>certs/</code> has to be readable by the panel: give it to group 998 on the host (<code>chgrp 998</code>, <code>chmod 640</code>). Until then the panel refuses to make a backup and names the file, rather than hand over an archive without the key. It holds secrets, so leave Encrypt with a password ticked: the file is sealed as <code>.spbk</code>, the password is not stored, and without it nothing can be recovered. To restore, unpack it into an empty project directory (decrypt a <code>.spbk</code> first with <code>selfpost-backup -decrypt</code>) and start the same SelfPost version that made it; the panel refuses data from another version. The reverse-proxy configuration and <code>mail.log</code> are not included. One domain moves on its own: Export domain on its settings page writes a <code>.json</code> (or encrypted <code>.spde</code>) with its DKIM key and its applications' passwords, and Import a domain here reads it into another instance, so the published DNS needs no change.</p></div> <div class="sp-help-topic" id="backup"><h3>Backup and moving a domain</h3><p>Full backup downloads one archive of the whole instance: <code>data/</code> (the database, DKIM keys and application credentials), <code>docker-compose.yml</code>, <code>.env</code> and <code>certs/</code>. The Postfix queue is not in it. The backup is taken while the server is running, and Postfix keeps moving messages through the queue, so a copy could hold a message twice or miss it; mail still waiting for delivery is therefore not carried over, and a restored server starts with an empty queue. To keep the queue, stop the container and archive the project directory instead. The private key in <code>certs/</code> has to be readable by the panel: give it to group 998 on the host (<code>chgrp 998</code>, <code>chmod 640</code>). Until then the panel refuses to make a backup and names the file, rather than hand over an archive without the key. It holds secrets, so leave Encrypt with a password ticked: the file is sealed as <code>.spbk</code>, the password is not stored, and without it nothing can be recovered. To restore, unpack it into an empty project directory (decrypt a <code>.spbk</code> first with <code>selfpost-backup -decrypt</code>) and start the same SelfPost version that made it; the panel refuses data from another version. The reverse-proxy configuration and <code>mail.log</code> are not included. One domain moves on its own: Export domain on its settings page writes a <code>.json</code> (or encrypted <code>.spde</code>) with its DKIM key and its applications' passwords, and Import a domain here reads it into another instance, so the published DNS needs no change.</p></div>
<div class="sp-help-topic" id="users"><h3>Users and roles</h3><p>The two roles are a reach, not a rank: every user is an administrator and signs in with a username and password. Global reaches the whole server — Overview, Server, the queue and every domain — and is the only role that adds or deletes domains and users. Domain reaches only the domains assigned to it, outbound and inbound separately (the same name on both lists is two assignments); <em>All</em> includes domains added later, and a domain user needs at least one domain or <em>All</em> on either list. The only global user cannot be demoted or deleted, and you cannot delete the account you are signed in with. A new password set here signs that user out of their other sessions.</p></div> <div class="sp-help-topic" id="users"><h3>Users and roles</h3><p>The two roles are a reach, not a rank: every user is an administrator and signs in with a username and password. Global reaches the whole server — Overview, Server, the queue and every domain — and is the only role that adds or deletes domains and users. Domain reaches only the domains assigned to it, outbound and inbound separately (the same name on both lists is two assignments); <em>All</em> includes domains added later, and a domain user needs at least one domain or <em>All</em> on either list. The only global user cannot be demoted or deleted, and you cannot delete the account you are signed in with. A new password set here signs that user out of their other sessions.</p></div>
<div class="sp-help-topic" id="settings"><h3>Settings</h3><p>Settings holds what is true of the whole instance. Log retention is how many days the Outbound log keeps its rows, from 7 to 365 and 90 by default; a sweep every six hours deletes older ones, and it is the main driver of how fast <code>/data</code> grows. With the inbound relay on there is a second number, for the Inbound log, with the same range. It does not change the rotation of <code>mail.log</code>, which is daily with about two weeks kept. The sending rate limit shown under it is level 1, the per-client-IP ceiling set in <code>.env</code>: changing it needs a restart, and no level-2 limit can exceed it.</p></div> <div class="sp-help-topic" id="settings"><h3>Settings</h3><p>Settings holds what is true of the whole instance. Log retention is how many days the Outbound log keeps its rows, from 7 to 365 and 90 by default; a sweep every six hours deletes older ones, and it is the main driver of how fast <code>/data</code> grows. With the inbound relay on there is a second number, for the Inbound log, with the same range. It does not change the rotation of <code>mail.log</code>, which is daily with about two weeks kept. The sending rate limit shown under it is level 1, the per-client-IP ceiling set in <code>.env</code>: changing it needs a restart, and no level-2 limit can exceed it. Notifications are turned on or off here for everyone.</p></div>
<div class="sp-help-topic" id="notifications"><h3>Notifications</h3><p>The panel writes to its users when something needs them, from <code>postmaster@</code> the server's hostname. Four things are reported. A sending domain's SPF, DKIM or DMARC record stops matching: sent once two hourly checks in a row have found it so, and once more when the record matches again. A level-2 rate limit refuses mail: sent at the first refusal, then at most once a day for the same limit. The TLS certificate is 14 days from expiry, and again at 3 days if it is still the same certificate. A message has waited in the queue for more than three days, with one more message when none is left. A global user is told all of it; a domain administrator is told about the DNS records and limits of their own domains.<br><br>Each user turns notifications on or off under Account and needs an e-mail there; Server › Settings turns them off for everyone. They are the panel's own mail: not rows of the Outbound log, and not counted against any limit.<br><br>For that mail to arrive, the server's own name needs two DNS records of its own, as each of your domains does: a DKIM key, which the panel generates and signs notifications with, and an SPF record, <code>v=spf1 a -all</code>. Server › Settings gives both with the values to publish, and Server › Health shows whether they are in place. Without them notifications are still sent, and a receiver may file them as spam. Mail of your domains does not depend on either.</p></div>
</div> </div>
<div class="box"> <div class="box">
<div class="sp-box-head"><span class="sp-no">05</span><h2>Account</h2></div> <div class="sp-box-head"><span class="sp-no">05</span><h2>Account</h2></div>
<div class="sp-help-topic" id="account"><h3>Your account</h3><p>Account is your own, whatever your role: your username and e-mail, and your password. Changing the password signs out every other session of this account and keeps the one you are using; applications have their own logins and are not affected.</p></div> <div class="sp-help-topic" id="account"><h3>Your account</h3><p>Account is your own, whatever your role: your username and e-mail, whether the panel sends you notifications, and your password. Changing the password signs out every other session of this account and keeps the one you are using; applications have their own logins and are not affected.</p></div>
</div> </div>
</div> </div>
</div> </div>
+19 -4
View File
@@ -108,7 +108,20 @@
} }
] ]
}, },
"box[Configuration]:body" {
"columns": [
{
"is-6": [
"box[Notification mail]:table+body"
]
},
{
"is-6": [
"box[Configuration]:body"
]
}
]
}
], ],
"help": [ "help": [
"head", "head",
@@ -124,7 +137,7 @@
"box[Overview]:help×3", "box[Overview]:help×3",
"box[Outbound]:help×6", "box[Outbound]:help×6",
"box[Inbound]:help×6", "box[Inbound]:help×6",
"box[Server]:help×3", "box[Server]:help×4",
"box[Account]:help" "box[Account]:help"
] ]
} }
@@ -406,12 +419,14 @@
"columns": [ "columns": [
{ {
"is-6": [ "is-6": [
"box[Log retention]:body" "box[Log retention]:body",
"box[Sending rate limits]:body"
] ]
}, },
{ {
"is-6": [ "is-6": [
"box[Sending rate limits]:body" "box[Notifications]:body",
"box[Notification mail]:record×2"
] ]
} }
] ]
+37 -2
View File
@@ -87,9 +87,7 @@
<p class="help">The inbound log also never holds more than 10 000 rows: refused mail is logged at the rate strangers knock on port 25.</p></div> <p class="help">The inbound log also never holds more than 10 000 rows: refused mail is logged at the rate strangers knock on port 25.</p></div>
</div> </div>
</div> </div>
</div>
<div class="column is-6">
<div class="box" id="rate-limits"> <div class="box" id="rate-limits">
<div class="sp-box-head"><span class="sp-no">02</span><h2>Sending rate limits</h2><div class="sp-end">Read-only</div><a class="sp-help" href="help.html#limits" title="Levels 1 and 2"><i class="ti ti-help-circle"></i></a></div> <div class="sp-box-head"><span class="sp-no">02</span><h2>Sending rate limits</h2><div class="sp-end">Read-only</div><a class="sp-help" href="help.html#limits" title="Levels 1 and 2"><i class="ti ti-help-circle"></i></a></div>
<div class="sp-box-body"> <div class="sp-box-body">
@@ -100,6 +98,43 @@
</div> </div>
</div> </div>
</div> </div>
<div class="column is-6">
<div class="box" id="notifications">
<div class="sp-box-head"><span class="sp-no">03</span><h2>Notifications</h2><a class="sp-help" href="help.html#notifications" title="What is sent and when"><i class="ti ti-help-circle"></i></a></div>
<div class="sp-box-body">
<div class="field"><label class="checkbox"><input type="checkbox" name="notify" checked> Send notification e-mails</label>
<p class="help">From <span class="sp-mono">postmaster@mail.example.org</span>, to every user who has an e-mail and has not turned them off under <a href="account.html">Account</a>.</p></div>
</div>
</div>
<div class="box" id="notification-mail">
<div class="sp-box-head"><span class="sp-no">04</span><h2>Notification mail</h2><div class="sp-end">DNS records of the server's own name</div><a class="sp-help" href="help.html#notifications" title="What the panel sends and what makes it arrive"><i class="ti ti-help-circle"></i></a></div>
<div class="sp-record">
<div class="sp-record-head"><h3>DKIM</h3><span class="tag is-success is-light">published</span><small>Not a secret · selector <code>sp2026</code></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="sp2026._domainkey.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"><textarea class="textarea" rows="5" readonly>v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq7n2Lw0c5vXeR1dYt8uJb3mKs6pHf9aZoWg4iNy2TqCx0lVjE7rUh5sBk1dMa8fPz3oGc6yXw9tIe4nQv2uL…IDAQAB</textarea></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
</div>
<div class="sp-record">
<div class="sp-record-head"><h3>SPF</h3><span class="tag is-warning is-light">missing</span><small>For the server's own name, not for your domains</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="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=spf1 a -all"></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
<p class="help">Notifications are sent without it, and a receiver may file them as spam.</p>
</div>
</div>
</div>
</div> </div>
<div class="buttons"><button type="submit" class="button is-primary">Save settings</button></div> <div class="buttons"><button type="submit" class="button is-primary">Save settings</button></div>
</form> </form>
@@ -16,6 +16,8 @@
<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="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> <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">Your own address. A domain's report address can be filled with it from Domain settings.</p></div> <p class="help">Your own address. A domain's report address can be filled with it from Domain settings.</p></div>
<div class="field"><label class="checkbox"><input type="checkbox" name="notify" checked> Send me notifications</label>
<p class="help">To this address, when something needs you: a DNS record of a domain stops matching, a rate limit refuses mail, the TLS certificate is about to expire, a message has waited in the queue for three days. <a href="help.html#notifications">What is sent and when</a></p></div>
<button type="submit" class="button is-primary">Save profile</button> <button type="submit" class="button is-primary">Save profile</button>
</form> </form>
</div> </div>
@@ -79,10 +79,29 @@
</div> </div>
</div> </div>
<div class="box" id="configuration"> <div class="columns">
<div class="sp-box-head"><span class="sp-no">06</span><h2>Configuration</h2><div class="sp-end">Safe to run at any time</div></div> <div class="column is-6">
<div class="sp-box-body"> <div class="box" id="notification-mail">
<p class="mb-3">Regenerates the OpenDKIM and Postfix configuration from the database and reloads both daemons. Use it after editing the files by hand, after restoring a backup, or when the running configuration looks out of step with the domain and application lists. It does not touch the queue or the TLS certificate.</p> <div class="sp-box-head"><span class="sp-no">06</span><h2>Notification mail</h2><div class="sp-end"><span class="tag is-warning is-light">warn</span></div><a class="sp-help" href="help.html#notifications" title="What the panel sends and what makes it arrive"><i class="ti ti-help-circle"></i></a></div>
<form method="post" action="#"><button type="submit" class="button"><i class="ti ti-reload"></i>Reload configuration</button></form> <div class="table-container"><table class="table is-fullwidth">
<thead><tr><th>Record</th><th>State</th></tr></thead>
<tbody>
<tr><td>DKIM<br><span class="sp-muted sp-mono sp-small">sp2026._domainkey.mail.example.org</span></td><td><span class="tag is-success is-light">published</span></td></tr>
<tr><td>SPF<br><span class="sp-muted sp-mono sp-small">mail.example.org</span></td><td><span class="tag is-warning is-light">missing</span></td></tr>
</tbody>
</table></div>
<div class="sp-box-body">
<p class="sp-muted">Sent from <span class="sp-mono">postmaster@mail.example.org</span>. What to publish is under <a href="settings.html#notification-mail">Settings</a>.</p>
</div>
</div>
</div>
<div class="column is-6">
<div class="box" id="configuration">
<div class="sp-box-head"><span class="sp-no">07</span><h2>Configuration</h2><div class="sp-end">Safe to run at any time</div></div>
<div class="sp-box-body">
<p class="mb-3">Regenerates the OpenDKIM and Postfix configuration from the database and reloads both daemons. Use it after editing the files by hand, after restoring a backup, or when the running configuration looks out of step with the domain and application lists. It does not touch the queue or the TLS certificate.</p>
<form method="post" action="#"><button type="submit" class="button"><i class="ti ti-reload"></i>Reload configuration</button></form>
</div>
</div>
</div> </div>
</div> </div>
@@ -39,6 +39,7 @@
<li><a href="#backup"><i class="ti ti-archive"></i>Backup and moving a domain</a></li> <li><a href="#backup"><i class="ti ti-archive"></i>Backup and moving a domain</a></li>
<li><a href="#users"><i class="ti ti-users"></i>Users and roles</a></li> <li><a href="#users"><i class="ti ti-users"></i>Users and roles</a></li>
<li><a href="#settings"><i class="ti ti-settings"></i>Settings</a></li> <li><a href="#settings"><i class="ti ti-settings"></i>Settings</a></li>
<li><a href="#notifications"><i class="ti ti-mail"></i>Notifications</a></li>
</ul> </ul>
<p class="menu-label">Account</p> <p class="menu-label">Account</p>
<ul class="menu-list"> <ul class="menu-list">
@@ -75,11 +76,12 @@
<div class="sp-box-head"><span class="sp-no">04</span><h2>Server</h2></div> <div class="sp-box-head"><span class="sp-no">04</span><h2>Server</h2></div>
<div class="sp-help-topic" id="backup"><h3>Backup and moving a domain</h3><p>Full backup downloads one archive of the whole instance: <code>data/</code> (the database, DKIM keys and application credentials), <code>docker-compose.yml</code>, <code>.env</code> and <code>certs/</code>. The Postfix queue is not in it. The backup is taken while the server is running, and Postfix keeps moving messages through the queue, so a copy could hold a message twice or miss it; mail still waiting for delivery is therefore not carried over, and a restored server starts with an empty queue. To keep the queue, stop the container and archive the project directory instead. The private key in <code>certs/</code> has to be readable by the panel: give it to group 998 on the host (<code>chgrp 998</code>, <code>chmod 640</code>). Until then the panel refuses to make a backup and names the file, rather than hand over an archive without the key. It holds secrets, so leave Encrypt with a password ticked: the file is sealed as <code>.spbk</code>, the password is not stored, and without it nothing can be recovered. To restore, unpack it into an empty project directory (decrypt a <code>.spbk</code> first with <code>selfpost-backup -decrypt</code>) and start the same SelfPost version that made it; the panel refuses data from another version. The reverse-proxy configuration and <code>mail.log</code> are not included. One domain moves on its own: Export domain on its settings page writes a <code>.json</code> (or encrypted <code>.spde</code>) with its DKIM key and its applications' passwords, and Import a domain here reads it into another instance, so the published DNS needs no change.</p></div> <div class="sp-help-topic" id="backup"><h3>Backup and moving a domain</h3><p>Full backup downloads one archive of the whole instance: <code>data/</code> (the database, DKIM keys and application credentials), <code>docker-compose.yml</code>, <code>.env</code> and <code>certs/</code>. The Postfix queue is not in it. The backup is taken while the server is running, and Postfix keeps moving messages through the queue, so a copy could hold a message twice or miss it; mail still waiting for delivery is therefore not carried over, and a restored server starts with an empty queue. To keep the queue, stop the container and archive the project directory instead. The private key in <code>certs/</code> has to be readable by the panel: give it to group 998 on the host (<code>chgrp 998</code>, <code>chmod 640</code>). Until then the panel refuses to make a backup and names the file, rather than hand over an archive without the key. It holds secrets, so leave Encrypt with a password ticked: the file is sealed as <code>.spbk</code>, the password is not stored, and without it nothing can be recovered. To restore, unpack it into an empty project directory (decrypt a <code>.spbk</code> first with <code>selfpost-backup -decrypt</code>) and start the same SelfPost version that made it; the panel refuses data from another version. The reverse-proxy configuration and <code>mail.log</code> are not included. One domain moves on its own: Export domain on its settings page writes a <code>.json</code> (or encrypted <code>.spde</code>) with its DKIM key and its applications' passwords, and Import a domain here reads it into another instance, so the published DNS needs no change.</p></div>
<div class="sp-help-topic" id="users"><h3>Users and roles</h3><p>The two roles are a reach, not a rank: every user is an administrator and signs in with a username and password. Global reaches the whole server — Overview, Server, the queue and every domain — and is the only role that adds or deletes domains and users. Domain reaches only the domains assigned to it, outbound and inbound separately (the same name on both lists is two assignments); <em>All</em> includes domains added later, and a domain user needs at least one domain or <em>All</em> on either list. The only global user cannot be demoted or deleted, and you cannot delete the account you are signed in with. A new password set here signs that user out of their other sessions.</p></div> <div class="sp-help-topic" id="users"><h3>Users and roles</h3><p>The two roles are a reach, not a rank: every user is an administrator and signs in with a username and password. Global reaches the whole server — Overview, Server, the queue and every domain — and is the only role that adds or deletes domains and users. Domain reaches only the domains assigned to it, outbound and inbound separately (the same name on both lists is two assignments); <em>All</em> includes domains added later, and a domain user needs at least one domain or <em>All</em> on either list. The only global user cannot be demoted or deleted, and you cannot delete the account you are signed in with. A new password set here signs that user out of their other sessions.</p></div>
<div class="sp-help-topic" id="settings"><h3>Settings</h3><p>Settings holds what is true of the whole instance. Log retention is how many days the Outbound log keeps its rows, from 7 to 365 and 90 by default; a sweep every six hours deletes older ones, and it is the main driver of how fast <code>/data</code> grows. With the inbound relay on there is a second number, for the Inbound log, with the same range. It does not change the rotation of <code>mail.log</code>, which is daily with about two weeks kept. The sending rate limit shown under it is level 1, the per-client-IP ceiling set in <code>.env</code>: changing it needs a restart, and no level-2 limit can exceed it.</p></div> <div class="sp-help-topic" id="settings"><h3>Settings</h3><p>Settings holds what is true of the whole instance. Log retention is how many days the Outbound log keeps its rows, from 7 to 365 and 90 by default; a sweep every six hours deletes older ones, and it is the main driver of how fast <code>/data</code> grows. With the inbound relay on there is a second number, for the Inbound log, with the same range. It does not change the rotation of <code>mail.log</code>, which is daily with about two weeks kept. The sending rate limit shown under it is level 1, the per-client-IP ceiling set in <code>.env</code>: changing it needs a restart, and no level-2 limit can exceed it. Notifications are turned on or off here for everyone.</p></div>
<div class="sp-help-topic" id="notifications"><h3>Notifications</h3><p>The panel writes to its users when something needs them, from <code>postmaster@</code> the server's hostname. Four things are reported. A sending domain's SPF, DKIM or DMARC record stops matching: sent once two hourly checks in a row have found it so, and once more when the record matches again. A level-2 rate limit refuses mail: sent at the first refusal, then at most once a day for the same limit. The TLS certificate is 14 days from expiry, and again at 3 days if it is still the same certificate. A message has waited in the queue for more than three days, with one more message when none is left. A global user is told all of it; a domain administrator is told about the DNS records and limits of their own domains.<br><br>Each user turns notifications on or off under Account and needs an e-mail there; Server › Settings turns them off for everyone. They are the panel's own mail: not rows of the Outbound log, and not counted against any limit.<br><br>For that mail to arrive, the server's own name needs two DNS records of its own, as each of your domains does: a DKIM key, which the panel generates and signs notifications with, and an SPF record, <code>v=spf1 a -all</code>. Server › Settings gives both with the values to publish, and Server › Health shows whether they are in place. Without them notifications are still sent, and a receiver may file them as spam. Mail of your domains does not depend on either.</p></div>
</div> </div>
<div class="box"> <div class="box">
<div class="sp-box-head"><span class="sp-no">05</span><h2>Account</h2></div> <div class="sp-box-head"><span class="sp-no">05</span><h2>Account</h2></div>
<div class="sp-help-topic" id="account"><h3>Your account</h3><p>Account is your own, whatever your role: your username and e-mail, and your password. Changing the password signs out every other session of this account and keeps the one you are using; applications have their own logins and are not affected.</p></div> <div class="sp-help-topic" id="account"><h3>Your account</h3><p>Account is your own, whatever your role: your username and e-mail, whether the panel sends you notifications, and your password. Changing the password signs out every other session of this account and keeps the one you are using; applications have their own logins and are not affected.</p></div>
</div> </div>
</div> </div>
</div> </div>
@@ -21,9 +21,7 @@
<p class="help">The inbound log also never holds more than 10 000 rows: refused mail is logged at the rate strangers knock on port 25.</p></div> <p class="help">The inbound log also never holds more than 10 000 rows: refused mail is logged at the rate strangers knock on port 25.</p></div>
</div> </div>
</div> </div>
</div>
<div class="column is-6">
<div class="box" id="rate-limits"> <div class="box" id="rate-limits">
<div class="sp-box-head"><span class="sp-no">02</span><h2>Sending rate limits</h2><div class="sp-end">Read-only</div><a class="sp-help" href="help.html#limits" title="Levels 1 and 2"><i class="ti ti-help-circle"></i></a></div> <div class="sp-box-head"><span class="sp-no">02</span><h2>Sending rate limits</h2><div class="sp-end">Read-only</div><a class="sp-help" href="help.html#limits" title="Levels 1 and 2"><i class="ti ti-help-circle"></i></a></div>
<div class="sp-box-body"> <div class="sp-box-body">
@@ -34,6 +32,43 @@
</div> </div>
</div> </div>
</div> </div>
<div class="column is-6">
<div class="box" id="notifications">
<div class="sp-box-head"><span class="sp-no">03</span><h2>Notifications</h2><a class="sp-help" href="help.html#notifications" title="What is sent and when"><i class="ti ti-help-circle"></i></a></div>
<div class="sp-box-body">
<div class="field"><label class="checkbox"><input type="checkbox" name="notify" checked> Send notification e-mails</label>
<p class="help">From <span class="sp-mono">postmaster@mail.example.org</span>, to every user who has an e-mail and has not turned them off under <a href="account.html">Account</a>.</p></div>
</div>
</div>
<div class="box" id="notification-mail">
<div class="sp-box-head"><span class="sp-no">04</span><h2>Notification mail</h2><div class="sp-end">DNS records of the server's own name</div><a class="sp-help" href="help.html#notifications" title="What the panel sends and what makes it arrive"><i class="ti ti-help-circle"></i></a></div>
<div class="sp-record">
<div class="sp-record-head"><h3>DKIM</h3><span class="tag is-success is-light">published</span><small>Not a secret · selector <code>sp2026</code></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="sp2026._domainkey.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"><textarea class="textarea" rows="5" readonly>v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAq7n2Lw0c5vXeR1dYt8uJb3mKs6pHf9aZoWg4iNy2TqCx0lVjE7rUh5sBk1dMa8fPz3oGc6yXw9tIe4nQv2uL…IDAQAB</textarea></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
</div>
<div class="sp-record">
<div class="sp-record-head"><h3>SPF</h3><span class="tag is-warning is-light">missing</span><small>For the server's own name, not for your domains</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="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=spf1 a -all"></div><div class="control"><button type="button" class="button"><i class="ti ti-copy"></i>Copy</button></div></div></div>
<p class="help">Notifications are sent without it, and a receiver may file them as spam.</p>
</div>
</div>
</div>
</div> </div>
<div class="buttons"><button type="submit" class="button is-primary">Save settings</button></div> <div class="buttons"><button type="submit" class="button is-primary">Save settings</button></div>
</form> </form>
+18 -5
View File
@@ -40,10 +40,23 @@ From `postmaster@<SELFPOST_HOSTNAME>`, submitted locally. Not from one of the
sending domains (owner): with several domains the choice is arbitrary, and a sending domains (owner): with several domains the choice is arbitrary, and a
message about the certificate has no business coming from a domain. message about the certificate has no business coming from a domain.
The server's name usually has no DKIM key. One DNS record is enough for such These messages have to arrive (owner: "we need the best deliverability for
mail to pass SPF and DMARC on the name itself: `v=spf1 a -all` for the them"), so the server's own name gets what a sending domain has:
hostname. Server › Health gains that check next to reverse DNS, and the guide
describes it. Without the record notifications are still sent. - a **DKIM key for the hostname**, generated by the panel, with which
notifications are signed - no domain has to be chosen for it. An SPF record
alone would pass DMARC, but not after the user's mailbox forwards the
message on, and large receivers treat unsigned mail more strictly;
- an **SPF record** for the hostname, `v=spf1 a -all`.
The records themselves are under **Server › Settings** (owner: "the DNS
records go to Settings, Health has only the status"), in a box of their own
drawn with the record component of a domain's DNS page: host, value to
publish, copy buttons, state. **Server › Health** has a card of its own,
*Notification mail*, with the state of each record and a link to Settings.
A DMARC record for the hostname is not asked for: the name inherits its
parent domain's policy, and with SPF and DKIM aligned it passes any.
Without the records notifications are still sent, and the card warns.
Notifications are the panel's own mail: they are not rows of the Outbound log Notifications are the panel's own mail: they are not rows of the Outbound log
and count against no level-2 limit. and count against no level-2 limit.
@@ -94,7 +107,7 @@ to the owner before any code ([panel-redesign.md](panel-redesign.md)
## Implementation checklist ## Implementation checklist
- [ ] Mockups: Account checkbox, Settings switch, Health SPF check, help topic; accepted by the owner (`design:` commit) - [x] Mockups: Account checkbox, Settings switch and records, Health status card, help topic; accepted by the owner 2026-10-12 (`design:` commit)
- [ ] Migration and store: user flag, instance switch, event state - [ ] Migration and store: user flag, instance switch, event state
- [ ] Sending: local submission from `postmaster@<hostname>`, message text - [ ] Sending: local submission from `postmaster@<hostname>`, message text
- [ ] Background checker: DNS, certificate, queue; the rules of *When a message is sent* - [ ] Background checker: DNS, certificate, queue; the rules of *When a message is sent*
+1 -1
View File
@@ -35,7 +35,7 @@ in `git log` and [CHANGELOG.md](../CHANGELOG.md).
| template-data-typing | Typed structs for template data instead of `map[string]any` | candidate | — | — | | template-data-typing | Typed structs for template data instead of `map[string]any` | candidate | — | — |
| structured-logging | `log/slog` with levels and fields | candidate | — | — | | structured-logging | `log/slog` with levels and fields | candidate | — | — |
| review-2026-08-followups | Remaining findings of the 2026-08-19 code review | candidate | — | — | | review-2026-08-followups | Remaining findings of the 2026-08-19 code review | candidate | — | — |
| panel-notifications | E-mail notifications about important events | agreed | 0/10 | [plans/panel-notifications.md](plans/panel-notifications.md) | | panel-notifications | E-mail notifications about important events | in progress | 1/10 | [plans/panel-notifications.md](plans/panel-notifications.md) |
| delivery-log-storage | Keep a message's `mail.log` lines with its send-log row | shipped `2.2.0` | 8/8 | [plans/delivery-log-storage.md](plans/delivery-log-storage.md) | | delivery-log-storage | Keep a message's `mail.log` lines with its send-log row | shipped `2.2.0` | 8/8 | [plans/delivery-log-storage.md](plans/delivery-log-storage.md) |
| password-reset | Password reset by e-mail | candidate | — | — | | password-reset | Password reset by e-mail | candidate | — | — |
| phone-desktop-mode | A phone browser in "desktop site" mode gets the phone navigation | candidate | — | — | | phone-desktop-mode | A phone browser in "desktop site" mode gets the phone navigation | candidate | — | — |