server-dmarc-hint: the server's own name gets a DMARC record
Owner, 2026-10-18: "Оба предложения делаем"; the mockup is 8b2dc8f. The
security audit's S3 left a product side: the panel showed the DKIM and SPF
records of the server's own name, not a DMARC one, and without it a
receiver does not refuse mail forged in that name - a false password-reset
message among it.
dnscheck.ServerMail checks a third record (Mail.DMARC, checkServerDMARC):
the sending-domain DMARC check held to more - missing is an error, as a
missing DKIM or SPF record of this name is (the page draws it as a
warning: notifications still go), p=none a warning ("weak": it refuses
nothing), quarantine or reject ok - and Overall includes it. The record to
publish is ServerMailDMARC, v=DMARC1; p=reject: only the panel sends as
the name, signed with its key, from the address its SPF names, so reject
costs nothing. Settings › Notification mail draws it after SPF
(view.ServerMailDMARC, folded when published), Health's card has the row,
Re-check DNS re-reads it with the others.
Tests: TestServerMailDMARC (missing, p=none, quarantine, reject); the
Settings and Health tests publish and expect three records. Help word for
word with the mockup; guide § Notifications; roadmap; CHANGELOG.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -188,6 +188,7 @@ type Mail struct {
|
||||
Name string
|
||||
DKIM Result
|
||||
SPF Result
|
||||
DMARC Result
|
||||
Overall health.Status
|
||||
CheckedAt time.Time
|
||||
}
|
||||
@@ -196,9 +197,17 @@ type Mail struct {
|
||||
// address record is the one sender.
|
||||
const ServerMailSPF = "v=spf1 a -all"
|
||||
|
||||
// ServerMail checks the DKIM and SPF records of the server's own name. q is as
|
||||
// for a domain, with Name the hostname. Cached like a domain; force skips the
|
||||
// cache.
|
||||
// ServerMailDMARC is the DMARC record to publish for the server's own name
|
||||
// (server-dmarc-hint; security audit 2026-10-17, S3). Only the panel sends as
|
||||
// that name - from postmaster@ it, signed with the name's own key, from the
|
||||
// address its SPF names - so every genuine message aligns and reject costs
|
||||
// nothing; without a record a receiver does not refuse mail forged in the
|
||||
// server's name, a false password-reset message among it.
|
||||
const ServerMailDMARC = "v=DMARC1; p=reject"
|
||||
|
||||
// ServerMail checks the DKIM, SPF and DMARC records of the server's own name.
|
||||
// q is as for a domain, with Name the hostname. Cached like a domain; force
|
||||
// skips the cache.
|
||||
func (c *Checker) ServerMail(q Query, force bool) Mail {
|
||||
key := "\x00mail\x00" + q.Name
|
||||
if !force {
|
||||
@@ -214,16 +223,17 @@ func (c *Checker) ServerMail(q Query, force bool) Mail {
|
||||
defer cancel()
|
||||
m := Mail{Name: q.Name, CheckedAt: time.Now()}
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(2)
|
||||
wg.Add(3)
|
||||
go func() { defer wg.Done(); m.DKIM = c.checkDKIM(ctx, q) }()
|
||||
go func() { defer wg.Done(); m.SPF = c.checkSPF(ctx, q) }()
|
||||
go func() { defer wg.Done(); m.DMARC = c.checkServerDMARC(ctx, q.Name) }()
|
||||
wg.Wait()
|
||||
// checkSPF words a missing record for a sending domain; for the server's
|
||||
// own name the record to publish is a different one.
|
||||
if m.SPF.Status == health.StatusError && len(m.SPF.Records) == 0 {
|
||||
m.SPF.Detail = fmt.Sprintf("No SPF record is published for %s. Publish the TXT record %q - without it a receiver has nothing authorising this server to send as its own name.", q.Name, ServerMailSPF)
|
||||
}
|
||||
m.Overall = health.Worst(m.DKIM.Status, m.SPF.Status)
|
||||
m.Overall = health.Worst(m.DKIM.Status, m.SPF.Status, m.DMARC.Status)
|
||||
|
||||
c.mu.Lock()
|
||||
c.mail[key] = cached[Mail]{value: m, expires: m.CheckedAt.Add(c.domainTTL)}
|
||||
|
||||
Reference in New Issue
Block a user