git » chasquid » commit f303e43

aliases: Implement catch-all

author Alberto Bertogli
2022-01-14 00:58:26 UTC
committer Alberto Bertogli
2022-03-11 20:51:06 UTC
parent 3255ff68018bc51908ba2ad864fd8e3d7b876c31

aliases: Implement catch-all

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 https://github.com/albertito/chasquid/issues/23 and
https://github.com/albertito/chasquid/pull/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.

cmd/chasquid-util/chasquid-util.go +4 -2
docs/aliases.md +11 -1
internal/aliases/aliases.go +66 -6
internal/aliases/aliases_test.go +182 -47
internal/aliases/testdata/erroring-hook.sh +5 -0
internal/queue/queue_test.go +16 -7
internal/smtpsrv/server.go +4 -2

diff --git a/cmd/chasquid-util/chasquid-util.go b/cmd/chasquid-util/chasquid-util.go
index e932ba8..ff9dcbe 100644
--- a/cmd/chasquid-util/chasquid-util.go
+++ b/cmd/chasquid-util/chasquid-util.go
@@ -224,7 +224,7 @@ func aliasesResolve() {
 	}
 	_ = os.Chdir(configDir)
 
-	r := aliases.NewResolver()
+	r := aliases.NewResolver(allUsersExist)
 	r.SuffixSep = *conf.SuffixSeparators
 	r.DropChars = *conf.DropCharacters
 
@@ -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"]
@@ -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
 
diff --git a/docs/aliases.md b/docs/aliases.md
index f4ad31a..1a957d0 100644
--- a/docs/aliases.md
+++ b/docs/aliases.md
@@ -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
 
@@ -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
diff --git a/internal/aliases/aliases.go b/internal/aliases/aliases.go
index 02079cd..4e62e50 100644
--- a/internal/aliases/aliases.go
+++ b/internal/aliases/aliases.go
@@ -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
 //
@@ -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.
@@ -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
@@ -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,
 	}
 }
 
@@ -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) {
@@ -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
 	}
@@ -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{}
@@ -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
 	}
 
diff --git a/internal/aliases/aliases_test.go b/internal/aliases/aliases_test.go
index 49118de..a0e091b 100644
--- a/internal/aliases/aliases_test.go
+++ b/internal/aliases/aliases_test.go
@@ -1,27 +1,32 @@
 package aliases
 
 import (
+	"errors"
 	"io/ioutil"
 	"os"
+	"os/exec"
 	"reflect"
+	"strings"
 	"testing"
 )
 
 type Cases []struct {
 	addr   string
 	expect []Recipient
+	err    error
 }
 
 func (cases Cases) check(t *testing.T, r *Resolver) {
 	t.Helper()
 	for _, c := range cases {
 		got, err := r.Resolve(c.addr)
-		if err != nil {
-			t.Errorf("case %q, got error: %v", c.addr, err)
-			continue
+		if err != c.err {
+			t.Errorf("case %q: expected error %v, got %v",
+				c.addr, c.err, err)
 		}
 		if !reflect.DeepEqual(got, c.expect) {
-			t.Errorf("case %q, got %+v, expected %+v", c.addr, got, c.expect)
+			t.Errorf("case %q: got %+v, expected %+v",
+				c.addr, got, c.expect)
 		}
 	}
 }
@@ -44,8 +49,31 @@ func mustNotExist(t *testing.T, r *Resolver, addrs ...string) {
 	}
 }
 
+func allUsersExist(user, domain string) (bool, error) {
+	return true, nil
+}
+
+func usersWithXDontExist(user, domain string) (bool, error) {
+	if strings.HasPrefix(user, "x") {
+		return false, nil
+	}
+	return true, nil
+}
+
+var userLookupError = errors.New("test error userLookupError")
+
+func usersWithXErrorYDontExist(user, domain string) (bool, error) {
+	if strings.HasPrefix(user, "x") {
+		return false, userLookupError
+	}
+	if strings.HasPrefix(user, "y") {
+		return false, nil
+	}
+	return true, nil
+}
+
 func TestBasic(t *testing.T) {
-	resolver := NewResolver()
+	resolver := NewResolver(allUsersExist)
 	resolver.AddDomain("localA")
 	resolver.AddDomain("localB")
 	resolver.aliases = map[string][]Recipient{
@@ -55,9 +83,9 @@ func TestBasic(t *testing.T) {
 	}
 
 	cases := Cases{
-		{"a@localA", []Recipient{{"c@d", EMAIL}, {"cmd", PIPE}}},
-		{"e@localB", []Recipient{{"cmd", PIPE}}},
-		{"x@y", []Recipient{{"x@y", EMAIL}}},
+		{"a@localA", []Recipient{{"c@d", EMAIL}, {"cmd", PIPE}}, nil},
+		{"e@localB", []Recipient{{"cmd", PIPE}}, nil},
+		{"x@y", []Recipient{{"x@y", EMAIL}}, nil},
 	}
 	cases.check(t, resolver)
 
@@ -65,8 +93,59 @@ func TestBasic(t *testing.T) {
 	mustNotExist(t, resolver, "x@y")
 }
 
+func TestCatchAll(t *testing.T) {
+	resolver := NewResolver(usersWithXDontExist)
+	resolver.AddDomain("dom")
+	resolver.aliases = map[string][]Recipient{
+		"a@dom": {{"a@remote", EMAIL}},
+		"b@dom": {{"c@dom", EMAIL}},
+		"c@dom": {{"cmd", PIPE}},
+		"*@dom": {{"c@dom", EMAIL}},
+	}
+
+	cases := Cases{
+		{"a@dom", []Recipient{{"a@remote", EMAIL}}, nil},
+		{"b@dom", []Recipient{{"cmd", PIPE}}, nil},
+		{"c@dom", []Recipient{{"cmd", PIPE}}, nil},
+		{"x@dom", []Recipient{{"cmd", PIPE}}, nil},
+
+		// Remote should be returned as-is regardless.
+		{"a@remote", []Recipient{{"a@remote", EMAIL}}, nil},
+		{"x@remote", []Recipient{{"x@remote", EMAIL}}, nil},
+	}
+	cases.check(t, resolver)
+
+	mustExist(t, resolver,
+		// Exist as users.
+		"a@dom", "b@dom", "c@dom",
+
+		// Do not exist as users, but catch-all saves them.
+		"x@dom", "x1@dom")
+}
+
+func TestUserLookupErrors(t *testing.T) {
+	resolver := NewResolver(usersWithXErrorYDontExist)
+	resolver.AddDomain("dom")
+	resolver.aliases = map[string][]Recipient{
+		"a@dom": {{"a@remote", EMAIL}},
+		"b@dom": {{"x@dom", EMAIL}},
+		"*@dom": {{"x@dom", EMAIL}},
+	}
+
+	cases := Cases{
+		{"a@dom", []Recipient{{"a@remote", EMAIL}}, nil},
+		{"b@dom", nil, userLookupError},
+		{"c@dom", []Recipient{{"c@dom", EMAIL}}, nil},
+		{"x@dom", nil, userLookupError},
+
+		// This one goes through the catch-all.
+		{"y@dom", nil, userLookupError},
+	}
+	cases.check(t, resolver)
+}
+
 func TestAddrRewrite(t *testing.T) {
-	resolver := NewResolver()
+	resolver := NewResolver(allUsersExist)
 	resolver.AddDomain("def")
 	resolver.AddDomain("p-q.com")
 	resolver.aliases = map[string][]Recipient{
@@ -79,36 +158,36 @@ func TestAddrRewrite(t *testing.T) {
 	resolver.SuffixSep = "-+"
 
 	cases := Cases{
-		{"abc@def", []Recipient{{"x@y", EMAIL}}},
-		{"a.b.c@def", []Recipient{{"x@y", EMAIL}}},
-		{"a~b~c@def", []Recipient{{"x@y", EMAIL}}},
-		{"a.b~c@def", []Recipient{{"x@y", EMAIL}}},
-		{"abc-ñaca@def", []Recipient{{"x@y", EMAIL}}},
-		{"abc-ñaca@def", []Recipient{{"x@y", EMAIL}}},
-		{"abc-xyz@def", []Recipient{{"x@y", EMAIL}}},
-		{"abc+xyz@def", []Recipient{{"x@y", EMAIL}}},
-		{"abc-x.y+z@def", []Recipient{{"x@y", EMAIL}}},
-
-		{"ñ.o~ño-ñaca@def", []Recipient{{"x@y", EMAIL}}},
+		{"abc@def", []Recipient{{"x@y", EMAIL}}, nil},
+		{"a.b.c@def", []Recipient{{"x@y", EMAIL}}, nil},
+		{"a~b~c@def", []Recipient{{"x@y", EMAIL}}, nil},
+		{"a.b~c@def", []Recipient{{"x@y", EMAIL}}, nil},
+		{"abc-ñaca@def", []Recipient{{"x@y", EMAIL}}, nil},
+		{"abc-ñaca@def", []Recipient{{"x@y", EMAIL}}, nil},
+		{"abc-xyz@def", []Recipient{{"x@y", EMAIL}}, nil},
+		{"abc+xyz@def", []Recipient{{"x@y", EMAIL}}, nil},
+		{"abc-x.y+z@def", []Recipient{{"x@y", EMAIL}}, nil},
+
+		{"ñ.o~ño-ñaca@def", []Recipient{{"x@y", EMAIL}}, nil},
 
 		// Don't mess with the domain, even if it's known.
-		{"a.bc-ñaca@p-q.com", []Recipient{{"abc@p-q.com", EMAIL}}},
+		{"a.bc-ñaca@p-q.com", []Recipient{{"abc@p-q.com", EMAIL}}, nil},
 
 		// Clean the right hand side too (if it's a local domain).
-		{"recu+blah@def", []Recipient{{"ab@p-q.com", EMAIL}}},
+		{"recu+blah@def", []Recipient{{"ab@p-q.com", EMAIL}}, nil},
 
 		// We should not mess with emails for domains we don't know.
-		{"xy@z.com", []Recipient{{"xy@z.com", EMAIL}}},
-		{"x.y@z.com", []Recipient{{"x.y@z.com", EMAIL}}},
-		{"x-@y-z.com", []Recipient{{"x-@y-z.com", EMAIL}}},
-		{"x+blah@y", []Recipient{{"x+blah@y", EMAIL}}},
-		{"remo@def", []Recipient{{"x-@y-z.com", EMAIL}}},
+		{"xy@z.com", []Recipient{{"xy@z.com", EMAIL}}, nil},
+		{"x.y@z.com", []Recipient{{"x.y@z.com", EMAIL}}, nil},
+		{"x-@y-z.com", []Recipient{{"x-@y-z.com", EMAIL}}, nil},
+		{"x+blah@y", []Recipient{{"x+blah@y", EMAIL}}, nil},
+		{"remo@def", []Recipient{{"x-@y-z.com", EMAIL}}, nil},
 	}
 	cases.check(t, resolver)
 }
 
 func TestExistsRewrite(t *testing.T) {
-	resolver := NewResolver()
+	resolver := NewResolver(allUsersExist)
 	resolver.AddDomain("def")
 	resolver.AddDomain("p-q.com")
 	resolver.aliases = map[string][]Recipient{
@@ -150,7 +229,7 @@ func TestExistsRewrite(t *testing.T) {
 }
 
 func TestTooMuchRecursion(t *testing.T) {
-	resolver := NewResolver()
+	resolver := NewResolver(allUsersExist)
 	resolver.AddDomain("b")
 	resolver.AddDomain("d")
 	resolver.aliases = map[string][]Recipient{
@@ -168,6 +247,34 @@ func TestTooMuchRecursion(t *testing.T) {
 	}
 }
 
+func TestTooMuchRecursionOnCatchAll(t *testing.T) {
+	resolver := NewResolver(usersWithXDontExist)
+	resolver.AddDomain("dom")
+	resolver.aliases = map[string][]Recipient{
+		"a@dom": {{"x@dom", EMAIL}},
+		"*@dom": {{"a@dom", EMAIL}},
+	}
+
+	cases := Cases{
+		// b@dom is local and exists.
+		{"b@dom", []Recipient{{"b@dom", EMAIL}}, nil},
+
+		// a@remote is remote.
+		{"a@remote", []Recipient{{"a@remote", EMAIL}}, nil},
+	}
+	cases.check(t, resolver)
+
+	for _, addr := range []string{"a@dom", "x@dom", "xx@dom"} {
+		rs, err := resolver.Resolve(addr)
+		if err != ErrRecursionLimitExceeded {
+			t.Errorf("%s: expected ErrRecursionLimitExceeded, got %v", addr, err)
+		}
+		if rs != nil {
+			t.Errorf("%s: expected nil recipients, got %+v", addr, rs)
+		}
+	}
+}
+
 func mustWriteFile(t *testing.T, content string) string {
 	f, err := ioutil.TempFile("", "aliases_test")
 	if err != nil {
@@ -217,7 +324,7 @@ func TestAddFile(t *testing.T) {
 		fname := mustWriteFile(t, c.contents)
 		defer os.Remove(fname)
 
-		resolver := NewResolver()
+		resolver := NewResolver(allUsersExist)
 		err := resolver.AddAliasesFile("dom", fname)
 		if err != nil {
 			t.Fatalf("error adding file: %v", err)
@@ -260,20 +367,20 @@ func TestRichFile(t *testing.T) {
 	fname := mustWriteFile(t, richFileContents)
 	defer os.Remove(fname)
 
-	resolver := NewResolver()
+	resolver := NewResolver(allUsersExist)
 	err := resolver.AddAliasesFile("dom", fname)
 	if err != nil {
 		t.Fatalf("failed to add file: %v", err)
 	}
 
 	cases := Cases{
-		{"a@dom", []Recipient{{"b@dom", EMAIL}}},
-		{"c@dom", []Recipient{{"d@e", EMAIL}, {"f@dom", EMAIL}}},
-		{"x@dom", []Recipient{{"command", PIPE}}},
-		{"o1@dom", []Recipient{{"b@dom", EMAIL}}},
-		{"aA@dom", []Recipient{{"bb@dom-b", EMAIL}}},
-		{"aa@dom", []Recipient{{"bb@dom-b", EMAIL}}},
-		{"y@dom", []Recipient{{"z@dom", EMAIL}}},
+		{"a@dom", []Recipient{{"b@dom", EMAIL}}, nil},
+		{"c@dom", []Recipient{{"d@e", EMAIL}, {"f@dom", EMAIL}}, nil},
+		{"x@dom", []Recipient{{"command", PIPE}}, nil},
+		{"o1@dom", []Recipient{{"b@dom", EMAIL}}, nil},
+		{"aA@dom", []Recipient{{"bb@dom-b", EMAIL}}, nil},
+		{"aa@dom", []Recipient{{"bb@dom-b", EMAIL}}, nil},
+		{"y@dom", []Recipient{{"z@dom", EMAIL}}, nil},
 	}
 	cases.check(t, resolver)
 }
@@ -293,7 +400,7 @@ func TestManyFiles(t *testing.T) {
 		defer os.Remove(fname)
 	}
 
-	resolver := NewResolver()
+	resolver := NewResolver(allUsersExist)
 	for domain, fname := range files {
 		err := resolver.AddAliasesFile(domain, fname)
 		if err != nil {
@@ -303,14 +410,14 @@ func TestManyFiles(t *testing.T) {
 
 	check := func() {
 		cases := Cases{
-			{"a@d1", []Recipient{{"b@d1", EMAIL}}},
-			{"c@d1", []Recipient{{"d@e", EMAIL}}},
-			{"x@d1", []Recipient{{"x@d1", EMAIL}}},
-			{"a@domain2", []Recipient{{"b@domain2", EMAIL}}},
-			{"c@domain2", []Recipient{{"d@e", EMAIL}}},
-			{"x@dom3", []Recipient{{"y@dom3", EMAIL}, {"z@dom3", EMAIL}}},
-			{"a@dom4", []Recipient{{"cmd", PIPE}}},
-			{"a@xd1", []Recipient{{"cmd", PIPE}}},
+			{"a@d1", []Recipient{{"b@d1", EMAIL}}, nil},
+			{"c@d1", []Recipient{{"d@e", EMAIL}}, nil},
+			{"x@d1", []Recipient{{"x@d1", EMAIL}}, nil},
+			{"a@domain2", []Recipient{{"b@domain2", EMAIL}}, nil},
+			{"c@domain2", []Recipient{{"d@e", EMAIL}}, nil},
+			{"x@dom3", []Recipient{{"y@dom3", EMAIL}, {"z@dom3", EMAIL}}, nil},
+			{"a@dom4", []Recipient{{"cmd", PIPE}}, nil},
+			{"a@xd1", []Recipient{{"cmd", PIPE}}, nil},
 		}
 		cases.check(t, resolver)
 	}
@@ -324,3 +431,31 @@ func TestManyFiles(t *testing.T) {
 
 	check()
 }
+
+func TestHookError(t *testing.T) {
+	resolver := NewResolver(allUsersExist)
+	resolver.AddDomain("localA")
+	resolver.aliases = map[string][]Recipient{
+		"a@localA": {{"c@d", EMAIL}},
+	}
+
+	// First check that the test is set up reasonably.
+	mustExist(t, resolver, "a@localA")
+	Cases{
+		{"a@localA", []Recipient{{"c@d", EMAIL}}, nil},
+	}.check(t, resolver)
+
+	// Now use a resolver that exits with an error.
+	resolver.ResolveHook = "testdata/erroring-hook.sh"
+
+	// Check that the hook is run and the error is propagated.
+	mustNotExist(t, resolver, "a@localA")
+	rcpts, err := resolver.Resolve("a@localA")
+	if len(rcpts) != 0 {
+		t.Errorf("expected no recipients, got %v", rcpts)
+	}
+	execErr := &exec.ExitError{}
+	if !errors.As(err, &execErr) {
+		t.Errorf("expected *exec.ExitError, got %T - %v", err, err)
+	}
+}
diff --git a/internal/aliases/testdata/erroring-hook.sh b/internal/aliases/testdata/erroring-hook.sh
new file mode 100755
index 0000000..551a5b6
--- /dev/null
+++ b/internal/aliases/testdata/erroring-hook.sh
@@ -0,0 +1,5 @@
+#!/bin/sh
+
+# Hook that always returns error.
+# This could be replaced by /bin/false, but that doesn't work on freebsd.
+exit 1
diff --git a/internal/queue/queue_test.go b/internal/queue/queue_test.go
index 9782c68..acef9cd 100644
--- a/internal/queue/queue_test.go
+++ b/internal/queue/queue_test.go
@@ -12,12 +12,15 @@ import (
 	"blitiri.com.ar/go/chasquid/internal/testlib"
 )
 
+func allUsersExist(user, domain string) (bool, error) { return true, nil }
+
 func TestBasic(t *testing.T) {
 	dir := testlib.MustTempDir(t)
 	defer testlib.RemoveIfOk(t, dir)
 	localC := testlib.NewTestCourier()
 	remoteC := testlib.NewTestCourier()
-	q, _ := New(dir, set.NewString("loco"), aliases.NewResolver(),
+	q, _ := New(dir, set.NewString("loco"),
+		aliases.NewResolver(allUsersExist),
 		localC, remoteC)
 
 	localC.Expect(2)
@@ -67,7 +70,8 @@ func TestDSNOnTimeout(t *testing.T) {
 	remoteC := testlib.NewTestCourier()
 	dir := testlib.MustTempDir(t)
 	defer testlib.RemoveIfOk(t, dir)
-	q, _ := New(dir, set.NewString("loco"), aliases.NewResolver(),
+	q, _ := New(dir, set.NewString("loco"),
+		aliases.NewResolver(allUsersExist),
 		localC, remoteC)
 
 	// Insert an expired item in the queue.
@@ -111,7 +115,8 @@ func TestAliases(t *testing.T) {
 	remoteC := testlib.NewTestCourier()
 	dir := testlib.MustTempDir(t)
 	defer testlib.RemoveIfOk(t, dir)
-	q, _ := New(dir, set.NewString("loco"), aliases.NewResolver(),
+	q, _ := New(dir, set.NewString("loco"),
+		aliases.NewResolver(allUsersExist),
 		localC, remoteC)
 
 	q.aliases.AddDomain("loco")
@@ -155,7 +160,8 @@ func TestAliases(t *testing.T) {
 func TestFullQueue(t *testing.T) {
 	dir := testlib.MustTempDir(t)
 	defer testlib.RemoveIfOk(t, dir)
-	q, _ := New(dir, set.NewString(), aliases.NewResolver(),
+	q, _ := New(dir, set.NewString(),
+		aliases.NewResolver(allUsersExist),
 		testlib.DumbCourier, testlib.DumbCourier)
 
 	// Force-insert maxQueueSize items in the queue.
@@ -197,7 +203,8 @@ func TestFullQueue(t *testing.T) {
 func TestPipes(t *testing.T) {
 	dir := testlib.MustTempDir(t)
 	defer testlib.RemoveIfOk(t, dir)
-	q, _ := New(dir, set.NewString("loco"), aliases.NewResolver(),
+	q, _ := New(dir, set.NewString("loco"),
+		aliases.NewResolver(allUsersExist),
 		testlib.DumbCourier, testlib.DumbCourier)
 	item := &Item{
 		Message: Message{
@@ -219,7 +226,8 @@ func TestBadPath(t *testing.T) {
 	// A new queue will attempt to os.MkdirAll the path.
 	// We expect this path to fail.
 	_, err := New("/proc/doesnotexist", set.NewString("loco"),
-		aliases.NewResolver(), testlib.DumbCourier, testlib.DumbCourier)
+		aliases.NewResolver(allUsersExist),
+		testlib.DumbCourier, testlib.DumbCourier)
 	if err == nil {
 		t.Errorf("could create queue, expected permission denied")
 	}
@@ -270,7 +278,8 @@ func TestSerialization(t *testing.T) {
 	// Create the queue; should load the
 	remoteC := testlib.NewTestCourier()
 	remoteC.Expect(1)
-	q, _ := New(dir, set.NewString("loco"), aliases.NewResolver(),
+	q, _ := New(dir, set.NewString("loco"),
+		aliases.NewResolver(allUsersExist),
 		testlib.DumbCourier, remoteC)
 	q.Load()
 
diff --git a/internal/smtpsrv/server.go b/internal/smtpsrv/server.go
index b4a5509..80484a4 100644
--- a/internal/smtpsrv/server.go
+++ b/internal/smtpsrv/server.go
@@ -76,6 +76,8 @@ type Server struct {
 
 // NewServer returns a new empty Server.
 func NewServer() *Server {
+	authr := auth.NewAuthenticator()
+	aliasesR := aliases.NewResolver(authr.Exists)
 	return &Server{
 		addrs:          map[SocketMode][]string{},
 		listeners:      map[SocketMode][]net.Listener{},
@@ -83,8 +85,8 @@ func NewServer() *Server {
 		connTimeout:    20 * time.Minute,
 		commandTimeout: 1 * time.Minute,
 		localDomains:   &set.String{},
-		authr:          auth.NewAuthenticator(),
-		aliasesR:       aliases.NewResolver(),
+		authr:          authr,
+		aliasesR:       aliasesR,
 	}
 }