From 42dac860d9903c0d191fd81b4cdb1e298ccaaf4f Mon Sep 17 00:00:00 2001 From: Tarmo Randma Date: Tue, 14 Jul 2020 19:12:13 +0300 Subject: [PATCH 1/7] Support for inline attachments with NewEmailFromReader --- email.go | 2 +- email_test.go | 23 +++++++++++++++++++++-- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/email.go b/email.go index bde51cd..c5f193a 100644 --- a/email.go +++ b/email.go @@ -160,7 +160,7 @@ func NewEmailFromReader(r io.Reader) (*Email, error) { if err != nil { return e, err } - if cd == "attachment" { + if cd == "attachment" || cd == "inline" { _, err = e.Attach(bytes.NewReader(p.body), params["filename"], ct) if err != nil { return e, err diff --git a/email_test.go b/email_test.go index 9c14753..534ccff 100644 --- a/email_test.go +++ b/email_test.go @@ -528,7 +528,7 @@ d-printable decoding. } } -func TestAttachmentEmailFromReader (t *testing.T) { +func TestAttachmentEmailFromReader(t *testing.T) { ex := &Email{ Subject: "Test Subject", To: []string{"Jordan Wright "}, @@ -540,6 +540,10 @@ func TestAttachmentEmailFromReader (t *testing.T) { if err != nil { t.Fatalf("Error attaching image %s", err.Error()) } + b, err := ex.Attach(bytes.NewReader([]byte("Let's just pretend this is raw JPEG data.")), "cat-inline.jpeg", "image/jpeg") + if err != nil { + t.Fatalf("Error attaching inline image %s", err.Error()) + } raw := []byte(` From: Jordan Wright Date: Thu, 17 Oct 2019 08:55:37 +0100 @@ -575,6 +579,15 @@ Content-Type: image/jpeg TGV0J3MganVzdCBwcmV0ZW5kIHRoaXMgaXMgcmF3IEpQRUcgZGF0YS4= +--35d10c2224bd787fe700c2c6f4769ddc936eb8a0b58e9c8717e406c5abb7 +Content-Disposition: inline; + filename="cat-inline.jpeg" +Content-Id: +Content-Transfer-Encoding: base64 +Content-Type: image/jpeg + +TGV0J3MganVzdCBwcmV0ZW5kIHRoaXMgaXMgcmF3IEpQRUcgZGF0YS4= + --35d10c2224bd787fe700c2c6f4769ddc936eb8a0b58e9c8717e406c5abb7--`) e, err := NewEmailFromReader(bytes.NewReader(raw)) if err != nil { @@ -592,7 +605,7 @@ TGV0J3MganVzdCBwcmV0ZW5kIHRoaXMgaXMgcmF3IEpQRUcgZGF0YS4= if e.From != ex.From { t.Fatalf("Incorrect \"From\": %#q != %#q", e.From, ex.From) } - if len(e.Attachments) != 1 { + if len(e.Attachments) != 2 { t.Fatalf("Incorrect number of attachments %d != %d", len(e.Attachments), 1) } if e.Attachments[0].Filename != a.Filename { @@ -601,6 +614,12 @@ TGV0J3MganVzdCBwcmV0ZW5kIHRoaXMgaXMgcmF3IEpQRUcgZGF0YS4= if !bytes.Equal(e.Attachments[0].Content, a.Content) { t.Fatalf("Incorrect attachment content %#q != %#q", e.Attachments[0].Content, a.Content) } + if e.Attachments[1].Filename != b.Filename { + t.Fatalf("Incorrect attachment filename %s != %s", e.Attachments[1].Filename, b.Filename) + } + if !bytes.Equal(e.Attachments[1].Content, b.Content) { + t.Fatalf("Incorrect attachment content %#q != %#q", e.Attachments[1].Content, b.Content) + } } func ExampleGmail() { From a5c5e9b85911c0010cd7b3804beb50e4fc15ac9b Mon Sep 17 00:00:00 2001 From: Tarmo Randma Date: Tue, 15 Sep 2020 14:37:10 +0300 Subject: [PATCH 2/7] Made inline attachments require a filename to be picked up by NewEmailReader --- email.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/email.go b/email.go index c5f193a..82a68c4 100644 --- a/email.go +++ b/email.go @@ -160,8 +160,9 @@ func NewEmailFromReader(r io.Reader) (*Email, error) { if err != nil { return e, err } - if cd == "attachment" || cd == "inline" { - _, err = e.Attach(bytes.NewReader(p.body), params["filename"], ct) + filename, filenameDefined := params["filename"] + if cd == "attachment" || (cd == "inline" && filenameDefined){ + _, err = e.Attach(bytes.NewReader(p.body), filename, ct) if err != nil { return e, err } From 5c17e3d0971c82db33aa4d3e55f31b8c9e804d2c Mon Sep 17 00:00:00 2001 From: Tarmo Randma Date: Tue, 15 Sep 2020 15:29:14 +0300 Subject: [PATCH 3/7] Support for multiple To, Cc, Bcc addresses with NewEmailFromReader --- email.go | 45 +++++++++++++++++++++++++++------------------ email_test.go | 36 +++++++++++++++++++++++++++++++++--- 2 files changed, 60 insertions(+), 21 deletions(-) diff --git a/email.go b/email.go index 5c88905..381bd1d 100644 --- a/email.go +++ b/email.go @@ -108,32 +108,41 @@ func NewEmailFromReader(r io.Reader) (*Email, error) { } delete(hdrs, h) case h == "To": - for _, to := range v { - tt, err := (&mime.WordDecoder{}).DecodeHeader(to) - if err == nil { - e.To = append(e.To, tt) - } else { - e.To = append(e.To, to) + for _, toA := range v { + w := strings.Split(toA, ",") + for _, to := range w { + tt, err := (&mime.WordDecoder{}).DecodeHeader(strings.TrimSpace(to)) + if err == nil { + e.To = append(e.To, tt) + } else { + e.To = append(e.To, to) + } } } delete(hdrs, h) case h == "Cc": - for _, cc := range v { - tcc, err := (&mime.WordDecoder{}).DecodeHeader(cc) - if err == nil { - e.Cc = append(e.Cc, tcc) - } else { - e.Cc = append(e.Cc, cc) + for _, ccA := range v { + w := strings.Split(ccA, ",") + for _, cc := range w { + tcc, err := (&mime.WordDecoder{}).DecodeHeader(strings.TrimSpace(cc)) + if err == nil { + e.Cc = append(e.Cc, tcc) + } else { + e.Cc = append(e.Cc, cc) + } } } delete(hdrs, h) case h == "Bcc": - for _, bcc := range v { - tbcc, err := (&mime.WordDecoder{}).DecodeHeader(bcc) - if err == nil { - e.Bcc = append(e.Bcc, tbcc) - } else { - e.Bcc = append(e.Bcc, bcc) + for _, bccA := range v { + w := strings.Split(bccA, ",") + for _, bcc := range w { + tbcc, err := (&mime.WordDecoder{}).DecodeHeader(strings.TrimSpace(bcc)) + if err == nil { + e.Bcc = append(e.Bcc, tbcc) + } else { + e.Bcc = append(e.Bcc, bcc) + } } } delete(hdrs, h) diff --git a/email_test.go b/email_test.go index dbbc235..9e53f2d 100644 --- a/email_test.go +++ b/email_test.go @@ -367,8 +367,10 @@ func TestHeaderEncoding(t *testing.T) { func TestEmailFromReader(t *testing.T) { ex := &Email{ Subject: "Test Subject", - To: []string{"Jordan Wright "}, + To: []string{"Jordan Wright ", "also@example.com"}, From: "Jordan Wright ", + Cc: []string{"one@example.com", "Two "}, + Bcc: []string{"three@example.com", "Four "}, Text: []byte("This is a test email with HTML Formatting. It also has very long lines so\nthat the content must be wrapped if using quoted-printable decoding.\n"), HTML: []byte("
This is a test email with HTML Formatting.\u00a0It also has very long lines so that the content must be wrapped if using quoted-printable decoding.
\n"), } @@ -376,7 +378,9 @@ func TestEmailFromReader(t *testing.T) { MIME-Version: 1.0 Subject: Test Subject From: Jordan Wright -To: Jordan Wright +To: Jordan Wright , also@example.com +Cc: one@example.com, Two +Bcc: three@example.com, Four Content-Type: multipart/alternative; boundary=001a114fb3fc42fd6b051f834280 --001a114fb3fc42fd6b051f834280 @@ -410,7 +414,33 @@ d-printable decoding. if e.From != ex.From { t.Fatalf("Incorrect \"From\": %#q != %#q", e.From, ex.From) } - + if len(e.To) != len(ex.To) { + t.Fatalf("Incorrect number of \"To\" addresses: %v != %v", len(e.To), len(ex.To)) + } + if e.To[0] != ex.To[0] { + t.Fatalf("Incorrect \"To[0]\": %#q != %#q", e.To[0], ex.To[0]) + } + if e.To[1] != ex.To[1] { + t.Fatalf("Incorrect \"To[1]\": %#q != %#q", e.To[1], ex.To[1]) + } + if len(e.Cc) != len(ex.Cc) { + t.Fatalf("Incorrect number of \"Cc\" addresses: %v != %v", len(e.Cc), len(ex.Cc)) + } + if e.Cc[0] != ex.Cc[0] { + t.Fatalf("Incorrect \"Cc[0]\": %#q != %#q", e.Cc[0], ex.Cc[0]) + } + if e.Cc[1] != ex.Cc[1] { + t.Fatalf("Incorrect \"Cc[1]\": %#q != %#q", e.Cc[1], ex.Cc[1]) + } + if len(e.Bcc) != len(ex.Bcc) { + t.Fatalf("Incorrect number of \"Bcc\" addresses: %v != %v", len(e.Bcc), len(ex.Bcc)) + } + if e.Bcc[0] != ex.Bcc[0] { + t.Fatalf("Incorrect \"Bcc[0]\": %#q != %#q", e.Cc[0], ex.Cc[0]) + } + if e.Bcc[1] != ex.Bcc[1] { + t.Fatalf("Incorrect \"Bcc[1]\": %#q != %#q", e.Bcc[1], ex.Bcc[1]) + } } func TestNonAsciiEmailFromReader(t *testing.T) { From b42033aa47e6e8ec230ac8144b2877b35d322204 Mon Sep 17 00:00:00 2001 From: Tarmo Randma Date: Tue, 16 Feb 2021 15:57:04 +0200 Subject: [PATCH 4/7] #139 - Missing MIMe headers with NewEmailFromReader --- email.go | 11 +++++++++-- email_test.go | 14 ++++++++++++-- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/email.go b/email.go index 57d1b53..507883e 100644 --- a/email.go +++ b/email.go @@ -167,7 +167,7 @@ func NewEmailFromReader(r io.Reader) (*Email, error) { } filename, filenameDefined := params["filename"] if cd == "attachment" || (cd == "inline" && filenameDefined) { - _, err = e.Attach(bytes.NewReader(p.body), filename, ct) + _, err = e.AttachWithHeaders(bytes.NewReader(p.body), filename, ct, p.header) if err != nil { return e, err } @@ -262,6 +262,13 @@ func parseMIMEParts(hs textproto.MIMEHeader, b io.Reader) ([]*part, error) { // Required parameters include an io.Reader, the desired filename for the attachment, and the Content-Type // The function will return the created Attachment for reference, as well as nil for the error, if successful. func (e *Email) Attach(r io.Reader, filename string, c string) (a *Attachment, err error) { + return e.AttachWithHeaders(r, filename, c, textproto.MIMEHeader{}) +} + +// AttachWithHeaders is used to attach content from an io.Reader to the email. Required parameters include an io.Reader, +// the desired filename for the attachment, the Content-Type and the original MIME headers. +// The function will return the created Attachment for reference, as well as nil for the error, if successful. +func (e *Email) AttachWithHeaders(r io.Reader, filename string, c string, headers textproto.MIMEHeader) (a *Attachment, err error) { var buffer bytes.Buffer if _, err = io.Copy(&buffer, r); err != nil { return @@ -269,7 +276,7 @@ func (e *Email) Attach(r io.Reader, filename string, c string) (a *Attachment, e at := &Attachment{ Filename: filename, ContentType: c, - Header: textproto.MIMEHeader{}, + Header: headers, Content: buffer.Bytes(), } e.Attachments = append(e.Attachments, at) diff --git a/email_test.go b/email_test.go index b6d62d2..7eabd11 100644 --- a/email_test.go +++ b/email_test.go @@ -679,7 +679,7 @@ Content-Type: text/html; charset=UTF-8 --35d10c2224bd787fe700c2c6f4769ddc936eb8a0b58e9c8717e406c5abb7 Content-Disposition: attachment; filename="cat.jpeg" -Content-Id: +Content-Id: Content-Transfer-Encoding: base64 Content-Type: image/jpeg @@ -688,7 +688,7 @@ TGV0J3MganVzdCBwcmV0ZW5kIHRoaXMgaXMgcmF3IEpQRUcgZGF0YS4= --35d10c2224bd787fe700c2c6f4769ddc936eb8a0b58e9c8717e406c5abb7 Content-Disposition: inline; filename="cat-inline.jpeg" -Content-Id: +Content-Id: Content-Transfer-Encoding: base64 Content-Type: image/jpeg @@ -720,12 +720,22 @@ TGV0J3MganVzdCBwcmV0ZW5kIHRoaXMgaXMgcmF3IEpQRUcgZGF0YS4= if !bytes.Equal(e.Attachments[0].Content, a.Content) { t.Fatalf("Incorrect attachment content %#q != %#q", e.Attachments[0].Content, a.Content) } + if e.Attachments[0].Header != nil { + if e.Attachments[0].Header.Get("Content-Id") != "" { + t.Fatalf("Incorrect attachment header Content-Id %s != %s", e.Attachments[0].Header.Get("Content-Id"), "") + } + } if e.Attachments[1].Filename != b.Filename { t.Fatalf("Incorrect attachment filename %s != %s", e.Attachments[1].Filename, b.Filename) } if !bytes.Equal(e.Attachments[1].Content, b.Content) { t.Fatalf("Incorrect attachment content %#q != %#q", e.Attachments[1].Content, b.Content) } + if e.Attachments[1].Header != nil { + if e.Attachments[1].Header.Get("Content-Id") != "" { + t.Fatalf("Incorrect attachment header Content-Id %s != %s", e.Attachments[1].Header.Get("Content-Id"), "") + } + } } func ExampleGmail() { From deb43c1376563bc0e45bd60daeb1992c93bf6b60 Mon Sep 17 00:00:00 2001 From: Tarmo Randma Date: Tue, 16 Mar 2021 12:37:06 +0200 Subject: [PATCH 5/7] Content-ID and filename interaction fix --- email.go | 10 +++++----- email_test.go | 42 ++++++++++++++++++++++++++++++++++++++---- 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/email.go b/email.go index 507883e..5a96966 100644 --- a/email.go +++ b/email.go @@ -165,8 +165,8 @@ func NewEmailFromReader(r io.Reader) (*Email, error) { if err != nil { return e, err } - filename, filenameDefined := params["filename"] - if cd == "attachment" || (cd == "inline" && filenameDefined) { + filename, _ := params["filename"] + if cd == "attachment" || (cd == "inline" && p.header.Get("Content-ID") != "") { _, err = e.AttachWithHeaders(bytes.NewReader(p.body), filename, ct, p.header) if err != nil { return e, err @@ -721,11 +721,11 @@ func (at *Attachment) setDefaultHeaders() { at.Header.Set("Content-Type", contentType) if len(at.Header.Get("Content-Disposition")) == 0 { - disposition := "attachment" if at.HTMLRelated { - disposition = "inline" + at.Header.Set("Content-Disposition", "inline") + }else{ + at.Header.Set("Content-Disposition", fmt.Sprintf("%s;\r\n filename=\"%s\"", "attachment", at.Filename)) } - at.Header.Set("Content-Disposition", fmt.Sprintf("%s;\r\n filename=\"%s\"", disposition, at.Filename)) } if len(at.Header.Get("Content-ID")) == 0 { at.Header.Set("Content-ID", fmt.Sprintf("<%s>", at.Filename)) diff --git a/email_test.go b/email_test.go index 7eabd11..39070f5 100644 --- a/email_test.go +++ b/email_test.go @@ -85,6 +85,12 @@ func TestEmailWithHTMLAttachments(t *testing.T) { } attachment.HTMLRelated = true + // Set regular HTML attachment. + _, err = e.Attach(bytes.NewBufferString("Normal attachment"), "normal.pdf", "application/pdf; charset=utf-8") + if err != nil { + t.Fatal("Could not add an attachment to the message: ", err) + } + b, err := e.Bytes() if err != nil { t.Fatal("Could not serialize e-mail:", err) @@ -110,14 +116,17 @@ func TestEmailWithHTMLAttachments(t *testing.T) { plainTextFound := false htmlFound := false imageFound := false - if expected, actual := 3, len(ps); actual != expected { + if expected, actual := 4, len(ps); actual != expected { t.Error("Unexpected number of parts. Expected:", expected, "Was:", actual) } for _, part := range ps { // part has "header" and "body []byte" cd := part.header.Get("Content-Disposition") ct := part.header.Get("Content-Type") - if strings.Contains(ct, "image/png") && strings.HasPrefix(cd, "inline") { + if strings.Contains(ct, "image/png") && strings.HasPrefix(cd, "inline") && !strings.Contains(cd, "rad.txt"){ + imageFound = true + } + if strings.Contains(ct, "application/pdf") && strings.HasPrefix(cd, "attachment") && strings.Contains(cd, "normal.pdf"){ imageFound = true } if strings.Contains(ct, "text/html") { @@ -650,6 +659,11 @@ func TestAttachmentEmailFromReader(t *testing.T) { if err != nil { t.Fatalf("Error attaching inline image %s", err.Error()) } + c, err := ex.Attach(bytes.NewReader([]byte("Let's just pretend this is raw JPEG data.")), "html-related-cat.jpeg", "image/jpeg") + if err != nil { + t.Fatalf("Error attaching html-related inline image %s", err.Error()) + } + b.HTMLRelated = true raw := []byte(` From: Jordan Wright Date: Thu, 17 Oct 2019 08:55:37 +0100 @@ -694,6 +708,14 @@ Content-Type: image/jpeg TGV0J3MganVzdCBwcmV0ZW5kIHRoaXMgaXMgcmF3IEpQRUcgZGF0YS4= +--35d10c2224bd787fe700c2c6f4769ddc936eb8a0b58e9c8717e406c5abb7 +Content-Disposition: inline +Content-Id: +Content-Transfer-Encoding: base64 +Content-Type: image/jpeg + +TGV0J3MganVzdCBwcmV0ZW5kIHRoaXMgaXMgcmF3IEpQRUcgZGF0YS4= + --35d10c2224bd787fe700c2c6f4769ddc936eb8a0b58e9c8717e406c5abb7--`) e, err := NewEmailFromReader(bytes.NewReader(raw)) if err != nil { @@ -711,8 +733,8 @@ TGV0J3MganVzdCBwcmV0ZW5kIHRoaXMgaXMgcmF3IEpQRUcgZGF0YS4= if e.From != ex.From { t.Fatalf("Incorrect \"From\": %#q != %#q", e.From, ex.From) } - if len(e.Attachments) != 2 { - t.Fatalf("Incorrect number of attachments %d != %d", len(e.Attachments), 1) + if len(e.Attachments) != 3 { + t.Fatalf("Incorrect number of attachments %d != %d", len(e.Attachments), 3) } if e.Attachments[0].Filename != a.Filename { t.Fatalf("Incorrect attachment filename %s != %s", e.Attachments[0].Filename, a.Filename) @@ -736,6 +758,18 @@ TGV0J3MganVzdCBwcmV0ZW5kIHRoaXMgaXMgcmF3IEpQRUcgZGF0YS4= t.Fatalf("Incorrect attachment header Content-Id %s != %s", e.Attachments[1].Header.Get("Content-Id"), "") } } + //Filename should be empty as we are using html-related and inline attachment + if e.Attachments[2].Filename != "" { + t.Fatalf("Incorrect attachment filename %s != %s", e.Attachments[2].Filename, "") + } + if !bytes.Equal(e.Attachments[2].Content, c.Content) { + t.Fatalf("Incorrect attachment content %#q != %#q", e.Attachments[2].Content, c.Content) + } + if e.Attachments[2].Header != nil { + if e.Attachments[2].Header.Get("Content-Id") != "" { + t.Fatalf("Incorrect attachment header Content-Id %s != %s", e.Attachments[2].Header.Get("Content-Id"), "") + } + } } func ExampleGmail() { From 252d37dc7ac55266feea0dca925c3e7dcf29c787 Mon Sep 17 00:00:00 2001 From: Tarmo Randma Date: Thu, 12 Aug 2021 16:21:01 +0300 Subject: [PATCH 6/7] Switched the GO111MODULE env variable to auto to allow building without go.mod --- .github/workflows/test.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 280d268..e3f27ee 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,6 +11,8 @@ jobs: build: name: Build runs-on: ubuntu-latest + env: + GO111MODULE: auto steps: - name: Set up Go 1.x From 7c4c797fdd2ec318d652875cc297d536bb2121c5 Mon Sep 17 00:00:00 2001 From: Tarmo Randma Date: Mon, 16 Aug 2021 14:38:19 +0300 Subject: [PATCH 7/7] #147 Proper handling of utf-8 characters in reply-to headers --- .github/workflows/test.yml | 2 ++ email.go | 2 +- email_test.go | 10 ++++++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 280d268..e3f27ee 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,6 +11,8 @@ jobs: build: name: Build runs-on: ubuntu-latest + env: + GO111MODULE: auto steps: - name: Set up Go 1.x diff --git a/email.go b/email.go index 57d1b53..084c2f9 100644 --- a/email.go +++ b/email.go @@ -763,7 +763,7 @@ func headerToBytes(buff io.Writer, header textproto.MIMEHeader) { switch { case field == "Content-Type" || field == "Content-Disposition": buff.Write([]byte(subval)) - case field == "From" || field == "To" || field == "Cc" || field == "Bcc": + case field == "From" || field == "To" || field == "Cc" || field == "Bcc" || field == "Reply-To": participants := strings.Split(subval, ",") for i, v := range participants { addr, err := mail.ParseAddress(v) diff --git a/email_test.go b/email_test.go index b6d62d2..45fffa5 100644 --- a/email_test.go +++ b/email_test.go @@ -397,6 +397,11 @@ func TestHeaderEncoding(t *testing.T) { have: "Needs Encóding , Only ASCII ", want: "=?utf-8?q?Needs_Enc=C3=B3ding?= , \"Only ASCII\" \r\n", }, + { + field: "Reply-To", + have: "Needs Encóding , Only ASCII ", + want: "=?utf-8?q?Needs_Enc=C3=B3ding?= , \"Only ASCII\" \r\n", + }, { field: "To", have: "Keith Moore , Keld Jørn Simonsen ", @@ -523,6 +528,7 @@ func TestNonAsciiEmailFromReader(t *testing.T) { ex := &Email{ Subject: "Test Subject", To: []string{"Anaïs "}, + ReplyTo: []string{"Anaïs "}, Cc: []string{"Patrik Fältström "}, From: "Mrs Valérie Dupont ", Text: []byte("This is a test message!"), @@ -532,6 +538,7 @@ func TestNonAsciiEmailFromReader(t *testing.T) { Subject: =?UTF-8?Q?Test Subject?= From: Mrs =?ISO-8859-1?Q?Val=C3=A9rie=20Dupont?= To: =?utf-8?q?Ana=C3=AFs?= +Reply-To: =?utf-8?q?Ana=C3=AFs?= Cc: =?ISO-8859-1?Q?Patrik_F=E4ltstr=F6m?= Content-type: text/plain; charset=ISO-8859-1 @@ -549,6 +556,9 @@ This is a test message!`) if e.To[0] != ex.To[0] { t.Fatalf("Incorrect \"To\": %#q != %#q", e.To, ex.To) } + if e.ReplyTo[0] != ex.ReplyTo[0] { + t.Fatalf("Incorrect \"Reply-To\": %#q != %#q", e.ReplyTo, ex.ReplyTo) + } if e.Cc[0] != ex.Cc[0] { t.Fatalf("Incorrect \"Cc\": %#q != %#q", e.Cc, ex.Cc) }