7 "github.com/mjl-/mox/config"
8 "github.com/mjl-/mox/dns"
9 "github.com/mjl-/mox/smtp"
13 ErrDomainNotFound = errors.New("domain not found")
14 ErrDomainDisabled = errors.New("message/transaction involving temporarily disabled domain")
15 ErrAddressNotFound = errors.New("address not found")
18// LookupAddress looks up the account for localpart and domain.
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"
27 postmasterDomain := func() bool {
28 var zerodomain dns.Domain
29 if domain == zerodomain || domain == Conf.Static.HostnameDomain {
32 for _, l := range Conf.Static.Listeners {
33 if l.SMTP.Enabled && domain == l.HostnameDomain {
40 // Check for special mail host addresses.
41 if localpart == "postmaster" && postmasterDomain() {
43 return "", nil, "", config.Destination{}, ErrAddressNotFound
45 return Conf.Static.Postmaster.Account, nil, "postmaster", config.Destination{Mailbox: Conf.Static.Postmaster.Mailbox}, nil
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)
52 return "", nil, "", config.Destination{}, ErrAddressNotFound
54 return accAddr.Account, nil, canonical, accAddr.Destination, nil
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
63 if d.Disabled && checkDomainDisabled {
64 return "", nil, "", config.Destination{}, ErrDomainDisabled
67 localpart = CanonicalLocalpart(localpart, d)
68 canonical := smtp.NewAddress(localpart, domain).String()
70 accAddr, alias, ok := Conf.AccountDestination(canonical)
71 if ok && alias != nil {
73 return "", nil, "", config.Destination{}, ErrAddressNotFound
75 return "", alias, canonical, config.Destination{}, nil
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
81 return "", nil, "", config.Destination{}, ErrAddressNotFound
83 canonical = "@" + domain.Name()
85 return accAddr.Account, nil, canonical, accAddr.Destination, nil
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])
96 if !d.LocalpartCaseSensitive {
97 localpart = smtp.Localpart(strings.ToLower(string(localpart)))
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)
108 return false, errors.Is(err, ErrDomainDisabled)
110 if alias != nil && alias.AllowMsgFrom {
111 for _, aa := range alias.ParsedAddresses {
112 if aa.AccountName == accountName {
118 return accName == accountName, false