All scripts
Microsoft 365 719

SPF, DKIM, and DMARC Record Checker

Checks SPF, DKIM, and DMARC DNS records for one or more domains and flags common mistakes, like a missing rua tag or a DMARC policy still stuck on p=none. Handy for a quick health check when onboarding a new domain into Microsoft 365 or doing a periodic deliverability review.

Test-EmailAuthenticationRecords.ps1
<#
.SYNOPSIS
    Checks SPF, DKIM, and DMARC DNS records for one or more domains.

.DESCRIPTION
    Queries public DNS for a list of domains and reports whether SPF, DKIM,
    and DMARC records exist, and flags a few common mistakes: more than one
    SPF record, an SPF record missing spf.protection.outlook.com, a DMARC
    policy still set to p=none, and a DMARC record with no rua reporting
    address. Useful for a quick health check before or after onboarding a
    new domain into Microsoft 365, or during a periodic deliverability
    review. DKIM selectors default to the two Microsoft 365 uses
    (selector1, selector2) but you can pass your own.

.PARAMETER Domain
    One or more domains to check, for example contoso.com.

.PARAMETER DkimSelector
    DKIM selectors to check. Defaults to selector1 and selector2, the
    Microsoft 365 defaults.

.EXAMPLE
    .\Test-EmailAuthenticationRecords.ps1 -Domain contoso.com

.EXAMPLE
    .\Test-EmailAuthenticationRecords.ps1 -Domain contoso.com, fabrikam.com -DkimSelector selector1, selector2, google

.AUTHOR
    Shehryar Hassan
#>

[CmdletBinding()]
param(
    [Parameter(Mandatory = $true)]
    [string[]]$Domain,

    [string[]]$DkimSelector = @('selector1', 'selector2')
)

function Get-SpfResult {
    param([string]$Domain)

    $result = [ordered]@{
        Found       = $false
        RecordCount = 0
        Record      = $null
        Issues      = New-Object System.Collections.Generic.List[string]
    }

    try {
        $txtRecords = Resolve-DnsName -Name $Domain -Type TXT -ErrorAction Stop |
            Where-Object { $_.Strings -join '' -match '^v=spf1' }
    }
    catch {
        $result.Issues.Add("DNS lookup failed: $($_.Exception.Message)")
        return [pscustomobject]$result
    }

    $result.RecordCount = ($txtRecords | Measure-Object).Count

    if ($result.RecordCount -eq 0) {
        $result.Issues.Add('No SPF record found')
        return [pscustomobject]$result
    }

    $result.Found = $true
    $result.Record = ($txtRecords[0].Strings -join '')

    if ($result.RecordCount -gt 1) {
        $result.Issues.Add('More than one SPF record published, only one is allowed')
    }

    if ($result.Record -notmatch 'spf\.protection\.outlook\.com') {
        $result.Issues.Add('SPF record does not include spf.protection.outlook.com')
    }

    if ($result.Record -notmatch '-all|~all') {
        $result.Issues.Add('SPF record has no -all or ~all qualifier at the end')
    }

    return [pscustomobject]$result
}

function Get-DkimResult {
    param([string]$Domain, [string[]]$Selectors)

    $found = New-Object System.Collections.Generic.List[string]

    foreach ($selector in $Selectors) {
        $name = "$selector._domainkey.$Domain"
        try {
            $record = Resolve-DnsName -Name $name -Type CNAME -ErrorAction Stop
            if ($record) {
                $found.Add($selector)
            }
        }
        catch {
            continue
        }
    }

    [pscustomobject]@{
        Found             = ($found.Count -gt 0)
        SelectorsFound    = $found -join ', '
        SelectorsChecked  = $Selectors -join ', '
    }
}

function Get-DmarcResult {
    param([string]$Domain)

    $result = [ordered]@{
        Found  = $false
        Record = $null
        Issues = New-Object System.Collections.Generic.List[string]
    }

    $name = "_dmarc.$Domain"
    try {
        $txtRecords = Resolve-DnsName -Name $name -Type TXT -ErrorAction Stop |
            Where-Object { $_.Strings -join '' -match '^v=DMARC1' }
    }
    catch {
        $result.Issues.Add('No DMARC record found')
        return [pscustomobject]$result
    }

    if (-not $txtRecords) {
        $result.Issues.Add('No DMARC record found')
        return [pscustomobject]$result
    }

    $result.Found = $true
    $result.Record = ($txtRecords[0].Strings -join '')

    if ($result.Record -match 'p=none') {
        $result.Issues.Add('Policy is still p=none, mail is monitored but not enforced')
    }

    if ($result.Record -notmatch 'rua=') {
        $result.Issues.Add('No rua tag, aggregate reports are not being sent anywhere')
    }

    return [pscustomobject]$result
}

$report = foreach ($d in $Domain) {
    Write-Host "Checking $d ..." -ForegroundColor Cyan

    $spf = Get-SpfResult -Domain $d
    $dkim = Get-DkimResult -Domain $d -Selectors $DkimSelector
    $dmarc = Get-DmarcResult -Domain $d

    [pscustomobject]@{
        Domain      = $d
        SpfFound    = $spf.Found
        SpfIssues   = ($spf.Issues -join '; ')
        DkimFound   = $dkim.Found
        DkimSelectors = $dkim.SelectorsFound
        DmarcFound  = $dmarc.Found
        DmarcIssues = ($dmarc.Issues -join '; ')
    }
}

$report | Format-Table -AutoSize

Read it before you run it, and test in a safe tenant first.