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) {
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) {
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 if !p.skipCFWS() {
483 return nil, errors.New("mail: misformatted parenthetical comment")
484 }
485 return group, nil
486 }
487
488 for {
489 p.skipSpace()
490
491 addrs, err := p.parseAddress(false)
492 if err != nil {
493 return nil, err
494 }
495 group = append(group, addrs...)
496
497 if !p.skipCFWS() {
498 return nil, errors.New("mail: misformatted parenthetical comment")
499 }
500 if p.consume(';') {
501 if !p.skipCFWS() {
502 return nil, errors.New("mail: misformatted parenthetical comment")
503 }
504 break
505 }
506 if !p.consume(',') {
507 return nil, errors.New("mail: expected comma")
508 }
509 }
510 return group, nil
511 }
512
513
514 func (p *addrParser) consumeAddrSpec() (spec string, err error) {
515 debug.Printf("consumeAddrSpec: %q", p.s)
516
517 orig := *p
518 defer func() {
519 if err != nil {
520 *p = orig
521 }
522 }()
523
524
525 var localPart string
526 p.skipSpace()
527 if p.empty() {
528 return "", errors.New("mail: no addr-spec")
529 }
530 if p.peek() == '"' {
531
532 debug.Printf("consumeAddrSpec: parsing quoted-string")
533 localPart, err = p.consumeQuotedString()
534 if localPart == "" {
535 err = errors.New("mail: empty quoted string in addr-spec")
536 }
537 } else {
538
539 debug.Printf("consumeAddrSpec: parsing dot-atom")
540 localPart, err = p.consumeAtom(true, false)
541 }
542 if err != nil {
543 debug.Printf("consumeAddrSpec: failed: %v", err)
544 return "", err
545 }
546
547 if !p.consume('@') {
548 return "", errors.New("mail: missing @ in addr-spec")
549 }
550
551
552 var domain string
553 p.skipSpace()
554 if p.empty() {
555 return "", errors.New("mail: no domain in addr-spec")
556 }
557
558 domain, err = p.consumeAtom(true, false)
559 if err != nil {
560 return "", err
561 }
562
563 return localPart + "@" + domain, nil
564 }
565
566
567 func (p *addrParser) consumePhrase() (phrase string, err error) {
568 debug.Printf("consumePhrase: [%s]", p.s)
569
570 var words []string
571 var isPrevEncoded bool
572 for {
573
574 if len(words) > 0 {
575 if !p.skipCFWS() {
576 return "", errors.New("mail: misformatted parenthetical comment")
577 }
578 }
579
580 var word string
581 p.skipSpace()
582 if p.empty() {
583 break
584 }
585 isEncoded := false
586 if p.peek() == '"' {
587
588 word, err = p.consumeQuotedString()
589 } else {
590
591
592
593 word, err = p.consumeAtom(true, true)
594 if err == nil {
595 word, isEncoded, err = p.decodeRFC2047Word(word)
596 }
597 }
598
599 if err != nil {
600 break
601 }
602 debug.Printf("consumePhrase: consumed %q", word)
603 if isPrevEncoded && isEncoded {
604 words[len(words)-1] += word
605 } else {
606 words = append(words, word)
607 }
608 isPrevEncoded = isEncoded
609 }
610
611 if err != nil && len(words) == 0 {
612 debug.Printf("consumePhrase: hit err: %v", err)
613 return "", fmt.Errorf("mail: missing word in phrase: %v", err)
614 }
615 phrase = strings.Join(words, " ")
616 return phrase, nil
617 }
618
619
620 func (p *addrParser) consumeQuotedString() (qs string, err error) {
621
622 i := 1
623 qsb := make([]rune, 0, 10)
624
625 escaped := false
626
627 Loop:
628 for {
629 r, size := utf8.DecodeRuneInString(p.s[i:])
630
631 switch {
632 case size == 0:
633 return "", errors.New("mail: unclosed quoted-string")
634
635 case size == 1 && r == utf8.RuneError:
636 return "", fmt.Errorf("mail: invalid utf-8 in quoted-string: %q", p.s)
637
638 case escaped:
639
640
641 if !isVchar(r) && !isWSP(r) {
642 return "", fmt.Errorf("mail: bad character in quoted-string: %q", r)
643 }
644
645 qsb = append(qsb, r)
646 escaped = false
647
648 case isQtext(r) || isWSP(r):
649
650
651 qsb = append(qsb, r)
652
653 case r == '"':
654 break Loop
655
656 case r == '\\':
657 escaped = true
658
659 default:
660 return "", fmt.Errorf("mail: bad character in quoted-string: %q", r)
661
662 }
663
664 i += size
665 }
666 p.s = p.s[i+1:]
667 return string(qsb), nil
668 }
669
670
671
672
673
674 func (p *addrParser) consumeAtom(dot bool, permissive bool) (atom string, err error) {
675 i := 0
676
677 Loop:
678 for {
679 r, size := utf8.DecodeRuneInString(p.s[i:])
680 switch {
681 case size == 1 && r == utf8.RuneError:
682 return "", fmt.Errorf("mail: invalid utf-8 in address: %q", p.s)
683
684 case size == 0 || !isAtext(r, dot):
685 break Loop
686
687 default:
688 i += size
689
690 }
691 }
692
693 if i == 0 {
694 return "", errors.New("mail: invalid string")
695 }
696 atom, p.s = p.s[:i], p.s[i:]
697 if !permissive {
698 if strings.HasPrefix(atom, ".") {
699 return "", errors.New("mail: leading dot in atom")
700 }
701 if strings.Contains(atom, "..") {
702 return "", errors.New("mail: double dot in atom")
703 }
704 if strings.HasSuffix(atom, ".") {
705 return "", errors.New("mail: trailing dot in atom")
706 }
707 }
708 return atom, nil
709 }
710
711 func (p *addrParser) consumeDisplayNameComment() (string, error) {
712 if !p.consume('(') {
713 return "", errors.New("mail: comment does not start with (")
714 }
715 comment, ok := p.consumeComment()
716 if !ok {
717 return "", errors.New("mail: misformatted parenthetical comment")
718 }
719
720
721 words := strings.FieldsFunc(comment, func(r rune) bool { return r == ' ' || r == '\t' })
722 for idx, word := range words {
723 decoded, isEncoded, err := p.decodeRFC2047Word(word)
724 if err != nil {
725 return "", err
726 }
727 if isEncoded {
728 words[idx] = decoded
729 }
730 }
731
732 return strings.Join(words, " "), nil
733 }
734
735 func (p *addrParser) consume(c byte) bool {
736 if p.empty() || p.peek() != c {
737 return false
738 }
739 p.s = p.s[1:]
740 return true
741 }
742
743
744 func (p *addrParser) skipSpace() {
745 p.s = strings.TrimLeft(p.s, " \t")
746 }
747
748 func (p *addrParser) peek() byte {
749 return p.s[0]
750 }
751
752 func (p *addrParser) empty() bool {
753 return p.len() == 0
754 }
755
756 func (p *addrParser) len() int {
757 return len(p.s)
758 }
759
760
761 func (p *addrParser) skipCFWS() bool {
762 p.skipSpace()
763
764 for {
765 if !p.consume('(') {
766 break
767 }
768
769 if _, ok := p.consumeComment(); !ok {
770 return false
771 }
772
773 p.skipSpace()
774 }
775
776 return true
777 }
778
779 func (p *addrParser) consumeComment() (string, bool) {
780
781 depth := 1
782
783 var comment string
784 for {
785 if p.empty() || depth == 0 {
786 break
787 }
788
789 if p.peek() == '\\' && p.len() > 1 {
790 p.s = p.s[1:]
791 } else if p.peek() == '(' {
792 depth++
793 } else if p.peek() == ')' {
794 depth--
795 }
796 if depth > 0 {
797 comment += p.s[:1]
798 }
799 p.s = p.s[1:]
800 }
801
802 return comment, depth == 0
803 }
804
805 func (p *addrParser) decodeRFC2047Word(s string) (word string, isEncoded bool, err error) {
806 dec := p.dec
807 if dec == nil {
808 dec = &rfc2047Decoder
809 }
810
811
812
813
814
815
816
817 adec := *dec
818 charsetReaderError := false
819 adec.CharsetReader = func(charset string, input io.Reader) (io.Reader, error) {
820 if dec.CharsetReader == nil {
821 charsetReaderError = true
822 return nil, charsetError(charset)
823 }
824 r, err := dec.CharsetReader(charset, input)
825 if err != nil {
826 charsetReaderError = true
827 }
828 return r, err
829 }
830 word, err = adec.Decode(s)
831 if err == nil {
832 return word, true, nil
833 }
834
835
836
837
838
839
840 if charsetReaderError {
841 return s, true, err
842 }
843
844
845 return s, false, nil
846 }
847
848 var rfc2047Decoder = mime.WordDecoder{
849 CharsetReader: func(charset string, input io.Reader) (io.Reader, error) {
850 return nil, charsetError(charset)
851 },
852 }
853
854 type charsetError string
855
856 func (e charsetError) Error() string {
857 return fmt.Sprintf("charset not supported: %q", string(e))
858 }
859
860
861
862 func isAtext(r rune, dot bool) bool {
863 switch r {
864 case '.':
865 return dot
866
867
868 case '(', ')', '<', '>', '[', ']', ':', ';', '@', '\\', ',', '"':
869 return false
870 }
871 return isVchar(r)
872 }
873
874
875 func isQtext(r rune) bool {
876
877 if r == '\\' || r == '"' {
878 return false
879 }
880 return isVchar(r)
881 }
882
883
884 func quoteString(s string) string {
885 var b strings.Builder
886 b.WriteByte('"')
887 for _, r := range s {
888 if isQtext(r) || isWSP(r) {
889 b.WriteRune(r)
890 } else if isVchar(r) {
891 b.WriteByte('\\')
892 b.WriteRune(r)
893 }
894 }
895 b.WriteByte('"')
896 return b.String()
897 }
898
899
900 func isVchar(r rune) bool {
901
902 return '!' <= r && r <= '~' || isMultibyte(r)
903 }
904
905
906
907 func isMultibyte(r rune) bool {
908 return r >= utf8.RuneSelf
909 }
910
911
912
913 func isWSP(r rune) bool {
914 return r == ' ' || r == '\t'
915 }
916
View as plain text