Source file src/net/mail/message.go
1
2
3
4
5
18 package mail
19
20 import (
21 "bufio"
22 "errors"
23 "fmt"
24 "io"
25 "log"
26 "mime"
27 "net/textproto"
28 "strings"
29 "sync"
30 "time"
31 "unicode/utf8"
32 )
33
34 var debug = debugT(false)
35
36 type debugT bool
37
38 func (d debugT) Printf(format string, args ...interface{}) {
39 if d {
40 log.Printf(format, args...)
41 }
42 }
43
44
45 type Message struct {
46 Header Header
47 Body io.Reader
48 }
49
50
51
52
53 func ReadMessage(r io.Reader) (msg *Message, err error) {
54 tp := textproto.NewReader(bufio.NewReader(r))
55
56 hdr, err := tp.ReadMIMEHeader()
57 if err != nil {
58 return nil, err
59 }
60
61 return &Message{
62 Header: Header(hdr),
63 Body: tp.R,
64 }, nil
65 }
66
67
68
69 var (
70 dateLayoutsBuildOnce sync.Once
71 dateLayouts []string
72 )
73
74 func buildDateLayouts() {
75
76
77 dows := [...]string{"", "Mon, "}
78 days := [...]string{"2", "02"}
79 years := [...]string{"2006", "06"}
80 seconds := [...]string{":05", ""}
81
82 zones := [...]string{"-0700", "MST", "-0700 (MST)"}
83
84 for _, dow := range dows {
85 for _, day := range days {
86 for _, year := range years {
87 for _, second := range seconds {
88 for _, zone := range zones {
89 s := dow + day + " Jan " + year + " 15:04" + second + " " + zone
90 dateLayouts = append(dateLayouts, s)
91 }
92 }
93 }
94 }
95 }
96 }
97
98
99 func ParseDate(date string) (time.Time, error) {
100 dateLayoutsBuildOnce.Do(buildDateLayouts)
101 for _, layout := range dateLayouts {
102 t, err := time.Parse(layout, date)
103 if err == nil {
104 return t, nil
105 }
106 }
107 return time.Time{}, errors.New("mail: header could not be parsed")
108 }
109
110
111 type Header map[string][]string
112
113
114
115
116
117
118
119 func (h Header) Get(key string) string {
120 return textproto.MIMEHeader(h).Get(key)
121 }
122
123 var ErrHeaderNotPresent = errors.New("mail: header not in message")
124
125
126 func (h Header) Date() (time.Time, error) {
127 hdr := h.Get("Date")
128 if hdr == "" {
129 return time.Time{}, ErrHeaderNotPresent
130 }
131 return ParseDate(hdr)
132 }
133
134
135 func (h Header) AddressList(key string) ([]*Address, error) {
136 hdr := h.Get(key)
137 if hdr == "" {
138 return nil, ErrHeaderNotPresent
139 }
140 return ParseAddressList(hdr)
141 }
142
143
144
145
146 type Address struct {
147 Name string
148 Address string
149 }
150
151
152 func ParseAddress(address string) (*Address, error) {
153 return (&addrParser{s: address}).parseSingleAddress()
154 }
155
156
157 func ParseAddressList(list string) ([]*Address, error) {
158 return (&addrParser{s: list}).parseAddressList()
159 }
160
161
162 type AddressParser struct {
163
164 WordDecoder *mime.WordDecoder
165 }
166
167
168
169 func (p *AddressParser) Parse(address string) (*Address, error) {
170 return (&addrParser{s: address, dec: p.WordDecoder}).parseSingleAddress()
171 }
172
173
174
175 func (p *AddressParser) ParseList(list string) ([]*Address, error) {
176 return (&addrParser{s: list, dec: p.WordDecoder}).parseAddressList()
177 }
178
179
180
181
182 func (a *Address) String() string {
183
184 at := strings.LastIndex(a.Address, "@")
185 var local, domain string
186 if at < 0 {
187
188
189 local = a.Address
190 } else {
191 local, domain = a.Address[:at], a.Address[at+1:]
192 }
193
194
195 quoteLocal := false
196 for i, r := range local {
197 if isAtext(r, false, false) {
198 continue
199 }
200 if r == '.' {
201
202
203
204 if i > 0 && local[i-1] != '.' && i < len(local)-1 {
205 continue
206 }
207 }
208 quoteLocal = true
209 break
210 }
211 if quoteLocal {
212 local = quoteString(local)
213
214 }
215
216 s := "<" + local + "@" + domain + ">"
217
218 if a.Name == "" {
219 return s
220 }
221
222
223 allPrintable := true
224 for _, r := range a.Name {
225
226
227 if !isVchar(r) && !isWSP(r) || isMultibyte(r) {
228 allPrintable = false
229 break
230 }
231 }
232 if allPrintable {
233 return quoteString(a.Name) + " " + s
234 }
235
236
237
238
239 if strings.ContainsAny(a.Name, "\"#$%&'(),.:;<>@[]^`{|}~") {
240 return mime.BEncoding.Encode("utf-8", a.Name) + " " + s
241 }
242 return mime.QEncoding.Encode("utf-8", a.Name) + " " + s
243 }
244
245 type addrParser struct {
246 s string
247 dec *mime.WordDecoder
248 }
249
250 func (p *addrParser) parseAddressList() ([]*Address, error) {
251 var list []*Address
252 for {
253 p.skipSpace()
254 addrs, err := p.parseAddress(true)
255 if err != nil {
256 return nil, err
257 }
258 list = append(list, addrs...)
259
260 if !p.skipCFWS() {
261 return nil, errors.New("mail: misformatted parenthetical comment")
262 }
263 if p.empty() {
264 break
265 }
266 if !p.consume(',') {
267 return nil, errors.New("mail: expected comma")
268 }
269 }
270 return list, nil
271 }
272
273 func (p *addrParser) parseSingleAddress() (*Address, error) {
274 addrs, err := p.parseAddress(true)
275 if err != nil {
276 return nil, err
277 }
278 if !p.skipCFWS() {
279 return nil, errors.New("mail: misformatted parenthetical comment")
280 }
281 if !p.empty() {
282 return nil, fmt.Errorf("mail: expected single address, got %q", p.s)
283 }
284 if len(addrs) == 0 {
285 return nil, errors.New("mail: empty group")
286 }
287 if len(addrs) > 1 {
288 return nil, errors.New("mail: group with multiple addresses")
289 }
290 return addrs[0], nil
291 }
292
293
294 func (p *addrParser) parseAddress(handleGroup bool) ([]*Address, error) {
295 debug.Printf("parseAddress: %q", p.s)
296 p.skipSpace()
297 if p.empty() {
298 return nil, errors.New("mail: no address")
299 }
300
301
302
303
304
305
306
307
308 spec, err := p.consumeAddrSpec()
309 if err == nil {
310 var displayName string
311 p.skipSpace()
312 if !p.empty() && p.peek() == '(' {
313 displayName, err = p.consumeDisplayNameComment()
314 if err != nil {
315 return nil, err
316 }
317 }
318
319 return []*Address{{
320 Name: displayName,
321 Address: spec,
322 }}, err
323 }
324 debug.Printf("parseAddress: not an addr-spec: %v", err)
325 debug.Printf("parseAddress: state is now %q", p.s)
326
327
328 var displayName string
329 if p.peek() != '<' {
330 displayName, err = p.consumePhrase()
331 if err != nil {
332 return nil, err
333 }
334 }
335 debug.Printf("parseAddress: displayName=%q", displayName)
336
337 p.skipSpace()
338 if handleGroup {
339 if p.consume(':') {
340 return p.consumeGroupList()
341 }
342 }
343
344 if !p.consume('<') {
345 atext := true
346 for _, r := range displayName {
347 if !isAtext(r, true, false) {
348 atext = false
349 break
350 }
351 }
352 if atext {
353
354
355 return nil, errors.New("mail: missing '@' or angle-addr")
356 }
357
358
359
360 return nil, errors.New("mail: no angle-addr")
361 }
362 spec, err = p.consumeAddrSpec()
363 if err != nil {
364 return nil, err
365 }
366 if !p.consume('>') {
367 return nil, errors.New("mail: unclosed angle-addr")
368 }
369 debug.Printf("parseAddress: spec=%q", spec)
370
371 return []*Address{{
372 Name: displayName,
373 Address: spec,
374 }}, nil
375 }
376
377 func (p *addrParser) consumeGroupList() ([]*Address, error) {
378 var group []*Address
379
380 p.skipSpace()
381 if p.consume(';') {
382 p.skipCFWS()
383 return group, nil
384 }
385
386 for {
387 p.skipSpace()
388
389 addrs, err := p.parseAddress(false)
390 if err != nil {
391 return nil, err
392 }
393 group = append(group, addrs...)
394
395 if !p.skipCFWS() {
396 return nil, errors.New("mail: misformatted parenthetical comment")
397 }
398 if p.consume(';') {
399 p.skipCFWS()
400 break
401 }
402 if !p.consume(',') {
403 return nil, errors.New("mail: expected comma")
404 }
405 }
406 return group, nil
407 }
408
409
410 func (p *addrParser) consumeAddrSpec() (spec string, err error) {
411 debug.Printf("consumeAddrSpec: %q", p.s)
412
413 orig := *p
414 defer func() {
415 if err != nil {
416 *p = orig
417 }
418 }()
419
420
421 var localPart string
422 p.skipSpace()
423 if p.empty() {
424 return "", errors.New("mail: no addr-spec")
425 }
426 if p.peek() == '"' {
427
428 debug.Printf("consumeAddrSpec: parsing quoted-string")
429 localPart, err = p.consumeQuotedString()
430 if localPart == "" {
431 err = errors.New("mail: empty quoted string in addr-spec")
432 }
433 } else {
434
435 debug.Printf("consumeAddrSpec: parsing dot-atom")
436 localPart, err = p.consumeAtom(true, false)
437 }
438 if err != nil {
439 debug.Printf("consumeAddrSpec: failed: %v", err)
440 return "", err
441 }
442
443 if !p.consume('@') {
444 return "", errors.New("mail: missing @ in addr-spec")
445 }
446
447
448 var domain string
449 p.skipSpace()
450 if p.empty() {
451 return "", errors.New("mail: no domain in addr-spec")
452 }
453
454 domain, err = p.consumeAtom(true, false)
455 if err != nil {
456 return "", err
457 }
458
459 return localPart + "@" + domain, nil
460 }
461
462
463 func (p *addrParser) consumePhrase() (phrase string, err error) {
464 debug.Printf("consumePhrase: [%s]", p.s)
465
466 var words []string
467 var isPrevEncoded bool
468 for {
469
470 var word string
471 p.skipSpace()
472 if p.empty() {
473 break
474 }
475 isEncoded := false
476 if p.peek() == '"' {
477
478 word, err = p.consumeQuotedString()
479 } else {
480
481
482
483 word, err = p.consumeAtom(true, true)
484 if err == nil {
485 word, isEncoded, err = p.decodeRFC2047Word(word)
486 }
487 }
488
489 if err != nil {
490 break
491 }
492 debug.Printf("consumePhrase: consumed %q", word)
493 if isPrevEncoded && isEncoded {
494 words[len(words)-1] += word
495 } else {
496 words = append(words, word)
497 }
498 isPrevEncoded = isEncoded
499 }
500
501 if err != nil && len(words) == 0 {
502 debug.Printf("consumePhrase: hit err: %v", err)
503 return "", fmt.Errorf("mail: missing word in phrase: %v", err)
504 }
505 phrase = strings.Join(words, " ")
506 return phrase, nil
507 }
508
509
510 func (p *addrParser) consumeQuotedString() (qs string, err error) {
511
512 i := 1
513 qsb := make([]rune, 0, 10)
514
515 escaped := false
516
517 Loop:
518 for {
519 r, size := utf8.DecodeRuneInString(p.s[i:])
520
521 switch {
522 case size == 0:
523 return "", errors.New("mail: unclosed quoted-string")
524
525 case size == 1 && r == utf8.RuneError:
526 return "", fmt.Errorf("mail: invalid utf-8 in quoted-string: %q", p.s)
527
528 case escaped:
529
530
531 if !isVchar(r) && !isWSP(r) {
532 return "", fmt.Errorf("mail: bad character in quoted-string: %q", r)
533 }
534
535 qsb = append(qsb, r)
536 escaped = false
537
538 case isQtext(r) || isWSP(r):
539
540
541 qsb = append(qsb, r)
542
543 case r == '"':
544 break Loop
545
546 case r == '\\':
547 escaped = true
548
549 default:
550 return "", fmt.Errorf("mail: bad character in quoted-string: %q", r)
551
552 }
553
554 i += size
555 }
556 p.s = p.s[i+1:]
557 return string(qsb), nil
558 }
559
560
561
562
563
564
565 func (p *addrParser) consumeAtom(dot bool, permissive bool) (atom string, err error) {
566 i := 0
567
568 Loop:
569 for {
570 r, size := utf8.DecodeRuneInString(p.s[i:])
571 switch {
572 case size == 1 && r == utf8.RuneError:
573 return "", fmt.Errorf("mail: invalid utf-8 in address: %q", p.s)
574
575 case size == 0 || !isAtext(r, dot, permissive):
576 break Loop
577
578 default:
579 i += size
580
581 }
582 }
583
584 if i == 0 {
585 return "", errors.New("mail: invalid string")
586 }
587 atom, p.s = p.s[:i], p.s[i:]
588 if !permissive {
589 if strings.HasPrefix(atom, ".") {
590 return "", errors.New("mail: leading dot in atom")
591 }
592 if strings.Contains(atom, "..") {
593 return "", errors.New("mail: double dot in atom")
594 }
595 if strings.HasSuffix(atom, ".") {
596 return "", errors.New("mail: trailing dot in atom")
597 }
598 }
599 return atom, nil
600 }
601
602 func (p *addrParser) consumeDisplayNameComment() (string, error) {
603 if !p.consume('(') {
604 return "", errors.New("mail: comment does not start with (")
605 }
606 comment, ok := p.consumeComment()
607 if !ok {
608 return "", errors.New("mail: misformatted parenthetical comment")
609 }
610
611
612 words := strings.FieldsFunc(comment, func(r rune) bool { return r == ' ' || r == '\t' })
613 for idx, word := range words {
614 decoded, isEncoded, err := p.decodeRFC2047Word(word)
615 if err != nil {
616 return "", err
617 }
618 if isEncoded {
619 words[idx] = decoded
620 }
621 }
622
623 return strings.Join(words, " "), nil
624 }
625
626 func (p *addrParser) consume(c byte) bool {
627 if p.empty() || p.peek() != c {
628 return false
629 }
630 p.s = p.s[1:]
631 return true
632 }
633
634
635 func (p *addrParser) skipSpace() {
636 p.s = strings.TrimLeft(p.s, " \t")
637 }
638
639 func (p *addrParser) peek() byte {
640 return p.s[0]
641 }
642
643 func (p *addrParser) empty() bool {
644 return p.len() == 0
645 }
646
647 func (p *addrParser) len() int {
648 return len(p.s)
649 }
650
651
652 func (p *addrParser) skipCFWS() bool {
653 p.skipSpace()
654
655 for {
656 if !p.consume('(') {
657 break
658 }
659
660 if _, ok := p.consumeComment(); !ok {
661 return false
662 }
663
664 p.skipSpace()
665 }
666
667 return true
668 }
669
670 func (p *addrParser) consumeComment() (string, bool) {
671
672 depth := 1
673
674 var comment string
675 for {
676 if p.empty() || depth == 0 {
677 break
678 }
679
680 if p.peek() == '\\' && p.len() > 1 {
681 p.s = p.s[1:]
682 } else if p.peek() == '(' {
683 depth++
684 } else if p.peek() == ')' {
685 depth--
686 }
687 if depth > 0 {
688 comment += p.s[:1]
689 }
690 p.s = p.s[1:]
691 }
692
693 return comment, depth == 0
694 }
695
696 func (p *addrParser) decodeRFC2047Word(s string) (word string, isEncoded bool, err error) {
697 if p.dec != nil {
698 word, err = p.dec.Decode(s)
699 } else {
700 word, err = rfc2047Decoder.Decode(s)
701 }
702
703 if err == nil {
704 return word, true, nil
705 }
706
707 if _, ok := err.(charsetError); ok {
708 return s, true, err
709 }
710
711
712 return s, false, nil
713 }
714
715 var rfc2047Decoder = mime.WordDecoder{
716 CharsetReader: func(charset string, input io.Reader) (io.Reader, error) {
717 return nil, charsetError(charset)
718 },
719 }
720
721 type charsetError string
722
723 func (e charsetError) Error() string {
724 return fmt.Sprintf("charset not supported: %q", string(e))
725 }
726
727
728
729
730
731 func isAtext(r rune, dot, permissive bool) bool {
732 switch r {
733 case '.':
734 return dot
735
736
737 case '(', ')', '[', ']', ';', '@', '\\', ',':
738 return permissive
739
740 case '<', '>', '"', ':':
741 return false
742 }
743 return isVchar(r)
744 }
745
746
747 func isQtext(r rune) bool {
748
749 if r == '\\' || r == '"' {
750 return false
751 }
752 return isVchar(r)
753 }
754
755
756 func quoteString(s string) string {
757 var buf strings.Builder
758 buf.WriteByte('"')
759 for _, r := range s {
760 if isQtext(r) || isWSP(r) {
761 buf.WriteRune(r)
762 } else if isVchar(r) {
763 buf.WriteByte('\\')
764 buf.WriteRune(r)
765 }
766 }
767 buf.WriteByte('"')
768 return buf.String()
769 }
770
771
772 func isVchar(r rune) bool {
773
774 return '!' <= r && r <= '~' || isMultibyte(r)
775 }
776
777
778
779 func isMultibyte(r rune) bool {
780 return r >= utf8.RuneSelf
781 }
782
783
784
785 func isWSP(r rune) bool {
786 return r == ' ' || r == '\t'
787 }
788
View as plain text