author | Alberto Bertogli
<albertito@blitiri.com.ar> 2025-03-24 10:38:11 UTC |
committer | Alberto Bertogli
<albertito@blitiri.com.ar> 2025-04-06 13:04:53 UTC |
parent | 8e4d31c74cca756a19cb799772ebd439ae905457 |
.mkdocs.yml | +1 | -0 |
docs/aliases.md | +11 | -0 |
internal/aliases/aliases.go | +10 | -0 |
internal/aliases/aliases_test.go | +82 | -0 |
diff --git a/.mkdocs.yml b/.mkdocs.yml index 792fe37..a557001 100644 --- a/.mkdocs.yml +++ b/.mkdocs.yml @@ -13,6 +13,7 @@ markdown_extensions: - codehilite: guess_lang: false - attr_list + - admonition theme: readthedocs diff --git a/docs/aliases.md b/docs/aliases.md index d3ba0c8..8af704a 100644 --- a/docs/aliases.md +++ b/docs/aliases.md @@ -76,6 +76,17 @@ pepe: jose *: pepe, rose@backgarden ``` +!!! warning "Experimental" + + If the destination address has `*` as its user, then it will be replaced + by the sender user. Note that in this case, the user is copied as-is, no + characters or suffixes will be dropped. + + For example, `*: *@pond` will redirect `lilly@domain` to `lilly@pond`. + + This is experimental as of chasquid 1.16.0, and subject to change. + + ### Overrides If the same left-side address appears more than once, the last one will take diff --git a/internal/aliases/aliases.go b/internal/aliases/aliases.go index 5db0f8e..72666d0 100644 --- a/internal/aliases/aliases.go +++ b/internal/aliases/aliases.go @@ -286,6 +286,16 @@ func (v *Resolver) resolve(rcount int, addr string, tr *trace.Trace) ([]Recipien continue } + // If the user of the destination is "*", then we replace it with the + // original user. + // This allows for catch-all aliases that forward using the original + // user, like "*: *@otherdomain". + if envelope.UserOf(r.Addr) == "*" { + newAddr := user + "@" + envelope.DomainOf(r.Addr) + tr.Debugf("%d| replacing %q with %q", rcount, r.Addr, newAddr) + r.Addr = newAddr + } + ar, err := v.resolve(rcount+1, r.Addr, tr) if err != nil { tr.Debugf("%d| resolve(%q) returned error: %v", rcount, r.Addr, err) diff --git a/internal/aliases/aliases_test.go b/internal/aliases/aliases_test.go index e8ab0ea..3854487 100644 --- a/internal/aliases/aliases_test.go +++ b/internal/aliases/aliases_test.go @@ -62,6 +62,10 @@ func allUsersExist(tr *trace.Trace, user, domain string) (bool, error) { return true, nil } +func noUsersExist(tr *trace.Trace, user, domain string) (bool, error) { + return false, nil +} + func usersWithXDontExist(tr *trace.Trace, user, domain string) (bool, error) { if strings.HasPrefix(user, "x") { return false, nil @@ -104,6 +108,8 @@ func TestBasic(t *testing.T) { func TestCatchAll(t *testing.T) { resolver := NewResolver(usersWithXDontExist) + resolver.DropChars = "." + resolver.SuffixSep = "+" resolver.AddDomain("dom") resolver.aliases = map[string][]Recipient{ "a@dom": {{"a@remote", EMAIL}}, @@ -114,6 +120,8 @@ func TestCatchAll(t *testing.T) { cases := Cases{ {"a@dom", []Recipient{{"a@remote", EMAIL}}, nil}, + {"a+z@dom", []Recipient{{"a@remote", EMAIL}}, nil}, + {"a.@dom", []Recipient{{"a@remote", EMAIL}}, nil}, {"b@dom", []Recipient{{"cmd", PIPE}}, nil}, {"c@dom", []Recipient{{"cmd", PIPE}}, nil}, {"x@dom", []Recipient{{"cmd", PIPE}}, nil}, @@ -132,6 +140,80 @@ func TestCatchAll(t *testing.T) { "x@dom", "x1@dom") } +func TestRightSideAsterisk(t *testing.T) { + resolver := NewResolver(noUsersExist) + resolver.DropChars = "." + resolver.SuffixSep = "+" + resolver.AddDomain("dom1") + resolver.AddDomain("dom2") + resolver.AddDomain("dom3") + resolver.AddDomain("dom4") + resolver.AddDomain("dom5") + resolver.aliases = map[string][]Recipient{ + "a@dom1": {{"aaa@remote", EMAIL}}, + + // Note this goes to dom2 which is local too, and will be resolved + // recursively. + "*@dom1": {{"*@dom2", EMAIL}}, + + "b@dom2": {{"bbb@remote", EMAIL}}, + "*@dom2": {{"*@remote", EMAIL}}, + + // A right hand asterisk on a specific address isn't very useful, but + // it is supported. + "z@dom1": {{"*@remote", EMAIL}}, + + // Asterisk to asterisk creates an infinite loop. + "*@dom3": {{"*@dom3", EMAIL}}, + + // A right-side asterisk as part of multiple addresses, some of which + // are fixed. + "*@dom4": {{"*@remote1", EMAIL}, {"*@remote2", EMAIL}, + {"fixed@remote3", EMAIL}}, + + // A chain of a -> b -> * -> *@remote. + // This checks which one is used as the "original" user. + "a@dom5": {{"b@dom5", EMAIL}}, + "*@dom5": {{"*@remote", EMAIL}}, + } + + cases := Cases{ + {"a@dom1", []Recipient{{"aaa@remote", EMAIL}}, nil}, + {"b@dom1", []Recipient{{"bbb@remote", EMAIL}}, nil}, + {"xyz@dom1", []Recipient{{"xyz@remote", EMAIL}}, nil}, + {"xyz@dom2", []Recipient{{"xyz@remote", EMAIL}}, nil}, + {"z@dom1", []Recipient{{"z@remote", EMAIL}}, nil}, + + // Check that we match after dropping the characters as needed. + // This is not specific to the right side asterisk, but serve to + // confirm we're not matching against it by accident. + {"a+lala@dom1", []Recipient{{"aaa@remote", EMAIL}}, nil}, + {"a..@dom1", []Recipient{{"aaa@remote", EMAIL}}, nil}, + + // Check we don't remove drop characters or suffixes when doing the + // rewrite: we expect to pass addresses as they come if they didn't + // match previously. + {"xyz+abcd@dom1", []Recipient{{"xyz+abcd@remote", EMAIL}}, nil}, + {"x.y.z@dom1", []Recipient{{"x.y.z@remote", EMAIL}}, nil}, + + // This one should fail because it creates an infinite loop. + {"x@dom3", nil, ErrRecursionLimitExceeded}, + + // Check the multiple addresses case. + {"abc@dom4", []Recipient{ + {"abc@remote1", EMAIL}, + {"abc@remote2", EMAIL}, + {"fixed@remote3", EMAIL}, + }, nil}, + + // Check the chain case: a -> b -> * -> remote. + {"a@dom5", []Recipient{{"b@remote", EMAIL}}, nil}, + {"b@dom5", []Recipient{{"b@remote", EMAIL}}, nil}, + {"c@dom5", []Recipient{{"c@remote", EMAIL}}, nil}, + } + cases.check(t, resolver) +} + func TestUserLookupErrors(t *testing.T) { resolver := NewResolver(usersWithXErrorYDontExist) resolver.AddDomain("dom")