1// Package quotedprintable implements quoted-printable encoding as specified by
2// RFC 2045.
3package quotedprintable
4
5import (
6	"bufio"
7	"bytes"
8	"fmt"
9	"io"
10)
11
12// Reader is a quoted-printable decoder.
13type Reader struct {
14	br   *bufio.Reader
15	rerr error  // last read error
16	line []byte // to be consumed before more of br
17}
18
19// NewReader returns a quoted-printable reader, decoding from r.
20func NewReader(r io.Reader) *Reader {
21	return &Reader{
22		br: bufio.NewReader(r),
23	}
24}
25
26func fromHex(b byte) (byte, error) {
27	switch {
28	case b >= '0' && b <= '9':
29		return b - '0', nil
30	case b >= 'A' && b <= 'F':
31		return b - 'A' + 10, nil
32	// Accept badly encoded bytes.
33	case b >= 'a' && b <= 'f':
34		return b - 'a' + 10, nil
35	}
36	return 0, fmt.Errorf("quotedprintable: invalid hex byte 0x%02x", b)
37}
38
39func readHexByte(a, b byte) (byte, error) {
40	var hb, lb byte
41	var err error
42	if hb, err = fromHex(a); err != nil {
43		return 0, err
44	}
45	if lb, err = fromHex(b); err != nil {
46		return 0, err
47	}
48	return hb<<4 | lb, nil
49}
50
51func isQPDiscardWhitespace(r rune) bool {
52	switch r {
53	case '\n', '\r', ' ', '\t':
54		return true
55	}
56	return false
57}
58
59var (
60	crlf       = []byte("\r\n")
61	lf         = []byte("\n")
62	softSuffix = []byte("=")
63)
64
65// Read reads and decodes quoted-printable data from the underlying reader.
66func (r *Reader) Read(p []byte) (n int, err error) {
67	// Deviations from RFC 2045:
68	// 1. in addition to "=\r\n", "=\n" is also treated as soft line break.
69	// 2. it will pass through a '\r' or '\n' not preceded by '=', consistent
70	//    with other broken QP encoders & decoders.
71	for len(p) > 0 {
72		if len(r.line) == 0 {
73			if r.rerr != nil {
74				return n, r.rerr
75			}
76			r.line, r.rerr = r.br.ReadSlice('\n')
77
78			// Does the line end in CRLF instead of just LF?
79			hasLF := bytes.HasSuffix(r.line, lf)
80			hasCR := bytes.HasSuffix(r.line, crlf)
81			wholeLine := r.line
82			r.line = bytes.TrimRightFunc(wholeLine, isQPDiscardWhitespace)
83			if bytes.HasSuffix(r.line, softSuffix) {
84				rightStripped := wholeLine[len(r.line):]
85				r.line = r.line[:len(r.line)-1]
86				if !bytes.HasPrefix(rightStripped, lf) && !bytes.HasPrefix(rightStripped, crlf) {
87					r.rerr = fmt.Errorf("quotedprintable: invalid bytes after =: %q", rightStripped)
88				}
89			} else if hasLF {
90				if hasCR {
91					r.line = append(r.line, '\r', '\n')
92				} else {
93					r.line = append(r.line, '\n')
94				}
95			}
96			continue
97		}
98		b := r.line[0]
99
100		switch {
101		case b == '=':
102			if len(r.line[1:]) < 2 {
103				return n, io.ErrUnexpectedEOF
104			}
105			b, err = readHexByte(r.line[1], r.line[2])
106			if err != nil {
107				return n, err
108			}
109			r.line = r.line[2:] // 2 of the 3; other 1 is done below
110		case b == '\t' || b == '\r' || b == '\n':
111			break
112		case b < ' ' || b > '~':
113			return n, fmt.Errorf("quotedprintable: invalid unescaped byte 0x%02x in body", b)
114		}
115		p[0] = b
116		p = p[1:]
117		r.line = r.line[1:]
118		n++
119	}
120	return n, nil
121}
122