diff --git a/header.go b/header.go index d4ec1da2..d838079d 100644 --- a/header.go +++ b/header.go @@ -3,6 +3,7 @@ package message import ( "mime" "net/textproto" + "regexp" "strings" "github.com/emersion/go-message/charset" @@ -10,6 +11,9 @@ import ( const maxHeaderLen = 76 +// Regexp that detects Quoted Printable (QP) characters +var qpReg = regexp.MustCompile("(=[0-9A-Z]{2,2})+") + func parseHeaderWithParams(s string) (f string, params map[string]string, err error) { f, params, err = mime.ParseMediaType(s) if err != nil { @@ -59,8 +63,24 @@ func formatHeaderField(k, v string) string { folding = "\r\n" } } else { + // Find the last QP character before limit + foldAtQP := qpReg.FindAllStringIndex(v[:foldBefore], -1) // Find the closest whitespace before i - foldAt = strings.LastIndexAny(v[:foldBefore], " \t\n") + foldAtEOL := strings.LastIndexAny(v[:foldBefore], " \t\n") + + // Fold at the latest whitespace by default + foldAt = foldAtEOL + + // if there are QP characters in the string + if len(foldAtQP) > 0 { + // Get the start index of the last QP character + foldAtQPLastIndex := foldAtQP[len(foldAtQP)-1][0] + if foldAtQPLastIndex > foldAt { + // Fold at the latest QP character if there are no whitespaces after it and before line hard limit + foldAt = foldAtQPLastIndex + } + } + if foldAt == 0 { // The whitespace we found was the previous folding WSP foldAt = foldBefore - 1 diff --git a/header_test.go b/header_test.go index f2dba815..455120ca 100644 --- a/header_test.go +++ b/header_test.go @@ -59,6 +59,21 @@ var formatHeaderFieldTests = []struct { v: "This is yet \t another subject \t with many whitespace characters", formatted: "Subject: This is yet \t another subject \t \r\n with many whitespace characters\r\n", }, + { + k: "Subject", + v: "=?utf-8?q?=E2=80=9CDeveloper_reads_customer_requested_change.=E2=80=9D=0A?= =?utf-8?q?=0ACaravaggio=0A=0AOil_on...?=", + formatted: "Subject: =?utf-8?q?=E2=80=9CDeveloper_reads_customer_requested_change.\r\n =E2=80=9D=0A?= =?utf-8?q?=0ACaravaggio=0A=0AOil_on...?=\r\n", + }, + { + k: "Subject", + v: "=?utf-8?q?=E2=80=9CShort subject=E2=80=9D=0A?= =?utf-8?q?=0AAuthor=0A=0AOil_on...?=", + formatted: "Subject: =?utf-8?q?=E2=80=9CShort subject=E2=80=9D=0A?= =?utf-8?q?\r\n =0AAuthor=0A=0AOil_on...?=\r\n", + }, + { + k: "Subject", + v: "=?utf-8?q?=E2=80=9CVery long subject very long subject very long subject very long subject=E2=80=9D=0A?= =?utf-8?q?=0ALong second part of subject long second part of subject long second part of subject long subject=0A=0AOil_on...?=", + formatted: "Subject: =?utf-8?q?=E2=80=9CVery long subject very long subject very long\r\n subject very long subject=E2=80=9D=0A?= =?utf-8?q?=0ALong second part of\r\n subject long second part of subject long second part of subject long\r\n subject=0A=0AOil_on...?=\r\n", + }, { k: "DKIM-Signature", v: "v=1;\r\n h=From:To:Reply-To:Subject:Message-ID:References:In-Reply-To:MIME-Version;\r\n d=example.org\r\n",