Files
zeekayandHanzo Dev 15909c7b79 Rename Go module path to github.com/hanzoai/mail
Fork of github.com/mjl-/mox at 5656ca6d1b
(upstream v0.0.15 plus 55 commits).

Rename the module path in go.mod and the import paths in 268 Go files. Also
update the five scripts that "go install" the module (genwebsite.sh,
docker-release.sh, apidiff.sh, test-upgrade.sh, Dockerfile.release), which
would otherwise build upstream rather than this fork.

Add NOTICE recording the upstream project, author, license, and fork point,
and stating which license covers which files: MIT for the mox source and our
changes, MPL-2.0 for publicsuffix/public_suffix_list.txt alone, and the
vendored dependencies' own licenses under licenses/. Embed NOTICE so
"mail licenses" prints the lineage from the running binary. LICENSE.MIT,
LICENSE.MPLv2.0 and the licenses/ tree are preserved verbatim; no code is
relicensed.

The mox command name, config keys, data directory layout and website content
are unchanged. LLM.md records the fork point, the rename, what was left alone,
and the steps to stand the server up.

Co-authored-by: Hanzo Dev <[email protected]>
2026-07-27 23:16:19 -07:00

152 lines
5.1 KiB
Go

package mox
import (
"errors"
"strings"
"github.com/hanzoai/mail/config"
"github.com/hanzoai/mail/dns"
"github.com/hanzoai/mail/smtp"
)
var (
ErrDomainNotFound = errors.New("domain not found")
ErrDomainDisabled = errors.New("message/transaction involving temporarily disabled domain")
ErrAddressNotFound = errors.New("address not found")
)
// LookupAddress looks up the account for localpart and domain.
//
// Can return ErrDomainNotFound and ErrAddressNotFound. If checkDomainDisabled is
// set, returns ErrDomainDisabled if domain is disabled.
func LookupAddress(localpart smtp.Localpart, domain dns.Domain, allowPostmaster, allowAlias, checkDomainDisabled bool) (accountName string, alias *config.Alias, canonicalAddress string, dest config.Destination, rerr error) {
if strings.EqualFold(string(localpart), "postmaster") {
localpart = "postmaster"
}
postmasterDomain := func() bool {
var zerodomain dns.Domain
if domain == zerodomain || domain == Conf.Static.HostnameDomain {
return true
}
for _, l := range Conf.Static.Listeners {
if l.SMTP.Enabled && domain == l.HostnameDomain {
return true
}
}
return false
}
// Check for special mail host addresses.
if localpart == "postmaster" && postmasterDomain() {
if !allowPostmaster {
return "", nil, "", config.Destination{}, ErrAddressNotFound
}
return Conf.Static.Postmaster.Account, nil, "postmaster", config.Destination{Mailbox: Conf.Static.Postmaster.Mailbox}, nil
}
if localpart == Conf.Static.HostTLSRPT.ParsedLocalpart && domain == Conf.Static.HostnameDomain {
// Get destination, should always be present.
canonical := smtp.NewAddress(localpart, domain).String()
accAddr, a, ok := Conf.AccountDestination(canonical)
if !ok || a != nil {
return "", nil, "", config.Destination{}, ErrAddressNotFound
}
return accAddr.Account, nil, canonical, accAddr.Destination, nil
}
d, ok := Conf.Domain(domain)
if !ok || d.ReportsOnly {
// For ReportsOnly, we also return ErrDomainNotFound, so this domain isn't
// considered local/authoritative during delivery.
return "", nil, "", config.Destination{}, ErrDomainNotFound
}
if d.Disabled && checkDomainDisabled {
return "", nil, "", config.Destination{}, ErrDomainDisabled
}
localpart = CanonicalLocalpart(localpart, d)
canonical := smtp.NewAddress(localpart, domain).String()
accAddr, alias, ok := Conf.AccountDestination(canonical)
if ok && alias != nil {
if !allowAlias {
return "", nil, "", config.Destination{}, ErrAddressNotFound
}
return "", alias, canonical, config.Destination{}, nil
} else if !ok {
if accAddr, alias, ok = Conf.AccountDestination("@" + domain.Name()); !ok || alias != nil {
if localpart == "postmaster" && allowPostmaster {
return Conf.Static.Postmaster.Account, nil, "postmaster", config.Destination{Mailbox: Conf.Static.Postmaster.Mailbox}, nil
}
return "", nil, "", config.Destination{}, ErrAddressNotFound
}
canonical = "@" + domain.Name()
}
return accAddr.Account, nil, canonical, accAddr.Destination, nil
}
// lp and rlp are both lower-case when domain localparts aren't case sensitive.
func matchReportingSeparators(lp, rlp smtp.Localpart, d config.Domain) bool {
lps := string(lp)
rlps := string(rlp)
if !strings.HasPrefix(lps, rlps) {
return false
}
if len(lps) == len(rlps) {
return true
}
rem := lps[len(rlps):]
for _, sep := range d.LocalpartCatchallSeparatorsEffective {
if strings.HasPrefix(rem, sep) {
return true
}
}
return false
}
// CanonicalLocalpart returns the canonical localpart, removing optional catchall
// separators, and optionally lower-casing the string.
// The DMARC and TLS reporting addresses are treated specially, they may contain a
// localpart catchall separator for historic configurations (not for new
// configurations). We try to match them first, still taking additional localpart
// catchall separators into account.
func CanonicalLocalpart(localpart smtp.Localpart, d config.Domain) smtp.Localpart {
if !d.LocalpartCaseSensitive {
localpart = smtp.Localpart(strings.ToLower(string(localpart)))
}
if d.DMARC != nil && matchReportingSeparators(localpart, d.DMARC.ParsedLocalpart, d) {
return d.DMARC.ParsedLocalpart
}
if d.TLSRPT != nil && matchReportingSeparators(localpart, d.TLSRPT.ParsedLocalpart, d) {
return d.TLSRPT.ParsedLocalpart
}
for _, sep := range d.LocalpartCatchallSeparatorsEffective {
t := strings.SplitN(string(localpart), sep, 2)
localpart = smtp.Localpart(t[0])
}
return localpart
}
// AllowMsgFrom returns whether account is allowed to submit messages with address
// as message From header, based on configured addresses and membership of aliases
// that allow using its address.
func AllowMsgFrom(accountName string, msgFrom smtp.Address) (ok, domainDisabled bool) {
accName, alias, _, _, err := LookupAddress(msgFrom.Localpart, msgFrom.Domain, false, true, true)
if err != nil {
return false, errors.Is(err, ErrDomainDisabled)
}
if alias != nil && alias.AllowMsgFrom {
for _, aa := range alias.ParsedAddresses {
if aa.AccountName == accountName {
return true, false
}
}
return false, false
}
return accName == accountName, false
}