forked from jhillyerd/enmime
-
Notifications
You must be signed in to change notification settings - Fork 1
/
encode.go
214 lines (200 loc) · 5.25 KB
/
encode.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
package enmime
import (
"bufio"
"encoding/base64"
"io"
"mime"
"mime/quotedprintable"
"net/textproto"
"sort"
"time"
"github.com/jhillyerd/enmime/internal/coding"
"github.com/jhillyerd/enmime/internal/stringutil"
)
// b64Percent determines the percent of non-ASCII characters enmime will tolerate before switching
// from quoted-printable to base64 encoding.
const b64Percent = 20
type transferEncoding byte
const (
te7Bit transferEncoding = iota
teQuoted
teBase64
)
var crnl = []byte{'\r', '\n'}
// Encode writes this Part and all its children to the specified writer in MIME format.
func (p *Part) Encode(writer io.Writer) error {
if p.Header == nil {
p.Header = make(textproto.MIMEHeader)
}
cte := p.setupMIMEHeaders()
// Encode this part.
b := bufio.NewWriter(writer)
p.encodeHeader(b)
if len(p.Content) > 0 {
b.Write(crnl)
if err := p.encodeContent(b, cte); err != nil {
return err
}
}
if p.FirstChild == nil {
return b.Flush()
}
// Encode children.
endMarker := []byte("\r\n--" + p.Boundary + "--")
marker := endMarker[:len(endMarker)-2]
c := p.FirstChild
for c != nil {
b.Write(marker)
b.Write(crnl)
if err := c.Encode(b); err != nil {
return err
}
c = c.NextSibling
}
b.Write(endMarker)
b.Write(crnl)
return b.Flush()
}
// setupMIMEHeaders determines content transfer encoding, generates a boundary string if required,
// then sets the Content-Type (type, charset, filename, boundary) and Content-Disposition headers.
func (p *Part) setupMIMEHeaders() transferEncoding {
// Determine content transfer encoding.
// If we are encoding a part that previously had content-transfer-encoding set, unset it so
// the correct encoding detection can be done below.
p.Header.Del(hnContentEncoding)
cte := te7Bit
if len(p.Content) > 0 {
cte = teBase64
if p.TextContent() {
cte = selectTransferEncoding(p.Content, false)
if p.Charset == "" {
p.Charset = utf8
}
}
// RFC 2045: 7bit is assumed if CTE header not present.
switch cte {
case teBase64:
p.Header.Set(hnContentEncoding, cteBase64)
case teQuoted:
p.Header.Set(hnContentEncoding, cteQuotedPrintable)
}
}
// Setup headers.
if p.FirstChild != nil && p.Boundary == "" {
// Multipart, generate random boundary marker.
p.Boundary = "enmime-" + stringutil.UUID()
}
if p.ContentID != "" {
p.Header.Set(hnContentID, coding.ToIDHeader(p.ContentID))
}
if p.ContentType != "" {
// Build content type header.
param := make(map[string]string)
for k, v := range p.ContentTypeParams {
param[k] = v
}
setParamValue(param, hpCharset, p.Charset)
setParamValue(param, hpName, stringutil.ToASCII(p.FileName))
setParamValue(param, hpBoundary, p.Boundary)
if mt := mime.FormatMediaType(p.ContentType, param); mt != "" {
p.ContentType = mt
}
p.Header.Set(hnContentType, p.ContentType)
}
if p.Disposition != "" {
// Build disposition header.
param := make(map[string]string)
setParamValue(param, hpFilename, stringutil.ToASCII(p.FileName))
if !p.FileModDate.IsZero() {
setParamValue(param, hpModDate, p.FileModDate.Format(time.RFC822))
}
if mt := mime.FormatMediaType(p.Disposition, param); mt != "" {
p.Disposition = mt
}
p.Header.Set(hnContentDisposition, p.Disposition)
}
return cte
}
// encodeHeader writes out a sorted list of headers.
func (p *Part) encodeHeader(b *bufio.Writer) {
keys := make([]string, 0, len(p.Header))
for k := range p.Header {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
for _, v := range p.Header[k] {
encv := v
switch selectTransferEncoding([]byte(v), true) {
case teBase64:
encv = mime.BEncoding.Encode(utf8, v)
case teQuoted:
encv = mime.QEncoding.Encode(utf8, v)
}
// _ used to prevent early wrapping
wb := stringutil.Wrap(76, k, ":_", encv, "\r\n")
wb[len(k)+1] = ' '
b.Write(wb)
}
}
}
// encodeContent writes out the content in the selected encoding.
func (p *Part) encodeContent(b *bufio.Writer, cte transferEncoding) (err error) {
switch cte {
case teBase64:
enc := base64.StdEncoding
text := make([]byte, enc.EncodedLen(len(p.Content)))
base64.StdEncoding.Encode(text, p.Content)
// Wrap lines.
lineLen := 76
for len(text) > 0 {
if lineLen > len(text) {
lineLen = len(text)
}
if _, err = b.Write(text[:lineLen]); err != nil {
return err
}
b.Write(crnl)
text = text[lineLen:]
}
case teQuoted:
qp := quotedprintable.NewWriter(b)
if _, err = qp.Write(p.Content); err != nil {
return err
}
err = qp.Close()
default:
_, err = b.Write(p.Content)
}
return err
}
// selectTransferEncoding scans content for non-ASCII characters and selects 'b' or 'q' encoding.
func selectTransferEncoding(content []byte, quoteLineBreaks bool) transferEncoding {
if len(content) == 0 {
return te7Bit
}
// Binary chars remaining before we choose b64 encoding.
threshold := b64Percent * len(content) / 100
bincount := 0
for _, b := range content {
if (b < ' ' || '~' < b) && b != '\t' {
if !quoteLineBreaks && (b == '\r' || b == '\n') {
continue
}
bincount++
if bincount >= threshold {
return teBase64
}
}
}
if bincount == 0 {
return te7Bit
}
return teQuoted
}
// setParamValue will ignore empty values
func setParamValue(p map[string]string, k, v string) {
if v != "" {
p[k] = v
}
}