Repository: jhillyerd/enmime
Pull request #307 (2023-10-11 23:11:19)

build(deps): bump golang.org/x/net from 0.15.0 to 0.17.0
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.15.0 to 0.17.0.
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/golang/net/commit/b225e7ca6dde1ef5a5ae5ce922861bda011cfabd"><code>b225e7c</code></a> http2: limit maximum handler goroutines to MaxConcurrentStreams</li>
<li><a href="https://github.com/golang/net/commit/88194ad8ab44a02ea952c169883c3f57db6cf9f4"><code>88194ad</code></a> go.mod: update golang.org/x dependencies</li>
<li><a href="https://github.com/golang/net/commit/2b60a61f1e4cf3a5ecded0bd7e77ea168289e6de"><code>2b60a61</code></a> quic: fix several bugs in flow control accounting</li>
<li><a href="https://github.com/golang/net/commit/73d82efb96cacc0c378bc150b56675fc191894b9"><code>73d82ef</code></a> quic: handle DATA_BLOCKED frames</li>
<li><a href="https://github.com/golang/net/commit/5d5a036a503f8accd748f7453c0162115187be13"><code>5d5a036</code></a> quic: handle streams moving from the data queue to the meta queue</li>
<li><a href="https://github.com/golang/net/commit/350aad2603e57013fafb1a9e2089a382fe67dc80"><code>350aad2</code></a> quic: correctly extend peer's flow control window after MAX_DATA</li>
<li><a href="https://github.com/golang/net/commit/21814e71db756f39b69fb1a3e06350fa555a79b1"><code>21814e7</code></a> quic: validate connection id transport parameters</li>
<li><a href="https://github.com/golang/net/commit/a600b3518eed7a9a4e24380b4b249cb986d9b64d"><code>a600b35</code></a> quic: avoid redundant MAX_DATA updates</li>
<li><a href="https://github.com/golang/net/commit/ea633599b58dc6a50d33c7f5438edfaa8bc313df"><code>ea63359</code></a> http2: check stream body is present on read timeout</li>
<li><a href="https://github.com/golang/net/commit/ddd8598e5694aa5e966e44573a53e895f6fa5eb2"><code>ddd8598</code></a> quic: version negotiation</li>
<li>Additional commits viewable in <a href="https://github.com/golang/net/compare/v0.15.0...v0.17.0">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=golang.org/x/net&package-manager=go_modules&previous-version=0.15.0&new-version=0.17.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
You can disable automated security fix PRs for this repo from the [Security Alerts page](https://github.com/jhillyerd/enmime/network/alerts).

</details>

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

coverage: 85.966%. remained the same when pulling **aa400ef257df8888a44e0fe2153a88daf683e891 on dependabot/go_modules/golang.org/x/net-0.17.0** into **6241177927360a8343ea9241d7ea314b7892d9f0 on main**.


Patch:
diff --git a/detect.go b/detect.go
index 81543d4..1dda921 100644
--- a/detect.go
+++ b/detect.go
@@ -4,14 +4,13 @@ import (
 	"strings"
 
 	inttp "github.com/jhillyerd/enmime/internal/textproto"
-	"github.com/jhillyerd/enmime/mediatype"
 )
 
 // detectMultipartMessage returns true if the message has a recognized multipart Content-Type header
 func detectMultipartMessage(root *Part, multipartWOBoundaryAsSinglepart bool) bool {
 	// Parse top-level multipart
 	ctype := root.Header.Get(hnContentType)
-	mtype, params, _, err := mediatype.Parse(ctype)
+	mtype, params, _, err := root.parseMediaType(ctype)
 	if err != nil {
 		return false
 	}
@@ -35,27 +34,27 @@ func detectMultipartMessage(root *Part, multipartWOBoundaryAsSinglepart bool) bo
 //   - Content-Disposition: attachment; filename="frog.jpg"
 //   - Content-Disposition: inline; filename="frog.jpg"
 //   - Content-Type: attachment; filename="frog.jpg"
-func detectAttachmentHeader(header inttp.MIMEHeader) bool {
-	mtype, params, _, _ := mediatype.Parse(header.Get(hnContentDisposition))
+func detectAttachmentHeader(root *Part, header inttp.MIMEHeader) bool {
+	mtype, params, _, _ := root.parseMediaType(header.Get(hnContentDisposition))
 	if strings.ToLower(mtype) == cdAttachment ||
 		(strings.ToLower(mtype) == cdInline && len(params) > 0) {
 		return true
 	}
 
-	mtype, _, _, _ = mediatype.Parse(header.Get(hnContentType))
+	mtype, _, _, _ = root.parseMediaType(header.Get(hnContentType))
 	return strings.ToLower(mtype) == cdAttachment
 }
 
 // detectTextHeader returns true, if the the MIME headers define a valid 'text/plain' or 'text/html'
 // part.  If the emptyContentTypeIsPlain argument is set to true, a missing Content-Type header will
 // result in a positive plain part detection.
-func detectTextHeader(header inttp.MIMEHeader, emptyContentTypeIsText bool) bool {
+func detectTextHeader(root *Part, header inttp.MIMEHeader, emptyContentTypeIsText bool) bool {
 	ctype := header.Get(hnContentType)
 	if ctype == "" && emptyContentTypeIsText {
 		return true
 	}
 
-	if mtype, _, _, err := mediatype.Parse(ctype); err == nil {
+	if mtype, _, _, err := root.parseMediaType(ctype); err == nil {
 		switch mtype {
 		case ctTextPlain, ctTextHTML:
 			return true
@@ -68,23 +67,23 @@ func detectTextHeader(header inttp.MIMEHeader, emptyContentTypeIsText bool) bool
 // detectBinaryBody returns true if the mail header defines a binary body.
 func detectBinaryBody(root *Part) bool {
 	header := inttp.MIMEHeader(root.Header) // Use internal header methods.
-	if detectTextHeader(header, true) {
+	if detectTextHeader(root, header, true) {
 		// It is text/plain, but an attachment.
 		// Content-Type: text/plain; name="test.csv"
 		// Content-Disposition: attachment; filename="test.csv"
 		// Check for attachment only, or inline body is marked
 		// as attachment, too.
-		mtype, _, _, _ := mediatype.Parse(header.Get(hnContentDisposition))
+		mtype, _, _, _ := root.parseMediaType(header.Get(hnContentDisposition))
 		return strings.ToLower(mtype) == cdAttachment
 	}
 
-	isBin := detectAttachmentHeader(header)
+	isBin := detectAttachmentHeader(root, header)
 	if !isBin {
 		// This must be an attachment, if the Content-Type is not
 		// 'text/plain' or 'text/html'.
 		// Example:
 		// Content-Type: application/pdf; name="doc.pdf"
-		mtype, _, _, _ := mediatype.Parse(header.Get(hnContentType))
+		mtype, _, _, _ := root.parseMediaType(header.Get(hnContentType))
 		mtype = strings.ToLower(mtype)
 		if mtype != ctTextPlain && mtype != ctTextHTML {
 			return true
diff --git a/encode.go b/encode.go
index 6016724..6a23662 100644
--- a/encode.go
+++ b/encode.go
@@ -24,6 +24,7 @@ const (
 	te7Bit transferEncoding = iota
 	teQuoted
 	teBase64
+	teRaw
 )
 
 const (
@@ -98,7 +99,9 @@ func (p *Part) setupMIMEHeaders() transferEncoding {
 
 	// If we are encoding a part that previously had content-transfer-encoding set, unset it so
 	// the correct encoding detection can be done below.
-	p.Header.Del(hnContentEncoding)
+	if p.parser != nil && !p.parser.rawContent {
+		p.Header.Del(hnContentEncoding)
+	}
 
 	cte := te7Bit
 	if len(p.Content) > 0 {
@@ -194,6 +197,10 @@ func (p *Part) encodeContent(b *bufio.Writer, cte transferEncoding) (err error)
 		return p.encodeContentFromReader(b)
 	}
 
+	if p.parser != nil && p.parser.rawContent {
+		cte = teRaw
+	}
+
 	switch cte {
 	case teBase64:
 		enc := base64.StdEncoding
diff --git a/envelope.go b/envelope.go
index aeb7cc3..3c4bb44 100644
--- a/envelope.go
+++ b/envelope.go
@@ -12,8 +12,6 @@ import (
 	"github.com/jaytaylor/html2text"
 	"github.com/jhillyerd/enmime/internal/coding"
 	inttp "github.com/jhillyerd/enmime/internal/textproto"
-	"github.com/jhillyerd/enmime/mediatype"
-
 	"github.com/pkg/errors"
 )
 
@@ -232,7 +230,7 @@ func parseTextOnlyBody(root *Part, e *Envelope) error {
 	var charset string
 	var isHTML bool
 	if ctype := root.Header.Get(hnContentType); ctype != "" {
-		if mediatype, mparams, _, err := mediatype.Parse(ctype); err == nil {
+		if mediatype, mparams, _, err := root.parseMediaType(ctype); err == nil {
 			isHTML = (mediatype == ctTextHTML)
 			if mparams[hpCharset] != "" {
 				charset = mparams[hpCharset]
@@ -271,7 +269,7 @@ func parseTextOnlyBody(root *Part, e *Envelope) error {
 func parseMultiPartBody(root *Part, e *Envelope) error {
 	// Parse top-level multipart
 	ctype := root.Header.Get(hnContentType)
-	mediatype, params, _, err := mediatype.Parse(ctype)
+	mediatype, params, _, err := root.parseMediaType(ctype)
 	if err != nil {
 		return fmt.Errorf("unable to parse media type: %v", err)
 	}
diff --git a/error.go b/error.go
index 416cfda..9f819b3 100644
--- a/error.go
+++ b/error.go
@@ -84,3 +84,22 @@ func (p *Part) addProblem(err *Error) {
 		p.Errors = append(p.Errors, err)
 	}
 }
+
+// ErrorCollector is an interface for collecting errors and warnings during
+// parsing.
+type ErrorCollector interface {
+	AddError(name string, detailFmt string, args ...any)
+	AddWarning(name string, detailFmt string, args ...any)
+}
+
+type partErrorCollector struct {
+	part *Part
+}
+
+func (p *partErrorCollector) AddError(name string, detailFmt string, args ...any) {
+	p.part.addError(name, detailFmt, args...)
+}
+
+func (p *partErrorCollector) AddWarning(name string, detailFmt string, args ...any) {
+	p.part.addWarning(name, detailFmt, args...)
+}
diff --git a/go.mod b/go.mod
index 22e2a49..4678062 100644
--- a/go.mod
+++ b/go.mod
@@ -19,6 +19,6 @@ require (
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	github.com/rivo/uniseg v0.4.4 // indirect
 	github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf // indirect
-	golang.org/x/net v0.15.0 // indirect
+	golang.org/x/net v0.17.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
diff --git a/go.sum b/go.sum
index 987a491..e50f8c8 100644
--- a/go.sum
+++ b/go.sum
@@ -24,8 +24,8 @@ github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf h1:pvbZ0lM0XWPBqUKqFU8cma
 github.com/ssor/bom v0.0.0-20170718123548-6386211fdfcf/go.mod h1:RJID2RhlZKId02nZ62WenDCkgHFerpIOmW0iT7GKmXM=
 github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
 github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
-golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
-golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
+golang.org/x/net v0.17.0 h1:pVaXccu2ozPjCXewfr1S7xza/zcXTity9cCdXQYSjIM=
+golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE=
 golang.org/x/text v0.13.0 h1:ablQoSUd0tRdKxZewP80B+BaqeKJuVhuRxj/dkrun3k=
 golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
diff --git a/header.go b/header.go
index 38498cc..5fc10db 100644
--- a/header.go
+++ b/header.go
@@ -119,9 +119,10 @@ func ParseMediaType(ctype string) (mtype string, params map[string]string, inval
 	return mediatype.Parse(ctype)
 }
 
-// readHeader reads a block of SMTP or MIME headers and returns a textproto.MIMEHeader.
-// Header parse warnings & errors will be added to p.Errors, io errors will be returned directly.
-func readHeader(r *bufio.Reader, p *Part) (textproto.MIMEHeader, error) {
+// ReadHeader reads a block of SMTP or MIME headers and returns a
+// textproto.MIMEHeader. Header parse warnings & errors will be added to
+// ErrorCollector, io errors will be returned directly.
+func ReadHeader(r *bufio.Reader, p ErrorCollector) (textproto.MIMEHeader, error) {
 	// buf holds the massaged output for textproto.Reader.ReadMIMEHeader()
 	buf := &bytes.Buffer{}
 	tp := inttp.NewReader(r)
@@ -145,7 +146,7 @@ line:
 		}
 		if firstColon == 0 {
 			// Can't parse line starting with colon: skip
-			p.addError(ErrorMalformedHeader, "Header line %q started with a colon", s)
+			p.AddError(ErrorMalformedHeader, "Header line %q started with a colon", s)
 			continue
 		}
 		if firstColon > 0 {
@@ -162,7 +163,7 @@ line:
 			// in header keys are no longer allowed; https://github.com/golang/go/issues/53188
 			for _, c := range s[:firstColon] {
 				if c != ' ' && !inttp.ValidEmailHeaderFieldByte(c) {
-					p.addError(
+					p.AddError(
 						ErrorMalformedHeader, "Header name %q contains invalid character %q", s, c)
 					continue line
 				}
@@ -183,7 +184,7 @@ line:
 				// Attempt to detect and repair a non-indented continuation of previous line
 				buf.WriteByte(' ')
 				buf.Write(s)
-				p.addWarning(ErrorMalformedHeader, "Continued line %q was not indented", s)
+				p.AddWarning(ErrorMalformedHeader, "Continued line %q was not indented", s)
 			} else {
 				// Empty line, finish header parsing
 				buf.Write([]byte{'\r', '\n'})
@@ -198,6 +199,12 @@ line:
 	return textproto.MIMEHeader(header), errors.WithStack(err)
 }
 
+// readHeader reads a block of SMTP or MIME headers and returns a textproto.MIMEHeader.
+// Header parse warnings & errors will be added to p.Errors, io errors will be returned directly.
+func readHeader(r *bufio.Reader, p *Part) (textproto.MIMEHeader, error) {
+	return ReadHeader(r, &partErrorCollector{p})
+}
+
 // decodeToUTF8Base64Header decodes a MIME header per RFC 2047, reencoding to =?utf-8b?
 func decodeToUTF8Base64Header(input string) string {
 	if !strings.Contains(input, "=?") {
diff --git a/options.go b/options.go
index 9bad427..ea9afe4 100644
--- a/options.go
+++ b/options.go
@@ -51,3 +51,28 @@ func (o maxStoredPartErrorsOption) apply(p *Parser) {
 	max := int(o)
 	p.maxStoredPartErrors = &max
 }
+
+// RawContent if set to true will not try to decode the CTE and return the raw part content.
+// Otherwise, will try to automatically decode the CTE.
+func RawContent(a bool) Option {
+	return rawContentOption(a)
+}
+
+type rawContentOption bool
+
+func (o rawContentOption) apply(p *Parser) {
+	p.rawContent = bool(o)
+}
+
+// SetCustomParseMediaType if provided, will be used to parse media type instead of the default ParseMediaType
+// function.  This may be used to parse media type parameters that would otherwise be considered malformed.
+// By default parsing happens using ParseMediaType
+func SetCustomParseMediaType(customParseMediaType CustomParseMediaType) Option {
+	return parseMediaTypeOption(customParseMediaType)
+}
+
+type parseMediaTypeOption CustomParseMediaType
+
+func (o parseMediaTypeOption) apply(p *Parser) {
+	p.customParseMediaType = CustomParseMediaType(o)
+}
diff --git a/parser.go b/parser.go
index 1bf7f6a..4894fbf 100644
--- a/parser.go
+++ b/parser.go
@@ -13,6 +13,9 @@ func AllowCorruptTextPartErrorPolicy(p *Part, err error) bool {
 	return false
 }
 
+// CustomParseMediaType parses media type. See ParseMediaType for more details
+type CustomParseMediaType func(ctype string) (mtype string, params map[string]string, invalidParams []string, err error)
+
 // Parser parses MIME.
 // Default parser is a valid one.
 type Parser struct {
@@ -20,6 +23,8 @@ type Parser struct {
 	multipartWOBoundaryAsSinglePart bool
 	readPartErrorPolicy             ReadPartErrorPolicy
 	skipMalformedParts              bool
+	rawContent                      bool
+	customParseMediaType            CustomParseMediaType
 }
 
 // defaultParser is a Parser with default configuration.
diff --git a/part.go b/part.go
index 042ed4a..4649fe5 100644
--- a/part.go
+++ b/part.go
@@ -15,8 +15,6 @@ import (
 	"github.com/gogs/chardet"
 	"github.com/jhillyerd/enmime/internal/coding"
 	inttp "github.com/jhillyerd/enmime/internal/textproto"
-	"github.com/jhillyerd/enmime/mediatype"
-
 	"github.com/pkg/errors"
 )
 
@@ -126,7 +124,7 @@ func (p *Part) setupHeaders(r *bufio.Reader, defaultContentType string) error {
 		ctype = defaultContentType
 	}
 	// Parse Content-Type header.
-	mtype, mparams, minvalidParams, err := mediatype.Parse(ctype)
+	mtype, mparams, minvalidParams, err := p.parseMediaType(ctype)
 	if err != nil {
 		return err
 	}
@@ -149,7 +147,7 @@ func (p *Part) setupHeaders(r *bufio.Reader, defaultContentType string) error {
 func (p *Part) setupContentHeaders(mediaParams map[string]string) {
 	header := inttp.MIMEHeader(p.Header)
 	// Determine content disposition, filename, character set.
-	disposition, dparams, _, err := mediatype.Parse(header.Get(hnContentDisposition))
+	disposition, dparams, _, err := p.parseMediaType(header.Get(hnContentDisposition))
 	if err == nil {
 		// Disposition is optional
 		p.Disposition = disposition
@@ -277,7 +275,10 @@ func (p *Part) decodeContent(r io.Reader, readPartErrorPolicy ReadPartErrorPolic
 	// b64cleaner aggregates errors, must maintain a reference to it to get them later.
 	var b64cleaner *coding.Base64Cleaner
 	// Build content decoding reader.
-	encoding := header.Get(hnContentEncoding)
+	encoding := ""
+	if p.parser != nil && !p.parser.rawContent {
+		encoding = header.Get(hnContentEncoding)
+	}
 	validEncoding := true
 	switch strings.ToLower(encoding) {
 	case cteQuotedPrintable:
@@ -324,6 +325,15 @@ func (p *Part) decodeContent(r io.Reader, readPartErrorPolicy ReadPartErrorPolic
 	return nil
 }
 
+// parses media type using custom or default media type parser
+func (p *Part) parseMediaType(ctype string) (mtype string, params map[string]string, invalidParams []string, err error) {
+	if p.parser == nil || p.parser.customParseMediaType == nil {
+		return ParseMediaType(ctype)
+	}
+
+	return p.parser.customParseMediaType(ctype)
+}
+
 // IsBase64CorruptInputError returns true when err is of type base64.CorruptInputError.
 //
 // It can be used to create ReadPartErrorPolicy functions.


Test patch:
diff --git a/detect_test.go b/detect_test.go
index b092db8..aba9b1f 100644
--- a/detect_test.go
+++ b/detect_test.go
@@ -141,8 +141,10 @@ func TestDetectAttachmentHeader(t *testing.T) {
 		},
 	}
 
+	root := &Part{parser: &defaultParser}
+
 	for _, s := range htests {
-		got := detectAttachmentHeader(s.header)
+		got := detectAttachmentHeader(root, s.header)
 		if got != s.want {
 			t.Errorf("detectAttachmentHeader(%v) == %v, want: %v", s.header, got, s.want)
 		}
@@ -192,8 +194,10 @@ func TestDetectTextHeader(t *testing.T) {
 		},
 	}
 
+	root := &Part{parser: &defaultParser}
+
 	for _, s := range htests {
-		got := detectTextHeader(s.header, s.emptyIsPlain)
+		got := detectTextHeader(root, s.header, s.emptyIsPlain)
 		if got != s.want {
 			t.Errorf("detectTextHeader(%v, %v) == %v, want: %v",
 				s.header, s.emptyIsPlain, got, s.want)
diff --git a/encode_test.go b/encode_test.go
index b545502..ecc054a 100644
--- a/encode_test.go
+++ b/encode_test.go
@@ -364,3 +364,31 @@ func TestEncodePartContentNonAsciiText(t *testing.T) {
 		}
 	}
 }
+
+func TestParseRawContentOptionTrue(t *testing.T) {
+	r := test.OpenTestData("encode", "parser-raw-content-option.raw")
+	p := enmime.NewParser(enmime.RawContent(true))
+	e, err := p.ReadEnvelope(r)
+	if err != nil {
+		t.Fatal("Failed to parse MIME:", err)
+	}
+	b := &bytes.Buffer{}
+	if err := e.Root.Encode(b); err != nil {
+		t.Fatal(err)
+	}
+	test.DiffGolden(t, b.Bytes(), "testdata", "encode", "parser-raw-content-option-true.raw.golden")
+}
+
+func TestParseRawContentOptionFalse(t *testing.T) {
+	r := test.OpenTestData("encode", "parser-raw-content-option.raw")
+	p := enmime.NewParser()
+	e, err := p.ReadEnvelope(r)
+	if err != nil {
+		t.Fatal("Failed to parse MIME:", err)
+	}
+	b := &bytes.Buffer{}
+	if err := e.Root.Encode(b); err != nil {
+		t.Fatal(err)
+	}
+	test.DiffGolden(t, b.Bytes(), "testdata", "encode", "parser-raw-content-option-false.raw.golden")
+}
diff --git a/options_test.go b/options_test.go
new file mode 100644
index 0000000..8a7010b
--- /dev/null
+++ b/options_test.go
@@ -0,0 +1,90 @@
+package enmime
+
+import (
+	"fmt"
+	"strings"
+	"testing"
+)
+
+func TestSetCustomParseMediaType(t *testing.T) {
+	alwaysReturnHTML := func(ctype string) (mtype string, params map[string]string, invalidParams []string, err error) {
+		return "text/html", nil, nil, err
+	}
+	changeAndUtilizeDefault := func(ctype string) (mtype string, params map[string]string, invalidParams []string, err error) {
+		modifiedStr := strings.ReplaceAll(ctype, "application/Pamir Viewer", "application/PamirViewer")
+		return ParseMediaType(modifiedStr)
+	}
+	tcases := []struct {
+		ctype                string
+		want                 string
+		customParseMediaType CustomParseMediaType
+	}{
+		{
+			ctype:                "text/plain",
+			want:                 "text/plain",
+			customParseMediaType: nil,
+		},
+		{
+			ctype:                "text/plain",
+			want:                 "text/html",
+			customParseMediaType: alwaysReturnHTML,
+		},
+		{
+			ctype:                "text/plain; charset=utf-8",
+			want:                 "text/html",
+			customParseMediaType: alwaysReturnHTML,
+		},
+		{
+			ctype:                "application/Pamir Viewer; name=\"2023-384.pmrv\"",
+			want:                 "application/pamirviewer",
+			customParseMediaType: changeAndUtilizeDefault,
+		},
+	}
+
+	for _, tcase := range tcases {
+		p := &Part{parser: NewParser(SetCustomParseMediaType(tcase.customParseMediaType))}
+
+		got, _, _, _ := p.parseMediaType(tcase.ctype)
+		if got != tcase.want {
+			t.Errorf("Parser.parseMediaType(%v) == %v, want: %v",
+				tcase.ctype, got, tcase.want)
+		}
+	}
+}
+
+func ExampleSetCustomParseMediaType() {
+	// for the sake of simplicity replaces space in a very specific invalid content-type: "application/Pamir Viewer"
+	replaceSpecificContentType := func(ctype string) (mtype string, params map[string]string, invalidParams []string, err error) {
+		modifiedStr := strings.ReplaceAll(ctype, "application/Pamir Viewer", "application/PamirViewer")
+
+		return ParseMediaType(modifiedStr)
+	}
+
+	invalidMessageContent := `From: <enmime@parser.git>
+Content-Type: multipart/mixed;
+	boundary="----=_NextPart_000_000F_01D9FAC6.09EB3B60"
+
+------=_NextPart_000_000F_01D9FAC6.09EB3B60
+Content-Type: application/Pamir Viewer;
+	name="2023-10-13.pmrv"
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+	filename="2023-10-13.pmrv"
+
+f6En7vFpNql3tfMkoKABP1iBEf+M/qF6LCAIvyRbpH6uDCqcKKGmH3e6OiqN5eCfqUk=
+`
+
+	p := NewParser(SetCustomParseMediaType(replaceSpecificContentType))
+	e, err := p.ReadEnvelope(strings.NewReader(invalidMessageContent))
+
+	fmt.Println(err)
+	fmt.Println(len(e.Attachments))
+	fmt.Println(e.Attachments[0].ContentType)
+	fmt.Println(e.Attachments[0].FileName)
+
+	// Output:
+	// <nil>
+	// 1
+	// application/pamirviewer
+	// 2023-10-13.pmrv
+}
diff --git a/testdata/encode/parser-raw-content-option-false.raw.golden b/testdata/encode/parser-raw-content-option-false.raw.golden
new file mode 100644
index 0000000..05a297b
--- /dev/null
+++ b/testdata/encode/parser-raw-content-option-false.raw.golden
@@ -0,0 +1,7 @@
+Content-Type: text/html; charset=utf-8
+From: me@me.com
+Mime-Version: 1.0
+Subject: Test subject
+To: you@you.com
+
+<!DOCTYPE html><html><body><h1>My First Heading</h1><p>My first paragraph.</p></body></html>
diff --git a/testdata/encode/parser-raw-content-option-true.raw.golden b/testdata/encode/parser-raw-content-option-true.raw.golden
new file mode 100644
index 0000000..7b680fd
--- /dev/null
+++ b/testdata/encode/parser-raw-content-option-true.raw.golden
@@ -0,0 +1,9 @@
+Content-Transfer-Encoding: quoted-printable
+Content-Type: text/html; charset=utf-8
+From: me@me.com
+Mime-Version: 1.0
+Subject: Test subject
+To: you@you.com
+
+<!DOCTYPE html><html><body><h1>My First Heading</h1><p>My first paragraph.<=
+/p></body></html>
diff --git a/testdata/encode/parser-raw-content-option.raw b/testdata/encode/parser-raw-content-option.raw
new file mode 100644
index 0000000..20266de
--- /dev/null
+++ b/testdata/encode/parser-raw-content-option.raw
@@ -0,0 +1,9 @@
+Subject: Test subject
+From: me@me.com
+To: you@you.com
+MIME-Version: 1.0
+Content-Transfer-Encoding: quoted-printable
+Content-Type: text/html; charset=utf-8
+
+<!DOCTYPE html><html><body><h1>My First Heading</h1><p>My first paragraph.<=
+/p></body></html>