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