Ver código fonte

Merge pull request #58 from diligiant/add-alternative-only

Optimizes email structure based on what attributes are added. Thanks @diligiant!
Jordan Wright 8 anos atrás
pai
commit
fdc030b482
2 arquivos alterados com 213 adições e 43 exclusões
  1. 65 37
      email.go
  2. 148 6
      email_test.go

+ 65 - 37
email.go

@@ -242,7 +242,7 @@ func (e *Email) AttachFile(filename string) (a *Attachment, err error) {
 func (e *Email) msgHeaders() (textproto.MIMEHeader, error) {
 	res := make(textproto.MIMEHeader, len(e.Headers)+4)
 	if e.Headers != nil {
-		for _, h := range []string{"To", "Cc", "From", "Subject", "Date", "Message-Id"} {
+		for _, h := range []string{"To", "Cc", "From", "Subject", "Date", "Message-Id", "MIME-Version"} {
 			if v, ok := e.Headers[h]; ok {
 				res[h] = v
 			}
@@ -272,8 +272,8 @@ func (e *Email) msgHeaders() (textproto.MIMEHeader, error) {
 	if _, ok := res["Date"]; !ok {
 		res.Set("Date", time.Now().Format(time.RFC1123Z))
 	}
-	if _, ok := res["Mime-Version"]; !ok {
-		res.Set("Mime-Version", "1.0")
+	if _, ok := res["MIME-Version"]; !ok {
+		res.Set("MIME-Version", "1.0")
 	}
 	for field, vals := range e.Headers {
 		if _, ok := res[field]; !ok {
@@ -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,63 @@ 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)
+	}
+	switch {
+	case isMixed:
+		headers.Set("Content-Type", "multipart/mixed;\r\n boundary="+w.Boundary())
+	case isAlternative:
+		headers.Set("Content-Type", "multipart/alternative;\r\n boundary="+w.Boundary())
+	case len(e.HTML) > 0:
+		headers.Set("Content-Type", "text/html; charset=UTF-8")
+	default:
+		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 +378,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) {