The owner, 2026-10-16: "Да, бери notification-mail-recheck". The records of the server's own name can be checked again where they are shown: Re- check DNS in Settings' page head (mockup98e4dfc) posts to POST /server/settings/dns-recheck (guard52a9811), global role, which runs Health's lookups - the server's address, then serverMail(true) - and returns to /server/settings?rechecked=1, "DNS re-checked.". The Notification mail box's end slot says when the records were checked ("The server's own name · checked 3 min ago", from the check's CheckedAt; the old wording while nothing was looked up). A record that has turned right comes back folded. Tests: the page head's form and the end slot, and the Settings form holding neither the re-check nor the right column (TestSettingsRechecksTheMailRecords); the re-check skips the cache, redirects and the page shows both records folded (TestSettingsRecheckAsksForNotificationMailAgain); a domain administrator gets 404 (globalOnlyRoutes). go vet + go test (26 packages) and make e2e on the test server for this tree. Docs: architecture.md routes, the panel-redesign plan's route table, guide § Settings, help (help.go word for word with the mockup's); CHANGELOG [Unreleased]; roadmap row done. The roadmap's settings-save-row note no longer claims the left-column form was needed for this - the action went to the page head. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -22,6 +22,15 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
|
||||
the button stood under both columns, after a long gap whenever the
|
||||
records of the server's own name were open.
|
||||
|
||||
### Added
|
||||
|
||||
- **Re-check DNS on Server › Settings** (notification-mail-recheck): the
|
||||
records of the server's own name can be checked again where they are
|
||||
shown, as a domain's can on its page; the Notification mail box says when
|
||||
they were checked. Before, the only way was Re-check DNS on Server ›
|
||||
Health, which nothing on Settings pointed to. `POST
|
||||
/server/settings/dns-recheck`, global role.
|
||||
|
||||
## [2.8.0] - 2026-10-16
|
||||
|
||||
The panel signs outgoing mail with DKIM itself, and OpenDKIM - a 2018 beta
|
||||
|
||||
@@ -797,7 +797,7 @@ only reads (the delete confirmation at `GET …/delete` renders a form).
|
||||
| Outbound › DMARC reports | `GET /outbound/dmarc` · `…/dmarc/domains/{id}` · `…/dmarc/reports/{id}` — registered only with `DMARC_REPORTS_ENABLE=true` |
|
||||
| Inbound › Domains | `GET, POST /inbound/domains` · `/inbound/domains/{id}` · `POST …/dns-recheck`, `…/upstream`, `…/recipients` · `GET, POST …/delete` — registered only with `INBOUND_RELAY_ENABLE=true` |
|
||||
| Inbound › Log, Filter lists | `GET /inbound/log` · `GET, POST /inbound/filter-lists` · `POST /inbound/filter-lists/{id}/delete` — registered only with `INBOUND_RELAY_ENABLE=true` |
|
||||
| Server | `GET /server/health` (+ `/fragment`) · `POST /server/health/recheck`, `/server/health/reload` · `GET /server/log` (+ `/fragment`) · `GET, POST /server/backup` · `POST /server/backup/import` · `/server/users`, `…/new`, `…/{uid}`, `…/{uid}/delete` · `GET, POST /server/settings` · `GET /server/components` |
|
||||
| Server | `GET /server/health` (+ `/fragment`) · `POST /server/health/recheck`, `/server/health/reload` · `GET /server/log` (+ `/fragment`) · `GET, POST /server/backup` · `POST /server/backup/import` · `/server/users`, `…/new`, `…/{uid}`, `…/{uid}/delete` · `GET, POST /server/settings` · `POST /server/settings/dns-recheck` · `GET /server/components` |
|
||||
| Account | `GET /account` · `POST /account/profile`, `/account/password` |
|
||||
| User menu | `GET /help` · `POST /logout` (public mux) |
|
||||
|
||||
|
||||
+3
-1
@@ -551,7 +551,9 @@ prune cycle without a container restart), and, read-only, the level-1 rate
|
||||
limit that comes from `.env` ([Rate limiting — level 1](#rate-limiting--level-1-ip-backstop)).
|
||||
There is no panel-wide DMARC report address: each domain has its own. The page
|
||||
also has the switch for [notifications](#notifications) and the two DNS records
|
||||
that make them arrive.
|
||||
that make them arrive, with when they were last checked; **Re-check DNS** at
|
||||
the top of the page checks them again once they are published (results are
|
||||
otherwise kept a few minutes).
|
||||
|
||||
### Notifications
|
||||
|
||||
|
||||
@@ -182,7 +182,7 @@ 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`: log retention, rate limits |
|
||||
| settings | `GET, POST /server/settings` · `POST …/dns-recheck` | the instance half of `/settings`: log retention, rate limits |
|
||||
| account | `GET /account` · `POST /account/profile`, `/password` | `/settings`: credentials and the DMARC default (today `/account` redirects there) |
|
||||
| help · components | `GET /help` · `GET /server/components` | `/help` · — |
|
||||
|
||||
|
||||
+11
-4
@@ -38,7 +38,7 @@ in `git log` and [CHANGELOG.md](../CHANGELOG.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 | agreed (2026-10-13) | — | — |
|
||||
| local-time | The panel shows time, and counts its days, in a time zone other than UTC | agreed (2026-10-13; design questions open) | — | — |
|
||||
| notification-mail-recheck | The Notification mail records on Server › Settings can be checked again from where they are shown | agreed (2026-10-15) | — | — |
|
||||
| notification-mail-recheck | The Notification mail records on Server › Settings can be checked again from where they are shown | done 2026-10-16, unreleased | — | — |
|
||||
| settings-save-row | Server › Settings: the Save button no longer stands alone under a long right column | done 2026-10-16, unreleased | — | — |
|
||||
| dns-record-collapse | A DNS record that is right is shown folded: its name and its tag, the values on request | done 2026-10-16, unreleased | — | — |
|
||||
| go-milter-upstream | Prepare an issue and a PR for go-milter: its header map outlives a message and is unbounded | prepared 2026-10-16, the owner posts | — | [third_party/go-milter/PATCHES.md](../third_party/go-milter/PATCHES.md) |
|
||||
@@ -756,6 +756,15 @@ the check is and how long a result is kept. Design change, mockup first;
|
||||
decide together with [dns-record-collapse](#dns-record-collapse) (a record
|
||||
that turns right on the re-check folds).
|
||||
|
||||
**Done (2026-10-16, the owner: "Да, бери notification-mail-recheck"):**
|
||||
*Re-check DNS* in the page head of Settings - where an outbound domain's
|
||||
page and Health put the same action; a box head holds a note, a tag, a
|
||||
link or a filter form - posting to `POST /server/settings/dns-recheck`
|
||||
(global role), which re-runs Health's lookups (the server's address and
|
||||
`serverMail(true)`) and returns to Settings with "DNS re-checked."; the
|
||||
Notification mail box's end slot reads "The server's own name · checked
|
||||
3 min ago". A record that has turned right comes back folded.
|
||||
|
||||
## own-mta-builds
|
||||
|
||||
**Question (owner, 2026-10-15):** is it worth patching Postfix and OpenDKIM
|
||||
@@ -892,9 +901,7 @@ on one whose records are right.
|
||||
closed the gap only while the records are right. The form is now the left
|
||||
column - 01 Log retention, 02 Notifications, *Save settings* - and the
|
||||
right column, outside it, holds 03 Notification mail and 04 Sending rate
|
||||
limits. It also leaves room for
|
||||
[notification-mail-recheck](#notification-mail-recheck): a *Re-check DNS*
|
||||
form in the Notification mail box is no longer a form inside a form.
|
||||
limits.
|
||||
|
||||
## go-milter-upstream
|
||||
|
||||
|
||||
@@ -43,6 +43,7 @@ var globalOnlyRoutes = []route{
|
||||
{"GET", "/server/health/fragment", func(h *Handlers) http.HandlerFunc { return h.HandleHealthFragment }, nil},
|
||||
{"GET", "/overview/fragment", func(h *Handlers) http.HandlerFunc { return h.HandleOverviewFragment }, nil},
|
||||
{"POST", "/server/health/recheck", func(h *Handlers) http.HandlerFunc { return h.HandleHealthRecheck }, nil},
|
||||
{"POST", "/server/settings/dns-recheck", func(h *Handlers) http.HandlerFunc { return h.HandleSettingsDNSRecheck }, nil},
|
||||
|
||||
{"GET", "/outbound/queue", func(h *Handlers) http.HandlerFunc { return h.HandleMailQueue }, nil},
|
||||
{"GET", "/outbound/queue/fragment", func(h *Handlers) http.HandlerFunc { return h.HandleMailQueueBody }, nil},
|
||||
|
||||
@@ -176,7 +176,7 @@ func TestSettingsMailRecordsPublished(t *testing.T) {
|
||||
|
||||
out := getBody(t, h.HandleServerSettings, "/server/settings")
|
||||
for _, want := range []string{
|
||||
`id="notification-mail"`, `DNS records of the server's own name`,
|
||||
`id="notification-mail"`, `The server's own name · checked just now`,
|
||||
`<h3>DKIM</h3><span class="tag is-success is-light">published</span>`,
|
||||
`selector <code>sp2026</code>`, `value="sp2026._domainkey.mail.example.org"`,
|
||||
`<h3>SPF</h3><span class="tag is-success is-light">published</span>`,
|
||||
@@ -313,3 +313,29 @@ func TestHealthRecheckAsksForNotificationMailAgain(t *testing.T) {
|
||||
t.Errorf("after Re-check DNS both records should be published:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
// Re-check DNS on Settings asks for the same records again and returns to
|
||||
// Settings, where a record that has just been published comes back folded.
|
||||
func TestSettingsRecheckAsksForNotificationMailAgain(t *testing.T) {
|
||||
h, zone, mail := serverMailStand(t)
|
||||
if out := getBody(t, h.HandleServerSettings, "/server/settings"); strings.Count(out, `is-warning is-light">missing`) != 2 {
|
||||
t.Fatalf("before publishing, both records should be missing:\n%s", out)
|
||||
}
|
||||
|
||||
publishMail(zone, mail)
|
||||
if out := getBody(t, h.HandleServerSettings, "/server/settings"); strings.Contains(out, `is-success is-light">published`) {
|
||||
t.Fatal("the cached answer was not used: the test cannot tell a re-check from a fresh read")
|
||||
}
|
||||
|
||||
rec := postFormAs(h.HandleSettingsDNSRecheck, globalPrincipal, "/server/settings/dns-recheck", nil, url.Values{})
|
||||
if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/server/settings?rechecked=1" {
|
||||
t.Fatalf("POST /server/settings/dns-recheck = %d to %q, want 303 to /server/settings?rechecked=1", rec.Code, rec.Header().Get("Location"))
|
||||
}
|
||||
out := getBody(t, h.HandleServerSettings, "/server/settings?rechecked=1")
|
||||
if n := strings.Count(out, `<details class="sp-record">`); n != 2 {
|
||||
t.Errorf("after Re-check DNS both records should be published and folded, %d are:\n%s", n, out)
|
||||
}
|
||||
if !strings.Contains(out, "DNS re-checked.") || !strings.Contains(out, "· checked just now") {
|
||||
t.Errorf("Settings does not say the records were just re-checked:\n%s", out)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/mixeme/selfpost/internal/dnscheck"
|
||||
"github.com/mixeme/selfpost/internal/health"
|
||||
@@ -57,14 +58,18 @@ func (h *Handlers) renderServerSettings(w http.ResponseWriter, r *http.Request,
|
||||
on = true
|
||||
}
|
||||
page.WithNotifications(on, notify.From(h.cfg.Hostname))
|
||||
page.WithMail(h.serverMail(false).records)
|
||||
mail := h.serverMail(false)
|
||||
page.WithMail(mail.records, view.FormatAge(mail.checked, time.Now()))
|
||||
// The inbound field exists while there is an inbound journal to keep.
|
||||
if h.cfg.InboundEnabled {
|
||||
page.WithInbound(strconv.Itoa(form.inbound))
|
||||
}
|
||||
flash := ""
|
||||
if r.URL.Query().Has("saved") {
|
||||
switch {
|
||||
case r.URL.Query().Has("saved"):
|
||||
flash = "Settings saved."
|
||||
case r.URL.Query().Has("rechecked"):
|
||||
flash = "DNS re-checked."
|
||||
}
|
||||
page.WithResult(flash, formErr)
|
||||
h.view.Render(w, status, "settings", page)
|
||||
@@ -147,6 +152,8 @@ func (h *Handlers) sendLogRetentionDays() int {
|
||||
type serverMailState struct {
|
||||
records []view.Record
|
||||
rows []view.MailRow
|
||||
// checked is when the records were looked up; zero when they were not.
|
||||
checked time.Time
|
||||
// status is the verdict of Health's card: ok, warn or unknown. A record
|
||||
// that is not in place is a warning, never an error: without them the
|
||||
// notifications are still sent.
|
||||
@@ -183,5 +190,19 @@ func (h *Handlers) serverMail(force bool) serverMailState {
|
||||
records: []view.Record{view.ServerMailDKIM(mail.Selector, mail.Record.Name, mail.Record.Value, dkim), spfRec},
|
||||
rows: []view.MailRow{view.NewMailRow("DKIM", mail.Record.Name, dkim), spfRow},
|
||||
status: string(status),
|
||||
checked: check.CheckedAt,
|
||||
}
|
||||
}
|
||||
|
||||
// HandleSettingsDNSRecheck re-checks the records of the server's own name
|
||||
// from Server › Settings, where they are shown, and returns there (global
|
||||
// role). The server's address is looked up again too: the SPF check reads it.
|
||||
// The same lookups as Re-check DNS on Health.
|
||||
func (h *Handlers) HandleSettingsDNSRecheck(w http.ResponseWriter, r *http.Request) {
|
||||
if _, ok := h.requireGlobal(w, r); !ok {
|
||||
return
|
||||
}
|
||||
h.dns.Server(h.cfg.Hostname, true)
|
||||
h.serverMail(true)
|
||||
http.Redirect(w, r, "/server/settings?rechecked=1", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
@@ -92,6 +92,7 @@ func NewSettings(m Meta, retention, level1 string) *Settings {
|
||||
Title: "Settings",
|
||||
Lead: Rich("What holds for the whole instance, whoever is signed in. Your own name, e-mail and password are under ",
|
||||
Link("/account", "Account"), "."),
|
||||
Actions: []Action{{Label: "Re-check DNS", Icon: "ti-refresh", Post: settingsDNSRecheck}},
|
||||
},
|
||||
Retention: Box{No: "01", Title: "Log retention", Help: topicLink(HelpSettings, "How long rows are kept")},
|
||||
RetentionDays: retention,
|
||||
@@ -117,10 +118,18 @@ func (s *Settings) WithNotifications(on bool, from string) *Settings {
|
||||
return s
|
||||
}
|
||||
|
||||
// settingsDNSRecheck is where Re-check DNS on Settings posts.
|
||||
const settingsDNSRecheck = "/server/settings/dns-recheck"
|
||||
|
||||
// WithMail sets the records of the server's own name, in the order they are
|
||||
// shown (ServerMailDKIM, ServerMailSPF).
|
||||
func (s *Settings) WithMail(records []Record) *Settings {
|
||||
// shown (ServerMailDKIM, ServerMailSPF), and how long ago they were checked
|
||||
// (FormatAge; "" when they were not).
|
||||
func (s *Settings) WithMail(records []Record, checked string) *Settings {
|
||||
s.MailRecords = records
|
||||
s.Mail.End = Plain("DNS records of the server's own name")
|
||||
if checked != "" {
|
||||
s.Mail.End = Plain("The server's own name · checked " + checked)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
|
||||
@@ -158,7 +158,7 @@ func settingsFixture() *Settings {
|
||||
WithMail([]Record{
|
||||
ServerMailDKIM("sp2026", mailFixtureDKIMHost, key, DNSCheck{Status: "ok"}),
|
||||
ServerMailSPF("mail.example.org", "v=spf1 a -all", DNSCheck{Status: "error", Detail: "No SPF record is published for mail.example.org."}),
|
||||
})
|
||||
}, "3 min ago")
|
||||
}
|
||||
|
||||
// outDomainsFixture is the mockup's Outbound domains: three domains, the first
|
||||
|
||||
@@ -256,7 +256,7 @@ var serverTopics = []HelpTopic{
|
||||
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"), ". 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."),
|
||||
"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"), ". Server › Settings gives both with the values to publish and whether they are in place, and its Re-check DNS checks them again once they are published; Server › Health shows them too. Without them notifications are still sent, and a receiver may file them as spam. Mail of your domains does not depend on either."),
|
||||
}},
|
||||
}
|
||||
|
||||
|
||||
@@ -368,11 +368,32 @@ func TestSettingsNotificationsSwitchAndRecords(t *testing.T) {
|
||||
s.WithMail([]Record{
|
||||
ServerMailDKIM("sp2026", mailFixtureDKIMHost, "v=DKIM1; k=rsa; p=AAAA", DNSCheck{Status: "ok"}),
|
||||
ServerMailSPF("mail.example.org", "v=spf1 a -all", DNSCheck{Status: "ok"}),
|
||||
})
|
||||
}, "")
|
||||
out = renderSignedIn(t, "settings", s)
|
||||
pageHas(t, "Settings", out, `<input type="checkbox" name="notify"> Send notification e-mails`,
|
||||
`<h3>SPF</h3><span class="tag is-success is-light">published</span>`)
|
||||
pageLacks(t, "Settings", out, `name="notify" checked`, `a receiver may file them as spam`)
|
||||
`<h3>SPF</h3><span class="tag is-success is-light">published</span>`,
|
||||
`<div class="sp-end">DNS records of the server's own name</div>`)
|
||||
pageLacks(t, "Settings", out, `name="notify" checked`, `a receiver may file them as spam`, `· checked`)
|
||||
}
|
||||
|
||||
// The records of the server's own name can be checked again from Settings,
|
||||
// where they are shown: Re-check DNS in the page head, outside the form, and
|
||||
// the box says when they were checked.
|
||||
func TestSettingsRechecksTheMailRecords(t *testing.T) {
|
||||
out := renderSignedIn(t, "settings", settingsFixture())
|
||||
pageHas(t, "Settings", out,
|
||||
`<div class="buttons"><form method="post" action="/server/settings/dns-recheck"><button type="submit" class="button"><i class="ti ti-refresh"></i>Re-check DNS</button></form></div>`,
|
||||
`<div class="sp-end">The server's own name · checked 3 min ago</div>`)
|
||||
form := out[strings.Index(out, `<form class="sp-form" method="post" action="/server/settings">`):]
|
||||
form = form[:strings.Index(form, "</form>")]
|
||||
for _, outside := range []string{"dns-recheck", `id="notification-mail"`, `id="rate-limits"`} {
|
||||
if strings.Contains(form, outside) {
|
||||
t.Errorf("the Settings form holds %s; only the fields and Save settings belong in it", outside)
|
||||
}
|
||||
}
|
||||
if !strings.Contains(form, `name="notify"`) || !strings.Contains(form, `Save settings`) {
|
||||
t.Errorf("the Settings form lost its switch or its button:\n%s", form)
|
||||
}
|
||||
}
|
||||
|
||||
// A record of the server's own name that is wrong or missing is a warning, never
|
||||
|
||||
@@ -294,6 +294,7 @@ func (s *Server) muxes() (public, authed *http.ServeMux) {
|
||||
server("POST /server/users/{uid}/delete", h.HandleUserDelete)
|
||||
server("GET /server/settings", h.HandleServerSettings)
|
||||
server("POST /server/settings", h.HandleServerSettings)
|
||||
server("POST /server/settings/dns-recheck", h.HandleSettingsDNSRecheck)
|
||||
server("GET /server/components", h.HandleComponents)
|
||||
|
||||
return mux, authed
|
||||
|
||||
Reference in New Issue
Block a user