Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 7 additions & 4 deletions docs/reference/modifiers/envelope.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ to specify 1:N mappings. This allows, for example, implementing mailing lists.
The address is normalized before lookup (Punycode in domain-part is decoded,
Unicode is normalized to NFC, the whole string is case-folded).

First, the whole address is looked up. If there is no replacement, local-part
of the address is looked up separately and is replaced in the address while
keeping the domain part intact. Replacements are not applied recursively, that
is, lookup is not repeated for the replacement.
First, the whole address is looked up. If there is no replacement, or the
lookup returns exactly one value equal to that normalized address, the
local-part is looked up separately and replaced while the domain part is kept
intact. An explicit identity mapping of the whole address is that same
unchanged result, so it cannot suppress a relative alias. Any other exact
result is used as-is. Replacements are not applied recursively, that is,
lookup is not repeated for the replacement.

Recipients are not deduplicated after expansion, so message may be delivered
multiple times to a single recipient. However, used delivery target can apply
Expand Down
14 changes: 12 additions & 2 deletions internal/modify/replace_addr.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,17 @@ func (r *replaceAddr) rewrite(ctx context.Context, val string) ([]string, error)
if err != nil {
return []string{val}, err
}

mbox, domain, splitErr := address.Split(normAddr)
// An unchanged singleton is not an exact replacement. table.chain optional
// steps echo the lookup key on a miss, and an explicit identity mapping of
// the whole address is the same result; either way a relative alias still
// applies. Any other exact result is returned as-is and is not rewritten
// again. Domain-less postmaster is its own local-part, so that echo stays
// on the exact path instead of being joined with an empty domain.
if splitErr == nil && domain != "" && len(replacements) == 1 && replacements[0] == normAddr {
replacements = nil
}
if len(replacements) > 0 {
for _, replacement := range replacements {
if !address.Valid(replacement) {
Expand All @@ -119,8 +130,7 @@ func (r *replaceAddr) rewrite(ctx context.Context, val string) ([]string, error)
return replacements, nil
}

mbox, domain, err := address.Split(normAddr)
if err != nil {
if splitErr != nil {
// If we have malformed address here, something is really wrong, but let's
// ignore it silently then anyway.
return []string{val}, nil
Expand Down
7 changes: 7 additions & 0 deletions internal/modify/replace_addr_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,13 @@ func testReplaceAddr(t *testing.T, modName string) {
"\u00E9@foo.example.com": []string{"rcpt@foo.example.com"},
})

// An unchanged exact result still allows a relative alias.
test("cat@example.org", []string{"dog@example.org"},
map[string][]string{
"cat@example.org": []string{"cat@example.org"},
"cat": []string{"dog"},
})

if modName == "modify.replace_rcpt" {
//multiple aliases
test("test@example.com", []string{"test@example.org", "test@example.net"},
Expand Down