Bläddra i källkod

feat: support for mime/related HTML attachments (#93)

Adds support for mime/related HTML attachments.
Jens Rantil 5 år sedan
förälder
incheckning
c069f37d90
2 ändrade filer med 118 tillägg och 6 borttagningar
  1. 48 6
      email.go
  2. 70 0
      email_test.go

+ 48 - 6
email.go

@@ -345,6 +345,17 @@ func writeMessage(buff io.Writer, msg []byte, multipart bool, mediaType string,
 	return qp.Close()
 }
 
+func (e *Email) categorizeAttachments() (htmlRelated, others []*Attachment) {
+	for _, a := range e.Attachments {
+		if a.HTMLRelated {
+			htmlRelated = append(htmlRelated, a)
+		} else {
+			others = append(others, a)
+		}
+	}
+	return
+}
+
 // Bytes converts the Email object to a []byte representation, including all needed MIMEHeaders, boundaries, etc.
 func (e *Email) Bytes() ([]byte, error) {
 	// TODO: better guess buffer size
@@ -355,8 +366,13 @@ func (e *Email) Bytes() ([]byte, error) {
 		return nil, err
 	}
 
+	htmlAttachments, otherAttachments := e.categorizeAttachments()
+	if len(e.HTML) == 0 && len(htmlAttachments) > 0 {
+		return nil, errors.New("there are HTML attachments, but no HTML body")
+	}
+
 	var (
-		isMixed       = len(e.Attachments) > 0
+		isMixed       = len(otherAttachments) > 0
 		isAlternative = len(e.Text) > 0 && len(e.HTML) > 0
 	)
 
@@ -406,10 +422,35 @@ func (e *Email) Bytes() ([]byte, error) {
 			}
 		}
 		if len(e.HTML) > 0 {
+			messageWriter := subWriter
+			var relatedWriter *multipart.Writer
+			if len(htmlAttachments) > 0 {
+				relatedWriter = multipart.NewWriter(buff)
+				header := textproto.MIMEHeader{
+					"Content-Type": {"multipart/related;\r\n boundary=" + relatedWriter.Boundary()},
+				}
+				if _, err := subWriter.CreatePart(header); err != nil {
+					return nil, err
+				}
+
+				messageWriter = relatedWriter
+			}
 			// Write the HTML
-			if err := writeMessage(buff, e.HTML, isMixed || isAlternative, "text/html", subWriter); err != nil {
+			if err := writeMessage(buff, e.HTML, isMixed || isAlternative, "text/html", messageWriter); err != nil {
 				return nil, err
 			}
+			if len(htmlAttachments) > 0 {
+				for _, a := range htmlAttachments {
+					ap, err := relatedWriter.CreatePart(a.Header)
+					if err != nil {
+						return nil, err
+					}
+					// Write the base64Wrapped content to the part
+					base64Wrap(ap, a.Content)
+				}
+
+				relatedWriter.Close()
+			}
 		}
 		if isMixed && isAlternative {
 			if err := subWriter.Close(); err != nil {
@@ -418,7 +459,7 @@ func (e *Email) Bytes() ([]byte, error) {
 		}
 	}
 	// Create attachment part, if necessary
-	for _, a := range e.Attachments {
+	for _, a := range otherAttachments {
 		ap, err := w.CreatePart(a.Header)
 		if err != nil {
 			return nil, err
@@ -559,9 +600,10 @@ func (e *Email) SendWithTLS(addr string, a smtp.Auth, t *tls.Config) error {
 // Attachment is a struct representing an email attachment.
 // Based on the mime/multipart.FileHeader struct, Attachment contains the name, MIMEHeader, and content of the attachment in question
 type Attachment struct {
-	Filename string
-	Header   textproto.MIMEHeader
-	Content  []byte
+	Filename    string
+	Header      textproto.MIMEHeader
+	Content     []byte
+	HTMLRelated bool
 }
 
 // base64Wrap encodes the attachment content, and wraps it according to RFC 2045 standards (every 76 chars)

+ 70 - 0
email_test.go

@@ -1,8 +1,10 @@
 package email
 
 import (
+	"strings"
 	"testing"
 
+	"bufio"
 	"bytes"
 	"crypto/rand"
 	"io"
@@ -67,6 +69,74 @@ func TestEmailText(t *testing.T) {
 	}
 }
 
+func TestEmailWithHTMLAttachments(t *testing.T) {
+	e := prepareEmail()
+
+	// Set plain text to exercise "mime/alternative"
+	e.Text = []byte("Text Body is, of course, supported!\n")
+
+	e.HTML = []byte("<html><body>This is a text.</body></html>")
+
+	// Set HTML attachment to exercise "mime/related".
+	attachment, err := e.Attach(bytes.NewBufferString("Rad attachment"), "rad.txt", "image/png; charset=utf-8")
+	if err != nil {
+		t.Fatal("Could not add an attachment to the message: ", err)
+	}
+	attachment.HTMLRelated = true
+
+	b, err := e.Bytes()
+	if err != nil {
+		t.Fatal("Could not serialize e-mail:", err)
+	}
+
+	// Print the bytes for ocular validation and make sure no errors.
+	//fmt.Println(string(b))
+
+	// TODO: Verify the attachments.
+	s := trimReader{rd: bytes.NewBuffer(b)}
+	tp := textproto.NewReader(bufio.NewReader(s))
+	// Parse the main headers
+	hdrs, err := tp.ReadMIMEHeader()
+	if err != nil {
+		t.Fatal("Could not parse the headers:", err)
+	}
+	// Recursively parse the MIME parts
+	ps, err := parseMIMEParts(hdrs, tp.R)
+	if err != nil {
+		t.Fatal("Could not parse the MIME parts recursively:", err)
+	}
+
+	plainTextFound := false
+	htmlFound := false
+	imageFound := false
+	if expected, actual := 3, len(ps); actual != expected {
+		t.Error("Unexpected number of parts. Expected:", expected, "Was:", actual)
+	}
+	for _, part := range ps {
+		// part has "header" and "body []byte"
+		ct := part.header.Get("Content-Type")
+		if strings.Contains(ct, "image/png") {
+			imageFound = true
+		}
+		if strings.Contains(ct, "text/html") {
+			htmlFound = true
+		}
+		if strings.Contains(ct, "text/plain") {
+			plainTextFound = true
+		}
+	}
+
+	if !plainTextFound {
+		t.Error("Did not find plain text part.")
+	}
+	if !htmlFound {
+		t.Error("Did not find HTML part.")
+	}
+	if !imageFound {
+		t.Error("Did not find image part.")
+	}
+}
+
 func TestEmailHTML(t *testing.T) {
 	e := prepareEmail()
 	e.HTML = []byte("<h1>Fancy Html is supported, too!</h1>\n")