Skip to content

Commit

Permalink
Added NewEmailFromReader method and tests
Browse files Browse the repository at this point in the history
  • Loading branch information
jordan-wright committed Sep 13, 2015
1 parent b7c768d commit 381271e
Show file tree
Hide file tree
Showing 2 changed files with 161 additions and 24 deletions.
113 changes: 113 additions & 0 deletions email.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package email

import (
"bufio"
"bytes"
"encoding/base64"
"errors"
Expand All @@ -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
Expand All @@ -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.
Expand Down
72 changes: 48 additions & 24 deletions email_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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>"
Expand Down Expand Up @@ -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)
}
}
*/

0 comments on commit 381271e

Please sign in to comment.