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 (
579 words []string
580 sb strings.Builder
581 )
582 for {
583
584 if len(words) > 0 {
585 if !p.skipCFWS() {
586 return "", errors.New("mail: misformatted parenthetical comment")
587 }
588 }
589
590 var word string
591 p.skipSpace()
592 if p.empty() {
593 break
594 }
595 isEncoded := false
596 if p.peek() == '"' {
597
598 word, err = p.consumeQuotedString()
599 } else {
600
601
602
603 word, err = p.consumeAtom(true, true)
604 if err == nil {
605 word, isEncoded, err = p.decodeRFC2047Word(word)
606 }
607 }
608
609 if err != nil {
610 break
611 }
612 debug.Printf("consumePhrase: consumed %q", word)
613 switch {
614 case isEncoded:
615 sb.WriteString(word)
616 case !isEncoded && sb.Len() > 0:
617 words = append(words, sb.String())
618 sb.Reset()
619 words = append(words, word)
620 default:
621 words = append(words, word)
622 }
623 }
624
625 if sb.Len() > 0 {
626 words = append(words, sb.String())
627 }
628
629
630 if err != nil && len(words) == 0 {
631 debug.Printf("consumePhrase: hit err: %v", err)
632 return "", fmt.Errorf("mail: missing word in phrase: %v", err)
633 }
634 phrase = strings.Join(words, " ")
635 return phrase, nil
636 }
637
638
639 func (p *addrParser) consumeQuotedString() (qs string, err error) {
640
641 i := 1
642 qsb := make([]rune, 0, 10)
643
644 escaped := false
645
646 Loop:
647 for {
648 r, size := utf8.DecodeRuneInString(p.s[i:])
649
650 switch {
651 case size == 0:
652 return "", errors.New("mail: unclosed quoted-string")
653
654 case size == 1 && r == utf8.RuneError:
655 return "", fmt.Errorf("mail: invalid utf-8 in quoted-string: %q", p.s)
656
657 case escaped:
658
659
660 if !isVchar(r) && !isWSP(r) {
661 return "", fmt.Errorf("mail: bad character in quoted-string: %q", r)
662 }
663
664 qsb = append(qsb, r)
665 escaped = false
666
667 case isQtext(r) || isWSP(r):
668
669
670 qsb = append(qsb, r)
671
672 case r == '"':
673 break Loop
674
675 case r == '\\':
676 escaped = true
677
678 default:
679 return "", fmt.Errorf("mail: bad character in quoted-string: %q", r)
680
681 }
682
683 i += size
684 }
685 p.s = p.s[i+1:]
686 return string(qsb), nil
687 }
688
689
690
691
692
693 func (p *addrParser) consumeAtom(dot bool, permissive bool) (atom string, err error) {
694 i := 0
695
696 Loop:
697 for {
698 r, size := utf8.DecodeRuneInString(p.s[i:])
699 switch {
700 case size == 1 && r == utf8.RuneError:
701 return "", fmt.Errorf("mail: invalid utf-8 in address: %q", p.s)
702
703 case size == 0 || !isAtext(r, dot):
704 break Loop
705
706 default:
707 i += size
708
709 }
710 }
711
712 if i == 0 {
713 return "", errors.New("mail: invalid string")
714 }
715 atom, p.s = p.s[:i], p.s[i:]
716 if !permissive {
717 if strings.HasPrefix(atom, ".") {
718 return "", errors.New("mail: leading dot in atom")
719 }
720 if strings.Contains(atom, "..") {
721 return "", errors.New("mail: double dot in atom")
722 }
723 if strings.HasSuffix(atom, ".") {
724 return "", errors.New("mail: trailing dot in atom")
725 }
726 }
727 return atom, nil
728 }
729
730
731 func (p *addrParser) consumeDomainLiteral() (string, error) {
732
733 if !p.consume('[') {
734 return "", errors.New(`mail: missing "[" in domain-literal`)
735 }
736
737
738 dtext := p.s
739 dtextLen := 0
740 for {
741 if p.empty() {
742 return "", errors.New("mail: unclosed domain-literal")
743 }
744 if p.peek() == ']' {
745 break
746 }
747
748 r, size := utf8.DecodeRuneInString(p.s)
749 if size == 1 && r == utf8.RuneError {
750 return "", fmt.Errorf("mail: invalid utf-8 in domain-literal: %q", p.s)
751 }
752 if !isDtext(r) {
753 return "", fmt.Errorf("mail: bad character in domain-literal: %q", r)
754 }
755
756 dtextLen += size
757 p.s = p.s[size:]
758 }
759 dtext = dtext[:dtextLen]
760
761
762 if !p.consume(']') {
763 return "", errors.New("mail: unclosed domain-literal")
764 }
765
766
767 if net.ParseIP(dtext) == nil {
768 return "", fmt.Errorf("mail: invalid IP address in domain-literal: %q", dtext)
769 }
770
771 return "[" + dtext + "]", nil
772 }
773
774 func (p *addrParser) consumeDisplayNameComment() (string, error) {
775 if !p.consume('(') {
776 return "", errors.New("mail: comment does not start with (")
777 }
778 comment, ok := p.consumeComment()
779 if !ok {
780 return "", errors.New("mail: misformatted parenthetical comment")
781 }
782
783
784 words := strings.FieldsFunc(comment, func(r rune) bool { return r == ' ' || r == '\t' })
785 for idx, word := range words {
786 decoded, isEncoded, err := p.decodeRFC2047Word(word)
787 if err != nil {
788 return "", err
789 }
790 if isEncoded {
791 words[idx] = decoded
792 }
793 }
794
795 return strings.Join(words, " "), nil
796 }
797
798 func (p *addrParser) consume(c byte) bool {
799 if p.empty() || p.peek() != c {
800 return false
801 }
802 p.s = p.s[1:]
803 return true
804 }
805
806
807 func (p *addrParser) skipSpace() {
808 p.s = strings.TrimLeft(p.s, " \t")
809 }
810
811 func (p *addrParser) peek() byte {
812 return p.s[0]
813 }
814
815 func (p *addrParser) empty() bool {
816 return p.len() == 0
817 }
818
819 func (p *addrParser) len() int {
820 return len(p.s)
821 }
822
823
824 func (p *addrParser) skipCFWS() bool {
825 p.skipSpace()
826
827 for {
828 if !p.consume('(') {
829 break
830 }
831
832 if _, ok := p.consumeComment(); !ok {
833 return false
834 }
835
836 p.skipSpace()
837 }
838
839 return true
840 }
841
842 func (p *addrParser) consumeComment() (string, bool) {
843
844 depth := 1
845
846 var comment strings.Builder
847 for {
848 if p.empty() || depth == 0 {
849 break
850 }
851
852 if p.peek() == '\\' && p.len() > 1 {
853 p.s = p.s[1:]
854 } else if p.peek() == '(' {
855 depth++
856 } else if p.peek() == ')' {
857 depth--
858 }
859 if depth > 0 {
860 comment.WriteByte(p.s[0])
861 }
862 p.s = p.s[1:]
863 }
864
865 return comment.String(), depth == 0
866 }
867
868 func (p *addrParser) decodeRFC2047Word(s string) (word string, isEncoded bool, err error) {
869 dec := p.dec
870 if dec == nil {
871 dec = &rfc2047Decoder
872 }
873
874
875
876
877
878
879
880 adec := *dec
881 charsetReaderError := false
882 adec.CharsetReader = func(charset string, input io.Reader) (io.Reader, error) {
883 if dec.CharsetReader == nil {
884 charsetReaderError = true
885 return nil, charsetError(charset)
886 }
887 r, err := dec.CharsetReader(charset, input)
888 if err != nil {
889 charsetReaderError = true
890 }
891 return r, err
892 }
893 word, err = adec.Decode(s)
894 if err == nil {
895 return word, true, nil
896 }
897
898
899
900
901
902
903 if charsetReaderError {
904 return s, true, err
905 }
906
907
908 return s, false, nil
909 }
910
911 var rfc2047Decoder = mime.WordDecoder{
912 CharsetReader: func(charset string, input io.Reader) (io.Reader, error) {
913 return nil, charsetError(charset)
914 },
915 }
916
917 type charsetError string
918
919 func (e charsetError) Error() string {
920 return fmt.Sprintf("charset not supported: %q", string(e))
921 }
922
923
924
925 func isAtext(r rune, dot bool) bool {
926 switch r {
927 case '.':
928 return dot
929
930
931 case '(', ')', '<', '>', '[', ']', ':', ';', '@', '\\', ',', '"':
932 return false
933 }
934 return isVchar(r)
935 }
936
937
938 func isQtext(r rune) bool {
939
940 if r == '\\' || r == '"' {
941 return false
942 }
943 return isVchar(r)
944 }
945
946
947 func quoteString(s string) string {
948 var b strings.Builder
949 b.WriteByte('"')
950 for _, r := range s {
951 if isQtext(r) || isWSP(r) {
952 b.WriteRune(r)
953 } else if isVchar(r) {
954 b.WriteByte('\\')
955 b.WriteRune(r)
956 }
957 }
958 b.WriteByte('"')
959 return b.String()
960 }
961
962
963 func isVchar(r rune) bool {
964
965 return '!' <= r && r <= '~' || isMultibyte(r)
966 }
967
968
969
970 func isMultibyte(r rune) bool {
971 return r >= utf8.RuneSelf
972 }
973
974
975
976 func isWSP(r rune) bool {
977 return r == ' ' || r == '\t'
978 }
979
980
981 func isDtext(r rune) bool {
982
983 if r == '[' || r == ']' || r == '\\' {
984 return false
985 }
986 return isVchar(r)
987 }
988
View as plain text