-
Notifications
You must be signed in to change notification settings - Fork 111
/
header.go
381 lines (323 loc) · 8.37 KB
/
header.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
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
package mail
import (
"crypto/rand"
"encoding/binary"
"errors"
"fmt"
"net/mail"
"os"
"strconv"
"strings"
"time"
"unicode/utf8"
"github.com/emersion/go-message"
)
const dateLayout = "Mon, 02 Jan 2006 15:04:05 -0700"
type headerParser struct {
s string
}
func (p *headerParser) len() int {
return len(p.s)
}
func (p *headerParser) empty() bool {
return p.len() == 0
}
func (p *headerParser) peek() byte {
return p.s[0]
}
func (p *headerParser) consume(c byte) bool {
if p.empty() || p.peek() != c {
return false
}
p.s = p.s[1:]
return true
}
// skipSpace skips the leading space and tab characters.
func (p *headerParser) skipSpace() {
p.s = strings.TrimLeft(p.s, " \t")
}
// skipCFWS skips CFWS as defined in RFC5322. It returns false if the CFWS is
// malformed.
func (p *headerParser) skipCFWS() bool {
p.skipSpace()
for {
if !p.consume('(') {
break
}
if _, ok := p.consumeComment(); !ok {
return false
}
p.skipSpace()
}
return true
}
func (p *headerParser) consumeComment() (string, bool) {
// '(' already consumed.
depth := 1
var comment string
for {
if p.empty() || depth == 0 {
break
}
if p.peek() == '\\' && p.len() > 1 {
p.s = p.s[1:]
} else if p.peek() == '(' {
depth++
} else if p.peek() == ')' {
depth--
}
if depth > 0 {
comment += p.s[:1]
}
p.s = p.s[1:]
}
return comment, depth == 0
}
func (p *headerParser) parseAtomText(dot bool) (string, error) {
i := 0
for {
r, size := utf8.DecodeRuneInString(p.s[i:])
if size == 1 && r == utf8.RuneError {
return "", fmt.Errorf("mail: invalid UTF-8 in atom-text: %q", p.s)
} else if size == 0 || !isAtext(r, dot) {
break
}
i += size
}
if i == 0 {
return "", errors.New("mail: invalid string")
}
var atom string
atom, p.s = p.s[:i], p.s[i:]
return atom, nil
}
func isAtext(r rune, dot bool) bool {
switch r {
case '.':
return dot
// RFC 5322 3.2.3 specials
case '(', ')', '[', ']', ';', '@', '\\', ',':
return false
case '<', '>', '"', ':':
return false
}
return isVchar(r)
}
// isVchar reports whether r is an RFC 5322 VCHAR character.
func isVchar(r rune) bool {
// Visible (printing) characters
return '!' <= r && r <= '~' || isMultibyte(r)
}
// isMultibyte reports whether r is a multi-byte UTF-8 character
// as supported by RFC 6532
func isMultibyte(r rune) bool {
return r >= utf8.RuneSelf
}
func (p *headerParser) parseNoFoldLiteral() (string, error) {
if !p.consume('[') {
return "", errors.New("mail: missing '[' in no-fold-literal")
}
i := 0
for {
r, size := utf8.DecodeRuneInString(p.s[i:])
if size == 1 && r == utf8.RuneError {
return "", fmt.Errorf("mail: invalid UTF-8 in no-fold-literal: %q", p.s)
} else if size == 0 || !isDtext(r) {
break
}
i += size
}
var lit string
lit, p.s = p.s[:i], p.s[i:]
if !p.consume(']') {
return "", errors.New("mail: missing ']' in no-fold-literal")
}
return "[" + lit + "]", nil
}
func isDtext(r rune) bool {
switch r {
case '[', ']', '\\':
return false
}
return isVchar(r)
}
func (p *headerParser) parseMsgID() (string, error) {
if !p.skipCFWS() {
return "", errors.New("mail: malformed parenthetical comment")
}
if !p.consume('<') {
return "", errors.New("mail: missing '<' in msg-id")
}
left, err := p.parseAtomText(true)
if err != nil {
return "", err
}
if !p.consume('@') {
return "", errors.New("mail: missing '@' in msg-id")
}
var right string
if !p.empty() && p.peek() == '[' {
// no-fold-literal
right, err = p.parseNoFoldLiteral()
} else {
right, err = p.parseAtomText(true)
}
if err != nil {
return "", err
}
if !p.consume('>') {
return "", errors.New("mail: missing '>' in msg-id")
}
if !p.skipCFWS() {
return "", errors.New("mail: malformed parenthetical comment")
}
return left + "@" + right, nil
}
// A Header is a mail header.
type Header struct {
message.Header
}
// HeaderFromMap creates a header from a map of header fields.
//
// This function is provided for interoperability with the standard library.
// If possible, ReadHeader should be used instead to avoid loosing information.
// The map representation looses the ordering of the fields, the capitalization
// of the header keys, and the whitespace of the original header.
func HeaderFromMap(m map[string][]string) Header {
return Header{message.HeaderFromMap(m)}
}
// AddressList parses the named header field as a list of addresses. If the
// header field is missing, it returns nil.
//
// This can be used on From, Sender, Reply-To, To, Cc and Bcc header fields.
func (h *Header) AddressList(key string) ([]*Address, error) {
v := h.Get(key)
if v == "" {
return nil, nil
}
return ParseAddressList(v)
}
// SetAddressList formats the named header field to the provided list of
// addresses.
//
// This can be used on From, Sender, Reply-To, To, Cc and Bcc header fields.
func (h *Header) SetAddressList(key string, addrs []*Address) {
if len(addrs) > 0 {
h.Set(key, formatAddressList(addrs))
} else {
h.Del(key)
}
}
// Date parses the Date header field. If the header field is missing, it
// returns the zero time.
func (h *Header) Date() (time.Time, error) {
v := h.Get("Date")
if v == "" {
return time.Time{}, nil
}
return mail.ParseDate(v)
}
// SetDate formats the Date header field.
func (h *Header) SetDate(t time.Time) {
if !t.IsZero() {
h.Set("Date", t.Format(dateLayout))
} else {
h.Del("Date")
}
}
// Subject parses the Subject header field. If there is an error, the raw field
// value is returned alongside the error.
func (h *Header) Subject() (string, error) {
return h.Text("Subject")
}
// SetSubject formats the Subject header field.
func (h *Header) SetSubject(s string) {
h.SetText("Subject", s)
}
// MessageID parses the Message-ID field. It returns the message identifier,
// without the angle brackets. If the message doesn't have a Message-ID header
// field, it returns an empty string.
func (h *Header) MessageID() (string, error) {
v := h.Get("Message-Id")
if v == "" {
return "", nil
}
p := headerParser{v}
return p.parseMsgID()
}
// MsgIDList parses a list of message identifiers. It returns message
// identifiers without angle brackets. If the header field is missing, it
// returns nil.
//
// This can be used on In-Reply-To and References header fields.
func (h *Header) MsgIDList(key string) ([]string, error) {
v := h.Get(key)
if v == "" {
return nil, nil
}
p := headerParser{v}
var l []string
for !p.empty() {
msgID, err := p.parseMsgID()
if err != nil {
return l, err
}
l = append(l, msgID)
}
return l, nil
}
// GenerateMessageID wraps GenerateMessageIDWithHostname and therefore uses the
// hostname of the local machine. This is done to not break existing software.
// Wherever possible better use GenerateMessageIDWithHostname, because the local
// hostname of a machine tends to not be unique nor a FQDN which especially
// brings problems with spam filters.
func (h *Header) GenerateMessageID() error {
var err error
hostname, err := os.Hostname()
if err != nil {
return err
}
return h.GenerateMessageIDWithHostname(hostname)
}
// GenerateMessageIDWithHostname generates an RFC 2822-compliant Message-Id
// based on the informational draft "Recommendations for generating Message
// IDs", it takes an hostname as argument, so that software using this library
// could use a hostname they know to be unique
func (h *Header) GenerateMessageIDWithHostname(hostname string) error {
now := uint64(time.Now().UnixNano())
nonceByte := make([]byte, 8)
if _, err := rand.Read(nonceByte); err != nil {
return err
}
nonce := binary.BigEndian.Uint64(nonceByte)
msgID := fmt.Sprintf("%s.%s@%s", base36(now), base36(nonce), hostname)
h.SetMessageID(msgID)
return nil
}
func base36(input uint64) string {
return strings.ToUpper(strconv.FormatUint(input, 36))
}
// SetMessageID sets the Message-ID field. id is the message identifier,
// without the angle brackets.
func (h *Header) SetMessageID(id string) {
if id != "" {
h.Set("Message-Id", "<"+id+">")
} else {
h.Del("Message-Id")
}
}
// SetMsgIDList formats a list of message identifiers. Message identifiers
// don't include angle brackets.
//
// This can be used on In-Reply-To and References header fields.
func (h *Header) SetMsgIDList(key string, l []string) {
if len(l) > 0 {
h.Set(key, "<"+strings.Join(l, "> <")+">")
} else {
h.Del(key)
}
}
// Copy creates a stand-alone copy of the header.
func (h *Header) Copy() Header {
return Header{h.Header.Copy()}
}