Repository: jhillyerd/enmime
Pull request #89 (2018-11-16 16:44:32)

Resolves an issue parsing a malformed Content-Type header
An email was received with this Content-Type header:

```
Content-Type: ;
	name=""
```

This change makes it so this MIME part can be parsed successfully.

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

Coverage increased (+0.08%) to 86.139% when pulling **2ddae1e37ae1ca2dc6aea69bb7a7328770d5c99a on dcormier:dc/emptyct** into **9600eed1a88a2736d1eeb40d3f7d4e519638f9a7 on jhillyerd:develop**.

jhillyerd: Thanks for this.  Will try to review this weekend, but there are a few in the hopper before you.

It may make sense to return ErrorMissingContentType and have it added to Part.Errors so that we can surface the warning to interested clients.
dcormier: > Thanks for this. Will try to review this weekend, but there are a few in the hopper before you.

Understood. Thanks for getting back to me.

> It may make sense to return ErrorMissingContentType and have it added to Part.Errors so that we can surface the warning to interested clients.

OK. I'll take a look at that.
dcormier: > It may make sense to return ErrorMissingContentType and have it added to Part.Errors so that we can surface the warning to interested clients.

OK, now the part will have that added as a warning.
dcormier: Alright, I believe I have addressed your comments.
jhillyerd: Looks great, thank you!

Patch:
diff --git a/header.go b/header.go
index d986e24..775ea73 100644
--- a/header.go
+++ b/header.go
@@ -27,6 +27,9 @@ const (
 	ctTextPlain        = "text/plain"
 	ctTextHTML         = "text/html"
 
+	// Used as a placeholder in case of malformed Content-Type headers
+	ctPlaceholder = "x-not-a-mime-type/x-not-a-mime-type"
+
 	// Standard Transfer encodings
 	cte7Bit            = "7bit"
 	cte8Bit            = "8bit"
@@ -210,6 +213,9 @@ func parseMediaType(ctype string) (mtype string, params map[string]string, err e
 			}
 		}
 	}
+	if mtype == ctPlaceholder {
+		mtype = ""
+	}
 	return mtype, params, err
 }
 
@@ -222,11 +228,19 @@ func fixMangledMediaType(mtype, sep string) string {
 	parts := strings.Split(mtype, sep)
 	mtype = ""
 	for i, p := range parts {
-		if strings.Contains(p, "=") {
-			pair := strings.Split(p, "=")
-			if strings.Contains(mtype, pair[0]+"=") {
-				// Ignore repeated parameters.
-				continue
+		switch i {
+		case 0:
+			if p == "" {
+				// The content type is completely missing. Put in a placeholder.
+				p = ctPlaceholder
+			}
+		default:
+			if strings.Contains(p, "=") {
+				pair := strings.Split(p, "=")
+				if strings.Contains(mtype, pair[0]+"=") {
+					// Ignore repeated parameters.
+					continue
+				}
 			}
 		}
 		mtype += p
diff --git a/part.go b/part.go
index 02f6925..3959f0f 100644
--- a/part.go
+++ b/part.go
@@ -114,6 +114,11 @@ func (p *Part) setupHeaders(r *bufio.Reader, defaultContentType string) error {
 	if err != nil {
 		return err
 	}
+	if mtype == "" && len(mparams) > 0 {
+		p.addWarning(
+			ErrorMissingContentType,
+			"Content-Type header has parameters but no content type")
+	}
 	p.ContentType = mtype
 	// Set disposition, filename, charset if available.
 	p.setupContentHeaders(mparams)


Test patch:
diff --git a/part_test.go b/part_test.go
index f319fc5..4436014 100644
--- a/part_test.go
+++ b/part_test.go
@@ -740,3 +740,47 @@ func TestClonePart(t *testing.T) {
 	clone := p.Clone(nil)
 	test.ComparePart(t, clone, p)
 }
+
+func TestBarrenContentType(t *testing.T) {
+	r := test.OpenTestData("parts", "barren-content-type.raw")
+	p, err := enmime.ReadParts(r)
+	if err != nil {
+		t.Fatal(err)
+	}
+
+	want := ""
+	if p.ContentType != want {
+		t.Errorf("ContentType %q, want %q", p.ContentType, want)
+	}
+	want = ""
+	if p.FileName != want {
+		t.Errorf("FileName %q, want %q", p.FileName, want)
+	}
+	want = ""
+	if p.Charset != want {
+		t.Errorf("Charset %q, want %q", p.Charset, want)
+	}
+	want = "attachment"
+	if p.Disposition != want {
+		t.Errorf("Disposition %q, want %q", p.Disposition, want)
+	}
+	want = ""
+	if p.ContentID != want {
+		t.Errorf("ContentID %q, want %q", p.ContentID, want)
+	}
+	satisfied := false
+	for _, perr := range p.Errors {
+		if perr.Name == enmime.ErrorMissingContentType {
+			satisfied = true
+			if perr.Severe {
+				t.Errorf("Expected Severe to be false, got true for %q", perr.Name)
+			}
+		}
+	}
+	if !satisfied {
+		t.Errorf(
+			"Did not find expected error on part. Expected %q, but had: %v",
+			enmime.ErrorMissingContentType,
+			p.Errors)
+	}
+}
diff --git a/testdata/parts/barren-content-type.raw b/testdata/parts/barren-content-type.raw
new file mode 100644
index 0000000..5ff5ec6
--- /dev/null
+++ b/testdata/parts/barren-content-type.raw
@@ -0,0 +1,6 @@
+Content-Type: ;
+	name=""
+Content-Transfer-Encoding: base64
+Content-Disposition: attachment;
+	filename=""
+