health-setup-checks: the certificate's name, Postfix's greeting, the proxy
Decided by the owner on 2026-10-13 (in place of preflight), taken up on 2026-10-16; mockupsb2dcefc,cc33890,0feaa55(his: the greeting on a row of its own, the name check a tag beside Names). - The certificate: health.Certificate keeps its names (DNS names, or the CN without any) and ForHost holds it to SELFPOST_HOSTNAME as a TLS client does - VerifyHostname over the DNS names, the CN only without any, as OpenSSL's X509_check_host and so a sending Postfix still take it (src/tls/tls_client.c:439, 513). A certificate for another name fails; the tag beside Names says matches / not for the hostname. - The greeting: postconf -x -h smtpd_banner, read once at the panel's start like the served certificate's path (main.cf is not rewritten after), is what smtpd writes after "220 " (src/smtpd/smtpd.c:5979). A first word other than the hostname warns, with what to set. A fact may now take the whole row (Fact.Wide, sp-wide) and carry a tag beside its label (Fact.LabelTag); both on the kit page. - The proxy: auth.Module.Seen says the address the panel counts sign-ins against, the peer, and whether X-Forwarded-For was read or left unread for an untrusted peer; the Configuration card shows "Your address" and, in the second case, warns that every visitor shares one sign-in limit and names the /32 (or /128) to put in TRUSTED_PROXY_CIDR. Tests: TestCertificateForHost (CN-only, DNS names, a wildcard, the CN ignored beside DNS names), TestSeenAddress, TestHealthSaysWhereItSeesTheViewer, TestHealthGreetingAndCertificateName; e2e reads the stand's right state on Health (matches, "220 mail.e2e.test ESMTP", a direct client). go vet + go test (26 packages) and make e2e on the test server for this tree. Docs: help (word for word with the mockup), guide § Overview and Health, CHANGELOG [Unreleased] § Added, roadmap row done. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -125,6 +125,32 @@ func clientIP(r *http.Request, trustedProxies []*net.IPNet) string {
|
||||
return host
|
||||
}
|
||||
|
||||
// SeenAddress is how the panel sees a request's address, for Health: the
|
||||
// address it counts sign-ins against (Client), the transport peer (Peer),
|
||||
// and whether Client came from X-Forwarded-For (FromHeader) or the header
|
||||
// was there and left unread because the peer is not a trusted proxy
|
||||
// (HeaderIgnored) - the case where every visitor shares the proxy's address
|
||||
// and one sign-in limit.
|
||||
type SeenAddress struct {
|
||||
Client string
|
||||
Peer string
|
||||
FromHeader bool
|
||||
HeaderIgnored bool
|
||||
}
|
||||
|
||||
// Seen reads a request's address as clientIP does.
|
||||
func (m *Module) Seen(r *http.Request) SeenAddress {
|
||||
peer, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||
if err != nil {
|
||||
peer = r.RemoteAddr
|
||||
}
|
||||
s := SeenAddress{Client: clientIP(r, m.trustedProxies), Peer: peer}
|
||||
hasHeader := r.Header.Get("X-Forwarded-For") != ""
|
||||
s.FromHeader = hasHeader && s.Client != peer
|
||||
s.HeaderIgnored = hasHeader && !s.FromHeader
|
||||
return s
|
||||
}
|
||||
|
||||
func ipInAny(ip net.IP, nets []*net.IPNet) bool {
|
||||
for _, n := range nets {
|
||||
if n.Contains(ip) {
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Health says where the panel sees a request from: through a trusted proxy,
|
||||
// the client's own address from X-Forwarded-For; through a proxy it does not
|
||||
// trust, the proxy's, with the header left unread - every visitor then
|
||||
// shares one sign-in limit; and with no header, the connection's own.
|
||||
func TestSeenAddress(t *testing.T) {
|
||||
_, proxyNet, _ := net.ParseCIDR("172.18.0.0/16")
|
||||
m := &Module{trustedProxies: []*net.IPNet{proxyNet}}
|
||||
untrusting := &Module{}
|
||||
|
||||
r := httptest.NewRequest("GET", "/server/health", nil)
|
||||
r.RemoteAddr = "172.18.0.1:40112"
|
||||
r.Header.Set("X-Forwarded-For", "198.51.100.7")
|
||||
if s := m.Seen(r); s != (SeenAddress{Client: "198.51.100.7", Peer: "172.18.0.1", FromHeader: true}) {
|
||||
t.Errorf("through a trusted proxy: %+v", s)
|
||||
}
|
||||
if s := untrusting.Seen(r); s != (SeenAddress{Client: "172.18.0.1", Peer: "172.18.0.1", HeaderIgnored: true}) {
|
||||
t.Errorf("through a proxy not trusted: %+v", s)
|
||||
}
|
||||
|
||||
r.Header.Del("X-Forwarded-For")
|
||||
r.RemoteAddr = "203.0.113.9:5000"
|
||||
if s := m.Seen(r); s != (SeenAddress{Client: "203.0.113.9", Peer: "203.0.113.9"}) {
|
||||
t.Errorf("direct: %+v", s)
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ type Config struct {
|
||||
TLSCertFile string
|
||||
TLSServedCertFile string
|
||||
TLSReloadInterval time.Duration
|
||||
SMTPBanner string
|
||||
SignSocket string
|
||||
SignLocalSocket string
|
||||
JournalSocket string
|
||||
|
||||
@@ -143,6 +143,8 @@ func (h *Handlers) HandleHealthRecheck(w http.ResponseWriter, r *http.Request) {
|
||||
// of Overview and the Notification mail card, which only Health shows.
|
||||
func (h *Handlers) healthView(r *http.Request, flash string) *view.Health {
|
||||
page := healthPage(h.shellMeta(r), h.healthChecks(), flash)
|
||||
seen := h.auth.Seen(r)
|
||||
page.WithClientAddress(seen.Client, seen.Peer, seen.FromHeader, seen.HeaderIgnored)
|
||||
mail := h.serverMail(false)
|
||||
page.WithNotificationMail(notify.From(h.cfg.Hostname), mail.rows, mail.status)
|
||||
stats, err := h.store.SenderCheckStats(time.Now())
|
||||
@@ -205,8 +207,15 @@ func healthPage(m view.Meta, c healthReadings, flash string) *view.Health {
|
||||
if !cert.NotAfter.IsZero() {
|
||||
page.CertFacts = append(page.CertFacts, view.Fact{Label: "Expires", Value: view.Plain(cert.NotAfter.UTC().Format("2006-01-02 15:04 UTC")), Mono: true})
|
||||
}
|
||||
if cert.Subject != "" {
|
||||
page.CertFacts = append(page.CertFacts, view.Fact{Label: "Names", Value: view.Plain(cert.Subject), Mono: true})
|
||||
if len(cert.Names) > 0 {
|
||||
names := view.Fact{Label: "Names", Value: view.Plain(strings.Join(cert.Names, ", ")), Mono: true}
|
||||
if m := cert.MatchesHost; m != nil {
|
||||
names.LabelTag = &view.Tag{Status: "ok", Label: "matches"}
|
||||
if !*m {
|
||||
names.LabelTag = &view.Tag{Status: "fail", Label: "not for the hostname"}
|
||||
}
|
||||
}
|
||||
page.CertFacts = append(page.CertFacts, names)
|
||||
}
|
||||
if r := cert.Renewal; r != nil {
|
||||
page.CertFacts = append(page.CertFacts,
|
||||
@@ -225,7 +234,7 @@ func healthPage(m view.Meta, c healthReadings, flash string) *view.Health {
|
||||
}
|
||||
|
||||
ptr := c.PTR
|
||||
page.Hostname.End = view.Verdict(string(ptr.Status))
|
||||
hostStatus := ptr.Status
|
||||
host := c.Hostname
|
||||
if host == "" {
|
||||
host = "(SELFPOST_HOSTNAME is not set)"
|
||||
@@ -241,6 +250,18 @@ func healthPage(m view.Meta, c healthReadings, flash string) *view.Health {
|
||||
}
|
||||
page.HostFacts = append(page.HostFacts, view.Fact{Label: "Lookup", Value: lookup, Mono: true})
|
||||
}
|
||||
// The greeting must name the hostname: a receiver compares it with the
|
||||
// name it looked up, and a container id or localhost there reads as a
|
||||
// misconfigured host. A banner that could not be read says nothing.
|
||||
if c.Banner != "" {
|
||||
fact := view.Fact{Label: "Greeting", Value: view.Plain("220 " + c.Banner), Mono: true, Wide: true}
|
||||
if f := strings.Fields(c.Banner); c.Hostname != "" && (len(f) == 0 || !strings.EqualFold(f[0], c.Hostname)) {
|
||||
hostStatus = health.Worst(hostStatus, health.StatusWarn)
|
||||
fact.Note = view.Plain("Postfix greets with another name than " + c.Hostname + ": set myhostname to it (build/postfix-config.sh does, from SELFPOST_HOSTNAME).")
|
||||
}
|
||||
page.HostFacts = append(page.HostFacts, fact)
|
||||
}
|
||||
page.Hostname.End = view.Verdict(string(hostStatus))
|
||||
return page
|
||||
}
|
||||
|
||||
@@ -439,6 +460,8 @@ type healthReadings struct {
|
||||
SocketStatus health.Status
|
||||
Hostname string
|
||||
PTR dnscheck.Result
|
||||
// Banner is Postfix's smtpd_banner, expanded: the greeting after "220".
|
||||
Banner string
|
||||
}
|
||||
|
||||
// healthChecks runs every server check once.
|
||||
@@ -459,7 +482,7 @@ func (h *Handlers) healthChecks() healthReadings {
|
||||
queueStatus = health.StatusWarn
|
||||
}
|
||||
|
||||
cert := health.CheckServedCertificate(h.cfg.TLSServedCertFile, h.cfg.TLSCertFile, h.cfg.TLSReloadInterval, time.Now())
|
||||
cert := health.CheckServedCertificate(h.cfg.TLSServedCertFile, h.cfg.TLSCertFile, h.cfg.TLSReloadInterval, time.Now()).ForHost(h.cfg.Hostname)
|
||||
sockets := []health.Socket{
|
||||
signerSocket(h.cfg.SignSocket, "applications"),
|
||||
signerSocket(h.cfg.SignLocalSocket, "notifications"),
|
||||
@@ -490,6 +513,7 @@ func (h *Handlers) healthChecks() healthReadings {
|
||||
SocketStatus: socketStatus,
|
||||
Hostname: h.cfg.Hostname,
|
||||
PTR: srv.PTR,
|
||||
Banner: h.cfg.SMTPBanner,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
|
||||
"github.com/mixeme/selfpost/internal/dnscheck"
|
||||
"github.com/mixeme/selfpost/internal/health"
|
||||
"github.com/mixeme/selfpost/internal/web/view"
|
||||
)
|
||||
|
||||
// checksFixture is one reading of every server check, shaped as healthChecks
|
||||
@@ -127,3 +128,44 @@ func TestProcessRowOfAnIdleOneShotSaysOnDemand(t *testing.T) {
|
||||
t.Errorf("stopped daemon = %+v", daemon)
|
||||
}
|
||||
}
|
||||
|
||||
// Health's Hostname card shows Postfix's greeting on a row of its own, and
|
||||
// warns when it names another host; the certificate's names carry whether
|
||||
// they are the hostname's.
|
||||
func TestHealthGreetingAndCertificateName(t *testing.T) {
|
||||
c := checksFixture()
|
||||
c.Banner = "mail.example.org ESMTP"
|
||||
matches := true
|
||||
c.Cert.Names, c.Cert.MatchesHost = []string{"mail.example.org"}, &matches
|
||||
page := healthPage(view.Meta{}, c, "")
|
||||
var greeting *view.Fact
|
||||
for i := range page.HostFacts {
|
||||
if page.HostFacts[i].Label == "Greeting" {
|
||||
greeting = &page.HostFacts[i]
|
||||
}
|
||||
}
|
||||
if greeting == nil || greeting.Value[0].Text != "220 mail.example.org ESMTP" || !greeting.Wide || greeting.Note != nil {
|
||||
t.Fatalf("greeting = %+v", greeting)
|
||||
}
|
||||
names := func(p *view.Health) *view.Tag {
|
||||
for _, f := range p.CertFacts {
|
||||
if f.Label == "Names" {
|
||||
return f.LabelTag
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if tag := names(page); tag == nil || tag.Label != "matches" {
|
||||
t.Errorf("names = %+v", page.CertFacts)
|
||||
}
|
||||
|
||||
c.Banner = "3f2a9c1d0e7b ESMTP"
|
||||
matches = false
|
||||
page = healthPage(view.Meta{}, c, "")
|
||||
if end := page.Hostname.End; len(end) == 0 || end[0].Tag == nil || end[0].Tag.Status != "warn" {
|
||||
t.Errorf("a greeting with a container id: card %+v, want warn", page.Hostname.End)
|
||||
}
|
||||
if tag := names(page); tag == nil || tag.Status != "fail" || tag.Label != "not for the hostname" {
|
||||
t.Errorf("a certificate for another name: %+v", tag)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -452,6 +452,12 @@ type Fact struct {
|
||||
Small bool
|
||||
Big bool
|
||||
Note Text
|
||||
// Wide takes the whole row of the grid (sp-wide): a line a server wrote,
|
||||
// read as one - Postfix's greeting.
|
||||
Wide bool
|
||||
// LabelTag is a verdict on the value, beside the label: the
|
||||
// certificate's names against the hostname.
|
||||
LabelTag *Tag
|
||||
}
|
||||
|
||||
// SideMenu is the input of side_menu, the left column of a detail page: groups
|
||||
|
||||
@@ -2,6 +2,7 @@ package view
|
||||
|
||||
import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
@@ -53,6 +54,38 @@ type Health struct {
|
||||
|
||||
Configuration Box
|
||||
Reload Action
|
||||
// ConfigFacts is the address the panel sees the viewer at
|
||||
// (WithClientAddress); ConfigProblem says what to do when it is the
|
||||
// reverse proxy's own.
|
||||
ConfigFacts []Fact
|
||||
ConfigProblem Text
|
||||
}
|
||||
|
||||
// WithClientAddress says where the panel sees the viewer's request from:
|
||||
// client is the address it counts sign-ins against, peer the connection's,
|
||||
// fromHeader that client came from X-Forwarded-For of a trusted proxy, and
|
||||
// ignored that the header was there but peer is no trusted proxy - then
|
||||
// every visitor has the proxy's address and shares one sign-in limit, which
|
||||
// nothing else shows (auth.Module.Seen; guide § TRUSTED_PROXY_CIDR).
|
||||
func (h *Health) WithClientAddress(client, peer string, fromHeader, ignored bool) *Health {
|
||||
f := Fact{Label: "Your address", Value: Plain(client), Mono: true}
|
||||
switch {
|
||||
case fromHeader:
|
||||
f.Note = Plain("From X-Forwarded-For, which the trusted proxy " + peer + " set")
|
||||
case ignored:
|
||||
f.Note = Plain("The proxy's own: its X-Forwarded-For is not read")
|
||||
mask := "/32"
|
||||
if strings.Contains(peer, ":") {
|
||||
mask = "/128"
|
||||
}
|
||||
h.ConfigProblem = Rich("Every visitor reaches the panel as ", Mono(peer),
|
||||
", the reverse proxy, and they all share one sign-in limit. Set ", Code("TRUSTED_PROXY_CIDR"),
|
||||
" to ", Code(peer+mask), " - or the proxy's network - and restart the container.")
|
||||
default:
|
||||
f.Note = Plain("The connection's own; no proxy in between said otherwise")
|
||||
}
|
||||
h.ConfigFacts = []Fact{f}
|
||||
return h
|
||||
}
|
||||
|
||||
// SenderCheckRow is one period of the Sender check table.
|
||||
|
||||
@@ -145,13 +145,18 @@ var overviewTopics = []HelpTopic{
|
||||
Rich("Six checks, and the worst of them is the postmark. ", TagOf("ok", ""), " needs nothing; ",
|
||||
TagOf("warn", ""), " still delivers mail but will stop doing so if ignored; ",
|
||||
TagOf("fail", ""), " means mail is being lost or refused now. ", Link("/server/health", "Health"),
|
||||
" shows the readings behind them: CPU and memory are the container's own, not the host's spare capacity, and network is a short window, not a daily total."),
|
||||
" shows the readings behind them: CPU and memory are the container's own, not the host's spare capacity, and network is a short window, not a daily total. ",
|
||||
Em("Your address"), ", in the Configuration card, is the address the panel sees your browser at: behind a reverse proxy it must be your own, taken from ", Code("X-Forwarded-For"),
|
||||
"; when it is the proxy's, every visitor shares one sign-in limit, and Health warns and names the address to put in ", Code("TRUSTED_PROXY_CIDR"), "."),
|
||||
}},
|
||||
{ID: HelpCertificate, Title: "TLS certificate", Body: []Text{
|
||||
Plain("The certificate Postfix presents on 465 and 587. The reverse proxy or the image mounts it, the panel does not issue it. Postfix serves its own copy of the mounted file and looks for a renewed one once a day by default; until it takes one up, the card shows it as a renewal waiting, and warns once it has waited longer than that check allows - the container log's cert-reload lines say why. It warns 14 days before expiry unless a renewal is already waiting, and fails once expired, or when the file is missing or unreadable — clients that verify certificates then refuse to connect."),
|
||||
Rich("The certificate Postfix presents on 465 and 587. The reverse proxy or the image mounts it, the panel does not issue it. Postfix serves its own copy of the mounted file and looks for a renewed one once a day by default; until it takes one up, the card shows it as a renewal waiting, and warns once it has waited longer than that check allows - the container log's cert-reload lines say why. It warns 14 days before expiry unless a renewal is already waiting, and fails once expired, or when the file is missing or unreadable — clients that verify certificates then refuse to connect. It also fails when none of its names is the hostname (",
|
||||
Code("SELFPOST_HOSTNAME"), ") - the tag beside ", Em("Names"), " says which: a client that verifies the name refuses it however valid it is."),
|
||||
}},
|
||||
{ID: HelpRDNS, Title: "Hostname and reverse DNS", Body: []Text{
|
||||
Rich("The hostname (", Code("SELFPOST_HOSTNAME"), ") must resolve to this server's address, and that address must resolve back to the hostname. Large receivers reject or spam-folder mail from hosts where the two disagree. The PTR record is set at your hosting provider, not in your DNS zone."),
|
||||
Rich("The hostname (", Code("SELFPOST_HOSTNAME"), ") must resolve to this server's address, and that address must resolve back to the hostname. Large receivers reject or spam-folder mail from hosts where the two disagree. The PTR record is set at your hosting provider, not in your DNS zone. ",
|
||||
Em("Greeting"), " is the line Postfix opens every connection with, ", Code("220"), " and its banner; it must name the hostname too - a receiver compares it with the name it looked up - and a container id or ",
|
||||
Code("localhost"), " there is a warning."),
|
||||
}},
|
||||
}
|
||||
|
||||
|
||||
@@ -145,6 +145,8 @@ func KitPage() *Kit {
|
||||
{Label: "Label", Value: Plain("Value")},
|
||||
{Label: "Mono value", Value: Plain("4XcB7k2Jm9z1"), Mono: true},
|
||||
{Label: "sp-big", Value: Plain("5 days"), Big: true, Note: Plain("a supporting line")},
|
||||
{Label: "Label with a tag", Value: Plain("a verdict on the value"), LabelTag: &Tag{Status: "ok", Label: "matches"}},
|
||||
{Label: "sp-wide", Value: Plain("a value that takes the whole row, a line of a server's own"), Mono: true, Wide: true},
|
||||
},
|
||||
|
||||
Timeline: Box{No: "08", Title: "timeline"},
|
||||
|
||||
@@ -331,3 +331,18 @@ func TestUserDeleteSaysNothingOfReportAddresses(t *testing.T) {
|
||||
pageLacks(t, "Delete user", out, `report address`, `will have no`, `The domains`)
|
||||
}
|
||||
}
|
||||
|
||||
// The Configuration card says where the panel sees the viewer from, and what
|
||||
// to set when it is the reverse proxy's own address.
|
||||
func TestHealthSaysWhereItSeesTheViewer(t *testing.T) {
|
||||
out := renderSignedIn(t, "health", NewHealth(admin(), "").WithClientAddress("198.51.100.7", "172.18.0.1", true, false))
|
||||
pageHas(t, "Health", out, `<div><dt>Your address</dt><dd class="sp-mono">198.51.100.7</dd><dd class="sp-muted">From X-Forwarded-For, which the trusted proxy 172.18.0.1 set</dd></div>`)
|
||||
pageLacks(t, "Health", out, `TRUSTED_PROXY_CIDR`)
|
||||
|
||||
out = renderSignedIn(t, "health", NewHealth(admin(), "").WithClientAddress("172.18.0.1", "172.18.0.1", false, true))
|
||||
pageHas(t, "Health", out, `The proxy's own: its X-Forwarded-For is not read`,
|
||||
`<p class="mb-3 has-text-danger">Every visitor reaches the panel as <span class="sp-mono">172.18.0.1</span>, the reverse proxy, and they all share one sign-in limit. Set <code>TRUSTED_PROXY_CIDR</code> to <code>172.18.0.1/32</code>`)
|
||||
|
||||
out = renderSignedIn(t, "health", NewHealth(admin(), "").WithClientAddress("fd00::1", "fd00::1", false, true))
|
||||
pageHas(t, "Health", out, `<code>fd00::1/128</code>`)
|
||||
}
|
||||
|
||||
@@ -278,6 +278,11 @@ code { background: var(--sp-well); color: var(--sp-ink); border-radius: 3px; pad
|
||||
.sp-facts { display: grid; grid-template-columns: repeat(auto-fit, minmax(11rem, 1fr)); gap: 1rem 1.5rem; margin: 0; }
|
||||
.sp-facts dt { font-family: var(--sp-mono); font-size: var(--sp-fs-xs); letter-spacing: .08em; text-transform: uppercase; color: var(--sp-muted); }
|
||||
.sp-facts dd { margin: 0; overflow-wrap: anywhere; }
|
||||
/* A label may carry a tag, the verdict on its value (the certificate's
|
||||
names against the hostname); it keeps the tag's own type. */
|
||||
.sp-facts dt .tag { margin-left: .4rem; text-transform: none; letter-spacing: 0; vertical-align: 1px; }
|
||||
/* A fact that takes the whole row: a line a server wrote, read as one. */
|
||||
.sp-facts .sp-wide { grid-column: 1 / -1; }
|
||||
.sp-facts dd.sp-big { font-size: 1.5rem; font-weight: 600; line-height: 1.2; }
|
||||
.sp-side { position: sticky; top: 1.5rem; }
|
||||
.menu-label { font-family: var(--sp-mono); letter-spacing: .1em; }
|
||||
|
||||
@@ -169,7 +169,7 @@
|
||||
{{/* facts is a label / value grid (input: []Fact); Big is for a headline
|
||||
number, with its supporting line under it. */}}
|
||||
{{define "facts"}}<dl class="sp-facts">{{range .}}
|
||||
<div><dt>{{.Label}}</dt><dd{{if and .Mono .Small}} class="sp-mono sp-small"{{else if .Mono}} class="sp-mono"{{else if .Big}} class="sp-big"{{end}}>{{template "inline" .Value}}</dd>{{if .Note}}<dd class="sp-muted">{{template "inline" .Note}}</dd>{{end}}</div>{{end}}
|
||||
<div{{if .Wide}} class="sp-wide"{{end}}><dt>{{.Label}}{{with .LabelTag}} {{template "tag" .}}{{end}}</dt><dd{{if and .Mono .Small}} class="sp-mono sp-small"{{else if .Mono}} class="sp-mono"{{else if .Big}} class="sp-big"{{end}}>{{template "inline" .Value}}</dd>{{if .Note}}<dd class="sp-muted">{{template "inline" .Note}}</dd>{{end}}</div>{{end}}
|
||||
</dl>{{end}}
|
||||
|
||||
{{/* side_menu is the Bulma menu of a detail page (input: SideMenu): icons,
|
||||
|
||||
@@ -15,7 +15,9 @@
|
||||
{{template "health_mail" .}}
|
||||
{{template "box_open" .Configuration}}
|
||||
{{template "body_open"}}
|
||||
<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>
|
||||
{{if .ConfigFacts}}<div class="mb-3">{{template "facts" .ConfigFacts}}</div>
|
||||
{{end}}{{if .ConfigProblem}}<p class="mb-3 has-text-danger">{{template "inline" .ConfigProblem}}</p>
|
||||
{{end}}<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"}}
|
||||
|
||||
@@ -66,6 +66,9 @@ type Config struct {
|
||||
TLSCertFile string
|
||||
TLSServedCertFile string
|
||||
TLSReloadInterval time.Duration
|
||||
// SMTPBanner is Postfix's smtpd_banner as postconf expands it, "" when
|
||||
// it could not be read (health.SMTPBanner).
|
||||
SMTPBanner 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
|
||||
@@ -146,6 +149,7 @@ func New(st *store.Store, domains *domain.Service, apps *app.Service, inboundSvc
|
||||
Version: cfg.Version,
|
||||
TLSCertFile: cfg.TLSCertFile,
|
||||
TLSServedCertFile: cfg.TLSServedCertFile,
|
||||
SMTPBanner: cfg.SMTPBanner,
|
||||
TLSReloadInterval: cfg.TLSReloadInterval,
|
||||
SignSocket: cfg.SignSocket,
|
||||
SignLocalSocket: cfg.SignLocalSocket,
|
||||
|
||||
Reference in New Issue
Block a user