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..a17a6fd 100644 --- a/email.go +++ b/email.go @@ -165,9 +165,9 @@ func NewEmailFromReader(r io.Reader) (*Email, error) { if err != nil { return e, err } - filename, filenameDefined := params["filename"] - if cd == "attachment" || (cd == "inline" && filenameDefined) { - _, err = e.Attach(bytes.NewReader(p.body), filename, ct) + 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 } @@ -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) @@ -714,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)) @@ -763,7 +770,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..2bcafc1 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") { @@ -397,6 +406,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 +537,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 +547,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 +565,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) } @@ -650,6 +669,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 @@ -679,7 +703,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 +712,15 @@ TGV0J3MganVzdCBwcmV0ZW5kIHRoaXMgaXMgcmF3IEpQRUcgZGF0YS4= --35d10c2224bd787fe700c2c6f4769ddc936eb8a0b58e9c8717e406c5abb7 Content-Disposition: inline; filename="cat-inline.jpeg" -Content-Id: +Content-Id: +Content-Transfer-Encoding: base64 +Content-Type: image/jpeg + +TGV0J3MganVzdCBwcmV0ZW5kIHRoaXMgaXMgcmF3IEpQRUcgZGF0YS4= + +--35d10c2224bd787fe700c2c6f4769ddc936eb8a0b58e9c8717e406c5abb7 +Content-Disposition: inline +Content-Id: Content-Transfer-Encoding: base64 Content-Type: image/jpeg @@ -711,8 +743,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) @@ -720,12 +752,34 @@ 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"), "") + } + } + //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() {