package dkim import ( "errors" "fmt" "strings" ) type header struct { Name string Value string Source string } type headers []header // FindAll the headers with the given name, in order of appearance. func (h headers) FindAll(name string) headers { hs := make(headers, 0) for _, header := range h { if strings.EqualFold(header.Name, name) { hs = append(hs, header) } } return hs } var errInvalidHeader = errors.New("invalid header") // Parse a RFC822 message, return the headers, body, and error if any. // We expect it to only contain CRLF line endings. // Does NOT touch whitespace, this is important to preserve the original // message and headers, which is required for the signature. func parseMessage(message string) (headers, string, error) { lines := strings.Split(message, "\r\n") // Group the lines of each header: the first line, followed by its // continuation lines (if any). groups := [][]string{} eoh := 0 for i, line := range lines { if line == "" { eoh = i break } if strings.HasPrefix(line, " ") || strings.HasPrefix(line, "\t") { // Continuation of the previous header. if len(groups) == 0 { return nil, "", fmt.Errorf( "%w: bad continuation", errInvalidHeader) } last := len(groups) - 1 groups[last] = append(groups[last], line) } else { // New header. groups = append(groups, []string{line}) } } headers := make(headers, 0, len(groups)) for _, g := range groups { h, err := parseHeader(g) if err != nil { return nil, "", err } headers = append(headers, h) } return headers, strings.Join(lines[eoh+1:], "\r\n"), nil } // Parse a header, given its lines (the first one, followed by the // continuation lines, if any). func parseHeader(lines []string) (header, error) { // The colon must be in the first line. if !strings.Contains(lines[0], ":") { return header{}, fmt.Errorf("%w: no colon", errInvalidHeader) } source := strings.Join(lines, "\r\n") name, value, _ := strings.Cut(source, ":") return header{ Name: name, Value: value, Source: source, }, nil }