1package mox
2
3import (
4 "errors"
5 "strings"
6
7 "github.com/mjl-/mox/config"
8 "github.com/mjl-/mox/dns"
9 "github.com/mjl-/mox/smtp"
10)
11
12var (
13 ErrDomainNotFound = errors.New("domain not found")
14 ErrDomainDisabled = errors.New("message/transaction involving temporarily disabled domain")
15 ErrAddressNotFound = errors.New("address not found")
16)
17
18// LookupAddress looks up the account for localpart and domain.
19//
20// Can return ErrDomainNotFound and ErrAddressNotFound. If checkDomainDisabled is
21// set, returns ErrDomainDisabled if domain is disabled.
22func LookupAddress(localpart smtp.Localpart, domain dns.Domain, allowPostmaster, allowAlias, checkDomainDisabled bool) (accountName string, alias *config.Alias, canonicalAddress string, dest config.Destination, rerr error) {
23 if strings.EqualFold(string(localpart), "postmaster") {
24 localpart = "postmaster"
25 }
26
27 postmasterDomain := func() bool {
28 var zerodomain dns.Domain
29 if domain == zerodomain || domain == Conf.Static.HostnameDomain {
30 return true
31 }
32 for _, l := range Conf.Static.Listeners {
33 if l.SMTP.Enabled && domain == l.HostnameDomain {
34 return true
35 }
36 }
37 return false
38 }
39
40 // Check for special mail host addresses.
41 if localpart == "postmaster" && postmasterDomain() {
42 if !allowPostmaster {
43 return "", nil, "", config.Destination{}, ErrAddressNotFound
44 }
45 return Conf.Static.Postmaster.Account, nil, "postmaster", config.Destination{Mailbox: Conf.Static.Postmaster.Mailbox}, nil
46 }
47 if localpart == Conf.Static.HostTLSRPT.ParsedLocalpart && domain == Conf.Static.HostnameDomain {
48 // Get destination, should always be present.
49 canonical := smtp.NewAddress(localpart, domain).String()
50 accAddr, a, ok := Conf.AccountDestination(canonical)
51 if !ok || a != nil {
52 return "", nil, "", config.Destination{}, ErrAddressNotFound
53 }
54 return accAddr.Account, nil, canonical, accAddr.Destination, nil
55 }
56
57 d, ok := Conf.Domain(domain)
58 if !ok || d.ReportsOnly {
59 // For ReportsOnly, we also return ErrDomainNotFound, so this domain isn't
60 // considered local/authoritative during delivery.
61 return "", nil, "", config.Destination{}, ErrDomainNotFound
62 }
63 if d.Disabled && checkDomainDisabled {
64 return "", nil, "", config.Destination{}, ErrDomainDisabled
65 }
66
67 localpart = CanonicalLocalpart(localpart, d)
68 canonical := smtp.NewAddress(localpart, domain).String()
69
70 accAddr, alias, ok := Conf.AccountDestination(canonical)
71 if ok && alias != nil {
72 if !allowAlias {
73 return "", nil, "", config.Destination{}, ErrAddressNotFound
74 }
75 return "", alias, canonical, config.Destination{}, nil
76 } else if !ok {
77 if accAddr, alias, ok = Conf.AccountDestination("@" + domain.Name()); !ok || alias != nil {
78 if localpart == "postmaster" && allowPostmaster {
79 return Conf.Static.Postmaster.Account, nil, "postmaster", config.Destination{Mailbox: Conf.Static.Postmaster.Mailbox}, nil
80 }
81 return "", nil, "", config.Destination{}, ErrAddressNotFound
82 }
83 canonical = "@" + domain.Name()
84 }
85 return accAddr.Account, nil, canonical, accAddr.Destination, nil
86}
87
88// lp and rlp are both lower-case when domain localparts aren't case sensitive.
89func matchReportingSeparators(lp, rlp smtp.Localpart, d config.Domain) bool {
90 lps := string(lp)
91 rlps := string(rlp)
92
93 if !strings.HasPrefix(lps, rlps) {
94 return false
95 }
96 if len(lps) == len(rlps) {
97 return true
98 }
99 rem := lps[len(rlps):]
100 for _, sep := range d.LocalpartCatchallSeparatorsEffective {
101 if strings.HasPrefix(rem, sep) {
102 return true
103 }
104 }
105 return false
106}
107
108// CanonicalLocalpart returns the canonical localpart, removing optional catchall
109// separators, and optionally lower-casing the string.
110// The DMARC and TLS reporting addresses are treated specially, they may contain a
111// localpart catchall separator for historic configurations (not for new
112// configurations). We try to match them first, still taking additional localpart
113// catchall separators into account.
114func CanonicalLocalpart(localpart smtp.Localpart, d config.Domain) smtp.Localpart {
115 if !d.LocalpartCaseSensitive {
116 localpart = smtp.Localpart(strings.ToLower(string(localpart)))
117 }
118
119 if d.DMARC != nil && matchReportingSeparators(localpart, d.DMARC.ParsedLocalpart, d) {
120 return d.DMARC.ParsedLocalpart
121 }
122 if d.TLSRPT != nil && matchReportingSeparators(localpart, d.TLSRPT.ParsedLocalpart, d) {
123 return d.TLSRPT.ParsedLocalpart
124 }
125
126 for _, sep := range d.LocalpartCatchallSeparatorsEffective {
127 t := strings.SplitN(string(localpart), sep, 2)
128 localpart = smtp.Localpart(t[0])
129 }
130
131 return localpart
132}
133
134// AllowMsgFrom returns whether account is allowed to submit messages with address
135// as message From header, based on configured addresses and membership of aliases
136// that allow using its address.
137func AllowMsgFrom(accountName string, msgFrom smtp.Address) (ok, domainDisabled bool) {
138 accName, alias, _, _, err := LookupAddress(msgFrom.Localpart, msgFrom.Domain, false, true, true)
139 if err != nil {
140 return false, errors.Is(err, ErrDomainDisabled)
141 }
142 if alias != nil && alias.AllowMsgFrom {
143 for _, aa := range alias.ParsedAddresses {
144 if aa.AccountName == accountName {
145 return true, false
146 }
147 }
148 return false, false
149 }
150 return accName == accountName, false
151}
152