Repository: jhillyerd/enmime
Pull request #88 (2018-11-15 22:24:15)

ensure that media parameters with special characters are quoted

coveralls: 
[![Coverage Status](https://coveralls.io/builds/20134183/badge)](https://coveralls.io/builds/20134183)

Coverage decreased (-0.2%) to 85.901% when pulling **aac5c8ec6d92962e678ba5353f3ac8a6ee7c9005 on requaos:requaos/unquotedspecials** into **9600eed1a88a2736d1eeb40d3f7d4e519638f9a7 on jhillyerd:develop**.

requaos: WIP: still sorting out the precedence of duplicate parameters, missing semicolon and lack of quotes around strings with special characters therein
requaos: Ready for review

Patch:
diff --git a/.gitignore b/.gitignore
index 67a5049..4d1c93f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,6 +21,9 @@ _testmain.go
 
 *.exe
 
+# goland ide
+.idea
+
 # vim swp files
 *.swp
 
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1e36baf..19c8c82 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,17 @@ Change Log
 All notable changes to this project will be documented in this file.
 This project adheres to [Semantic Versioning](http://semver.org/).
 
+## [Unreleased]
+
+### Added
+- Override declared character set if another is detected with high confidence
+  (thanks to nerdlich.)
+
+### Changed
+- Use iso-8859-1 character map when implicitly declared (thanks to requaos.)
+- Treat "inline" disposition as message content, not attachment unless it is
+  accompanied by parameters (e.g. a filename, thanks to requaos.)
+
 ## [0.3.0] - 2018-11-01
 
 ### Added
diff --git a/detect.go b/detect.go
index fa013ec..955ba77 100644
--- a/detect.go
+++ b/detect.go
@@ -29,9 +29,9 @@ func detectMultipartMessage(root *Part) bool {
 //  - Content-Disposition: inline; filename="frog.jpg"
 //  - Content-Type: attachment; filename="frog.jpg"
 func detectAttachmentHeader(header textproto.MIMEHeader) bool {
-	mediatype, _, _ := parseMediaType(header.Get(hnContentDisposition))
+	mediatype, params, _ := parseMediaType(header.Get(hnContentDisposition))
 	if strings.ToLower(mediatype) == cdAttachment ||
-		strings.ToLower(mediatype) == cdInline {
+		(strings.ToLower(mediatype) == cdInline && len(params) > 0) {
 		return true
 	}
 
diff --git a/envelope.go b/envelope.go
index f7f2956..16742ad 100644
--- a/envelope.go
+++ b/envelope.go
@@ -254,15 +254,6 @@ func parseTextOnlyBody(root *Part, e *Envelope) error {
 			// Converted from charset in HTML
 			return nil
 		}
-
-		// Use charset found in header
-		if convHTML, err := coding.ConvertToUTF8String(charset, root.Content); err == nil {
-			// Successful conversion
-			e.HTML = convHTML
-		} else {
-			// Conversion failed
-			root.addWarning(ErrorCharsetConversion, err.Error())
-		}
 	} else {
 		e.Text = string(root.Content)
 	}
diff --git a/error.go b/error.go
index 558c042..81b2148 100644
--- a/error.go
+++ b/error.go
@@ -19,6 +19,8 @@ const (
 	ErrorContentEncoding = "Content Encoding"
 	// ErrorPlainTextFromHTML name.
 	ErrorPlainTextFromHTML = "Plain Text from HTML"
+	// ErrorCharsetDeclaration name.
+	ErrorCharsetDeclaration = "Character Set Declaration Mismatch"
 )
 
 // Error describes an error encountered while parsing.
diff --git a/go.mod b/go.mod
index 188131d..b33cdaa 100644
--- a/go.mod
+++ b/go.mod
@@ -2,6 +2,7 @@ module github.com/jhillyerd/enmime
 
 require (
 	github.com/go-test/deep v1.0.1
+	github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561
 	github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0
 	github.com/mattn/go-runewidth v0.0.3 // indirect
 	github.com/olekukonko/tablewriter v0.0.0-20180912035003-be2c049b30cc // indirect
diff --git a/go.sum b/go.sum
index 53d2a29..8ec97c7 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,7 @@
 github.com/go-test/deep v1.0.1 h1:UQhStjbkDClarlmv0am7OXXO4/GaPdCGiUiMTvi28sg=
 github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
+github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561 h1:aBzukfDxQlCTVS0NBUjI5YA3iVeaZ9Tb5PxNrrIP1xs=
+github.com/gogs/chardet v0.0.0-20150115103509-2404f7772561/go.mod h1:Pcatq5tYkCW2Q6yrR2VRHlbHpZ/R4/7qyL1TCF7vl14=
 github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0 h1:xqgexXAGQgY3HAjNPSaCqn5Aahbo5TKsmhp8VRfr1iQ=
 github.com/jaytaylor/html2text v0.0.0-20180606194806-57d518f124b0/go.mod h1:CVKlgaMiht+LXvHG173ujK6JUhZXKb2u/BQtjPDIvyk=
 github.com/mattn/go-runewidth v0.0.3 h1:a+kO+98RDGEfo6asOGMmpodZq4FNtnGP54yps8BzLR4=
diff --git a/header.go b/header.go
index 85b9f37..2240b48 100644
--- a/header.go
+++ b/header.go
@@ -4,6 +4,7 @@ import (
 	"bufio"
 	"bytes"
 	"errors"
+	"fmt"
 	"io"
 	"mime"
 	"net/textproto"
@@ -201,7 +202,11 @@ func parseMediaType(ctype string) (mtype string, params map[string]string, err e
 			}
 			mtype, params, err = mime.ParseMediaType(mctype)
 			if err != nil {
-				return "", nil, err
+				// If the media parameter has special characters, ensure that it is quoted
+				mtype, params, err = mime.ParseMediaType(fixUnquotedSpecials(mctype))
+				if err != nil {
+					return "", nil, err
+				}
 			}
 		}
 	}
@@ -216,7 +221,7 @@ func fixMangledMediaType(mtype, sep string) string {
 	}
 	parts := strings.Split(mtype, sep)
 	mtype = ""
-	for _, p := range parts {
+	for i, p := range parts {
 		if strings.Contains(p, "=") {
 			pair := strings.Split(p, "=")
 			if strings.Contains(mtype, pair[0]+"=") {
@@ -224,11 +229,29 @@ func fixMangledMediaType(mtype, sep string) string {
 				continue
 			}
 		}
-		mtype += p + ";"
+		mtype += p
+		// only terminate with semicolon if not the last parameter and if it doesn't already have a semicolon
+		if i != len(parts)-1 && !strings.HasSuffix(mtype, ";") {
+			mtype += ";"
+		}
+	}
+	if strings.HasSuffix(mtype, ";") {
+		mtype = mtype[:len(mtype)-1]
 	}
 	return mtype
 }
 
+// fixUnquotedSpecials as defined in https://www.w3.org/Protocols/rfc1341/4_Content-Type.html
+func fixUnquotedSpecials(s string) string {
+	if strings.Contains(s, "name=") {
+		nameSplit := strings.SplitAfter(s, "name=")
+		if strings.ContainsAny(nameSplit[1], "()<>@,;:\\/[]?.=") && !strings.HasSuffix(nameSplit[1], "\"") {
+			return fmt.Sprintf("%s\"%s\"", nameSplit[0], nameSplit[1])
+		}
+	}
+	return s
+}
+
 // Detects a RFC-822 linear-white-space, passed to strings.FieldsFunc
 func whiteSpaceRune(r rune) bool {
 	return r == ' ' || r == '\t' || r == '\r' || r == '\n'
diff --git a/internal/coding/charsets.go b/internal/coding/charsets.go
index 731776a..2481ecf 100644
--- a/internal/coding/charsets.go
+++ b/internal/coding/charsets.go
@@ -155,13 +155,13 @@ var encodings = map[string]struct {
 	"cp819":               {charmap.Windows1252, "windows-1252"},
 	"csisolatin1":         {charmap.Windows1252, "windows-1252"},
 	"ibm819":              {charmap.Windows1252, "windows-1252"},
-	"iso-8859-1":          {charmap.Windows1252, "windows-1252"},
+	"iso-8859-1":          {charmap.ISO8859_1, "iso-8859-1"},
 	"iso-ir-100":          {charmap.Windows1252, "windows-1252"},
-	"iso8859-1":           {charmap.Windows1252, "windows-1252"},
-	"iso8859_1":           {charmap.Windows1252, "windows-1252"},
-	"iso88591":            {charmap.Windows1252, "windows-1252"},
-	"iso_8859-1":          {charmap.Windows1252, "windows-1252"},
-	"iso_8859-1:1987":     {charmap.Windows1252, "windows-1252"},
+	"iso8859-1":           {charmap.ISO8859_1, "iso-8859-1"},
+	"iso8859_1":           {charmap.ISO8859_1, "iso-8859-1"},
+	"iso88591":            {charmap.ISO8859_1, "iso-8859-1"},
+	"iso_8859-1":          {charmap.ISO8859_1, "iso-8859-1"},
+	"iso_8859-1:1987":     {charmap.ISO8859_1, "iso-8859-1"},
 	"l1":                  {charmap.Windows1252, "windows-1252"},
 	"latin1":              {charmap.Windows1252, "windows-1252"},
 	"us-ascii":            {charmap.Windows1252, "windows-1252"},
diff --git a/part.go b/part.go
index ddebd44..02f6925 100644
--- a/part.go
+++ b/part.go
@@ -2,6 +2,7 @@ package enmime
 
 import (
 	"bufio"
+	"bytes"
 	"encoding/base64"
 	"fmt"
 	"io"
@@ -11,26 +12,32 @@ import (
 	"strconv"
 	"strings"
 
+	"github.com/gogs/chardet"
 	"github.com/jhillyerd/enmime/internal/coding"
 )
 
+const minCharsetConfidence = 85
+
 // Part represents a node in the MIME multipart tree.  The Content-Type, Disposition and File Name
 // are parsed out of the header for easier access.
 type Part struct {
 	PartID      string               // PartID labels this parts position within the tree.
-	Header      textproto.MIMEHeader // Header for this Part.
 	Parent      *Part                // Parent of this part (can be nil.)
 	FirstChild  *Part                // FirstChild is the top most child of this part.
 	NextSibling *Part                // NextSibling of this part.
-	Boundary    string               // Boundary marker used within this part.
-	ContentID   string               // ContentID header for cid URL scheme.
-	ContentType string               // ContentType header without parameters.
-	Disposition string               // Content-Disposition header without parameters.
-	FileName    string               // The file-name from disposition or type header.
-	Charset     string               // The content charset encoding label.
-	Errors      []*Error             // Errors encountered while parsing this part.
-	Content     []byte               // Content after decoding, UTF-8 conversion if applicable.
-	Epilogue    []byte               // Epilogue contains data following the closing boundary marker.
+	Header      textproto.MIMEHeader // Header for this Part.
+
+	Boundary    string // Boundary marker used within this part.
+	ContentID   string // ContentID header for cid URL scheme.
+	ContentType string // ContentType header without parameters.
+	Disposition string // Content-Disposition header without parameters.
+	FileName    string // The file-name from disposition or type header.
+	Charset     string // The content charset encoding, may differ from charset in header.
+	OrigCharset string // The original content charset when a different charset was detected.
+
+	Errors   []*Error // Errors encountered while parsing this part.
+	Content  []byte   // Content after decoding, UTF-8 conversion if applicable.
+	Epilogue []byte   // Epilogue contains data following the closing boundary marker.
 }
 
 // NewPart creates a new Part object.
@@ -166,24 +173,60 @@ func (p *Part) decodeContent(r io.Reader) error {
 	}
 	// Build charset decoding reader.
 	if validEncoding && !detectAttachmentHeader(p.Header) {
-		if p.Charset != "" {
-			if reader, err := coding.NewCharsetReader(p.Charset, contentReader); err == nil {
+		// Attempt to detect character set from part content.
+		cd := chardet.NewTextDetector()
+		if p.ContentType == "text/html" {
+			cd = chardet.NewHtmlDetector()
+		}
+		buf, err := ioutil.ReadAll(contentReader)
+		if err != nil {
+			return err
+		}
+		cs, err := cd.DetectBest(buf)
+		if err != nil {
+			return err
+		}
+		contentReader = bytes.NewReader(buf) // Restore contentReader.
+
+		if cs != nil && cs.Confidence >= minCharsetConfidence {
+			// Confidence exceeded our threshold, use detected character set.
+			if p.Charset != "" && !strings.EqualFold(cs.Charset, p.Charset) {
+				p.addWarning(ErrorCharsetDeclaration,
+					fmt.Sprintf("Part %s: declared charset %q, detected %q, confidence %d",
+						p.PartID, p.Charset, cs.Charset, cs.Confidence))
+			}
+			reader, err := coding.NewCharsetReader(cs.Charset, contentReader)
+			if err == nil {
 				contentReader = reader
+				p.OrigCharset = p.Charset
+				p.Charset = cs.Charset
 			} else {
-				// Try to parse charset again here to see if we can salvage some badly formed ones
-				// like charset="charset=utf-8".
-				charsetp := strings.Split(p.Charset, "=")
-				if strings.ToLower(charsetp[0]) == "charset" && len(charsetp) > 1 {
-					p.Charset = charsetp[1]
-					if reader, err := coding.NewCharsetReader(p.Charset, contentReader); err == nil {
-						contentReader = reader
+				// Failed to get a conversion reader.
+				p.addWarning(ErrorCharsetConversion, err.Error())
+			}
+		} else {
+			// Low confidence, use declared character set.
+			if p.Charset != "" {
+				reader, err := coding.NewCharsetReader(p.Charset, contentReader)
+				if err == nil {
+					contentReader = reader
+				} else {
+					// Try to parse charset again here to see if we can salvage some badly formed
+					// ones like charset="charset=utf-8".
+					charsetp := strings.Split(p.Charset, "=")
+					if strings.ToLower(charsetp[0]) == "charset" && len(charsetp) > 1 {
+						p.Charset = charsetp[1]
+						reader, err = coding.NewCharsetReader(p.Charset, contentReader)
+						if err == nil {
+							contentReader = reader
+						} else {
+							// Failed to get a conversion reader.
+							p.addWarning(ErrorCharsetConversion, err.Error())
+						}
 					} else {
 						// Failed to get a conversion reader.
 						p.addWarning(ErrorCharsetConversion, err.Error())
 					}
-				} else {
-					// Failed to get a conversion reader.
-					p.addWarning(ErrorCharsetConversion, err.Error())
 				}
 			}
 		}


Test patch:
diff --git a/detect_test.go b/detect_test.go
index 1e50114..4072937 100644
--- a/detect_test.go
+++ b/detect_test.go
@@ -92,6 +92,11 @@ func TestDetectAttachmentHeader(t *testing.T) {
 			header: textproto.MIMEHeader{
 				"Content-Type": []string{"attachment; filename=\"test.jpg\""}},
 		},
+		{
+			want: false,
+			header: textproto.MIMEHeader{
+				"Content-Disposition": []string{"inline"}},
+		},
 		{
 			want: true,
 			header: textproto.MIMEHeader{
diff --git a/envelope_test.go b/envelope_test.go
index b1540cb..6e697f0 100644
--- a/envelope_test.go
+++ b/envelope_test.go
@@ -856,6 +856,19 @@ func TestDuplicateParamsInMime(t *testing.T) {
 	}
 }
 
+func TestUnquotedSpecialCharParamsInMime(t *testing.T) {
+	msg := test.OpenTestData("mail", "mime-unquoted-tspecials-param.raw")
+	e, err := enmime.ReadEnvelope(msg)
+
+	if err != nil {
+		t.Fatal("Failed to parse MIME:", err)
+	}
+
+	if e.Attachments[0].FileName != "Invoice_(302232133150612).pdf" {
+		t.Fatal("Mail should have a part with filename Invoice_(302232133150612).pdf")
+	}
+}
+
 func TestBadContentTypeInMime(t *testing.T) {
 	msg := test.OpenTestData("mail", "mime-bad-content-type.raw")
 	e, err := enmime.ReadEnvelope(msg)
diff --git a/error_test.go b/error_test.go
index 2d56eb5..c51ee25 100644
--- a/error_test.go
+++ b/error_test.go
@@ -90,6 +90,7 @@ func TestErrorEnvelopeWarnings(t *testing.T) {
 		{"unk-charset-html-only.raw", ErrorCharsetConversion},
 		{"unk-charset-part.raw", ErrorCharsetConversion},
 		{"malformed-base64-attach.raw", ErrorMalformedBase64},
+		{"incorrect-charset.raw", ErrorCharsetDeclaration},
 	}
 
 	for _, tt := range files {
diff --git a/header_test.go b/header_test.go
index f8822c5..c170e15 100644
--- a/header_test.go
+++ b/header_test.go
@@ -169,17 +169,17 @@ func TestFixMangledMediaType(t *testing.T) {
 		{
 			input: "application/pdf name=\"file.pdf\"",
 			sep:   " ",
-			want:  "application/pdf;name=\"file.pdf\";",
+			want:  "application/pdf;name=\"file.pdf\"",
 		},
 		{
 			input: "one/two; name=\"file.two\"; name=\"file.two\"",
 			sep:   ";",
-			want:  "one/two; name=\"file.two\";",
+			want:  "one/two; name=\"file.two\"",
 		},
 		{
 			input: "one/two name=\"file.two\" name=\"file.two\"",
 			sep:   " ",
-			want:  "one/two;name=\"file.two\";",
+			want:  "one/two;name=\"file.two\"",
 		},
 	}
 	for _, tc := range testCases {
diff --git a/part_test.go b/part_test.go
index 1d14bde..f319fc5 100644
--- a/part_test.go
+++ b/part_test.go
@@ -328,7 +328,7 @@ func TestMultiMixedParts(t *testing.T) {
 		Parent:      test.PartExists,
 		NextSibling: test.PartExists,
 		ContentType: "text/plain",
-		Charset:     "us-ascii",
+		Charset:     "ISO-8859-1",
 		PartID:      "1",
 	}
 	test.ComparePart(t, p, wantp)
@@ -341,7 +341,7 @@ func TestMultiMixedParts(t *testing.T) {
 	wantp = &enmime.Part{
 		Parent:      test.PartExists,
 		ContentType: "text/plain",
-		Charset:     "us-ascii",
+		Charset:     "ISO-8859-1",
 		PartID:      "2",
 	}
 	test.ComparePart(t, p, wantp)
@@ -379,7 +379,7 @@ func TestMultiOtherParts(t *testing.T) {
 		Parent:      test.PartExists,
 		NextSibling: test.PartExists,
 		ContentType: "text/plain",
-		Charset:     "us-ascii",
+		Charset:     "ISO-8859-1",
 		PartID:      "1",
 	}
 	test.ComparePart(t, p, wantp)
@@ -392,7 +392,7 @@ func TestMultiOtherParts(t *testing.T) {
 	wantp = &enmime.Part{
 		Parent:      test.PartExists,
 		ContentType: "text/plain",
-		Charset:     "us-ascii",
+		Charset:     "ISO-8859-1",
 		PartID:      "2",
 	}
 	test.ComparePart(t, p, wantp)
@@ -523,7 +523,7 @@ func TestPartSimilarBoundary(t *testing.T) {
 		Parent:      test.PartExists,
 		NextSibling: test.PartExists,
 		ContentType: "text/plain",
-		Charset:     "us-ascii",
+		Charset:     "ISO-8859-1",
 		PartID:      "1",
 	}
 	test.ComparePart(t, p, wantp)
diff --git a/testdata/low-quality/incorrect-charset.raw b/testdata/low-quality/incorrect-charset.raw
new file mode 100644
index 0000000..6b04bb4
--- /dev/null
+++ b/testdata/low-quality/incorrect-charset.raw
@@ -0,0 +1,31 @@
+From: James Hillyerd <james@makita.skynet>
+Subject: plain text
+Date: Thu, 18 Oct 2012 22:48:39 -0700
+Message-Id: <07B7061D-2676-487E-942E-C341CE4D13DC@makita.skynet>
+To: greg@inbucket
+Content-Type: multipart/alternative; boundary="Enmime-Test-100"
+
+--Enmime-Test-100
+Content-Transfer-Encoding: 8bit
+Content-Type: text/plain; charset=us-ascii
+
+Using Unicode/UTF-8, you can write in emails and source code things such as
+
+Mathematics and sciences:
+
+  ∮ E⋅da = Q,  n → ∞, ∑ f(i) = ∏ g(i),      ⎧⎡⎛┌─────┐⎞⎤⎫
+                                            ⎪⎢⎜│a²+b³ ⎟⎥⎪
+  ∀x∈ℝ: ⌈x⌉ = −⌊−x⌋, α ∧ ¬β = ¬(¬α ∨ β),    ⎪⎢⎜│───── ⎟⎥⎪
+                                            ⎪⎢⎜⎷ c₈   ⎟⎥⎪
+  ℕ ⊆ ℕ₀ ⊂ ℤ ⊂ ℚ ⊂ ℝ ⊂ ℂ,                   ⎨⎢⎜       ⎟⎥⎬
+                                            ⎪⎢⎜ ∞     ⎟⎥⎪
+  ⊥ < a ≠ b ≡ c ≤ d ≪ ⊤ ⇒ (⟦A⟧ ⇔ ⟪B⟫),      ⎪⎢⎜ ⎲     ⎟⎥⎪
+                                            ⎪⎢⎜ ⎳aⁱ-bⁱ⎟⎥⎪
+  2H₂ + O₂ ⇌ 2H₂O, R = 4.7 kΩ, ⌀ 200 mm     ⎩⎣⎝i=1    ⎠⎦⎭
+
+Linguistics and dictionaries:
+
+  ði ıntəˈnæʃənəl fəˈnɛtık əsoʊsiˈeıʃn
+  Y [ˈʏpsilɔn], Yen [jɛn], Yoga [ˈjoːgɑ]
+
+--Enmime-Test-100--
diff --git a/testdata/mail/html-only-inline.raw b/testdata/mail/html-only-inline.raw
index 31ab74c..718408f 100644
--- a/testdata/mail/html-only-inline.raw
+++ b/testdata/mail/html-only-inline.raw
@@ -17,7 +17,7 @@ Content-Type: multipart/related;
 --Apple-Mail=_D2ABE25A-F0FE-404E-94EE-D98BD23448D5
 Content-Transfer-Encoding: 7bit
 Content-Type: text/html;
-	charset=us-ascii
+	charset=ISO-8859-1
 
 <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
diff --git a/testdata/mail/mime-unquoted-tspecials-param.raw b/testdata/mail/mime-unquoted-tspecials-param.raw
new file mode 100644
index 0000000..486ec2b
--- /dev/null
+++ b/testdata/mail/mime-unquoted-tspecials-param.raw
@@ -0,0 +1,91 @@
+From: "easyHotel Booking" <bookings@easyHotel.com>
+To: <deepak.prabhakara@gmail.com>
+Subject: Confirmation of Booking with easyHotel
+Date: Fri, 12 Jun 2015 23:24:01 +0100
+Message-ID: <F21768750C2E4303B37CA8EDCD498C67@dm3.entee.co.uk>
+MIME-Version: 1.0
+Content-Type: multipart/mixed; boundary="----=_NextPart_000_057C_01D0A566.DD8BD380"
+X-Mailer: Microsoft CDO for Windows 2000
+Content-Class: urn:content-classes:message
+Importance: normal
+Priority: normal
+X-MimeOLE: Produced By Microsoft MimeOLE V6.1.7601.17609
+Return-Path: bookings@easyHotel.com
+X-OriginalArrivalTime: 12 Jun 2015 22:24:01.0592 (UTC)
+
+    FILETIME=[7BC76B80:01D0A55E]
+
+
+
+This is a multi-part message in MIME format.
+
+
+
+------=_NextPart_000_057C_01D0A566.DD8BD380
+Content-Type: text/plain
+Content-Transfer-Encoding: 7bit
+
+
+
+Thank you for booking with easyHotel.
+
+
+
+You may review your booking details here: 
+
+https://bookings.easyHotel.com/AccClient/MyInformation-MyBookings.asp
+
+
+
+You may review your account including payment information here: 
+
+https://bookings.easyHotel.com/AccClient/MyInformation-MyAccount.asp
+
+
+
+You may obtain general help and assistance here: 
+
+http://support.easyhotel.com/hc/en-gb/categories/200490531-Frequently-asked-questions
+
+
+
+You can review our terms and conditions (including refund policy) again here: 
+
+https://bookings.easyhotel.com/secbook/GB/tandc.html
+
+
+
+You will need Adobe Acrobat Reader to open the attached invoice. If you do not already have it installed on your computer, you can download it free of charge from  http://get.adobe.com/uk/reader/
+
+
+
+
+
+We wish you a pleasant stay.
+
+
+
+easyHotel
+
+
+
+------=_NextPart_000_057C_01D0A566.DD8BD380
+Content-Type: application/pdf; name=Invoice_(302232133150612).pdf
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment; filename="Invoice_(302232133150612).pdf"
+
+
+
+JVBERi0xLjINCg0KNCAwIG9iag0KPDwNCi9FIDYxOTUNCi9IIFsgMTIxMSAxNDUgXQ0KL0wgNjUy
+MA0KL0xpbmVhcml6ZWQgMQ0KL04gMQ0KL08gNw0KL1QgNjM5MA0KPj4gICAgICAgICAgICAgICAg
+ICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAg
+DQplbmRvYmoNCg0KeHJlZg0KNCA4DQowMDAwMDAwMDEyIDAwMDAwIG4NCjAwMDAwMDEwODggMDAw
+MDAgbg0KMDAwMDAwMTIxMSAwMDAwMCBuDQowMDAwMDAxMzU2IDAwMDAwIG4NCjAwMDAwMDE2MDcg
+MDAwMDAgbg0KMDAwMDAwMTcxNiAwMDAwMCBuDQowMDAwMDAxODI0IDAwMDAwIG4NCjAwMDAwMDQz
+MTkgMDAwMDAgbg0KdHJhaWxlcg0KPDwNCi9BQkNwZGYgODEwNw0KL0lEIFsgPDNENzc5REU2NjU0
+RDM4ODczMTYzODBBNTY3REY0MDhFPg0KPDc5OEQ0RjVGNkUyNzIzQjA1MTFCQzU0QzYwMTczRDI3
+PiBdDQovSW5mbyAzIDAgUg0KL1ByZXYgNjM4MA0KL1Jvb3QgNSAwIFINCi9TaXplIDEyDQovU291
+cmNlIChXZUpYRnh
+
+------=_NextPart_000_057C_01D0A566.DD8BD380--
+
diff --git a/testdata/mail/non-mime-html.raw b/testdata/mail/non-mime-html.raw
index 843859d..dee3323 100644
--- a/testdata/mail/non-mime-html.raw
+++ b/testdata/mail/non-mime-html.raw
@@ -3,12 +3,12 @@ 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/
-Content-Type: text/html; charset="UTF-8"
+Content-Type: text/html; charset="ISO-8859-1"
 
 <html>
   <head>
     <title>Document Title</title>
-    <meta charset="utf-8"/>
+    <meta charset="iso-8859-1"/>
     <style>
       strong {
         font-size: 110%;
@@ -25,4 +25,3 @@ Content-Type: text/html; charset="UTF-8"
     </p>
   </body>
 </html>
-