RFC 7489: DMARC DNS Record Specification

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.

What You Will Learn

Understand DMARC record syntax and ABNF grammar
Distinguish between mandatory (v, p) and optional tags (sp, adkim, aspf, rua, ruf)
Configure alignment modes for SPF and DKIM
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.

The complete grammar specification from RFC 7489:

; 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 binary logic 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) - RFC 7489 Section 6.3

; 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) - RFC 7489 Section 6.3

; 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 (RFC 7489 Section 7)

rua (Aggregate Report URI) - Detailed Specification

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.

; 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 (RFC 7489 Section 7.1)

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.

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 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.

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 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: 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.

Additional Resources

DMARC Implementation Tools