The panel's signer is the only one. Gone: the opendkim package and its
configuration, the syslog-relay and opendkim programs, /dev/log and
/run/opendkim, the reload by USR1 through supervisord
(internal/supervisor/supervisor.go), internal/syslogrelay, the port of
OpenDKIM's From: parser (internal/milter/mailparse.go and its probe,
harness and test/upstream scripts), OPENDKIM_SOCKET and OPENDKIM_DIR.
Keys: internal/domain/keys.go (Keys, NewKeys, DKIM_DIR=/data/dkim) keeps
the keys and writes a selector table, /data/dkim/selectors ("<name>
<selector>"), which the signer reads in place of the KeyTable - it reads
no database, as the sender check reads none. The table is written before
the signer serves (EnsureServerSigned moved there from the notifier).
Keys and table 0600, directories 0700, the panel's. The entrypoint copies
/data/opendkim/keys once, as the panel user, into /data/dkim.new renamed
when whole, and stops the start if the copy fails; /data/opendkim is left
for a rollback to 2.7.x.
Sender check: one RFC 5322 address as a mail program reads it, held
against the bindings; the 256-byte refusal and the texts naming OpenDKIM
go. Allowed() folded the domain through strings.ToLower, which makes the
Kelvin sign a "k" - the comparison with OpenDKIM's reading had hidden it;
it folds ASCII only now.
Health: two dkim-signer rows (applications, notifications) in Milter
sockets, opendkim out of the liveness set; the card no longer takes any
row with a note for the spam filter. Texts as in the design commit.
KILL stays in deploy/docker-compose.yml: measured on the test server,
root without it cannot signal a process of another uid - supervisord
stopping the panel, Postfix's master its children. The plan's "only use
was the reload" was wrong; the comment now gives the real uses.
e2e: one signature, the panel's, verified by dkimpy; the /data/dkim modes;
the OpenDKIM log and process checks, testNeverSignedForAnother and the
cited opendkim, libopendkim11 and libmilter1.0.1 go.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -29,7 +29,8 @@ type Config struct {
|
||||
TLSCertFile string
|
||||
TLSServedCertFile string
|
||||
TLSReloadInterval time.Duration
|
||||
OpenDKIMSocket string
|
||||
SignSocket string
|
||||
SignLocalSocket string
|
||||
JournalSocket string
|
||||
SenderSocket string
|
||||
// Level-1 Postfix anvil backstop (env RATE_LIMIT_*), shown in the panel
|
||||
|
||||
@@ -56,11 +56,11 @@ func newAppStandAt(t *testing.T, dbPath string) appStand {
|
||||
// The domain page is rendered with the domain's DKIM record and its DNS
|
||||
// state: a real key in a temporary directory, and a resolver nobody
|
||||
// listens on, so every check answers at once without leaving the machine.
|
||||
odk := domain.NewOpenDKIM(t.TempDir())
|
||||
if _, err := odk.EnsureKey("example.org", "mail"); err != nil {
|
||||
keys := domain.NewKeys(t.TempDir())
|
||||
if _, err := keys.EnsureKey("example.org", "mail"); err != nil {
|
||||
t.Fatalf("EnsureKey: %v", err)
|
||||
}
|
||||
h.domains = domain.NewService(h.store, odk, h.apps, "mail")
|
||||
h.domains = domain.NewService(h.store, keys, h.apps, "mail")
|
||||
h.dns = offlineDNS
|
||||
h.cfg.RateLimitMessagesPerIP, h.cfg.RateLimitWindowSeconds = 600, 3600
|
||||
var err error
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -81,13 +83,11 @@ func contains(list []string, want string) bool {
|
||||
// and its DMARC record says so.
|
||||
func TestNewDomainHasNoReports(t *testing.T) {
|
||||
s, _ := hostedStand(t)
|
||||
// A second OpenDKIM that reloads nothing: the stand's has no supervisor.
|
||||
odk := domain.NewOpenDKIM(t.TempDir())
|
||||
odk.SetReloadHook(func() error { return nil })
|
||||
if _, err := odk.EnsureKey("example.org", "mail"); err != nil {
|
||||
keys := domain.NewKeys(t.TempDir())
|
||||
if _, err := keys.EnsureKey("example.org", "mail"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.h.domains = domain.NewService(s.h.store, odk, s.h.apps, "mail")
|
||||
s.h.domains = domain.NewService(s.h.store, keys, s.h.apps, "mail")
|
||||
rec := postFormAs(s.h.HandleAddDomain, globalPrincipal, "/outbound/domains", nil, url.Values{"name": {"new.example"}})
|
||||
if rec.Code != http.StatusSeeOther {
|
||||
t.Fatalf("adding a domain = %d:\n%s", rec.Code, rec.Body.String())
|
||||
@@ -309,11 +309,9 @@ func (failingDMARCMaps) RebuildDMARCMaps(postfix.DMARCMapsConfig) error {
|
||||
// not a plain-text page (audit 2026-10-12, L13).
|
||||
func TestReloadWritesTheDMARCMapsAndSaysWhereItFailed(t *testing.T) {
|
||||
s, maps := hostedStand(t)
|
||||
// An OpenDKIM that reloads nothing - the stand has no supervisor - and
|
||||
// a sampler for the Health page a failure is shown on.
|
||||
odk := domain.NewOpenDKIM(t.TempDir())
|
||||
odk.SetReloadHook(func() error { return nil })
|
||||
s.h.domains = domain.NewService(s.h.store, odk, s.h.apps, "mail")
|
||||
// Keys of its own, and a sampler for the Health page a failure is shown
|
||||
// on.
|
||||
s.h.domains = domain.NewService(s.h.store, domain.NewKeys(t.TempDir()), s.h.apps, "mail")
|
||||
s.h.machine = &health.MachineSampler{}
|
||||
rec := postFormAs(s.h.HandleReload, globalPrincipal, "/server/health/reload", nil, url.Values{})
|
||||
if rec.Code != http.StatusSeeOther || rec.Header().Get("Location") != "/server/health?reloaded=1" {
|
||||
@@ -335,14 +333,20 @@ func TestReloadWritesTheDMARCMapsAndSaysWhereItFailed(t *testing.T) {
|
||||
// the way to finish, not "internal error" (audit 2026-10-12, L17).
|
||||
func TestDeleteDomainSaysItIsGoneWhenARebuildFails(t *testing.T) {
|
||||
s, _ := hostedStand(t)
|
||||
odk := domain.NewOpenDKIM(t.TempDir())
|
||||
odk.SetReloadHook(func() error { return nil })
|
||||
s.h.domains = domain.NewService(s.h.store, odk, s.h.apps, "mail")
|
||||
dir := t.TempDir()
|
||||
s.h.domains = domain.NewService(s.h.store, domain.NewKeys(dir), s.h.apps, "mail")
|
||||
d, err := s.h.domains.Add("gone.example")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
odk.SetReloadHook(func() error { return errors.New("supervisor away") })
|
||||
// A directory where the selector table goes: it cannot be rewritten.
|
||||
table := filepath.Join(dir, "selectors")
|
||||
if err := os.Remove(table); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(table, "x"), 0o700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
rec := postFormAs(s.h.HandleDeleteDomain, globalPrincipal, "/outbound/domains/x/delete", map[string]string{"id": strconv.FormatInt(d.ID, 10)}, url.Values{})
|
||||
if rec.Code != http.StatusInternalServerError {
|
||||
t.Fatalf("delete with a failing rebuild = %d", rec.Code)
|
||||
|
||||
@@ -85,7 +85,7 @@ func dashboardFlash(r *http.Request) string {
|
||||
}
|
||||
|
||||
// HandleAddDomain validates the submitted name, creates the domain (DKIM key +
|
||||
// OpenDKIM reload), and redirects to the domain's page so the DNS record to
|
||||
// selector table), and redirects to the domain's page so the DNS record to
|
||||
// publish is shown (product.md).
|
||||
func (h *Handlers) HandleAddDomain(w http.ResponseWriter, r *http.Request) {
|
||||
_, ok := h.requireGlobal(w, r)
|
||||
@@ -171,9 +171,10 @@ func (h *Handlers) HandleDeleteDomain(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
// HandleReload writes again, from the database, every file the panel keeps
|
||||
// for the mail path and has OpenDKIM and Postfix re-read them - what the
|
||||
// first start after a restore does (cmd/panel mailStack.Resync): OpenDKIM's
|
||||
// tables, the sender map, with the inbound relay on its maps and the filter
|
||||
// for the mail path and has Postfix re-read them - what the first start after
|
||||
// a restore does (cmd/panel mailStack.Resync): the DKIM selector table (and
|
||||
// the keys of deleted domains removed), the sender map, with the inbound
|
||||
// relay on its maps and the filter
|
||||
// lists, with DMARC reports on their maps (architecture.md § Panel HTTP
|
||||
// surface). Before 2.5.4 it left the last two out, and said "regenerated"
|
||||
// over them (audit 2026-10-12, L13). A failure is a message on Health that
|
||||
@@ -187,7 +188,7 @@ func (h *Handlers) HandleReload(w http.ResponseWriter, r *http.Request) {
|
||||
run func() error
|
||||
}
|
||||
steps := []step{
|
||||
{"OpenDKIM's tables", h.domains.Resync},
|
||||
{"the DKIM selector table", h.domains.Resync},
|
||||
{"the sender map", h.apps.Resync},
|
||||
}
|
||||
if h.cfg.InboundEnabled && h.inbound != nil {
|
||||
|
||||
@@ -16,17 +16,18 @@ import (
|
||||
// The inbound spam filter's socket is one of the milter sockets on Health and
|
||||
// counts in the Milter sockets card of Overview — and only when the filter is on.
|
||||
|
||||
// socketsServer is a server whose two Postfix milter sockets answer, so that
|
||||
// socketsServer is a server whose Postfix milter sockets answer, so that
|
||||
// every difference in the Milter sockets card comes from the filter.
|
||||
func socketsServer(t *testing.T) *Handlers {
|
||||
t.Helper()
|
||||
h, _ := settingsServer(t)
|
||||
h.machine = &health.MachineSampler{}
|
||||
dir := t.TempDir()
|
||||
h.cfg.OpenDKIMSocket = filepath.Join(dir, "opendkim.sock")
|
||||
h.cfg.SignSocket = filepath.Join(dir, "sign.sock")
|
||||
h.cfg.SignLocalSocket = filepath.Join(dir, "sign-local.sock")
|
||||
h.cfg.JournalSocket = filepath.Join(dir, "journal.sock")
|
||||
h.cfg.SenderSocket = filepath.Join(dir, "sender.sock")
|
||||
for _, path := range []string{h.cfg.OpenDKIMSocket, h.cfg.SenderSocket, h.cfg.JournalSocket} {
|
||||
for _, path := range []string{h.cfg.SignSocket, h.cfg.SignLocalSocket, h.cfg.SenderSocket, h.cfg.JournalSocket} {
|
||||
l, err := net.Listen("unix", path)
|
||||
if err != nil {
|
||||
t.Skipf("unix sockets unavailable here: %v", err)
|
||||
@@ -56,24 +57,24 @@ func TestFilterOffAddsNoSocket(t *testing.T) {
|
||||
h := socketsServer(t)
|
||||
h.cfg.InboundEnabled = true
|
||||
h.cfg.InboundAntispamMilter = "" // no filter attached
|
||||
if got := socketsOf(t, h); len(got) != 3 {
|
||||
t.Fatalf("%d sockets with the filter off, want 3: %+v", len(got), got)
|
||||
if got := socketsOf(t, h); len(got) != 4 {
|
||||
t.Fatalf("%d sockets with the filter off, want 4: %+v", len(got), got)
|
||||
}
|
||||
|
||||
// A milter address with the inbound relay off is not a filter either.
|
||||
h.cfg.InboundEnabled = false
|
||||
h.cfg.InboundAntispamMilter = "inet:127.0.0.1:1"
|
||||
got := socketsOf(t, h)
|
||||
if len(got) != 3 {
|
||||
t.Fatalf("%d sockets with the inbound relay off, want 3: %+v", len(got), got)
|
||||
if len(got) != 4 {
|
||||
t.Fatalf("%d sockets with the inbound relay off, want 4: %+v", len(got), got)
|
||||
}
|
||||
card := socketCard(t, h)
|
||||
if card.Value != "3 of 3 answering" || card.Sub != "OpenDKIM, sender-check, send-log" || card.Level != "" {
|
||||
if card.Value != "4 of 4 answering" || card.Sub != "dkim-signer, sender-check, send-log" || card.Level != "" {
|
||||
t.Errorf("card = %+v", card)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFilterOnAnsweringIsTheThirdSocket(t *testing.T) {
|
||||
func TestFilterOnAnsweringIsTheLastSocket(t *testing.T) {
|
||||
h := socketsServer(t)
|
||||
l, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
@@ -84,7 +85,7 @@ func TestFilterOnAnsweringIsTheThirdSocket(t *testing.T) {
|
||||
h.cfg.InboundAntispamMilter, h.cfg.InboundAntispamAction = "inet:"+l.Addr().String(), "tempfail"
|
||||
|
||||
got := socketsOf(t, h)
|
||||
if len(got) != 4 || got[3].Name != "Spam filter" || got[3].Note != "inbound" || got[3].Path != "inet:"+l.Addr().String() {
|
||||
if len(got) != 5 || got[4].Name != "Spam filter" || got[4].Note != "inbound" || got[4].Path != "inet:"+l.Addr().String() {
|
||||
t.Fatalf("sockets = %+v", got)
|
||||
}
|
||||
for _, s := range got {
|
||||
@@ -93,7 +94,7 @@ func TestFilterOnAnsweringIsTheThirdSocket(t *testing.T) {
|
||||
}
|
||||
}
|
||||
card := socketCard(t, h)
|
||||
if card.Value != "4 of 4 answering" || card.Sub != "incl. the spam filter" || card.Level != "" {
|
||||
if card.Value != "5 of 5 answering" || card.Sub != "incl. the spam filter" || card.Level != "" {
|
||||
t.Errorf("card = %+v", card)
|
||||
}
|
||||
|
||||
@@ -111,7 +112,7 @@ func TestFilterOnAnsweringIsTheThirdSocket(t *testing.T) {
|
||||
}
|
||||
|
||||
// A filter that does not answer is a socket that is missing: an error when
|
||||
// Postfix defers inbound mail while it is down (as for OpenDKIM), a warning when
|
||||
// Postfix defers inbound mail while it is down (as for the signer), a warning when
|
||||
// it accepts the mail unfiltered (as for the send log).
|
||||
func TestFilterDownIsAMissingSocket(t *testing.T) {
|
||||
h := socketsServer(t)
|
||||
@@ -133,10 +134,10 @@ func TestFilterDownIsAMissingSocket(t *testing.T) {
|
||||
} {
|
||||
h.cfg.InboundAntispamMilter, h.cfg.InboundAntispamAction = dead, tt.action
|
||||
got := socketsOf(t, h)
|
||||
if len(got) != 4 {
|
||||
t.Fatalf("action %q: %d sockets, want 4", tt.action, len(got))
|
||||
if len(got) != 5 {
|
||||
t.Fatalf("action %q: %d sockets, want 5", tt.action, len(got))
|
||||
}
|
||||
f := got[3]
|
||||
f := got[4]
|
||||
refused := "Connection to " + l.Addr().String() + " refused. " + tt.detail
|
||||
if f.Present || f.Status != tt.status || f.Detail != refused {
|
||||
t.Errorf("action %q: filter = %+v, want status %q, detail %q", tt.action, f, tt.status, refused)
|
||||
@@ -147,7 +148,7 @@ func TestFilterDownIsAMissingSocket(t *testing.T) {
|
||||
t.Errorf("action %q: SocketStatus = %q, want %q", tt.action, c.SocketStatus, tt.status)
|
||||
}
|
||||
card := socketCard(t, h)
|
||||
if card.Value != "3 of 4 answering" || card.Level != tt.level || !strings.HasSuffix(card.Sub, "Spam filter") {
|
||||
if card.Value != "4 of 5 answering" || card.Level != tt.level || !strings.HasSuffix(card.Sub, "Spam filter") {
|
||||
t.Errorf("action %q: card = %+v", tt.action, card)
|
||||
}
|
||||
|
||||
@@ -161,7 +162,7 @@ func TestFilterDownIsAMissingSocket(t *testing.T) {
|
||||
rec := httptest.NewRecorder()
|
||||
h.HandleOverviewFragment(rec, reqAs("/overview/fragment", globalPrincipal))
|
||||
page := rec.Body.String()
|
||||
for _, want := range []string{`>` + tt.stamp + `<`, `Milter sockets</a> (3 of 4 answering)`} {
|
||||
for _, want := range []string{`>` + tt.stamp + `<`, `Milter sockets</a> (4 of 5 answering)`} {
|
||||
if !strings.Contains(page, want) {
|
||||
t.Errorf("action %q: Overview is missing %q", tt.action, want)
|
||||
}
|
||||
@@ -201,3 +202,19 @@ func TestFilterAddressIsGlobalOnly(t *testing.T) {
|
||||
func reqAs(path string, p auth.Principal) *http.Request {
|
||||
return auth.RequestWithPrincipal(httptest.NewRequest("GET", path, nil), p)
|
||||
}
|
||||
|
||||
// The signer's two rows are one name on the card, and a row that does not
|
||||
// answer says which of the two it is.
|
||||
func TestSignerSocketsOnTheCard(t *testing.T) {
|
||||
h := socketsServer(t)
|
||||
h.cfg.SignLocalSocket = filepath.Join(t.TempDir(), "gone.sock")
|
||||
got := socketsOf(t, h)
|
||||
if got[0].Name != "dkim-signer" || got[0].Note != "applications" || !got[0].Present ||
|
||||
got[1].Name != "dkim-signer" || got[1].Note != "notifications" || got[1].Present || got[1].Status != health.StatusError {
|
||||
t.Fatalf("sockets = %+v", got)
|
||||
}
|
||||
card := socketCard(t, h)
|
||||
if card.Value != "3 of 4 answering" || card.Sub != "dkim-signer · notifications" || card.Level != "fail" {
|
||||
t.Errorf("card = %+v", card)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
func healthReading() healthReadings {
|
||||
c := checksFixture()
|
||||
c.Processes = []health.Process{
|
||||
{Name: "opendkim", State: "RUNNING", Detail: "pid 21", Status: health.StatusOK},
|
||||
{Name: "panel", State: "RUNNING", Detail: "pid 21", Status: health.StatusOK},
|
||||
{Name: "postfix", State: "FATAL", Detail: "exited too quickly", Status: health.StatusError},
|
||||
}
|
||||
c.Cert = health.Certificate{
|
||||
@@ -44,7 +44,7 @@ func healthReading() healthReadings {
|
||||
Status: health.StatusOK,
|
||||
}
|
||||
c.Sockets = []health.Socket{
|
||||
{Name: "OpenDKIM", Path: "/run/opendkim/opendkim.sock", Present: true, Status: health.StatusOK, Detail: "Listening"},
|
||||
{Name: "dkim-signer", Note: "applications", Path: "/run/selfpost/sign.sock", Present: true, Status: health.StatusOK, Detail: "Listening"},
|
||||
{Name: "send-log", Path: "/run/selfpost/journal.sock", Status: health.StatusWarn, Detail: "send-log socket missing: the log stops being written."},
|
||||
}
|
||||
c.SocketStatus = health.StatusWarn
|
||||
@@ -73,7 +73,7 @@ func TestHealthPageRendersEveryCheck(t *testing.T) {
|
||||
h, _ := settingsServer(t)
|
||||
out := renderHealth(t, h, healthReading(), "")
|
||||
for _, want := range []string{
|
||||
"opendkim", "fatal", "exited too quickly",
|
||||
"panel", "fatal", "exited too quickly",
|
||||
"mail.example.com", "203.0.113.10 → no PTR record", "2026-11-08 09:14 UTC",
|
||||
`action="/server/health/reload"`, `action="/server/health/recheck"`,
|
||||
`hx-get="/server/health/fragment"`,
|
||||
@@ -208,7 +208,7 @@ func TestHealthFragmentCarriesTheSameRows(t *testing.T) {
|
||||
func TestOverviewPageShowsVerdictsAndDomains(t *testing.T) {
|
||||
h, _ := settingsServer(t)
|
||||
h.machine = &health.MachineSampler{}
|
||||
h.domains = domain.NewService(h.store, domain.NewOpenDKIM(t.TempDir()), nil, "selfpost")
|
||||
h.domains = domain.NewService(h.store, domain.NewKeys(t.TempDir()), nil, "selfpost")
|
||||
h.cfg.Hostname = "mail.example.org"
|
||||
d, err := h.store.AddDomain("example.org", "mail")
|
||||
if err != nil {
|
||||
|
||||
@@ -153,7 +153,7 @@ func serverMailStand(t *testing.T) (*Handlers, *fakeZone, domain.ServerMail) {
|
||||
h.dns = dnscheck.New([]string{addr})
|
||||
h.cfg.Hostname = notifyHost
|
||||
h.machine = &health.MachineSampler{}
|
||||
h.domains = domain.NewService(h.store, domain.NewOpenDKIM(t.TempDir()), nil, "sp2026")
|
||||
h.domains = domain.NewService(h.store, domain.NewKeys(t.TempDir()), nil, "sp2026")
|
||||
h.domains.SetServerName(notifyHost)
|
||||
mail, err := h.domains.ServerMail()
|
||||
if err != nil {
|
||||
|
||||
@@ -124,7 +124,7 @@ func settingsServerAt(t *testing.T, dbPath string) (*Handlers, string) {
|
||||
// A domain service that does not know the server's own name, so that
|
||||
// the records of Settings and Health come out as "cannot be read";
|
||||
// the tests of those records use serverMailStand.
|
||||
domains: domain.NewService(st, domain.NewOpenDKIM(t.TempDir()), nil, "selfpost"),
|
||||
domains: domain.NewService(st, domain.NewKeys(t.TempDir()), nil, "selfpost"),
|
||||
cfg: Config{SendLogRetentionEnvDefault: 90},
|
||||
}, password
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -346,19 +347,24 @@ func healthCards(c healthReadings) []view.HealthCard {
|
||||
}
|
||||
|
||||
// The line under the count has to fit a card: the sockets that do not
|
||||
// answer when some do not; otherwise their names, or - with the inbound spam
|
||||
// filter among them, when three names no longer fit - that it is included.
|
||||
// answer when some do not; otherwise their names - the signer's two once -
|
||||
// or, with the inbound spam filter among them, when three names no longer
|
||||
// fit, that it is included.
|
||||
answering, socketNames, silent, withFilter := 0, make([]string, 0, len(sockets)), []string{}, false
|
||||
for _, s := range sockets {
|
||||
if s.Present {
|
||||
answering++
|
||||
} else if s.Note != "" && s.Name != filterName {
|
||||
silent = append(silent, s.Name+" · "+s.Note)
|
||||
} else {
|
||||
silent = append(silent, s.Name)
|
||||
}
|
||||
if s.Note != "" {
|
||||
if s.Name == filterName {
|
||||
withFilter = true
|
||||
}
|
||||
socketNames = append(socketNames, s.Name)
|
||||
if !slices.Contains(socketNames, s.Name) {
|
||||
socketNames = append(socketNames, s.Name)
|
||||
}
|
||||
}
|
||||
socketSub := strings.Join(socketNames, ", ")
|
||||
switch {
|
||||
@@ -455,7 +461,8 @@ func (h *Handlers) healthChecks() healthReadings {
|
||||
|
||||
cert := health.CheckServedCertificate(h.cfg.TLSServedCertFile, h.cfg.TLSCertFile, h.cfg.TLSReloadInterval, time.Now())
|
||||
sockets := []health.Socket{
|
||||
health.CheckSocket("OpenDKIM", h.cfg.OpenDKIMSocket, true),
|
||||
signerSocket(h.cfg.SignSocket, "applications"),
|
||||
signerSocket(h.cfg.SignLocalSocket, "notifications"),
|
||||
health.CheckSocket("sender-check", h.cfg.SenderSocket, true),
|
||||
health.CheckSocket("send-log", h.cfg.JournalSocket, false),
|
||||
}
|
||||
@@ -486,15 +493,27 @@ func (h *Handlers) healthChecks() healthReadings {
|
||||
}
|
||||
}
|
||||
|
||||
// filterName is the inbound spam filter's row among the milter sockets.
|
||||
const filterName = "Spam filter"
|
||||
|
||||
// signerSocket is a row of the DKIM signer's: one socket for what
|
||||
// applications send, one for the panel's own notifications. Postfix attaches
|
||||
// both strictly, so either one down defers the mail that goes through it.
|
||||
func signerSocket(path, note string) health.Socket {
|
||||
s := health.CheckSocket("dkim-signer", path, true)
|
||||
s.Note = note
|
||||
return s
|
||||
}
|
||||
|
||||
// filterSocket is the inbound spam filter's row among the milter sockets. A
|
||||
// filter that is down costs what the server's setting says: with the action
|
||||
// tempfail Postfix defers inbound mail (an error, like OpenDKIM), otherwise it
|
||||
// tempfail Postfix defers inbound mail (an error, like the signer), otherwise it
|
||||
// accepts the mail unfiltered (a warning, like the send log). The address is the
|
||||
// one in INBOUND_ANTISPAM_MILTER as configured; Health and Overview, the only
|
||||
// pages that show it, are for the global administrator.
|
||||
func filterSocket(milter, action string) health.Socket {
|
||||
tempfail := action == "tempfail"
|
||||
s := health.CheckMilter("Spam filter", milter, tempfail)
|
||||
s := health.CheckMilter(filterName, milter, tempfail)
|
||||
s.Note = "inbound"
|
||||
switch {
|
||||
case s.Status != health.StatusError && s.Status != health.StatusWarn:
|
||||
@@ -519,7 +538,7 @@ func queueSummary(out string) string {
|
||||
func statusFlash(r *http.Request) string {
|
||||
switch {
|
||||
case r.URL.Query().Get("reloaded") != "":
|
||||
return "Configuration regenerated from the database - OpenDKIM's tables, every map the panel writes for Postfix, the filter lists - and OpenDKIM and Postfix have re-read it."
|
||||
return "Configuration regenerated from the database - the DKIM selector table, every map the panel writes for Postfix, the filter lists - and Postfix has re-read it."
|
||||
case r.URL.Query().Get("rechecked") != "":
|
||||
return "DNS re-checked."
|
||||
default:
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
func checksFixture() healthReadings {
|
||||
return healthReadings{
|
||||
Processes: []health.Process{
|
||||
{Name: "opendkim", State: "RUNNING", Status: health.StatusOK},
|
||||
{Name: "panel", State: "RUNNING", Status: health.StatusOK},
|
||||
{Name: "postfix", State: "FATAL", Status: health.StatusError},
|
||||
},
|
||||
ProcessError: false,
|
||||
@@ -24,7 +24,7 @@ func checksFixture() healthReadings {
|
||||
Machine: health.Machine{Status: health.StatusUnknown},
|
||||
Cert: health.Certificate{Subject: "mail.example.org", DaysLeft: 12, Status: health.StatusWarn, Detail: "Expires soon."},
|
||||
Sockets: []health.Socket{
|
||||
{Name: "OpenDKIM", Present: true, Status: health.StatusOK},
|
||||
{Name: "dkim-signer", Present: true, Status: health.StatusOK},
|
||||
{Name: "send-log", Present: false, Status: health.StatusWarn},
|
||||
},
|
||||
SocketStatus: health.StatusWarn,
|
||||
@@ -87,7 +87,7 @@ func TestQueueCardValue(t *testing.T) {
|
||||
func TestProcessesCardLeavesOutTheIdleOneShot(t *testing.T) {
|
||||
c := checksFixture()
|
||||
c.Processes = []health.Process{
|
||||
{Name: "opendkim", State: "RUNNING", Status: health.StatusOK},
|
||||
{Name: "panel", State: "RUNNING", Status: health.StatusOK},
|
||||
{Name: "postfix", State: "RUNNING", Status: health.StatusOK},
|
||||
{Name: "postfix-reload", State: "STOPPED", Status: health.StatusOK, OneShot: true},
|
||||
}
|
||||
@@ -98,7 +98,7 @@ func TestProcessesCardLeavesOutTheIdleOneShot(t *testing.T) {
|
||||
}
|
||||
|
||||
c.Processes = []health.Process{
|
||||
{Name: "opendkim", State: "RUNNING", Status: health.StatusOK},
|
||||
{Name: "panel", State: "RUNNING", Status: health.StatusOK},
|
||||
{Name: "postfix-reload", State: "FATAL", Status: health.StatusError, OneShot: true},
|
||||
}
|
||||
card = healthCards(c)[1]
|
||||
|
||||
@@ -52,15 +52,15 @@ func isASCIILetterOrDigit(r rune) bool {
|
||||
const maxDomainLen = 253 // RFC 1035 limit on a fully-qualified name
|
||||
|
||||
// NormalizeDomain lower-cases and trims a domain name. Domain names are
|
||||
// case-insensitive, and the generated OpenDKIM tables/keys use the canonical
|
||||
// case-insensitive, and the DKIM selector table and keys use the canonical
|
||||
// lower-case form, so we normalise before both validation and storage.
|
||||
func NormalizeDomain(name string) string {
|
||||
return strings.ToLower(strings.TrimSpace(name))
|
||||
}
|
||||
|
||||
// Domain enforces a strict server-side whitelist for sending-domain names
|
||||
// (security.md). The result is safe to write verbatim into the OpenDKIM
|
||||
// KeyTable/SigningTable and to use as a filesystem path segment: only
|
||||
// (security.md). The result is safe to write verbatim into the DKIM
|
||||
// selector table and to use as a filesystem path segment: only
|
||||
// lower-case letters, digits, '.' and '-' are allowed, in valid DNS label
|
||||
// shape. Input must already be normalised with NormalizeDomain.
|
||||
func Domain(name string) error {
|
||||
@@ -85,7 +85,7 @@ func Domain(name string) error {
|
||||
// Selector checks a DKIM selector: one or more DNS labels as Domain allows
|
||||
// them, at most 63 characters in all. The panel's own selector is fixed in
|
||||
// its configuration; this is for one read from a domain export file, which
|
||||
// goes into OpenDKIM's tables, a key file's name and every signature's s=
|
||||
// goes into the selector table, a key file's name and every signature's s=
|
||||
// (review of audit stage 2).
|
||||
func Selector(s string) error {
|
||||
if s == "" {
|
||||
|
||||
@@ -65,10 +65,10 @@ func admin() Meta { return Meta{User: "admin", IsGlobal: true} }
|
||||
func overviewFixture() *Overview {
|
||||
cards := []HealthCard{
|
||||
{Name: "Machine", Value: "CPU 12%", Sub: "RAM 41%", Icon: "ti-cpu", Href: "/server/health"},
|
||||
{Name: "Processes", Value: "5 of 5 running", Sub: "all programs", Icon: "ti-server-cog", Href: "/server/health"},
|
||||
{Name: "Processes", Value: "4 of 4 running", Sub: "all programs", Icon: "ti-server-cog", Href: "/server/health"},
|
||||
{Name: "TLS certificate", Value: "Expires in 12 days", Sub: "mail.example.org", Icon: "ti-certificate", Href: "/server/health", Level: LevelWarn},
|
||||
{Name: "Queue", Value: "Empty", Sub: "outbound", Icon: "ti-stack-2", Href: "/outbound/queue"},
|
||||
{Name: "Milter sockets", Value: "2 of 2 answering", Sub: "OpenDKIM, send-log", Icon: "ti-plug-connected", Href: "/server/health"},
|
||||
{Name: "Milter sockets", Value: "5 of 5 answering", Sub: "incl. the spam filter", Icon: "ti-plug-connected", Href: "/server/health"},
|
||||
{Name: "Reverse DNS", Value: "Forward = reverse", Sub: "mail.example.org", Icon: "ti-arrows-exchange", Href: "/server/health"},
|
||||
}
|
||||
o := NewOverview(admin(), "mail.example.org", cards, time.Date(2026, 10, 9, 14, 2, 31, 0, time.UTC), 30, true)
|
||||
@@ -87,8 +87,8 @@ func overviewFixture() *Overview {
|
||||
return o
|
||||
}
|
||||
|
||||
// healthFixture is the mockup's Health: a healthy machine and five processes,
|
||||
// a certificate close to expiry, the two milter sockets and a good reverse
|
||||
// healthFixture is the mockup's Health: a healthy machine and four processes,
|
||||
// a certificate close to expiry, the milter sockets and a good reverse
|
||||
// record. refresh renders it as the polled fragment.
|
||||
func healthFixture(refresh bool) *Health {
|
||||
h := NewHealth(admin(), "")
|
||||
@@ -107,7 +107,7 @@ func healthFixture(refresh bool) *Health {
|
||||
{Resource: "Network", Usage: "↓ 18.0 KiB/s · ↑ 44.0 KiB/s", Detail: []string{"eth0: 2.1 GiB in, 6.4 GiB out"}},
|
||||
}
|
||||
h.Processes.End = Verdict("ok")
|
||||
for i, name := range []string{"postfix", "opendkim", "journal-milter", "panel", "dmarc-ingest"} {
|
||||
for i, name := range []string{"postfix", "journal-milter", "panel", "dmarc-ingest"} {
|
||||
h.ProcessRows = append(h.ProcessRows, ProcessRow{Name: name, State: Tag{Status: "ok", Label: "running"},
|
||||
Detail: "pid " + strconv.Itoa(41-i) + ", uptime 18 days"})
|
||||
}
|
||||
@@ -119,7 +119,8 @@ func healthFixture(refresh bool) *Health {
|
||||
h.CertDetail, h.CertProblem = "Expires in 12 day(s). Check that renewal on the host still works.", true
|
||||
h.Sockets.End = Verdict("ok")
|
||||
h.SocketRows = []SocketRow{
|
||||
{Name: "OpenDKIM", Path: "inet:127.0.0.1:8891", State: Tag{Status: "ok"}},
|
||||
{Name: "dkim-signer", Note: "applications", Path: "unix:/run/selfpost/sign.sock", State: Tag{Status: "ok"}},
|
||||
{Name: "dkim-signer", Note: "notifications", Path: "unix:/run/selfpost/sign-local.sock", State: Tag{Status: "ok"}},
|
||||
{Name: "sender-check", Path: "unix:/run/selfpost/sender.sock", State: Tag{Status: "ok"}},
|
||||
{Name: "send-log", Path: "unix:/run/selfpost/journal.sock", State: Tag{Status: "ok"}},
|
||||
}
|
||||
@@ -271,7 +272,7 @@ func outLogFixture() *OutLog {
|
||||
}
|
||||
|
||||
// outMessageFixture is the mockup's message page: a deferred alert, with the
|
||||
// four lines Postfix and OpenDKIM wrote about it.
|
||||
// four lines Postfix wrote about it.
|
||||
func outMessageFixture() *OutMessage {
|
||||
at := time.Date(2026, 9, 21, 13, 58, 7, 0, time.UTC)
|
||||
p := NewOutMessage(admin(), MessageInput{
|
||||
@@ -290,7 +291,7 @@ func outMessageFixture() *OutMessage {
|
||||
return p.WithDeliveryLog([]LogLine{
|
||||
{Time: "13:58:07", Text: "postfix/smtpd[2114]: 4XcB7k2Jm9z1: client=unknown[198.51.100.7], sasl_username=alerts"},
|
||||
{Time: "13:58:07", Text: "postfix/cleanup[2120]: 4XcB7k2Jm9z1: message-id=<a81f0c@alertmanager>"},
|
||||
{Time: "13:58:07", Text: "opendkim[39]: 4XcB7k2Jm9z1: DKIM-Signature field added (s=sp2026, d=example.org)"},
|
||||
{Time: "13:58:07", Text: "postfix/qmgr[2061]: 4XcB7k2Jm9z1: from=<alerts@example.org>, size=2214, nrcpt=1 (queue active)"},
|
||||
{Time: "13:58:09", Level: "warn", Text: "postfix/smtp[2123]: 4XcB7k2Jm9z1: to=<noc@example.org>, relay=mx1.example.org[192.0.2.10]:25, delay=1.9, status=deferred (host mx1.example.org said: 451 4.7.1 Greylisted, try again in 5 minutes)"},
|
||||
}, "")
|
||||
}
|
||||
@@ -389,12 +390,11 @@ func outQueueFixture() *OutQueue {
|
||||
return NewOutQueue(admin(), listing, "").WithRetryPolicy("5 minutes", "about 1 hour 7 minutes", "5 days", false)
|
||||
}
|
||||
|
||||
// systemLogFixture is the mockup's System log: ten lines of mail.log, as the
|
||||
// systemLogFixture is the mockup's System log: nine lines of mail.log, as the
|
||||
// handler reads them (oldest first), with the deferrals in amber and the
|
||||
// refusals and the bounce in red.
|
||||
func systemLogFixture() *SystemLog {
|
||||
return NewSystemLog(admin(), []LogLine{
|
||||
{Time: "Sep 21 13:00:00", Text: "opendkim[39]: key table reloaded (3 domains)"},
|
||||
{Time: "Sep 21 13:40:02", Text: "journal-milter[40]: rate limit: app=prod-server 201/200 in 3600s — tempfail at MAIL FROM", Level: "warn"},
|
||||
{Time: "Sep 21 13:41:19", Text: "postfix/smtp[2098]: 4XcB1c3Rk8z1: to=<j.doe@nonexistent.example>, status=bounced (Host or domain name not found)", Level: "error"},
|
||||
{Time: "Sep 21 13:44:02", Text: "postfix/smtp[2101]: 4XcB2m7Hd4z1: to=<jobs@acme.io>, relay=none, status=deferred (connect to 10.0.4.12[10.0.4.12]:2525: Connection timed out)", Level: "warn"},
|
||||
|
||||
@@ -167,9 +167,9 @@ var outboundTopics = []HelpTopic{
|
||||
}},
|
||||
{ID: HelpSenderCheck, Title: "Sender check", Body: []Text{
|
||||
Rich("An application sends only as itself, and that holds for both senders a message has. The envelope sender is held against the login by Postfix. The ",
|
||||
Code("From:"), " line, the one a mail program shows and the one the DKIM signature is chosen by, is held against it by the panel, under the same rule: an application set to any address of its domain may write any address of that domain, and one with a list of addresses only those. A message with any other ",
|
||||
Code("From:"), " line, the one a mail program shows and the one DMARC holds the DKIM signature to, is held against it by the panel, under the same rule: an application set to any address of its domain may write any address of that domain, and one with a list of addresses only those. A message with any other ",
|
||||
Code("From:"), " - another of your domains, the server's own name, an address somewhere else, or none at all - is refused for good with ",
|
||||
Code("550 5.7.1"), ". The line is read the way the signer, OpenDKIM, reads it, so only its address counts: what a display name or a comment around it says is not judged, and a line holding more than one address is refused. It is a rejected row of the Outbound log, and its page says which address was refused. This is what keeps one application from getting mail signed with another domain's key.",
|
||||
Code("550 5.7.1"), ". The line must hold one address, read as a mail program reads it: what a display name around it says is not judged, and a line holding more than one address is refused. It is a rejected row of the Outbound log, and its page says which address was refused. This is what keeps one application from sending, under this server's signature, as another domain or another application.",
|
||||
Br(), Br(),
|
||||
"The check is strict. While the panel does not answer, Postfix does not accept mail unchecked: it replies with a temporary error and the application sends again later. Server › Health counts what that costs: for how long the panel did not answer while Postfix itself was running; how many connections Postfix turned away because of it - it refuses from the connection on, before a message is offered, so a client that tries three times is three and a monitor that only connects is one; and how many messages were refused with a temporary error in the middle, when the panel stopped answering after a message had begun. A time marked ≈ includes a start at which supervisord could not say since when Postfix was running: the time since the panel's last sign of life is counted, which may be more than Postfix was up. The card warns when a connection was turned away or a message refused in the last 24 hours. If the numbers stay at nothing, the check is where it should be; if they do not, it belongs in a process of its own."),
|
||||
}},
|
||||
|
||||
@@ -797,6 +797,6 @@ func NewOutDomainDelete(m Meta, id int64, name string, logins []string) *OutDoma
|
||||
item = append(item, Inline{Text: ") with their credentials and sender bindings;"})
|
||||
p.Consequences = append(p.Consequences, item)
|
||||
}
|
||||
p.Consequences = append(p.Consequences, Rich("reload OpenDKIM so the domain is no longer signed."))
|
||||
p.Consequences = append(p.Consequences, Rich("stop signing mail as the domain."))
|
||||
return p
|
||||
}
|
||||
|
||||
@@ -213,7 +213,7 @@ func TestStepLevelAndLogLineLevel(t *testing.T) {
|
||||
"postfix/smtp[1]: Q1: to=<a@b>, status=bounced (no such user)": "error",
|
||||
"postfix/qmgr[1]: Q1: status=expired, returned to sender": "error",
|
||||
"postfix/smtp[1]: warning: TLS library problem": "warn",
|
||||
"opendkim[1]: Q1: DKIM-Signature field added": "",
|
||||
"postfix/qmgr[1]: Q1: from=<a@b>, size=1, nrcpt=1 (queue active)": "",
|
||||
"postfix/cleanup[1]: Q1: milter-reject: END-OF-MESSAGE from unknown[10.77.0.1]: 5.7.1 From: x - no": "error",
|
||||
"postfix/smtpd[1]: NOQUEUE: milter-reject: MAIL from unknown[10.77.0.1]: 451 4.7.1 Service unavailable": "warn",
|
||||
"postfix/smtpd[1]: NOQUEUE: reject: RCPT from unknown[2001:db8::1]: 554 5.7.1 <a@b>: Relay access denied": "error",
|
||||
|
||||
@@ -434,7 +434,7 @@ func TestOutDomainDeleteNamesWhatGoesWithIt(t *testing.T) {
|
||||
`<h1 class="title is-3">Delete example.org</h1>`, `<div class="box sp-danger-zone">`, `<h2>This cannot be undone</h2>`,
|
||||
`Deleting <strong>example.org</strong> will:`, `permanently delete its DKIM signing key;`,
|
||||
`delete <strong>both applications</strong> (<span class="sp-mono">prod-server</span>, <span class="sp-mono">alerts</span>) with their credentials and sender bindings;`,
|
||||
`reload OpenDKIM so the domain is no longer signed.`, `Inbound domains are not touched.`,
|
||||
`stop signing mail as the domain.`, `Inbound domains are not touched.`,
|
||||
`<form method="post" action="/outbound/domains/1/delete">`, `class="button is-danger"`, `Delete example.org</button>`,
|
||||
`href="/outbound/domains/1">Keep it</a>`,
|
||||
`<h2>Moving it instead?</h2>`, `<a href="/outbound/domains/1/settings">Export the domain</a>`,
|
||||
@@ -443,7 +443,7 @@ func TestOutDomainDeleteNamesWhatGoesWithIt(t *testing.T) {
|
||||
// No applications: nothing to say about them.
|
||||
out = renderSignedIn(t, "out-domain-delete", NewOutDomainDelete(admin(), 1, "example.org", nil))
|
||||
pageLacks(t, "Delete", out, `applications`, `credentials`)
|
||||
pageHas(t, "Delete", out, `permanently delete its DKIM signing key;`, `reload OpenDKIM`)
|
||||
pageHas(t, "Delete", out, `permanently delete its DKIM signing key;`, `stop signing mail as the domain.`)
|
||||
out = renderSignedIn(t, "out-domain-delete", NewOutDomainDelete(admin(), 1, "example.org", []string{"only"}))
|
||||
pageHas(t, "Delete", out, `<strong>its application</strong> (<span class="sp-mono">only</span>)`)
|
||||
}
|
||||
|
||||
@@ -155,7 +155,7 @@ func TestHealthShowsEveryCheckAndBothActions(t *testing.T) {
|
||||
`↓ 18.0 KiB/s`, `eth0: 2.1 GiB in, 6.4 GiB out`,
|
||||
`pid 41, uptime 18 days`, `<span class="tag is-success is-light">running</span>`,
|
||||
`2026-10-21 09:14 UTC`, `class="mt-4 has-text-danger"`, `Check that renewal on the host still works.`,
|
||||
`inet:127.0.0.1:8891`, `→ 203.0.113.25<br>← mail.example.org`,
|
||||
`dkim-signer <span class="sp-muted">· notifications</span>`, `unix:/run/selfpost/sign-local.sock`, `→ 203.0.113.25<br>← mail.example.org`,
|
||||
`hx-get="/server/health/fragment"`,
|
||||
)
|
||||
pageLacks(t, "Health", out, `hx-swap-oob`, `/overview/fragment`)
|
||||
@@ -252,8 +252,8 @@ func TestHealthStatesTheFixtureDoesNotDraw(t *testing.T) {
|
||||
h.ProcessError = true
|
||||
h.ProcessRows = nil
|
||||
h.CertFacts, h.CertDetail, h.CertProblem = nil, "No certificate path is configured (TLS_CERT_FILE).", true
|
||||
h.SocketRows = []SocketRow{{Name: "OpenDKIM", Path: "/run/opendkim/opendkim.sock", State: Tag{Status: "error"},
|
||||
Detail: "OpenDKIM socket missing: mail cannot leave."}}
|
||||
h.SocketRows = []SocketRow{{Name: "dkim-signer", Note: "applications", Path: "/run/selfpost/sign.sock", State: Tag{Status: "error"},
|
||||
Detail: "dkim-signer socket missing: mail cannot leave."}}
|
||||
h.SocketRows = append(h.SocketRows, SocketRow{Name: "rspamd", Note: "inbound spam filter", Path: "inet:antispam:11332", State: Tag{Status: "ok"}})
|
||||
h.HostFacts = []Fact{{Label: "Hostname", Value: Plain("(SELFPOST_HOSTNAME is not set)"), Mono: true}}
|
||||
h.Flash = &Flash{Text: Plain("DNS re-checked.")}
|
||||
@@ -261,7 +261,7 @@ func TestHealthStatesTheFixtureDoesNotDraw(t *testing.T) {
|
||||
pageHas(t, "Health", out,
|
||||
`Could not ask supervisord for the process list.`,
|
||||
`No certificate path is configured`,
|
||||
`<span class="tag is-danger is-light">error</span>`, `OpenDKIM socket missing: mail cannot leave.`,
|
||||
`<span class="tag is-danger is-light">error</span>`, `dkim-signer socket missing: mail cannot leave.`,
|
||||
`SELFPOST_HOSTNAME is not set`, `rspamd <span class="sp-muted">· inbound spam filter</span>`,
|
||||
`<div class="notification is-success is-light">`, `DNS re-checked.`,
|
||||
)
|
||||
|
||||
@@ -51,7 +51,7 @@ func NewSystemLog(m Meta, lines []LogLine, errText string) *SystemLog {
|
||||
Head: Head{
|
||||
Kicker: "Server",
|
||||
Title: "System log",
|
||||
Lead: Rich("The tail of ", Code("mail.log"), " — Postfix, OpenDKIM and the milters. For one message, open it from the ", Link(outboundLog, "Outbound log"), " instead."),
|
||||
Lead: Rich("The tail of ", Code("mail.log"), ", the log Postfix writes. For one message, open it from the ", Link(outboundLog, "Outbound log"), " instead."),
|
||||
},
|
||||
Log: Box{No: "01", Title: "Recent lines", ID: "system-log", Poll: serverLog + "/fragment",
|
||||
End: Plain("Last 200 lines · refreshes on its own")},
|
||||
|
||||
@@ -18,7 +18,7 @@ func TestSystemLogShowsNewestFirstAndPollsItsBox(t *testing.T) {
|
||||
`<span class="sp-d">Sep 21 14:01:52</span> postfix/smtp[2131]`,
|
||||
`<span class="sp-w">postfix/smtp[2123]`, `<span class="sp-e">postfix/smtpd[2119]`,
|
||||
)
|
||||
if strings.Index(out, "postfix/smtp[2131]") > strings.Index(out, "opendkim[39]") {
|
||||
if strings.Index(out, "postfix/smtp[2131]") > strings.Index(out, "journal-milter[40]") {
|
||||
t.Error("the log is not shown newest line first")
|
||||
}
|
||||
// What a line says is escaped, not interpreted.
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
{{template "health_mail" .}}
|
||||
{{template "box_open" .Configuration}}
|
||||
{{template "body_open"}}
|
||||
<p class="mb-3">Regenerates the OpenDKIM and Postfix configuration and the spam filter's lists from the database, removes the keys of deleted domains, 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>
|
||||
<p class="mb-3">Regenerates the Postfix configuration and the spam filter's lists from the database, removes the keys of deleted domains, and reloads Postfix. 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>
|
||||
{{template "action" .Reload}}
|
||||
{{template "body_close"}}
|
||||
{{template "box_close"}}
|
||||
|
||||
+11
-8
@@ -66,12 +66,14 @@ type Config struct {
|
||||
TLSCertFile string
|
||||
TLSServedCertFile string
|
||||
TLSReloadInterval time.Duration
|
||||
// OpenDKIMSocket and JournalSocket are the two milter sockets Postfix
|
||||
// connects to. The status page dials them: the first is required for mail
|
||||
// to leave at all (OpenDKIM runs with default_action=tempfail), the second
|
||||
// only for the send log (the journal-milter fails open).
|
||||
OpenDKIMSocket string
|
||||
JournalSocket string
|
||||
// SignSocket and SignLocalSocket are the DKIM signer's two milter
|
||||
// sockets, applications' mail and the panel's own; JournalSocket is the
|
||||
// journal-milter's. The status page dials them: the signer's are required
|
||||
// for mail to leave at all (default_action=tempfail), the journal's only
|
||||
// for the send log (the journal-milter fails open).
|
||||
SignSocket string
|
||||
SignLocalSocket string
|
||||
JournalSocket string
|
||||
// SenderSocket is the sender check's socket, attached strictly.
|
||||
SenderSocket string
|
||||
// SessionIdleDays is the sliding inactivity window after which a login
|
||||
@@ -118,7 +120,7 @@ type Server struct {
|
||||
|
||||
// New builds the panel server. setupTokenPath is where the current setup token
|
||||
// is mirrored on disk (security.md); domains is the sending-domain service
|
||||
// that owns DKIM keys and the OpenDKIM tables (architecture.md § OpenDKIM);
|
||||
// that owns DKIM keys and the selector table (architecture.md § DKIM signing);
|
||||
// apps owns application SASL accounts and the Postfix sender map
|
||||
// (architecture.md § Mail path).
|
||||
func New(st *store.Store, domains *domain.Service, apps *app.Service, inboundSvc *inbound.Service, lists *inbound.Lists, dmarcSvc *dmarc.Service, cfg Config, setupTokenPath string) (*Server, error) {
|
||||
@@ -145,7 +147,8 @@ func New(st *store.Store, domains *domain.Service, apps *app.Service, inboundSvc
|
||||
TLSCertFile: cfg.TLSCertFile,
|
||||
TLSServedCertFile: cfg.TLSServedCertFile,
|
||||
TLSReloadInterval: cfg.TLSReloadInterval,
|
||||
OpenDKIMSocket: cfg.OpenDKIMSocket,
|
||||
SignSocket: cfg.SignSocket,
|
||||
SignLocalSocket: cfg.SignLocalSocket,
|
||||
JournalSocket: cfg.JournalSocket,
|
||||
SenderSocket: cfg.SenderSocket,
|
||||
RateLimitMessagesPerIP: cfg.RateLimitMessagesPerIP,
|
||||
|
||||
Reference in New Issue
Block a user