Skip to content

Commit

Permalink
textproto: don't insert whitespace in long header field values
Browse files Browse the repository at this point in the history
Inserting whitespace after 76 chars can break header field values like
In-Reply-To. Only do so at the hard 998 char limit.

Closes: #44
  • Loading branch information
emersion committed Jul 27, 2019
1 parent 8f23f91 commit c83e537
Show file tree
Hide file tree
Showing 2 changed files with 83 additions and 56 deletions.
132 changes: 77 additions & 55 deletions textproto/header.go
Original file line number Diff line number Diff line change
Expand Up @@ -424,11 +424,75 @@ func ReadHeader(r *bufio.Reader) (Header, error) {
}
}

const maxHeaderLen = 76

// Regexp that detects Quoted Printable (QP) characters
var qpReg = regexp.MustCompile("(=[0-9A-Z]{2,2})+")

func foldLine(v string, maxlen int) (line, next string, ok bool) {
ok = true

// We'll need to fold before maxlen
foldBefore := maxlen + 1
foldAt := len(v)

var folding string
if foldBefore > len(v) {
// We reached the end of the string
if v[len(v)-1] != '\n' {
// If there isn't already a trailing CRLF, insert one
folding = "\r\n"
}
} else {
// Find the last QP character before limit
foldAtQP := qpReg.FindAllStringIndex(v[:foldBefore], -1)
// Find the closest whitespace before maxlen
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 length limit
foldAt = foldAtQPLastIndex
}
}

if foldAt == 0 {
// The whitespace we found was the previous folding WSP
foldAt = foldBefore - 1
} else if foldAt < 0 {
// We didn't find any whitespace, we have to insert one
foldAt = foldBefore - 2
}

switch v[foldAt] {
case ' ', '\t':
if v[foldAt-1] != '\n' {
folding = "\r\n" // The next char will be a WSP, don't need to insert one
}
case '\n':
folding = "" // There is already a CRLF, nothing to do
default:
// Another char, we need to insert CRLF + WSP. This will insert an
// extra space in the string, so this should be avoided if
// possible.
folding = "\r\n "
ok = len(foldAtQP) > 0
}
}

return v[:foldAt] + folding, v[foldAt:], ok
}

const (
preferredHeaderLen = 76
maxHeaderLen = 998
)

// formatHeaderField formats a header field, ensuring each line is no longer
// than 76 characters. It tries to fold lines at whitespace characters if
// possible. If the header contains a word longer than this limit, it will be
Expand All @@ -442,63 +506,21 @@ func formatHeaderField(k, v string) string {

first := true
for len(v) > 0 {
maxlen := maxHeaderLen
// If this is the first line, substract the length of the key
keylen := 0
if first {
maxlen -= len(s)
keylen = len(s)
}

// We'll need to fold before i
foldBefore := maxlen + 1
foldAt := len(v)

var folding string
if foldBefore > len(v) {
// We reached the end of the string
if v[len(v)-1] != '\n' {
// If there isn't already a trailing CRLF, insert one
folding = "\r\n"
}
} else {
// Find the last QP character before limit
foldAtQP := qpReg.FindAllStringIndex(v[:foldBefore], -1)
// Find the closest whitespace before i
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
} else if foldAt < 0 {
// We didn't find any whitespace, we have to insert one
foldAt = foldBefore - 2
}

switch v[foldAt] {
case ' ', '\t':
if v[foldAt-1] != '\n' {
folding = "\r\n" // The next char will be a WSP, don't need to insert one
}
case '\n':
folding = "" // There is already a CRLF, nothing to do
default:
folding = "\r\n " // Another char, we need to insert CRLF + WSP
}
// First try with a soft limit
l, next, ok := foldLine(v, preferredHeaderLen - keylen)
if !ok {
// Folding failed to preserve the original header field value. Try
// with a larger, hard limit.
l, next, _ = foldLine(v, maxHeaderLen - keylen)
}

s += v[:foldAt] + folding
v = v[foldAt:]
v = next
s += l
first = false
}

Expand Down
7 changes: 6 additions & 1 deletion textproto/header_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,11 @@ 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: "In-Reply-To",
v: "<CAOzTU0ikmAnr1ebhLEfks2crdHcotR-cXeJ-7ySd4X4VJ-B2fg@mail.gmail.com>",
formatted: "In-Reply-To: <CAOzTU0ikmAnr1ebhLEfks2crdHcotR-cXeJ-7ySd4X4VJ-B2fg@mail.gmail.com>\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...?=",
Expand All @@ -325,7 +330,7 @@ var formatHeaderFieldTests = []struct {
},
{
k: "DKIM-Signature",
v: "v=1; h=From; d=example.org; b=AuUoFEfDxTDkHlLXSZEpZj79LICEps6eda7W3deTVFOk4yAUoqOB4nujc7YopdG5dWLSdNg6xNAZpOPr+kHxt1IrE+NahM6L/LbvaHutKVdkLLkpVaVVQPzeRDI009SO2Il5Lu7rDNH6mZckBdrIx0orEtZV4bmp/YzhwvcubU4=\r\n",
v: "v=1; h=From; d=example.org; b=AuUoFEfDxTDkHlLXSZEpZj79LICEps6eda7W3deTVFOk4yAUoqOB4nujc7YopdG5dWLSdNg6x NAZpOPr+kHxt1IrE+NahM6L/LbvaHutKVdkLLkpVaVVQPzeRDI009SO2Il5Lu7rDNH6mZckBdrI x0orEtZV4bmp/YzhwvcubU4=\r\n",
formatted: "Dkim-Signature: v=1; h=From; d=example.org;\r\n b=AuUoFEfDxTDkHlLXSZEpZj79LICEps6eda7W3deTVFOk4yAUoqOB4nujc7YopdG5dWLSdNg6x\r\n NAZpOPr+kHxt1IrE+NahM6L/LbvaHutKVdkLLkpVaVVQPzeRDI009SO2Il5Lu7rDNH6mZckBdrI\r\n x0orEtZV4bmp/YzhwvcubU4=\r\n",
},
{
Expand Down

0 comments on commit c83e537

Please sign in to comment.