forked from matthewljsmith/go-email
-
Notifications
You must be signed in to change notification settings - Fork 0
/
message.go
324 lines (279 loc) · 9.93 KB
/
message.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
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
/*
Package email implements the parsing of email and mime mail messages,
and may also be used to create and send email messages.
*/
package email
import (
"bytes"
"encoding/base64"
"fmt"
"io"
"mime/quotedprintable"
"strings"
)
const (
// MaxBodyLineLength ...
MaxBodyLineLength = 76
)
// Message represents a full email message, or a mime-message
// (such as a single part in a multipart message).
// It has fields for the Header and the payload, which may take
// several forms depending on the Content-Type of this message.
// If the Content-Type is "message", then the payload will be a SubMessage.
// If the Content-Type is "multipart", then the payload will be Parts,
// and optionally the Preamble and Epilogue will be full.
// If the Content-Type is neither "message" nor "multipart", then
// the payload will be a Body (decoded if quoted-printable or base64).
type Message struct {
// Header is this message's key-value MIME-style pairs in its header.
Header Header
// Preamble is any text that appears before the first mime multipart,
// and may only be full in the case where this Message has a Content-Type of "multipart".
Preamble []byte
// Epilogue is any text that appears after the last mime multipart,
// and may only be full in the case where this Message has a Content-Type of "multipart".
Epilogue []byte
// Can only have one of the following:
// Parts is a slice of Messages contained within this Message,
// and is full in the case where this Message has a Content-Type of "multipart".
Parts []*Message
// SubMessage is an encapsulated message, and is full in the case
// where this Message has a Content-Type of "message".
SubMessage *Message
// Body is a byte array of the body of this message, and is full
// whenever this message doesn't have a Content-Type of "multipart" or "message".
// The Body is already decoded if the Content-Transfer-Encoding was
// quoted-printable or base64, and will be re-encoded when written out
// based on the Content-Type.
Body []byte
}
// Payload will return the payload of the message, which can only be one the
// following: Body ([]byte), SubMessage (*Message), or Parts ([]*Message)
func (m *Message) Payload() interface{} {
if m.HasParts() {
return m.Parts
}
if m.HasSubMessage() {
return m.SubMessage
}
return m.Body
}
// HasParts returns true if the Content-Type is "multipart"
func (m *Message) HasParts() bool {
mediaType, _, err := m.Header.ContentType()
if err != nil {
return false
}
return strings.HasPrefix(mediaType, "multipart")
}
// HasSubMessage returns true if the Content-Type is "message"
func (m *Message) HasSubMessage() bool {
mediaType, _, err := m.Header.ContentType()
if err != nil {
return false
}
return strings.HasPrefix(mediaType, "message")
}
// HasBody returns true if the Content-Type is not "multipart" nor "message"
func (m *Message) HasBody() bool {
mediaType, _, err := m.Header.ContentType()
if err != nil && err != ErrHeadersMissingField {
return false
}
return !strings.HasPrefix(mediaType, "multipart") && !strings.HasPrefix(mediaType, "message")
}
// PartsContentTypePrefix will return a slice of all parts of this message
// that have this contentTypePrefix.
// contentTypePrefix can be a prefix ("text"), or a full type ("text/html").
// The slice will be empty if this message is not a multipart message.
// This method does NOT recurse into sub-messages and sub-parts.
func (m *Message) PartsContentTypePrefix(contentTypePrefix string) []*Message {
return m.PartsFilter(contentTypePrefixFilterClosure(contentTypePrefix))
}
// PartsFilter will return a slice of all parts of this message
// that match this lambda function.
// The slice will be empty if this message is not a multipart message.
// This method does NOT recurse into sub-messages and sub-parts.
func (m *Message) PartsFilter(filter func(*Message) bool) []*Message {
messages := make([]*Message, 0, 1)
if m.HasParts() {
for _, part := range m.Parts {
if filter(part) {
messages = append(messages, part)
}
}
}
return messages
}
// MessagesAll will return a slice of Messages, starting with this message,
// and followed by all messages contained within this message, recursively.
// This method is similar to Python's email message "walk" function.
// This method DOES recurse into sub-messages and sub-parts.
func (m *Message) MessagesAll() []*Message {
return m.MessagesFilter(func(tested *Message) bool {
return true
})
}
// MessagesContentTypePrefix will return a slice of all Messages that have
// this contentTypePrefix, potentially including this message and messages
// contained within this message.
// contentTypePrefix can be a prefix ("text"), or a full type ("text/html").
// This method DOES recurse into sub-messages and sub-parts.
func (m *Message) MessagesContentTypePrefix(contentTypePrefix string) []*Message {
return m.MessagesFilter(contentTypePrefixFilterClosure(contentTypePrefix))
}
// MessagesFilter will return a slice of all Messages that match this lambda
// function, potentially including this message and messages contained within
// this message.
// This method DOES recurse into sub-messages and sub-parts.
func (m *Message) MessagesFilter(filter func(*Message) bool) []*Message {
messages := make([]*Message, 0, 1)
if filter(m) {
messages = append(messages, m)
}
if m.HasSubMessage() {
return append(messages, m.SubMessage.MessagesFilter(filter)...)
}
if m.HasParts() {
for _, part := range m.Parts {
messages = append(messages, part.MessagesFilter(filter)...)
}
}
return messages
}
// contentTypePrefixFilterClosure returns a closure that returns true if
// the message has this contentTypePrefix.
func contentTypePrefixFilterClosure(contentTypePrefix string) func(*Message) bool {
return func(tested *Message) bool {
mediaType, _, err := tested.Header.ContentType()
if err != nil {
return false
}
return strings.HasPrefix(mediaType, contentTypePrefix)
}
}
// Methods required for sending a message:
// Save adds headers for the "Message-Id", "Date", and "MIME-Version", if missing.
// An error is returned if the Message-Id can not be created.
func (m *Message) Save() error {
return m.Header.Save()
}
// Bytes returns the bytes representing this message. It is a convenience
// method that calls WriteTo on a buffer, returning its bytes.
func (m *Message) Bytes() ([]byte, error) {
buffer := &bytes.Buffer{}
_, err := m.WriteTo(buffer)
return buffer.Bytes(), err
}
// WriteTo writes out this Message and its payloads, recursively.
// Any text bodies will be quoted-printable encoded,
// and all other bodies will be base64 encoded.
func (m *Message) WriteTo(w io.Writer) (int64, error) {
total, err := m.Header.WriteTo(w)
if err != nil {
return total, err
}
mediaType, mediaTypeParams, err := m.Header.ContentType()
if err != nil && err != ErrHeadersMissingField {
return total, err
}
hasParts := strings.HasPrefix(mediaType, "multipart")
hasSubMessage := strings.HasPrefix(mediaType, "message")
if !hasParts && !hasSubMessage {
return m.writeBody(w, total)
}
written, err := io.WriteString(w, "\r\n")
total += int64(written)
if err != nil {
return total, err
}
if hasSubMessage {
written2, err := m.SubMessage.WriteTo(w)
return total + written2, err
}
// hasParts
return m.writeParts(w, mediaTypeParams["boundary"], total)
}
// writeParts ...
func (m *Message) writeParts(w io.Writer, boundary string, total int64) (int64, error) {
if len(m.Preamble) > 0 {
written, err := fmt.Fprintf(w, "%s\r\n", m.Preamble)
total += int64(written)
if err != nil {
return total, err
}
}
for _, part := range m.Parts {
written, err := fmt.Fprintf(w, "\r\n--%s\r\n", boundary)
total += int64(written)
if err != nil {
return total, err
}
written2, err2 := part.WriteTo(w)
total += written2
if err2 != nil {
return total, err2
}
}
written, err := fmt.Fprintf(w, "\r\n--%s--\r\n", boundary)
total += int64(written)
if err != nil {
return total, err
}
if len(m.Epilogue) > 0 {
written, err = fmt.Fprintf(w, "%s\r\n", m.Epilogue)
total += int64(written)
if err != nil {
return total, err
}
}
return total, err
}
// writeBody ...
func (m *Message) writeBody(w io.Writer, total int64) (int64, error) {
var written int
var err error
// Encode if we have Content-Type, and we do not have Content-Transfer-Encoding set
if contentType := m.Header.Get("Content-Type"); len(contentType) > 0 && !m.Header.IsSet("Content-Transfer-Encoding") {
if strings.HasPrefix(contentType, "text") {
return m.writeText(w, total)
}
return m.writeBase64(w, total)
}
written, err = io.WriteString(w, "\r\n")
total += int64(written)
if err != nil {
return total, err
}
written, err = w.Write(m.Body)
return total + int64(written), err
}
// writeText ...
func (m *Message) writeText(w io.Writer, total int64) (int64, error) {
written, err := io.WriteString(w, "Content-Transfer-Encoding: quoted-printable\r\n\r\n")
total += int64(written)
if err != nil {
return total, err
}
// quotedprintable takes care of wrapping content at a good line length already
qpWriter := quotedprintable.NewWriter(w)
written, err = qpWriter.Write(m.Body)
qpWriter.Close() // Must remember to close the wrapper, as it needs to flush to underlying writer
return total + int64(written), err
}
// writeBase64 ...
func (m *Message) writeBase64(w io.Writer, total int64) (int64, error) {
written, err := io.WriteString(w, "Content-Transfer-Encoding: base64\r\n\r\n")
total += int64(written)
if err != nil {
return total, err
}
// must wrap content at 76 characters
b64Writer := base64.NewEncoder(base64.StdEncoding, &base64Writer{w: w, maxLineLen: MaxBodyLineLength})
written, err = b64Writer.Write(m.Body)
b64Writer.Close() // Must remember to close the wrapper, as it needs to flush to underlying writer
return total + int64(written), err
}