mox/mox-/lookup.go
Mechiel Lukkien 794ef75d17
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.
2025-04-18 12:36:01 +02:00

152 lines
5.1 KiB
Go

package mox
import (
"errors"
"strings"
"github.com/mjl-/mox/config"
"github.com/mjl-/mox/dns"
"github.com/mjl-/mox/smtp"
)
var (
ErrDomainNotFound = errors.New("domain not found")
ErrDomainDisabled = errors.New("message/transaction involving temporarily disabled domain")
ErrAddressNotFound = errors.New("address not found")
)
// LookupAddress looks up the account for localpart and domain.
//
// Can return ErrDomainNotFound and ErrAddressNotFound. If checkDomainDisabled is
// set, returns ErrDomainDisabled if domain is disabled.
func LookupAddress(localpart smtp.Localpart, domain dns.Domain, allowPostmaster, allowAlias, checkDomainDisabled bool) (accountName string, alias *config.Alias, canonicalAddress string, dest config.Destination, rerr error) {
if strings.EqualFold(string(localpart), "postmaster") {
localpart = "postmaster"
}
postmasterDomain := func() bool {
var zerodomain dns.Domain
if domain == zerodomain || domain == Conf.Static.HostnameDomain {
return true
}
for _, l := range Conf.Static.Listeners {
if l.SMTP.Enabled && domain == l.HostnameDomain {
return true
}
}
return false
}
// Check for special mail host addresses.
if localpart == "postmaster" && postmasterDomain() {
if !allowPostmaster {
return "", nil, "", config.Destination{}, ErrAddressNotFound
}
return Conf.Static.Postmaster.Account, nil, "postmaster", config.Destination{Mailbox: Conf.Static.Postmaster.Mailbox}, nil
}
if localpart == Conf.Static.HostTLSRPT.ParsedLocalpart && domain == Conf.Static.HostnameDomain {
// Get destination, should always be present.
canonical := smtp.NewAddress(localpart, domain).String()
accAddr, a, ok := Conf.AccountDestination(canonical)
if !ok || a != nil {
return "", nil, "", config.Destination{}, ErrAddressNotFound
}
return accAddr.Account, nil, canonical, accAddr.Destination, nil
}
d, ok := Conf.Domain(domain)
if !ok || d.ReportsOnly {
// For ReportsOnly, we also return ErrDomainNotFound, so this domain isn't
// considered local/authoritative during delivery.
return "", nil, "", config.Destination{}, ErrDomainNotFound
}
if d.Disabled && checkDomainDisabled {
return "", nil, "", config.Destination{}, ErrDomainDisabled
}
localpart = CanonicalLocalpart(localpart, d)
canonical := smtp.NewAddress(localpart, domain).String()
accAddr, alias, ok := Conf.AccountDestination(canonical)
if ok && alias != nil {
if !allowAlias {
return "", nil, "", config.Destination{}, ErrAddressNotFound
}
return "", alias, canonical, config.Destination{}, nil
} else if !ok {
if accAddr, alias, ok = Conf.AccountDestination("@" + domain.Name()); !ok || alias != nil {
if localpart == "postmaster" && allowPostmaster {
return Conf.Static.Postmaster.Account, nil, "postmaster", config.Destination{Mailbox: Conf.Static.Postmaster.Mailbox}, nil
}
return "", nil, "", config.Destination{}, ErrAddressNotFound
}
canonical = "@" + domain.Name()
}
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
// 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 {
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 {
t := strings.SplitN(string(localpart), sep, 2)
localpart = smtp.Localpart(t[0])
}
return localpart
}
// AllowMsgFrom returns whether account is allowed to submit messages with address
// as message From header, based on configured addresses and membership of aliases
// that allow using its address.
func AllowMsgFrom(accountName string, msgFrom smtp.Address) (ok, domainDisabled bool) {
accName, alias, _, _, err := LookupAddress(msgFrom.Localpart, msgFrom.Domain, false, true, true)
if err != nil {
return false, errors.Is(err, ErrDomainDisabled)
}
if alias != nil && alias.AllowMsgFrom {
for _, aa := range alias.ParsedAddresses {
if aa.AccountName == accountName {
return true, false
}
}
return false, false
}
return accName == accountName, false
}