Skip to content

Commit

Permalink
aliases: Implement catch-all
Browse files Browse the repository at this point in the history
This patch implements support for catch-all aliases, where users can add
a `*: destination` alias. Mails sent to unknown users (or other aliases)
will not be rejected, but sent to the indicated destination instead.

Please see #23 and
#24 for more discussion and
background.

Thanks to Alex Ellwein (aellwein@github) for the alternative patch and
help with testing; and to ThinkChaos (ThinkChaos@github) for help with
testing.
  • Loading branch information
albertito committed Mar 11, 2022
1 parent 3255ff6 commit f303e43
Show file tree
Hide file tree
Showing 7 changed files with 288 additions and 65 deletions.
6 changes: 4 additions & 2 deletions cmd/chasquid-util/chasquid-util.go
Original file line number Diff line number Diff line change
Expand Up @@ -224,7 +224,7 @@ func aliasesResolve() {
}
_ = os.Chdir(configDir)

r := aliases.NewResolver()
r := aliases.NewResolver(allUsersExist)
r.SuffixSep = *conf.SuffixSeparators
r.DropChars = *conf.DropCharacters

Expand Down Expand Up @@ -289,6 +289,8 @@ func domaininfoRemove() {
}
}

func allUsersExist(user, domain string) (bool, error) { return true, nil }

// chasquid-util aliases-add <source> <target>
func aliasesAdd() {
source := args["$2"]
Expand All @@ -315,7 +317,7 @@ func aliasesAdd() {
_ = os.Chdir(configDir)

// Setup alias resolver.
r := aliases.NewResolver()
r := aliases.NewResolver(allUsersExist)
r.SuffixSep = *conf.SuffixSeparators
r.DropChars = *conf.DropCharacters

Expand Down
12 changes: 11 additions & 1 deletion docs/aliases.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,16 @@ user: | /usr/bin/email-handler --work
null: | cat
```

### Catch-all

If the aliased user is `*`, then mail sent to an unknown user will not be
rejected, but redirected to the indicated destination instead.

```
pepe: jose
*: pepe, rose@backgarden
```

## Processing

Expand All @@ -80,7 +90,7 @@ will fail. If the command exits with an error (non-0 exit code), the delivery
will be considered failed.

The `chasquid-util` command-line tool can be used to check and resolve
aliases.
aliases. Note that it doesn't run aliases hooks, or handle catch-all aliases.


## Hooks
Expand Down
72 changes: 66 additions & 6 deletions internal/aliases/aliases.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@
// Usually there will be one database per domain, and there's no need to
// include the "@" in the user (in this case, "@" will be forbidden).
//
// If the user is the string "*", then it is considered a "catch-all alias":
// emails that don't match any known users or other aliases will be sent here.
//
//
// Recipients
//
Expand Down Expand Up @@ -104,6 +107,9 @@ var (
recursionLimit = 10
)

// Type of the "does this user exist" function", for convenience.
type existsFn func(user, domain string) (bool, error)

// Resolver represents the aliases resolver.
type Resolver struct {
// Suffix separator, to perform suffix removal.
Expand All @@ -115,6 +121,9 @@ type Resolver struct {
// Path to the resolve hook.
ResolveHook string

// Function to check if a user exists in the userdb.
userExistsInDB existsFn

// Map of domain -> alias files for that domain.
// We keep track of them for reloading purposes.
files map[string][]string
Expand All @@ -128,11 +137,13 @@ type Resolver struct {
}

// NewResolver returns a new, empty Resolver.
func NewResolver() *Resolver {
func NewResolver(userExists existsFn) *Resolver {
return &Resolver{
files: map[string][]string{},
domains: map[string]bool{},
aliases: map[string][]Recipient{},

userExistsInDB: userExists,
}
}

Expand All @@ -155,7 +166,17 @@ func (v *Resolver) Exists(addr string) (string, bool) {
addr = v.cleanIfLocal(addr)

rcpts, _ := v.lookup(addr, tr)
return addr, len(rcpts) > 0
if len(rcpts) > 0 {
return addr, true
}

domain := envelope.DomainOf(addr)
catchAll, _ := v.lookup("*@"+domain, tr)
if len(catchAll) > 0 {
return addr, true
}

return addr, false
}

func (v *Resolver) lookup(addr string, tr *trace.Trace) ([]Recipient, error) {
Expand Down Expand Up @@ -183,7 +204,8 @@ func (v *Resolver) resolve(rcount int, addr string, tr *trace.Trace) ([]Recipien
// If the address is not local, we return it as-is, so delivery is
// attempted against it.
// Example: an alias that resolves to a non-local address.
if _, ok := v.domains[envelope.DomainOf(addr)]; !ok {
user, domain := envelope.Split(addr)
if _, ok := v.domains[domain]; !ok {
tr.Debugf("%d| non-local domain, returning %q", rcount, addr)
return []Recipient{{addr, EMAIL}}, nil
}
Expand All @@ -200,9 +222,43 @@ func (v *Resolver) resolve(rcount int, addr string, tr *trace.Trace) ([]Recipien
return nil, err
}

// No alias for this local address.
if len(rcpts) == 0 {
tr.Debugf("%d| no aliases found, returning %q", rcount, addr)
return []Recipient{{addr, EMAIL}}, nil
tr.Debugf("%d| no alias found", rcount)
// If the user exists, then use it as-is, no need to recurse further.
ok, err := v.userExistsInDB(user, domain)
if err != nil {
tr.Debugf("%d| error checking if user exists: %v", rcount, err)
return nil, err
}
if ok {
tr.Debugf("%d| user exists, returning %q", rcount, addr)
return []Recipient{{addr, EMAIL}}, nil
}

catchAll, err := v.lookup("*@"+domain, tr)
if err != nil {
tr.Debugf("%d| error in catchall lookup: %v", rcount, err)
return nil, err
}
if len(catchAll) > 0 {
// If there's a catch-all, then use it and keep resolving
// recursively (since the catch-all destination could be an
// alias).
tr.Debugf("%d| using catch-all: %v", rcount, catchAll)
rcpts = catchAll
} else {
// Otherwise, return the original address unchanged.
// The caller will handle that situation, and we don't need to
// invalidate the whole resolution (there could be other valid
// aliases).
// The queue will attempt delivery against this local (but
// evidently non-existing) address, and the courier will emit a
// clearer failure, re-using the existing codepaths and
// simplifying the logic.
tr.Debugf("%d| no catch-all, returning %q", rcount, addr)
return []Recipient{{addr, EMAIL}}, nil
}
}

ret := []Recipient{}
Expand All @@ -229,7 +285,11 @@ func (v *Resolver) resolve(rcount int, addr string, tr *trace.Trace) ([]Recipien
func (v *Resolver) cleanIfLocal(addr string) string {
user, domain := envelope.Split(addr)

if !v.domains[domain] {
v.mu.Lock()
isLocal := v.domains[domain]
v.mu.Unlock()

if !isLocal {
return addr
}

Expand Down

0 comments on commit f303e43

Please sign in to comment.