Repository: jhillyerd/enmime
Pull request #105 (2018-12-03 15:33:57)

Requaos/unquoted specials index out of range
Minor fix

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

Coverage increased (+0.02%) to 87.147% when pulling **2bbbda67202bebd006df04f28f6e8ef04e320c2a on requaos:requaos/unquotedSpecialsIndexOutOfRange** into **b2ef4130fb4a5c34369640aa611f9c7f2e4d675f on jhillyerd:develop**.

requaos: @jhillyerd If you are opposed to my last two commits piggy-backing on this PR for a minor content-type fix, I can open a seperate PR for them.
jhillyerd: Looks good to me.

Patch:
diff --git a/header.go b/header.go
index fae13e4..84f3e07 100644
--- a/header.go
+++ b/header.go
@@ -256,6 +256,13 @@ func fixMangledMediaType(mtype, sep string) string {
 				// Ignore repeated parameters.
 				continue
 			}
+			if strings.ContainsAny(pair[0], "()<>@,;:\"\\/[]?") {
+				// attribute is a strict token and cannot be a quoted-string
+				// if any of the above characters are present in a token it
+				// must be quoted and is therefor an invalid attribute.
+				// Discard the pair.
+				continue
+			}
 		}
 		mtype += p
 		// Only terminate with semicolon if not the last parameter and if it doesn't already have a
@@ -329,84 +336,93 @@ findValueStart:
 		}
 	}
 
-	s = s[i+1:]
+	if len(s)-i < 1 {
+		// parameter value starts at the end of the string, make empty
+		// quoted string to play nice with mime.ParseMediaType
+		param.WriteString(`""`)
 
-	quoteIfUnquoted := func() {
-		if !valueQuoteNeeded {
-			if !valueQuoteAdded {
-				param.WriteByte('"')
-
-				valueQuoteAdded = true
-			}
+	} else {
+		// The beginning of the value is not at the end of the string
 
-			valueQuoteNeeded = true
-		}
-	}
-
-findValueEnd:
-	for len(s) > 0 {
-		switch s[0] {
-		case ';', ' ', '\t':
-			if valueQuotedOriginally {
-				// We're in a quoted string, so whitespace is allowed.
-				value.WriteByte(s[0])
-				s = s[1:]
-				break
-			}
+		s = s[i+1:]
 
-			// Otherwise, we've reached the end of an unquoted value.
+		quoteIfUnquoted := func() {
+			if !valueQuoteNeeded {
+				if !valueQuoteAdded {
+					param.WriteByte('"')
 
-			param.WriteString(value.String())
-			value.Reset()
+					valueQuoteAdded = true
+				}
 
-			if valueQuoteNeeded {
-				param.WriteByte('"')
+				valueQuoteNeeded = true
 			}
+		}
 
-			param.WriteByte(s[0])
-			s = s[1:]
+	findValueEnd:
+		for len(s) > 0 {
+			switch s[0] {
+			case ';', ' ', '\t':
+				if valueQuotedOriginally {
+					// We're in a quoted string, so whitespace is allowed.
+					value.WriteByte(s[0])
+					s = s[1:]
+					break
+				}
 
-			break findValueEnd
+				// Otherwise, we've reached the end of an unquoted value.
 
-		case '"':
-			if valueQuotedOriginally {
-				// We're in a quoted value. This is the end of that value.
 				param.WriteString(value.String())
 				value.Reset()
 
+				if valueQuoteNeeded {
+					param.WriteByte('"')
+				}
+
 				param.WriteByte(s[0])
 				s = s[1:]
 
 				break findValueEnd
-			}
 
-			quoteIfUnquoted()
+			case '"':
+				if valueQuotedOriginally {
+					// We're in a quoted value. This is the end of that value.
+					param.WriteString(value.String())
+					value.Reset()
 
-			value.WriteByte('\\')
-			value.WriteByte(s[0])
-			s = s[1:]
+					param.WriteByte(s[0])
+					s = s[1:]
 
-		case '\\':
-			if len(s) > 1 {
+					break findValueEnd
+				}
+
+				quoteIfUnquoted()
+
+				value.WriteByte('\\')
 				value.WriteByte(s[0])
 				s = s[1:]
 
-				// Backslash escapes the next char. Consume that next char.
-				value.WriteByte(s[0])
+			case '\\':
+				if len(s) > 1 {
+					value.WriteByte(s[0])
+					s = s[1:]
 
-				quoteIfUnquoted()
-			}
-			// Else there is no next char to consume.
-			s = s[1:]
+					// Backslash escapes the next char. Consume that next char.
+					value.WriteByte(s[0])
 
-		case '(', ')', '<', '>', '@', ',', ':', '/', '[', ']', '?', '=':
-			quoteIfUnquoted()
+					quoteIfUnquoted()
+				}
+				// Else there is no next char to consume.
+				s = s[1:]
 
-			fallthrough
+			case '(', ')', '<', '>', '@', ',', ':', '/', '[', ']', '?', '=':
+				quoteIfUnquoted()
 
-		default:
-			value.WriteByte(s[0])
-			s = s[1:]
+				fallthrough
+
+			default:
+				value.WriteByte(s[0])
+				s = s[1:]
+			}
 		}
 	}
 


Test patch:
diff --git a/header_test.go b/header_test.go
index 4a3c3c3..171ff20 100644
--- a/header_test.go
+++ b/header_test.go
@@ -165,7 +165,18 @@ func TestFixMangledMediaType(t *testing.T) {
 		{
 			input: "",
 			sep:   "",
-			want:  ""},
+			want:  "",
+		},
+		{
+			input: `text/HTML; charset=UTF-8; format=flowed; content-transfer-encoding: 7bit=`,
+			sep:   ";",
+			want:  "text/HTML; charset=UTF-8; format=flowed",
+		},
+		{
+			input: "text/html;charset=",
+			sep:   ";",
+			want:  "text/html;charset=",
+		},
 		{
 			input: "application/pdf name=\"file.pdf\"",
 			sep:   " ",
@@ -299,6 +310,14 @@ func TestFixUnquotedSpecials(t *testing.T) {
 			input: `text/HTML; charset="UTF-8Return-Path: bounce-810_HTML-769869545-477063-1070564-43@bounce.email.oflce57578375.com`,
 			want:  `text/HTML; charset="UTF-8Return-Path: bounce-810_HTML-769869545-477063-1070564-43@bounce.email.oflce57578375.com"`,
 		},
+		{
+			input: `text/html;charset=`,
+			want:  `text/html;charset=""`,
+		},
+		{
+			input: `text/html;charset="`,
+			want:  `text/html;charset=""`,
+		},
 	}
 	for _, tc := range testCases {
 		t.Run(tc.input, func(t *testing.T) {
diff --git a/part_test.go b/part_test.go
index 6982471..ba18775 100644
--- a/part_test.go
+++ b/part_test.go
@@ -828,7 +828,7 @@ func TestContentTypeParamMissingClosingQuote(t *testing.T) {
 	wantp := &enmime.Part{
 		PartID:      "0",
 		ContentType: "text/html",
-		Charset:     "UTF-8Return-Path:;bounce-810_HTML-769869545-477063-1070564-43@bounce.email.oflce57578375.com=not-a-param-value",
+		Charset:     "UTF-8Return-Path:",
 	}
 	test.ComparePart(t, p, wantp)