The DMARC standard (RFC 7489, March 2015) defines 11 official fields published in DNS TXT records at _dmarc.domain.com. The upcoming DMARCbis (expected 2025) will deprecate some tags while maintaining v=DMARC1 compatibility.
DMARC Record Syntax (ABNF Grammar)
DMARC records use Augmented Backus-Naur Form (ABNF) per RFC 5234. The complete grammar specification:
; Complete ABNF Grammar for DMARC Records (RFC 7489)
dmarc-record = dmarc-version dmarc-sep dmarc-request
*( dmarc-sep dmarc-tag )
dmarc-version = "v" *WSP "=" *WSP %s"DMARC1"
dmarc-request = "p" *WSP "=" *WSP dmarc-preq
dmarc-preq = "none" / "quarantine" / "reject"
dmarc-sep = *WSP ";" *WSP
dmarc-tag = dmarc-atag / dmarc-ftag / dmarc-otag / dmarc-ptag /
dmarc-rtag / dmarc-stag
; Alignment tags
dmarc-atag = "adkim" *WSP "=" *WSP ("r" / "s")
dmarc-asfpt = "aspf" *WSP "=" *WSP ("r" / "s")
; Percentage tag (being replaced by t=y/n in DMARCbis)
dmarc-ptag = "pct" *WSP "=" *WSP 1*3DIGIT
; Reporting tags
dmarc-rtag = "rua" *WSP "=" *WSP dmarc-uri
/ "ruf" *WSP "=" *WSP dmarc-uri
dmarc-uri = URI [ "!" 1*DIGIT [ "k" / "m" / "g" / "t" ] ]
*( "," URI [ "!" 1*DIGIT [ "k" / "m" / "g" / "t" ] ] )
; Subdomain policy
dmarc-stag = "sp" *WSP "=" *WSP dmarc-preq
; Failure reporting options
dmarc-ftag = "fo" *WSP "=" *WSP dmarc-fopt
dmarc-fopt = "0" / "1" / "d" / "s" / (dmarc-fv *(";" dmarc-fv))
dmarc-fv = "0" / "1" / "d" / "s"
; Tags deprecated in DMARCbis
dmarc-otag = "ri" *WSP "=" *WSP 1*DIGIT
/ "rf" *WSP "=" *WSP "afrf"
Critical Syntax Rules
- 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.
adkim (DKIM Alignment Mode)
; Syntax
adkim-tag = "adkim" *WSP "=" *WSP ( "r" / "s" )
; Alignment Behavior
Relaxed (r): DKIM d= organizational domain matches From: domain
- From:
[email protected]
- DKIM d=example.com → PASS (organizational match)
Strict (s): DKIM d= domain exactly matches From: domain
- From:
[email protected]
- DKIM d=example.com → FAIL (exact match required)
- DKIM d=mail.example.com → PASS (exact match)
aspf (SPF Alignment Mode)
; Syntax
aspf-tag = "aspf" *WSP "=" *WSP ( "r" / "s" )
; Alignment Behavior
Relaxed (r): SPF-validated domain organizationally matches From:
- From:
[email protected]
- SPF validates example.com → PASS
Strict (s): SPF-validated domain exactly matches From:
- From:
[email protected]
- SPF validates example.com → FAIL
- SPF validates mail.example.com → PASS
Organizational Domain Determination
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
Reporting Fields
rua (Aggregate Report URI) - Detailed Specification
; Complete ABNF for RUA
rua-tag = "rua" *WSP "=" *WSP rua-uri-list
rua-uri-list = rua-uri *( "," rua-uri )
rua-uri = "mailto:" addr-spec [ "!" max-size ]
max-size = 1*DIGIT [ size-unit ]
size-unit = "k" / "m" / "g" / "t" ; kilo/mega/giga/tera
; Size Limits (bytes)
k = 1024
m = 1024^2 (1,048,576)
g = 1024^3 (1,073,741,824)
t = 1024^4 (1,099,511,627,776)
External Domain Validation: When rua points to external domains, receiving systems MUST verify authorization:
# DMARC record at example.com
v=DMARC1; p=none; rua=mailto:
[email protected]
# Required authorization at external.com
example.com._report._dmarc.external.com. IN TXT "v=DMARC1"
# Without this record, reports to external.com are discarded
# Format: {policy-domain}._report._dmarc.{external-domain}
ruf (Forensic Report URI) - Technical Details
; 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.
DMARC Record Examples and Validation
Progressive Deployment Examples
Phase 1: Initial Monitoring
# Minimal monitoring record
v=DMARC1; p=none; rua=mailto:
[email protected]
# Enhanced monitoring with forensic reports
v=DMARC1; p=none; rua=mailto:
[email protected]; ruf=mailto:
[email protected]; fo=1
Phase 2: Gradual Enforcement
# Quarantine with percentage rollout (operational issues at intermediate values)
v=DMARC1; p=quarantine; pct=25; rua=mailto:
[email protected]
# DMARCbis approach: use testing flag instead
v=DMARC1; p=quarantine; t=y; rua=mailto:
[email protected]
# t=y means testing mode (like pct=0), t=n means enforce (like pct=100)
Phase 3: Full Protection
# Strict alignment with subdomain protection
v=DMARC1; p=reject; sp=reject; adkim=s; aspf=s; rua=mailto:
[email protected]
# Production enterprise configuration
v=DMARC1; p=reject; sp=quarantine; rua=mailto:
[email protected]!50m,mailto:
[email protected]
Complex Configuration Examples
Multi-Domain Reporting
# 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:
- RFC 7489 tags: adkim, aspf, fo, p, pct, rf, ri, rua, ruf, sp, v
- DMARCbis changes: Removes pct, rf, ri; adds t (testing flag), np (non-existent policy), psd (public suffix domain)
- 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: Being replaced by t=y/n flag in DMARCbis
- Issues: Only pct=0 and pct=100 work reliably; intermediate values inconsistent
- Migration: Use t=y for testing (like pct=0), omit 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;.*p=(none|quarantine|reject)', 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
DMARC_RECORD=$(dig TXT "_dmarc.$DOMAIN" +short | tr -d '"' | tr -d ' ')
if [ -z "$DMARC_RECORD" ]; then
echo "❌ No DMARC record found at _dmarc.$DOMAIN"
exit 1
fi
echo "📧 DMARC Record: $DMARC_RECORD"
# Validate version tag (must be first)
if [[ $DMARC_RECORD =~ ^v=DMARC1 ]]; then
echo "✅ Valid version tag (v=DMARC1)"
else
echo "❌ Invalid or missing version tag"
fi
# Check policy tag (must be second)
if [[ $DMARC_RECORD =~ ;p=(none|quarantine|reject) ]]; then
POLICY=$(echo "$DMARC_RECORD" | grep -o 'p=[^;]*' | cut -d'=' -f2)
echo "✅ Valid policy: $POLICY"
else
echo "❌ Invalid or missing policy tag"
fi
# Check for reporting addresses
if [[ $DMARC_RECORD =~ rua=mailto: ]]; then
RUA=$(echo "$DMARC_RECORD" | grep -o 'rua=[^;]*' | cut -d'=' -f2-)
echo "📊 Aggregate reporting: $RUA"
else
echo "⚠️ No aggregate reporting configured"
fi
# Validate alignment modes
if [[ $DMARC_RECORD =~ adkim=([rs]) ]]; then
ADKIM="${BASH_REMATCH[1]}"
echo "🔒 DKIM alignment: $([ "$ADKIM" = "s" ] && echo "strict" || echo "relaxed")"
fi
if [[ $DMARC_RECORD =~ aspf=([rs]) ]]; then
ASPF="${BASH_REMATCH[1]}"
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" | grep -o 'sp=[^;]*' | cut -d'=' -f2)
echo "🏢 Subdomain policy: $SP"
else
echo "🏢 Subdomain policy: inherits from main policy ($POLICY)"
fi
echo "✅ DMARC validation complete for $DOMAIN"
Technical Implementation: This specification provides the complete foundation for implementing RFC 7489 compliant DMARC record generators and validators. All syntax rules, validation constraints, and edge cases are comprehensively documented.
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.
Additional Resources