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:
Mechiel Lukkien
2023-03-29 21:11:43 +02:00
parent 51ad345dbb
commit b571dd4b28
16 changed files with 176 additions and 58 deletions

View File

@ -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
}