accept incoming DMARC and TLS reports with reporting addresses containing catchall separator(s)

Such as "-" when addresses are dmarc-reports@ and tls-reports@.

Existing configuration files can have these combinations. We don't allow them
to be created through the webadmin interface, as this is a likely source of
confusion about how addresses will be matched. We already didn't allow regular
addresses containing catchall separators.
This commit is contained in:
Mechiel Lukkien
2025-04-18 12:34:07 +02:00
parent 4eddf5885d
commit 794ef75d17
11 changed files with 175 additions and 27 deletions

View File

@ -2535,6 +2535,23 @@ func (Admin) DomainClientSettingsDomainSave(ctx context.Context, domainName, cli
// settings for a domain.
func (Admin) DomainLocalpartConfigSave(ctx context.Context, domainName string, localpartCatchallSeparators []string, localpartCaseSensitive bool) {
err := admin.DomainSave(ctx, domainName, func(domain *config.Domain) error {
// We don't allow introducing new catchall separators that are used in DMARC/TLS
// reporting. Can occur in existing configs for backwards compatibility.
containsSep := func(seps []string) bool {
for _, sep := range seps {
if domain.DMARC != nil && strings.Contains(domain.DMARC.Localpart, sep) {
return true
}
if domain.TLSRPT != nil && strings.Contains(domain.TLSRPT.Localpart, sep) {
return true
}
}
return false
}
if !containsSep(domain.LocalpartCatchallSeparatorsEffective) && containsSep(localpartCatchallSeparators) {
xusererrorf(ctx, "cannot add localpart catchall separators that are used in dmarc and/or tls reporting addresses, change reporting addresses first")
}
domain.LocalpartCatchallSeparatorsEffective = localpartCatchallSeparators
// If there is a single separator, we prefer the non-list form, it's easier to
// read/edit and should suffice for most setups.
@ -2557,6 +2574,17 @@ func (Admin) DomainLocalpartConfigSave(ctx context.Context, domainName string, l
// disabled.
func (Admin) DomainDMARCAddressSave(ctx context.Context, domainName, localpart, domain, account, mailbox string) {
err := admin.DomainSave(ctx, domainName, func(d *config.Domain) error {
// DMARC reporting addresses can contain the localpart catchall separator(s) for
// backwards compability (hence not enforced when parsing the config files), but we
// don't allow creating them.
if d.DMARC == nil || d.DMARC.Localpart != localpart {
for _, sep := range d.LocalpartCatchallSeparatorsEffective {
if strings.Contains(localpart, sep) {
xusererrorf(ctx, "dmarc reporting address cannot contain catchall separator %q in localpart (%q)", sep, localpart)
}
}
}
if localpart == "" {
d.DMARC = nil
} else {
@ -2577,6 +2605,17 @@ func (Admin) DomainDMARCAddressSave(ctx context.Context, domainName, localpart,
// disabled.
func (Admin) DomainTLSRPTAddressSave(ctx context.Context, domainName, localpart, domain, account, mailbox string) {
err := admin.DomainSave(ctx, domainName, func(d *config.Domain) error {
// TLS reporting addresses can contain the localpart catchall separator(s) for
// backwards compability (hence not enforced when parsing the config files), but we
// don't allow creating them.
if d.TLSRPT == nil || d.TLSRPT.Localpart != localpart {
for _, sep := range d.LocalpartCatchallSeparatorsEffective {
if strings.Contains(localpart, sep) {
xusererrorf(ctx, "tls reporting address cannot contain catchall separator %q in localpart (%q)", sep, localpart)
}
}
}
if localpart == "" {
d.TLSRPT = nil
} else {

View File

@ -279,17 +279,25 @@ func TestAdmin(t *testing.T) {
api.DomainLocalpartConfigSave(ctxbg, "mox.example", []string{"-"}, true)
tneedErrorCode(t, "user:error", func() { api.DomainLocalpartConfigSave(ctxbg, "bogus.example", nil, false) })
api.DomainLocalpartConfigSave(ctxbg, "mox.example", nil, false) // Restore.
api.DomainDMARCAddressSave(ctxbg, "mox.example", "dmarcreports", "", "mjl", "DMARC")
api.DomainDMARCAddressSave(ctxbg, "mox.example", "dmarc+reports", "", "mjl", "DMARC")
// Catchall separator, bad domain, bad account.
tneedErrorCode(t, "user:error", func() { api.DomainDMARCAddressSave(ctxbg, "mox.example", "dmarc-reports", "", "mjl", "DMARC") })
tneedErrorCode(t, "user:error", func() { api.DomainDMARCAddressSave(ctxbg, "bogus.example", "dmarcreports", "", "mjl", "DMARC") })
tneedErrorCode(t, "user:error", func() { api.DomainDMARCAddressSave(ctxbg, "mox.example", "dmarcreports", "", "bogus", "DMARC") })
api.DomainDMARCAddressSave(ctxbg, "mox.example", "", "", "", "") // Restore.
api.DomainTLSRPTAddressSave(ctxbg, "mox.example", "tlsreports", "", "mjl", "TLSRPT")
api.DomainTLSRPTAddressSave(ctxbg, "mox.example", "tls+reports", "", "mjl", "TLSRPT")
// Catchall separator, bad domain, bad account.
tneedErrorCode(t, "user:error", func() { api.DomainTLSRPTAddressSave(ctxbg, "mox.example", "tls-reports", "", "mjl", "TLSRPT") })
tneedErrorCode(t, "user:error", func() { api.DomainTLSRPTAddressSave(ctxbg, "bogus.example", "tlsreports", "", "mjl", "TLSRPT") })
tneedErrorCode(t, "user:error", func() { api.DomainTLSRPTAddressSave(ctxbg, "mox.example", "tlsreports", "", "bogus", "TLSRPT") })
// DMARC/TLS reporting addresses contain separator.
tneedErrorCode(t, "user:error", func() { api.DomainLocalpartConfigSave(ctxbg, "mox.example", []string{"+"}, true) })
api.DomainDMARCAddressSave(ctxbg, "mox.example", "", "", "", "") // Restore.
api.DomainTLSRPTAddressSave(ctxbg, "mox.example", "", "", "", "") // Restore.
api.DomainLocalpartConfigSave(ctxbg, "mox.example", nil, false) // Restore.
// todo: cannot enable mta-sts because we have no listener, which would require a tls cert for the domain.
// api.DomainMTASTSSave(ctxbg, "mox.example", "id0", mtasts.ModeEnforce, time.Hour, []string{"mail.mox.example"})

View File

@ -3693,7 +3693,7 @@
},
{
"Name": "ParsedLocalpart",
"Docs": "",
"Docs": "Lower-case if case-sensitivity is not configured for domain. Not \"canonical\" for catchall separators for backwards compatibility.",
"Typewords": [
"Localpart"
]
@ -3776,7 +3776,7 @@
},
{
"Name": "ParsedLocalpart",
"Docs": "",
"Docs": "Lower-case if case-sensitivity is not configured for domain. Not \"canonical\" for catchall separators for backwards compatibility.",
"Typewords": [
"Localpart"
]

View File

@ -309,7 +309,7 @@ export interface DMARC {
Domain: string
Account: string
Mailbox: string
ParsedLocalpart: Localpart
ParsedLocalpart: Localpart // Lower-case if case-sensitivity is not configured for domain. Not "canonical" for catchall separators for backwards compatibility.
DNSDomain: Domain // Effective domain, always set based on Domain field or Domain where this is configured.
}
@ -325,7 +325,7 @@ export interface TLSRPT {
Domain: string
Account: string
Mailbox: string
ParsedLocalpart: Localpart
ParsedLocalpart: Localpart // Lower-case if case-sensitivity is not configured for domain. Not "canonical" for catchall separators for backwards compatibility.
DNSDomain: Domain // Effective domain, always set based on Domain field or Domain where this is configured.
}