1package mox
2
3import (
4 "errors"
5 "fmt"
6 "strings"
7
8 "github.com/mjl-/mox/config"
9 "github.com/mjl-/mox/dns"
10 "github.com/mjl-/mox/smtp"
11)
12
13var (
14 ErrDomainNotFound = errors.New("domain not found")
15 ErrAccountNotFound = errors.New("account not found")
16)
17
18// FindAccount looks up the account for localpart and domain.
19//
20// Can return ErrDomainNotFound and ErrAccountNotFound.
21func FindAccount(localpart smtp.Localpart, domain dns.Domain, allowPostmaster bool) (accountName string, canonicalAddress string, dest config.Destination, rerr error) {
22 if strings.EqualFold(string(localpart), "postmaster") {
23 localpart = "postmaster"
24 }
25
26 postmasterDomain := func() bool {
27 var zerodomain dns.Domain
28 if domain == zerodomain || domain == Conf.Static.HostnameDomain {
29 return true
30 }
31 for _, l := range Conf.Static.Listeners {
32 if l.SMTP.Enabled && domain == l.HostnameDomain {
33 return true
34 }
35 }
36 return false
37 }
38
39 // Check for special mail host addresses.
40 if localpart == "postmaster" && postmasterDomain() {
41 if !allowPostmaster {
42 return "", "", config.Destination{}, ErrAccountNotFound
43 }
44 return Conf.Static.Postmaster.Account, "postmaster", config.Destination{Mailbox: Conf.Static.Postmaster.Mailbox}, nil
45 }
46 if localpart == Conf.Static.HostTLSRPT.ParsedLocalpart && domain == Conf.Static.HostnameDomain {
47 // Get destination, should always be present.
48 canonical := smtp.NewAddress(localpart, domain).String()
49 accAddr, ok := Conf.AccountDestination(canonical)
50 if !ok {
51 return "", "", config.Destination{}, ErrAccountNotFound
52 }
53 return accAddr.Account, canonical, accAddr.Destination, nil
54 }
55
56 d, ok := Conf.Domain(domain)
57 if !ok || d.ReportsOnly {
58 // For ReportsOnly, we also return ErrDomainNotFound, so this domain isn't
59 // considered local/authoritative during delivery.
60 return "", "", config.Destination{}, ErrDomainNotFound
61 }
62
63 localpart, err := CanonicalLocalpart(localpart, d)
64 if err != nil {
65 return "", "", config.Destination{}, fmt.Errorf("%w: %s", ErrAccountNotFound, err)
66 }
67 canonical := smtp.NewAddress(localpart, domain).String()
68
69 accAddr, ok := Conf.AccountDestination(canonical)
70 if !ok {
71 if accAddr, ok = Conf.AccountDestination("@" + domain.Name()); !ok {
72 if localpart == "postmaster" && allowPostmaster {
73 return Conf.Static.Postmaster.Account, "postmaster", config.Destination{Mailbox: Conf.Static.Postmaster.Mailbox}, nil
74 }
75 return "", "", config.Destination{}, ErrAccountNotFound
76 }
77 canonical = "@" + domain.Name()
78 }
79 return accAddr.Account, canonical, accAddr.Destination, nil
80}
81
82// CanonicalLocalpart returns the canonical localpart, removing optional catchall
83// separator, and optionally lower-casing the string.
84func CanonicalLocalpart(localpart smtp.Localpart, d config.Domain) (smtp.Localpart, error) {
85 if d.LocalpartCatchallSeparator != "" {
86 t := strings.SplitN(string(localpart), d.LocalpartCatchallSeparator, 2)
87 localpart = smtp.Localpart(t[0])
88 }
89
90 if !d.LocalpartCaseSensitive {
91 localpart = smtp.Localpart(strings.ToLower(string(localpart)))
92 }
93 return localpart, nil
94}
95