mirror of
https://github.com/jhillyerd/inbucket.git
synced 2025-12-17 17:47:03 +00:00
Improved message rendering
Added a decodeSection function to mime.go that uses go-qprintable to parse quoted-printable emails or MIME parts, fixes #7 Added a very basic TextToHtml converter to provide nicer rending of text message bodies.
This commit is contained in:
@@ -69,7 +69,7 @@ func (c Mailbox) Show(name string, id string) rev.Result {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return c.RenderError(err)
|
return c.RenderError(err)
|
||||||
}
|
}
|
||||||
body := mime.Text
|
body := template.HTML(inbucket.TextToHtml(mime.Text))
|
||||||
htmlAvailable := mime.Html != ""
|
htmlAvailable := mime.Html != ""
|
||||||
|
|
||||||
c.Response.Out.Header().Set("Expires", "-1")
|
c.Response.Out.Header().Set("Expires", "-1")
|
||||||
|
|||||||
@@ -4,11 +4,12 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"container/list"
|
"container/list"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"github.com/sloonz/go-qprintable"
|
||||||
"io"
|
"io"
|
||||||
"io/ioutil"
|
|
||||||
"mime"
|
"mime"
|
||||||
"mime/multipart"
|
"mime/multipart"
|
||||||
"net/mail"
|
"net/mail"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MIMENodeMatcher func(node *MIMENode) bool
|
type MIMENodeMatcher func(node *MIMENode) bool
|
||||||
@@ -85,7 +86,7 @@ func ParseMIMEBody(mailMsg *mail.Message) (*MIMEBody, error) {
|
|||||||
|
|
||||||
if !IsMIMEMessage(mailMsg) {
|
if !IsMIMEMessage(mailMsg) {
|
||||||
// Parse as text only
|
// Parse as text only
|
||||||
bodyBytes, err := ioutil.ReadAll(mailMsg.Body)
|
bodyBytes, err := decodeSection(mailMsg.Header.Get("Content-Transfer-Encoding"), mailMsg.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -136,6 +137,29 @@ func (m *MIMEBody) String() string {
|
|||||||
return fmt.Sprintf("----TEXT----\n%v\n----HTML----\n%v\n----END----\n", m.Text, m.Html)
|
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 {
|
func parseNodes(parent *MIMENode, reader io.Reader, boundary string) error {
|
||||||
var prevSibling *MIMENode
|
var prevSibling *MIMENode
|
||||||
|
|
||||||
@@ -172,13 +196,12 @@ func parseNodes(parent *MIMENode, reader io.Reader, boundary string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Content is data, allocate a buffer
|
// Content is text or data, decode it
|
||||||
buf := new(bytes.Buffer)
|
data, err := decodeSection(part.Header.Get("Content-Transfer-Encoding"), part)
|
||||||
_, err = buf.ReadFrom(part)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
node.Content = buf.Bytes()
|
node.Content = data
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,28 @@ func TestParseInlineText(t *testing.T) {
|
|||||||
assert.Equal(t, mime.Text, "Test of HTML section")
|
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) {
|
func TestParseInlineHtml(t *testing.T) {
|
||||||
msg := readMessage("html-mime-inline.raw")
|
msg := readMessage("html-mime-inline.raw")
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package inbucket
|
|||||||
import (
|
import (
|
||||||
"crypto/sha1"
|
"crypto/sha1"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html"
|
||||||
"io"
|
"io"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -26,3 +27,11 @@ func HashMailboxName(mailbox string) string {
|
|||||||
return fmt.Sprintf("%x", h.Sum(nil))
|
return fmt.Sprintf("%x", h.Sum(nil))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TextToHtml takes plain text, escapes it and tries to pretty it up for
|
||||||
|
// HTML display
|
||||||
|
func TextToHtml(text string) string {
|
||||||
|
text = html.EscapeString(text)
|
||||||
|
replacer := strings.NewReplacer("\r\n", "<br/>\n", "\r", "<br/>\n", "\n", "<br/>\n")
|
||||||
|
return replacer.Replace(text)
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
package inbucket
|
package inbucket
|
||||||
|
|
||||||
import "testing"
|
import (
|
||||||
|
"github.com/stretchrcom/testify/assert"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
func TestParseMailboxName(t *testing.T) {
|
func TestParseMailboxName(t *testing.T) {
|
||||||
in, out := "MailBOX", "mailbox"
|
in, out := "MailBOX", "mailbox"
|
||||||
@@ -26,3 +29,16 @@ func TestHashMailboxName(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestTextToHtml(t *testing.T) {
|
||||||
|
// Identity
|
||||||
|
assert.Equal(t, TextToHtml("html"), "html")
|
||||||
|
|
||||||
|
// Check it escapes
|
||||||
|
assert.Equal(t, TextToHtml("<html>"), "<html>")
|
||||||
|
|
||||||
|
// Check for linebreaks
|
||||||
|
assert.Equal(t, TextToHtml("line\nbreak"), "line<br/>\nbreak")
|
||||||
|
assert.Equal(t, TextToHtml("line\r\nbreak"), "line<br/>\nbreak")
|
||||||
|
assert.Equal(t, TextToHtml("line\rbreak"), "line<br/>\nbreak")
|
||||||
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,5 +17,5 @@
|
|||||||
<table>
|
<table>
|
||||||
<div id="emailSubject"><h3>{{.message.Subject}}</h3></div>
|
<div id="emailSubject"><h3>{{.message.Subject}}</h3></div>
|
||||||
|
|
||||||
<pre id="emailBody">{{.body}}</pre>
|
<div id="emailBody">{{.body}}</div>
|
||||||
|
|
||||||
|
|||||||
@@ -286,6 +286,7 @@ a:hover {
|
|||||||
|
|
||||||
#emailBody {
|
#emailBody {
|
||||||
color: #000000;
|
color: #000000;
|
||||||
|
margin-top: 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#emailActions {
|
#emailActions {
|
||||||
|
|||||||
79
test-data/quoted-printable-mime.raw
Normal file
79
test-data/quoted-printable-mime.raw
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
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--
|
||||||
Reference in New Issue
Block a user