1
0
mirror of https://github.com/jhillyerd/inbucket.git synced 2025-12-21 19:47:03 +00:00

Replace internal MIME with go.enmime

This commit is contained in:
James Hillyerd
2012-11-04 13:25:24 -08:00
parent 24e74936b7
commit afe0d779cb
9 changed files with 8 additions and 468 deletions

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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, "<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
}

View File

@@ -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) {