diff --git a/app/controllers/mailbox.go b/app/controllers/mailbox.go index 389c136..45d1777 100644 --- a/app/controllers/mailbox.go +++ b/app/controllers/mailbox.go @@ -69,7 +69,7 @@ func (c Mailbox) Show(name string, id string) rev.Result { if err != nil { return c.RenderError(err) } - body := mime.Text + body := template.HTML(inbucket.TextToHtml(mime.Text)) htmlAvailable := mime.Html != "" c.Response.Out.Header().Set("Expires", "-1") diff --git a/app/inbucket/mime.go b/app/inbucket/mime.go index 6142a1f..66f4738 100644 --- a/app/inbucket/mime.go +++ b/app/inbucket/mime.go @@ -4,11 +4,12 @@ import ( "bytes" "container/list" "fmt" + "github.com/sloonz/go-qprintable" "io" - "io/ioutil" "mime" "mime/multipart" "net/mail" + "strings" ) type MIMENodeMatcher func(node *MIMENode) bool @@ -85,7 +86,7 @@ func ParseMIMEBody(mailMsg *mail.Message) (*MIMEBody, error) { if !IsMIMEMessage(mailMsg) { // Parse as text only - bodyBytes, err := ioutil.ReadAll(mailMsg.Body) + bodyBytes, err := decodeSection(mailMsg.Header.Get("Content-Transfer-Encoding"), mailMsg.Body) if err != nil { 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) } +// 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 @@ -172,13 +196,12 @@ func parseNodes(parent *MIMENode, reader io.Reader, boundary string) error { return err } } else { - // Content is data, allocate a buffer - buf := new(bytes.Buffer) - _, err = buf.ReadFrom(part) + // Content is text or data, decode it + data, err := decodeSection(part.Header.Get("Content-Transfer-Encoding"), part) if err != nil { return err } - node.Content = buf.Bytes() + node.Content = data } } diff --git a/app/inbucket/mime_test.go b/app/inbucket/mime_test.go index 743344f..b2e1e4a 100644 --- a/app/inbucket/mime_test.go +++ b/app/inbucket/mime_test.go @@ -42,6 +42,28 @@ func TestParseInlineText(t *testing.T) { 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") diff --git a/app/inbucket/utils.go b/app/inbucket/utils.go index 8ae9130..85047a0 100644 --- a/app/inbucket/utils.go +++ b/app/inbucket/utils.go @@ -3,6 +3,7 @@ package inbucket import ( "crypto/sha1" "fmt" + "html" "io" "strings" ) @@ -26,3 +27,11 @@ func HashMailboxName(mailbox string) string { 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", "
\n", "\r", "
\n", "\n", "
\n") + return replacer.Replace(text) +} + diff --git a/app/inbucket/utils_test.go b/app/inbucket/utils_test.go index af12e16..0d6f9f3 100644 --- a/app/inbucket/utils_test.go +++ b/app/inbucket/utils_test.go @@ -1,28 +1,44 @@ package inbucket -import "testing" +import ( + "github.com/stretchrcom/testify/assert" + "testing" +) func TestParseMailboxName(t *testing.T) { - in, out := "MailBOX", "mailbox" - if x := ParseMailboxName(in); x != out { - t.Errorf("ParseMailboxName(%v) = %v, want %v", in, x, out) - } + in, out := "MailBOX", "mailbox" + if x := ParseMailboxName(in); x != out { + t.Errorf("ParseMailboxName(%v) = %v, want %v", in, x, out) + } - in, out = "MailBox@Host.Com", "mailbox" - if x := ParseMailboxName(in); x != out { - t.Errorf("ParseMailboxName(%v) = %v, want %v", in, x, out) - } + in, out = "MailBox@Host.Com", "mailbox" + if x := ParseMailboxName(in); x != out { + t.Errorf("ParseMailboxName(%v) = %v, want %v", in, x, out) + } - in, out = "Mail+extra@Host.Com", "mail" - if x := ParseMailboxName(in); x != out { - t.Errorf("ParseMailboxName(%v) = %v, want %v", in, x, out) - } + in, out = "Mail+extra@Host.Com", "mail" + if x := ParseMailboxName(in); x != out { + t.Errorf("ParseMailboxName(%v) = %v, want %v", in, x, out) + } } func TestHashMailboxName(t *testing.T) { - in, out := "mail", "1d6e1cf70ec6f9ab28d3ea4b27a49a77654d370e" - if x := HashMailboxName(in); x != out { - t.Errorf("HashMailboxName(%v) = %v, want %v", in, x, out) - } + in, out := "mail", "1d6e1cf70ec6f9ab28d3ea4b27a49a77654d370e" + if x := HashMailboxName(in); x != out { + t.Errorf("HashMailboxName(%v) = %v, want %v", in, x, out) + } +} + +func TestTextToHtml(t *testing.T) { + // Identity + assert.Equal(t, TextToHtml("html"), "html") + + // Check it escapes + assert.Equal(t, TextToHtml(""), "<html>") + + // Check for linebreaks + assert.Equal(t, TextToHtml("line\nbreak"), "line
\nbreak") + assert.Equal(t, TextToHtml("line\r\nbreak"), "line
\nbreak") + assert.Equal(t, TextToHtml("line\rbreak"), "line
\nbreak") } diff --git a/app/views/Mailbox/Show.html b/app/views/Mailbox/Show.html index 53a5367..116debd 100644 --- a/app/views/Mailbox/Show.html +++ b/app/views/Mailbox/Show.html @@ -17,5 +17,5 @@

{{.message.Subject}}

-
{{.body}}
+
{{.body}}
diff --git a/public/stylesheets/main.css b/public/stylesheets/main.css index 0264dc1..77b3505 100644 --- a/public/stylesheets/main.css +++ b/public/stylesheets/main.css @@ -286,6 +286,7 @@ a:hover { #emailBody { color: #000000; + margin-top: 15px; } #emailActions { diff --git a/test-data/quoted-printable-mime.raw b/test-data/quoted-printable-mime.raw new file mode 100644 index 0000000..3f543bd --- /dev/null +++ b/test-data/quoted-printable-mime.raw @@ -0,0 +1,79 @@ +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--