Skip to content

Commit

Permalink
Add qpCleaner, a Reader that fixes quoted-printable
Browse files Browse the repository at this point in the history
- qpCleaner escapes invalid bytes in a quoted-printable encoded stream,
  this prevents Golang's quoted-printable decoder from blowing up on
  poorly encoded MIME parts.
- Benchmarked this ByteReader implementation against one that used
  a bytes.Buffer.  This was slightly faster and much easier to read.
  • Loading branch information
jhillyerd committed Feb 12, 2017
1 parent 23dadad commit 7706cdf
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 0 deletions.
1 change: 1 addition & 0 deletions part.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,7 @@ func (p *Part) buildContentReaders(r io.Reader) error {
encoding := p.Header.Get(hnContentEncoding)
switch strings.ToLower(encoding) {
case "quoted-printable":
contentReader = newQPCleaner(contentReader.(io.ByteReader))
contentReader = quotedprintable.NewReader(contentReader)
case "base64":
contentReader = newBase64Cleaner(contentReader)
Expand Down
53 changes: 53 additions & 0 deletions quotedprint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package enmime

import (
"fmt"
"io"
)

// qpCleaner scans quoted printable content for invalid characters and encodes them so that
// Go's quoted-printable decoder does not abort with an error.
type qpCleaner struct {
in io.ByteReader
}

// Assert qpCleaner implements io.Reader
var _ io.Reader = &qpCleaner{}

// newBase64Cleaner returns a Base64Cleaner object for the specified reader. Base64Cleaner
// implements the io.Reader interface.
func newQPCleaner(r io.ByteReader) *qpCleaner {
return &qpCleaner{
in: r,
}
}

// Read method for io.Reader interface. Reasonably efficient for well-formed quoted-printable
// streams. Less so when invalid characters are encountered; reads will be short.
func (qp *qpCleaner) Read(dest []byte) (n int, err error) {
// Ensure room to write a byte or =XX string
destLen := len(dest) - 3
// Loop over bytes in qp.in ByteReader
for n < destLen {
b, err := qp.in.ReadByte()
if err != nil {
return n, err
}
// Test character type
switch {
case b == '\t' || b == '\r' || b == '\n':
// Valid special characters
dest[n] = b
n++
case b < ' ' || '~' < b:
// Invalid character, render quoted-printable into buffer
s := fmt.Sprintf("=%02X", b)
n += copy(dest[n:], s)
default:
// Acceptable character
dest[n] = b
n++
}
}
return
}
87 changes: 87 additions & 0 deletions quotedprint_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package enmime

import (
"bytes"
"io"
"strings"
"testing"
)

func TestQPCleaner(t *testing.T) {
ttable := []struct {
input string
want string
}{
{"", ""},
{"abcDEF_", "abcDEF_"},
{"=5bSlack=5d", "=5bSlack=5d"},
{"low: ,high:~", "low: ,high:~"},
{"\r\n\t", "\r\n\t"},
{"pédagogues", "p=C3=A9dagogues"},
{"Stuffs’s", "Stuffs=E2=80=99s"},
}

for _, tc := range ttable {
// Run cleaner
cleaner := newQPCleaner(strings.NewReader(tc.input))
buf := new(bytes.Buffer)
_, err := buf.ReadFrom(cleaner)
if err != nil {
t.Fatal(err)
}

got := buf.String()
if got != tc.want {
t.Errorf("Got: %q, want: %q", got, tc.want)
}
}
}

// TestQPCleanerOverflow attempts to confuse the cleaner by issuing a smaller subsequent read
func TestQPCleanerOverflow(t *testing.T) {
input := bytes.Repeat([]byte("pédagogues =\r\n"), 1000)
want := bytes.Repeat([]byte("p=C3=A9dagogues =\r\n"), 1000)
inbuf := bytes.NewBuffer(input)
qp := newQPCleaner(inbuf)

offset := 0
for len := 1000; len > 0; len -= 100 {
p := make([]byte, len)
n, err := qp.Read(p)
if err != nil {
t.Fatal(err)
}
if n < 1 {
t.Fatalf("Read(p) = %v, wanted >0", n)
}
for i := 0; i < n; i++ {
if p[i] != want[offset] {
t.Errorf("p[%v] = %q, want: %q (want[%v])", i, p[i], want[offset], offset)
}
offset++
}
}
}

var result int

func BenchmarkQPCleaner(b *testing.B) {
b.StopTimer()
input := bytes.Repeat([]byte("pédagogues =\r\n"), b.N)
b.SetBytes(int64(len(input)))
inbuf := bytes.NewBuffer(input)
qp := newQPCleaner(inbuf)
p := make([]byte, 1024)
b.StartTimer()

for {
n, err := qp.Read(p)
result += n
if err == io.EOF {
break
}
if err != nil {
b.Fatalf("Read(): %v", err)
}
}
}

0 comments on commit 7706cdf

Please sign in to comment.