Repository: jhillyerd/enmime
Pull request #237 (2022-03-03 12:06:07)

fix: fix for quote-printed utf-8 header with quotes
This PR fixes decoding addresses with quotes. 
For example, `"=?UTF-8?Q?Miros=C5=82aw_Marczak?="` should be decoded as `Mirosław Marczak`.

jhillyerd: I think this may revert the fix for #218 by decoding the 2047 words too early.
jhillyerd: Thinking about how to get better test coverage for this, without introducing a bunch more file based envelope tests: 

We could copy https://github.com/jhillyerd/enmime/blob/master/header_test.go#L60 to envelope_test.go, then modify it to create a minimal message, putting the input values on the `To:` line
dmytrokasianenko-outreach: You are right, it was breaking the the fix for https://github.com/jhillyerd/enmime/issues/218.

I've changed solution by trying to additionally decode `address.Name` with `mime.WordDecoder`. It works fine for the test cases, however I'm not sure it is a valid one. What do you think?
dmytrokasianenko-outreach: I've added a fix for `ParseAddressList` to be able to decode different charsets
jhillyerd: I'll try and take a deeper look at the address.Name decoding in the next few days, thanks
jhillyerd: I think this is a good, thanks for your work on this!

Patch:
diff --git a/header.go b/header.go
index 5c72a97..d1d7845 100644
--- a/header.go
+++ b/header.go
@@ -78,7 +78,9 @@ var AddressHeaders = map[string]bool{
 // It is more tolerant of malformed headers than the ParseAddressList func provided in Go's net/mail
 // package.
 func ParseAddressList(list string) ([]*mail.Address, error) {
-	ret, err := mail.ParseAddressList(list)
+	parser := mail.AddressParser{WordDecoder: coding.NewExtMimeDecoder()}
+
+	ret, err := parser.ParseList(list)
 	if err != nil {
 		switch err.Error() {
 		case "mail: expected comma":
@@ -90,6 +92,11 @@ func ParseAddressList(list string) ([]*mail.Address, error) {
 		return nil, err
 	}
 
+	for i := range ret {
+		// try to additionally decode Name with less strict decoder
+		ret[i].Name = coding.DecodeExtHeader(ret[i].Name)
+	}
+
 	return ret, nil
 }
 
diff --git a/internal/coding/headerext.go b/internal/coding/headerext.go
index 10b3fe2..a463208 100644
--- a/internal/coding/headerext.go
+++ b/internal/coding/headerext.go
@@ -7,6 +7,13 @@ import (
 	"strings"
 )
 
+// NewExtMimeDecoder creates new MIME word decoder which allows decoding of additional charsets.
+func NewExtMimeDecoder() *mime.WordDecoder {
+	return &mime.WordDecoder{
+		CharsetReader: NewCharsetReader,
+	}
+}
+
 // DecodeExtHeader decodes a single line (per RFC 2047, aka Message Header Extensions) using Golang's
 // mime.WordDecoder.
 func DecodeExtHeader(input string) string {
@@ -15,9 +22,7 @@ func DecodeExtHeader(input string) string {
 		return input
 	}
 
-	dec := new(mime.WordDecoder)
-	dec.CharsetReader = NewCharsetReader
-	header, err := dec.DecodeHeader(input)
+	header, err := NewExtMimeDecoder().DecodeHeader(input)
 	if err != nil {
 		return input
 	}


Test patch:
diff --git a/envelope_test.go b/envelope_test.go
index 508b4ac..0854807 100644
--- a/envelope_test.go
+++ b/envelope_test.go
@@ -2,6 +2,8 @@ package enmime_test
 
 import (
 	"bytes"
+	"fmt"
+	"net/mail"
 	"sort"
 	"strings"
 	"testing"
@@ -714,7 +716,7 @@ func TestEnvelopeAddHeader(t *testing.T) {
 
 	// add to existing header
 	to := "James Hillyerd <jamehi03@jamehi03lx.noa.com>"
-	wantSlice := []string{"Mirosław Marczak <marczak@inbucket.com>", "James Hillyerd <jamehi03@jamehi03lx.noa.com>"}
+	wantSlice := []string{"\"Mirosław Marczak\" <marczak@inbucket.com>", "James Hillyerd <jamehi03@jamehi03lx.noa.com>"}
 	e.AddHeader("To", to)
 	gotSlice := e.GetHeaderValues("To")
 	diff := deep.Equal(gotSlice, wantSlice)
@@ -747,6 +749,79 @@ func TestEnvelopeDeleteHeader(t *testing.T) {
 	}
 }
 
+func TestEnvelopeAddressListInDifferentFormats(t *testing.T) {
+	template := `Date: Mon, 23 Jun 2015 11:40:36 -0400
+From: Gopher <from@example.com>
+To: %s
+Subject: Gophers at Gophercon
+
+Message body
+
+`
+	testCases := []struct {
+		input string
+		want  []*mail.Address
+	}{
+		{
+			"Example User <user@example.com>",
+			[]*mail.Address{{
+				Name:    "Example User",
+				Address: "user@example.com"}},
+		},
+		{
+			"a@h, b@h, c@h",
+			[]*mail.Address{
+				{Name: "", Address: "a@h"},
+				{Name: "", Address: "b@h"},
+				{Name: "", Address: "c@h"},
+			},
+		},
+		{
+			"=?utf-8?Q?ze=3AStore=20?= =?utf-8?Q?Orange=20?= =?utf-8?Q?Premium=20?= =?utf-8?Q?Reseller?= <noreply@mail.zestore.ru>",
+			[]*mail.Address{{
+				Name:    "ze:Store Orange Premium Reseller",
+				Address: "noreply@mail.zestore.ru"}},
+		},
+		{
+			`"=?UTF-8?Q?Miros=C5=82aw_Marczak?=" <marczak@inbucket.com>`,
+			[]*mail.Address{{
+				Name:    "Mirosław Marczak",
+				Address: "marczak@inbucket.com",
+			}},
+		},
+	}
+	for _, tc := range testCases {
+		t.Run(tc.input, func(t *testing.T) {
+			r := strings.NewReader(fmt.Sprintf(template, tc.input))
+			e, err := enmime.ReadEnvelope(r)
+			if err != nil {
+				t.Errorf("Failed to read envelope for %q:\n%v", tc.input, err)
+				return
+			}
+
+			got, err := e.AddressList("to")
+			if err != nil {
+				t.Errorf("Failed to parse address list %q:\n%v", tc.input, err)
+				return
+			}
+
+			if len(got) != len(tc.want) {
+				t.Errorf("Got %v addresses, wanted %v", len(got), len(tc.want))
+				return
+			}
+
+			for i, want := range tc.want {
+				if got[i].Name != want.Name {
+					t.Errorf("Got Name %q, wanted %q", got[i].Name, want.Name)
+				}
+				if got[i].Address != want.Address {
+					t.Errorf("Got Address %q, wanted %q", got[i].Address, want.Address)
+				}
+			}
+		})
+	}
+}
+
 func TestEnvelopeAddressList(t *testing.T) {
 	// Test empty header
 	e := &enmime.Envelope{}
diff --git a/example_test.go b/example_test.go
index 2a59451..0b797a7 100644
--- a/example_test.go
+++ b/example_test.go
@@ -215,6 +215,6 @@ func ExampleEnvelope_GetHeaderKeys() {
 	// Mime-Version: 1.0
 	// Sender: André Pirard <PIRARD@vm1.ulg.ac.be>
 	// Subject: MIME UTF8 Test ¢ More Text
-	// To: Mirosław Marczak <marczak@inbucket.com>
+	// To: "Mirosław Marczak" <marczak@inbucket.com>
 	// User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:16.0) Gecko/20121010 Thunderbird/16.0.1
 }
diff --git a/header_test.go b/header_test.go
index f3c0b60..322e8e9 100644
--- a/header_test.go
+++ b/header_test.go
@@ -82,6 +82,26 @@ func TestParseAddressListResult(t *testing.T) {
 				Name:    "ze:Store Orange Premium Reseller",
 				Address: "noreply@mail.zestore.ru"}},
 		},
+		{
+			`"=?UTF-8?Q?Miros=C5=82aw_Marczak?=" <marczak@inbucket.com>`,
+			[]*mail.Address{{
+				Name:    "Mirosław Marczak",
+				Address: "marczak@inbucket.com",
+			}},
+		},
+		{
+			`=?iso-8859-1?q?#=a3_c=a9_r=ae_u=b5?= <a@h>, =?big5?q?=a1=5d_=a1=61_=a1=71?= <b@h>`,
+			[]*mail.Address{
+				{
+					Name:    "#\u00a3 c\u00a9 r\u00ae u\u00b5",
+					Address: "a@h",
+				},
+				{
+					Name:    "\uff08 \uff5b \u3008",
+					Address: "b@h",
+				},
+			},
+		},
 	}
 	for _, tc := range testCases {
 		t.Run(tc.input, func(t *testing.T) {
diff --git a/testdata/mail/qp-utf8-header.raw b/testdata/mail/qp-utf8-header.raw
index ac267d8..d5cdcf2 100644
--- a/testdata/mail/qp-utf8-header.raw
+++ b/testdata/mail/qp-utf8-header.raw
@@ -4,7 +4,7 @@ From: James Hillyerd <jamehi03@jamehi03lx.noa.com>, =?ISO-8859-1?Q?Andr=E9?= Pir
 Sender: =?ISO-8859-1?Q?Andr=E9?= Pirard <PIRARD@vm1.ulg.ac.be>
 User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:16.0) Gecko/20121010 Thunderbird/16.0.1
 MIME-Version: 1.0
-To: =?UTF-8?Q?Miros=C5=82aw_Marczak?= <marczak@inbucket.com>
+To: "=?UTF-8?Q?Miros=C5=82aw_Marczak?=" <marczak@inbucket.com>
 Subject: =?utf-8?q?MIME_UTF8_Test_=c2=a2?= More Text
 Content-Type: multipart/alternative;
  boundary="------------020203040006070307010003"