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// CanonicalLocalpart returns the canonical localpart, removing optional catchall
89// separator, and optionally lower-casing the string.
90func CanonicalLocalpart(localpart smtp.Localpart, d config.Domain) smtp.Localpart {
91 if d.LocalpartCatchallSeparator != "" {
92 t := strings.SplitN(string(localpart), d.LocalpartCatchallSeparator, 2)
93 localpart = smtp.Localpart(t[0])
94 }
95
96 if !d.LocalpartCaseSensitive {
97 localpart = smtp.Localpart(strings.ToLower(string(localpart)))
98 }
99 return localpart
100}
101
102// AllowMsgFrom returns whether account is allowed to submit messages with address
103// as message From header, based on configured addresses and membership of aliases
104// that allow using its address.
105func AllowMsgFrom(accountName string, msgFrom smtp.Address) (ok, domainDisabled bool) {
106 accName, alias, _, _, err := LookupAddress(msgFrom.Localpart, msgFrom.Domain, false, true, true)
107 if err != nil {
108 return false, errors.Is(err, ErrDomainDisabled)
109 }
110 if alias != nil && alias.AllowMsgFrom {
111 for _, aa := range alias.ParsedAddresses {
112 if aa.AccountName == accountName {
113 return true, false
114 }
115 }
116 return false, false
117 }
118 return accName == accountName, false
119}
120