mirror of
https://blitiri.com.ar/repos/chasquid
synced 2025-12-18 14:47:03 +00:00
aliases: Implement "via" aliases
This patch implements "via" aliases, which let us explicitly select a server to use for delivery. This feature is useful in different scenarios, such as a secondary MX server that forwards all incoming email to a primary. For now, it is experimental and the syntax and semantics are subject to change.
This commit is contained in:
@@ -86,6 +86,38 @@ pepe: jose
|
|||||||
|
|
||||||
This is experimental as of chasquid 1.16.0, and subject to change.
|
This is experimental as of chasquid 1.16.0, and subject to change.
|
||||||
|
|
||||||
|
### "Via" aliases (experimental)
|
||||||
|
|
||||||
|
!!! warning
|
||||||
|
|
||||||
|
This feature is experimental as of chasquid 1.16.0, and subject to change.
|
||||||
|
|
||||||
|
A "via" alias is like an email alias, but it explicitly specifies which
|
||||||
|
server(s) to use when delivering that email. The servers are used to attempt
|
||||||
|
delivery in the given order.
|
||||||
|
|
||||||
|
This can be useful in scenarios such as secondary MX servers that forward all
|
||||||
|
email to the primary server, or send-only servers.
|
||||||
|
|
||||||
|
The syntax is `user: address via server1[/server2/...]`.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
```
|
||||||
|
# Emails sent to pepe@domain will be forwarded to jose@domain using
|
||||||
|
# mail.example.com (instead of the MX records of the domain).
|
||||||
|
pepe: jose via mail1.example.com
|
||||||
|
|
||||||
|
# Same as above, but with multiple servers. They will be tried in order.
|
||||||
|
flowers: lilly@pond via mail1.pond/mail2.pond
|
||||||
|
|
||||||
|
# Forward all email (that does not match other users or aliases) using
|
||||||
|
# mail1.example.com.
|
||||||
|
# This is a typical setup for a secondary MX server that forwards email to
|
||||||
|
# the primary.
|
||||||
|
*: * via mail1.example.com
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
### Overrides
|
### Overrides
|
||||||
|
|
||||||
|
|||||||
@@ -83,6 +83,7 @@ var (
|
|||||||
// anyway.
|
// anyway.
|
||||||
type Recipient struct {
|
type Recipient struct {
|
||||||
Addr string
|
Addr string
|
||||||
|
Via []string // Used when Type == FORWARD.
|
||||||
Type RType
|
Type RType
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,6 +94,7 @@ type RType string
|
|||||||
const (
|
const (
|
||||||
EMAIL RType = "(email)"
|
EMAIL RType = "(email)"
|
||||||
PIPE RType = "(pipe)"
|
PIPE RType = "(pipe)"
|
||||||
|
FORWARD RType = "(forward)"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -215,7 +217,7 @@ func (v *Resolver) resolve(rcount int, addr string, tr *trace.Trace) ([]Recipien
|
|||||||
user, domain := envelope.Split(addr)
|
user, domain := envelope.Split(addr)
|
||||||
if _, ok := v.domains[domain]; !ok {
|
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, nil, EMAIL}}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// First, see if there's an exact match in the database.
|
// First, see if there's an exact match in the database.
|
||||||
@@ -250,7 +252,7 @@ func (v *Resolver) resolve(rcount int, addr string, tr *trace.Trace) ([]Recipien
|
|||||||
}
|
}
|
||||||
if ok {
|
if ok {
|
||||||
tr.Debugf("%d| user exists, returning %q", rcount, addr)
|
tr.Debugf("%d| user exists, returning %q", rcount, addr)
|
||||||
return []Recipient{{addr, EMAIL}}, nil
|
return []Recipient{{addr, nil, EMAIL}}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
catchAll, err := v.lookup("*@"+domain, tr)
|
catchAll, err := v.lookup("*@"+domain, tr)
|
||||||
@@ -274,14 +276,14 @@ func (v *Resolver) resolve(rcount int, addr string, tr *trace.Trace) ([]Recipien
|
|||||||
// clearer failure, reusing the existing codepaths and simplifying
|
// clearer failure, reusing the existing codepaths and simplifying
|
||||||
// the logic.
|
// the logic.
|
||||||
tr.Debugf("%d| no catch-all, returning %q", rcount, addr)
|
tr.Debugf("%d| no catch-all, returning %q", rcount, addr)
|
||||||
return []Recipient{{addr, EMAIL}}, nil
|
return []Recipient{{addr, nil, EMAIL}}, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ret := []Recipient{}
|
ret := []Recipient{}
|
||||||
for _, r := range rcpts {
|
for _, r := range rcpts {
|
||||||
// Only recurse for email recipients.
|
// PIPE recipients get added as-is. No modification, and no recursion.
|
||||||
if r.Type != EMAIL {
|
if r.Type == PIPE {
|
||||||
ret = append(ret, r)
|
ret = append(ret, r)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -296,6 +298,14 @@ func (v *Resolver) resolve(rcount int, addr string, tr *trace.Trace) ([]Recipien
|
|||||||
r.Addr = newAddr
|
r.Addr = newAddr
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Don't recurse FORWARD recipients, since we explicitly want them to
|
||||||
|
// be sent through the given servers.
|
||||||
|
// Note we do this here and not above so we support the right-side *.
|
||||||
|
if r.Type == FORWARD {
|
||||||
|
ret = append(ret, r)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
ar, err := v.resolve(rcount+1, r.Addr, tr)
|
ar, err := v.resolve(rcount+1, r.Addr, tr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tr.Debugf("%d| resolve(%q) returned error: %v", rcount, r.Addr, err)
|
tr.Debugf("%d| resolve(%q) returned error: %v", rcount, r.Addr, err)
|
||||||
@@ -384,8 +394,8 @@ func (v *Resolver) AddAliasesFile(domain, path string) (int, error) {
|
|||||||
|
|
||||||
// AddAliasForTesting adds an alias to the resolver, for testing purposes.
|
// AddAliasForTesting adds an alias to the resolver, for testing purposes.
|
||||||
// Not for use in production code.
|
// Not for use in production code.
|
||||||
func (v *Resolver) AddAliasForTesting(addr, rcpt string, rType RType) {
|
func (v *Resolver) AddAliasForTesting(addr, rcpt string, via []string, rType RType) {
|
||||||
v.aliases[addr] = append(v.aliases[addr], Recipient{rcpt, rType})
|
v.aliases[addr] = append(v.aliases[addr], Recipient{rcpt, via, rType})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reload aliases files for all known domains.
|
// Reload aliases files for all known domains.
|
||||||
@@ -473,6 +483,7 @@ func (v *Resolver) parseReader(domain string, r io.Reader) (map[string][]Recipie
|
|||||||
}
|
}
|
||||||
|
|
||||||
func parseRHS(rawalias, domain string) ([]Recipient, error) {
|
func parseRHS(rawalias, domain string) ([]Recipient, error) {
|
||||||
|
var err error
|
||||||
if len(rawalias) == 0 {
|
if len(rawalias) == 0 {
|
||||||
// Explicitly allow empty rawalias strings at this point: the file
|
// Explicitly allow empty rawalias strings at this point: the file
|
||||||
// parsing will prevent this at the upper level, and when we parse the
|
// parsing will prevent this at the upper level, and when we parse the
|
||||||
@@ -485,7 +496,7 @@ func parseRHS(rawalias, domain string) ([]Recipient, error) {
|
|||||||
// A pipe alias without a command is invalid.
|
// A pipe alias without a command is invalid.
|
||||||
return nil, fmt.Errorf("the pipe alias is missing a command")
|
return nil, fmt.Errorf("the pipe alias is missing a command")
|
||||||
}
|
}
|
||||||
return []Recipient{{cmd, PIPE}}, nil
|
return []Recipient{{cmd, nil, PIPE}}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
rs := []Recipient{}
|
rs := []Recipient{}
|
||||||
@@ -497,20 +508,67 @@ func parseRHS(rawalias, domain string) ([]Recipient, error) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Addresses with no domain get the current one added, so it's
|
r := Recipient{
|
||||||
// easier to share alias files.
|
Addr: a,
|
||||||
if !strings.Contains(a, "@") {
|
Via: nil,
|
||||||
a = a + "@" + domain
|
Type: EMAIL,
|
||||||
}
|
}
|
||||||
a, err := normalize.Addr(a)
|
if strings.Contains(a, " via ") {
|
||||||
|
// It's a FORWARD, so extract the Via part and continue.
|
||||||
|
r.Type = FORWARD
|
||||||
|
r.Addr, r.Via, err = parseForward(a)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Addresses with no domain get the current one added, so it's
|
||||||
|
// easier to share alias files. Note we also do this for FORWARD
|
||||||
|
// aliases.
|
||||||
|
if !strings.Contains(r.Addr, "@") {
|
||||||
|
r.Addr = r.Addr + "@" + domain
|
||||||
|
}
|
||||||
|
|
||||||
|
r.Addr, err = normalize.Addr(r.Addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("normalizing address %q: %w", a, err)
|
return nil, fmt.Errorf("normalizing address %q: %w", a, err)
|
||||||
}
|
}
|
||||||
rs = append(rs, Recipient{a, EMAIL})
|
|
||||||
|
rs = append(rs, r)
|
||||||
}
|
}
|
||||||
return rs, nil
|
return rs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Parse a raw FORWARD alias, returning the address and the via part.
|
||||||
|
// Expected format is "address via server1/server2/server3".
|
||||||
|
func parseForward(rawalias string) (string, []string, error) {
|
||||||
|
// Split the alias into the address and the via part.
|
||||||
|
addr, viaS, found := strings.Cut(rawalias, " via ")
|
||||||
|
if !found {
|
||||||
|
return "", nil, fmt.Errorf("via separator not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
// No need to normalize the address, the caller will do it.
|
||||||
|
// We only trim it, and enforce that there is one.
|
||||||
|
addr = strings.TrimSpace(addr)
|
||||||
|
if addr == "" {
|
||||||
|
return "", nil, fmt.Errorf("forwarding alias is missing the address")
|
||||||
|
}
|
||||||
|
|
||||||
|
// The via part is a list of servers, separated by "/". Split it up.
|
||||||
|
// For now we don't validate the servers, but we may in the future.
|
||||||
|
via := []string{}
|
||||||
|
for _, v := range strings.Split(viaS, "/") {
|
||||||
|
server := strings.TrimSpace(v)
|
||||||
|
if server == "" {
|
||||||
|
return "", nil, fmt.Errorf("empty server in via list")
|
||||||
|
}
|
||||||
|
via = append(via, server)
|
||||||
|
}
|
||||||
|
|
||||||
|
return addr, via, nil
|
||||||
|
}
|
||||||
|
|
||||||
// removeAllAfter removes everything from s that comes after the separators,
|
// removeAllAfter removes everything from s that comes after the separators,
|
||||||
// including them.
|
// including them.
|
||||||
func removeAllAfter(s, seps string) string {
|
func removeAllAfter(s, seps string) string {
|
||||||
|
|||||||
@@ -86,11 +86,15 @@ func usersWithXErrorYDontExist(tr *trace.Trace, user, domain string) (bool, erro
|
|||||||
}
|
}
|
||||||
|
|
||||||
func email(addr string) Recipient {
|
func email(addr string) Recipient {
|
||||||
return Recipient{addr, EMAIL}
|
return Recipient{addr, nil, EMAIL}
|
||||||
}
|
}
|
||||||
|
|
||||||
func pipe(addr string) Recipient {
|
func pipe(addr string) Recipient {
|
||||||
return Recipient{addr, PIPE}
|
return Recipient{addr, nil, PIPE}
|
||||||
|
}
|
||||||
|
|
||||||
|
func forward(addr string, via []string) Recipient {
|
||||||
|
return Recipient{addr, via, FORWARD}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBasic(t *testing.T) {
|
func TestBasic(t *testing.T) {
|
||||||
@@ -101,17 +105,20 @@ func TestBasic(t *testing.T) {
|
|||||||
"a@localA": {email("c@d"), email("e@localB")},
|
"a@localA": {email("c@d"), email("e@localB")},
|
||||||
"e@localB": {pipe("cmd")},
|
"e@localB": {pipe("cmd")},
|
||||||
"cmd@localA": {email("x@y")},
|
"cmd@localA": {email("x@y")},
|
||||||
|
"x@localA": {forward("z@localA", []string{"serverX"})},
|
||||||
}
|
}
|
||||||
|
|
||||||
cases := Cases{
|
cases := Cases{
|
||||||
{"a@localA", []Recipient{email("c@d"), pipe("cmd")}, nil},
|
{"a@localA", []Recipient{email("c@d"), pipe("cmd")}, nil},
|
||||||
{"e@localB", []Recipient{pipe("cmd")}, nil},
|
{"e@localB", []Recipient{pipe("cmd")}, nil},
|
||||||
{"x@y", []Recipient{email("x@y")}, nil},
|
{"x@y", []Recipient{email("x@y")}, nil},
|
||||||
|
{"x@localA", []Recipient{
|
||||||
|
forward("z@localA", []string{"serverX"})}, nil},
|
||||||
}
|
}
|
||||||
cases.check(t, resolver)
|
cases.check(t, resolver)
|
||||||
|
|
||||||
mustExist(t, resolver, "a@localA", "e@localB", "cmd@localA")
|
mustExist(t, resolver, "a@localA", "e@localB", "cmd@localA", "x@localA")
|
||||||
mustNotExist(t, resolver, "x@y")
|
mustNotExist(t, resolver, "x@y", "z@localA")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCatchAll(t *testing.T) {
|
func TestCatchAll(t *testing.T) {
|
||||||
@@ -157,6 +164,7 @@ func TestRightSideAsterisk(t *testing.T) {
|
|||||||
resolver.AddDomain("dom3")
|
resolver.AddDomain("dom3")
|
||||||
resolver.AddDomain("dom4")
|
resolver.AddDomain("dom4")
|
||||||
resolver.AddDomain("dom5")
|
resolver.AddDomain("dom5")
|
||||||
|
resolver.AddDomain("dom6")
|
||||||
resolver.aliases = map[string][]Recipient{
|
resolver.aliases = map[string][]Recipient{
|
||||||
"a@dom1": {email("aaa@remote")},
|
"a@dom1": {email("aaa@remote")},
|
||||||
|
|
||||||
@@ -183,6 +191,10 @@ func TestRightSideAsterisk(t *testing.T) {
|
|||||||
// This checks which one is used as the "original" user.
|
// This checks which one is used as the "original" user.
|
||||||
"a@dom5": {email("b@dom5")},
|
"a@dom5": {email("b@dom5")},
|
||||||
"*@dom5": {email("*@remote")},
|
"*@dom5": {email("*@remote")},
|
||||||
|
|
||||||
|
// A forward on the right side.
|
||||||
|
// It forwards to a local domain also check that there's no recursion.
|
||||||
|
"*@dom6": {forward("*@dom1", []string{"server"})},
|
||||||
}
|
}
|
||||||
|
|
||||||
cases := Cases{
|
cases := Cases{
|
||||||
@@ -218,6 +230,10 @@ func TestRightSideAsterisk(t *testing.T) {
|
|||||||
{"a@dom5", []Recipient{email("b@remote")}, nil},
|
{"a@dom5", []Recipient{email("b@remote")}, nil},
|
||||||
{"b@dom5", []Recipient{email("b@remote")}, nil},
|
{"b@dom5", []Recipient{email("b@remote")}, nil},
|
||||||
{"c@dom5", []Recipient{email("c@remote")}, nil},
|
{"c@dom5", []Recipient{email("c@remote")}, nil},
|
||||||
|
|
||||||
|
// Forwarding case.
|
||||||
|
{"a@dom6", []Recipient{forward("a@dom1", []string{"server"})}, nil},
|
||||||
|
{"b@dom6", []Recipient{forward("b@dom1", []string{"server"})}, nil},
|
||||||
}
|
}
|
||||||
cases.check(t, resolver)
|
cases.check(t, resolver)
|
||||||
}
|
}
|
||||||
@@ -527,6 +543,11 @@ func TestAddFile(t *testing.T) {
|
|||||||
|
|
||||||
{"a: c@d, e@f, g\n",
|
{"a: c@d, e@f, g\n",
|
||||||
[]Recipient{email("c@d"), email("e@f"), email("g@dom")}},
|
[]Recipient{email("c@d"), email("e@f"), email("g@dom")}},
|
||||||
|
|
||||||
|
{"a: b@c, b via sA/sB, d\n", []Recipient{
|
||||||
|
email("b@c"),
|
||||||
|
forward("b@dom", []string{"sA", "sB"}),
|
||||||
|
email("d@dom")}},
|
||||||
}
|
}
|
||||||
|
|
||||||
tr := trace.New("test", "TestAddFile")
|
tr := trace.New("test", "TestAddFile")
|
||||||
@@ -564,6 +585,7 @@ func TestAddFile(t *testing.T) {
|
|||||||
{"a@dom: b@c \n", "left-side: cannot contain @"},
|
{"a@dom: b@c \n", "left-side: cannot contain @"},
|
||||||
{"a", "line 1: missing ':' in line"},
|
{"a", "line 1: missing ':' in line"},
|
||||||
{"a: x y z\n", "disallowed rune encountered"},
|
{"a: x y z\n", "disallowed rune encountered"},
|
||||||
|
{"a: f via sA//sB\n", "empty server in via list"},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, c := range errcases {
|
for _, c := range errcases {
|
||||||
@@ -607,6 +629,9 @@ ppp1: p.q+r
|
|||||||
ppp2: p.q
|
ppp2: p.q
|
||||||
ppp3: ppp2
|
ppp3: ppp2
|
||||||
|
|
||||||
|
# Test some forwarding cases.
|
||||||
|
f1: f2 via server1, f3 via server2/server3, c
|
||||||
|
|
||||||
# Finally one to make the file NOT end in \n:
|
# Finally one to make the file NOT end in \n:
|
||||||
y: z`
|
y: z`
|
||||||
|
|
||||||
@@ -622,8 +647,8 @@ func TestRichFile(t *testing.T) {
|
|||||||
t.Fatalf("failed to add file: %v", err)
|
t.Fatalf("failed to add file: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if n != 11 {
|
if n != 12 {
|
||||||
t.Fatalf("expected 11 aliases, got %d", n)
|
t.Fatalf("expected 12 aliases, got %d", n)
|
||||||
}
|
}
|
||||||
|
|
||||||
cases := Cases{
|
cases := Cases{
|
||||||
@@ -647,6 +672,12 @@ func TestRichFile(t *testing.T) {
|
|||||||
{"ppp2@dom", []Recipient{email("pb@dom")}, nil},
|
{"ppp2@dom", []Recipient{email("pb@dom")}, nil},
|
||||||
{"ppp3@dom", []Recipient{email("pb@dom")}, nil},
|
{"ppp3@dom", []Recipient{email("pb@dom")}, nil},
|
||||||
|
|
||||||
|
{"f1@dom", []Recipient{
|
||||||
|
forward("f2@dom", []string{"server1"}),
|
||||||
|
forward("f3@dom", []string{"server2", "server3"}),
|
||||||
|
email("d@e"), email("f@dom"),
|
||||||
|
}, nil},
|
||||||
|
|
||||||
{"y@dom", []Recipient{email("z@dom")}, nil},
|
{"y@dom", []Recipient{email("z@dom")}, nil},
|
||||||
}
|
}
|
||||||
cases.check(t, resolver)
|
cases.check(t, resolver)
|
||||||
@@ -779,6 +810,48 @@ func TestHook(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestParseForward(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
raw string
|
||||||
|
addr string
|
||||||
|
via []string
|
||||||
|
err string
|
||||||
|
}{
|
||||||
|
{"", "", nil, "via separator not found"},
|
||||||
|
{"via", "", nil, "via separator not found"},
|
||||||
|
{"via ", "", nil, "via separator not found"},
|
||||||
|
{" via ", "", nil, "forwarding alias is missing the address"},
|
||||||
|
{" via S1", "", nil, "forwarding alias is missing the address"},
|
||||||
|
{"a via ", "", nil, "empty server in via list"},
|
||||||
|
{"a via S1", "a", []string{"S1"}, ""},
|
||||||
|
{"a via S1/S2", "a", []string{"S1", "S2"}, ""},
|
||||||
|
{"a via S1 / S2 ", "a", []string{"S1", "S2"}, ""},
|
||||||
|
{"a via S1/S2/", "", nil, "empty server in via list"},
|
||||||
|
{"a via S1/S2/ ", "", nil, "empty server in via list"},
|
||||||
|
{"a via S1//S2", "", nil, "empty server in via list"},
|
||||||
|
}
|
||||||
|
for _, c := range cases {
|
||||||
|
addr, via, err := parseForward(c.raw)
|
||||||
|
|
||||||
|
if err != nil && !strings.Contains(err.Error(), c.err) {
|
||||||
|
t.Errorf("case %q: got error %v, expected to contain %q",
|
||||||
|
c.raw, err, c.err)
|
||||||
|
continue
|
||||||
|
} else if err == nil && c.err != "" {
|
||||||
|
t.Errorf("case %q: got nil error, expected %q", c.raw, c.err)
|
||||||
|
t.Logf(" got addr: %q, via: %q", addr, via)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if addr != c.addr {
|
||||||
|
t.Errorf("case %q: got addr %q, expected %q", c.raw, addr, c.addr)
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(via, c.via) {
|
||||||
|
t.Errorf("case %q: got via %q, expected %q", c.raw, via, c.via)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Fuzz testing for the parser.
|
// Fuzz testing for the parser.
|
||||||
func FuzzReader(f *testing.F) {
|
func FuzzReader(f *testing.F) {
|
||||||
resolver := NewResolver(allUsersExist)
|
resolver := NewResolver(allUsersExist)
|
||||||
|
|||||||
@@ -8,4 +8,9 @@ type Courier interface {
|
|||||||
// Deliver mail to a recipient. Return the error (if any), and whether it
|
// Deliver mail to a recipient. Return the error (if any), and whether it
|
||||||
// is permanent (true) or transient (false).
|
// is permanent (true) or transient (false).
|
||||||
Deliver(from string, to string, data []byte) (error, bool)
|
Deliver(from string, to string, data []byte) (error, bool)
|
||||||
|
|
||||||
|
// Forward mail using the given servers.
|
||||||
|
// Return the error (if any), and whether it is permanent (true) or
|
||||||
|
// transient (false).
|
||||||
|
Forward(from string, to string, data []byte, servers []string) (error, bool)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package courier
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -108,3 +109,11 @@ func sanitizeForMDA(s string) string {
|
|||||||
}
|
}
|
||||||
return strings.Map(valid, s)
|
return strings.Map(valid, s)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var errForwardNotSupported = errors.New(
|
||||||
|
"forwarding not supported by the MDA courier")
|
||||||
|
|
||||||
|
// Forward is not supported by the MDA courier.
|
||||||
|
func (p *MDA) Forward(from string, to string, data []byte, servers []string) (error, bool) {
|
||||||
|
return errForwardNotSupported, true
|
||||||
|
}
|
||||||
|
|||||||
@@ -123,3 +123,15 @@ func TestSanitize(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestForward(t *testing.T) {
|
||||||
|
p := MDA{"thisdoesnotexist", nil, 1 * time.Minute}
|
||||||
|
err, permanent := p.Forward(
|
||||||
|
"from", "to", []byte("data"), []string{"server"})
|
||||||
|
if err != errForwardNotSupported {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if !permanent {
|
||||||
|
t.Errorf("expected permanent, got transient")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ func (s *SMTP) Deliver(from string, to string, data []byte) (error, bool) {
|
|||||||
to: to,
|
to: to,
|
||||||
toDomain: envelope.DomainOf(to),
|
toDomain: envelope.DomainOf(to),
|
||||||
data: data,
|
data: data,
|
||||||
tr: trace.New("Courier.SMTP", to),
|
tr: trace.New("Courier.SMTP.Deliver", to),
|
||||||
}
|
}
|
||||||
defer a.tr.Finish()
|
defer a.tr.Finish()
|
||||||
a.tr.Debugf("%s -> %s", from, to)
|
a.tr.Debugf("%s -> %s", from, to)
|
||||||
@@ -105,6 +105,42 @@ func (s *SMTP) Deliver(from string, to string, data []byte) (error, bool) {
|
|||||||
return a.tr.Errorf("all MXs returned transient failures (last: %v)", err), false
|
return a.tr.Errorf("all MXs returned transient failures (last: %v)", err), false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Forward an email. On failures, returns an error, and whether or not it is
|
||||||
|
// permanent.
|
||||||
|
func (s *SMTP) Forward(from string, to string, data []byte, servers []string) (error, bool) {
|
||||||
|
a := &attempt{
|
||||||
|
courier: s,
|
||||||
|
from: from,
|
||||||
|
to: to,
|
||||||
|
toDomain: envelope.DomainOf(to),
|
||||||
|
data: data,
|
||||||
|
tr: trace.New("Courier.SMTP.Forward", to),
|
||||||
|
}
|
||||||
|
defer a.tr.Finish()
|
||||||
|
a.tr.Debugf("%s -> %s", from, to)
|
||||||
|
|
||||||
|
// smtp.Client.Mail will add the <> for us when the address is empty.
|
||||||
|
if a.from == "<>" {
|
||||||
|
a.from = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
for _, server := range servers {
|
||||||
|
var permanent bool
|
||||||
|
err, permanent = a.deliver(server)
|
||||||
|
if err == nil {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
if permanent {
|
||||||
|
return err, true
|
||||||
|
}
|
||||||
|
a.tr.Errorf("%q returned transient error: %v", server, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We exhausted all servers, try again later.
|
||||||
|
return a.tr.Errorf("all servers returned transient failures (last: %v)", err), false
|
||||||
|
}
|
||||||
|
|
||||||
type attempt struct {
|
type attempt struct {
|
||||||
courier *SMTP
|
courier *SMTP
|
||||||
|
|
||||||
|
|||||||
@@ -191,6 +191,9 @@ func (q *Queue) Put(tr *trace.Trace, from string, to []string, data []byte) (str
|
|||||||
r.Type = Recipient_EMAIL
|
r.Type = Recipient_EMAIL
|
||||||
case aliases.PIPE:
|
case aliases.PIPE:
|
||||||
r.Type = Recipient_PIPE
|
r.Type = Recipient_PIPE
|
||||||
|
case aliases.FORWARD:
|
||||||
|
r.Type = Recipient_FORWARD
|
||||||
|
r.Via = aliasRcpt.Via
|
||||||
default:
|
default:
|
||||||
log.Errorf("unknown alias type %v when resolving %q",
|
log.Errorf("unknown alias type %v when resolving %q",
|
||||||
aliasRcpt.Type, t)
|
aliasRcpt.Type, t)
|
||||||
@@ -387,6 +390,21 @@ func (item *Item) deliver(q *Queue, rcpt *Recipient) (err error, permanent bool)
|
|||||||
return cmd.Run(), true
|
return cmd.Run(), true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Recipient type is FORWARD: we always use the remote courier, and pass
|
||||||
|
// the list of servers that was given to us.
|
||||||
|
if rcpt.Type == Recipient_FORWARD {
|
||||||
|
deliverAttempts.Add("forward", 1)
|
||||||
|
|
||||||
|
// When forwarding with an explicit list of servers, we use SRS if
|
||||||
|
// we're sending from a non-local domain (regardless of the
|
||||||
|
// destination).
|
||||||
|
from := item.From
|
||||||
|
if !envelope.DomainIn(item.From, q.localDomains) {
|
||||||
|
from = rewriteSender(item.From, rcpt.OriginalAddress)
|
||||||
|
}
|
||||||
|
return q.remoteC.Forward(from, rcpt.Address, item.Data, rcpt.Via)
|
||||||
|
}
|
||||||
|
|
||||||
// Recipient type is EMAIL.
|
// Recipient type is EMAIL.
|
||||||
if envelope.DomainIn(rcpt.Address, q.localDomains) {
|
if envelope.DomainIn(rcpt.Address, q.localDomains) {
|
||||||
deliverAttempts.Add("email:local", 1)
|
deliverAttempts.Add("email:local", 1)
|
||||||
@@ -396,22 +414,30 @@ func (item *Item) deliver(q *Queue, rcpt *Recipient) (err error, permanent bool)
|
|||||||
deliverAttempts.Add("email:remote", 1)
|
deliverAttempts.Add("email:remote", 1)
|
||||||
from := item.From
|
from := item.From
|
||||||
if !envelope.DomainIn(item.From, q.localDomains) {
|
if !envelope.DomainIn(item.From, q.localDomains) {
|
||||||
// We're sending from a non-local to a non-local. This should
|
// We're sending from a non-local to a non-local, need to do SRS.
|
||||||
// happen only when there's an alias to forward email to a
|
from = rewriteSender(item.From, rcpt.OriginalAddress)
|
||||||
// non-local domain. In this case, using the original From is
|
}
|
||||||
// problematic, as we may not be an authorized sender for this.
|
return q.remoteC.Deliver(from, rcpt.Address, item.Data)
|
||||||
// Some MTAs (like Exim) will do it anyway, others (like
|
}
|
||||||
// gmail) will construct a special address based on the
|
|
||||||
// original address. We go with the latter.
|
func rewriteSender(from, originalAddr string) string {
|
||||||
|
// Apply a send-only Sender Rewriting Scheme (SRS).
|
||||||
|
// This is used when we are sending from a (potentially) non-local domain,
|
||||||
|
// to a non-local domain.
|
||||||
|
// This should happen only when there's an alias to forward email to a
|
||||||
|
// non-local domain (either a normal "email" alias with a remote
|
||||||
|
// destination, or a "forward" alias with a list of servers).
|
||||||
|
// In this case, using the original From is problematic, as we may not be
|
||||||
|
// an authorized sender for this.
|
||||||
|
// To do this, we use a sender rewriting scheme, similar to what other
|
||||||
|
// MTAs do (e.g. gmail or postfix).
|
||||||
// Note this assumes "+" is an alias suffix separator.
|
// Note this assumes "+" is an alias suffix separator.
|
||||||
// We use the IDNA version of the domain if possible, because
|
// We use the IDNA version of the domain if possible, because
|
||||||
// we can't know if the other side will support SMTPUTF8.
|
// we can't know if the other side will support SMTPUTF8.
|
||||||
from = fmt.Sprintf("%s+fwd_from=%s@%s",
|
return fmt.Sprintf("%s+fwd_from=%s@%s",
|
||||||
envelope.UserOf(rcpt.OriginalAddress),
|
envelope.UserOf(originalAddr),
|
||||||
strings.Replace(from, "@", "=", -1),
|
strings.Replace(from, "@", "=", -1),
|
||||||
mustIDNAToASCII(envelope.DomainOf(rcpt.OriginalAddress)))
|
mustIDNAToASCII(envelope.DomainOf(originalAddr)))
|
||||||
}
|
|
||||||
return q.remoteC.Deliver(from, rcpt.Address, item.Data)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// countRcpt counts how many recipients are in the given status.
|
// countRcpt counts how many recipients are in the given status.
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ type Recipient_Type int32
|
|||||||
const (
|
const (
|
||||||
Recipient_EMAIL Recipient_Type = 0
|
Recipient_EMAIL Recipient_Type = 0
|
||||||
Recipient_PIPE Recipient_Type = 1
|
Recipient_PIPE Recipient_Type = 1
|
||||||
|
Recipient_FORWARD Recipient_Type = 2
|
||||||
)
|
)
|
||||||
|
|
||||||
// Enum value maps for Recipient_Type.
|
// Enum value maps for Recipient_Type.
|
||||||
@@ -32,10 +33,12 @@ var (
|
|||||||
Recipient_Type_name = map[int32]string{
|
Recipient_Type_name = map[int32]string{
|
||||||
0: "EMAIL",
|
0: "EMAIL",
|
||||||
1: "PIPE",
|
1: "PIPE",
|
||||||
|
2: "FORWARD",
|
||||||
}
|
}
|
||||||
Recipient_Type_value = map[string]int32{
|
Recipient_Type_value = map[string]int32{
|
||||||
"EMAIL": 0,
|
"EMAIL": 0,
|
||||||
"PIPE": 1,
|
"PIPE": 1,
|
||||||
|
"FORWARD": 2,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -221,6 +224,8 @@ type Recipient struct {
|
|||||||
// This is before expanding aliases and only used in very particular
|
// This is before expanding aliases and only used in very particular
|
||||||
// cases.
|
// cases.
|
||||||
OriginalAddress string `protobuf:"bytes,5,opt,name=original_address,json=originalAddress,proto3" json:"original_address,omitempty"`
|
OriginalAddress string `protobuf:"bytes,5,opt,name=original_address,json=originalAddress,proto3" json:"original_address,omitempty"`
|
||||||
|
// The list of servers to use, for recipients of type == FORWARD.
|
||||||
|
Via []string `protobuf:"bytes,6,rep,name=via,proto3" json:"via,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (x *Recipient) Reset() {
|
func (x *Recipient) Reset() {
|
||||||
@@ -290,6 +295,13 @@ func (x *Recipient) GetOriginalAddress() string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (x *Recipient) GetVia() []string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Via
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Timestamp representation, for convenience.
|
// Timestamp representation, for convenience.
|
||||||
// We used to use the well-known type, but the dependency makes packaging much
|
// We used to use the well-known type, but the dependency makes packaging much
|
||||||
// more convoluted and adds very little value, so we now just include it here.
|
// more convoluted and adds very little value, so we now just include it here.
|
||||||
@@ -365,7 +377,7 @@ var file_queue_proto_rawDesc = []byte{
|
|||||||
0x0a, 0x0d, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x74, 0x73, 0x18,
|
0x0a, 0x0d, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x74, 0x73, 0x18,
|
||||||
0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x71, 0x75, 0x65, 0x75, 0x65, 0x2e, 0x54, 0x69,
|
0x06, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x10, 0x2e, 0x71, 0x75, 0x65, 0x75, 0x65, 0x2e, 0x54, 0x69,
|
||||||
0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64,
|
0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x0b, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64,
|
||||||
0x41, 0x74, 0x54, 0x73, 0x22, 0xa8, 0x02, 0x0a, 0x09, 0x52, 0x65, 0x63, 0x69, 0x70, 0x69, 0x65,
|
0x41, 0x74, 0x54, 0x73, 0x22, 0xc7, 0x02, 0x0a, 0x09, 0x52, 0x65, 0x63, 0x69, 0x70, 0x69, 0x65,
|
||||||
0x6e, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20,
|
0x6e, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20,
|
||||||
0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x29, 0x0a, 0x04,
|
0x01, 0x28, 0x09, 0x52, 0x07, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x29, 0x0a, 0x04,
|
||||||
0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x71, 0x75, 0x65,
|
0x74, 0x79, 0x70, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x15, 0x2e, 0x71, 0x75, 0x65,
|
||||||
@@ -379,19 +391,20 @@ var file_queue_proto_rawDesc = []byte{
|
|||||||
0x75, 0x72, 0x65, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x6f, 0x72,
|
0x75, 0x72, 0x65, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x29, 0x0a, 0x10, 0x6f, 0x72,
|
||||||
0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x05,
|
0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x61, 0x64, 0x64, 0x72, 0x65, 0x73, 0x73, 0x18, 0x05,
|
||||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x41, 0x64,
|
0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x6f, 0x72, 0x69, 0x67, 0x69, 0x6e, 0x61, 0x6c, 0x41, 0x64,
|
||||||
0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0x1b, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x09, 0x0a,
|
0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x76, 0x69, 0x61, 0x18, 0x06, 0x20, 0x03,
|
||||||
0x05, 0x45, 0x4d, 0x41, 0x49, 0x4c, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x50, 0x49, 0x50, 0x45,
|
0x28, 0x09, 0x52, 0x03, 0x76, 0x69, 0x61, 0x22, 0x28, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12,
|
||||||
0x10, 0x01, 0x22, 0x2b, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0b, 0x0a, 0x07,
|
0x09, 0x0a, 0x05, 0x45, 0x4d, 0x41, 0x49, 0x4c, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x50, 0x49,
|
||||||
0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x53, 0x45, 0x4e,
|
0x50, 0x45, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x46, 0x4f, 0x52, 0x57, 0x41, 0x52, 0x44, 0x10,
|
||||||
0x54, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x02, 0x22,
|
0x02, 0x22, 0x2b, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0b, 0x0a, 0x07, 0x50,
|
||||||
0x3b, 0x0a, 0x09, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x18, 0x0a, 0x07,
|
0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x53, 0x45, 0x4e, 0x54,
|
||||||
0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x73,
|
0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x02, 0x22, 0x3b,
|
||||||
0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x61, 0x6e, 0x6f, 0x73, 0x18,
|
0x0a, 0x09, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x18, 0x0a, 0x07, 0x73,
|
||||||
0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x6e, 0x61, 0x6e, 0x6f, 0x73, 0x42, 0x2b, 0x5a, 0x29,
|
0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x73, 0x65,
|
||||||
0x62, 0x6c, 0x69, 0x74, 0x69, 0x72, 0x69, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x72, 0x2f, 0x67,
|
0x63, 0x6f, 0x6e, 0x64, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x61, 0x6e, 0x6f, 0x73, 0x18, 0x02,
|
||||||
0x6f, 0x2f, 0x63, 0x68, 0x61, 0x73, 0x71, 0x75, 0x69, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72,
|
0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x6e, 0x61, 0x6e, 0x6f, 0x73, 0x42, 0x2b, 0x5a, 0x29, 0x62,
|
||||||
0x6e, 0x61, 0x6c, 0x2f, 0x71, 0x75, 0x65, 0x75, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f,
|
0x6c, 0x69, 0x74, 0x69, 0x72, 0x69, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x72, 0x2f, 0x67, 0x6f,
|
||||||
0x33,
|
0x2f, 0x63, 0x68, 0x61, 0x73, 0x71, 0x75, 0x69, 0x64, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e,
|
||||||
|
0x61, 0x6c, 0x2f, 0x71, 0x75, 0x65, 0x75, 0x65, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ message Recipient {
|
|||||||
enum Type {
|
enum Type {
|
||||||
EMAIL = 0;
|
EMAIL = 0;
|
||||||
PIPE = 1;
|
PIPE = 1;
|
||||||
|
FORWARD = 2;
|
||||||
}
|
}
|
||||||
Type type = 2;
|
Type type = 2;
|
||||||
|
|
||||||
@@ -43,6 +44,9 @@ message Recipient {
|
|||||||
// This is before expanding aliases and only used in very particular
|
// This is before expanding aliases and only used in very particular
|
||||||
// cases.
|
// cases.
|
||||||
string original_address = 5;
|
string original_address = 5;
|
||||||
|
|
||||||
|
// The list of servers to use, for recipients of type == FORWARD.
|
||||||
|
repeated string via = 6;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Timestamp representation, for convenience.
|
// Timestamp representation, for convenience.
|
||||||
|
|||||||
@@ -127,28 +127,52 @@ func TestAliases(t *testing.T) {
|
|||||||
defer tr.Finish()
|
defer tr.Finish()
|
||||||
|
|
||||||
q.aliases.AddDomain("loco")
|
q.aliases.AddDomain("loco")
|
||||||
q.aliases.AddAliasForTesting("ab@loco", "pq@loco", aliases.EMAIL)
|
q.aliases.AddAliasForTesting("ab@loco", "pq@loco", nil, aliases.EMAIL)
|
||||||
q.aliases.AddAliasForTesting("ab@loco", "rs@loco", aliases.EMAIL)
|
q.aliases.AddAliasForTesting("ab@loco", "rs@loco", nil, aliases.EMAIL)
|
||||||
q.aliases.AddAliasForTesting("cd@loco", "ata@hualpa", aliases.EMAIL)
|
q.aliases.AddAliasForTesting("cd@loco", "ata@hualpa", nil, aliases.EMAIL)
|
||||||
|
q.aliases.AddAliasForTesting(
|
||||||
|
"fwd@loco", "fwd@loco", []string{"server"}, aliases.FORWARD)
|
||||||
|
q.aliases.AddAliasForTesting(
|
||||||
|
"remote@loco", "remote@rana", []string{"server"}, aliases.FORWARD)
|
||||||
// Note the pipe aliases are tested below, as they don't use the couriers
|
// Note the pipe aliases are tested below, as they don't use the couriers
|
||||||
// and it can be quite inconvenient to test them in this way.
|
// and it can be quite inconvenient to test them in this way.
|
||||||
|
|
||||||
localC.Expect(2)
|
localC.Expect(2)
|
||||||
remoteC.Expect(1)
|
remoteC.Expect(3)
|
||||||
_, err := q.Put(tr, "from", []string{"ab@loco", "cd@loco"}, []byte("data"))
|
|
||||||
|
// One email from a local domain: from@loco -> ab@loco, cd@loco, fwd@loco.
|
||||||
|
_, err := q.Put(tr, "from@loco",
|
||||||
|
[]string{"ab@loco", "cd@loco", "fwd@loco"},
|
||||||
|
[]byte("data"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("Put: %v", err)
|
t.Fatalf("Put: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// And another from a remote domain: from@rana -> remote@loco
|
||||||
|
_, err = q.Put(tr, "from@rana",
|
||||||
|
[]string{"remote@loco"},
|
||||||
|
[]byte("data"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Put: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
localC.Wait()
|
localC.Wait()
|
||||||
remoteC.Wait()
|
remoteC.Wait()
|
||||||
|
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
courier *testlib.TestCourier
|
courier *testlib.TestCourier
|
||||||
|
expectedFrom string
|
||||||
expectedTo string
|
expectedTo string
|
||||||
}{
|
}{
|
||||||
{localC, "pq@loco"},
|
// From the local domain: from@loco
|
||||||
{localC, "rs@loco"},
|
{localC, "from@loco", "pq@loco"},
|
||||||
{remoteC, "ata@hualpa"},
|
{localC, "from@loco", "rs@loco"},
|
||||||
|
{remoteC, "from@loco", "ata@hualpa"},
|
||||||
|
{remoteC, "from@loco", "fwd@loco"},
|
||||||
|
|
||||||
|
// From the remote domain: from@rana.
|
||||||
|
// Note the SRS in the remoteC.
|
||||||
|
{remoteC, "remote+fwd_from=from=rana@loco", "remote@rana"},
|
||||||
}
|
}
|
||||||
for _, c := range cases {
|
for _, c := range cases {
|
||||||
req := c.courier.ReqFor[c.expectedTo]
|
req := c.courier.ReqFor[c.expectedTo]
|
||||||
@@ -157,9 +181,9 @@ func TestAliases(t *testing.T) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if req.From != "from" || req.To != c.expectedTo ||
|
if req.From != c.expectedFrom || req.To != c.expectedTo ||
|
||||||
!bytes.Equal(req.Data, []byte("data")) {
|
!bytes.Equal(req.Data, []byte("data")) {
|
||||||
t.Errorf("wrong request for %q: %v", c.expectedTo, req)
|
t.Errorf("wrong request for %q: %v", c.expectedTo, *req)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -671,7 +671,7 @@ func realMain(m *testing.M) int {
|
|||||||
udb := userdb.New("/dev/null")
|
udb := userdb.New("/dev/null")
|
||||||
udb.AddUser("testuser", "testpasswd")
|
udb.AddUser("testuser", "testpasswd")
|
||||||
s.aliasesR.AddAliasForTesting(
|
s.aliasesR.AddAliasForTesting(
|
||||||
"to@localhost", "testuser@localhost", aliases.EMAIL)
|
"to@localhost", "testuser@localhost", nil, aliases.EMAIL)
|
||||||
s.authr.Register("localhost", auth.WrapNoErrorBackend(udb))
|
s.authr.Register("localhost", auth.WrapNoErrorBackend(udb))
|
||||||
s.AddDomain("localhost")
|
s.AddDomain("localhost")
|
||||||
|
|
||||||
|
|||||||
@@ -89,6 +89,7 @@ type deliverRequest struct {
|
|||||||
From string
|
From string
|
||||||
To string
|
To string
|
||||||
Data []byte
|
Data []byte
|
||||||
|
Via []string
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestCourier never fails, and always remembers everything.
|
// TestCourier never fails, and always remembers everything.
|
||||||
@@ -102,7 +103,17 @@ type TestCourier struct {
|
|||||||
// Deliver the given mail (saving it in tc.Requests).
|
// Deliver the given mail (saving it in tc.Requests).
|
||||||
func (tc *TestCourier) Deliver(from string, to string, data []byte) (error, bool) {
|
func (tc *TestCourier) Deliver(from string, to string, data []byte) (error, bool) {
|
||||||
defer tc.wg.Done()
|
defer tc.wg.Done()
|
||||||
dr := &deliverRequest{from, to, data}
|
dr := &deliverRequest{from, to, data, nil}
|
||||||
|
tc.Lock()
|
||||||
|
tc.Requests = append(tc.Requests, dr)
|
||||||
|
tc.ReqFor[to] = dr
|
||||||
|
tc.Unlock()
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (tc *TestCourier) Forward(from string, to string, data []byte, servers []string) (error, bool) {
|
||||||
|
defer tc.wg.Done()
|
||||||
|
dr := &deliverRequest{from, to, data, servers}
|
||||||
tc.Lock()
|
tc.Lock()
|
||||||
tc.Requests = append(tc.Requests, dr)
|
tc.Requests = append(tc.Requests, dr)
|
||||||
tc.ReqFor[to] = dr
|
tc.ReqFor[to] = dr
|
||||||
@@ -133,6 +144,10 @@ func (c dumbCourier) Deliver(from string, to string, data []byte) (error, bool)
|
|||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c dumbCourier) Forward(from string, to string, data []byte, servers []string) (error, bool) {
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
// DumbCourier always succeeds delivery, and ignores everything.
|
// DumbCourier always succeeds delivery, and ignores everything.
|
||||||
var DumbCourier = dumbCourier{}
|
var DumbCourier = dumbCourier{}
|
||||||
|
|
||||||
|
|||||||
3
test/t-22-forward_via/.gitignore
vendored
Normal file
3
test/t-22-forward_via/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
primary/**/users
|
||||||
|
secondary/**/users
|
||||||
|
external/**/users
|
||||||
7
test/t-22-forward_via/content
Normal file
7
test/t-22-forward_via/content
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
Subject: Los espejos
|
||||||
|
|
||||||
|
Yo que sentí el horror de los espejos
|
||||||
|
no sólo ante el cristal impenetrable
|
||||||
|
donde acaba y empieza, inhabitable,
|
||||||
|
un imposible espacio de reflejos
|
||||||
|
|
||||||
54
test/t-22-forward_via/expected-chain-1
Normal file
54
test/t-22-forward_via/expected-chain-1
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
Authentication-Results: primary
|
||||||
|
;spf=pass (matched mx)
|
||||||
|
;dkim=pass header.b=???????????? header.d=dodo
|
||||||
|
Received-SPF: pass (matched mx)
|
||||||
|
Received: from *
|
||||||
|
by primary (chasquid) with ESMTPS
|
||||||
|
tls TLS_*
|
||||||
|
(over SMTP, TLS-1.3, envelope from "chain-1-4+fwd_from=chain-1-3+fwd_from=user222=dodo=kiwi@dodo")
|
||||||
|
; *
|
||||||
|
Authentication-Results: secondary
|
||||||
|
;spf=pass (matched mx)
|
||||||
|
;dkim=pass header.b=???????????? header.d=dodo
|
||||||
|
Received-SPF: pass (matched mx)
|
||||||
|
Received: from *
|
||||||
|
by secondary (chasquid) with ESMTPS
|
||||||
|
tls TLS_*
|
||||||
|
(over SMTP, TLS-1.3, envelope from "chain-1-3+fwd_from=user222=dodo@kiwi")
|
||||||
|
; *
|
||||||
|
Authentication-Results: external
|
||||||
|
;spf=pass (matched mx)
|
||||||
|
;dkim=pass header.b=???????????? header.d=dodo
|
||||||
|
Received-SPF: pass (matched mx)
|
||||||
|
Received: from *
|
||||||
|
by external (chasquid) with ESMTPS
|
||||||
|
tls TLS_*
|
||||||
|
(over SMTP, TLS-1.3, envelope from "user222@dodo")
|
||||||
|
; *
|
||||||
|
Authentication-Results: primary
|
||||||
|
;spf=pass (matched mx)
|
||||||
|
;dkim=pass header.b=???????????? header.d=dodo
|
||||||
|
Received-SPF: pass (matched mx)
|
||||||
|
Received: from *
|
||||||
|
by primary (chasquid) with ESMTPS
|
||||||
|
tls TLS_*
|
||||||
|
(over SMTP, TLS-1.3, envelope from "user222@dodo")
|
||||||
|
; *
|
||||||
|
Received: from localhost
|
||||||
|
by secondary (chasquid) with ESMTPSA
|
||||||
|
tls TLS_*
|
||||||
|
(over submission+TLS, TLS-1.3, envelope from "user222@dodo")
|
||||||
|
; *
|
||||||
|
DKIM-Signature: v=1; a=ed25519-sha256; c=relaxed/relaxed;
|
||||||
|
d=dodo; s=sel-secondary-1; t=*
|
||||||
|
h=subject:from:subject:date:to:cc:message-id;
|
||||||
|
bh=*
|
||||||
|
b=*
|
||||||
|
*
|
||||||
|
Subject: Los espejos
|
||||||
|
|
||||||
|
Yo que sentí el horror de los espejos
|
||||||
|
no sólo ante el cristal impenetrable
|
||||||
|
donde acaba y empieza, inhabitable,
|
||||||
|
un imposible espacio de reflejos
|
||||||
|
|
||||||
27
test/t-22-forward_via/expected-external-user333@kiwi
Normal file
27
test/t-22-forward_via/expected-external-user333@kiwi
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
Authentication-Results: external
|
||||||
|
;spf=pass (matched mx)
|
||||||
|
;dkim=pass header.b=???????????? header.d=dodo
|
||||||
|
Received-SPF: pass (matched mx)
|
||||||
|
Received: from *
|
||||||
|
by external (chasquid) with ESMTPS
|
||||||
|
tls TLS_*
|
||||||
|
(over SMTP, TLS-1.3, envelope from "user222@dodo")
|
||||||
|
; *
|
||||||
|
Received: from localhost
|
||||||
|
by secondary (chasquid) with ESMTPSA
|
||||||
|
tls TLS_*
|
||||||
|
(over submission+TLS, TLS-1.3, envelope from "user222@dodo")
|
||||||
|
; *
|
||||||
|
DKIM-Signature: v=1; a=ed25519-sha256; c=relaxed/relaxed;
|
||||||
|
d=dodo; s=sel-secondary-1; t=*
|
||||||
|
h=subject:from:subject:date:to:cc:message-id;
|
||||||
|
bh=*
|
||||||
|
b=*
|
||||||
|
*
|
||||||
|
Subject: Los espejos
|
||||||
|
|
||||||
|
Yo que sentí el horror de los espejos
|
||||||
|
no sólo ante el cristal impenetrable
|
||||||
|
donde acaba y empieza, inhabitable,
|
||||||
|
un imposible espacio de reflejos
|
||||||
|
|
||||||
27
test/t-22-forward_via/expected-primary-user111@dodo
Normal file
27
test/t-22-forward_via/expected-primary-user111@dodo
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
Authentication-Results: primary
|
||||||
|
;spf=pass (matched mx)
|
||||||
|
;dkim=pass header.b=???????????? header.d=dodo
|
||||||
|
Received-SPF: pass (matched mx)
|
||||||
|
Received: from *
|
||||||
|
by primary (chasquid) with ESMTPS
|
||||||
|
tls TLS_*
|
||||||
|
(over SMTP, TLS-1.3, envelope from "user222@dodo")
|
||||||
|
; *
|
||||||
|
Received: from localhost
|
||||||
|
by secondary (chasquid) with ESMTPSA
|
||||||
|
tls TLS_*
|
||||||
|
(over submission+TLS, TLS-1.3, envelope from "user222@dodo")
|
||||||
|
; *
|
||||||
|
DKIM-Signature: v=1; a=ed25519-sha256; c=relaxed/relaxed;
|
||||||
|
d=dodo; s=sel-secondary-1; t=*
|
||||||
|
h=subject:from:subject:date:to:cc:message-id;
|
||||||
|
bh=*
|
||||||
|
b=*
|
||||||
|
*
|
||||||
|
Subject: Los espejos
|
||||||
|
|
||||||
|
Yo que sentí el horror de los espejos
|
||||||
|
no sólo ante el cristal impenetrable
|
||||||
|
donde acaba y empieza, inhabitable,
|
||||||
|
un imposible espacio de reflejos
|
||||||
|
|
||||||
10
test/t-22-forward_via/external/chasquid.conf
vendored
Normal file
10
test/t-22-forward_via/external/chasquid.conf
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
smtp_address: "127.0.0.20:1025"
|
||||||
|
submission_address: ":3587"
|
||||||
|
submission_over_tls_address: ":3465"
|
||||||
|
monitoring_address: ":3099"
|
||||||
|
|
||||||
|
mail_delivery_agent_bin: "test-mda"
|
||||||
|
mail_delivery_agent_args: "external:%to%"
|
||||||
|
|
||||||
|
data_dir: "../.data-external"
|
||||||
|
mail_log_path: "../.logs/external-mail_log"
|
||||||
2
test/t-22-forward_via/external/domains/kiwi/aliases
vendored
Normal file
2
test/t-22-forward_via/external/domains/kiwi/aliases
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Part 3 of chain-1 (see run.sh for the full structure).
|
||||||
|
chain-1-3: chain-1-4@dodo via secondary
|
||||||
10
test/t-22-forward_via/primary/chasquid.conf
Normal file
10
test/t-22-forward_via/primary/chasquid.conf
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
smtp_address: "127.0.0.10:1025"
|
||||||
|
submission_address: ":1587"
|
||||||
|
submission_over_tls_address: ":1465"
|
||||||
|
monitoring_address: ":1099"
|
||||||
|
|
||||||
|
mail_delivery_agent_bin: "test-mda"
|
||||||
|
mail_delivery_agent_args: "primary:%to%"
|
||||||
|
|
||||||
|
data_dir: "../.data-primary"
|
||||||
|
mail_log_path: "../.logs/primary-mail_log"
|
||||||
5
test/t-22-forward_via/primary/domains/dodo/aliases
Normal file
5
test/t-22-forward_via/primary/domains/dodo/aliases
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
# Part 2 of chain-1 (see run.sh for the full structure).
|
||||||
|
chain-1-2: chain-1-3@kiwi
|
||||||
|
|
||||||
|
# Part 5 of chain-1 (see run.sh for the full structure).
|
||||||
|
chain-1-5: user111@dodo
|
||||||
92
test/t-22-forward_via/run.sh
Executable file
92
test/t-22-forward_via/run.sh
Executable file
@@ -0,0 +1,92 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
. "$(dirname "$0")/../util/lib.sh"
|
||||||
|
|
||||||
|
init
|
||||||
|
check_hostaliases
|
||||||
|
|
||||||
|
rm -rf .data-primary .data-secondary .data-external .mail
|
||||||
|
rm -f {primary,secondary,external}/domains/*/dkim:*.pem
|
||||||
|
|
||||||
|
# Build with the DNS override, so we can fake DNS records.
|
||||||
|
export GOTAGS="dnsoverride"
|
||||||
|
|
||||||
|
# Two servers for the same domain "dodo":
|
||||||
|
# primary - listens on 127.0.0.10:1025
|
||||||
|
# secondary - listens on 127.0.0.11:1025
|
||||||
|
#
|
||||||
|
# One server for domain "kiwi":
|
||||||
|
# external - listens on 127.0.0.20:1025
|
||||||
|
|
||||||
|
CONFDIR=primary generate_certs_for primary
|
||||||
|
CONFDIR=primary add_user user111@dodo user111
|
||||||
|
CONFDIR=primary chasquid-util dkim-keygen --algo=ed25519 \
|
||||||
|
dodo sel-primary-1 > /dev/null
|
||||||
|
|
||||||
|
CONFDIR=secondary generate_certs_for secondary
|
||||||
|
CONFDIR=secondary add_user user222@dodo user222
|
||||||
|
CONFDIR=secondary chasquid-util dkim-keygen --algo=ed25519 \
|
||||||
|
dodo sel-secondary-1 > /dev/null
|
||||||
|
|
||||||
|
CONFDIR=external generate_certs_for external
|
||||||
|
CONFDIR=external add_user user333@kiwi user333
|
||||||
|
CONFDIR=external chasquid-util dkim-keygen --algo=ed25519 \
|
||||||
|
kiwi sel-external-1 > /dev/null
|
||||||
|
|
||||||
|
|
||||||
|
# Launch minidns in the background using our configuration.
|
||||||
|
# Augment the zones with the dkim ones.
|
||||||
|
cp zones .zones
|
||||||
|
CONFDIR=primary chasquid-util dkim-dns dodo | sed 's/"//g' >> .zones
|
||||||
|
CONFDIR=secondary chasquid-util dkim-dns dodo | sed 's/"//g' >> .zones
|
||||||
|
CONFDIR=external chasquid-util dkim-dns kiwi | sed 's/"//g' >> .zones
|
||||||
|
minidns_bg --addr=":9053" -zones=.zones >> .minidns.log 2>&1
|
||||||
|
|
||||||
|
|
||||||
|
mkdir -p .logs
|
||||||
|
|
||||||
|
chasquid -v=2 --logfile=.logs/primary-chasquid.log --config_dir=primary \
|
||||||
|
--testing__dns_addr=127.0.0.1:9053 \
|
||||||
|
--testing__outgoing_smtp_port=1025 &
|
||||||
|
chasquid -v=2 --logfile=.logs/secondary-chasquid.log --config_dir=secondary \
|
||||||
|
--testing__dns_addr=127.0.0.1:9053 \
|
||||||
|
--testing__outgoing_smtp_port=1025 &
|
||||||
|
chasquid -v=2 --logfile=.logs/external-chasquid.log --config_dir=external \
|
||||||
|
--testing__dns_addr=127.0.0.1:9053 \
|
||||||
|
--testing__outgoing_smtp_port=1025 &
|
||||||
|
|
||||||
|
wait_until "true < /dev/tcp/127.0.0.10/1025" 2>/dev/null
|
||||||
|
wait_until "true < /dev/tcp/127.0.0.11/1025" 2>/dev/null
|
||||||
|
wait_until "true < /dev/tcp/127.0.0.20/1025" 2>/dev/null
|
||||||
|
wait_until_ready 9053
|
||||||
|
|
||||||
|
# Connect to secondary, send an email to user111@dodo (which exists only in
|
||||||
|
# the primary). It should be forwarded to the primary.
|
||||||
|
# Note this also verifies SRS is correct (by comparing the Received headers),
|
||||||
|
# and that DKIM signatures are generated by secondary, and successfully
|
||||||
|
# validated by primary.
|
||||||
|
smtpc -c=smtpc-secondary.conf user111@dodo < content
|
||||||
|
wait_for_file .mail/primary:user111@dodo
|
||||||
|
mail_diff expected-primary-user111@dodo .mail/primary:user111@dodo
|
||||||
|
|
||||||
|
# Connect to the secondary, send an email to user333@kiwi (which exists only
|
||||||
|
# in external). It should be DKIM signed and delivered to the external server.
|
||||||
|
# This is a normal delivery.
|
||||||
|
smtpc -c=smtpc-secondary.conf user333@kiwi < content
|
||||||
|
wait_for_file .mail/external:user333@kiwi
|
||||||
|
mail_diff expected-external-user333@kiwi .mail/external:user333@kiwi
|
||||||
|
|
||||||
|
# Connect to the secondary, send an email to chain-1@dodo, which has a long
|
||||||
|
# alias chain:
|
||||||
|
# secondary: chain-1-1@dodo -> chain-1-2@dodo via primary
|
||||||
|
# primary: chain-1-2@dodo -> chain-1-3@kiwi
|
||||||
|
# external: chain-1-3@kiwi -> chain-1-4@dodo via secondary
|
||||||
|
# secondary: chain-1-4@dodo -> chain-1-5@dodo via primary
|
||||||
|
# primary: chain-1-5@dodo -> user111@dodo
|
||||||
|
rm .mail/primary:user111@dodo
|
||||||
|
smtpc -c=smtpc-secondary.conf chain-1-1@dodo < content
|
||||||
|
wait_for_file .mail/primary:user111@dodo
|
||||||
|
mail_diff expected-chain-1 .mail/primary:user111@dodo
|
||||||
|
|
||||||
|
success
|
||||||
10
test/t-22-forward_via/secondary/chasquid.conf
Normal file
10
test/t-22-forward_via/secondary/chasquid.conf
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
smtp_address: "127.0.0.11:1025"
|
||||||
|
submission_address: ":2587"
|
||||||
|
submission_over_tls_address: ":2465"
|
||||||
|
monitoring_address: ":2099"
|
||||||
|
|
||||||
|
mail_delivery_agent_bin: "test-mda"
|
||||||
|
mail_delivery_agent_args: "secondary:%to%"
|
||||||
|
|
||||||
|
data_dir: "../.data-secondary"
|
||||||
|
mail_log_path: "../.logs/secondary-mail_log"
|
||||||
8
test/t-22-forward_via/secondary/domains/dodo/aliases
Normal file
8
test/t-22-forward_via/secondary/domains/dodo/aliases
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
# Part 1 of chain-1 (see run.sh for the full structure).
|
||||||
|
chain-1-1: chain-1-2@dodo via primary
|
||||||
|
|
||||||
|
# Part 4 chain-1 (see run.sh for the full structure).
|
||||||
|
chain-1-4: chain-1-5@dodo via primary
|
||||||
|
|
||||||
|
# Forward all email to the primary server.
|
||||||
|
*: * via primary
|
||||||
4
test/t-22-forward_via/smtpc-secondary.conf
Normal file
4
test/t-22-forward_via/smtpc-secondary.conf
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
addr localhost:2465
|
||||||
|
server_cert secondary/certs/secondary/fullchain.pem
|
||||||
|
user user222@dodo
|
||||||
|
password user222
|
||||||
15
test/t-22-forward_via/zones
Normal file
15
test/t-22-forward_via/zones
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
primary A 127.0.0.10
|
||||||
|
secondary A 127.0.0.11
|
||||||
|
external A 127.0.0.20
|
||||||
|
|
||||||
|
dodo MX 10 primary
|
||||||
|
dodo MX 20 secondary
|
||||||
|
|
||||||
|
# We need to use mx/8 because the source address will usually be 127.0.0.1,
|
||||||
|
# not 127.0.0.10 or 127.0.0.11.
|
||||||
|
# TODO: Once we support specifying a sender IP address, we should use that
|
||||||
|
# and remove the /8.
|
||||||
|
dodo TXT v=spf1 mx/8 -all
|
||||||
|
|
||||||
|
kiwi MX 10 external
|
||||||
|
kiwi TXT v=spf1 mx/8 -all
|
||||||
@@ -57,11 +57,27 @@ def msg_equals(expected, msg):
|
|||||||
if expected[h] == "*":
|
if expected[h] == "*":
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
msg_hdr_vals = msg.get_all(h, [])
|
||||||
|
if len(msg_hdr_vals) == 1:
|
||||||
if not flexible_eq(val, msg[h]):
|
if not flexible_eq(val, msg[h]):
|
||||||
print("Header %r differs:" % h)
|
print("Header %r differs:" % h)
|
||||||
print("Exp: %r" % val)
|
print("Exp: %r" % val)
|
||||||
print("Got: %r" % msg[h])
|
print("Got: %r" % msg[h])
|
||||||
diff = True
|
diff = True
|
||||||
|
else:
|
||||||
|
# We have multiple values for this header, so we need to check each
|
||||||
|
# one, and only return a diff if none of them match.
|
||||||
|
# Note this will result in a false positive if two headers match
|
||||||
|
# the same expected one, but this is good enough for now.
|
||||||
|
for msg_hdr_val in msg_hdr_vals:
|
||||||
|
if flexible_eq(val, msg_hdr_val):
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
print("Header %r differs, no matching header found" % h)
|
||||||
|
print("Exp: %r" % val)
|
||||||
|
for i, msg_hdr_val in enumerate(msg_hdr_vals):
|
||||||
|
print("Got %d: %r" % (i, msg_hdr_val))
|
||||||
|
diff = True
|
||||||
|
|
||||||
if diff:
|
if diff:
|
||||||
return False
|
return False
|
||||||
|
|||||||
Reference in New Issue
Block a user