1
0
mirror of https://blitiri.com.ar/repos/chasquid synced 2026-01-26 20:35:56 +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:
Alberto Bertogli
2025-04-06 12:35:51 +01:00
parent 1cf24ba94a
commit 9999a69086
28 changed files with 671 additions and 74 deletions

View File

@@ -83,6 +83,7 @@ var (
// anyway.
type Recipient struct {
Addr string
Via []string // Used when Type == FORWARD.
Type RType
}
@@ -91,8 +92,9 @@ type RType string
// Valid recipient types.
const (
EMAIL RType = "(email)"
PIPE RType = "(pipe)"
EMAIL RType = "(email)"
PIPE RType = "(pipe)"
FORWARD RType = "(forward)"
)
var (
@@ -215,7 +217,7 @@ func (v *Resolver) resolve(rcount int, addr string, tr *trace.Trace) ([]Recipien
user, domain := envelope.Split(addr)
if _, ok := v.domains[domain]; !ok {
tr.Debugf("%d| non-local domain, returning %q", rcount, addr)
return []Recipient{{addr, EMAIL}}, nil
return []Recipient{{addr, nil, EMAIL}}, nil
}
// 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 {
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)
@@ -274,14 +276,14 @@ func (v *Resolver) resolve(rcount int, addr string, tr *trace.Trace) ([]Recipien
// clearer failure, reusing the existing codepaths and simplifying
// the logic.
tr.Debugf("%d| no catch-all, returning %q", rcount, addr)
return []Recipient{{addr, EMAIL}}, nil
return []Recipient{{addr, nil, EMAIL}}, nil
}
}
ret := []Recipient{}
for _, r := range rcpts {
// Only recurse for email recipients.
if r.Type != EMAIL {
// PIPE recipients get added as-is. No modification, and no recursion.
if r.Type == PIPE {
ret = append(ret, r)
continue
}
@@ -296,6 +298,14 @@ func (v *Resolver) resolve(rcount int, addr string, tr *trace.Trace) ([]Recipien
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)
if err != nil {
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.
// Not for use in production code.
func (v *Resolver) AddAliasForTesting(addr, rcpt string, rType RType) {
v.aliases[addr] = append(v.aliases[addr], Recipient{rcpt, rType})
func (v *Resolver) AddAliasForTesting(addr, rcpt string, via []string, rType RType) {
v.aliases[addr] = append(v.aliases[addr], Recipient{rcpt, via, rType})
}
// 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) {
var err error
if len(rawalias) == 0 {
// Explicitly allow empty rawalias strings at this point: the file
// 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.
return nil, fmt.Errorf("the pipe alias is missing a command")
}
return []Recipient{{cmd, PIPE}}, nil
return []Recipient{{cmd, nil, PIPE}}, nil
}
rs := []Recipient{}
@@ -497,20 +508,67 @@ func parseRHS(rawalias, domain string) ([]Recipient, error) {
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
r := Recipient{
Addr: a,
Via: nil,
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 {
return nil, fmt.Errorf("normalizing address %q: %w", a, err)
}
rs = append(rs, Recipient{a, EMAIL})
rs = append(rs, r)
}
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,
// including them.
func removeAllAfter(s, seps string) string {

View File

@@ -86,11 +86,15 @@ func usersWithXErrorYDontExist(tr *trace.Trace, user, domain string) (bool, erro
}
func email(addr string) Recipient {
return Recipient{addr, EMAIL}
return Recipient{addr, nil, EMAIL}
}
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) {
@@ -101,17 +105,20 @@ func TestBasic(t *testing.T) {
"a@localA": {email("c@d"), email("e@localB")},
"e@localB": {pipe("cmd")},
"cmd@localA": {email("x@y")},
"x@localA": {forward("z@localA", []string{"serverX"})},
}
cases := Cases{
{"a@localA", []Recipient{email("c@d"), pipe("cmd")}, nil},
{"e@localB", []Recipient{pipe("cmd")}, nil},
{"x@y", []Recipient{email("x@y")}, nil},
{"x@localA", []Recipient{
forward("z@localA", []string{"serverX"})}, nil},
}
cases.check(t, resolver)
mustExist(t, resolver, "a@localA", "e@localB", "cmd@localA")
mustNotExist(t, resolver, "x@y")
mustExist(t, resolver, "a@localA", "e@localB", "cmd@localA", "x@localA")
mustNotExist(t, resolver, "x@y", "z@localA")
}
func TestCatchAll(t *testing.T) {
@@ -157,6 +164,7 @@ func TestRightSideAsterisk(t *testing.T) {
resolver.AddDomain("dom3")
resolver.AddDomain("dom4")
resolver.AddDomain("dom5")
resolver.AddDomain("dom6")
resolver.aliases = map[string][]Recipient{
"a@dom1": {email("aaa@remote")},
@@ -183,6 +191,10 @@ func TestRightSideAsterisk(t *testing.T) {
// This checks which one is used as the "original" user.
"a@dom5": {email("b@dom5")},
"*@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{
@@ -218,6 +230,10 @@ func TestRightSideAsterisk(t *testing.T) {
{"a@dom5", []Recipient{email("b@remote")}, nil},
{"b@dom5", []Recipient{email("b@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)
}
@@ -527,6 +543,11 @@ func TestAddFile(t *testing.T) {
{"a: c@d, e@f, g\n",
[]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")
@@ -564,6 +585,7 @@ func TestAddFile(t *testing.T) {
{"a@dom: b@c \n", "left-side: cannot contain @"},
{"a", "line 1: missing ':' in line"},
{"a: x y z\n", "disallowed rune encountered"},
{"a: f via sA//sB\n", "empty server in via list"},
}
for _, c := range errcases {
@@ -607,6 +629,9 @@ ppp1: p.q+r
ppp2: p.q
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:
y: z`
@@ -622,8 +647,8 @@ func TestRichFile(t *testing.T) {
t.Fatalf("failed to add file: %v", err)
}
if n != 11 {
t.Fatalf("expected 11 aliases, got %d", n)
if n != 12 {
t.Fatalf("expected 12 aliases, got %d", n)
}
cases := Cases{
@@ -647,6 +672,12 @@ func TestRichFile(t *testing.T) {
{"ppp2@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},
}
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.
func FuzzReader(f *testing.F) {
resolver := NewResolver(allUsersExist)

View File

@@ -8,4 +8,9 @@ type Courier interface {
// Deliver mail to a recipient. Return the error (if any), and whether it
// is permanent (true) or transient (false).
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)
}

View File

@@ -3,6 +3,7 @@ package courier
import (
"bytes"
"context"
"errors"
"fmt"
"os/exec"
"strings"
@@ -108,3 +109,11 @@ func sanitizeForMDA(s string) string {
}
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
}

View File

@@ -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")
}
}

View File

@@ -63,7 +63,7 @@ func (s *SMTP) Deliver(from string, to string, data []byte) (error, bool) {
to: to,
toDomain: envelope.DomainOf(to),
data: data,
tr: trace.New("Courier.SMTP", to),
tr: trace.New("Courier.SMTP.Deliver", to),
}
defer a.tr.Finish()
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
}
// 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 {
courier *SMTP

View File

@@ -191,6 +191,9 @@ func (q *Queue) Put(tr *trace.Trace, from string, to []string, data []byte) (str
r.Type = Recipient_EMAIL
case aliases.PIPE:
r.Type = Recipient_PIPE
case aliases.FORWARD:
r.Type = Recipient_FORWARD
r.Via = aliasRcpt.Via
default:
log.Errorf("unknown alias type %v when resolving %q",
aliasRcpt.Type, t)
@@ -387,6 +390,21 @@ func (item *Item) deliver(q *Queue, rcpt *Recipient) (err error, permanent bool)
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.
if envelope.DomainIn(rcpt.Address, q.localDomains) {
deliverAttempts.Add("email:local", 1)
@@ -396,24 +414,32 @@ func (item *Item) deliver(q *Queue, rcpt *Recipient) (err error, permanent bool)
deliverAttempts.Add("email:remote", 1)
from := item.From
if !envelope.DomainIn(item.From, q.localDomains) {
// We're sending from a non-local to a non-local. This should
// happen only when there's an alias to forward email to a
// non-local domain. In this case, using the original From is
// problematic, as we may not be an authorized sender for this.
// 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.
// Note this assumes "+" is an alias suffix separator.
// We use the IDNA version of the domain if possible, because
// we can't know if the other side will support SMTPUTF8.
from = fmt.Sprintf("%s+fwd_from=%s@%s",
envelope.UserOf(rcpt.OriginalAddress),
strings.Replace(from, "@", "=", -1),
mustIDNAToASCII(envelope.DomainOf(rcpt.OriginalAddress)))
// We're sending from a non-local to a non-local, need to do SRS.
from = rewriteSender(item.From, rcpt.OriginalAddress)
}
return q.remoteC.Deliver(from, rcpt.Address, item.Data)
}
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.
// We use the IDNA version of the domain if possible, because
// we can't know if the other side will support SMTPUTF8.
return fmt.Sprintf("%s+fwd_from=%s@%s",
envelope.UserOf(originalAddr),
strings.Replace(from, "@", "=", -1),
mustIDNAToASCII(envelope.DomainOf(originalAddr)))
}
// countRcpt counts how many recipients are in the given status.
func (item *Item) countRcpt(statuses ...Recipient_Status) int {
c := 0

View File

@@ -23,8 +23,9 @@ const (
type Recipient_Type int32
const (
Recipient_EMAIL Recipient_Type = 0
Recipient_PIPE Recipient_Type = 1
Recipient_EMAIL Recipient_Type = 0
Recipient_PIPE Recipient_Type = 1
Recipient_FORWARD Recipient_Type = 2
)
// Enum value maps for Recipient_Type.
@@ -32,10 +33,12 @@ var (
Recipient_Type_name = map[int32]string{
0: "EMAIL",
1: "PIPE",
2: "FORWARD",
}
Recipient_Type_value = map[string]int32{
"EMAIL": 0,
"PIPE": 1,
"EMAIL": 0,
"PIPE": 1,
"FORWARD": 2,
}
)
@@ -221,6 +224,8 @@ type Recipient struct {
// This is before expanding aliases and only used in very particular
// cases.
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() {
@@ -290,6 +295,13 @@ func (x *Recipient) GetOriginalAddress() string {
return ""
}
func (x *Recipient) GetVia() []string {
if x != nil {
return x.Via
}
return nil
}
// Timestamp representation, for convenience.
// 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.
@@ -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,
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,
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,
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,
@@ -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,
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,
0x64, 0x72, 0x65, 0x73, 0x73, 0x22, 0x1b, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12, 0x09, 0x0a,
0x05, 0x45, 0x4d, 0x41, 0x49, 0x4c, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x50, 0x49, 0x50, 0x45,
0x10, 0x01, 0x22, 0x2b, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0b, 0x0a, 0x07,
0x50, 0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x53, 0x45, 0x4e,
0x54, 0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x02, 0x22,
0x3b, 0x0a, 0x09, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x18, 0x0a, 0x07,
0x73, 0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x73,
0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x61, 0x6e, 0x6f, 0x73, 0x18,
0x02, 0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x6e, 0x61, 0x6e, 0x6f, 0x73, 0x42, 0x2b, 0x5a, 0x29,
0x62, 0x6c, 0x69, 0x74, 0x69, 0x72, 0x69, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x72, 0x2f, 0x67,
0x6f, 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,
0x64, 0x72, 0x65, 0x73, 0x73, 0x12, 0x10, 0x0a, 0x03, 0x76, 0x69, 0x61, 0x18, 0x06, 0x20, 0x03,
0x28, 0x09, 0x52, 0x03, 0x76, 0x69, 0x61, 0x22, 0x28, 0x0a, 0x04, 0x54, 0x79, 0x70, 0x65, 0x12,
0x09, 0x0a, 0x05, 0x45, 0x4d, 0x41, 0x49, 0x4c, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x50, 0x49,
0x50, 0x45, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x46, 0x4f, 0x52, 0x57, 0x41, 0x52, 0x44, 0x10,
0x02, 0x22, 0x2b, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x0b, 0x0a, 0x07, 0x50,
0x45, 0x4e, 0x44, 0x49, 0x4e, 0x47, 0x10, 0x00, 0x12, 0x08, 0x0a, 0x04, 0x53, 0x45, 0x4e, 0x54,
0x10, 0x01, 0x12, 0x0a, 0x0a, 0x06, 0x46, 0x41, 0x49, 0x4c, 0x45, 0x44, 0x10, 0x02, 0x22, 0x3b,
0x0a, 0x09, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x12, 0x18, 0x0a, 0x07, 0x73,
0x65, 0x63, 0x6f, 0x6e, 0x64, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x07, 0x73, 0x65,
0x63, 0x6f, 0x6e, 0x64, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x6e, 0x61, 0x6e, 0x6f, 0x73, 0x18, 0x02,
0x20, 0x01, 0x28, 0x05, 0x52, 0x05, 0x6e, 0x61, 0x6e, 0x6f, 0x73, 0x42, 0x2b, 0x5a, 0x29, 0x62,
0x6c, 0x69, 0x74, 0x69, 0x72, 0x69, 0x2e, 0x63, 0x6f, 0x6d, 0x2e, 0x61, 0x72, 0x2f, 0x67, 0x6f,
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 (

View File

@@ -27,6 +27,7 @@ message Recipient {
enum Type {
EMAIL = 0;
PIPE = 1;
FORWARD = 2;
}
Type type = 2;
@@ -43,6 +44,9 @@ message Recipient {
// This is before expanding aliases and only used in very particular
// cases.
string original_address = 5;
// The list of servers to use, for recipients of type == FORWARD.
repeated string via = 6;
}
// Timestamp representation, for convenience.

View File

@@ -127,28 +127,52 @@ func TestAliases(t *testing.T) {
defer tr.Finish()
q.aliases.AddDomain("loco")
q.aliases.AddAliasForTesting("ab@loco", "pq@loco", aliases.EMAIL)
q.aliases.AddAliasForTesting("ab@loco", "rs@loco", aliases.EMAIL)
q.aliases.AddAliasForTesting("cd@loco", "ata@hualpa", aliases.EMAIL)
q.aliases.AddAliasForTesting("ab@loco", "pq@loco", nil, aliases.EMAIL)
q.aliases.AddAliasForTesting("ab@loco", "rs@loco", nil, 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
// and it can be quite inconvenient to test them in this way.
localC.Expect(2)
remoteC.Expect(1)
_, err := q.Put(tr, "from", []string{"ab@loco", "cd@loco"}, []byte("data"))
remoteC.Expect(3)
// 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 {
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()
remoteC.Wait()
cases := []struct {
courier *testlib.TestCourier
expectedTo string
courier *testlib.TestCourier
expectedFrom string
expectedTo string
}{
{localC, "pq@loco"},
{localC, "rs@loco"},
{remoteC, "ata@hualpa"},
// From the local domain: from@loco
{localC, "from@loco", "pq@loco"},
{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 {
req := c.courier.ReqFor[c.expectedTo]
@@ -157,9 +181,9 @@ func TestAliases(t *testing.T) {
continue
}
if req.From != "from" || req.To != c.expectedTo ||
if req.From != c.expectedFrom || req.To != c.expectedTo ||
!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)
}
}
}

View File

@@ -671,7 +671,7 @@ func realMain(m *testing.M) int {
udb := userdb.New("/dev/null")
udb.AddUser("testuser", "testpasswd")
s.aliasesR.AddAliasForTesting(
"to@localhost", "testuser@localhost", aliases.EMAIL)
"to@localhost", "testuser@localhost", nil, aliases.EMAIL)
s.authr.Register("localhost", auth.WrapNoErrorBackend(udb))
s.AddDomain("localhost")

View File

@@ -89,6 +89,7 @@ type deliverRequest struct {
From string
To string
Data []byte
Via []string
}
// TestCourier never fails, and always remembers everything.
@@ -102,7 +103,17 @@ type TestCourier struct {
// Deliver the given mail (saving it in tc.Requests).
func (tc *TestCourier) Deliver(from string, to string, data []byte) (error, bool) {
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.Requests = append(tc.Requests, dr)
tc.ReqFor[to] = dr
@@ -133,6 +144,10 @@ func (c dumbCourier) Deliver(from string, to string, data []byte) (error, bool)
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.
var DumbCourier = dumbCourier{}