From 2c5e0b09a4188757c38b0ef3a0d61802b649c331 Mon Sep 17 00:00:00 2001 From: Sasha Mitchell Date: Sat, 3 Oct 2026 01:36:58 +0700 Subject: [PATCH] fix: keep a comma inside an address name NewEmailFromReader split To and Cc on every comma. "Doe, John" became two fields. A comma in a comment did the same. A comma between addresses still splits. --- email.go | 41 ++++++++++++++++++++++++++++++++++++++++- email_test.go | 18 ++++++++++++++++-- 2 files changed, 56 insertions(+), 3 deletions(-) diff --git a/email.go b/email.go index 57d1b53..3684fc8 100644 --- a/email.go +++ b/email.go @@ -85,10 +85,49 @@ func (tr *trimReader) Read(buf []byte) (int, error) { return n, err } +// splitAddressList splits on commas that separate addresses. A comma inside a +// quoted string or a comment is part of that address. +func splitAddressList(s string) []string { + var parts []string + var b strings.Builder + var quote, escape bool + var paren int + for _, r := range s { + if escape { + b.WriteRune(r) + escape = false + continue + } + if quote && r == '\\' { + b.WriteRune(r) + escape = true + continue + } + if r == '"' { + quote = !quote + b.WriteRune(r) + continue + } + if !quote && r == '(' { + paren++ + } else if !quote && r == ')' && paren > 0 { + paren-- + } + if r == ',' && !quote && paren == 0 { + parts = append(parts, b.String()) + b.Reset() + continue + } + b.WriteRune(r) + } + parts = append(parts, b.String()) + return parts +} + func handleAddressList(v []string) []string { res := []string{} for _, a := range v { - w := strings.Split(a, ",") + w := splitAddressList(a) for _, addr := range w { decodedAddr, err := (&mime.WordDecoder{}).DecodeHeader(strings.TrimSpace(addr)) if err == nil { diff --git a/email_test.go b/email_test.go index b6d62d2..7074d25 100644 --- a/email_test.go +++ b/email_test.go @@ -554,6 +554,20 @@ This is a test message!`) } } +func TestAddressListCommaInsideName(t *testing.T) { + raw := []byte("From: a@b.com\r\nTo: \"Doe, John\" , Jane \r\nCc: (Doe, John) \r\nSubject: hi\r\nContent-Type: text/plain\r\n\r\nhello\r\n") + e, err := NewEmailFromReader(bytes.NewReader(raw)) + if err != nil { + t.Fatal(err) + } + if len(e.To) != 2 || e.To[0] != `"Doe, John" ` || e.To[1] != "Jane " { + t.Fatalf("To: got %#v", e.To) + } + if len(e.Cc) != 1 || e.Cc[0] != "(Doe, John) " { + t.Fatalf("Cc: got %#v", e.Cc) + } +} + func TestNonMultipartEmailFromReader(t *testing.T) { ex := &Email{ Text: []byte("This is a test message!"), @@ -728,7 +742,7 @@ TGV0J3MganVzdCBwcmV0ZW5kIHRoaXMgaXMgcmF3IEpQRUcgZGF0YS4= } } -func ExampleGmail() { +func Example_gmail() { e := NewEmail() e.From = "Jordan Wright " e.To = []string{"test@example.com"} @@ -740,7 +754,7 @@ func ExampleGmail() { e.Send("smtp.gmail.com:587", smtp.PlainAuth("", e.From, "password123", "smtp.gmail.com")) } -func ExampleAttach() { +func Example_attach() { e := NewEmail() e.AttachFile("test.txt") }