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