From afe0d779cbe740bf839f8418719004968ec90e6c Mon Sep 17 00:00:00 2001 From: James Hillyerd Date: Sun, 4 Nov 2012 13:25:24 -0800 Subject: [PATCH] Replace internal MIME with go.enmime --- smtpd/datastore.go | 4 +- smtpd/filestore.go | 5 +- smtpd/mime.go | 209 ---------------------------- smtpd/mime_test.go | 95 ------------- smtpd/retention_test.go | 5 +- test-data/html-mime-inline.raw | 54 ------- test-data/non-mime.raw | 8 -- test-data/quoted-printable-mime.raw | 79 ----------- test-data/quoted-printable.raw | 17 --- 9 files changed, 8 insertions(+), 468 deletions(-) delete mode 100644 smtpd/mime.go delete mode 100644 smtpd/mime_test.go delete mode 100644 test-data/html-mime-inline.raw delete mode 100644 test-data/non-mime.raw delete mode 100644 test-data/quoted-printable-mime.raw delete mode 100644 test-data/quoted-printable.raw diff --git a/smtpd/datastore.go b/smtpd/datastore.go index d81947f..cd770de 100644 --- a/smtpd/datastore.go +++ b/smtpd/datastore.go @@ -1,6 +1,7 @@ package smtpd import ( + "github.com/jhillyerd/go.enmime" "net/mail" "time" ) @@ -23,11 +24,10 @@ type Message interface { Date() time.Time Subject() string ReadHeader() (msg *mail.Message, err error) - ReadBody() (msg *mail.Message, body *MIMEBody, err error) + ReadBody() (msg *mail.Message, body *enmime.MIMEBody, err error) ReadRaw() (raw *string, err error) Append(data []byte) error Close() error Delete() error String() string } - diff --git a/smtpd/filestore.go b/smtpd/filestore.go index 221ccbb..67e8db2 100644 --- a/smtpd/filestore.go +++ b/smtpd/filestore.go @@ -5,6 +5,7 @@ import ( "encoding/gob" "errors" "fmt" + "github.com/jhillyerd/go.enmime" "github.com/jhillyerd/inbucket/config" "github.com/jhillyerd/inbucket/log" "io/ioutil" @@ -239,7 +240,7 @@ func (m *FileMessage) ReadHeader() (msg *mail.Message, err error) { // ReadBody opens the .raw portion of a Message and returns a MIMEBody object, along // with a free mail.Message containing the Headers, since we had to make one of those // anyway. -func (m *FileMessage) ReadBody() (msg *mail.Message, body *MIMEBody, err error) { +func (m *FileMessage) ReadBody() (msg *mail.Message, body *enmime.MIMEBody, err error) { file, err := os.Open(m.rawPath()) defer file.Close() if err != nil { @@ -250,7 +251,7 @@ func (m *FileMessage) ReadBody() (msg *mail.Message, body *MIMEBody, err error) if err != nil { return nil, nil, err } - mime, err := ParseMIMEBody(msg) + mime, err := enmime.ParseMIMEBody(msg) if err != nil { return nil, nil, err } diff --git a/smtpd/mime.go b/smtpd/mime.go deleted file mode 100644 index 39771f1..0000000 --- a/smtpd/mime.go +++ /dev/null @@ -1,209 +0,0 @@ -package smtpd - -import ( - "bytes" - "container/list" - "fmt" - "github.com/sloonz/go-qprintable" - "io" - "mime" - "mime/multipart" - "net/mail" - "strings" -) - -type MIMENodeMatcher func(node *MIMENode) bool - -type MIMENode struct { - Parent *MIMENode - FirstChild *MIMENode - NextSibling *MIMENode - Type string - Content []byte -} - -type MIMEBody struct { - Text string - Html string - Root *MIMENode -} - -func NewMIMENode(parent *MIMENode, contentType string) *MIMENode { - return &MIMENode{Parent: parent, Type: contentType} -} - -func (n *MIMENode) BreadthFirstSearch(matcher MIMENodeMatcher) *MIMENode { - q := list.New() - q.PushBack(n) - - // Push children onto queue and attempt to match in that order - for q.Len() > 0 { - e := q.Front() - n := e.Value.(*MIMENode) - if matcher(n) { - return n - } - q.Remove(e) - c := n.FirstChild - for c != nil { - q.PushBack(c) - c = c.NextSibling - } - } - - return nil -} - -func (n *MIMENode) String() string { - children := "" - siblings := "" - if n.FirstChild != nil { - children = n.FirstChild.String() - } - if n.NextSibling != nil { - siblings = n.NextSibling.String() - } - return fmt.Sprintf("[%v %v] %v", n.Type, children, siblings) -} - -func IsMIMEMessage(mailMsg *mail.Message) bool { - // Parse top-level multipart - ctype := mailMsg.Header.Get("Content-Type") - mediatype, _, err := mime.ParseMediaType(ctype) - if err != nil { - return false - } - switch mediatype { - case "multipart/alternative": - return true - } - - return false -} - -func ParseMIMEBody(mailMsg *mail.Message) (*MIMEBody, error) { - mimeMsg := new(MIMEBody) - - if !IsMIMEMessage(mailMsg) { - // Parse as text only - bodyBytes, err := decodeSection(mailMsg.Header.Get("Content-Transfer-Encoding"), mailMsg.Body) - if err != nil { - return nil, err - } - mimeMsg.Text = string(bodyBytes) - } else { - // Parse top-level multipart - ctype := mailMsg.Header.Get("Content-Type") - mediatype, params, err := mime.ParseMediaType(ctype) - if err != nil { - return nil, err - } - switch mediatype { - case "multipart/alternative": - // Good - default: - return nil, fmt.Errorf("Unknown mediatype: %v", mediatype) - } - boundary := params["boundary"] - if boundary == "" { - return nil, fmt.Errorf("Unable to locate boundary param in Content-Type header") - } - - // Root Node of our tree - root := NewMIMENode(nil, mediatype) - err = parseNodes(root, mailMsg.Body, boundary) - - // Locate text body - match := root.BreadthFirstSearch(func(node *MIMENode) bool { - return node.Type == "text/plain" - }) - if match != nil { - mimeMsg.Text = string(match.Content) - } - - // Locate HTML body - match = root.BreadthFirstSearch(func(node *MIMENode) bool { - return node.Type == "text/html" - }) - if match != nil { - mimeMsg.Html = string(match.Content) - } - } - - return mimeMsg, nil -} - -func (m *MIMEBody) String() string { - return fmt.Sprintf("----TEXT----\n%v\n----HTML----\n%v\n----END----\n", m.Text, m.Html) -} - -// decodeSection attempts to decode the data from reader using the algorithm listed in -// the Content-Transfer-Encoding header, returning the raw data if it does not know -// the encoding type. -func decodeSection(encoding string, reader io.Reader) ([]byte, error) { - switch strings.ToLower(encoding) { - case "quoted-printable": - decoder := qprintable.NewDecoder(qprintable.WindowsTextEncoding, reader) - buf := new(bytes.Buffer) - _, err := buf.ReadFrom(decoder) - if err != nil { - return nil, err - } - return buf.Bytes(), nil - } - // Don't know this type, just return bytes - buf := new(bytes.Buffer) - _, err := buf.ReadFrom(reader) - if err != nil { - return nil, err - } - return buf.Bytes(), nil -} - -func parseNodes(parent *MIMENode, reader io.Reader, boundary string) error { - var prevSibling *MIMENode - - // Loop over MIME parts - mr := multipart.NewReader(reader, boundary) - for { - part, err := mr.NextPart() - if err != nil { - if err == io.EOF { - // This is a clean end-of-message signal - break - } - return err - } - mediatype, params, err := mime.ParseMediaType(part.Header.Get("Content-Type")) - if err != nil { - return err - } - - // Insert ourselves into tree - node := NewMIMENode(parent, mediatype) - if prevSibling != nil { - prevSibling.NextSibling = node - } else { - parent.FirstChild = node - } - prevSibling = node - - boundary := params["boundary"] - if boundary != "" { - // Content is another multipart - err = parseNodes(node, part, boundary) - if err != nil { - return err - } - } else { - // Content is text or data, decode it - data, err := decodeSection(part.Header.Get("Content-Transfer-Encoding"), part) - if err != nil { - return err - } - node.Content = data - } - } - - return nil -} diff --git a/smtpd/mime_test.go b/smtpd/mime_test.go deleted file mode 100644 index feac9ea..0000000 --- a/smtpd/mime_test.go +++ /dev/null @@ -1,95 +0,0 @@ -package smtpd - -import ( - "bufio" - "fmt" - "github.com/stretchrcom/testify/assert" - "net/mail" - "os" - "path/filepath" - "testing" -) - -func TestIdentifyNonMime(t *testing.T) { - msg := readMessage("non-mime.raw") - assert.False(t, IsMIMEMessage(msg), "Failed to identify non-MIME message") -} - -func TestIdentifyMime(t *testing.T) { - msg := readMessage("html-mime-inline.raw") - assert.True(t, IsMIMEMessage(msg), "Failed to identify MIME message") -} - -func TestParseNonMime(t *testing.T) { - msg := readMessage("non-mime.raw") - - mime, err := ParseMIMEBody(msg) - if err != nil { - t.Fatalf("Failed to parse non-MIME: %v", err) - } - - assert.Contains(t, mime.Text, "This is a test mailing") -} - -func TestParseInlineText(t *testing.T) { - msg := readMessage("html-mime-inline.raw") - - mime, err := ParseMIMEBody(msg) - if err != nil { - t.Fatalf("Failed to parse MIME: %v", err) - } - - assert.Equal(t, mime.Text, "Test of HTML section") -} - -func TestParseQuotedPrintable(t *testing.T) { - msg := readMessage("quoted-printable.raw") - - mime, err := ParseMIMEBody(msg) - if err != nil { - t.Fatalf("Failed to parse MIME: %v", err) - } - - assert.Contains(t, mime.Text, "Phasellus sit amet arcu") -} - -func TestParseQuotedPrintableMime(t *testing.T) { - msg := readMessage("quoted-printable-mime.raw") - - mime, err := ParseMIMEBody(msg) - if err != nil { - t.Fatalf("Failed to parse MIME: %v", err) - } - - assert.Contains(t, mime.Text, "Nullam venenatis ante") -} - -func TestParseInlineHtml(t *testing.T) { - msg := readMessage("html-mime-inline.raw") - - mime, err := ParseMIMEBody(msg) - if err != nil { - t.Fatalf("Failed to parse MIME: %v", err) - } - - assert.Contains(t, mime.Html, "") - assert.Contains(t, mime.Html, "Test of HTML section") -} - -// readMessage is a test utility function to fetch a mail.Message object. -func readMessage(filename string) *mail.Message { - // Open test email for parsing - raw, err := os.Open(filepath.Join("..", "test-data", filename)) - if err != nil { - panic(fmt.Sprintf("Failed to open test data: %v", err)) - } - - // Parse email into a mail.Message object like we do - reader := bufio.NewReader(raw) - msg, err := mail.ReadMessage(reader) - if err != nil { - panic(fmt.Sprintf("Failed to read message: %v", err)) - } - - return msg -} diff --git a/smtpd/retention_test.go b/smtpd/retention_test.go index bf0113d..e2b9210 100644 --- a/smtpd/retention_test.go +++ b/smtpd/retention_test.go @@ -2,6 +2,7 @@ package smtpd import ( "fmt" + "github.com/jhillyerd/go.enmime" "github.com/stretchrcom/testify/mock" "net/mail" "testing" @@ -130,9 +131,9 @@ func (m *MockMessage) ReadHeader() (msg *mail.Message, err error) { return args.Get(0).(*mail.Message), args.Error(1) } -func (m *MockMessage) ReadBody() (msg *mail.Message, body *MIMEBody, err error) { +func (m *MockMessage) ReadBody() (msg *mail.Message, body *enmime.MIMEBody, err error) { args := m.Called() - return args.Get(0).(*mail.Message), args.Get(1).(*MIMEBody), args.Error(2) + return args.Get(0).(*mail.Message), args.Get(1).(*enmime.MIMEBody), args.Error(2) } func (m *MockMessage) ReadRaw() (raw *string, err error) { diff --git a/test-data/html-mime-inline.raw b/test-data/html-mime-inline.raw deleted file mode 100644 index 72573a3..0000000 --- a/test-data/html-mime-inline.raw +++ /dev/null @@ -1,54 +0,0 @@ -From: James Hillyerd -Content-Type: multipart/alternative; boundary="Apple-Mail=_E091454E-BCFA-43B4-99C0-678AEC9868D6" -Subject: MIME test 1 -Date: Sat, 13 Oct 2012 15:33:07 -0700 -Message-Id: <4E2E5A48-1A2C-4450-8663-D41B451DA93E@makita.skynet> -To: greg@nobody.com -Mime-Version: 1.0 (Apple Message framework v1283) -X-Mailer: Apple Mail (2.1283) - - ---Apple-Mail=_E091454E-BCFA-43B4-99C0-678AEC9868D6 -Content-Transfer-Encoding: 7bit -Content-Type: text/plain; - charset=us-ascii - -Test of HTML section ---Apple-Mail=_E091454E-BCFA-43B4-99C0-678AEC9868D6 -Content-Type: multipart/related; - type="text/html"; - boundary="Apple-Mail=_D2ABE25A-F0FE-404E-94EE-D98BD23448D5" - - ---Apple-Mail=_D2ABE25A-F0FE-404E-94EE-D98BD23448D5 -Content-Transfer-Encoding: 7bit -Content-Type: text/html; - charset=us-ascii - -Test of HTML section ---Apple-Mail=_D2ABE25A-F0FE-404E-94EE-D98BD23448D5 -Content-Transfer-Encoding: base64 -Content-Disposition: inline; - filename=favicon.png -Content-Type: image/png; - x-unix-mode=0644; - name="favicon.png" -Content-Id: <8B8481A2-25CA-4886-9B5A-8EB9115DD064@skynet> - -iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJ -bWFnZVJlYWR5ccllPAAAAlFJREFUeNqUU8tOFEEUPVVdNV3dPe8xYRBnjGhmBgKjKzCIiQvBoIaN -bly5Z+PSv3Aj7DSiP2B0rwkLGVdGgxITSCRIJGSMEQWZR3eVt5sEFBgTb/dN1yvnnHtPNTPG4Pqd -HgCMXnPRSZrpSuH8vUJu4DE4rYHDGAZDX62BZttHqTiIayM3gGiXQsgYLEvATaqxU+dy1U13YXap -XptpNHY8iwn8KyIAzm1KBdtRZWErpI5lEWTXp5Z/vHpZ3/wyKKwYGGOdAYwR0EZwoezTYApBEIOb -yELl/aE1/83cp40Pt5mxqCKrE4Ck+mVWKKcI5tA8BLEhRBKJLjez6a7MLq7XZtp+yyOawwCBtkiB -VZDKzRk4NN7NQBMYPHiZDFhXY+p9ff7F961vVcnl4R5I2ykJ5XFN7Ab7Gc61VoipNBKF+PDyztu5 -lfrSLT/wIwCxq0CAGtXHZTzqR2jtwQiXONma6hHpj9sLT7YaPxfTXuZdBGA02Wi7FS48YiTfj+i2 -NhqtdhP5RC8mh2/Op7y0v6eAcWVLFT8D7kWX5S9mepp+C450MV6aWL1cGnvkxbwHtLW2B9AOkLeU -d9KEDuh9fl/7CEj7YH5g+3r/lWfF9In7tPz6T4IIwBJOr1SJyIGQMZQbsh5P9uBq5VJtqHh2mo49 -pdw5WFoEwKWqWHacaWOjQXWGcifKo6vj5RGS6zykI587XeUIQDqJSmAp+lE4qt19W5P9o8+Lma5D -cjsC8JiT607lMVkdqQ0Vyh3lHhmh52tfNy78ajXv0rgYzv8nfwswANuk+7sD/Q0aAAAAAElFTkSu -QmCC - ---Apple-Mail=_D2ABE25A-F0FE-404E-94EE-D98BD23448D5-- - ---Apple-Mail=_E091454E-BCFA-43B4-99C0-678AEC9868D6-- diff --git a/test-data/non-mime.raw b/test-data/non-mime.raw deleted file mode 100644 index ce1f0af..0000000 --- a/test-data/non-mime.raw +++ /dev/null @@ -1,8 +0,0 @@ -Date: Sun, 14 Oct 2012 16:09:01 -0700 -To: greg@inbucket.com -From: James Hillyerd -Subject: test Sun, 14 Oct 2012 16:09:01 -0700 -X-Mailer: swaks v20120320.0 jetmore.org/john/code/swaks/ - -This is a test mailing - diff --git a/test-data/quoted-printable-mime.raw b/test-data/quoted-printable-mime.raw deleted file mode 100644 index 3f543bd..0000000 --- a/test-data/quoted-printable-mime.raw +++ /dev/null @@ -1,79 +0,0 @@ -Message-ID: <5081A889.3020108@jamehi03lx.noa.com> -Date: Fri, 19 Oct 2012 12:22:49 -0700 -From: James Hillyerd -User-Agent: Mozilla/5.0 (Windows NT 6.1; WOW64; rv:16.0) Gecko/20121010 Thunderbird/16.0.1 -MIME-Version: 1.0 -To: greg@inbucket.com -Subject: MIME Quoted Printable -Content-Type: multipart/alternative; - boundary="------------020203040006070307010003" - -This is a multi-part message in MIME format. ---------------020203040006070307010003 -Content-Type: text/plain; charset=ISO-8859-1 -Content-Transfer-Encoding: quoted-printable - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam = -venenatis ante fermentum justo varius hendrerit. Sed adipiscing = -adipiscing nisi at placerat. Sed luctus neque pellentesque sem laoreet = -dapibus. Class aptent taciti sociosqu ad litora torquent per conubia = -nostra, per inceptos himenaeos. In eu scelerisque nibh. Fusce faucibus, = -nisl vel tincidunt sodales, quam est condimentum lorem, vitae semper = -lacus nisl vel lacus. Vestibulum vitae iaculis urna. Donec pellentesque = -pellentesque ipsum sit amet suscipit. Ut aliquam vestibulum justo sit = -amet congue. Mauris nisl lacus, varius a ultrices id, suscipit mattis = -libero. - -Donec iaculis dapibus purus in accumsan. Cras faucibus, orci dictum = -molestie congue, neque magna adipiscing lacus, ut laoreet sapien ante et = -mi. Suspendisse potenti. Nullam sed dui magna. Nullam vel purus augue, = -imperdiet vehicula purus. Lorem ipsum dolor sit amet, consectetur = -adipiscing elit. In aliquet, ante at tincidunt ultricies, arcu dolor = -rutrum odio, quis tempus lacus leo a quam. Fusce hendrerit, urna sed = -elementum pulvinar, dolor sem imperdiet arcu, ut ornare erat metus sit = -amet diam. Duis sagittis libero ut metus vulputate dictum. Etiam eget = -augue dolor, in lacinia felis. Nulla facilisi. Proin sollicitudin = -laoreet vehicula. Praesent non nibh odio. - - ---------------020203040006070307010003 -Content-Type: text/html; charset=ISO-8859-1 -Content-Transfer-Encoding: 7bit - - - - - - - -
-

- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam - venenatis ante fermentum justo varius hendrerit. Sed adipiscing - adipiscing nisi at placerat. Sed luctus neque pellentesque sem - laoreet dapibus. Class aptent taciti sociosqu ad litora torquent - per conubia nostra, per inceptos himenaeos. In eu scelerisque - nibh. Fusce faucibus, nisl vel tincidunt sodales, quam est - condimentum lorem, vitae semper lacus nisl vel lacus. Vestibulum - vitae iaculis urna. Donec pellentesque pellentesque ipsum sit - amet suscipit. Ut aliquam vestibulum justo sit amet congue. - Mauris nisl lacus, varius a ultrices id, suscipit mattis libero. -

-

- Donec iaculis dapibus purus in accumsan. Cras faucibus, orci - dictum molestie congue, neque magna adipiscing lacus, ut laoreet - sapien ante et mi. Suspendisse potenti. Nullam sed dui magna. - Nullam vel purus augue, imperdiet vehicula purus. Lorem ipsum - dolor sit amet, consectetur adipiscing elit. In aliquet, ante at - tincidunt ultricies, arcu dolor rutrum odio, quis tempus lacus - leo a quam. Fusce hendrerit, urna sed elementum pulvinar, dolor - sem imperdiet arcu, ut ornare erat metus sit amet diam. Duis - sagittis libero ut metus vulputate dictum. Etiam eget augue - dolor, in lacinia felis. Nulla facilisi. Proin sollicitudin - laoreet vehicula. Praesent non nibh odio. -

-
- - - ---------------020203040006070307010003-- diff --git a/test-data/quoted-printable.raw b/test-data/quoted-printable.raw deleted file mode 100644 index c1ad8cd..0000000 --- a/test-data/quoted-printable.raw +++ /dev/null @@ -1,17 +0,0 @@ -From: James Hillyerd -Content-Type: text/plain; charset=us-ascii -Content-Transfer-Encoding: quoted-printable -Subject: Quoted Printable -Date: Thu, 18 Oct 2012 22:48:39 -0700 -Message-Id: <07B7061D-2676-487E-942E-C341CE4D13DC@makita.skynet> -To: greg@inbucket -Mime-Version: 1.0 (Apple Message framework v1283) -X-Mailer: Apple Mail (2.1283) - -Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus sit = -amet arcu non lacus porta faucibus. Nulla gravida tempus rutrum. = -Maecenas vehicula cursus libero sed faucibus. Morbi iaculis interdum = -lacus, eget porta turpis ultrices id. Nulla sit amet massa mauris. Morbi = -augue tellus, pharetra a varius at, dignissim eget orci. Nulla molestie = -interdum tortor, id tincidunt purus lacinia ac. Integer sodales velit = -sed neque faucibus egestas eu vel dolor.=20=