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 ErrAddressNotFound = errors.New("address not found")
15)
16
17// FindAccount looks up the account for localpart and domain.
18//
19// Can return ErrDomainNotFound and ErrAddressNotFound.
20func LookupAddress(localpart smtp.Localpart, domain dns.Domain, allowPostmaster, allowAlias bool) (accountName string, alias *config.Alias, canonicalAddress string, dest config.Destination, rerr error) {
21 if strings.EqualFold(string(localpart), "postmaster") {
22 localpart = "postmaster"
23 }
24
25 postmasterDomain := func() bool {
26 var zerodomain dns.Domain
27 if domain == zerodomain || domain == Conf.Static.HostnameDomain {
28 return true
29 }
30 for _, l := range Conf.Static.Listeners {
31 if l.SMTP.Enabled && domain == l.HostnameDomain {
32 return true
33 }
34 }
35 return false
36 }
37
38 // Check for special mail host addresses.
39 if localpart == "postmaster" && postmasterDomain() {
40 if !allowPostmaster {
41 return "", nil, "", config.Destination{}, ErrAddressNotFound
42 }
43 return Conf.Static.Postmaster.Account, nil, "postmaster", config.Destination{Mailbox: Conf.Static.Postmaster.Mailbox}, nil
44 }
45 if localpart == Conf.Static.HostTLSRPT.ParsedLocalpart && domain == Conf.Static.HostnameDomain {
46 // Get destination, should always be present.
47 canonical := smtp.NewAddress(localpart, domain).String()
48 accAddr, a, ok := Conf.AccountDestination(canonical)
49 if !ok || a != nil {
50 return "", nil, "", config.Destination{}, ErrAddressNotFound
51 }
52 return accAddr.Account, nil, canonical, accAddr.Destination, nil
53 }
54
55 d, ok := Conf.Domain(domain)
56 if !ok || d.ReportsOnly {
57 // For ReportsOnly, we also return ErrDomainNotFound, so this domain isn't
58 // considered local/authoritative during delivery.
59 return "", nil, "", config.Destination{}, ErrDomainNotFound
60 }
61
62 localpart = CanonicalLocalpart(localpart, d)
63 canonical := smtp.NewAddress(localpart, domain).String()
64
65 accAddr, alias, ok := Conf.AccountDestination(canonical)
66 if ok && alias != nil {
67 if !allowAlias {
68 return "", nil, "", config.Destination{}, ErrAddressNotFound
69 }
70 return "", alias, canonical, config.Destination{}, nil
71 } else if !ok {
72 if accAddr, alias, ok = Conf.AccountDestination("@" + domain.Name()); !ok || alias != nil {
73 if localpart == "postmaster" && allowPostmaster {
74 return Conf.Static.Postmaster.Account, nil, "postmaster", config.Destination{Mailbox: Conf.Static.Postmaster.Mailbox}, nil
75 }
76 return "", nil, "", config.Destination{}, ErrAddressNotFound
77 }
78 canonical = "@" + domain.Name()
79 }
80 return accAddr.Account, nil, canonical, accAddr.Destination, nil
81}
82
83// CanonicalLocalpart returns the canonical localpart, removing optional catchall
84// separator, and optionally lower-casing the string.
85func CanonicalLocalpart(localpart smtp.Localpart, d config.Domain) smtp.Localpart {
86 if d.LocalpartCatchallSeparator != "" {
87 t := strings.SplitN(string(localpart), d.LocalpartCatchallSeparator, 2)
88 localpart = smtp.Localpart(t[0])
89 }
90
91 if !d.LocalpartCaseSensitive {
92 localpart = smtp.Localpart(strings.ToLower(string(localpart)))
93 }
94 return localpart
95}
96
97// AllowMsgFrom returns whether account is allowed to submit messages with address
98// as message From header, based on configured addresses and membership of aliases
99// that allow using its address.
100func AllowMsgFrom(accountName string, msgFrom smtp.Address) bool {
101 accName, alias, _, _, err := LookupAddress(msgFrom.Localpart, msgFrom.Domain, false, true)
102 if err != nil {
103 return false
104 }
105 if alias != nil && alias.AllowMsgFrom {
106 for _, aa := range alias.ParsedAddresses {
107 if aa.AccountName == accountName {
108 return true
109 }
110 }
111 return false
112 }
113 return accName == accountName
114}
115