The 2024-2026 Email Authentication Revolution
In February 2024, Google and Yahoo enforced their bulk sender requirements: SPF or DKIM alignment, DMARC policy, one-click unsubscribe, and a spam rate below 0.3%. By 2025, Microsoft 365 followed with similar requirements for high-volume senders. Organizations that failed to comply saw delivery rates plummet overnight β in some cases, 30-40% of business email began routing to spam.
This guide covers every layer of email authentication, from the basics of how SPF/DKIM/DMARC work together, to advanced configurations that achieve 99%+ inbox placement, to BIMI implementation that puts your logo in Gmail inboxes.
The Email Authentication Stack Explained
How Email Authentication Works (The Big Picture)
Sender Receiving Server (Gmail)
β β
ββ SMTP Connection βββββββββββ β
β ββ 1. Check SPF
β From: marketing@company.com β "Is this IP allowed to send for company.com?"
β DKIM-Signature: v=1; a=... ββ 2. Check DKIM
β β "Is this signature valid? Was email tampered?"
β ββ 3. Check DMARC
β β "Do SPF/DKIM align with From: header?
β β What should I do if they don't?"
β ββ 4. Check BIMI (if DMARC=quarantine/reject)
β β "Is there a verified logo to display?"
β ββ 5. Apply policy + inbox/spam/reject
SPF: Sender Policy Framework
SPF authorizes specific IP addresses and mail servers to send email on behalf of your domain. It's a DNS TXT record that receiving servers check when your email arrives.
Writing a Correct SPF Record
# Basic SPF record structure
v=spf1 [mechanisms] [modifier]
# Mechanisms (what is authorized):
# ip4:1.2.3.4 - Specific IPv4 address
# ip4:1.2.3.0/24 - IPv4 CIDR range
# ip6:2001:db8::/32 - IPv6 range
# a - Domain's A record IPs
# mx - Domain's MX record IPs
# include:domain - Include another domain's SPF
# redirect=domain - Delegate entirely to another domain's SPF
# Qualifiers (what to do when matched):
# + (pass, default) - Authorize this source
# - (fail) - Unauthorized, should reject
# ~ (softfail) - Probably unauthorized, soft reject
# ? (neutral) - No policy
# EXAMPLE: Company using multiple services
# AWS SES + Google Workspace + Mailchimp + Sendgrid
v=spf1
ip4:54.240.0.0/18 # AWS SES us-east-1
include:_spf.google.com # Google Workspace
include:servers.mcsv.net # Mailchimp
include:sendgrid.net # SendGrid
~all # Softfail everything else
SPF Lookup Limit Problem
SPF has a 10 DNS lookup limit. Modern stacks easily exceed this with multiple ESPs (Email Service Providers). Exceeding it causes SPF permerror β effectively a failure.
# Check your SPF lookup count
dig +short TXT company.com | grep spf
# Use dmarcian's SPF survey tool
curl "https://dmarcian.com/spf-survey/?domain=company.com"
# Count lookups manually:
# include: = 1 lookup per include
# a: = 1 lookup
# mx: = 1 lookup + 1 per MX record
# exists: = 1 lookup
# FIX: SPF flattening (resolve includes to IPs at publish time)
# Tools: mxtoolbox.com/spf, dmarcian, PowerSPF, AutoSPF
# Result:
v=spf1
ip4:54.240.0.0/18
ip4:74.125.0.0/16
ip4:198.2.128.0/18
ip4:149.72.0.0/16
ip4:167.89.0.0/17
~all
# (flattened from 4 includes to direct IP ranges, 1 lookup total)
DKIM: DomainKeys Identified Mail
DKIM adds a cryptographic signature to every outbound email. The receiving server verifies the signature using a public key in your DNS. If email is tampered in transit, the signature breaks.
Generating and Configuring DKIM Keys
# Generate 2048-bit RSA key pair (minimum; use 4096 for new setups)
openssl genrsa -out dkim-private.pem 2048
openssl rsa -in dkim-private.pem -pubout -out dkim-public.pem
# Extract public key for DNS (remove header/footer, strip newlines)
openssl rsa -in dkim-private.pem -pubout -outform DER | openssl base64 -A
# DNS TXT record format:
# Selector: mail (can be any string, use date-based for rotation: 20260301)
# Record name: mail._domainkey.company.com
# Record value:
v=DKIM1; k=rsa; p=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...
# Postfix DKIM setup with OpenDKIM
sudo apt-get install opendkim opendkim-tools
# /etc/opendkim.conf
AutoRestart Yes
AutoRestartRate 10/1h
UMask 002
Syslog yes
LogWhy Yes
Canonicalization relaxed/simple
ExternalIgnoreList refile:/etc/opendkim/TrustedHosts
InternalHosts refile:/etc/opendkim/TrustedHosts
KeyTable refile:/etc/opendkim/KeyTable
SigningTable refile:/etc/opendkim/SigningTable
Mode sv
PidFile /var/run/opendkim/opendkim.pid
SignatureAlgorithm rsa-sha256
UserID opendkim:opendkim
Socket inet:8891@localhost
# /etc/opendkim/KeyTable
mail._domainkey.company.com company.com:mail:/etc/opendkim/keys/company.com/mail.private
# /etc/opendkim/SigningTable
*@company.com mail._domainkey.company.com
# Test DKIM signing
echo "Test email" | mail -s "DKIM Test" test@gmail.com
# Check headers for DKIM-Signature field
DKIM Key Rotation (Security Best Practice)
# Rotate DKIM keys every 6-12 months
# Step 1: Generate new key with new selector (date-based)
opendkim-genkey -b 2048 -d company.com -s 20260301
# Step 2: Publish NEW key in DNS (both old and new coexist)
# 20260301._domainkey.company.com β new key
# mail._domainkey.company.com β old key (still valid)
# Step 3: Wait 48 hours for DNS propagation
# Step 4: Switch signing to new selector
# Update /etc/opendkim/KeyTable to use 20260301 selector
# Step 5: After 1 week (old emails still being validated)
# Remove old DNS record
# mail._domainkey.company.com β DELETE
# Step 6: Verify with MXToolbox
curl "https://mxtoolbox.com/dkim.aspx?domain=company.com&selector=20260301"
DMARC: Domain-Based Message Authentication, Reporting, and Conformance
DMARC ties SPF and DKIM together and tells receiving servers what to do when authentication fails. Critically, it also provides reporting β you receive daily XML reports showing who is sending email with your domain.
DMARC Record Configuration
# DNS TXT record at _dmarc.company.com
# Start permissive (monitoring only)
v=DMARC1; p=none; rua=mailto:dmarc@company.com; ruf=mailto:dmarc-forensic@company.com; fo=1
# After reviewing reports and fixing issues: quarantine
v=DMARC1; p=quarantine; pct=10; rua=mailto:dmarc@company.com; fo=1
# Gradually increase pct to 100%
v=DMARC1; p=quarantine; pct=100; rua=mailto:dmarc@company.com
# Full enforcement (recommended end state)
v=DMARC1; p=reject; rua=mailto:dmarc@company.com; ruf=mailto:dmarc-forensic@company.com; fo=1; adkim=s; aspf=s
# Tag reference:
# p=none|quarantine|reject Policy for failing messages
# pct=100 Percentage of messages to apply policy to
# rua=mailto:... Aggregate report destination
# ruf=mailto:... Forensic report destination
# fo=0|1|d|s Failure reporting options
# adkim=r|s DKIM alignment (relaxed/strict)
# aspf=r|s SPF alignment (relaxed/strict)
# sp=none|quarantine|reject Subdomain policy
DMARC Alignment Explained
STRICT alignment (adkim=s, aspf=s):
DKIM domain MUST exactly match From: header domain
company.com β mail.company.com (FAILS)
RELAXED alignment (adkim=r, aspf=r) [DEFAULT]:
DKIM/SPF domain just needs to be same organizational domain
mail.company.com aligns with company.com (PASSES)
IMPORTANT for transactional email services:
If Mailchimp sends FROM noreply@company.com:
SPF: passes for Mailchimp IPs (included in your SPF)
SPF alignment: Return-Path is @mc.com (NOT company.com) β FAILS alignment
DKIM: Mailchimp signs with their key for bounce.company.com
DKIM alignment: relaxed β bounce.company.com aligns with company.com β PASSES
DMARC: only needs ONE of SPF/DKIM to align β PASSES (via DKIM)
Always enable DKIM signing in your ESP to ensure at least DKIM alignment passes.
Parsing DMARC Aggregate Reports
import zipfile
import xml.etree.ElementTree as ET
from email import message_from_bytes
import imaplib
def parse_dmarc_report(xml_content: str) -> dict:
"""Parse DMARC aggregate XML report"""
root = ET.fromstring(xml_content)
report = {
'org_name': root.findtext('./report_metadata/org_name'),
'date_range': {
'begin': root.findtext('./report_metadata/date_range/begin'),
'end': root.findtext('./report_metadata/date_range/end'),
},
'domain': root.findtext('./policy_published/domain'),
'policy': root.findtext('./policy_published/p'),
'records': []
}
for record in root.findall('./record'):
source_ip = record.findtext('./row/source_ip')
count = int(record.findtext('./row/count', 0))
dkim_result = record.findtext('./row/policy_evaluated/dkim')
spf_result = record.findtext('./row/policy_evaluated/spf')
# Check if this is your legitimate sending IP
is_passing = dkim_result == 'pass' or spf_result == 'pass'
report['records'].append({
'source_ip': source_ip,
'count': count,
'dkim': dkim_result,
'spf': spf_result,
'passing': is_passing,
})
# Find failing sources (potential spoofing)
failing = [r for r in report['records'] if not r['passing']]
if failing:
print(f"WARNING: {len(failing)} failing sources found!")
for r in failing:
print(f" IP: {r['source_ip']}, Count: {r['count']}, DKIM: {r['dkim']}, SPF: {r['spf']}")
return report
BIMI: Brand Indicators for Message Identification
BIMI displays your company logo in Gmail, Apple Mail, and Yahoo Mail inboxes next to your email β but only if you have DMARC at p=quarantine or p=reject. For Gmail, you also need a Verified Mark Certificate (VMC).
BIMI Requirements Checklist
- DMARC policy:
p=quarantineorp=reject(notp=none) - SVG logo: Tiny PS (SVG Basic 1.1 subset), square aspect ratio, solid background
- Logo hosted at HTTPS URL (accessible publicly)
- VMC (Verified Mark Certificate) from DigiCert or Entrust β required for Gmail (~$1,500/year)
- Trademark registration for your logo (required for VMC)
# BIMI DNS record at default._bimi.company.com
v=BIMI1; l=https://cdn.company.com/bimi-logo.svg; a=https://cdn.company.com/company-vmc.pem
# Without VMC (Yahoo/Apple Mail, not Gmail):
v=BIMI1; l=https://cdn.company.com/bimi-logo.svg
# SVG requirements for BIMI
# Must be SVG Tiny PS profile:
# - viewBox attribute required
# - No JavaScript
# - No external resources
# - No animations
# - Dimensions: 1:1 aspect ratio recommended
# Convert regular SVG to BIMI-compliant
# Install: npm install -g svgo
svgo --config svgo.config.js company-logo.svg -o company-bimi.svg
# Validate BIMI SVG
curl -X POST https://bimigroup.org/bimi-svg-validator/ -F "svg=@company-bimi.svg"
Subdomain Strategy for Email
Never send bulk email from your root domain (company.com). Use subdomains to protect your main domain's reputation.
Domain strategy:
company.com β Corporate email (employees, no bulk)
mail.company.com β Transactional email (receipts, notifications)
news.company.com β Marketing/newsletters
bounce.company.com β Bounce handling (set as Return-Path)
DNS records per subdomain:
mail.company.com:
MX β mail.company.com.
TXT β v=spf1 include:amazonses.com ~all
TXT β v=DMARC1; p=reject; rua=mailto:dmarc@company.com
TXT (at sendgrid._domainkey.mail.company.com) β DKIM public key
news.company.com:
TXT β v=spf1 include:servers.mcsv.net ~all (Mailchimp)
TXT β v=DMARC1; p=quarantine; rua=mailto:dmarc@company.com
If company.com has no bulk sending:
_dmarc.company.com β v=DMARC1; p=reject; sp=reject
# sp=reject: subdomains inherit reject policy
Troubleshooting Deliverability Issues
Step-by-Step Diagnosis
# 1. Check authentication headers on received email
# Send test email to: check-auth-XXXXXXXX@verifier.port25.com
# Receive full auth report back
# 2. Test SPF
dig +short TXT company.com | grep spf
nslookup -type=TXT company.com
# Validate at: mxtoolbox.com/spf
# 3. Test DKIM
dig +short TXT mail._domainkey.company.com
# Validate: mxtoolbox.com/dkim (enter domain + selector)
# 4. Test DMARC
dig +short TXT _dmarc.company.com
# Full report: mxtoolbox.com/dmarc
# 5. Check sending IP reputation
# Check IP at: mxtoolbox.com/blacklists
curl "https://www.mxtoolbox.com/api/v1/Lookup/blacklist/?argument=203.0.113.5"
# Check Google Postmaster Tools
# https://postmaster.google.com - see domain reputation, spam rate
# 6. Investigate specific Gmail rejection
# Look for SMTP error codes:
# 550-5.7.26 β DMARC failure
# 421-4.7.28 β Newly created IP, warm up needed
# 550-5.7.1 β IP in Spamhaus PBL/SBL/XBL
IP Warmup Schedule
New dedicated IP warmup (never sent before):
Day 1: 200 emails
Day 2: 400 emails
Day 3: 800 emails
Day 4: 1,600 emails
Day 5: 3,200 emails
Day 6: 6,500 emails
Day 7: 13,000 emails
Day 8: 26,000 emails
Week 3: 100,000 emails/day
Week 4: Full volume
Rules:
- Only send to engaged subscribers (opened in last 90 days)
- Monitor bounce rate daily (>5% soft bounce: pause, >2% hard bounce: stop)
- Monitor spam complaints (>0.3%: stop and diagnose)
- Spread sending throughout the day (not all at 9am)
Gmail and Yahoo Requirements (2024+ Enforcement)
For senders sending >5,000 emails/day to Gmail:
REQUIRED:
β Valid forward and reverse DNS for sending IPs
β Valid TLS for transmitting email
β SPF OR DKIM authentication (both recommended)
β DMARC policy at p=none minimum (p=reject recommended)
β From: header matches DMARC-aligned domain
β One-click unsubscribe (RFC 8058: List-Unsubscribe-Post header)
β Process unsubscribe requests within 2 days
β Spam rate below 0.10% (above 0.30% = delivery problems)
DMARC enforcement timeline:
2024-02: p=none required
2024-06: Began applying p=quarantine for non-compliant senders
2025-01: p=reject enforcement for bulk senders
List-Unsubscribe header implementation:
List-Unsubscribe: <mailto:unsub@company.com?subject=unsub>, <https://company.com/unsub?id=USER_ID>
List-Unsubscribe-Post: List-Unsubscribe=One-Click
Monitoring Dashboard Setup
# Weekly deliverability health check script
import smtplib
import dns.resolver
import requests
def check_deliverability_health(domain: str) -> dict:
issues = []
# Check SPF
try:
spf = dns.resolver.resolve(domain, 'TXT')
spf_found = any('v=spf1' in str(r) for r in spf)
if not spf_found:
issues.append(f"CRITICAL: No SPF record for {domain}")
except Exception as e:
issues.append(f"CRITICAL: SPF lookup failed: {e}")
# Check DMARC
try:
dmarc = dns.resolver.resolve(f"_dmarc.{domain}", 'TXT')
dmarc_str = str(list(dmarc)[0])
if 'p=none' in dmarc_str:
issues.append(f"WARNING: DMARC p=none (not enforcing)")
elif 'p=reject' in dmarc_str:
print(f"β DMARC: p=reject (excellent)")
except Exception as e:
issues.append(f"CRITICAL: No DMARC record: {e}")
# Check Spamhaus (simplified)
# In production, use DNSBL query
print(f"Deliverability check for {domain}: {len(issues)} issues found")
for issue in issues:
print(f" {issue}")
return {"domain": domain, "issues": issues}
check_deliverability_health("company.com")
Conclusion
Email authentication is no longer optional β it's table stakes for inbox delivery. The good news: SPF, DKIM, and DMARC have been around since 2012, the tooling is mature, and a disciplined implementation takes a few hours, not weeks.
The path forward: implement SPF and DKIM immediately, deploy DMARC at p=none with reporting, spend 2-4 weeks reviewing your aggregate reports to discover all your sending sources, then incrementally ramp to p=quarantine and finally p=reject. Once you're at p=reject, consider BIMI for brand recognition in email clients. Your delivery rates, sender reputation, and protection against domain spoofing will all improve significantly.
Emily Watson
Technical Writer and Developer Advocate who simplifies complex technology for everyday readers.