package dkim
import (
"errors"
"fmt"
"regexp"
"strings"
)
var (
errNoBody = errors.New("no body found")
errUnknownCanonicalization = errors.New("unknown canonicalization")
)
type canonicalization string
var (
simpleCanonicalization canonicalization = "simple"
relaxedCanonicalization canonicalization = "relaxed"
)
func (c canonicalization) body(b string) string {
switch c {
case simpleCanonicalization:
return simpleBody(b)
case relaxedCanonicalization:
return relaxBody(b)
default:
panic("unknown canonicalization")
}
}
func (c canonicalization) headers(hs headers) headers {
switch c {
case simpleCanonicalization:
return hs
case relaxedCanonicalization:
return relaxHeaders(hs)
default:
panic("unknown canonicalization")
}
}
func (c canonicalization) header(h header) header {
switch c {
case simpleCanonicalization:
return h
case relaxedCanonicalization:
return relaxHeader(h)
default:
panic("unknown canonicalization")
}
}
func stringToCanonicalization(s string) (canonicalization, error) {
switch s {
case "simple":
return simpleCanonicalization, nil
case "relaxed":
return relaxedCanonicalization, nil
default:
return "", fmt.Errorf("%w: %s", errUnknownCanonicalization, s)
}
}
// Notes on whitespace reduction:
// https://datatracker.ietf.org/doc/html/rfc6376#section-2.8
// There are only 3 forms of whitespace:
// - WSP = SP / HTAB
// Simple whitespace: space or tab.
// - LWSP = *(WSP / CRLF WSP)
// Linear whitespace: any number of { simple whitespace OR CRLF followed by
// simple whitespace }.
// - FWS = [*WSP CRLF] 1*WSP
// Folding whitespace: optional { simple whitespace OR CRLF } followed by
// one or more simple whitespace.
func simpleBody(body string) string {
// https://datatracker.ietf.org/doc/html/rfc6376#section-3.4.3
// Replace repeated CRLF at the end of the body with a single CRLF.
body = repeatedCRLFAtTheEnd.ReplaceAllString(body, "\r\n")
// Ensure a non-empty body ends with a single CRLF.
// All bodies (including an empty one) must end with a CRLF.
if !strings.HasSuffix(body, "\r\n") {
body += "\r\n"
}
return body
}
var (
// Continued header: WSP after CRLF.
continuedHeader = regexp.MustCompile(`\r\n[ \t]+`)
// WSP before CRLF.
wspBeforeCRLF = regexp.MustCompile(`[ \t]+\r\n`)
// Repeated WSP.
repeatedWSP = regexp.MustCompile(`[ \t]+`)
// Empty lines at the end of the body.
repeatedCRLFAtTheEnd = regexp.MustCompile(`(\r\n)+$`)
)
func relaxBody(body string) string {
// https://datatracker.ietf.org/doc/html/rfc6376#section-3.4.4
body = wspBeforeCRLF.ReplaceAllLiteralString(body, "\r\n")
body = repeatedWSP.ReplaceAllLiteralString(body, " ")
body = repeatedCRLFAtTheEnd.ReplaceAllLiteralString(body, "\r\n")
// Ensure a non-empty body ends with a single CRLF.
if len(body) >= 1 && !strings.HasSuffix(body, "\r\n") {
body += "\r\n"
}
return body
}
func relaxHeader(h header) header {
// https://datatracker.ietf.org/doc/html/rfc6376#section-3.4.2
// Convert all header field names to lowercase.
name := strings.ToLower(h.Name)
// Remove WSP before the ":" separating the name and value.
name = strings.TrimRight(name, " \t")
// Unfold continuation lines in values.
value := continuedHeader.ReplaceAllString(h.Value, " ")
// Reduce all sequences of WSP to a single SP.
value = repeatedWSP.ReplaceAllLiteralString(value, " ")
// Delete all WSP at the end of each unfolded header field value.
value = strings.TrimRight(value, " \t")
// Remove WSP after the ":" separating the name and value.
value = strings.TrimLeft(value, " \t")
return header{
Name: name,
Value: value,
// The "source" is the relaxed field: name, colon, and value (with
// no space around the colon).
Source: name + ":" + value,
}
}
func relaxHeaders(hs headers) headers {
rh := make(headers, 0, len(hs))
for _, h := range hs {
rh = append(rh, relaxHeader(h))
}
return rh
}