git » chasquid » next » tree

[next] / internal / dkim / message.go

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 {
		// Compare using strings.ToLower and not strings.EqualFold, to be
		// consistent with the verifier (see headersToInclude). They differ
		// for some non-ASCII characters, which are invalid in header names
		// but can still appear in messages.
		if strings.ToLower(header.Name) == strings.ToLower(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{}

	// End of headers. If there's no blank line, all the lines are headers.
	eoh := len(lines)
	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)
	}

	// The body is everything after the blank line (if there is one).
	body := ""
	if eoh < len(lines) {
		body = strings.Join(lines[eoh+1:], "\r\n")
	}

	return headers, body, 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
}