1
0
mirror of https://github.com/jhillyerd/inbucket.git synced 2025-12-18 10:07:02 +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 package smtpd
import ( import (
"github.com/jhillyerd/go.enmime"
"net/mail" "net/mail"
"time" "time"
) )
@@ -23,11 +24,10 @@ type Message interface {
Date() time.Time Date() time.Time
Subject() string Subject() string
ReadHeader() (msg *mail.Message, err error) 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) ReadRaw() (raw *string, err error)
Append(data []byte) error Append(data []byte) error
Close() error Close() error
Delete() error Delete() error
String() string String() string
} }

View File

@@ -5,6 +5,7 @@ import (
"encoding/gob" "encoding/gob"
"errors" "errors"
"fmt" "fmt"
"github.com/jhillyerd/go.enmime"
"github.com/jhillyerd/inbucket/config" "github.com/jhillyerd/inbucket/config"
"github.com/jhillyerd/inbucket/log" "github.com/jhillyerd/inbucket/log"
"io/ioutil" "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 // 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 // with a free mail.Message containing the Headers, since we had to make one of those
// anyway. // 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()) file, err := os.Open(m.rawPath())
defer file.Close() defer file.Close()
if err != nil { if err != nil {
@@ -250,7 +251,7 @@ func (m *FileMessage) ReadBody() (msg *mail.Message, body *MIMEBody, err error)
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
mime, err := ParseMIMEBody(msg) mime, err := enmime.ParseMIMEBody(msg)
if err != nil { if err != nil {
return nil, nil, err 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 ( import (
"fmt" "fmt"
"github.com/jhillyerd/go.enmime"
"github.com/stretchrcom/testify/mock" "github.com/stretchrcom/testify/mock"
"net/mail" "net/mail"
"testing" "testing"
@@ -130,9 +131,9 @@ func (m *MockMessage) ReadHeader() (msg *mail.Message, err error) {
return args.Get(0).(*mail.Message), args.Error(1) 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() 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) { func (m *MockMessage) ReadRaw() (raw *string, err error) {

View File

@@ -1,54 +0,0 @@
From: James Hillyerd <james@makita.skynet>
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
<html><head></head><body style="word-wrap: break-word; -webkit-nbsp-mode: space; -webkit-line-break: after-white-space; "><font class="Apple-style-span" face="'Comic Sans MS'">Test of HTML section</font><img height="16" width="16" apple-width="yes" apple-height="yes" id="4579722f-d53d-45d0-88bc-f8209a2ca569" src="cid:8B8481A2-25CA-4886-9B5A-8EB9115DD064@skynet"></body></html>
--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--

View File

@@ -1,8 +0,0 @@
Date: Sun, 14 Oct 2012 16:09:01 -0700
To: greg@inbucket.com
From: James Hillyerd <james@hillyerd.com>
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

View File

@@ -1,79 +0,0 @@
Message-ID: <5081A889.3020108@jamehi03lx.noa.com>
Date: Fri, 19 Oct 2012 12:22:49 -0700
From: James Hillyerd <jamehi03@jamehi03lx.noa.com>
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
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=ISO-8859-1">
</head>
<body bgcolor="#FFFFFF" text="#000000">
<div id="lipsum">
<p>
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.
</p>
<p>
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.
</p>
</div>
</body>
</html>
--------------020203040006070307010003--

View File

@@ -1,17 +0,0 @@
From: James Hillyerd <james@makita.skynet>
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=