Skip to content

Commit c83e537

Browse files
committed
textproto: don't insert whitespace in long header field values
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
1 parent 8f23f91 commit c83e537

File tree

2 files changed

+83
-56
lines changed

2 files changed

+83
-56
lines changed

textproto/header.go

+77-55
Original file line numberDiff line numberDiff line change
@@ -424,11 +424,75 @@ func ReadHeader(r *bufio.Reader) (Header, error) {
424424
}
425425
}
426426

427-
const maxHeaderLen = 76
428-
429427
// Regexp that detects Quoted Printable (QP) characters
430428
var qpReg = regexp.MustCompile("(=[0-9A-Z]{2,2})+")
431429

430+
func foldLine(v string, maxlen int) (line, next string, ok bool) {
431+
ok = true
432+
433+
// We'll need to fold before maxlen
434+
foldBefore := maxlen + 1
435+
foldAt := len(v)
436+
437+
var folding string
438+
if foldBefore > len(v) {
439+
// We reached the end of the string
440+
if v[len(v)-1] != '\n' {
441+
// If there isn't already a trailing CRLF, insert one
442+
folding = "\r\n"
443+
}
444+
} else {
445+
// Find the last QP character before limit
446+
foldAtQP := qpReg.FindAllStringIndex(v[:foldBefore], -1)
447+
// Find the closest whitespace before maxlen
448+
foldAtEOL := strings.LastIndexAny(v[:foldBefore], " \t\n")
449+
450+
// Fold at the latest whitespace by default
451+
foldAt = foldAtEOL
452+
453+
// if there are QP characters in the string
454+
if len(foldAtQP) > 0 {
455+
// Get the start index of the last QP character
456+
foldAtQPLastIndex := foldAtQP[len(foldAtQP)-1][0]
457+
if foldAtQPLastIndex > foldAt {
458+
// Fold at the latest QP character if there are no whitespaces
459+
// after it and before line length limit
460+
foldAt = foldAtQPLastIndex
461+
}
462+
}
463+
464+
if foldAt == 0 {
465+
// The whitespace we found was the previous folding WSP
466+
foldAt = foldBefore - 1
467+
} else if foldAt < 0 {
468+
// We didn't find any whitespace, we have to insert one
469+
foldAt = foldBefore - 2
470+
}
471+
472+
switch v[foldAt] {
473+
case ' ', '\t':
474+
if v[foldAt-1] != '\n' {
475+
folding = "\r\n" // The next char will be a WSP, don't need to insert one
476+
}
477+
case '\n':
478+
folding = "" // There is already a CRLF, nothing to do
479+
default:
480+
// Another char, we need to insert CRLF + WSP. This will insert an
481+
// extra space in the string, so this should be avoided if
482+
// possible.
483+
folding = "\r\n "
484+
ok = len(foldAtQP) > 0
485+
}
486+
}
487+
488+
return v[:foldAt] + folding, v[foldAt:], ok
489+
}
490+
491+
const (
492+
preferredHeaderLen = 76
493+
maxHeaderLen = 998
494+
)
495+
432496
// formatHeaderField formats a header field, ensuring each line is no longer
433497
// than 76 characters. It tries to fold lines at whitespace characters if
434498
// possible. If the header contains a word longer than this limit, it will be
@@ -442,63 +506,21 @@ func formatHeaderField(k, v string) string {
442506

443507
first := true
444508
for len(v) > 0 {
445-
maxlen := maxHeaderLen
509+
// If this is the first line, substract the length of the key
510+
keylen := 0
446511
if first {
447-
maxlen -= len(s)
512+
keylen = len(s)
448513
}
449514

450-
// We'll need to fold before i
451-
foldBefore := maxlen + 1
452-
foldAt := len(v)
453-
454-
var folding string
455-
if foldBefore > len(v) {
456-
// We reached the end of the string
457-
if v[len(v)-1] != '\n' {
458-
// If there isn't already a trailing CRLF, insert one
459-
folding = "\r\n"
460-
}
461-
} else {
462-
// Find the last QP character before limit
463-
foldAtQP := qpReg.FindAllStringIndex(v[:foldBefore], -1)
464-
// Find the closest whitespace before i
465-
foldAtEOL := strings.LastIndexAny(v[:foldBefore], " \t\n")
466-
467-
// Fold at the latest whitespace by default
468-
foldAt = foldAtEOL
469-
470-
// if there are QP characters in the string
471-
if len(foldAtQP) > 0 {
472-
// Get the start index of the last QP character
473-
foldAtQPLastIndex := foldAtQP[len(foldAtQP)-1][0]
474-
if foldAtQPLastIndex > foldAt {
475-
// Fold at the latest QP character if there are no whitespaces after it and before line hard limit
476-
foldAt = foldAtQPLastIndex
477-
}
478-
}
479-
480-
if foldAt == 0 {
481-
// The whitespace we found was the previous folding WSP
482-
foldAt = foldBefore - 1
483-
} else if foldAt < 0 {
484-
// We didn't find any whitespace, we have to insert one
485-
foldAt = foldBefore - 2
486-
}
487-
488-
switch v[foldAt] {
489-
case ' ', '\t':
490-
if v[foldAt-1] != '\n' {
491-
folding = "\r\n" // The next char will be a WSP, don't need to insert one
492-
}
493-
case '\n':
494-
folding = "" // There is already a CRLF, nothing to do
495-
default:
496-
folding = "\r\n " // Another char, we need to insert CRLF + WSP
497-
}
515+
// First try with a soft limit
516+
l, next, ok := foldLine(v, preferredHeaderLen - keylen)
517+
if !ok {
518+
// Folding failed to preserve the original header field value. Try
519+
// with a larger, hard limit.
520+
l, next, _ = foldLine(v, maxHeaderLen - keylen)
498521
}
499-
500-
s += v[:foldAt] + folding
501-
v = v[foldAt:]
522+
v = next
523+
s += l
502524
first = false
503525
}
504526

textproto/header_test.go

+6-1
Original file line numberDiff line numberDiff line change
@@ -303,6 +303,11 @@ var formatHeaderFieldTests = []struct {
303303
v: "This is yet \t another subject \t with many whitespace characters",
304304
formatted: "Subject: This is yet \t another subject \t \r\n with many whitespace characters\r\n",
305305
},
306+
{
307+
k: "In-Reply-To",
308+
v: "<CAOzTU0ikmAnr1ebhLEfks2crdHcotR-cXeJ-7ySd4X4VJ-B2fg@mail.gmail.com>",
309+
formatted: "In-Reply-To: <CAOzTU0ikmAnr1ebhLEfks2crdHcotR-cXeJ-7ySd4X4VJ-B2fg@mail.gmail.com>\r\n",
310+
},
306311
{
307312
k: "Subject",
308313
v: "=?utf-8?q?=E2=80=9CDeveloper_reads_customer_requested_change.=E2=80=9D=0A?= =?utf-8?q?=0ACaravaggio=0A=0AOil_on...?=",
@@ -325,7 +330,7 @@ var formatHeaderFieldTests = []struct {
325330
},
326331
{
327332
k: "DKIM-Signature",
328-
v: "v=1; h=From; d=example.org; b=AuUoFEfDxTDkHlLXSZEpZj79LICEps6eda7W3deTVFOk4yAUoqOB4nujc7YopdG5dWLSdNg6xNAZpOPr+kHxt1IrE+NahM6L/LbvaHutKVdkLLkpVaVVQPzeRDI009SO2Il5Lu7rDNH6mZckBdrIx0orEtZV4bmp/YzhwvcubU4=\r\n",
333+
v: "v=1; h=From; d=example.org; b=AuUoFEfDxTDkHlLXSZEpZj79LICEps6eda7W3deTVFOk4yAUoqOB4nujc7YopdG5dWLSdNg6x NAZpOPr+kHxt1IrE+NahM6L/LbvaHutKVdkLLkpVaVVQPzeRDI009SO2Il5Lu7rDNH6mZckBdrI x0orEtZV4bmp/YzhwvcubU4=\r\n",
329334
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",
330335
},
331336
{

0 commit comments

Comments
 (0)