internal/mailin/header.go
102 lines · 2721 bytes
4 symbols in this file
1package mailin
2
3import (
4 "bytes"
5 "strings"
6)
7
8// rawHeader is what the checks read from the header section as
9// fetched, before net/mail or the dkim package interpret it.
10type rawHeader struct {
11 count map[string]int // field name, lower case, to occurrences
12 dkimB []string // b= of each DKIM-Signature, in order, whitespace removed
13 cte string // Content-Transfer-Encoding, unfolded, trimmed, lower case
14}
15
16// single names the fields a reply may carry at most once; From must
17// appear exactly once.
18var single = []string{"from", "to", "cc", "message-id", "content-type", "content-transfer-encoding"}
19
20// parseRawHeader reads the header section of raw. A field name must be
21// RFC 5322 ftext (printable US-ASCII other than ":"), so "From : x",
22// which net/mail files under another name than the dkim package does,
23// is refused rather than read two ways. It returns the refusal's
24// reason, or "".
25func parseRawHeader(raw []byte) (rawHeader, string) {
26 h := rawHeader{count: map[string]int{}}
27 var dkimVals []string
28 cur := -1 // index in dkimVals of the DKIM-Signature being continued
29 inCTE := false
30 for len(raw) > 0 {
31 line := raw
32 if i := bytes.IndexByte(raw, '\n'); i >= 0 {
33 line, raw = raw[:i], raw[i+1:]
34 } else {
35 raw = nil
36 }
37 line = bytes.TrimSuffix(line, []byte("\r"))
38 if len(line) == 0 {
39 break
40 }
41 if line[0] == ' ' || line[0] == '\t' {
42 if len(h.count) == 0 {
43 return h, "malformed header"
44 }
45 if cur >= 0 {
46 dkimVals[cur] += string(line)
47 }
48 if inCTE {
49 h.cte += string(line)
50 }
51 continue
52 }
53 name, value, ok := bytes.Cut(line, []byte(":"))
54 if !ok || len(name) == 0 {
55 return h, "malformed header"
56 }
57 for _, c := range name {
58 if c < 33 || c > 126 {
59 return h, "malformed header field name"
60 }
61 }
62 n := strings.ToLower(string(name))
63 h.count[n]++
64 cur = -1
65 inCTE = n == "content-transfer-encoding"
66 if inCTE {
67 h.cte = string(value)
68 }
69 if n == "dkim-signature" {
70 dkimVals = append(dkimVals, string(value))
71 cur = len(dkimVals) - 1
72 }
73 }
74 h.cte = strings.ToLower(strings.TrimSpace(h.cte))
75 for _, v := range dkimVals {
76 h.dkimB = append(h.dkimB, tagB(v))
77 }
78 if n := h.count["from"]; n != 1 {
79 if n == 0 {
80 return h, "no From field"
81 }
82 return h, "more than one From field"
83 }
84 for _, n := range single[1:] {
85 if h.count[n] > 1 {
86 return h, "more than one " + n + " field"
87 }
88 }
89 return h, ""
90}
91
92// tagB returns the b= tag of a DKIM-Signature value with whitespace
93// removed, or "".
94func tagB(v string) string {
95 for _, t := range strings.Split(v, ";") {
96 k, val, ok := strings.Cut(t, "=")
97 if ok && strings.TrimSpace(k) == "b" {
98 return strings.Join(strings.Fields(val), "")
99 }
100 }
101 return ""
102}