mirror of
https://blitiri.com.ar/repos/chasquid
synced 2025-12-18 14:47:03 +00:00
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.
This commit is contained in:
@@ -224,7 +224,7 @@ func aliasesResolve() {
|
|||||||
}
|
}
|
||||||
_ = os.Chdir(configDir)
|
_ = os.Chdir(configDir)
|
||||||
|
|
||||||
r := aliases.NewResolver()
|
r := aliases.NewResolver(allUsersExist)
|
||||||
r.SuffixSep = *conf.SuffixSeparators
|
r.SuffixSep = *conf.SuffixSeparators
|
||||||
r.DropChars = *conf.DropCharacters
|
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>
|
// chasquid-util aliases-add <source> <target>
|
||||||
func aliasesAdd() {
|
func aliasesAdd() {
|
||||||
source := args["$2"]
|
source := args["$2"]
|
||||||
@@ -315,7 +317,7 @@ func aliasesAdd() {
|
|||||||
_ = os.Chdir(configDir)
|
_ = os.Chdir(configDir)
|
||||||
|
|
||||||
// Setup alias resolver.
|
// Setup alias resolver.
|
||||||
r := aliases.NewResolver()
|
r := aliases.NewResolver(allUsersExist)
|
||||||
r.SuffixSep = *conf.SuffixSeparators
|
r.SuffixSep = *conf.SuffixSeparators
|
||||||
r.DropChars = *conf.DropCharacters
|
r.DropChars = *conf.DropCharacters
|
||||||
|
|
||||||
|
|||||||
@@ -65,6 +65,16 @@ user: | /usr/bin/email-handler --work
|
|||||||
null: | cat
|
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
|
## Processing
|
||||||
|
|
||||||
@@ -80,7 +90,7 @@ will fail. If the command exits with an error (non-0 exit code), the delivery
|
|||||||
will be considered failed.
|
will be considered failed.
|
||||||
|
|
||||||
The `chasquid-util` command-line tool can be used to check and resolve
|
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
|
## Hooks
|
||||||
|
|||||||
@@ -24,6 +24,9 @@
|
|||||||
// Usually there will be one database per domain, and there's no need to
|
// 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).
|
// 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
|
// Recipients
|
||||||
//
|
//
|
||||||
@@ -104,6 +107,9 @@ var (
|
|||||||
recursionLimit = 10
|
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.
|
// Resolver represents the aliases resolver.
|
||||||
type Resolver struct {
|
type Resolver struct {
|
||||||
// Suffix separator, to perform suffix removal.
|
// Suffix separator, to perform suffix removal.
|
||||||
@@ -115,6 +121,9 @@ type Resolver struct {
|
|||||||
// Path to the resolve hook.
|
// Path to the resolve hook.
|
||||||
ResolveHook string
|
ResolveHook string
|
||||||
|
|
||||||
|
// Function to check if a user exists in the userdb.
|
||||||
|
userExistsInDB existsFn
|
||||||
|
|
||||||
// Map of domain -> alias files for that domain.
|
// Map of domain -> alias files for that domain.
|
||||||
// We keep track of them for reloading purposes.
|
// We keep track of them for reloading purposes.
|
||||||
files map[string][]string
|
files map[string][]string
|
||||||
@@ -128,11 +137,13 @@ type Resolver struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewResolver returns a new, empty Resolver.
|
// NewResolver returns a new, empty Resolver.
|
||||||
func NewResolver() *Resolver {
|
func NewResolver(userExists existsFn) *Resolver {
|
||||||
return &Resolver{
|
return &Resolver{
|
||||||
files: map[string][]string{},
|
files: map[string][]string{},
|
||||||
domains: map[string]bool{},
|
domains: map[string]bool{},
|
||||||
aliases: map[string][]Recipient{},
|
aliases: map[string][]Recipient{},
|
||||||
|
|
||||||
|
userExistsInDB: userExists,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,7 +166,17 @@ func (v *Resolver) Exists(addr string) (string, bool) {
|
|||||||
addr = v.cleanIfLocal(addr)
|
addr = v.cleanIfLocal(addr)
|
||||||
|
|
||||||
rcpts, _ := v.lookup(addr, tr)
|
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) {
|
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
|
// If the address is not local, we return it as-is, so delivery is
|
||||||
// attempted against it.
|
// attempted against it.
|
||||||
// Example: an alias that resolves to a non-local address.
|
// 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)
|
tr.Debugf("%d| non-local domain, returning %q", rcount, addr)
|
||||||
return []Recipient{{addr, EMAIL}}, nil
|
return []Recipient{{addr, EMAIL}}, nil
|
||||||
}
|
}
|
||||||
@@ -200,9 +222,43 @@ func (v *Resolver) resolve(rcount int, addr string, tr *trace.Trace) ([]Recipien
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No alias for this local address.
|
||||||
if len(rcpts) == 0 {
|
if len(rcpts) == 0 {
|
||||||
tr.Debugf("%d| no aliases found, returning %q", rcount, addr)
|
tr.Debugf("%d| no alias found", rcount)
|
||||||
return []Recipient{{addr, EMAIL}}, nil
|
// 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{}
|
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 {
|
func (v *Resolver) cleanIfLocal(addr string) string {
|
||||||
user, domain := envelope.Split(addr)
|
user, domain := envelope.Split(addr)
|
||||||
|
|
||||||
if !v.domains[domain] {
|
v.mu.Lock()
|
||||||
|
isLocal := v.domains[domain]
|
||||||
|
v.mu.Unlock()
|
||||||
|
|
||||||
|
if !isLocal {
|
||||||
return addr
|
return addr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,27 +1,32 @@
|
|||||||
package aliases
|
package aliases
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Cases []struct {
|
type Cases []struct {
|
||||||
addr string
|
addr string
|
||||||
expect []Recipient
|
expect []Recipient
|
||||||
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (cases Cases) check(t *testing.T, r *Resolver) {
|
func (cases Cases) check(t *testing.T, r *Resolver) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
got, err := r.Resolve(c.addr)
|
got, err := r.Resolve(c.addr)
|
||||||
if err != nil {
|
if err != c.err {
|
||||||
t.Errorf("case %q, got error: %v", c.addr, err)
|
t.Errorf("case %q: expected error %v, got %v",
|
||||||
continue
|
c.addr, c.err, err)
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(got, c.expect) {
|
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) {
|
func TestBasic(t *testing.T) {
|
||||||
resolver := NewResolver()
|
resolver := NewResolver(allUsersExist)
|
||||||
resolver.AddDomain("localA")
|
resolver.AddDomain("localA")
|
||||||
resolver.AddDomain("localB")
|
resolver.AddDomain("localB")
|
||||||
resolver.aliases = map[string][]Recipient{
|
resolver.aliases = map[string][]Recipient{
|
||||||
@@ -55,9 +83,9 @@ func TestBasic(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cases := Cases{
|
cases := Cases{
|
||||||
{"a@localA", []Recipient{{"c@d", EMAIL}, {"cmd", PIPE}}},
|
{"a@localA", []Recipient{{"c@d", EMAIL}, {"cmd", PIPE}}, nil},
|
||||||
{"e@localB", []Recipient{{"cmd", PIPE}}},
|
{"e@localB", []Recipient{{"cmd", PIPE}}, nil},
|
||||||
{"x@y", []Recipient{{"x@y", EMAIL}}},
|
{"x@y", []Recipient{{"x@y", EMAIL}}, nil},
|
||||||
}
|
}
|
||||||
cases.check(t, resolver)
|
cases.check(t, resolver)
|
||||||
|
|
||||||
@@ -65,8 +93,59 @@ func TestBasic(t *testing.T) {
|
|||||||
mustNotExist(t, resolver, "x@y")
|
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) {
|
func TestAddrRewrite(t *testing.T) {
|
||||||
resolver := NewResolver()
|
resolver := NewResolver(allUsersExist)
|
||||||
resolver.AddDomain("def")
|
resolver.AddDomain("def")
|
||||||
resolver.AddDomain("p-q.com")
|
resolver.AddDomain("p-q.com")
|
||||||
resolver.aliases = map[string][]Recipient{
|
resolver.aliases = map[string][]Recipient{
|
||||||
@@ -79,36 +158,36 @@ func TestAddrRewrite(t *testing.T) {
|
|||||||
resolver.SuffixSep = "-+"
|
resolver.SuffixSep = "-+"
|
||||||
|
|
||||||
cases := Cases{
|
cases := Cases{
|
||||||
{"abc@def", []Recipient{{"x@y", EMAIL}}},
|
{"abc@def", []Recipient{{"x@y", EMAIL}}, nil},
|
||||||
{"a.b.c@def", []Recipient{{"x@y", EMAIL}}},
|
{"a.b.c@def", []Recipient{{"x@y", EMAIL}}, nil},
|
||||||
{"a~b~c@def", []Recipient{{"x@y", EMAIL}}},
|
{"a~b~c@def", []Recipient{{"x@y", EMAIL}}, nil},
|
||||||
{"a.b~c@def", []Recipient{{"x@y", EMAIL}}},
|
{"a.b~c@def", []Recipient{{"x@y", EMAIL}}, nil},
|
||||||
{"abc-ñaca@def", []Recipient{{"x@y", EMAIL}}},
|
{"abc-ñaca@def", []Recipient{{"x@y", EMAIL}}, nil},
|
||||||
{"abc-ñaca@def", []Recipient{{"x@y", EMAIL}}},
|
{"abc-ñaca@def", []Recipient{{"x@y", EMAIL}}, nil},
|
||||||
{"abc-xyz@def", []Recipient{{"x@y", EMAIL}}},
|
{"abc-xyz@def", []Recipient{{"x@y", EMAIL}}, nil},
|
||||||
{"abc+xyz@def", []Recipient{{"x@y", EMAIL}}},
|
{"abc+xyz@def", []Recipient{{"x@y", EMAIL}}, nil},
|
||||||
{"abc-x.y+z@def", []Recipient{{"x@y", EMAIL}}},
|
{"abc-x.y+z@def", []Recipient{{"x@y", EMAIL}}, nil},
|
||||||
|
|
||||||
{"ñ.o~ño-ñaca@def", []Recipient{{"x@y", EMAIL}}},
|
{"ñ.o~ño-ñaca@def", []Recipient{{"x@y", EMAIL}}, nil},
|
||||||
|
|
||||||
// Don't mess with the domain, even if it's known.
|
// 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).
|
// 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.
|
// We should not mess with emails for domains we don't know.
|
||||||
{"xy@z.com", []Recipient{{"xy@z.com", EMAIL}}},
|
{"xy@z.com", []Recipient{{"xy@z.com", EMAIL}}, nil},
|
||||||
{"x.y@z.com", []Recipient{{"x.y@z.com", EMAIL}}},
|
{"x.y@z.com", []Recipient{{"x.y@z.com", EMAIL}}, nil},
|
||||||
{"x-@y-z.com", []Recipient{{"x-@y-z.com", EMAIL}}},
|
{"x-@y-z.com", []Recipient{{"x-@y-z.com", EMAIL}}, nil},
|
||||||
{"x+blah@y", []Recipient{{"x+blah@y", EMAIL}}},
|
{"x+blah@y", []Recipient{{"x+blah@y", EMAIL}}, nil},
|
||||||
{"remo@def", []Recipient{{"x-@y-z.com", EMAIL}}},
|
{"remo@def", []Recipient{{"x-@y-z.com", EMAIL}}, nil},
|
||||||
}
|
}
|
||||||
cases.check(t, resolver)
|
cases.check(t, resolver)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestExistsRewrite(t *testing.T) {
|
func TestExistsRewrite(t *testing.T) {
|
||||||
resolver := NewResolver()
|
resolver := NewResolver(allUsersExist)
|
||||||
resolver.AddDomain("def")
|
resolver.AddDomain("def")
|
||||||
resolver.AddDomain("p-q.com")
|
resolver.AddDomain("p-q.com")
|
||||||
resolver.aliases = map[string][]Recipient{
|
resolver.aliases = map[string][]Recipient{
|
||||||
@@ -150,7 +229,7 @@ func TestExistsRewrite(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestTooMuchRecursion(t *testing.T) {
|
func TestTooMuchRecursion(t *testing.T) {
|
||||||
resolver := NewResolver()
|
resolver := NewResolver(allUsersExist)
|
||||||
resolver.AddDomain("b")
|
resolver.AddDomain("b")
|
||||||
resolver.AddDomain("d")
|
resolver.AddDomain("d")
|
||||||
resolver.aliases = map[string][]Recipient{
|
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 {
|
func mustWriteFile(t *testing.T, content string) string {
|
||||||
f, err := ioutil.TempFile("", "aliases_test")
|
f, err := ioutil.TempFile("", "aliases_test")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -217,7 +324,7 @@ func TestAddFile(t *testing.T) {
|
|||||||
fname := mustWriteFile(t, c.contents)
|
fname := mustWriteFile(t, c.contents)
|
||||||
defer os.Remove(fname)
|
defer os.Remove(fname)
|
||||||
|
|
||||||
resolver := NewResolver()
|
resolver := NewResolver(allUsersExist)
|
||||||
err := resolver.AddAliasesFile("dom", fname)
|
err := resolver.AddAliasesFile("dom", fname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("error adding file: %v", err)
|
t.Fatalf("error adding file: %v", err)
|
||||||
@@ -260,20 +367,20 @@ func TestRichFile(t *testing.T) {
|
|||||||
fname := mustWriteFile(t, richFileContents)
|
fname := mustWriteFile(t, richFileContents)
|
||||||
defer os.Remove(fname)
|
defer os.Remove(fname)
|
||||||
|
|
||||||
resolver := NewResolver()
|
resolver := NewResolver(allUsersExist)
|
||||||
err := resolver.AddAliasesFile("dom", fname)
|
err := resolver.AddAliasesFile("dom", fname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("failed to add file: %v", err)
|
t.Fatalf("failed to add file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
cases := Cases{
|
cases := Cases{
|
||||||
{"a@dom", []Recipient{{"b@dom", EMAIL}}},
|
{"a@dom", []Recipient{{"b@dom", EMAIL}}, nil},
|
||||||
{"c@dom", []Recipient{{"d@e", EMAIL}, {"f@dom", EMAIL}}},
|
{"c@dom", []Recipient{{"d@e", EMAIL}, {"f@dom", EMAIL}}, nil},
|
||||||
{"x@dom", []Recipient{{"command", PIPE}}},
|
{"x@dom", []Recipient{{"command", PIPE}}, nil},
|
||||||
{"o1@dom", []Recipient{{"b@dom", EMAIL}}},
|
{"o1@dom", []Recipient{{"b@dom", EMAIL}}, nil},
|
||||||
{"aA@dom", []Recipient{{"bb@dom-b", EMAIL}}},
|
{"aA@dom", []Recipient{{"bb@dom-b", EMAIL}}, nil},
|
||||||
{"aa@dom", []Recipient{{"bb@dom-b", EMAIL}}},
|
{"aa@dom", []Recipient{{"bb@dom-b", EMAIL}}, nil},
|
||||||
{"y@dom", []Recipient{{"z@dom", EMAIL}}},
|
{"y@dom", []Recipient{{"z@dom", EMAIL}}, nil},
|
||||||
}
|
}
|
||||||
cases.check(t, resolver)
|
cases.check(t, resolver)
|
||||||
}
|
}
|
||||||
@@ -293,7 +400,7 @@ func TestManyFiles(t *testing.T) {
|
|||||||
defer os.Remove(fname)
|
defer os.Remove(fname)
|
||||||
}
|
}
|
||||||
|
|
||||||
resolver := NewResolver()
|
resolver := NewResolver(allUsersExist)
|
||||||
for domain, fname := range files {
|
for domain, fname := range files {
|
||||||
err := resolver.AddAliasesFile(domain, fname)
|
err := resolver.AddAliasesFile(domain, fname)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -303,14 +410,14 @@ func TestManyFiles(t *testing.T) {
|
|||||||
|
|
||||||
check := func() {
|
check := func() {
|
||||||
cases := Cases{
|
cases := Cases{
|
||||||
{"a@d1", []Recipient{{"b@d1", EMAIL}}},
|
{"a@d1", []Recipient{{"b@d1", EMAIL}}, nil},
|
||||||
{"c@d1", []Recipient{{"d@e", EMAIL}}},
|
{"c@d1", []Recipient{{"d@e", EMAIL}}, nil},
|
||||||
{"x@d1", []Recipient{{"x@d1", EMAIL}}},
|
{"x@d1", []Recipient{{"x@d1", EMAIL}}, nil},
|
||||||
{"a@domain2", []Recipient{{"b@domain2", EMAIL}}},
|
{"a@domain2", []Recipient{{"b@domain2", EMAIL}}, nil},
|
||||||
{"c@domain2", []Recipient{{"d@e", EMAIL}}},
|
{"c@domain2", []Recipient{{"d@e", EMAIL}}, nil},
|
||||||
{"x@dom3", []Recipient{{"y@dom3", EMAIL}, {"z@dom3", EMAIL}}},
|
{"x@dom3", []Recipient{{"y@dom3", EMAIL}, {"z@dom3", EMAIL}}, nil},
|
||||||
{"a@dom4", []Recipient{{"cmd", PIPE}}},
|
{"a@dom4", []Recipient{{"cmd", PIPE}}, nil},
|
||||||
{"a@xd1", []Recipient{{"cmd", PIPE}}},
|
{"a@xd1", []Recipient{{"cmd", PIPE}}, nil},
|
||||||
}
|
}
|
||||||
cases.check(t, resolver)
|
cases.check(t, resolver)
|
||||||
}
|
}
|
||||||
@@ -324,3 +431,31 @@ func TestManyFiles(t *testing.T) {
|
|||||||
|
|
||||||
check()
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
5
internal/aliases/testdata/erroring-hook.sh
vendored
Executable file
5
internal/aliases/testdata/erroring-hook.sh
vendored
Executable file
@@ -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
|
||||||
@@ -12,12 +12,15 @@ import (
|
|||||||
"blitiri.com.ar/go/chasquid/internal/testlib"
|
"blitiri.com.ar/go/chasquid/internal/testlib"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func allUsersExist(user, domain string) (bool, error) { return true, nil }
|
||||||
|
|
||||||
func TestBasic(t *testing.T) {
|
func TestBasic(t *testing.T) {
|
||||||
dir := testlib.MustTempDir(t)
|
dir := testlib.MustTempDir(t)
|
||||||
defer testlib.RemoveIfOk(t, dir)
|
defer testlib.RemoveIfOk(t, dir)
|
||||||
localC := testlib.NewTestCourier()
|
localC := testlib.NewTestCourier()
|
||||||
remoteC := 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, remoteC)
|
||||||
|
|
||||||
localC.Expect(2)
|
localC.Expect(2)
|
||||||
@@ -67,7 +70,8 @@ func TestDSNOnTimeout(t *testing.T) {
|
|||||||
remoteC := testlib.NewTestCourier()
|
remoteC := testlib.NewTestCourier()
|
||||||
dir := testlib.MustTempDir(t)
|
dir := testlib.MustTempDir(t)
|
||||||
defer testlib.RemoveIfOk(t, dir)
|
defer testlib.RemoveIfOk(t, dir)
|
||||||
q, _ := New(dir, set.NewString("loco"), aliases.NewResolver(),
|
q, _ := New(dir, set.NewString("loco"),
|
||||||
|
aliases.NewResolver(allUsersExist),
|
||||||
localC, remoteC)
|
localC, remoteC)
|
||||||
|
|
||||||
// Insert an expired item in the queue.
|
// Insert an expired item in the queue.
|
||||||
@@ -111,7 +115,8 @@ func TestAliases(t *testing.T) {
|
|||||||
remoteC := testlib.NewTestCourier()
|
remoteC := testlib.NewTestCourier()
|
||||||
dir := testlib.MustTempDir(t)
|
dir := testlib.MustTempDir(t)
|
||||||
defer testlib.RemoveIfOk(t, dir)
|
defer testlib.RemoveIfOk(t, dir)
|
||||||
q, _ := New(dir, set.NewString("loco"), aliases.NewResolver(),
|
q, _ := New(dir, set.NewString("loco"),
|
||||||
|
aliases.NewResolver(allUsersExist),
|
||||||
localC, remoteC)
|
localC, remoteC)
|
||||||
|
|
||||||
q.aliases.AddDomain("loco")
|
q.aliases.AddDomain("loco")
|
||||||
@@ -155,7 +160,8 @@ func TestAliases(t *testing.T) {
|
|||||||
func TestFullQueue(t *testing.T) {
|
func TestFullQueue(t *testing.T) {
|
||||||
dir := testlib.MustTempDir(t)
|
dir := testlib.MustTempDir(t)
|
||||||
defer testlib.RemoveIfOk(t, dir)
|
defer testlib.RemoveIfOk(t, dir)
|
||||||
q, _ := New(dir, set.NewString(), aliases.NewResolver(),
|
q, _ := New(dir, set.NewString(),
|
||||||
|
aliases.NewResolver(allUsersExist),
|
||||||
testlib.DumbCourier, testlib.DumbCourier)
|
testlib.DumbCourier, testlib.DumbCourier)
|
||||||
|
|
||||||
// Force-insert maxQueueSize items in the queue.
|
// Force-insert maxQueueSize items in the queue.
|
||||||
@@ -197,7 +203,8 @@ func TestFullQueue(t *testing.T) {
|
|||||||
func TestPipes(t *testing.T) {
|
func TestPipes(t *testing.T) {
|
||||||
dir := testlib.MustTempDir(t)
|
dir := testlib.MustTempDir(t)
|
||||||
defer testlib.RemoveIfOk(t, dir)
|
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)
|
testlib.DumbCourier, testlib.DumbCourier)
|
||||||
item := &Item{
|
item := &Item{
|
||||||
Message: Message{
|
Message: Message{
|
||||||
@@ -219,7 +226,8 @@ func TestBadPath(t *testing.T) {
|
|||||||
// A new queue will attempt to os.MkdirAll the path.
|
// A new queue will attempt to os.MkdirAll the path.
|
||||||
// We expect this path to fail.
|
// We expect this path to fail.
|
||||||
_, err := New("/proc/doesnotexist", set.NewString("loco"),
|
_, err := New("/proc/doesnotexist", set.NewString("loco"),
|
||||||
aliases.NewResolver(), testlib.DumbCourier, testlib.DumbCourier)
|
aliases.NewResolver(allUsersExist),
|
||||||
|
testlib.DumbCourier, testlib.DumbCourier)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Errorf("could create queue, expected permission denied")
|
t.Errorf("could create queue, expected permission denied")
|
||||||
}
|
}
|
||||||
@@ -270,7 +278,8 @@ func TestSerialization(t *testing.T) {
|
|||||||
// Create the queue; should load the
|
// Create the queue; should load the
|
||||||
remoteC := testlib.NewTestCourier()
|
remoteC := testlib.NewTestCourier()
|
||||||
remoteC.Expect(1)
|
remoteC.Expect(1)
|
||||||
q, _ := New(dir, set.NewString("loco"), aliases.NewResolver(),
|
q, _ := New(dir, set.NewString("loco"),
|
||||||
|
aliases.NewResolver(allUsersExist),
|
||||||
testlib.DumbCourier, remoteC)
|
testlib.DumbCourier, remoteC)
|
||||||
q.Load()
|
q.Load()
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,8 @@ type Server struct {
|
|||||||
|
|
||||||
// NewServer returns a new empty Server.
|
// NewServer returns a new empty Server.
|
||||||
func NewServer() *Server {
|
func NewServer() *Server {
|
||||||
|
authr := auth.NewAuthenticator()
|
||||||
|
aliasesR := aliases.NewResolver(authr.Exists)
|
||||||
return &Server{
|
return &Server{
|
||||||
addrs: map[SocketMode][]string{},
|
addrs: map[SocketMode][]string{},
|
||||||
listeners: map[SocketMode][]net.Listener{},
|
listeners: map[SocketMode][]net.Listener{},
|
||||||
@@ -83,8 +85,8 @@ func NewServer() *Server {
|
|||||||
connTimeout: 20 * time.Minute,
|
connTimeout: 20 * time.Minute,
|
||||||
commandTimeout: 1 * time.Minute,
|
commandTimeout: 1 * time.Minute,
|
||||||
localDomains: &set.String{},
|
localDomains: &set.String{},
|
||||||
authr: auth.NewAuthenticator(),
|
authr: authr,
|
||||||
aliasesR: aliases.NewResolver(),
|
aliasesR: aliasesR,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user