add config option to an account destination to reject messages that don't pass a dmarc-like aligned spf/aligned dkim check

intended for automated processors that don't want to send messages to senders
without verified domains (because the address may be forged, and the processor
doesn't want to bother innocent bystanders).

such delivery attempts will fail with a permanent error immediately, typically
resulting in a DSN message to the original sender. the configurable error
message will normally be included in the DSN, so it could have alternative
instructions.
This commit is contained in:
Mechiel Lukkien
2025-02-15 17:32:31 +01:00
parent f33870ba85
commit 6da5f8f586
13 changed files with 108 additions and 9 deletions

View File

@ -84,6 +84,7 @@ const (
reasonSubjectpassError = "subjectpass-error"
reasonIPrev = "iprev" // No or mild junk reputation signals, and bad iprev.
reasonHighRate = "high-rate" // Too many messages, not added to rejects.
reasonMsgAuthRequired = "msg-auth-required"
)
func isListDomain(d delivery, ld dns.Domain) bool {
@ -396,6 +397,19 @@ func analyze(ctx context.Context, log mlog.Log, resolver dns.Resolver, d deliver
}
}
// We may have to reject messages that don't pass a relaxed aligned SPF and/or DKIM
// check. Useful for services with autoresponders.
if d.destination.MessageAuthRequiredSMTPError != "" && !d.m.MsgFromValidated {
code := smtp.C550MailboxUnavail
msg := d.destination.MessageAuthRequiredSMTPError
if d.dmarcResult.Status == dmarc.StatusTemperror {
code = smtp.C451LocalErr
msg = "transient verification error: " + msg
}
addReasonText("message does not pass required aligned spf and/or dkim check required for destination")
return reject(code, smtp.SePol7MultiAuthFails26, msg, nil, reasonMsgAuthRequired)
}
// Determine if message is acceptable based on DMARC domain, DKIM identities, or
// host-based reputation.
var isjunk *bool

View File

@ -2038,3 +2038,36 @@ func TestDestinationSMTPError(t *testing.T) {
ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SeAddr1UnknownDestMailbox1})
})
}
// TestDestinationMessageAuthRequiredSMTPError checks delivery to a destination
// with an MessageAuthRequiredSMTPError is accepted/rejected as configured.
func TestDestinationMessageAuthRequiredSMTPError(t *testing.T) {
resolver := dns.MockResolver{
A: map[string][]string{
"example.org.": {"127.0.0.10"}, // For mx check.
},
PTR: map[string][]string{
"127.0.0.10": {"example.org."},
},
TXT: map[string][]string{},
}
ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/mox.conf"), resolver)
defer ts.close()
ts.run(func(client *smtpclient.Client) {
mailFrom := "mjl@example.org"
rcptTo := "msgauthrequired@mox.example"
err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
ts.smtpErr(err, &smtpclient.Error{Permanent: true, Code: smtp.C550MailboxUnavail, Secode: smtp.SePol7MultiAuthFails26})
})
// Ensure SPF pass, message should now be accepted.
resolver.TXT["example.org."] = []string{"v=spf1 ip4:127.0.0.10 -all"}
ts.run(func(client *smtpclient.Client) {
mailFrom := "mjl@example.org"
rcptTo := "msgauthrequired@mox.example"
err := client.Deliver(ctxbg, mailFrom, rcptTo, int64(len(deliverMessage)), strings.NewReader(deliverMessage), false, false, false)
ts.smtpErr(err, nil)
})
}