mirror of
https://blitiri.com.ar/repos/chasquid
synced 2025-12-18 14:47:03 +00:00
aliasesdb: Implement an aliases database resolver
aliases databases can be very useful, so this patch adds a package to parse and resolve aliases. It uses an existing, well known and widely used format for aliases, although it doesn't necessarily match 100% any existing implementation at the moment.
This commit is contained in:
304
internal/aliases/aliases.go
Normal file
304
internal/aliases/aliases.go
Normal file
@@ -0,0 +1,304 @@
|
|||||||
|
// Package aliases implements an email aliases resolver.
|
||||||
|
//
|
||||||
|
// The resolver can parse many files for different domains, and perform
|
||||||
|
// lookups to resolve the aliases.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// File format
|
||||||
|
//
|
||||||
|
// It generally follows the traditional aliases format used by sendmail and
|
||||||
|
// exim.
|
||||||
|
//
|
||||||
|
// The file can contain lines of the form:
|
||||||
|
//
|
||||||
|
// user: address, address
|
||||||
|
// user: | command
|
||||||
|
//
|
||||||
|
// Lines starting with "#" are ignored, as well as empty lines.
|
||||||
|
// User names cannot contain spaces, ":" or commas, for parsing reasons. This
|
||||||
|
// is a tradeoff between flexibility and keeping the file format easy to edit
|
||||||
|
// for people.
|
||||||
|
//
|
||||||
|
// 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).
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Recipients
|
||||||
|
//
|
||||||
|
// Recipients can be of different types:
|
||||||
|
// - Email: the usual user@domain we all know and love, this is the default.
|
||||||
|
// - Pipe: if the right side starts with "| ", the rest of the line specifies
|
||||||
|
// a command to pipe the email through.
|
||||||
|
// Command and arguments are space separated. No quoting, escaping, or
|
||||||
|
// replacements of any kind.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Lookups
|
||||||
|
//
|
||||||
|
// The resolver will perform lookups recursively, until it finds all the final
|
||||||
|
// recipients.
|
||||||
|
//
|
||||||
|
// There are recursion limits to avoid alias loops. If the limit is reached,
|
||||||
|
// theat entire resolution will fail.
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Suffix removal
|
||||||
|
//
|
||||||
|
// The resolver can also remove suffixes from emails, and drop characters
|
||||||
|
// completely. This can be used to turn "user+blah@domain" into "user@domain",
|
||||||
|
// and "us.er@domain" into "user@domain".
|
||||||
|
//
|
||||||
|
// Both are optional, and the characters configurable globally.
|
||||||
|
package aliases
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"blitiri.com.ar/go/chasquid/internal/envelope"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Recipient represents a single recipient, after resolving aliases.
|
||||||
|
// They don't have any special interface, the callers will do a type switch
|
||||||
|
// anyway.
|
||||||
|
type Recipient struct {
|
||||||
|
Addr string
|
||||||
|
Type RType
|
||||||
|
}
|
||||||
|
|
||||||
|
type RType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
EMAIL RType = iota
|
||||||
|
PIPE
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrRecursionLimitExceeded = fmt.Errorf("recursion limit exceeded")
|
||||||
|
|
||||||
|
// How many levels of recursions we allow during lookups.
|
||||||
|
// We don't expect much recursion, so keeping this low to catch errors
|
||||||
|
// quickly.
|
||||||
|
recursionLimit = 10
|
||||||
|
)
|
||||||
|
|
||||||
|
// Resolver represents the aliases resolver.
|
||||||
|
type Resolver struct {
|
||||||
|
// Suffix separator, to perform suffix removal.
|
||||||
|
SuffixSep string
|
||||||
|
|
||||||
|
// Characters to drop from the user part.
|
||||||
|
DropChars string
|
||||||
|
|
||||||
|
// Map of domain -> alias files for that domain.
|
||||||
|
// We keep track of them for reloading purposes.
|
||||||
|
files map[string][]string
|
||||||
|
domains map[string]bool
|
||||||
|
|
||||||
|
// Map of address -> aliases.
|
||||||
|
aliases map[string][]Recipient
|
||||||
|
|
||||||
|
// Mutex protecting the structure.
|
||||||
|
mu sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewResolver() *Resolver {
|
||||||
|
return &Resolver{
|
||||||
|
files: map[string][]string{},
|
||||||
|
domains: map[string]bool{},
|
||||||
|
aliases: map[string][]Recipient{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Resolver) Resolve(addr string) ([]Recipient, error) {
|
||||||
|
v.mu.Lock()
|
||||||
|
defer v.mu.Unlock()
|
||||||
|
|
||||||
|
return v.resolve(0, addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Resolver) resolve(rcount int, addr string) ([]Recipient, error) {
|
||||||
|
if rcount >= recursionLimit {
|
||||||
|
return nil, ErrRecursionLimitExceeded
|
||||||
|
}
|
||||||
|
|
||||||
|
// Drop suffixes and chars to get the "clean" address before resolving.
|
||||||
|
// This also means that we will return the clean version if there's no
|
||||||
|
// match, which our callers can rely upon.
|
||||||
|
addr = v.cleanIfLocal(addr)
|
||||||
|
|
||||||
|
rcpts := v.aliases[addr]
|
||||||
|
if len(rcpts) == 0 {
|
||||||
|
return []Recipient{Recipient{addr, EMAIL}}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ret := []Recipient{}
|
||||||
|
for _, r := range rcpts {
|
||||||
|
// Only recurse for email recipients.
|
||||||
|
if r.Type != EMAIL {
|
||||||
|
ret = append(ret, r)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ar, err := v.resolve(rcount+1, r.Addr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
ret = append(ret, ar...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ret, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Resolver) cleanIfLocal(addr string) string {
|
||||||
|
user, domain := envelope.Split(addr)
|
||||||
|
|
||||||
|
if !v.domains[domain] {
|
||||||
|
return addr
|
||||||
|
}
|
||||||
|
|
||||||
|
user = removeAllAfter(user, v.SuffixSep)
|
||||||
|
user = removeChars(user, v.DropChars)
|
||||||
|
return user + "@" + domain
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Resolver) AddDomain(domain string) {
|
||||||
|
v.mu.Lock()
|
||||||
|
v.domains[domain] = true
|
||||||
|
v.mu.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Resolver) AddAliasesFile(domain, path string) error {
|
||||||
|
aliases, err := parseFile(domain, path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
v.mu.Lock()
|
||||||
|
v.files[domain] = append(v.files[domain], path)
|
||||||
|
v.domains[domain] = true
|
||||||
|
|
||||||
|
// Add the aliases to the resolver, overriding any previous values.
|
||||||
|
for addr, rs := range aliases {
|
||||||
|
v.aliases[addr] = rs
|
||||||
|
}
|
||||||
|
v.mu.Unlock()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *Resolver) Reload() error {
|
||||||
|
newAliases := map[string][]Recipient{}
|
||||||
|
|
||||||
|
for domain, paths := range v.files {
|
||||||
|
for _, path := range paths {
|
||||||
|
aliases, err := parseFile(domain, path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error parsing %q: %v", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add the aliases to the resolver, overriding any previous values.
|
||||||
|
for addr, rs := range aliases {
|
||||||
|
newAliases[addr] = rs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
v.mu.Lock()
|
||||||
|
v.aliases = newAliases
|
||||||
|
v.mu.Unlock()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseFile(domain, path string) (map[string][]Recipient, error) {
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
aliases := map[string][]Recipient{}
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(f)
|
||||||
|
for i := 1; scanner.Scan(); i++ {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
sp := strings.SplitN(line, ":", 2)
|
||||||
|
if len(sp) != 2 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
addr, rawalias := strings.TrimSpace(sp[0]), strings.TrimSpace(sp[1])
|
||||||
|
if len(addr) == 0 || len(rawalias) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.Contains(addr, "@") {
|
||||||
|
// It's invalid for lhs addresses to contain @ (for now).
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
addr = addr + "@" + domain
|
||||||
|
|
||||||
|
if rawalias[0] == '|' {
|
||||||
|
cmd := strings.TrimSpace(rawalias[1:])
|
||||||
|
aliases[addr] = []Recipient{Recipient{cmd, PIPE}}
|
||||||
|
} else {
|
||||||
|
rs := []Recipient{}
|
||||||
|
for _, a := range strings.Split(rawalias, ",") {
|
||||||
|
a = strings.TrimSpace(a)
|
||||||
|
if a == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// Addresses with no domain get the current one added, so it's
|
||||||
|
// easier to share alias files.
|
||||||
|
if !strings.Contains(a, "@") {
|
||||||
|
a = a + "@" + domain
|
||||||
|
}
|
||||||
|
rs = append(rs, Recipient{a, EMAIL})
|
||||||
|
}
|
||||||
|
aliases[addr] = rs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("reading %q: %v", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return aliases, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeAllAfter removes everything from s that comes after the separators,
|
||||||
|
// including them.
|
||||||
|
func removeAllAfter(s, seps string) string {
|
||||||
|
for _, c := range strings.Split(seps, "") {
|
||||||
|
if c == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
i := strings.Index(s, c)
|
||||||
|
if i == -1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
s = s[:i]
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// removeChars removes the runes in "chars" from s.
|
||||||
|
func removeChars(s, chars string) string {
|
||||||
|
for _, c := range strings.Split(chars, "") {
|
||||||
|
s = strings.Replace(s, c, "", -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
247
internal/aliases/aliases_test.go
Normal file
247
internal/aliases/aliases_test.go
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
package aliases
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Cases []struct {
|
||||||
|
addr string
|
||||||
|
expect []Recipient
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cases Cases) check(t *testing.T, r *Resolver) {
|
||||||
|
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 !reflect.DeepEqual(got, c.expect) {
|
||||||
|
t.Errorf("case %q, got %+v, expected %+v", c.addr, got, c.expect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBasic(t *testing.T) {
|
||||||
|
resolver := NewResolver()
|
||||||
|
resolver.aliases = map[string][]Recipient{
|
||||||
|
"a@b": {{"c@d", EMAIL}, {"e@f", EMAIL}},
|
||||||
|
"e@f": {{"cmd", PIPE}},
|
||||||
|
"cmd": {{"x@y", EMAIL}}, // it's a trap!
|
||||||
|
}
|
||||||
|
|
||||||
|
cases := Cases{
|
||||||
|
{"a@b", []Recipient{{"c@d", EMAIL}, {"cmd", PIPE}}},
|
||||||
|
{"e@f", []Recipient{{"cmd", PIPE}}},
|
||||||
|
{"x@y", []Recipient{{"x@y", EMAIL}}},
|
||||||
|
}
|
||||||
|
cases.check(t, resolver)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddrRewrite(t *testing.T) {
|
||||||
|
resolver := NewResolver()
|
||||||
|
resolver.AddDomain("def")
|
||||||
|
resolver.AddDomain("p-q.com")
|
||||||
|
resolver.aliases = map[string][]Recipient{
|
||||||
|
"abc@def": {{"x@y", EMAIL}},
|
||||||
|
"ñoño@def": {{"x@y", EMAIL}},
|
||||||
|
"recu@def": {{"ab+cd@p-q.com", EMAIL}},
|
||||||
|
}
|
||||||
|
resolver.DropChars = ".~"
|
||||||
|
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}}},
|
||||||
|
|
||||||
|
// Don't mess with the domain, even if it's known.
|
||||||
|
{"a.bc-ñaca@p-q.com", []Recipient{{"abc@p-q.com", EMAIL}}},
|
||||||
|
|
||||||
|
// Clean the right hand side too (if it's a local domain).
|
||||||
|
{"recu+blah@def", []Recipient{{"ab@p-q.com", EMAIL}}},
|
||||||
|
|
||||||
|
// 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}}},
|
||||||
|
}
|
||||||
|
cases.check(t, resolver)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTooMuchRecursion(t *testing.T) {
|
||||||
|
resolver := Resolver{}
|
||||||
|
resolver.aliases = map[string][]Recipient{
|
||||||
|
"a@b": {{"c@d", EMAIL}},
|
||||||
|
"c@d": {{"a@b", EMAIL}},
|
||||||
|
}
|
||||||
|
|
||||||
|
rs, err := resolver.Resolve("a@b")
|
||||||
|
if err != ErrRecursionLimitExceeded {
|
||||||
|
t.Errorf("expected ErrRecursionLimitExceeded, got %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if rs != nil {
|
||||||
|
t.Errorf("expected nil recipients, got %+v", rs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func mustWriteFile(t *testing.T, content string) string {
|
||||||
|
f, err := ioutil.TempFile("", "aliases_test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to get temp file: %v", err)
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
_, err = f.WriteString(content)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to write temp file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return f.Name()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAddFile(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
contents string
|
||||||
|
expected []Recipient
|
||||||
|
}{
|
||||||
|
{"\n", []Recipient{{"a@dom", EMAIL}}},
|
||||||
|
{" # Comment\n", []Recipient{{"a@dom", EMAIL}}},
|
||||||
|
{":\n", []Recipient{{"a@dom", EMAIL}}},
|
||||||
|
{"a: \n", []Recipient{{"a@dom", EMAIL}}},
|
||||||
|
{"a@dom: b@c \n", []Recipient{{"a@dom", EMAIL}}},
|
||||||
|
|
||||||
|
{"a: b\n", []Recipient{{"b@dom", EMAIL}}},
|
||||||
|
{"a:b\n", []Recipient{{"b@dom", EMAIL}}},
|
||||||
|
{"a : b \n", []Recipient{{"b@dom", EMAIL}}},
|
||||||
|
{"a : b, \n", []Recipient{{"b@dom", EMAIL}}},
|
||||||
|
|
||||||
|
{"a: |cmd\n", []Recipient{{"cmd", PIPE}}},
|
||||||
|
{"a:|cmd\n", []Recipient{{"cmd", PIPE}}},
|
||||||
|
{"a:| cmd \n", []Recipient{{"cmd", PIPE}}},
|
||||||
|
{"a :| cmd \n", []Recipient{{"cmd", PIPE}}},
|
||||||
|
{"a: | cmd arg1 arg2\n", []Recipient{{"cmd arg1 arg2", PIPE}}},
|
||||||
|
|
||||||
|
{"a: c@d, e@f, g\n",
|
||||||
|
[]Recipient{{"c@d", EMAIL}, {"e@f", EMAIL}, {"g@dom", EMAIL}}},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range cases {
|
||||||
|
fname := mustWriteFile(t, c.contents)
|
||||||
|
defer os.Remove(fname)
|
||||||
|
|
||||||
|
resolver := NewResolver()
|
||||||
|
err := resolver.AddAliasesFile("dom", fname)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error adding file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
got, err := resolver.Resolve("a@dom")
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("case %q, got error: %v", c.contents, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(got, c.expected) {
|
||||||
|
t.Errorf("case %q, got %v, expected %v", c.contents, got, c.expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const richFileContents = `
|
||||||
|
# This is a "complex" alias file, with a few tricky situations.
|
||||||
|
# It is used in TestRichFile.
|
||||||
|
|
||||||
|
# First some valid cases.
|
||||||
|
a: b
|
||||||
|
c: d@e, f,
|
||||||
|
x: | command
|
||||||
|
|
||||||
|
# The following is invalid, should be ignored.
|
||||||
|
a@dom: x@dom
|
||||||
|
|
||||||
|
# Overrides.
|
||||||
|
o1: a
|
||||||
|
o1: b
|
||||||
|
|
||||||
|
# Finally one to make the file NOT end in \n:
|
||||||
|
y: z`
|
||||||
|
|
||||||
|
func TestRichFile(t *testing.T) {
|
||||||
|
fname := mustWriteFile(t, richFileContents)
|
||||||
|
defer os.Remove(fname)
|
||||||
|
|
||||||
|
resolver := NewResolver()
|
||||||
|
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}}},
|
||||||
|
{"y@dom", []Recipient{{"z@dom", EMAIL}}},
|
||||||
|
}
|
||||||
|
cases.check(t, resolver)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestManyFiles(t *testing.T) {
|
||||||
|
files := map[string]string{
|
||||||
|
"d1": mustWriteFile(t, "a: b\nc:d@e"),
|
||||||
|
"domain2": mustWriteFile(t, "a: b\nc:d@e"),
|
||||||
|
"dom3": mustWriteFile(t, "x: y, z"),
|
||||||
|
"dom4": mustWriteFile(t, "a: |cmd"),
|
||||||
|
|
||||||
|
// Cross-domain.
|
||||||
|
"xd1": mustWriteFile(t, "a: b@xd2"),
|
||||||
|
"xd2": mustWriteFile(t, "b: |cmd"),
|
||||||
|
}
|
||||||
|
for _, fname := range files {
|
||||||
|
defer os.Remove(fname)
|
||||||
|
}
|
||||||
|
|
||||||
|
resolver := NewResolver()
|
||||||
|
for domain, fname := range files {
|
||||||
|
err := resolver.AddAliasesFile(domain, fname)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to add file: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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}}},
|
||||||
|
}
|
||||||
|
cases.check(t, resolver)
|
||||||
|
}
|
||||||
|
|
||||||
|
check()
|
||||||
|
|
||||||
|
// Reload, and check again just in case.
|
||||||
|
if err := resolver.Reload(); err != nil {
|
||||||
|
t.Fatalf("failed to reload: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
check()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user