Initial feature-complete implementation of the v1.0 specification (phases 0-11 of `docs/implementation-plan.md`). Added: - Panel (Go, single static binary) with SQLite persistence, one-time - Domain management with per-domain DKIM (RSA-2048, generated in pure Go) and - Application (sender identity) management: SASL credentials via `sasldb2`, - Full Postfix relay config generated from env at container start: SMTPS 465, - Journal milter (pure Go, `go-milter`) recording every send to `send_log`; … (6 more in CHANGELOG) Full detail: CHANGELOG.md (0.1.0).
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
|
|
}
|