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