internal/mailin/header.go

e6cd75b5f28bacf51620bb531320c30fd4e66bfd
gitbay/internal/mailin/header.go history · blame · raw

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}