header.go raw
1 package dkim
2
3 import (
4 "bufio"
5 "bytes"
6 "errors"
7 "fmt"
8 "io"
9 "net/textproto"
10 "sort"
11 "strings"
12 )
13
14 const crlf = "\r\n"
15
16 type header []string
17
18 func readHeader(r *bufio.Reader) (header, error) {
19 tr := textproto.NewReader(r)
20
21 var h header
22 for {
23 l, err := tr.ReadLine()
24 if err != nil {
25 return h, fmt.Errorf("failed to read header: %v", err)
26 }
27
28 if len(l) == 0 {
29 break
30 } else if len(h) > 0 && (l[0] == ' ' || l[0] == '\t') {
31 // This is a continuation line
32 h[len(h)-1] += l + crlf
33 } else {
34 h = append(h, l+crlf)
35 }
36 }
37
38 return h, nil
39 }
40
41 func writeHeader(w io.Writer, h header) error {
42 for _, kv := range h {
43 if _, err := w.Write([]byte(kv)); err != nil {
44 return err
45 }
46 }
47 _, err := w.Write([]byte(crlf))
48 return err
49 }
50
51 func foldHeaderField(kv string) string {
52 buf := bytes.NewBufferString(kv)
53
54 line := make([]byte, 75) // 78 - len("\r\n\s")
55 first := true
56 var fold strings.Builder
57 for len, err := buf.Read(line); err != io.EOF; len, err = buf.Read(line) {
58 if first {
59 first = false
60 } else {
61 fold.WriteString("\r\n ")
62 }
63 fold.Write(line[:len])
64 }
65
66 return fold.String() + crlf
67 }
68
69 func parseHeaderField(s string) (string, string) {
70 key, value, _ := strings.Cut(s, ":")
71 return strings.TrimSpace(key), strings.TrimSpace(value)
72 }
73
74 func parseHeaderParams(s string) (map[string]string, error) {
75 pairs := strings.Split(s, ";")
76 params := make(map[string]string)
77 for _, s := range pairs {
78 key, value, ok := strings.Cut(s, "=")
79 if !ok {
80 if strings.TrimSpace(s) == "" {
81 continue
82 }
83 return params, errors.New("dkim: malformed header params")
84 }
85
86 trimmedKey := strings.TrimSpace(key)
87 _, present := params[trimmedKey]
88 if present {
89 return params, errors.New("dkim: duplicate tag name")
90 }
91 params[trimmedKey] = strings.TrimSpace(value)
92 }
93 return params, nil
94 }
95
96 func formatHeaderParams(headerFieldName string, params map[string]string) string {
97 keys, bvalue, bfound := sortParams(params)
98
99 s := headerFieldName + ":"
100 var line string
101
102 for _, k := range keys {
103 v := params[k]
104 nextLength := 3 + len(line) + len(v) + len(k)
105 if nextLength > 75 {
106 s += line + crlf
107 line = ""
108 }
109 line = fmt.Sprintf("%v %v=%v;", line, k, v)
110 }
111
112 if line != "" {
113 s += line
114 }
115
116 if bfound {
117 bfiled := foldHeaderField(" b=" + bvalue)
118 s += crlf + bfiled
119 }
120
121 return s
122 }
123
124 func sortParams(params map[string]string) ([]string, string, bool) {
125 keys := make([]string, 0, len(params))
126 bfound := false
127 var bvalue string
128 for k := range params {
129 if k == "b" {
130 bvalue = params["b"]
131 bfound = true
132 } else {
133 keys = append(keys, k)
134 }
135 }
136 sort.Strings(keys)
137 return keys, bvalue, bfound
138 }
139
140 type headerPicker struct {
141 h header
142 picked map[string]int
143 }
144
145 func newHeaderPicker(h header) *headerPicker {
146 return &headerPicker{
147 h: h,
148 picked: make(map[string]int),
149 }
150 }
151
152 func (p *headerPicker) Pick(key string) string {
153 key = strings.ToLower(key)
154
155 at := p.picked[key]
156 for i := len(p.h) - 1; i >= 0; i-- {
157 kv := p.h[i]
158 k, _ := parseHeaderField(kv)
159
160 if !strings.EqualFold(k, key) {
161 continue
162 }
163
164 if at == 0 {
165 p.picked[key]++
166 return kv
167 }
168 at--
169 }
170
171 return ""
172 }
173