Repository: jhillyerd/enmime (go)

Issue:
Unquoted param in Content-Type header is not automatically quoted
What I did:
Tried to parse a mime file with an attachment whose file name had spaces and was unquoted in the `Content-Type` header.

What I expected:
The package would quote the unquoted string before processing

What I got:
`got err mime: invalid media parameter`

Release or branch I am using:
master

(Please attach a sample message if you feel it will help reproduce the issue)

https://github.com/jhillyerd/enmime/pull/183
I created a failing test to replicated the issue on the above PR. Would be willing to submit a fix, however I am unsure if this is a case that this package is willing to cover.

Thanks for the great package!

Pull request:
header: Quote values with spaces
Closes #184


Patch:
diff --git a/header.go b/header.go
index 4462382..290a0bd 100644
--- a/header.go
+++ b/header.go
@@ -361,6 +361,7 @@ func consumeParam(s string) (consumed, rest string) {
 		return "", s
 	}
 
+	// Write out parameter name.
 	param := strings.Builder{}
 	param.WriteString(s[:i+1])
 	s = s[i+1:]
@@ -388,7 +389,9 @@ findValueStart:
 
 		case ';':
 			if value.Len() == 0 {
-				value.WriteString(`"";`)
+				// Value was empty, return immediately.
+				param.WriteString(`"";`)
+				return param.String(), s[i+1:]
 			}
 
 			break findValueStart
@@ -444,7 +447,7 @@ findValueStart:
 			}
 
 			switch r {
-			case ';', ' ', '\t':
+			case ';':
 				if valueQuotedOriginally {
 					// We're in a quoted string, so whitespace is allowed.
 					value.WriteRune(r)
@@ -455,6 +458,17 @@ findValueStart:
 				rest = s[i:]
 				break findValueEnd
 
+			case ' ', '\t':
+				if valueQuotedOriginally {
+					// We're in a quoted string, so whitespace is allowed.
+					value.WriteRune(r)
+					break
+				}
+
+				// This string contains whitespace, must be quoted.
+				quoteIfUnquoted()
+				value.WriteRune(r)
+
 			case '"':
 				if valueQuotedOriginally {
 					// We're in a quoted value. This is the end of that value.
@@ -463,13 +477,12 @@ findValueStart:
 				}
 
 				quoteIfUnquoted()
-
 				value.WriteByte('\\')
 				value.WriteRune(r)
 
 			case '\\':
 				if i < len(s)-1 {
-					// If next char is present, escape it with backslash
+					// If next char is present, escape it with backslash.
 					value.WriteRune(r)
 					escaped = true
 					quoteIfUnquoted()
@@ -477,7 +490,6 @@ findValueStart:
 
 			case '(', ')', '<', '>', '@', ',', ':', '/', '[', ']', '?', '=':
 				quoteIfUnquoted()
-
 				fallthrough
 
 			default:


Test patch:
diff --git a/header_test.go b/header_test.go
index 09e1418..a9c6ca5 100644
--- a/header_test.go
+++ b/header_test.go
@@ -438,12 +438,22 @@ func TestFixUnquotedSpecials(t *testing.T) {
 			input: "application/msword;name=\xe2\x28\xa1.doc",
 			want:  `application/msword;name="=?utf-8?b?77+9KO+/vS5kb2M=?="`,
 		},
+		{
+			// Value with spaces is surrounded with quotes.
+			input: `text/plain; name=Untitled document.txt`,
+			want:  `text/plain; name="Untitled document.txt"`,
+		},
+		{
+			// Value with spaces is surrounded with quotes.
+			input: `text/plain; name=Untitled document.txt; disposition=inline`,
+			want:  `text/plain; name="Untitled document.txt"; disposition=inline`,
+		},
 	}
 	for _, tc := range testCases {
 		t.Run(tc.input, func(t *testing.T) {
 			got := fixUnquotedSpecials(tc.input)
 			if got != tc.want {
-				t.Errorf("\ngot:  %s\nwant: %s", got, tc.want)
+				t.Errorf("\ngot  : %s\nwant : %s\ninput: %s", got, tc.want, tc.input)
 			}
 		})
 	}
@@ -901,6 +911,12 @@ func TestParseMediaType(t *testing.T) {
 			mtype:  "application/pdf",
 			params: map[string]string{"name": "foo", "format": "flowed"},
 		},
+		{
+			label:  "unquoted with spaces",
+			input:  "application/pdf; x-unix-mode=0644; name=File name with spaces.pdf",
+			mtype:  "application/pdf",
+			params: map[string]string{"x-unix-mode": "0644", "name": "File name with spaces.pdf"},
+		},
 	}
 	for _, tc := range testCases {
 		t.Run(tc.label, func(t *testing.T) {