The definitive technical reference for implementing RFC 7489 compliant DMARC records. Complete ABNF grammar, all 11 official tags, validation constraints, and practical examples.
RFC 7489, published by the IETF in March 2015, is the authoritative specification for DMARC (Domain-based Message Authentication, Reporting, and Conformance). This RFC defines 11 official tags for DNS TXT records published at _dmarc.domain.com. The upcoming DMARCbis revision will deprecate some tags while maintaining backward compatibility with v=DMARC1.
This guide breaks down RFC 7489, including ABNF grammar, all tag specifications, validation rules, and implementation examples. Whether you are building a DMARC record generator, validating existing records, or troubleshooting authentication failures, you will find what you need here.
Set up aggregate (rua) and forensic (ruf) reporting
Avoid common validation errors and anti-patterns
RFC 7489 Overview
RFC 7489, titled "Domain-based Message Authentication, Reporting, and Conformance (DMARC)", was published as an Informational RFC in March 2015. It builds upon two existing email authentication mechanisms: SPF (RFC 7208) and DKIM (RFC 6376).
RFC 7489 Document Structure
The specification is organized into the following key sections:
Section
Title
Description
3
Terminology
Defines key terms including Identifier Alignment, Organizational Domain, and policy modes
4
Policy
Covers DMARC policy discovery, record format, and formal ABNF grammar
5
DMARC Policy Actions
Defines how receivers should handle messages based on DMARC evaluation
6
Identifier Alignment
Explains relaxed vs strict alignment for SPF and DKIM
7
DMARC Feedback
Specifies aggregate (RUA) and failure (RUF) reporting mechanisms
7.1
Verifying External Destinations
External report destination verification via DNS
Appendix A
DMARC XML Schema
Complete XML schema for aggregate report format
Key Concepts from RFC 7489
RFC 7489 introduces several foundational concepts for email authentication:
Identifier Alignment (Section 3.1): The mechanism that connects SPF and DKIM results to the RFC5322.From domain. This is the core innovation of DMARC.
Organizational Domain (Section 3.2): The domain used for alignment comparisons, determined using the Public Suffix List.
Policy Discovery (Section 4.1): The DNS lookup procedure at _dmarc.domain.com to retrieve DMARC records.
Policy Evaluation (Section 5): The algorithm receivers use to determine disposition based on authentication results and published policy.
RFC 7489 Tag Quick Reference
The following table summarizes all 11 tags defined in RFC 7489 Section 4.2, their status, and default values:
Tag
Purpose
Values
Default
Status
v
Version identifier
DMARC1
Required
Mandatory
p
Domain policy
none | quarantine | reject
Required
Mandatory
sp
Subdomain policy
none | quarantine | reject
p value
Optional
adkim
DKIM alignment mode
r (relaxed) | s (strict)
r
Optional
aspf
SPF alignment mode
r (relaxed) | s (strict)
r
Optional
rua
Aggregate report URIs
mailto:address[!size]
None
Optional
ruf
Forensic report URIs
mailto:address[!size]
None
Optional
fo
Failure reporting options
0 | 1 | d | s
0
Optional
pct
Policy percentage
0-100
100
Deprecated
rf
Report format
afrf
afrf
Deprecated
ri
Report interval (seconds)
Positive integer
86400
Deprecated
Note: Tags marked as "Deprecated" are being removed in the DMARCbis revision. The pct, rf, and ri tags are rarely honored by receivers in practice.
DMARC Record Syntax (RFC 7489 Section 4.2)
RFC 7489 Section 4.2 defines the formal syntax for DMARC records using ABNF (Augmented Backus-Naur Form) as specified in RFC 5234. You need to understand this grammar to implement compliant DMARC parsers and validators.
Mandatory ordering: v=DMARC1 MUST be first tag, p= MUST be second
Case sensitivity: Tag names lowercase, DMARC1 uppercase, policy values lowercase
Whitespace handling: Optional around = and ; separators
String concatenation: Multiple DNS TXT strings automatically concatenated
Maximum length: Individual TXT strings limited to 255 characters
Mandatory separators: Semicolon (;) between tags, equals sign (=) between tag and value, comma (,) for URI lists.
Mandatory Fields
v (Version)
Type: Fixed string
Value: "DMARC1" (case-sensitive)
Position: First mandatory tag
Validation: Exact match required
p (Policy)
Type: Enumeration
Values: "none" | "quarantine" | "reject"
Position: Second mandatory tag
Function: Processing policy for primary domain
Optional Control Fields (Detailed Specifications)
sp (Subdomain Policy)
; Syntax
sp-tag = "sp" *WSP "=" *WSP ( "none" / "quarantine" / "reject" )
; Validation Rules
- MUST use exact lowercase string values
- If omitted, subdomains inherit parent domain policy (p=)
- Applies to all subdomains (*.example.com)
- Does not affect parent domain processing
Subdomain Inheritance Logic: When sp= is absent, mail.example.com inherits the p= policy from example.com. When sp= is present, it overrides this inheritance for all subdomains.
DMARC alignment currently uses the Public Suffix List (PSL) to determine organizational domains. DMARCbis will replace this with DNS Tree Walk algorithm:
# Examples of organizational domain matching
example.com → example.com (base domain)
mail.example.com → example.com (subdomain of base)
api.mail.example.com → example.com (nested subdomain)
# Special cases with PSL
example.co.uk → example.co.uk (co.uk is public suffix)
mail.example.co.uk → example.co.uk (organizational match)
# Edge cases
example.github.io → example.github.io (github.io is public suffix)
api.example.github.io → example.github.io (organizational match)
# DMARCbis Tree Walk (upcoming)
# Traverses DNS hierarchy up to 7 labels to find DMARC records
# Replaces external PSL dependency with pure DNS approach
The "Daily News" of Your Domain. Think of rua as your daily executive summary. It tells you who is sending email as your domain and how those emails are performing against authentication checks (SPF/DKIM).
These reports are XML files sent periodically (usually daily) to the addresses you list. They provide aggregated data and statistics (e.g., "IP 1.2.3.4 sent 500 emails claiming to be you; 490 passed SPF"). While they do not contain email message content, they **do include source IP addresses and other identifiers**. These identifiers can be considered Personal Data (PII) under privacy regulations like GDPR. Therefore, handling RUA reports still requires appropriate data protection considerations.
The "Knock on the Door" Rule. You cannot simply tell the internet to dump your reports on someone else's doorstep without their permission. This prevents malicious actors from "spamming" a victim's inbox with millions of DMARC reports from random domains.
If your domain is example.com but you want reports sent to security-service.com, the receiving domain (security-service.com) must explicitly publish a DNS record saying, "Yes, I accept reports from example.com."
# Scenario: example.com sending reports to [email protected]
# 1. The DMARC record at example.com
v=DMARC1; p=none; rua=mailto:[email protected]
# 2. The REQUIRED "Permission Slip" at external.com
# DNS Host: external.com
# Record Name: example.com._report._dmarc
# Record Type: TXT
# Value: v=DMARC1
# Without this "digital handshake," reports are silently discarded.
ruf (Forensic Report URI) - Technical Details
The "Crime Scene Investigation" Kit. While RUA gives you statistics, ruf attempts to give you the smoking gun. When a specific email fails authentication, this triggers a "Failure Report" containing the actual message headers and sometimes the body.
Privacy Warning: Because these reports contain real user data (Subject lines, email content), they are a privacy minefield (GDPR, etc.). Consequently, most major email providers (Gmail, Yahoo, Microsoft) refuse to send them. They are rarely used in modern production environments.
; RUF uses identical syntax to RUA
ruf-tag = "ruf" *WSP "=" *WSP rua-uri-list
; Privacy Concerns (Why rarely implemented)
- Contains full message headers
- May include message body content
- Privacy regulations (GDPR) compliance issues
- High volume potential (per-message vs daily aggregate)
- Major providers (Gmail, Yahoo, Microsoft) no longer send
; Recommended: Use RUA only for most implementations
fo (Failure Options) - Complete Specification
; ABNF Grammar
fo-tag = "fo" *WSP "=" *WSP fo-option-list
fo-option-list = fo-option *( ":" fo-option )
fo-option = "0" / "1" / "d" / "s"
; Detailed Semantics
0 - Generate report if both SPF and DKIM produce something
other than "pass" (default behavior)
1 - Generate report if either SPF or DKIM produces something
other than "pass"
d - Generate report if DKIM evaluation fails to produce "pass"
s - Generate report if SPF evaluation fails to produce "pass"
; Combination Examples
fo=0 - Both must fail (most restrictive)
fo=1 - Either fails (most reports)
fo=1:d - Either fails OR DKIM specifically fails
fo=d:s - DKIM fails OR SPF fails (equivalent to fo=1)
Failure Option Processing Logic
# Pseudo-code for fo= processing
def should_generate_ruf_report(spf_result, dkim_result, fo_options):
spf_fail = spf_result != "pass"
dkim_fail = dkim_result != "pass"
for option in fo_options:
if option == "0" and spf_fail and dkim_fail:
return True
elif option == "1" and (spf_fail or dkim_fail):
return True
elif option == "d" and dkim_fail:
return True
elif option == "s" and spf_fail:
return True
return False
Critical Validation Constraints
Mandatory tag ordering: The most common error is incorrect ordering. v=DMARC1 must be first, followed by p=.
Strict syntax validation: Enumerated values are case-sensitive (except DMARC1). A value "Reject" instead of "reject" invalidates the entire record.
DNS TXT constraints: Maximum length of 255 characters per string. Long records require automatic concatenation of multiple strings.
Cross-domain verification: For rua and ruf URIs pointing to external domains, a DNS record _report._dmarc.external-domain is mandatory to authorize report reception.
# Quarantine with percentage rollout
v=DMARC1; p=quarantine; pct=25; rua=mailto:[email protected]
# Note: While pct allows values 0-100, many experts recommend
# using only pct=0 (testing) or pct=100 (enforcement)
# to avoid inconsistent handling by receivers.
# Company using external DMARC service
v=DMARC1; p=quarantine; rua=mailto:[email protected],mailto:[email protected]!100m
# Required authorization at external service
# DNS: example.com._report._dmarc.dmarcanalyzer.com. IN TXT "v=DMARC1"
High-Volume Domain Configuration
# Large organization with size limits and backup reporting
v=DMARC1; p=reject; sp=none; rua=mailto:[email protected]!500m,mailto:[email protected]!1g; adkim=r; aspf=r
Edge Cases and Special Configurations
Subdomain-Only Protection
# Protect subdomains while leaving parent domain open
v=DMARC1; p=none; sp=reject; rua=mailto:[email protected]
# Use case: Parent domain doesn't send email,
# but subdomains (mail.example.com) do
DNS TXT Record Splitting (Long Records)
# Single logical record split across multiple TXT strings
_dmarc.example.com. IN TXT "v=DMARC1; p=reject; rua=mailto:[email protected]!10m"
"mailto:backup-reports-with-another-very-long-email@monitoring.external-service.com!50m; adkim=s; aspf=s; sp=quarantine"
# DNS automatically concatenates: "v=DMARC1; p=reject; rua=mailto:..."
Common Invalid Records (Anti-Patterns)
# INVALID: Wrong tag order
p=reject; v=DMARC1; rua=mailto:[email protected]
# INVALID: Case sensitivity error
v=dmarc1; p=REJECT; rua=mailto:[email protected]
# INVALID: Missing mandatory p= tag
v=DMARC1; rua=mailto:[email protected]
# INVALID: Invalid policy value
v=DMARC1; p=block; rua=mailto:[email protected]
# INVALID: Malformed URI
v=DMARC1; p=none; [email protected] ; missing mailto:
# INVALID: Percentage out of range
v=DMARC1; p=quarantine; pct=150; rua=mailto:[email protected]
DMARC Record Generation and Validation Algorithm
Standard Generation Algorithm
# DMARC Record Generation (Pseudocode)
def generate_dmarc_record(policy, options={}):
# Step 1: Initialize with mandatory tags
record = "v=DMARC1"
record += "; p=" + policy # policy must be none|quarantine|reject
# Step 2: Add optional tags in recommended order
if 'sp' in options:
record += "; sp=" + options['sp']
if 'adkim' in options:
record += "; adkim=" + options['adkim'] # r|s
if 'aspf' in options:
record += "; aspf=" + options['aspf'] # r|s
if 'pct' in options and options['pct'] != 100:
record += "; pct=" + str(options['pct']) # 0-100
if 'rua' in options:
record += "; rua=" + format_uri_list(options['rua'])
if 'ruf' in options:
record += "; ruf=" + format_uri_list(options['ruf'])
if 'fo' in options:
record += "; fo=" + ":".join(options['fo']) # 0,1,d,s
# Step 3: Validate total length and split if needed
return split_dns_record_if_needed(record)
def format_uri_list(uri_list):
formatted = []
for uri_info in uri_list:
uri = uri_info['address']
if 'size_limit' in uri_info:
uri += "!" + str(uri_info['size_limit']) + uri_info['unit']
formatted.append(uri)
return ",".join(formatted)
Validation Algorithm
# DMARC Record Validation (Pseudocode)
def validate_dmarc_record(record_string):
errors = []
# Parse tag-value pairs
tags = parse_dmarc_tags(record_string)
# Rule 1: First tag must be v=DMARC1
if not tags or tags[0] != ('v', 'DMARC1'):
errors.append("First tag must be v=DMARC1")
# Rule 2: Second tag must be p=
if len(tags) < 2 or not tags[1][0] == 'p':
errors.append("Second tag must be p=")
# Rule 3: Validate policy values
if tags[1][1] not in ['none', 'quarantine', 'reject']:
errors.append("Invalid policy value: " + tags[1][1])
# Rule 4: Validate all tag syntax
for tag_name, tag_value in tags:
if tag_name == 'sp' and tag_value not in ['none', 'quarantine', 'reject']:
errors.append("Invalid sp value: " + tag_value)
elif tag_name == 'adkim' and tag_value not in ['r', 's']:
errors.append("Invalid adkim value: " + tag_value)
elif tag_name == 'aspf' and tag_value not in ['r', 's']:
errors.append("Invalid aspf value: " + tag_value)
elif tag_name == 'pct':
try:
pct_val = int(tag_value)
if not 0 <= pct_val <= 100:
errors.append("pct must be 0-100")
except ValueError:
errors.append("pct must be integer")
elif tag_name in ['rua', 'ruf']:
validate_uri_list(tag_value, errors)
return errors
def validate_uri_list(uri_string, errors):
for uri in uri_string.split(','):
if not uri.strip().startswith('mailto:'):
errors.append(f"URI must use mailto: scheme: {uri}")
# Additional URI validation...
DNS Record Length Handling
# Handle DNS TXT record 255-character limit
def split_dns_record_if_needed(record):
if len(record) <= 255:
return record
# Split at word boundaries, preferring after semicolons
parts = []
current_part = ""
for char in record:
if len(current_part) + 1 > 255 and char in [';', ' ']:
parts.append(current_part)
current_part = ""
current_part += char
if current_part:
parts.append(current_part)
# Return as multiple DNS TXT strings
return parts
# Example DNS zone file format for long records
_dmarc.example.com. IN TXT "v=DMARC1; p=reject; rua=mailto:[email protected]"
"additional tags and values continue here"
IANA DMARC Tag Registry
The official IANA registry defines 11 standardized tags:
Note: Records remain v=DMARC1 for backward compatibility
Deprecated Fields (DMARCbis)
The following fields are being deprecated in DMARCbis and should be avoided in new implementations:
pct (Percentage)
Type: Integer
Range: 0-100
Default: 100
Status: Likely to be deprecated in DMARCbis in favor of binary logic
Issues: Only pct=0 and pct=100 work reliably; intermediate values inconsistent
Migration: Stick to pct=0 for testing and pct=100 for enforcement
ri (Report Interval)
Type: Integer (seconds)
Default: 86400 (24h)
Status: Removed in DMARCbis (rarely honored by receivers)
Reality: Most receivers send daily regardless of ri value
rf (Report Format)
Type: Enumeration
Value: "afrf" (only supported value)
Status: Removed in DMARCbis (no other formats ever implemented)
DMARC Testing and Validation Tools
Command-Line Validation
# Check DMARC DNS record
dig TXT _dmarc.example.com +short
# Test from multiple DNS servers
for server in 8.8.8.8 1.1.1.1 208.67.222.222; do
echo "Testing $server:"
dig @$server TXT _dmarc.example.com +short
done
# Validate DMARC syntax with Python
python3 -c "
import re
record = 'v=DMARC1; p=reject; rua=mailto:[email protected]'
if re.match(r'^v=DMARC1;\s*p=(none|quarantine|reject)\b', record):
print('Valid DMARC record')
else:
print('Invalid DMARC record')
"
Comprehensive DMARC Testing Script
#!/bin/bash
# Comprehensive DMARC record validation script
DOMAIN="$1"
if [ -z "$DOMAIN" ]; then
echo "Usage: $0 domain.com"
exit 1
fi
echo "Testing DMARC for $DOMAIN..."
# Check if DMARC record exists
# Using tr -d ' ' simplifies parsing by removing whitespace
DMARC_RECORD=$(dig TXT "_dmarc.$DOMAIN" +short | tr -d '"' | tr -d ' ')
if [ -z "$DMARC_RECORD" ]; then
echo "ERROR: No DMARC record found at _dmarc.$DOMAIN"
exit 1
fi
echo "DMARC Record: $DMARC_RECORD"
# Validate version tag (must be first)
# Note: We removed spaces, so it must start with v=DMARC1
if [[ "$DMARC_RECORD" =~ ^v=DMARC1 ]]; then
echo "OK: Valid version tag (v=DMARC1)"
else
echo "ERROR: Invalid or missing version tag (Must start with v=DMARC1)"
fi
# Check policy tag (must be second)
# Regex matches ;p= followed by policy value
if [[ "$DMARC_RECORD" =~ \;p=(none|quarantine|reject) ]]; then
# Extract policy value
POLICY=$(echo "$DMARC_RECORD" | sed -n 's/.*;p=\([^;]*\).*/\1/p')
echo "OK: Valid policy: $POLICY"
else
echo "ERROR: Invalid or missing policy tag (Must be second tag)"
fi
# Check for reporting addresses
if [[ "$DMARC_RECORD" =~ rua=mailto: ]]; then
# Extract RUA
RUA=$(echo "$DMARC_RECORD" | sed -n 's/.*rua=\([^;]*\).*/\1/p')
echo "Aggregate reporting: $RUA"
else
echo "WARNING: No aggregate reporting configured"
fi
# Validate alignment modes
if [[ "$DMARC_RECORD" =~ adkim=([rs]) ]]; then
ADKIM=$(echo "$DMARC_RECORD" | sed -n 's/.*adkim=\([rs]\).*/\1/p')
echo "DKIM alignment: $([ "$ADKIM" = "s" ] && echo "strict" || echo "relaxed")"
fi
if [[ "$DMARC_RECORD" =~ aspf=([rs]) ]]; then
ASPF=$(echo "$DMARC_RECORD" | sed -n 's/.*aspf=\([rs]\).*/\1/p')
echo "SPF alignment: $([ "$ASPF" = "s" ] && echo "strict" || echo "relaxed")"
fi
# Check subdomain policy
if [[ "$DMARC_RECORD" =~ sp=(none|quarantine|reject) ]]; then
SP=$(echo "$DMARC_RECORD" | sed -n 's/.*sp=\([^;]*\).*/\1/p')
echo "Subdomain policy: $SP"
else
# If policy is defined, we can reference it
if [ -n "$POLICY" ]; then
echo "Subdomain policy: inherits from main policy ($POLICY)"
else
echo "Subdomain policy: unknown (main policy missing)"
fi
fi
echo "DMARC validation complete for $DOMAIN"
Technical Implementation: This specification covers what you need to implement RFC 7489 compliant DMARC record generators and validators, including syntax rules, validation constraints, and edge cases.
Common Implementation Errors: Wrong tag ordering, case sensitivity mistakes, invalid policy values, malformed URIs, missing external domain authorization, and exceeding DNS record length limits. Always validate against this specification.