git » chasquid » commit 2ee64de

aliases: Support '*' as the destination user

author Alberto Bertogli
2025-03-24 10:38:11 UTC
committer Alberto Bertogli
2025-04-06 13:04:53 UTC
parent 8e4d31c74cca756a19cb799772ebd439ae905457

aliases: Support '*' as the destination user

This patch implements support for aliases that contain '*' as the
destination user.

In that case, we replace it with the original user.

For example, `*: *@pond` will redirect `lilly@domain` to `lilly@pond`.

This is experimental for now, and marked as such in the documentation.
The semantics can be subtle, so we may need to adjust them later.

.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")