From 1c58d382803ccec1be80b020799ee3034a79df9b Mon Sep 17 00:00:00 2001 From: Mechiel Lukkien Date: Fri, 7 Mar 2025 15:48:24 +0100 Subject: [PATCH] webmail: When completing a recipient address, quote the "name" if necessary for proper interpretation. Especially relevant when the name contains a comma, e.g. "lastname, firstname". Or when it contains parentheses, e.g. "(organization)". When sending to an address with a comma that isn't quoted, we would actually interpret it as two addresses: One without an "@" before the comma, and the second part after the comma with half of the name and the email addrss. This resulted in an error message. When sending to a recipient with unquoted parentheses in the name, those parentheses would be interpreted as an generic email header comment, and left out. For issue #305 by mattfbacon. --- webmail/api.go | 26 +++++++++++++++++++++----- webmail/api_test.go | 10 +++++----- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/webmail/api.go b/webmail/api.go index 82f854a..8479e02 100644 --- a/webmail/api.go +++ b/webmail/api.go @@ -1417,12 +1417,28 @@ func addressString(a message.Address, smtputf8 bool) string { host = dom.ASCII } } - s := "<" + a.User + "@" + host + ">" - if a.Name != "" { - // todo: properly encoded/escaped name - s = a.Name + " " + s + if a.Name == "" { + return "<" + a.User + "@" + host + ">" } - return s + // We only quote the name if we have to. ../rfc/5322:679 + const atom = "!#$%&'*+-/=?^_`{|}~" + name := a.Name + for _, c := range a.Name { + if c == '\t' || c == ' ' || c >= 0x80 || c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || strings.ContainsAny(string(c), atom) { + continue + } + // We need to quote. + q := `"` + for _, c := range a.Name { + if c == '\\' || c == '"' { + q += `\` + } + q += string(c) + } + q += `"` + name = q + } + return name + " <" + a.User + "@" + host + ">" } // MailboxSetSpecialUse sets the special use flags of a mailbox. diff --git a/webmail/api_test.go b/webmail/api_test.go index 7276905..171af43 100644 --- a/webmail/api_test.go +++ b/webmail/api_test.go @@ -315,7 +315,7 @@ func TestAPI(t *testing.T) { draftID := api.MessageCompose(ctx, ComposeMessage{ From: "mjl@mox.example", To: []string{"mjl+to@mox.example", "mjl to2 "}, - Cc: []string{"mjl+cc@mox.example", "mjl cc2 "}, + Cc: []string{"mjl+cc@mox.example", `"mjl, cc2" `}, Bcc: []string{"mjl+bcc@mox.example", "mjl bcc2 "}, Subject: "test email", TextBody: "this is the content\n\ncheers,\nmox", @@ -325,7 +325,7 @@ func TestAPI(t *testing.T) { draftID = api.MessageCompose(ctx, ComposeMessage{ From: "mjl@mox.example", To: []string{"mjl+to@mox.example", "mjl to2 "}, - Cc: []string{"mjl+cc@mox.example", "mjl cc2 "}, + Cc: []string{"mjl+cc@mox.example", `"mjl, cc2" `}, Bcc: []string{"mjl+bcc@mox.example", "mjl bcc2 "}, Subject: "test email", TextBody: "this is the content\n\ncheers,\nmox", @@ -345,7 +345,7 @@ func TestAPI(t *testing.T) { api.MessageSubmit(ctx, SubmitMessage{ From: "mjl@mox.example", To: []string{"mjl+to@mox.example", "mjl to2 "}, - Cc: []string{"mjl+cc@mox.example", "mjl cc2 "}, + Cc: []string{"mjl+cc@mox.example", `"mjl, cc2" `}, Bcc: []string{"mjl+bcc@mox.example", "mjl bcc2 "}, Subject: "test email", TextBody: "this is the content\n\ncheers,\nmox", @@ -358,7 +358,7 @@ func TestAPI(t *testing.T) { api.MessageSubmit(ctx, SubmitMessage{ From: "mjl-altcatchall@mox.example", To: []string{"mjl-to@mox.example", "mjl to2 "}, - Cc: []string{"mjl-cc@mox.example", "mjl cc2 "}, + Cc: []string{"mjl-cc@mox.example", `"mjl, cc2" `}, Bcc: []string{"mjl-bcc@mox.example", "mjl bcc2 "}, Subject: "test email", TextBody: "this is the content\n\ncheers,\nmox", @@ -511,7 +511,7 @@ func TestAPI(t *testing.T) { tcompare(t, len(l), 0) tcompare(t, full, true) l, full = api.CompleteRecipient(ctx, "cc2") - tcompare(t, l, []string{"mjl cc2 ", "mjl bcc2 "}) + tcompare(t, l, []string{`"mjl, cc2" `, "mjl bcc2 "}) tcompare(t, full, true) // RecipientSecurity