mirror of
https://github.com/mjl-/mox.git
synced 2025-07-12 17:44:35 +03:00
add aliases/lists: when sending to an alias, the message gets delivered to all members
the members must currently all be addresses of local accounts. a message sent to an alias is accepted if at least one of the members accepts it. if no members accepts it (e.g. due to bad reputation of sender), the message is rejected. if a message is submitted to both an alias addresses and to recipients that are members of the alias in an smtp transaction, the message will be delivered to such members only once. the same applies if the address in the message from-header is the address of a member: that member won't receive the message (they sent it). this prevents duplicate messages. aliases have three configuration options: - PostPublic: whether anyone can send through the alias, or only members. members-only lists can be useful inside organizations for internal communication. public lists can be useful for support addresses. - ListMembers: whether members can see the addresses of other members. this can be seen in the account web interface. in the future, we could export this in other ways, so clients can expand the list. - AllowMsgFrom: whether messages can be sent through the alias with the alias address used in the message from-header. the webmail knows it can use that address, and will use it as from-address when replying to a message sent to that address. ideas for the future: - allow external addresses as members. still with some restrictions, such as requiring a valid dkim-signature so delivery has a chance to succeed. will also need configuration of an admin that can receive any bounces. - allow specifying specific members who can sent through the list (instead of all members). for github issue #57 by hmfaysal. also relevant for #99 by naturalethic. thanks to damir & marin from sartura for discussing requirements/features.
This commit is contained in:
110
mox-/admin.go
110
mox-/admin.go
@ -17,6 +17,7 @@ import (
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
@ -605,7 +606,7 @@ func moveAwayKeys(log mlog.Log, sels map[string]config.Selector, usedKeyPaths ma
|
||||
// can modify the config, but must clone all referencing data it changes.
|
||||
// xmodify may employ panic-based error handling. After xmodify returns, the
|
||||
// modified config is verified, saved and takes effect.
|
||||
func DomainSave(ctx context.Context, domainName string, xmodify func(config *config.Domain)) (rerr error) {
|
||||
func DomainSave(ctx context.Context, domainName string, xmodify func(config *config.Domain) error) (rerr error) {
|
||||
log := pkglog.WithContext(ctx)
|
||||
defer func() {
|
||||
if rerr != nil {
|
||||
@ -622,7 +623,9 @@ func DomainSave(ctx context.Context, domainName string, xmodify func(config *con
|
||||
return fmt.Errorf("%w: domain not present", ErrRequest)
|
||||
}
|
||||
|
||||
xmodify(&dom)
|
||||
if err := xmodify(&dom); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Compose new config without modifying existing data structures. If we fail, we
|
||||
// leave no trace.
|
||||
@ -1031,14 +1034,17 @@ func AccountRemove(ctx context.Context, account string) (rerr error) {
|
||||
//
|
||||
// Must be called with config lock held.
|
||||
func checkAddressAvailable(addr smtp.Address) error {
|
||||
if dc, ok := Conf.Dynamic.Domains[addr.Domain.Name()]; !ok {
|
||||
dc, ok := Conf.Dynamic.Domains[addr.Domain.Name()]
|
||||
if !ok {
|
||||
return fmt.Errorf("domain does not exist")
|
||||
} else if lp, err := CanonicalLocalpart(addr.Localpart, dc); err != nil {
|
||||
return fmt.Errorf("canonicalizing localpart: %v", err)
|
||||
} else if _, ok := Conf.accountDestinations[smtp.NewAddress(lp, addr.Domain).String()]; ok {
|
||||
}
|
||||
lp := CanonicalLocalpart(addr.Localpart, dc)
|
||||
if _, ok := Conf.accountDestinations[smtp.NewAddress(lp, addr.Domain).String()]; ok {
|
||||
return fmt.Errorf("canonicalized address %s already configured", smtp.NewAddress(lp, addr.Domain))
|
||||
} else if dc.LocalpartCatchallSeparator != "" && strings.Contains(string(addr.Localpart), dc.LocalpartCatchallSeparator) {
|
||||
return fmt.Errorf("localpart cannot include domain catchall separator %s", dc.LocalpartCatchallSeparator)
|
||||
} else if _, ok := dc.Aliases[lp.String()]; ok {
|
||||
return fmt.Errorf("address in use as alias")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -1177,14 +1183,8 @@ func AddressRemove(ctx context.Context, address string) (rerr error) {
|
||||
if !ok {
|
||||
return fmt.Errorf("%w: unknown domain in fromid login address %q", ErrRequest, fa.Pack(true))
|
||||
}
|
||||
flp, err := CanonicalLocalpart(fa.Localpart, dc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: getting canonical localpart for fromid login address %q: %v", ErrRequest, fa.Localpart, err)
|
||||
}
|
||||
alp, err := CanonicalLocalpart(pa.Localpart, dc)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: getting canonical part for address: %v", ErrRequest, err)
|
||||
}
|
||||
flp := CanonicalLocalpart(fa.Localpart, dc)
|
||||
alp := CanonicalLocalpart(pa.Localpart, dc)
|
||||
if alp != flp {
|
||||
// Keep for different localpart.
|
||||
fromIDLoginAddresses = append(fromIDLoginAddresses, a.FromIDLoginAddresses[i])
|
||||
@ -1206,6 +1206,88 @@ func AddressRemove(ctx context.Context, address string) (rerr error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
func AliasAdd(ctx context.Context, addr smtp.Address, alias config.Alias) error {
|
||||
return DomainSave(ctx, addr.Domain.Name(), func(d *config.Domain) error {
|
||||
if _, ok := d.Aliases[addr.Localpart.String()]; ok {
|
||||
return fmt.Errorf("%w: alias already present", ErrRequest)
|
||||
}
|
||||
if d.Aliases == nil {
|
||||
d.Aliases = map[string]config.Alias{}
|
||||
}
|
||||
d.Aliases = maps.Clone(d.Aliases)
|
||||
d.Aliases[addr.Localpart.String()] = alias
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func AliasUpdate(ctx context.Context, addr smtp.Address, alias config.Alias) error {
|
||||
return DomainSave(ctx, addr.Domain.Name(), func(d *config.Domain) error {
|
||||
a, ok := d.Aliases[addr.Localpart.String()]
|
||||
if !ok {
|
||||
return fmt.Errorf("%w: alias does not exist", ErrRequest)
|
||||
}
|
||||
a.PostPublic = alias.PostPublic
|
||||
a.ListMembers = alias.ListMembers
|
||||
a.AllowMsgFrom = alias.AllowMsgFrom
|
||||
d.Aliases = maps.Clone(d.Aliases)
|
||||
d.Aliases[addr.Localpart.String()] = a
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func AliasRemove(ctx context.Context, addr smtp.Address) error {
|
||||
return DomainSave(ctx, addr.Domain.Name(), func(d *config.Domain) error {
|
||||
_, ok := d.Aliases[addr.Localpart.String()]
|
||||
if !ok {
|
||||
return fmt.Errorf("%w: alias does not exist", ErrRequest)
|
||||
}
|
||||
d.Aliases = maps.Clone(d.Aliases)
|
||||
delete(d.Aliases, addr.Localpart.String())
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func AliasAddressesAdd(ctx context.Context, addr smtp.Address, addresses []string) error {
|
||||
if len(addresses) == 0 {
|
||||
return fmt.Errorf("%w: at least one address required", ErrRequest)
|
||||
}
|
||||
return DomainSave(ctx, addr.Domain.Name(), func(d *config.Domain) error {
|
||||
alias, ok := d.Aliases[addr.Localpart.String()]
|
||||
if !ok {
|
||||
return fmt.Errorf("%w: no such alias", ErrRequest)
|
||||
}
|
||||
alias.Addresses = append(slices.Clone(alias.Addresses), addresses...)
|
||||
alias.ParsedAddresses = nil
|
||||
d.Aliases = maps.Clone(d.Aliases)
|
||||
d.Aliases[addr.Localpart.String()] = alias
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func AliasAddressesRemove(ctx context.Context, addr smtp.Address, addresses []string) error {
|
||||
if len(addresses) == 0 {
|
||||
return fmt.Errorf("%w: need at least one address", ErrRequest)
|
||||
}
|
||||
return DomainSave(ctx, addr.Domain.Name(), func(d *config.Domain) error {
|
||||
alias, ok := d.Aliases[addr.Localpart.String()]
|
||||
if !ok {
|
||||
return fmt.Errorf("%w: no such alias", ErrRequest)
|
||||
}
|
||||
alias.Addresses = slices.DeleteFunc(slices.Clone(alias.Addresses), func(addr string) bool {
|
||||
n := len(addresses)
|
||||
addresses = slices.DeleteFunc(addresses, func(a string) bool { return a == addr })
|
||||
return n > len(addresses)
|
||||
})
|
||||
if len(addresses) > 0 {
|
||||
return fmt.Errorf("%w: address not found: %s", ErrRequest, strings.Join(addresses, ", "))
|
||||
}
|
||||
alias.ParsedAddresses = nil
|
||||
d.Aliases = maps.Clone(d.Aliases)
|
||||
d.Aliases[addr.Localpart.String()] = alias
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
// AccountSave updates the configuration of an account. Function xmodify is called
|
||||
// with a shallow copy of the current configuration of the account. It must not
|
||||
// change referencing fields (e.g. existing slice/map/pointer), they may still be
|
||||
|
140
mox-/config.go
140
mox-/config.go
@ -80,6 +80,8 @@ type Config struct {
|
||||
// case-insensitive, stripped of catchall separator) to account and address.
|
||||
// Domains are IDNA names in utf8.
|
||||
accountDestinations map[string]AccountDestination
|
||||
// Like accountDestinations, but for aliases.
|
||||
aliases map[string]config.Alias
|
||||
}
|
||||
|
||||
type AccountDestination struct {
|
||||
@ -152,13 +154,14 @@ func (c *Config) withDynamicLock(fn func()) {
|
||||
|
||||
// must be called with dynamic lock held.
|
||||
func (c *Config) loadDynamic() []error {
|
||||
d, mtime, accDests, err := ParseDynamicConfig(context.Background(), pkglog, ConfigDynamicPath, c.Static)
|
||||
d, mtime, accDests, aliases, err := ParseDynamicConfig(context.Background(), pkglog, ConfigDynamicPath, c.Static)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.Dynamic = d
|
||||
c.dynamicMtime = mtime
|
||||
c.accountDestinations = accDests
|
||||
c.aliases = aliases
|
||||
c.allowACMEHosts(pkglog, true)
|
||||
return nil
|
||||
}
|
||||
@ -193,10 +196,12 @@ func (c *Config) Accounts() (l []string) {
|
||||
}
|
||||
|
||||
// DomainLocalparts returns a mapping of encoded localparts to account names for a
|
||||
// domain. An empty localpart is a catchall destination for a domain.
|
||||
func (c *Config) DomainLocalparts(d dns.Domain) map[string]string {
|
||||
// domain, and encoded localparts to aliases. An empty localpart is a catchall
|
||||
// destination for a domain.
|
||||
func (c *Config) DomainLocalparts(d dns.Domain) (map[string]string, map[string]config.Alias) {
|
||||
suffix := "@" + d.Name()
|
||||
m := map[string]string{}
|
||||
aliases := map[string]config.Alias{}
|
||||
c.withDynamicLock(func() {
|
||||
for addr, ad := range c.accountDestinations {
|
||||
if strings.HasSuffix(addr, suffix) {
|
||||
@ -207,8 +212,13 @@ func (c *Config) DomainLocalparts(d dns.Domain) map[string]string {
|
||||
}
|
||||
}
|
||||
}
|
||||
for addr, a := range c.aliases {
|
||||
if strings.HasSuffix(addr, suffix) {
|
||||
aliases[a.LocalpartStr] = a
|
||||
}
|
||||
}
|
||||
})
|
||||
return m
|
||||
return m, aliases
|
||||
}
|
||||
|
||||
func (c *Config) Domain(d dns.Domain) (dom config.Domain, ok bool) {
|
||||
@ -225,9 +235,16 @@ func (c *Config) Account(name string) (acc config.Account, ok bool) {
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Config) AccountDestination(addr string) (accDests AccountDestination, ok bool) {
|
||||
func (c *Config) AccountDestination(addr string) (accDest AccountDestination, alias *config.Alias, ok bool) {
|
||||
c.withDynamicLock(func() {
|
||||
accDests, ok = c.accountDestinations[addr]
|
||||
accDest, ok = c.accountDestinations[addr]
|
||||
if !ok {
|
||||
var a config.Alias
|
||||
a, ok = c.aliases[addr]
|
||||
if ok {
|
||||
alias = &a
|
||||
}
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
@ -314,7 +331,7 @@ func (c *Config) allowACMEHosts(log mlog.Log, checkACMEHosts bool) {
|
||||
// must be called with lock held.
|
||||
// Returns ErrConfig if the configuration is not valid.
|
||||
func writeDynamic(ctx context.Context, log mlog.Log, c config.Dynamic) error {
|
||||
accDests, errs := prepareDynamicConfig(ctx, log, ConfigDynamicPath, Conf.Static, &c)
|
||||
accDests, aliases, errs := prepareDynamicConfig(ctx, log, ConfigDynamicPath, Conf.Static, &c)
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("%w: %v", ErrConfig, errs[0])
|
||||
}
|
||||
@ -362,6 +379,7 @@ func writeDynamic(ctx context.Context, log mlog.Log, c config.Dynamic) error {
|
||||
Conf.DynamicLastCheck = time.Now()
|
||||
Conf.Dynamic = c
|
||||
Conf.accountDestinations = accDests
|
||||
Conf.aliases = aliases
|
||||
|
||||
Conf.allowACMEHosts(log, true)
|
||||
|
||||
@ -401,7 +419,7 @@ func LoadConfig(ctx context.Context, log mlog.Log, doLoadTLSKeyCerts, checkACMEH
|
||||
// SetConfig sets a new config. Not to be used during normal operation.
|
||||
func SetConfig(c *Config) {
|
||||
// Cannot just assign *c to Conf, it would copy the mutex.
|
||||
Conf = Config{c.Static, sync.Mutex{}, c.Log, sync.Mutex{}, c.Dynamic, c.dynamicMtime, c.DynamicLastCheck, c.accountDestinations}
|
||||
Conf = Config{c.Static, sync.Mutex{}, c.Log, sync.Mutex{}, c.Dynamic, c.dynamicMtime, c.DynamicLastCheck, c.accountDestinations, c.aliases}
|
||||
|
||||
// If we have non-standard CA roots, use them for all HTTPS requests.
|
||||
if Conf.Static.TLS.CertPool != nil {
|
||||
@ -452,7 +470,7 @@ func ParseConfig(ctx context.Context, log mlog.Log, p string, checkOnly, doLoadT
|
||||
}
|
||||
|
||||
pp := filepath.Join(filepath.Dir(p), "domains.conf")
|
||||
c.Dynamic, c.dynamicMtime, c.accountDestinations, errs = ParseDynamicConfig(ctx, log, pp, c.Static)
|
||||
c.Dynamic, c.dynamicMtime, c.accountDestinations, c.aliases, errs = ParseDynamicConfig(ctx, log, pp, c.Static)
|
||||
|
||||
if !checkOnly {
|
||||
c.allowACMEHosts(log, checkACMEHosts)
|
||||
@ -992,7 +1010,7 @@ func PrepareStaticConfig(ctx context.Context, log mlog.Log, configFile string, c
|
||||
}
|
||||
|
||||
// PrepareDynamicConfig parses the dynamic config file given a static file.
|
||||
func ParseDynamicConfig(ctx context.Context, log mlog.Log, dynamicPath string, static config.Static) (c config.Dynamic, mtime time.Time, accDests map[string]AccountDestination, errs []error) {
|
||||
func ParseDynamicConfig(ctx context.Context, log mlog.Log, dynamicPath string, static config.Static) (c config.Dynamic, mtime time.Time, accDests map[string]AccountDestination, aliases map[string]config.Alias, errs []error) {
|
||||
addErrorf := func(format string, args ...any) {
|
||||
errs = append(errs, fmt.Errorf(format, args...))
|
||||
}
|
||||
@ -1012,11 +1030,11 @@ func ParseDynamicConfig(ctx context.Context, log mlog.Log, dynamicPath string, s
|
||||
return
|
||||
}
|
||||
|
||||
accDests, errs = prepareDynamicConfig(ctx, log, dynamicPath, static, &c)
|
||||
return c, fi.ModTime(), accDests, errs
|
||||
accDests, aliases, errs = prepareDynamicConfig(ctx, log, dynamicPath, static, &c)
|
||||
return c, fi.ModTime(), accDests, aliases, errs
|
||||
}
|
||||
|
||||
func prepareDynamicConfig(ctx context.Context, log mlog.Log, dynamicPath string, static config.Static, c *config.Dynamic) (accDests map[string]AccountDestination, errs []error) {
|
||||
func prepareDynamicConfig(ctx context.Context, log mlog.Log, dynamicPath string, static config.Static, c *config.Dynamic) (accDests map[string]AccountDestination, aliases map[string]config.Alias, errs []error) {
|
||||
addErrorf := func(format string, args ...any) {
|
||||
errs = append(errs, fmt.Errorf(format, args...))
|
||||
}
|
||||
@ -1037,6 +1055,7 @@ func prepareDynamicConfig(ctx context.Context, log mlog.Log, dynamicPath string,
|
||||
checkMailboxNormf(static.Postmaster.Mailbox, "postmaster mailbox")
|
||||
|
||||
accDests = map[string]AccountDestination{}
|
||||
aliases = map[string]config.Alias{}
|
||||
|
||||
// Validate host TLSRPT account/address.
|
||||
if static.HostTLSRPT.Account != "" {
|
||||
@ -1287,6 +1306,9 @@ func prepareDynamicConfig(ctx context.Context, log mlog.Log, dynamicPath string,
|
||||
acc.ParsedFromIDLoginAddresses[i] = a
|
||||
}
|
||||
|
||||
// Clear any previously derived state.
|
||||
acc.Aliases = nil
|
||||
|
||||
c.Accounts[accName] = acc
|
||||
|
||||
if acc.OutgoingWebhook != nil {
|
||||
@ -1445,9 +1467,8 @@ func prepareDynamicConfig(ctx context.Context, log mlog.Log, dynamicPath string,
|
||||
origLP := address.Localpart
|
||||
dc := c.Domains[address.Domain.Name()]
|
||||
domainHasAddress[address.Domain.Name()] = true
|
||||
if lp, err := CanonicalLocalpart(address.Localpart, dc); err != nil {
|
||||
addErrorf("canonicalizing localpart %s: %v", address.Localpart, err)
|
||||
} else if dc.LocalpartCatchallSeparator != "" && strings.Contains(string(address.Localpart), dc.LocalpartCatchallSeparator) {
|
||||
lp := CanonicalLocalpart(address.Localpart, dc)
|
||||
if dc.LocalpartCatchallSeparator != "" && strings.Contains(string(address.Localpart), dc.LocalpartCatchallSeparator) {
|
||||
addErrorf("localpart of address %s includes domain catchall separator %s", address, dc.LocalpartCatchallSeparator)
|
||||
} else {
|
||||
address.Localpart = lp
|
||||
@ -1481,12 +1502,7 @@ func prepareDynamicConfig(ctx context.Context, log mlog.Log, dynamicPath string,
|
||||
continue
|
||||
}
|
||||
dc := c.Domains[a.Domain.Name()]
|
||||
lp, err := CanonicalLocalpart(a.Localpart, dc)
|
||||
if err != nil {
|
||||
addErrorf("canonicalizing localpart for fromid login address %q in account %q: %v", acc.FromIDLoginAddresses[i], accName, err)
|
||||
continue
|
||||
}
|
||||
a.Localpart = lp
|
||||
a.Localpart = CanonicalLocalpart(a.Localpart, dc)
|
||||
if _, ok := accDests[a.Pack(true)]; !ok {
|
||||
addErrorf("fromid login address %q for account %q does not match its destination addresses", acc.FromIDLoginAddresses[i], accName)
|
||||
}
|
||||
@ -1587,6 +1603,86 @@ func prepareDynamicConfig(ctx context.Context, log mlog.Log, dynamicPath string,
|
||||
c.Domains[d] = domain
|
||||
}
|
||||
|
||||
// Aliases, per domain. Also add references to accounts.
|
||||
for d, domain := range c.Domains {
|
||||
for lpstr, a := range domain.Aliases {
|
||||
var err error
|
||||
a.LocalpartStr = lpstr
|
||||
var clp smtp.Localpart
|
||||
lp, err := smtp.ParseLocalpart(lpstr)
|
||||
if err != nil {
|
||||
addErrorf("domain %q: parsing localpart %q for alias: %v", d, lpstr, err)
|
||||
continue
|
||||
} else if domain.LocalpartCatchallSeparator != "" && strings.Contains(string(lp), domain.LocalpartCatchallSeparator) {
|
||||
addErrorf("domain %q: alias %q contains localpart catchall separator", d, a.LocalpartStr)
|
||||
continue
|
||||
} else {
|
||||
clp = CanonicalLocalpart(lp, domain)
|
||||
}
|
||||
|
||||
addr := smtp.NewAddress(clp, domain.Domain).Pack(true)
|
||||
if _, ok := aliases[addr]; ok {
|
||||
addErrorf("domain %q: duplicate alias address %q", d, addr)
|
||||
continue
|
||||
}
|
||||
if _, ok := accDests[addr]; ok {
|
||||
addErrorf("domain %q: alias %q already present as regular address", d, addr)
|
||||
continue
|
||||
}
|
||||
if len(a.Addresses) == 0 {
|
||||
// Not currently possible, Addresses isn't optional.
|
||||
addErrorf("domain %q: alias %q needs at least one destination address", d, addr)
|
||||
continue
|
||||
}
|
||||
a.ParsedAddresses = make([]config.AliasAddress, 0, len(a.Addresses))
|
||||
seen := map[string]bool{}
|
||||
for _, destAddr := range a.Addresses {
|
||||
da, err := smtp.ParseAddress(destAddr)
|
||||
if err != nil {
|
||||
addErrorf("domain %q: parsing destination address %q in alias %q: %v", d, destAddr, addr, err)
|
||||
continue
|
||||
}
|
||||
dastr := da.Pack(true)
|
||||
accDest, ok := accDests[dastr]
|
||||
if !ok {
|
||||
addErrorf("domain %q: alias %q references non-existent address %q", d, addr, destAddr)
|
||||
continue
|
||||
}
|
||||
if seen[dastr] {
|
||||
addErrorf("domain %q: alias %q has duplicate address %q", d, addr, destAddr)
|
||||
continue
|
||||
}
|
||||
seen[dastr] = true
|
||||
aa := config.AliasAddress{Address: da, AccountName: accDest.Account, Destination: accDest.Destination}
|
||||
a.ParsedAddresses = append(a.ParsedAddresses, aa)
|
||||
}
|
||||
a.Domain = domain.Domain
|
||||
c.Domains[d].Aliases[lpstr] = a
|
||||
aliases[addr] = a
|
||||
|
||||
for _, aa := range a.ParsedAddresses {
|
||||
acc := c.Accounts[aa.AccountName]
|
||||
var addrs []string
|
||||
if a.ListMembers {
|
||||
addrs = make([]string, len(a.ParsedAddresses))
|
||||
for i := range a.ParsedAddresses {
|
||||
addrs[i] = a.ParsedAddresses[i].Address.Pack(true)
|
||||
}
|
||||
}
|
||||
// Keep the non-sensitive fields.
|
||||
accAlias := config.Alias{
|
||||
PostPublic: a.PostPublic,
|
||||
ListMembers: a.ListMembers,
|
||||
AllowMsgFrom: a.AllowMsgFrom,
|
||||
LocalpartStr: a.LocalpartStr,
|
||||
Domain: a.Domain,
|
||||
}
|
||||
acc.Aliases = append(acc.Aliases, config.AddressAlias{SubscriptionAddress: aa.Address.Pack(true), Alias: accAlias, MemberAddresses: addrs})
|
||||
c.Accounts[aa.AccountName] = acc
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check webserver configs.
|
||||
if (len(c.WebDomainRedirects) > 0 || len(c.WebHandlers) > 0) && !haveWebserverListener {
|
||||
addErrorf("WebDomainRedirects or WebHandlers configured but no listener with WebserverHTTP or WebserverHTTPS enabled")
|
||||
|
@ -2,7 +2,6 @@ package mox
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/mjl-/mox/config"
|
||||
@ -12,13 +11,13 @@ import (
|
||||
|
||||
var (
|
||||
ErrDomainNotFound = errors.New("domain not found")
|
||||
ErrAccountNotFound = errors.New("account not found")
|
||||
ErrAddressNotFound = errors.New("address not found")
|
||||
)
|
||||
|
||||
// FindAccount looks up the account for localpart and domain.
|
||||
//
|
||||
// Can return ErrDomainNotFound and ErrAccountNotFound.
|
||||
func FindAccount(localpart smtp.Localpart, domain dns.Domain, allowPostmaster bool) (accountName string, canonicalAddress string, dest config.Destination, rerr error) {
|
||||
// Can return ErrDomainNotFound and ErrAddressNotFound.
|
||||
func LookupAddress(localpart smtp.Localpart, domain dns.Domain, allowPostmaster, allowAlias bool) (accountName string, alias *config.Alias, canonicalAddress string, dest config.Destination, rerr error) {
|
||||
if strings.EqualFold(string(localpart), "postmaster") {
|
||||
localpart = "postmaster"
|
||||
}
|
||||
@ -39,49 +38,48 @@ func FindAccount(localpart smtp.Localpart, domain dns.Domain, allowPostmaster bo
|
||||
// Check for special mail host addresses.
|
||||
if localpart == "postmaster" && postmasterDomain() {
|
||||
if !allowPostmaster {
|
||||
return "", "", config.Destination{}, ErrAccountNotFound
|
||||
return "", nil, "", config.Destination{}, ErrAddressNotFound
|
||||
}
|
||||
return Conf.Static.Postmaster.Account, "postmaster", config.Destination{Mailbox: Conf.Static.Postmaster.Mailbox}, nil
|
||||
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, ok := Conf.AccountDestination(canonical)
|
||||
if !ok {
|
||||
return "", "", config.Destination{}, ErrAccountNotFound
|
||||
accAddr, a, ok := Conf.AccountDestination(canonical)
|
||||
if !ok || a != nil {
|
||||
return "", nil, "", config.Destination{}, ErrAddressNotFound
|
||||
}
|
||||
return accAddr.Account, canonical, accAddr.Destination, nil
|
||||
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 "", "", config.Destination{}, ErrDomainNotFound
|
||||
return "", nil, "", config.Destination{}, ErrDomainNotFound
|
||||
}
|
||||
|
||||
localpart, err := CanonicalLocalpart(localpart, d)
|
||||
if err != nil {
|
||||
return "", "", config.Destination{}, fmt.Errorf("%w: %s", ErrAccountNotFound, err)
|
||||
}
|
||||
localpart = CanonicalLocalpart(localpart, d)
|
||||
canonical := smtp.NewAddress(localpart, domain).String()
|
||||
|
||||
accAddr, ok := Conf.AccountDestination(canonical)
|
||||
if !ok {
|
||||
if accAddr, ok = Conf.AccountDestination("@" + domain.Name()); !ok {
|
||||
accAddr, alias, ok := Conf.AccountDestination(canonical)
|
||||
if ok && alias != nil && allowAlias {
|
||||
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, "postmaster", config.Destination{Mailbox: Conf.Static.Postmaster.Mailbox}, nil
|
||||
return Conf.Static.Postmaster.Account, nil, "postmaster", config.Destination{Mailbox: Conf.Static.Postmaster.Mailbox}, nil
|
||||
}
|
||||
return "", "", config.Destination{}, ErrAccountNotFound
|
||||
return "", nil, "", config.Destination{}, ErrAddressNotFound
|
||||
}
|
||||
canonical = "@" + domain.Name()
|
||||
}
|
||||
return accAddr.Account, canonical, accAddr.Destination, nil
|
||||
return accAddr.Account, nil, canonical, accAddr.Destination, nil
|
||||
}
|
||||
|
||||
// CanonicalLocalpart returns the canonical localpart, removing optional catchall
|
||||
// separator, and optionally lower-casing the string.
|
||||
func CanonicalLocalpart(localpart smtp.Localpart, d config.Domain) (smtp.Localpart, error) {
|
||||
func CanonicalLocalpart(localpart smtp.Localpart, d config.Domain) smtp.Localpart {
|
||||
if d.LocalpartCatchallSeparator != "" {
|
||||
t := strings.SplitN(string(localpart), d.LocalpartCatchallSeparator, 2)
|
||||
localpart = smtp.Localpart(t[0])
|
||||
@ -90,5 +88,24 @@ func CanonicalLocalpart(localpart smtp.Localpart, d config.Domain) (smtp.Localpa
|
||||
if !d.LocalpartCaseSensitive {
|
||||
localpart = smtp.Localpart(strings.ToLower(string(localpart)))
|
||||
}
|
||||
return localpart, nil
|
||||
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) bool {
|
||||
accName, alias, _, _, err := LookupAddress(msgFrom.Localpart, msgFrom.Domain, false, true)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if alias != nil && alias.AllowMsgFrom {
|
||||
for _, aa := range alias.ParsedAddresses {
|
||||
if aa.AccountName == accountName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
return accName == accountName
|
||||
}
|
||||
|
Reference in New Issue
Block a user