Explorar o código

Added NewEmailFromReader method and tests

Jordan Wright %!s(int64=10) %!d(string=hai) anos
pai
achega
381271e2a1
Modificáronse 2 ficheiros con 161 adicións e 24 borrados
  1. 113 0
      email.go
  2. 48 24
      email_test.go

+ 113 - 0
email.go

@@ -3,6 +3,7 @@
 package email
 
 import (
+	"bufio"
 	"bytes"
 	"encoding/base64"
 	"errors"
@@ -25,6 +26,12 @@ const (
 	MaxLineLength = 76
 )
 
+// ErrMissingBoundary is returned when there is no boundary given for a multipart entity
+var ErrMissingBoundary = errors.New("No boundary found for multipart entity")
+
+// ErrMissingContentType is returned when there is no "Content-Type" header for a MIME entity
+var ErrMissingContentType = errors.New("No Content-Type found for MIME entity")
+
 // Email is the type used for email messages
 type Email struct {
 	From        string
@@ -39,11 +46,117 @@ type Email struct {
 	ReadReceipt []string
 }
 
+// part is a copyable representation of a multipart.Part
+type part struct {
+	header textproto.MIMEHeader
+	body   []byte
+}
+
 // NewEmail creates an Email, and returns the pointer to it.
 func NewEmail() *Email {
 	return &Email{Headers: textproto.MIMEHeader{}}
 }
 
+// NewEmailFromReader reads a stream of bytes from an io.Reader, r,
+// and returns an email struct containing the parsed data.
+// This function expects the data in RFC 5322 format.
+func NewEmailFromReader(r io.Reader) (*Email, error) {
+	e := NewEmail()
+	tp := textproto.NewReader(bufio.NewReader(r))
+	// Parse the main headers
+	hdrs, err := tp.ReadMIMEHeader()
+	if err != nil {
+		return e, err
+	}
+	// Set the subject, to, cc, bcc, and from
+	for h, v := range hdrs {
+		switch {
+		case h == "Subject":
+			e.Subject = v[0]
+			delete(hdrs, h)
+		case h == "To":
+			e.To = v
+			delete(hdrs, h)
+		case h == "Cc":
+			e.Cc = v
+			delete(hdrs, h)
+		case h == "Bcc":
+			e.Bcc = v
+			delete(hdrs, h)
+		case h == "From":
+			e.From = v[0]
+			delete(hdrs, h)
+		}
+	}
+	e.Headers = hdrs
+	body := tp.R
+	// Recursively parse the MIME parts
+	ps, err := parseMIMEParts(e.Headers, body)
+	if err != nil {
+		return e, err
+	}
+	for _, p := range ps {
+		if ct := p.header.Get("Content-Type"); ct == "" {
+			return e, ErrMissingContentType
+		}
+		ct, _, err := mime.ParseMediaType(p.header.Get("Content-Type"))
+		if err != nil {
+			return e, err
+		}
+		switch {
+		case ct == "text/plain":
+			e.Text = p.body
+		case ct == "text/html":
+			e.HTML = p.body
+		}
+	}
+	return e, nil
+}
+
+// parseMIMEParts will recursively walk a MIME entity and return a []mime.Part containing
+// each (flattened) mime.Part found.
+// It is important to note that there are no limits to the number of recursions, so be
+// careful when parsing unknown MIME structures!
+func parseMIMEParts(hs textproto.MIMEHeader, b io.Reader) ([]*part, error) {
+	var ps []*part
+	ct, params, err := mime.ParseMediaType(hs.Get("Content-Type"))
+	if err != nil {
+		return ps, err
+	}
+	if strings.HasPrefix(ct, "multipart/") {
+		if _, ok := params["boundary"]; !ok {
+			return ps, ErrMissingBoundary
+		}
+		mr := multipart.NewReader(b, params["boundary"])
+		for {
+			var buf bytes.Buffer
+			p, err := mr.NextPart()
+			if err == io.EOF {
+				break
+			}
+			if err != nil {
+				return ps, err
+			}
+			subct, _, err := mime.ParseMediaType(p.Header.Get("Content-Type"))
+			if strings.HasPrefix(subct, "multipart/") {
+				sps, err := parseMIMEParts(p.Header, p)
+				if err != nil {
+					return ps, err
+				}
+				ps = append(ps, sps...)
+			} else {
+				// Otherwise, just append the part to the list
+				// Copy the part data into the buffer
+				if _, err := io.Copy(&buf, p); err != nil {
+					return ps, err
+				}
+				ps = append(ps, &part{body: buf.Bytes(), header: p.Header})
+			}
+		}
+	}
+	return ps, nil
+}
+
 // Attach is used to attach content from an io.Reader to the email.
 // 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.

+ 48 - 24
email_test.go

@@ -104,6 +104,54 @@ func TestEmailTextHtmlAttachment(t *testing.T) {
 
 }
 
+func TestEmailFromReader(t *testing.T) {
+	ex := &Email{
+		Subject: "Test Subject",
+		To:      []string{"Jordan Wright <jmwright798@gmail.com>"},
+		From:    "Jordan Wright <jmwright798@gmail.com>",
+		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("<div dir=\"ltr\">This is a test email with <b>HTML Formatting.</b>\u00a0It also has very long lines so that the content must be wrapped if using quoted-printable decoding.</div>\n"),
+	}
+	raw := []byte(`MIME-Version: 1.0
+Subject: Test Subject
+From: Jordan Wright <jmwright798@gmail.com>
+To: Jordan Wright <jmwright798@gmail.com>
+Content-Type: multipart/alternative; boundary=001a114fb3fc42fd6b051f834280
+
+--001a114fb3fc42fd6b051f834280
+Content-Type: text/plain; charset=UTF-8
+
+This is a test email with HTML Formatting. It also has very long lines so
+that the content must be wrapped if using quoted-printable decoding.
+
+--001a114fb3fc42fd6b051f834280
+Content-Type: text/html; charset=UTF-8
+Content-Transfer-Encoding: quoted-printable
+
+<div dir=3D"ltr">This is a test email with <b>HTML Formatting.</b>=C2=A0It =
+also has very long lines so that the content must be wrapped if using quote=
+d-printable decoding.</div>
+
+--001a114fb3fc42fd6b051f834280--`)
+	e, err := NewEmailFromReader(bytes.NewReader(raw))
+	if err != nil {
+		t.Fatalf("Error creating email %s", err.Error())
+	}
+	if e.Subject != ex.Subject {
+		t.Fatalf("Incorrect subject. %#q != %#q", e.Subject, ex.Subject)
+	}
+	if !bytes.Equal(e.Text, ex.Text) {
+		t.Fatalf("Incorrect text: %#q != %#q", e.Text, ex.Text)
+	}
+	if !bytes.Equal(e.HTML, ex.HTML) {
+		t.Fatalf("Incorrect HTML: %#q != %#q", e.HTML, ex.HTML)
+	}
+	if e.From != ex.From {
+		t.Fatalf("Incorrect \"From\": %#q != %#q", e.From, ex.From)
+	}
+
+}
+
 func ExampleGmail() {
 	e := NewEmail()
 	e.From = "Jordan Wright <test@gmail.com>"
@@ -200,27 +248,3 @@ func Benchmark_base64Wrap(b *testing.B) {
 		base64Wrap(ioutil.Discard, file)
 	}
 }
-
-/*
-func Test_encodeHeader(t *testing.T) {
-	// Plain ASCII (unchanged).
-	subject := "Plain ASCII email subject, !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"
-	expected := []byte("Plain ASCII email subject, !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~")
-
-	b := encodeHeader("Subject", subject)
-	if !bytes.Equal(b, expected) {
-		t.Errorf("encodeHeader generated incorrect results: %#q != %#q", b, expected)
-	}
-
-	// UTF-8 ('q' encoded).
-	subject = "UTF-8 email subject. It can contain é, ñ, or £. Long subject headers will be split in multiple lines!"
-	expected = []byte("=?UTF-8?Q?UTF-8_email_subject._It_c?=\r\n" +
-		" =?UTF-8?Q?an_contain_=C3=A9,_=C3=B1,_or_=C2=A3._Lo?=\r\n" +
-		" =?UTF-8?Q?ng_subject_headers_will_be_split_in_multiple_lines!?=")
-
-	b = encodeHeader("Subject", subject)
-	if !bytes.Equal(b, expected) {
-		t.Errorf("encodeHeader generated incorrect results: %#q != %#q", b, expected)
-	}
-}
-*/