e6910c2c2d
The domain page generated and displayed the DKIM record but said only "also configure SPF and DMARC for the domain (see the documentation)". The concrete example existed — buried in the check's remediation text, and only visible once the check had already failed. Show both records up front, host and value with a Copy button, the way the DKIM record is shown, plus the two things that actually bite: a domain may carry only one SPF record (add the mechanism to the existing one rather than publishing a second), and p=none is safe to publish immediately. The SPF value names the addresses SELFPOST_HOSTNAME resolves to, taken from the hostname check the page already runs, and falls back to an "a:" mechanism when it does not resolve. New dnscheck.SPFExample/DMARCExample are the single source for both the page and the checks' advice, so the two cannot drift into recommending different records; dnscheck.Query gains Hostname for the fallback. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
50 lines
1.9 KiB
Go
50 lines
1.9 KiB
Go
package dnscheck
|
|
|
|
import (
|
|
"net"
|
|
"strings"
|
|
)
|
|
|
|
// SPF and DMARC are the two records SelfPost cannot generate for the operator:
|
|
// unlike the DKIM record they are policy, not a key, and a domain may already
|
|
// publish one for other senders. The panel still has to say what "correct"
|
|
// looks like, and the checks below have to suggest the same thing when a record
|
|
// is missing — so both take their example from here rather than each spelling
|
|
// out its own.
|
|
|
|
// DMARCRecordName is the name a DMARC record is published at. (SPF has no such
|
|
// helper: it is published at the domain itself.)
|
|
func DMARCRecordName(domainName string) string { return "_dmarc." + domainName }
|
|
|
|
// SPFExample is the SPF record this server expects for a sending domain: the
|
|
// addresses its mail actually leaves from, and "-all" to say that nothing else
|
|
// is authorised. When the server's own addresses are not known (its hostname
|
|
// does not resolve) it falls back to an "a:" mechanism naming the host, so the
|
|
// panel always has something concrete to show.
|
|
func SPFExample(hostname string, serverIPs []string) string {
|
|
var mechanisms []string
|
|
for _, s := range serverIPs {
|
|
ip := net.ParseIP(strings.TrimSpace(s))
|
|
switch {
|
|
case ip == nil:
|
|
continue
|
|
case ip.To4() != nil:
|
|
mechanisms = append(mechanisms, "ip4:"+ip.String())
|
|
default:
|
|
mechanisms = append(mechanisms, "ip6:"+ip.String())
|
|
}
|
|
}
|
|
if len(mechanisms) == 0 {
|
|
mechanisms = []string{"a:" + hostname}
|
|
}
|
|
return "v=spf1 " + strings.Join(mechanisms, " ") + " -all"
|
|
}
|
|
|
|
// DMARCExample is the least a domain should publish: monitoring only, with an
|
|
// address the aggregate reports go to. p=none is deliberate — it changes
|
|
// nothing about delivery, so it is safe to publish before the reports have
|
|
// shown that DKIM and SPF pass everywhere.
|
|
func DMARCExample(domainName string) string {
|
|
return "v=DMARC1; p=none; rua=mailto:dmarc@" + domainName
|
|
}
|