mirror of
https://github.com/mjl-/mox.git
synced 2025-07-12 18:24:35 +03:00
implement a catchall address for a domain
by specifying a "destination" in an account that is just "@" followed by the domain, e.g. "@example.org". messages are only delivered to the catchall address when no regular destination matches (taking the per-domain catchall-separator and case-sensisitivity into account). for issue #18
This commit is contained in:
@ -555,6 +555,8 @@ func DomainRecords(domConf config.Domain, domain dns.Domain) ([]string, error) {
|
||||
//
|
||||
// The new account does not have a password, so cannot yet log in. Email can be
|
||||
// delivered.
|
||||
//
|
||||
// Catchall addresses are not supported for AccountAdd. Add separately with AddressAdd.
|
||||
func AccountAdd(ctx context.Context, account, address string) (rerr error) {
|
||||
log := xlog.WithContext(ctx)
|
||||
defer func() {
|
||||
@ -648,8 +650,8 @@ func checkAddressAvailable(addr smtp.Address) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// AddressAdd adds an email address to an account and reloads the
|
||||
// configuration.
|
||||
// AddressAdd adds an email address to an account and reloads the configuration. If
|
||||
// address starts with an @ it is treated as a catchall address for the domain.
|
||||
func AddressAdd(ctx context.Context, address, account string) (rerr error) {
|
||||
log := xlog.WithContext(ctx)
|
||||
defer func() {
|
||||
@ -658,11 +660,6 @@ func AddressAdd(ctx context.Context, address, account string) (rerr error) {
|
||||
}
|
||||
}()
|
||||
|
||||
addr, err := smtp.ParseAddress(address)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing email address: %v", err)
|
||||
}
|
||||
|
||||
Conf.dynamicMutex.Lock()
|
||||
defer Conf.dynamicMutex.Unlock()
|
||||
|
||||
@ -672,8 +669,29 @@ func AddressAdd(ctx context.Context, address, account string) (rerr error) {
|
||||
return fmt.Errorf("account does not exist")
|
||||
}
|
||||
|
||||
if err := checkAddressAvailable(addr); err != nil {
|
||||
return fmt.Errorf("address not available: %v", err)
|
||||
var destAddr string
|
||||
if strings.HasPrefix(address, "@") {
|
||||
d, err := dns.ParseDomain(address[1:])
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing domain: %v", err)
|
||||
}
|
||||
dname := d.Name()
|
||||
destAddr = "@" + dname
|
||||
if _, ok := Conf.Dynamic.Domains[dname]; !ok {
|
||||
return fmt.Errorf("domain does not exist")
|
||||
} else if _, ok := Conf.accountDestinations[destAddr]; ok {
|
||||
return fmt.Errorf("catchall address already configured for domain")
|
||||
}
|
||||
} else {
|
||||
addr, err := smtp.ParseAddress(address)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing email address: %v", err)
|
||||
}
|
||||
|
||||
if err := checkAddressAvailable(addr); err != nil {
|
||||
return fmt.Errorf("address not available: %v", err)
|
||||
}
|
||||
destAddr = addr.String()
|
||||
}
|
||||
|
||||
// Compose new config without modifying existing data structures. If we fail, we
|
||||
@ -687,14 +705,14 @@ func AddressAdd(ctx context.Context, address, account string) (rerr error) {
|
||||
for name, d := range a.Destinations {
|
||||
nd[name] = d
|
||||
}
|
||||
nd[addr.String()] = config.Destination{}
|
||||
nd[destAddr] = config.Destination{}
|
||||
a.Destinations = nd
|
||||
nc.Accounts[account] = a
|
||||
|
||||
if err := writeDynamic(ctx, log, nc); err != nil {
|
||||
return fmt.Errorf("writing domains.conf: %v", err)
|
||||
}
|
||||
log.Info("address added", mlog.Field("address", addr), mlog.Field("account", account))
|
||||
log.Info("address added", mlog.Field("address", address), mlog.Field("account", account))
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -710,31 +728,23 @@ func AddressRemove(ctx context.Context, address string) (rerr error) {
|
||||
Conf.dynamicMutex.Lock()
|
||||
defer Conf.dynamicMutex.Unlock()
|
||||
|
||||
c := Conf.Dynamic
|
||||
|
||||
addr, err := smtp.ParseAddress(address)
|
||||
if err != nil {
|
||||
return fmt.Errorf("parsing email address: %v", err)
|
||||
}
|
||||
ad, ok := Conf.accountDestinations[addr.String()]
|
||||
ad, ok := Conf.accountDestinations[address]
|
||||
if !ok {
|
||||
return fmt.Errorf("address does not exists")
|
||||
}
|
||||
addrStr := addr.String()
|
||||
|
||||
// Compose new config without modifying existing data structures. If we fail, we
|
||||
// leave no trace.
|
||||
a, ok := c.Accounts[ad.Account]
|
||||
a, ok := Conf.Dynamic.Accounts[ad.Account]
|
||||
if !ok {
|
||||
return fmt.Errorf("internal error: cannot find account")
|
||||
}
|
||||
na := a
|
||||
na.Destinations = map[string]config.Destination{}
|
||||
var dropped bool
|
||||
for name, d := range a.Destinations {
|
||||
// todo deprecated: remove support for localpart-only with default domain as destination address.
|
||||
if !(name == addr.Localpart.String() && a.DNSDomain == addr.Domain || name == addrStr) {
|
||||
na.Destinations[name] = d
|
||||
for destAddr, d := range a.Destinations {
|
||||
if destAddr != address {
|
||||
na.Destinations[destAddr] = d
|
||||
} else {
|
||||
dropped = true
|
||||
}
|
||||
@ -742,9 +752,9 @@ func AddressRemove(ctx context.Context, address string) (rerr error) {
|
||||
if !dropped {
|
||||
return fmt.Errorf("address not removed, likely a postmaster/reporting address")
|
||||
}
|
||||
nc := c
|
||||
nc := Conf.Dynamic
|
||||
nc.Accounts = map[string]config.Account{}
|
||||
for name, a := range c.Accounts {
|
||||
for name, a := range Conf.Dynamic.Accounts {
|
||||
nc.Accounts[name] = a
|
||||
}
|
||||
nc.Accounts[ad.Account] = na
|
||||
@ -752,7 +762,7 @@ func AddressRemove(ctx context.Context, address string) (rerr error) {
|
||||
if err := writeDynamic(ctx, log, nc); err != nil {
|
||||
return fmt.Errorf("writing domains.conf: %v", err)
|
||||
}
|
||||
log.Info("address removed", mlog.Field("address", addr), mlog.Field("account", ad.Account))
|
||||
log.Info("address removed", mlog.Field("address", address), mlog.Field("account", ad.Account))
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -67,7 +67,8 @@ type Config struct {
|
||||
}
|
||||
|
||||
type AccountDestination struct {
|
||||
Localpart smtp.Localpart
|
||||
Catchall bool // If catchall destination for its domain.
|
||||
Localpart smtp.Localpart // In original casing as written in config file.
|
||||
Account string
|
||||
Destination config.Destination
|
||||
}
|
||||
@ -167,13 +168,19 @@ func (c *Config) Accounts() (l []string) {
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Config) DomainLocalparts(d dns.Domain) map[smtp.Localpart]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 {
|
||||
suffix := "@" + d.Name()
|
||||
m := map[smtp.Localpart]string{}
|
||||
m := map[string]string{}
|
||||
c.withDynamicLock(func() {
|
||||
for addr, ad := range c.accountDestinations {
|
||||
if strings.HasSuffix(addr, suffix) {
|
||||
m[ad.Localpart] = ad.Account
|
||||
if ad.Catchall {
|
||||
m[""] = ad.Account
|
||||
} else {
|
||||
m[ad.Localpart.String()] = ad.Account
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -685,7 +692,7 @@ func prepareDynamicConfig(ctx context.Context, dynamicPath string, static config
|
||||
errs = append(errs, fmt.Errorf(format, args...))
|
||||
}
|
||||
|
||||
// check that mailbox is in unicode NFC normalized form.
|
||||
// Check that mailbox is in unicode NFC normalized form.
|
||||
checkMailboxNormf := func(mailbox string, format string, args ...any) {
|
||||
s := norm.NFC.String(mailbox)
|
||||
if mailbox != s {
|
||||
@ -930,10 +937,27 @@ func prepareDynamicConfig(ctx context.Context, dynamicPath string, static config
|
||||
}
|
||||
}
|
||||
|
||||
// Catchall destination for domain.
|
||||
if strings.HasPrefix(addrName, "@") {
|
||||
d, err := dns.ParseDomain(addrName[1:])
|
||||
if err != nil {
|
||||
addErrorf("parsing domain %q in account %q", addrName[1:], accName)
|
||||
continue
|
||||
} else if _, ok := c.Domains[d.Name()]; !ok {
|
||||
addErrorf("unknown domain for address %q in account %q", addrName, accName)
|
||||
continue
|
||||
}
|
||||
addrFull := "@" + d.Name()
|
||||
if _, ok := accDests[addrFull]; ok {
|
||||
addErrorf("duplicate canonicalized catchall destination address %s", addrFull)
|
||||
}
|
||||
accDests[addrFull] = AccountDestination{true, "", accName, dest}
|
||||
continue
|
||||
}
|
||||
|
||||
// todo deprecated: remove support for parsing destination as just a localpart instead full address.
|
||||
var address smtp.Address
|
||||
localpart, err := smtp.ParseLocalpart(addrName)
|
||||
if err != nil && errors.Is(err, smtp.ErrBadLocalpart) {
|
||||
if localpart, err := smtp.ParseLocalpart(addrName); err != nil && errors.Is(err, smtp.ErrBadLocalpart) {
|
||||
address, err = smtp.ParseAddress(addrName)
|
||||
if err != nil {
|
||||
addErrorf("invalid email address %q in account %q", addrName, accName)
|
||||
@ -955,6 +979,7 @@ func prepareDynamicConfig(ctx context.Context, dynamicPath string, static config
|
||||
replaceLocalparts[addrName] = address.Pack(true)
|
||||
}
|
||||
|
||||
origLP := address.Localpart
|
||||
dc := c.Domains[address.Domain.Name()]
|
||||
if lp, err := CanonicalLocalpart(address.Localpart, dc); err != nil {
|
||||
addErrorf("canonicalizing localpart %s: %v", address.Localpart, err)
|
||||
@ -967,7 +992,7 @@ func prepareDynamicConfig(ctx context.Context, dynamicPath string, static config
|
||||
if _, ok := accDests[addrFull]; ok {
|
||||
addErrorf("duplicate canonicalized destination address %s", addrFull)
|
||||
}
|
||||
accDests[addrFull] = AccountDestination{address.Localpart, accName, dest}
|
||||
accDests[addrFull] = AccountDestination{false, origLP, accName, dest}
|
||||
}
|
||||
|
||||
for lp, addr := range replaceLocalparts {
|
||||
@ -1007,7 +1032,7 @@ func prepareDynamicConfig(ctx context.Context, dynamicPath string, static config
|
||||
DMARCReports: true,
|
||||
}
|
||||
checkMailboxNormf(dmarc.Mailbox, "DMARC mailbox for account %q", dmarc.Account)
|
||||
accDests[addrFull] = AccountDestination{lp, dmarc.Account, dest}
|
||||
accDests[addrFull] = AccountDestination{false, lp, dmarc.Account, dest}
|
||||
}
|
||||
|
||||
// Set TLSRPT destinations.
|
||||
@ -1036,7 +1061,7 @@ func prepareDynamicConfig(ctx context.Context, dynamicPath string, static config
|
||||
TLSReports: true,
|
||||
}
|
||||
checkMailboxNormf(tlsrpt.Mailbox, "TLSRPT mailbox for account %q", tlsrpt.Account)
|
||||
accDests[addrFull] = AccountDestination{lp, tlsrpt.Account, dest}
|
||||
accDests[addrFull] = AccountDestination{false, lp, tlsrpt.Account, dest}
|
||||
}
|
||||
|
||||
// Check webserver configs.
|
||||
|
@ -43,7 +43,10 @@ func FindAccount(localpart smtp.Localpart, domain dns.Domain, allowPostmaster bo
|
||||
|
||||
accAddr, ok := Conf.AccountDestination(canonical)
|
||||
if !ok {
|
||||
return "", "", config.Destination{}, ErrAccountNotFound
|
||||
if accAddr, ok = Conf.AccountDestination("@" + domain.Name()); !ok {
|
||||
return "", "", config.Destination{}, ErrAccountNotFound
|
||||
}
|
||||
canonical = "@" + domain.Name()
|
||||
}
|
||||
return accAddr.Account, canonical, accAddr.Destination, nil
|
||||
}
|
||||
|
Reference in New Issue
Block a user