12func (e parseErr) Error() string {
 
16// ParseRecord parses a DMARC TXT record.
 
18// Fields and values are are case-insensitive in DMARC are returned in lower case
 
19// for easy comparison.
 
21// DefaultRecord provides default values for tags not present in s.
 
23// isdmarc indicates if the record starts tag "v" with value "DMARC1", and should
 
24// be treated as a valid DMARC record. Used to detect possibly multiple DMARC
 
25// records (invalid) for a domain with multiple TXT record (quite common).
 
26func ParseRecord(s string) (record *Record, isdmarc bool, rerr error) {
 
27	return parseRecord(s, true)
 
30// ParseRecordNoRequired is like ParseRecord, but don't check for required fields
 
31// for regular DMARC records. Useful for checking the _report._dmarc record,
 
32// used for opting into receiving reports for other domains.
 
33func ParseRecordNoRequired(s string) (record *Record, isdmarc bool, rerr error) {
 
34	return parseRecord(s, false)
 
37func parseRecord(s string, checkRequired bool) (record *Record, isdmarc bool, rerr error) {
 
43		if err, ok := x.(parseErr); ok {
 
58	r.Version = p.xtakecase("DMARC1")
 
62	seen := map[string]bool{}
 
69		w := strings.ToLower(W)
 
71			// RFC does not say anything about duplicate tags. They can only confuse, so we
 
73			p.xerrorf("duplicate tag %q", W)
 
83			// We just parse until the next semicolon or end.
 
93				p.xerrorf("p= (policy) must be first tag")
 
95			r.Policy = Policy(p.xtakelist("none", "quarantine", "reject"))
 
97			r.SubdomainPolicy = Policy(p.xkeyword())
 
98			// note: we check if the value is valid before returning.
 
100			r.AggregateReportAddresses = append(r.AggregateReportAddresses, p.xuri())
 
104				r.AggregateReportAddresses = append(r.AggregateReportAddresses, p.xuri())
 
108			r.FailureReportAddresses = append(r.FailureReportAddresses, p.xuri())
 
112				r.FailureReportAddresses = append(r.FailureReportAddresses, p.xuri())
 
116			r.ADKIM = Align(p.xtakelist("r", "s"))
 
118			r.ASPF = Align(p.xtakelist("r", "s"))
 
120			r.AggregateReportingInterval = p.xnumber()
 
122			r.FailureReportingOptions = []string{p.xtakelist("0", "1", "d", "s")}
 
126				r.FailureReportingOptions = append(r.FailureReportingOptions, p.xtakelist("0", "1", "d", "s"))
 
130			r.ReportingFormat = []string{p.xkeyword()}
 
134				r.ReportingFormat = append(r.ReportingFormat, p.xkeyword())
 
138			r.Percentage = p.xnumber()
 
139			if r.Percentage > 100 {
 
140				p.xerrorf("bad percentage %d", r.Percentage)
 
144		if !p.take(";") && !p.empty() {
 
145			p.xerrorf("expected ;")
 
150	// able to parse a record without a "p" or with invalid "sp" tag.
 
151	sp := r.SubdomainPolicy
 
152	if checkRequired && (!seen["p"] || sp != PolicyEmpty && sp != PolicyNone && sp != PolicyQuarantine && sp != PolicyReject) {
 
153		if len(r.AggregateReportAddresses) > 0 {
 
154			r.Policy = PolicyNone
 
155			r.SubdomainPolicy = PolicyEmpty
 
157			p.xerrorf("invalid (subdomain)policy and no valid aggregate reporting address")
 
170// toLower lower cases bytes that are A-Z. strings.ToLower does too much. and
 
171// would replace invalid bytes with unicode replacement characters, which would
 
172// break our requirement that offsets into the original and upper case strings
 
173// point to the same character.
 
174func toLower(s string) string {
 
176	for i, c := range r {
 
177		if c >= 'A' && c <= 'Z' {
 
184func newParser(s string) *parser {
 
191func (p *parser) xerrorf(format string, args ...any) {
 
192	msg := fmt.Sprintf(format, args...)
 
194		msg += fmt.Sprintf(" (remain %q)", p.s[p.o:])
 
199func (p *parser) empty() bool {
 
200	return p.o >= len(p.s)
 
203func (p *parser) peek(b byte) bool {
 
204	return p.o < len(p.s) && p.s[p.o] == b
 
207// case insensitive prefix
 
208func (p *parser) prefix(s string) bool {
 
209	return strings.HasPrefix(p.lower[p.o:], s)
 
212func (p *parser) take(s string) bool {
 
220func (p *parser) xtaken(n int) string {
 
221	r := p.lower[p.o : p.o+n]
 
226func (p *parser) xtake(s string) string {
 
228		p.xerrorf("expected %q", s)
 
230	return p.xtaken(len(s))
 
233func (p *parser) xtakecase(s string) string {
 
234	if !strings.HasPrefix(p.s[p.o:], s) {
 
235		p.xerrorf("expected %q", s)
 
237	r := p.s[p.o : p.o+len(s)]
 
243func (p *parser) wsp() {
 
244	for !p.empty() && (p.s[p.o] == ' ' || p.s[p.o] == '\t') {
 
249// take one of the strings in l.
 
250func (p *parser) xtakelist(l ...string) string {
 
251	for _, s := range l {
 
253			return p.xtaken(len(s))
 
256	p.xerrorf("expected on one %v", l)
 
260func (p *parser) xtakefn1case(fn func(byte, int) bool) string {
 
261	for i, b := range []byte(p.lower[p.o:]) {
 
264				p.xerrorf("expected at least one char")
 
270		p.xerrorf("expected at least 1 char")
 
277// used for the tag keys.
 
278func (p *parser) xword() string {
 
279	return p.xtakefn1case(func(c byte, i int) bool {
 
280		return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9'
 
284func (p *parser) xdigits() string {
 
285	return p.xtakefn1case(func(b byte, i int) bool {
 
292func (p *parser) xuri() URI {
 
293	// Ideally, we would simply parse an URI here. But a URI can contain a semicolon so
 
294	// could consume the rest of the DMARC record. Instead, we'll assume no one uses
 
295	// semicolons in URIs in DMARC records and first collect
 
296	// space/comma/semicolon/end-separated characters, then parse.
 
298	v := p.xtakefn1case(func(b byte, i int) bool {
 
299		return b != ',' && b != ' ' && b != '\t' && b != ';'
 
301	t := strings.SplitN(v, "!", 2)
 
302	u, err := url.Parse(t[0])
 
304		p.xerrorf("parsing uri %q: %s", t[0], err)
 
307		p.xerrorf("missing scheme in uri")
 
317			case 'k', 'K', 'm', 'M', 'g', 'G', 't', 'T':
 
318				uri.Unit = strings.ToLower(o[len(o)-1:])
 
322		uri.MaxSize, err = strconv.ParseUint(o, 10, 64)
 
324			p.xerrorf("parsing max size for uri: %s", err)
 
330func (p *parser) xnumber() int {
 
331	digits := p.xdigits()
 
332	v, err := strconv.Atoi(digits)
 
334		p.xerrorf("parsing %q: %s", digits, err)
 
339func (p *parser) xkeyword() string {
 
343	return p.xtakefn1case(func(b byte, i int) bool {
 
344		return isalphadigit(b) || (b == '-' && i < n-1 && isalphadigit(p.s[p.o+i+1]))
 
348func isdigit(b byte) bool {
 
349	return b >= '0' && b <= '9'
 
352func isalpha(b byte) bool {
 
353	return b >= 'a' && b <= 'z' || b >= 'A' && b <= 'Z'
 
356func isalphadigit(b byte) bool {
 
357	return isdigit(b) || isalpha(b)