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
No known key found for this signature in database
11 changed files with 175 additions and 27 deletions

View File

@ -329,7 +329,7 @@ type DMARC struct {
Account string `sconf-doc:"Account to deliver to."` Account string `sconf-doc:"Account to deliver to."`
Mailbox string `sconf-doc:"Mailbox to deliver to, e.g. DMARC."` Mailbox string `sconf-doc:"Mailbox to deliver to, e.g. DMARC."`
ParsedLocalpart smtp.Localpart `sconf:"-"` ParsedLocalpart smtp.Localpart `sconf:"-"` // Lower-case if case-sensitivity is not configured for domain. Not "canonical" for catchall separators for backwards compatibility.
DNSDomain dns.Domain `sconf:"-"` // Effective domain, always set based on Domain field or Domain where this is configured. DNSDomain dns.Domain `sconf:"-"` // Effective domain, always set based on Domain field or Domain where this is configured.
} }
@ -347,7 +347,7 @@ type TLSRPT struct {
Account string `sconf-doc:"Account to deliver to."` Account string `sconf-doc:"Account to deliver to."`
Mailbox string `sconf-doc:"Mailbox to deliver to, e.g. TLSRPT."` Mailbox string `sconf-doc:"Mailbox to deliver to, e.g. TLSRPT."`
ParsedLocalpart smtp.Localpart `sconf:"-"` ParsedLocalpart smtp.Localpart `sconf:"-"` // Lower-case if case-sensitivity is not configured for domain. Not "canonical" for catchall separators for backwards compatibility.
DNSDomain dns.Domain `sconf:"-"` // Effective domain, always set based on Domain field or Domain where this is configured. DNSDomain dns.Domain `sconf:"-"` // Effective domain, always set based on Domain field or Domain where this is configured.
} }

View File

@ -1747,6 +1747,8 @@ func prepareDynamicConfig(ctx context.Context, log mlog.Log, dynamicPath string,
if _, ok := c.Accounts[dmarc.Account]; !ok { if _, ok := c.Accounts[dmarc.Account]; !ok {
addDomainErrorf("DMARC account %q does not exist", dmarc.Account) addDomainErrorf("DMARC account %q does not exist", dmarc.Account)
} }
// Note: For backwards compabilitiy, DMARC reporting localparts can contain catchall separators.
lp, err := smtp.ParseLocalpart(dmarc.Localpart) lp, err := smtp.ParseLocalpart(dmarc.Localpart)
if err != nil { if err != nil {
addDomainErrorf("invalid DMARC localpart %q: %s", dmarc.Localpart, err) addDomainErrorf("invalid DMARC localpart %q: %s", dmarc.Localpart, err)
@ -1760,9 +1762,13 @@ func prepareDynamicConfig(ctx context.Context, log mlog.Log, dynamicPath string,
addrdom, err = dns.ParseDomain(dmarc.Domain) addrdom, err = dns.ParseDomain(dmarc.Domain)
if err != nil { if err != nil {
addDomainErrorf("DMARC domain %q: %s", dmarc.Domain, err) addDomainErrorf("DMARC domain %q: %s", dmarc.Domain, err)
} else if _, ok := c.Domains[addrdom.Name()]; !ok { } else if adomain, ok := c.Domains[addrdom.Name()]; !ok {
addDomainErrorf("unknown domain %q for DMARC address", addrdom) addDomainErrorf("unknown domain %q for DMARC address", addrdom)
} else if !adomain.LocalpartCaseSensitive {
lp = smtp.Localpart(strings.ToLower(string(lp)))
} }
} else if !domain.LocalpartCaseSensitive {
lp = smtp.Localpart(strings.ToLower(string(lp)))
} }
if addrdom == domain.Domain { if addrdom == domain.Domain {
domainHasAddress[addrdom.Name()] = true domainHasAddress[addrdom.Name()] = true
@ -1793,6 +1799,8 @@ func prepareDynamicConfig(ctx context.Context, log mlog.Log, dynamicPath string,
if _, ok := c.Accounts[tlsrpt.Account]; !ok { if _, ok := c.Accounts[tlsrpt.Account]; !ok {
addDomainErrorf("TLSRPT account %q does not exist", tlsrpt.Account) addDomainErrorf("TLSRPT account %q does not exist", tlsrpt.Account)
} }
// Note: For backwards compabilitiy, TLS reporting localparts can contain catchall separators.
lp, err := smtp.ParseLocalpart(tlsrpt.Localpart) lp, err := smtp.ParseLocalpart(tlsrpt.Localpart)
if err != nil { if err != nil {
addDomainErrorf("invalid TLSRPT localpart %q: %s", tlsrpt.Localpart, err) addDomainErrorf("invalid TLSRPT localpart %q: %s", tlsrpt.Localpart, err)
@ -1807,9 +1815,13 @@ func prepareDynamicConfig(ctx context.Context, log mlog.Log, dynamicPath string,
addrdom, err = dns.ParseDomain(tlsrpt.Domain) addrdom, err = dns.ParseDomain(tlsrpt.Domain)
if err != nil { if err != nil {
addDomainErrorf("TLSRPT domain %q: %s", tlsrpt.Domain, err) addDomainErrorf("TLSRPT domain %q: %s", tlsrpt.Domain, err)
} else if _, ok := c.Domains[addrdom.Name()]; !ok { } else if adomain, ok := c.Domains[addrdom.Name()]; !ok {
addDomainErrorf("unknown domain %q for TLSRPT address", tlsrpt.Domain) addDomainErrorf("unknown domain %q for TLSRPT address", tlsrpt.Domain)
} else if !adomain.LocalpartCaseSensitive {
lp = smtp.Localpart(strings.ToLower(string(lp)))
} }
} else if !domain.LocalpartCaseSensitive {
lp = smtp.Localpart(strings.ToLower(string(lp)))
} }
if addrdom == domain.Domain { if addrdom == domain.Domain {
domainHasAddress[addrdom.Name()] = true domainHasAddress[addrdom.Name()] = true

View File

@ -85,17 +85,49 @@ func LookupAddress(localpart smtp.Localpart, domain dns.Domain, allowPostmaster,
return accAddr.Account, nil, canonical, accAddr.Destination, nil return accAddr.Account, nil, canonical, accAddr.Destination, nil
} }
// lp and rlp are both lower-case when domain localparts aren't case sensitive.
func matchReportingSeparators(lp, rlp smtp.Localpart, d config.Domain) bool {
lps := string(lp)
rlps := string(rlp)
if !strings.HasPrefix(lps, rlps) {
return false
}
if len(lps) == len(rlps) {
return true
}
rem := lps[len(rlps):]
for _, sep := range d.LocalpartCatchallSeparatorsEffective {
if strings.HasPrefix(rem, sep) {
return true
}
}
return false
}
// CanonicalLocalpart returns the canonical localpart, removing optional catchall // CanonicalLocalpart returns the canonical localpart, removing optional catchall
// separators, and optionally lower-casing the string. // separators, and optionally lower-casing the string.
// The DMARC and TLS reporting addresses are treated specially, they may contain a
// localpart catchall separator for historic configurations (not for new
// configurations). We try to match them first, still taking additional localpart
// catchall separators into account.
func CanonicalLocalpart(localpart smtp.Localpart, d config.Domain) smtp.Localpart { func CanonicalLocalpart(localpart smtp.Localpart, d config.Domain) smtp.Localpart {
if !d.LocalpartCaseSensitive {
localpart = smtp.Localpart(strings.ToLower(string(localpart)))
}
if d.DMARC != nil && matchReportingSeparators(localpart, d.DMARC.ParsedLocalpart, d) {
return d.DMARC.ParsedLocalpart
}
if d.TLSRPT != nil && matchReportingSeparators(localpart, d.TLSRPT.ParsedLocalpart, d) {
return d.TLSRPT.ParsedLocalpart
}
for _, sep := range d.LocalpartCatchallSeparatorsEffective { for _, sep := range d.LocalpartCatchallSeparatorsEffective {
t := strings.SplitN(string(localpart), sep, 2) t := strings.SplitN(string(localpart), sep, 2)
localpart = smtp.Localpart(t[0]) localpart = smtp.Localpart(t[0])
} }
if !d.LocalpartCaseSensitive {
localpart = smtp.Localpart(strings.ToLower(string(localpart)))
}
return localpart return localpart
} }

View File

@ -1094,13 +1094,12 @@ func TestDMARCReport(t *testing.T) {
ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/dmarcreport/mox.conf"), resolver) ts := newTestServer(t, filepath.FromSlash("../testdata/smtp/dmarcreport/mox.conf"), resolver)
defer ts.close() defer ts.close()
run := func(report string, n int) { run := func(rcptTo, report string, n int) {
t.Helper() t.Helper()
ts.run(func(client *smtpclient.Client) { ts.run(func(client *smtpclient.Client) {
t.Helper() t.Helper()
mailFrom := "remote@example.org" mailFrom := "remote@example.org"
rcptTo := "mjl@mox.example"
msgb := &bytes.Buffer{} msgb := &bytes.Buffer{}
_, xerr := fmt.Fprintf(msgb, "From: %s\r\nTo: %s\r\nSubject: dmarc report\r\nMIME-Version: 1.0\r\nContent-Type: text/xml\r\n\r\n", mailFrom, rcptTo) _, xerr := fmt.Fprintf(msgb, "From: %s\r\nTo: %s\r\nSubject: dmarc report\r\nMIME-Version: 1.0\r\nContent-Type: text/xml\r\n\r\n", mailFrom, rcptTo)
@ -1121,13 +1120,33 @@ func TestDMARCReport(t *testing.T) {
}) })
} }
run(dmarcReport, 0) n := 0
run(strings.ReplaceAll(dmarcReport, "xmox.nl", "mox.example"), 1) run("dmarc-reports@mox.example", dmarcReport, 0) // Wrong domain in report.
report := strings.ReplaceAll(dmarcReport, "xmox.nl", "mox.example")
n++
run("dmarc-reports@mox.example", report, n)
// We always store as an evaluation, but as optional for reports. // We always store as an evaluation, but as optional for reports.
evals := checkEvaluationCount(t, 2) evals := checkEvaluationCount(t, 2)
tcompare(t, evals[0].Optional, true) tcompare(t, evals[0].Optional, true)
tcompare(t, evals[1].Optional, true) tcompare(t, evals[1].Optional, true)
// Not a dmarc recipient, delivery should succeed.
run("mjl@mox.example", report, n)
run("mjl-test@mox.example", report, n)
run("mjl+test@mox.example", report, n)
// Likewise, address that is prefix of reporting address.
run("dmarc@mox.example", report, n)
run("Dmarc-test@mox.example", report, n)
run("dmarc+test@mox.example", report, n)
// Localpart catchall separators work for dmarc reporting addresses too.
n++
run("Dmarc-reports-test@mox.example", report, n)
n++
run("dmarc-Reports+test@mox.example", report, n)
} }
const dmarcReport = `<?xml version="1.0" encoding="UTF-8" ?> const dmarcReport = `<?xml version="1.0" encoding="UTF-8" ?>
@ -1245,17 +1264,39 @@ func TestTLSReport(t *testing.T) {
}) })
} }
const tlsrpt = `{"organization-name":"Example.org","date-range":{"start-datetime":"2022-01-07T00:00:00Z","end-datetime":"2022-01-07T23:59:59Z"},"contact-info":"tlsrpt@example.org","report-id":"1","policies":[{"policy":{"policy-type":"no-policy-found","policy-domain":"xmox.nl"},"summary":{"total-successful-session-count":1,"total-failure-session-count":0}}]}` tlsrpt := `{"organization-name":"Example.org","date-range":{"start-datetime":"2022-01-07T00:00:00Z","end-datetime":"2022-01-07T23:59:59Z"},"contact-info":"tlsrpt@example.org","report-id":"1","policies":[{"policy":{"policy-type":"no-policy-found","policy-domain":"xmox.nl"},"summary":{"total-successful-session-count":1,"total-failure-session-count":0}}]}`
run("mjl@mox.example", tlsrpt, 0) n := 0
run("mjl@mox.example", strings.ReplaceAll(tlsrpt, "xmox.nl", "mox.example"), 1) run("tls-reports@mox.example", tlsrpt, n) // Wrong domain in report.
run("mjl@mailhost.mox.example", strings.ReplaceAll(tlsrpt, "xmox.nl", "mailhost.mox.example"), 2)
tlsrptdom := strings.ReplaceAll(tlsrpt, "xmox.nl", "mox.example")
n++
run("tls-reports@mox.example", tlsrptdom, n)
tlsrpthost := strings.ReplaceAll(tlsrpt, "xmox.nl", "mailhost.mox.example")
n++
run("tls-reports@mailhost.mox.example", tlsrpthost, n)
// We always store as an evaluation, but as optional for reports. // We always store as an evaluation, but as optional for reports.
evals := checkEvaluationCount(t, 3) evals := checkEvaluationCount(t, 3)
tcompare(t, evals[0].Optional, true) tcompare(t, evals[0].Optional, true)
tcompare(t, evals[1].Optional, true) tcompare(t, evals[1].Optional, true)
tcompare(t, evals[2].Optional, true) tcompare(t, evals[2].Optional, true)
// Catchall separators work for reporting address too.
n++
run("Tls-reports+more@mox.example", tlsrptdom, n)
n++
run("tls-Reports-more@mox.example", tlsrptdom, n)
// Non-reporting addresses, mail accepted, but not as report.
run("mjl@mox.example", tlsrptdom, n)
run("Mjl-other@mox.example", tlsrptdom, n)
run("mjl+other@mox.example", tlsrptdom, n)
// Likewise, address that is prefix of reporting address.
run("tls@mox.example", tlsrptdom, n)
run("Tls-other@mox.example", tlsrptdom, n)
run("tls+other@mox.example", tlsrptdom, n)
} }
func TestRatelimitConnectionrate(t *testing.T) { func TestRatelimitConnectionrate(t *testing.T) {

View File

@ -1,11 +1,19 @@
Domains: Domains:
mox.example: mox.example:
DMARC: DMARC:
Account: mjl Account: dmarc
Localpart: mjl # Localpart contains a catchall separator
Localpart: dmarc-reports
Mailbox: DMARC Mailbox: DMARC
LocalpartCatchallSeparators:
- +
- -
Accounts: Accounts:
mjl: mjl:
Domain: mox.example Domain: mox.example
Destinations: Destinations:
mjl@mox.example: nil mjl@mox.example: nil
dmarc:
Domain: mox.example
Destinations:
dmarc@mox.example: nil

View File

@ -2,10 +2,18 @@ Domains:
mox.example: mox.example:
TLSRPT: TLSRPT:
Account: mjl Account: mjl
Localpart: mjl # Localpart contains a catchall separator
Localpart: tls-reports
Mailbox: TLSRPT Mailbox: TLSRPT
LocalpartCatchallSeparators:
- +
- -
Accounts: Accounts:
mjl: mjl:
Domain: mox.example Domain: mox.example
Destinations: Destinations:
mjl@mox.example: nil mjl@mox.example: nil
tls:
Domain: mox.example
Destinations:
tls@mox.example: nil

View File

@ -10,4 +10,4 @@ Listeners:
HostTLSRPT: HostTLSRPT:
Account: mjl Account: mjl
Mailbox: TLSRPT Mailbox: TLSRPT
Localpart: mjl Localpart: tls-reports

View File

@ -2535,6 +2535,23 @@ func (Admin) DomainClientSettingsDomainSave(ctx context.Context, domainName, cli
// settings for a domain. // settings for a domain.
func (Admin) DomainLocalpartConfigSave(ctx context.Context, domainName string, localpartCatchallSeparators []string, localpartCaseSensitive bool) { func (Admin) DomainLocalpartConfigSave(ctx context.Context, domainName string, localpartCatchallSeparators []string, localpartCaseSensitive bool) {
err := admin.DomainSave(ctx, domainName, func(domain *config.Domain) error { 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 domain.LocalpartCatchallSeparatorsEffective = localpartCatchallSeparators
// If there is a single separator, we prefer the non-list form, it's easier to // If there is a single separator, we prefer the non-list form, it's easier to
// read/edit and should suffice for most setups. // read/edit and should suffice for most setups.
@ -2557,6 +2574,17 @@ func (Admin) DomainLocalpartConfigSave(ctx context.Context, domainName string, l
// disabled. // disabled.
func (Admin) DomainDMARCAddressSave(ctx context.Context, domainName, localpart, domain, account, mailbox string) { func (Admin) DomainDMARCAddressSave(ctx context.Context, domainName, localpart, domain, account, mailbox string) {
err := admin.DomainSave(ctx, domainName, func(d *config.Domain) error { 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 == "" { if localpart == "" {
d.DMARC = nil d.DMARC = nil
} else { } else {
@ -2577,6 +2605,17 @@ func (Admin) DomainDMARCAddressSave(ctx context.Context, domainName, localpart,
// disabled. // disabled.
func (Admin) DomainTLSRPTAddressSave(ctx context.Context, domainName, localpart, domain, account, mailbox string) { func (Admin) DomainTLSRPTAddressSave(ctx context.Context, domainName, localpart, domain, account, mailbox string) {
err := admin.DomainSave(ctx, domainName, func(d *config.Domain) error { 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 == "" { if localpart == "" {
d.TLSRPT = nil d.TLSRPT = nil
} else { } else {

View File

@ -279,17 +279,25 @@ func TestAdmin(t *testing.T) {
api.DomainLocalpartConfigSave(ctxbg, "mox.example", []string{"-"}, true) api.DomainLocalpartConfigSave(ctxbg, "mox.example", []string{"-"}, true)
tneedErrorCode(t, "user:error", func() { api.DomainLocalpartConfigSave(ctxbg, "bogus.example", nil, false) }) 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, "bogus.example", "dmarcreports", "", "mjl", "DMARC") })
tneedErrorCode(t, "user:error", func() { api.DomainDMARCAddressSave(ctxbg, "mox.example", "dmarcreports", "", "bogus", "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, "bogus.example", "tlsreports", "", "mjl", "TLSRPT") })
tneedErrorCode(t, "user:error", func() { api.DomainTLSRPTAddressSave(ctxbg, "mox.example", "tlsreports", "", "bogus", "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.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. // 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"}) // api.DomainMTASTSSave(ctxbg, "mox.example", "id0", mtasts.ModeEnforce, time.Hour, []string{"mail.mox.example"})

View File

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

View File

@ -309,7 +309,7 @@ export interface DMARC {
Domain: string Domain: string
Account: string Account: string
Mailbox: 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. 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 Domain: string
Account: string Account: string
Mailbox: 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. DNSDomain: Domain // Effective domain, always set based on Domain field or Domain where this is configured.
} }