Browse Source

Added Content-Type logic

Bytes tries to optimize the parts created:
1. no mixed for a text or html message
2. no alternate for a text or html message with attachments

Special handling, if any, or an email consisting of a sole attachment hasn’t been added.

Test cases have been added to cover the different cases.

Signed-off-by: Frédéric Miserey <frederic@none.net>
Frédéric Miserey 8 years ago
parent
commit
83b84678d9
2 changed files with 209 additions and 40 deletions
  1. 61 34
      email.go
  2. 148 6
      email_test.go

+ 61 - 34
email.go

@@ -283,6 +283,25 @@ func (e *Email) msgHeaders() (textproto.MIMEHeader, error) {
 	return res, nil
 }
 
+func writeMessage(buff *bytes.Buffer, msg []byte, multipart bool, mediaType string, w *multipart.Writer) error {
+	if multipart {
+		header := textproto.MIMEHeader{
+			"Content-Type":              {mediaType + "; charset=UTF-8"},
+			"Content-Transfer-Encoding": {"quoted-printable"},
+		}
+		if _, err := w.CreatePart(header); err != nil {
+			return err
+		}
+	}
+
+	qp := quotedprintable.NewWriter(buff)
+	// Write the text
+	if _, err := qp.Write(msg); err != nil {
+		return err
+	}
+	return qp.Close()
+}
+
 // 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
@@ -292,56 +311,62 @@ func (e *Email) Bytes() ([]byte, error) {
 	if err != nil {
 		return nil, err
 	}
-	w := multipart.NewWriter(buff)
-	// TODO: determine the content type based on message/attachment mix.
-	headers.Set("Content-Type", "multipart/mixed;\r\n boundary="+w.Boundary())
+
+	var (
+		isMixed       = len(e.Attachments) > 0
+		isAlternative = len(e.Text) > 0 && len(e.HTML) > 0
+	)
+
+	var w *multipart.Writer
+	if isMixed || isAlternative {
+		w = multipart.NewWriter(buff)
+	}
+	if isMixed {
+		headers.Set("Content-Type", "multipart/mixed;\r\n boundary="+w.Boundary())
+	} else if isAlternative {
+		headers.Set("Content-Type", "multipart/alternative;\r\n boundary="+w.Boundary())
+	} else if len(e.HTML) > 0 {
+		headers.Set("Content-Type", "text/html; charset=UTF-8")
+	} else {
+		headers.Set("Content-Type", "text/plain; charset=UTF-8")
+	}
 	headerToBytes(buff, headers)
 	io.WriteString(buff, "\r\n")
 
-	// Start the multipart/mixed part
-	fmt.Fprintf(buff, "--%s\r\n", w.Boundary())
-	header := textproto.MIMEHeader{}
 	// Check to see if there is a Text or HTML field
 	if len(e.Text) > 0 || len(e.HTML) > 0 {
-		subWriter := multipart.NewWriter(buff)
-		// Create the multipart alternative part
-		header.Set("Content-Type", fmt.Sprintf("multipart/alternative;\r\n boundary=%s\r\n", subWriter.Boundary()))
-		// Write the header
-		headerToBytes(buff, header)
-		// Create the body sections
-		if len(e.Text) > 0 {
-			header.Set("Content-Type", fmt.Sprintf("text/plain; charset=UTF-8"))
-			header.Set("Content-Transfer-Encoding", "quoted-printable")
-			if _, err := subWriter.CreatePart(header); err != nil {
-				return nil, err
+		var subWriter *multipart.Writer
+
+		if isMixed && isAlternative {
+			// Create the multipart alternative part
+			subWriter = multipart.NewWriter(buff)
+			header := textproto.MIMEHeader{
+				"Content-Type": {"multipart/alternative;\r\n boundary=" + subWriter.Boundary()},
 			}
-			qp := quotedprintable.NewWriter(buff)
-			// Write the text
-			if _, err := qp.Write(e.Text); err != nil {
+			if _, err := w.CreatePart(header); err != nil {
 				return nil, err
 			}
-			if err := qp.Close(); err != nil {
+		} else {
+			subWriter = w
+		}
+		// Create the body sections
+		if len(e.Text) > 0 {
+			// Write the text
+			if err := writeMessage(buff, e.Text, isMixed || isAlternative, "text/plain", subWriter); err != nil {
 				return nil, err
 			}
 		}
 		if len(e.HTML) > 0 {
-			header.Set("Content-Type", fmt.Sprintf("text/html; charset=UTF-8"))
-			header.Set("Content-Transfer-Encoding", "quoted-printable")
-			if _, err := subWriter.CreatePart(header); err != nil {
-				return nil, err
-			}
-			qp := quotedprintable.NewWriter(buff)
 			// Write the HTML
-			if _, err := qp.Write(e.HTML); err != nil {
+			if err := writeMessage(buff, e.HTML, isMixed || isAlternative, "text/html", subWriter); err != nil {
 				return nil, err
 			}
-			if err := qp.Close(); err != nil {
+		}
+		if isMixed && isAlternative {
+			if err := subWriter.Close(); err != nil {
 				return nil, err
 			}
 		}
-		if err := subWriter.Close(); err != nil {
-			return nil, err
-		}
 	}
 	// Create attachment part, if necessary
 	for _, a := range e.Attachments {
@@ -352,8 +377,10 @@ func (e *Email) Bytes() ([]byte, error) {
 		// Write the base64Wrapped content to the part
 		base64Wrap(ap, a.Content)
 	}
-	if err := w.Close(); err != nil {
-		return nil, err
+	if isMixed || isAlternative {
+		if err := w.Close(); err != nil {
+			return nil, err
+		}
 	}
 	return buff.Bytes(), nil
 }

+ 148 - 6
email_test.go

@@ -15,17 +15,17 @@ import (
 	"net/textproto"
 )
 
-func TestEmailTextHtmlAttachment(t *testing.T) {
+func prepareEmail() *Email {
 	e := NewEmail()
 	e.From = "Jordan Wright <test@example.com>"
 	e.To = []string{"test@example.com"}
 	e.Bcc = []string{"test_bcc@example.com"}
 	e.Cc = []string{"test_cc@example.com"}
 	e.Subject = "Awesome Subject"
-	e.Text = []byte("Text Body is, of course, supported!\n")
-	e.HTML = []byte("<h1>Fancy Html is supported, too!</h1>\n")
-	e.Attach(bytes.NewBufferString("Rad attachement"), "rad.txt", "text/plain; charset=utf-8")
+	return e
+}
 
+func basicTests(t *testing.T, e *Email) *mail.Message {
 	raw, err := e.Bytes()
 	if err != nil {
 		t.Fatal("Failed to render message: ", e)
@@ -48,6 +48,111 @@ func TestEmailTextHtmlAttachment(t *testing.T) {
 			t.Errorf("Wrong value for message header %s: %v != %v", header, expected, val)
 		}
 	}
+	return msg
+}
+
+func TestEmailText(t *testing.T) {
+	e := prepareEmail()
+	e.Text = []byte("Text Body is, of course, supported!\n")
+
+	msg := basicTests(t, e)
+
+	// Were the right headers set?
+	ct := msg.Header.Get("Content-type")
+	mt, _, err := mime.ParseMediaType(ct)
+	if err != nil {
+		t.Fatal("Content-type header is invalid: ", ct)
+	} else if mt != "text/plain" {
+		t.Fatalf("Content-type expected \"text/plain\", not %v", mt)
+	}
+}
+
+func TestEmailHTML(t *testing.T) {
+	e := prepareEmail()
+	e.HTML = []byte("<h1>Fancy Html is supported, too!</h1>\n")
+
+	msg := basicTests(t, e)
+
+	// Were the right headers set?
+	ct := msg.Header.Get("Content-type")
+	mt, _, err := mime.ParseMediaType(ct)
+	if err != nil {
+		t.Fatal("Content-type header is invalid: ", ct)
+	} else if mt != "text/html" {
+		t.Fatalf("Content-type expected \"text/html\", not %v", mt)
+	}
+}
+
+func TestEmailTextAttachment(t *testing.T) {
+	e := prepareEmail()
+	e.Text = []byte("Text Body is, of course, supported!\n")
+	_, err := e.Attach(bytes.NewBufferString("Rad attachment"), "rad.txt", "text/plain; charset=utf-8")
+	if err != nil {
+		t.Fatal("Could not add an attachment to the message: ", err)
+	}
+
+	msg := basicTests(t, e)
+
+	// Were the right headers set?
+	ct := msg.Header.Get("Content-type")
+	mt, params, err := mime.ParseMediaType(ct)
+	if err != nil {
+		t.Fatal("Content-type header is invalid: ", ct)
+	} else if mt != "multipart/mixed" {
+		t.Fatalf("Content-type expected \"multipart/mixed\", not %v", mt)
+	}
+	b := params["boundary"]
+	if b == "" {
+		t.Fatalf("Invalid or missing boundary parameter: ", b)
+	}
+	if len(params) != 1 {
+		t.Fatal("Unexpected content-type parameters")
+	}
+
+	// Is the generated message parsable?
+	mixed := multipart.NewReader(msg.Body, params["boundary"])
+
+	text, err := mixed.NextPart()
+	if err != nil {
+		t.Fatalf("Could not find text component of email: ", err)
+	}
+
+	// Does the text portion match what we expect?
+	mt, _, err = mime.ParseMediaType(text.Header.Get("Content-type"))
+	if err != nil {
+		t.Fatal("Could not parse message's Content-Type")
+	} else if mt != "text/plain" {
+		t.Fatal("Message missing text/plain")
+	}
+	plainText, err := ioutil.ReadAll(text)
+	if err != nil {
+		t.Fatal("Could not read plain text component of message: ", err)
+	}
+	if !bytes.Equal(plainText, []byte("Text Body is, of course, supported!\r\n")) {
+		t.Fatalf("Plain text is broken: %#q", plainText)
+	}
+
+	// Check attachments.
+	_, err = mixed.NextPart()
+	if err != nil {
+		t.Fatalf("Could not find attachment component of email: ", err)
+	}
+
+	if _, err = mixed.NextPart(); err != io.EOF {
+		t.Error("Expected only text and one attachment!")
+	}
+}
+
+func TestEmailTextHtmlAttachment(t *testing.T) {
+	e := prepareEmail()
+	e.Text = []byte("Text Body is, of course, supported!\n")
+	e.HTML = []byte("<h1>Fancy Html is supported, too!</h1>\n")
+	_, err := e.Attach(bytes.NewBufferString("Rad attachment"), "rad.txt", "text/plain; charset=utf-8")
+	if err != nil {
+		t.Fatal("Could not add an attachment to the message: ", err)
+	}
+
+	msg := basicTests(t, e)
 
 	// Were the right headers set?
 	ct := msg.Header.Get("Content-type")
@@ -96,13 +201,50 @@ func TestEmailTextHtmlAttachment(t *testing.T) {
 	// Check attachments.
 	_, err = mixed.NextPart()
 	if err != nil {
-		t.Fatalf("Could not find attachemnt compoenent of email: ", err)
+		t.Fatalf("Could not find attachment component of email: ", err)
 	}
 
 	if _, err = mixed.NextPart(); err != io.EOF {
-		t.Error("Expected only text and one attachement!")
+		t.Error("Expected only text and one attachment!")
 	}
+}
 
+func TestEmailAttachment(t *testing.T) {
+	e := prepareEmail()
+	_, err := e.Attach(bytes.NewBufferString("Rad attachment"), "rad.txt", "text/plain; charset=utf-8")
+	if err != nil {
+		t.Fatal("Could not add an attachment to the message: ", err)
+	}
+	msg := basicTests(t, e)
+
+	// Were the right headers set?
+	ct := msg.Header.Get("Content-type")
+	mt, params, err := mime.ParseMediaType(ct)
+	if err != nil {
+		t.Fatal("Content-type header is invalid: ", ct)
+	} else if mt != "multipart/mixed" {
+		t.Fatalf("Content-type expected \"multipart/mixed\", not %v", mt)
+	}
+	b := params["boundary"]
+	if b == "" {
+		t.Fatalf("Invalid or missing boundary parameter: ", b)
+	}
+	if len(params) != 1 {
+		t.Fatal("Unexpected content-type parameters")
+	}
+
+	// Is the generated message parsable?
+	mixed := multipart.NewReader(msg.Body, params["boundary"])
+
+	// Check attachments.
+	_, err = mixed.NextPart()
+	if err != nil {
+		t.Fatalf("Could not find attachment component of email: ", err)
+	}
+
+	if _, err = mixed.NextPart(); err != io.EOF {
+		t.Error("Expected only one attachment!")
+	}
 }
 
 func TestEmailFromReader(t *testing.T) {