fix(notifications): don't re-escape email vars for each recipient

escapeStringMap mutated its input map. The recipient loop in eventsNotifier.render reuses that map across iterations, so each recipient past the first got values with one extra HTML escape layer. Return a new map instead.

Fixes #2804

Signed-off-by: Michael Stingl <mail@michaelstingl.com>
This commit is contained in:
Michael Stingl
2026-05-20 18:45:24 +02:00
parent 1462301116
commit 12206e3e70
3 changed files with 38 additions and 3 deletions

View File

@@ -0,0 +1,9 @@
Bugfix: Don't re-escape notification email vars for each recipient
When a notification went to several recipients in one call (e.g. a group
invite to a space), each recipient past the first got subjects and bodies
with one extra layer of HTML escaping. escapeStringMap mutated its input
map, and the recipient loop reused the same map across iterations. It now
returns a new map.
https://github.com/opencloud-eu/opencloud/issues/2804

View File

@@ -204,8 +204,9 @@ func validateMime(incipit []byte) bool {
}
func escapeStringMap(vars map[string]string) map[string]string {
for k := range vars {
vars[k] = html.EscapeString(vars[k])
escaped := make(map[string]string, len(vars))
for k, v := range vars {
escaped[k] = html.EscapeString(v)
}
return vars
return escaped
}

View File

@@ -0,0 +1,25 @@
package email
import "testing"
// Regression for #2804: escapeStringMap used to mutate its input map, and the
// recipient render loop reuses that map across iterations.
func TestEscapeStringMapDoesNotMutateInput(t *testing.T) {
const raw = "Test & Demo"
input := map[string]string{"SpaceName": raw}
first := escapeStringMap(input)
second := escapeStringMap(input)
if got, want := first["SpaceName"], "Test &amp; Demo"; got != want {
t.Errorf("first call: got %q, want %q", got, want)
}
if first["SpaceName"] != second["SpaceName"] {
t.Errorf("escapeStringMap not idempotent on shared input: first=%q second=%q",
first["SpaceName"], second["SpaceName"])
}
if input["SpaceName"] != raw {
t.Errorf("escapeStringMap mutated its input: got %q, want %q",
input["SpaceName"], raw)
}
}