Overview

dnscheck is a PowerShell function that queries the common DNS records for a domain and shows them in grouped sections with colour-coded statuses. It covers web, zone, mail routing, email authentication (SPF, DMARC, DKIM) and the optional mail security records (MTA-STS, TLS-RPT, BIMI).

It accepts a domain or a full email address, queries a public resolver by default, and runs the DKIM CNAME and key lookups as separate steps. That way it can tell whether a DKIM failure is in your own zone or at the provider.

Requirements

RequirementDetail
OSWindows. Resolve-DnsName is part of the DnsClient module, which only exists on Windows.
PowerShellWindows PowerShell 5.1 or PowerShell 7+
NetworkOutbound UDP/TCP 53 to the resolver (default 1.1.1.1)
Admin rightsNot required

Full Function

function dnscheck {
<#
.SYNOPSIS
    Queries common DNS records for a domain and outputs a grouped, colour-coded report.

.DESCRIPTION
    Checks web (A, AAAA, www), zone (NS, SOA), mail routing (MX, Autodiscover),
    email authentication (SPF, DMARC, DKIM) and optional records (MTA-STS,
    TLS-RPT, BIMI). Accepts a domain or an email address. DKIM is checked in
    two steps (CNAME, then key at target) to show where a failure sits.

.PARAMETER Domain
    Domain name or email address. Anything up to and including the @ is removed.

.PARAMETER Server
    DNS resolver to query. Default 1.1.1.1.

.PARAMETER Selectors
    DKIM selectors to check. Default selector1, selector2 (Microsoft 365).

.EXAMPLE
    dnscheck yourdomain.com

.EXAMPLE
    dnscheck user@yourdomain.com -Server 8.8.8.8

.EXAMPLE
    dnscheck yourdomain.com -Selectors selector1,selector2,google

.NOTES
    Author: Andrew Jones
    Version: 2.0
    Date: 2026-11-06
    Requires: Windows (DnsClient module / Resolve-DnsName)
#>
    param(
        [Parameter(Mandatory)][string]$Domain,
        [string]$Server = '1.1.1.1',
        [string[]]$Selectors = @('selector1','selector2')
    )
    $d = $Domain -replace '.*@'

    function Q($n, $t) {
        Resolve-DnsName $n -Type $t -Server $Server -DnsOnly -ErrorAction SilentlyContinue |
            Where-Object { $_.Section -eq 'Answer' -and "$($_.Type)" -eq $t }
    }
    function Row($label, $status, $value) {
        $c = @{ OK='Green'; WARN='Yellow'; MISSING='Red'; NONE='DarkGray' }[$status]
        if (-not $c) { $c = 'Gray' }
        Write-Host ('  {0,-18}' -f $label) -NoNewline
        Write-Host ('{0,-9}' -f $status) -ForegroundColor $c -NoNewline
        Write-Host $value
    }
    function Show($label, $vals, $miss = 'MISSING', $note = 'not found') {
        $vals = @($vals | Where-Object { $_ })
        if (-not $vals) { Row $label $miss $note; return }
        Row $label 'OK' $vals[0]
        $vals | Select-Object -Skip 1 | ForEach-Object { Row '' '' $_ }
    }
    function Head($t) { Write-Host "`n$t" -ForegroundColor Cyan }

    Write-Host "`nDNS check: $d  (resolver $Server)"

    Head 'Web'
    Show 'A'    (Q $d A).IPAddress 'NONE' 'not set'
    Show 'AAAA' (Q $d AAAA).IPAddress 'NONE' 'not set'
    Show 'WWW'  (@((Q ('www.' + $d) CNAME) | ForEach-Object { "CNAME -> $($_.NameHost)" }) + @((Q ('www.' + $d) A).IPAddress)) 'NONE' 'not set'

    Head 'DNS zone'
    Show 'NS'  ((Q $d NS).NameHost | Sort-Object)
    Show 'SOA' ((Q $d SOA) | ForEach-Object { "$($_.PrimaryServer)  serial $($_.SerialNumber)" })

    Head 'Mail routing'
    Show 'MX'               ((Q $d MX) | Sort-Object Preference | ForEach-Object { "$($_.Preference)  $($_.NameExchange)" })
    Show 'Autodiscover'     ((Q "autodiscover.$d" CNAME) | ForEach-Object { "CNAME -> $($_.NameHost)" }) 'NONE' 'not set'
    Show 'Autodiscover SRV' ((Q "_autodiscover._tcp.$d" SRV) | ForEach-Object { "$($_.Port)  $($_.NameTarget)" }) 'NONE' 'not set'

    Head 'Email authentication'
    $txt   = @((Q $d TXT) | ForEach-Object { $_.Strings -join '' })
    $spf   = @($txt | Where-Object { $_ -match '^v=spf1' })
    $dmarc = @((Q "_dmarc.$d" TXT) | ForEach-Object { $_.Strings -join '' } | Where-Object { $_ -match '^v=DMARC1' })
    if ($spf.Count -gt 1)   { Row 'SPF' 'WARN' "$($spf.Count) records - only one allowed"; $spf | ForEach-Object { Row '' '' $_ } } else { Show 'SPF' $spf }
    if ($dmarc.Count -gt 1) { Row 'DMARC' 'WARN' "$($dmarc.Count) records - only one allowed"; $dmarc | ForEach-Object { Row '' '' $_ } } else { Show 'DMARC' $dmarc }

    foreach ($s in $Selectors) {
        $n   = "$s._domainkey.$d"
        $cn  = (Q $n CNAME).NameHost | Select-Object -First 1
        $key = (Q $(if ($cn) { $cn } else { $n }) TXT) | ForEach-Object { $_.Strings -join '' } |
               Where-Object { $_ -match 'p=' } | Select-Object -First 1
        $label = "DKIM $s"
        if ($key) {
            $p = [regex]::Match($key, 'p=([^;\s]*)').Groups[1].Value
            if (-not $p) { Row $label 'WARN' 'key revoked (empty p=)' }
            else {
                $bits = if ($p.Length -lt 300) { '~1024-bit' } elseif ($p.Length -lt 600) { '~2048-bit' } else { '~4096-bit' }
                Row $label 'OK' "key published ($bits)"
            }
        }
        elseif ($cn) { Row $label 'WARN' 'CNAME found but no key at target' }
        else         { Row $label 'MISSING' 'no CNAME or TXT record' }
        if ($cn) { Row '' '' "CNAME -> $cn" }
    }

    Head 'Optional'
    Show 'MTA-STS' ((Q "_mta-sts.$d" TXT)        | ForEach-Object { $_.Strings -join '' }) 'NONE' 'not set'
    Show 'TLS-RPT' ((Q "_smtp._tls.$d" TXT)      | ForEach-Object { $_.Strings -join '' }) 'NONE' 'not set'
    Show 'BIMI'    ((Q "default._bimi.$d" TXT)   | ForEach-Object { $_.Strings -join '' }) 'NONE' 'not set'

    Head 'Other TXT'
    Show 'TXT' @($txt | Where-Object { $_ -notmatch '^v=spf1' }) 'NONE' 'none'
    Write-Host ''
}

Parameters

ParameterTypeRequiredDefaultPurpose
-DomainStringYes (positional)NoneDomain or email address to check
-ServerStringNo1.1.1.1Resolver to query
-SelectorsString arrayNoselector1, selector2DKIM selectors to check

Records Checked

SectionCheckQuery NameType
WebAdomainA
WebAAAAdomainAAAA
WebWWWwww.domainCNAME + A
DNS zoneNSdomainNS
DNS zoneSOAdomainSOA
Mail routingMXdomainMX
Mail routingAutodiscoverautodiscover.domainCNAME
Mail routingAutodiscover SRV_autodiscover._tcp.domainSRV
Email authenticationSPFdomainTXT (v=spf1)
Email authenticationDMARC_dmarc.domainTXT (v=DMARC1)
Email authenticationDKIM<selector>._domainkey.domainCNAME, then TXT at target
OptionalMTA-STS_mta-sts.domainTXT (RFC 8461)
OptionalTLS-RPT_smtp._tls.domainTXT (RFC 8460)
OptionalBIMIdefault._bimi.domainTXT
Other TXTTXTdomainTXT, excluding SPF

Status Values

StatusColourMeaning
OKGreenRecord found
WARNYellowRecord found, but there’s a problem (duplicate SPF or DMARC, revoked DKIM key, CNAME with no key behind it)
MISSINGRedA required record wasn’t found (NS, SOA, MX, SPF, DMARC, DKIM)
NONEGreyAn optional record isn’t set. This is normal for many domains.

How It Works

Input Handling

$d = $Domain -replace '.*@'

Removes everything up to and including the @, so user@yourdomain.com becomes yourdomain.com. A bare domain passes through unchanged. Without this, Resolve-DnsName returns “DNS name contains an invalid character”.

Helper Functions

The helpers are defined inside dnscheck, so they only exist while it runs and don’t clutter the session:

HelperPurpose
QRuns Resolve-DnsName and returns only Answer-section records of the requested type
RowWrites one aligned, colour-coded line
ShowWrites a label and status with the first value, then any further values indented below it, or the “missing” status if there are none
HeadWrites a section header

The www query name is built as ('www.' + $d) and not as "www.$d". Both give the same result, but Markdown renderers such as Obsidian, chat clients and Hugo can auto-link www.$d into [www.$d](https://www.$d) when the code is copied from a rendered view. That would break the query. See Copying Code Safely.

Answer Section Filtering

Where-Object { $_.Section -eq 'Answer' -and "$($_.Type)" -eq $t }
FilterReason
Section -eq 'Answer'When a name has no record of the requested type, the server returns the zone SOA in the Authority section (a NODATA response, RFC 2308). Without this filter, the SOA would look like a result.
Type -eq $tA query that follows a CNAME returns the CNAME and the target record. This keeps only the type that was asked for.
-DnsOnlyStops Windows falling back to LLMNR or NetBIOS
-ServerBypasses local DNS cache

DKIM Two-Step Lookup

$cn  = (Q $n CNAME).NameHost
$key = (Q $(if ($cn) { $cn } else { $n }) TXT) ...
  1. Query the selector for a CNAME, which is the record in your own zone.
  2. Query the TXT key at the CNAME target (provider-hosted), or at the selector itself if there’s no CNAME (self-hosted or Google-style TXT DKIM).

A single TXT query can’t separate the two cases. When the CNAME exists but its target has no key, Resolve-DnsName errors and returns nothing, including the CNAME. The two-step lookup reports which part is missing.

The key size is estimated from the length of the p= base64 value. Treat it as approximate.

Duplicate SPF and DMARC Detection

More than one SPF record makes SPF return a permanent error (RFC 7208 section 4.5). More than one DMARC record causes DMARC processing to be skipped (RFC 7489 section 6.6.3). Both show as WARN, with every record listed.

Usage Examples

dnscheck yourdomain.com
dnscheck user@yourdomain.com
dnscheck yourdomain.com -Server 8.8.8.8
dnscheck yourdomain.com -Selectors selector1,selector2,google

To query the authoritative nameserver directly, which bypasses all caching:

dnscheck yourdomain.com -Server ns1.bdm.microsoftonline.com

Common DKIM selectors:

ProviderSelector(s)Record Type
Microsoft 365selector1, selector2CNAME
Google WorkspacegoogleTXT
Mailchimp / Mandrillk1, k2, k3CNAME
SendGrids1, s2CNAME

Selectors are set by each provider and can change. Confirm them against the provider’s dashboard, or against the s= value in a received message’s DKIM-Signature header.

Example Output

Output for a Microsoft 365 domain hosted on Azure DNS. The domain, tenant, IP addresses and verification token are anonymised:

DNS check: contoso.com  (resolver 1.1.1.1)

Web
  A                 NONE     not set
  AAAA              NONE     not set
  WWW               NONE     not set

DNS zone
  NS                OK       ns1.bdm.microsoftonline.com
                             ns2.bdm.microsoftonline.com
                             ns3.bdm.microsoftonline.com
                             ns4.bdm.microsoftonline.com
  SOA               OK       ns1.bdm.microsoftonline.com  serial 1

Mail routing
  MX                OK       0  contoso-com.mail.protection.outlook.com
  Autodiscover      OK       CNAME -> autodiscover.outlook.com
  Autodiscover SRV  NONE     not set

Email authentication
  SPF               OK       v=spf1 include:spf.protection.outlook.com ip4:203.0.113.10 ip4:203.0.113.20 ip6:2001:db8::10 -all
  DMARC             OK       v=DMARC1; p=quarantine; pct=100; rua=mailto:dmarc@contoso.com; aspf=r; adkim=r; fo=1
  DKIM selector1    OK       key published (~2048-bit)
                             CNAME -> selector1-contoso-com._domainkey.contosoltd.w-v1.dkim.mail.microsoft
  DKIM selector2    OK       key published (~2048-bit)
                             CNAME -> selector2-contoso-com._domainkey.contosoltd.w-v1.dkim.mail.microsoft

Optional
  MTA-STS           NONE     not set
  TLS-RPT           NONE     not set
  BIMI              NONE     not set

Other TXT
  TXT               OK       mscid=xxxxxxxxxxxxxxxx...

Reading the Output

A Record Shows NONE

A NONE not set means the domain has no A record at the root. This is normal when nothing is hosted on the bare domain, for example when the website is on a subdomain.

To confirm by hand:

Resolve-DnsName yourdomain.com -Type A
Raw ResultMeaning
A record in the Answer sectionAn A record exists
Only SOA, in the Authority sectionNODATA. The name exists but has no A record (RFC 2308).
“DNS name does not exist”NXDOMAIN. The name doesn’t exist at all.

dnscheck only checks the root and www. Check subdomains directly:

Resolve-DnsName blog.yourdomain.com -Type A -Server 1.1.1.1

DKIM CNAME Found but No Key at Target

DKIM selector2    WARN     CNAME found but no key at target
                           CNAME -> selector2-yourdomain-com._domainkey.tenant.w-v1.dkim.mail.microsoft

Your zone is correct, but the provider isn’t publishing the key at the target. With Microsoft 365, selector2 often has no key published until a key rotation is triggered. Mail isn’t affected while signing uses selector1.

Check the signing configuration:

Connect-ExchangeOnline
Get-DkimSigningConfig -Identity yourdomain.com | Format-List Status,Enabled,Selector1CNAME,Selector2CNAME,RotateOnDate

Trigger a rotation:

Rotate-DkimSigningConfig -Identity yourdomain.com

Or in the Defender portal:

security.microsoft.com > Email & collaboration > Policies & rules > Threat policies > Email authentication settings > DKIM > [domain] > Rotate DKIM keys

RotateOnDate shows when the switch is scheduled, and it follows the session’s regional date format (UK dd/MM/yyyy). Don’t rotate again before that date. Re-run dnscheck once it has passed.

If the key still doesn’t appear, users have reported (not verified) that disabling and re-enabling DKIM for the domain fixes it.

Mixed case in Microsoft’s CNAME values, such as ContosoLtd, doesn’t matter. DNS names aren’t case-sensitive (RFC 4343).

Other Findings

FindingMeaningAction
SPF WARN, more than one recordSPF returns a permanent errorMerge into a single v=spf1 record
DMARC WARN, more than one recordDMARC is not processedDelete the extra records
DKIM MISSINGNo CNAME or TXT at the selectorPublish the provider’s records, or check the selector name
DKIM WARN, key revokedThe p= value is empty (RFC 6376 section 3.6.1)Expected after a key has been retired, otherwise republish the key
NS not at the expected providerRecords are being edited in the wrong DNS panelMake the changes at the provider shown in NS

Installation

The function file goes in the IT-Scripts repo and is dot-sourced from the profile. This keeps it version-controlled and safe from profile overwrites. Put the repo in a synced folder such as OneDrive and the same file is used on every machine. Adjust the paths below to your own repo location.

Step 1: Save the Function File

New-Item -Path 'C:\Scripts\IT-Scripts\Utils\DNS-Tools' -ItemType Directory -Force
notepad 'C:\Scripts\IT-Scripts\Utils\DNS-Tools\dnscheck.ps1'

Paste the full function and save.

Step 2: Dot-Source from the Profile

notepad $PROFILE

Add this block after the Show-Help function and before the oh-my-posh and zoxide init block. Init commands should stay at the end of the profile.

# region Custom: dnscheck
$dnscheck = 'C:\Scripts\IT-Scripts\Utils\DNS-Tools\dnscheck.ps1'
if (Test-Path $dnscheck) { . $dnscheck } else { Write-Warning "dnscheck not found at $dnscheck" }
# endregion Custom: dnscheck

The leading . (dot-source) runs the file in the current scope, so the function stays available after the file has run. If you run the file without the dot, the function is discarded when the script ends.

Step 3: Add to Show-Help

Add this line inside the Show-Help here-string (between @" and "@), at the top of the System Shortcuts section:

  ${command}dnscheck <domain>${reset}  ${accent}→${reset} ${desc}DNS / mail auth check${reset}

The line must go inside the here-string. The ${command}, ${reset} and related variables are only defined inside Show-Help. Placed anywhere else in the profile, the line causes a parse error. The variables are $PSStyle colour codes, which need PowerShell 7.2 or later.

Step 4: Reload and Verify

. $PROFILE
Show-Help
Get-Command dnscheck | Select-Object Name, @{n='File';e={$_.ScriptBlock.File}}
dnscheck yourdomain.com

File should show the dnscheck.ps1 path.

Chris Titus Tech Profile Notes

IssueDetailFix
Update-Profile overwrites $PROFILEDownloads the upstream profile and saves it over the local file, removing all custom editsKeep custom functions in separate files and dot-source them. After an update, re-add only the dot-source lines.
oh-my-posh initialised twiceThe $shouldInitPosh block loads the configured theme, and the trailing oh-my-posh init ... tokyo.omp.json line loads it again with no Warp checkDelete the trailing line and set $poshTheme to Join-Path $env:POSH_THEMES_PATH 'tokyo.omp.json'
Profile load timeThe double init adds load timeCheck with Measure-Command { . $PROFILE }

Function Management

How Functions Persist

Defined ByLifetime
Pasted into the consoleCurrent session only. Gone when the window closes.
Script run normally (.\script.ps1)Discarded when the script ends
Script dot-sourced (. .\script.ps1)Current session
In $PROFILE, or dot-sourced from itEvery new session

PowerShell 7 (pwsh.exe) and Windows PowerShell 5.1 (powershell.exe) use separate profile files. Run $PROFILE in each to see its path. To have a function in both, dot-source the same .ps1 from both profiles.

List Functions

All functions in the session, including built-ins:

Get-ChildItem Function:

Only the functions you added, compared against a clean -NoProfile session, with module functions excluded:

$base = & (Get-Process -Id $PID).Path -NoProfile -Command '(Get-ChildItem Function:).Name'
Get-ChildItem Function: | Where-Object { $_.Name -notin $base -and -not $_.Source } |
    Select-Object Name, @{n='DefinedIn';e={ if ($_.ScriptBlock.File) { $_.ScriptBlock.File } else { 'console (pasted)' } }}
DefinedInMeaning
$PROFILE pathLoads in every session from the profile
Script pathDot-sourced from that file
console (pasted)Current session only

View a Function

${function:dnscheck}
Get-Command dnscheck | Select-Object Name, CommandType, @{n='File';e={$_.ScriptBlock.File}}
Get-Help dnscheck -Full

Get-Help reads the comment-based help block in the function.

Remove a Function

From the current session:

Remove-Item Function:\dnscheck -ErrorAction SilentlyContinue

Several at once:

Remove-Item Function:\dnscheck, Function:\mailcheck -ErrorAction SilentlyContinue

Check they’ve gone (this should return nothing):

Get-Command dnscheck, mailcheck -ErrorAction SilentlyContinue

To remove a function permanently, also take it out of the profile, or out of the file the profile dot-sources. Otherwise the next session loads it again:

Select-String -Path $PROFILE -Pattern 'function (dnscheck|mailcheck)|dnscheck\.ps1'
notepad $PROFILE

Profile Management

Create the profile if it doesn’t exist:

if (-not (Test-Path $PROFILE)) { New-Item -Path $PROFILE -ItemType File -Force }

Edit and reload:

notepad $PROFILE
. $PROFILE

Back up before major edits or before running Update-Profile:

Copy-Item $PROFILE "$PROFILE.$(Get-Date -Format yyyyMMdd-HHmm).bak"

Measure load time:

Measure-Command { . $PROFILE } | Select-Object TotalMilliseconds

Fix the execution policy if the profile won’t load (“running scripts is disabled”):

Set-ExecutionPolicy RemoteSigned -Scope CurrentUser

Verifying the Running Version

When the same function behaves or looks different on two machines, run this on each and compare. CodeHash fingerprints the code that’s actually loaded in the session:

$f = Get-Command dnscheck -ErrorAction SilentlyContinue
[pscustomobject]@{
    Computer   = $env:COMPUTERNAME
    PSVersion  = $PSVersionTable.PSVersion.ToString()
    Edition    = $PSVersionTable.PSEdition
    Host       = $Host.Name
    Terminal   = if ($env:WT_SESSION) {'Windows Terminal'} elseif ($env:TERM_PROGRAM) {$env:TERM_PROGRAM} else {'Console host / other'}
    Profile    = $PROFILE
    Loaded     = [bool]$f
    DefinedIn  = if ($f.ScriptBlock.File) { $f.ScriptBlock.File } else { 'console (pasted)' }
    Version    = ($f.ScriptBlock.ToString() -split "`n" | Select-String 'Version:' | Select-Object -First 1) -replace '.*Version:\s*'
    CodeHash   = if ($f) { (Get-FileHash -InputStream ([IO.MemoryStream]::new([Text.Encoding]::UTF8.GetBytes(($f.ScriptBlock.ToString() -replace "`r`n","`n").Trim())))).Hash.Substring(0,16) }
    Corrupted  = [bool]($f.ScriptBlock.ToString() -match '\]\(https?://')
} | Format-List
FieldMeaning
CodeHashThe same on both machines means the same code is running. Different means the code differs.
DefinedInShould be the dnscheck.ps1 path. console (pasted) means a session copy is overriding the file.
VersionBlank means the loaded copy has no comment-based help header
CorruptedTrue means auto-linked Markdown is present in the code
PSVersion / Edition5.1 (Desktop) and 7.x (Core) render differently
TerminalWhich terminal is drawing the output

If the hashes differ, reload from the file:

Remove-Item Function:\dnscheck -ErrorAction SilentlyContinue
. 'C:\Scripts\IT-Scripts\Utils\DNS-Tools\dnscheck.ps1'

Then compare the file itself on both machines:

Get-FileHash 'C:\Scripts\IT-Scripts\Utils\DNS-Tools\dnscheck.ps1' | Select-Object Hash

If the file hashes differ, OneDrive hasn’t finished syncing or there’s a local edit on one machine.

Copying Code Safely

Copying code from a rendered Markdown view (Obsidian reading view, a chat window, a published Hugo page) can change it:

ChangeExampleResult
Auto-linking"www.$d" becomes "[www.$d](https://www.$d)"The query name is invalid, so the lookup always fails
Smart quotes' becomes a curly quoteParse errors
Non-breaking spacesVisible spaces become U+00A0Unexpected token errors

To avoid this:

  • Copy from Obsidian source mode or the code block’s copy button
  • Copy from the .ps1 file in the repo, not from documentation
  • Scan a script for auto-linked text after pasting it:
Select-String -Path '.\dnscheck.ps1' -Pattern '\]\(https?://'

Nothing returned means the script is clean.

Display Differences Between Systems

If CodeHash matches but the output still looks different, the cause is the environment, not the code:

CauseEffectCheck
PowerShell edition$PSStyle only exists in 7.2 and later, so Show-Help has no colours on 5.1$PSVersionTable.PSVersion
TerminalWindows Terminal, Warp and conhost map colours such as DarkGray and Cyan to different shades$env:WT_SESSION, $env:TERM_PROGRAM
FontThe Show-Help icons need a Nerd Font, and show as boxes without oneTerminal font settings
ProfileThe two machines load different profile files, or one has no dot-source line$PROFILE, Select-String -Path $PROFILE -Pattern dnscheck
Colour schemeThe terminal theme overrides the console colour tableTerminal appearance settings

Quick Reference

TaskCommand
Fingerprint loaded codeSee Verifying the Running Version
Compare file across machinesGet-FileHash <path>
Scan for auto-link corruptionSelect-String -Path <file> -Pattern '\]\(https?://'
List all functionsGet-ChildItem Function:
View function code${function:Name}
Where a function is defined(Get-Command Name).ScriptBlock.File
Remove from sessionRemove-Item Function:\Name
Profile path$PROFILE
Edit profilenotepad $PROFILE
Reload profile. $PROFILE
Back up profileCopy-Item $PROFILE "$PROFILE.bak"
Load a function file. 'C:\path\file.ps1'

Minimal Version (mailcheck)

A single-line check of SPF, DMARC and the Microsoft 365 DKIM CNAMEs only:

function mailcheck($d){$d=$d -replace '.*@';(Resolve-DnsName $d -Type TXT).Strings -match 'v=spf1';(Resolve-DnsName "_dmarc.$d" -Type TXT).Strings;'selector1','selector2'|%{(Resolve-DnsName "$_._domainkey.$d" -Type CNAME).NameHost}}
mailcheck yourdomain.com

It outputs four lines in order: SPF, DMARC, selector1, selector2. It uses the local resolver.

Limitations

LimitationDetail
Windows onlyUse dig on macOS and Linux
On-screen output onlyWrite-Host output can’t be piped to Export-Csv or other cmdlets
No CAACAA isn’t in the Resolve-DnsName record type list
No DNSSEC validationRecords are shown but not validated
No PTRReverse DNS for mail hosts isn’t checked
Root and www onlyOther subdomains have to be queried separately
Selectors must be knownDKIM selectors can’t be listed from DNS
Key size approximateEstimated from the length of the p= value

Troubleshooting

SymptomCauseFix
“DNS name contains an invalid character”An email address was passed to a version without -replace '.*@'Use v2 or pass the domain only
Everything shows NONE or MISSINGThe resolver is unreachableTest-NetConnection 1.1.1.1 -Port 53, or use -Server with your local resolver
dnscheck not recognised in a new windowThe function was pasted, not added to the profileFollow Installation
“dnscheck not found at …” warning on startupThe dot-source path is wrong, or OneDrive hasn’t synced the fileCheck with Test-Path on the path
Parse error on profile loadA Show-Help line was placed outside the here-stringMove it between @" and "@
Results differ from the DNS panelPropagation or cache, or the panel isn’t the authoritative providerQuery with -Server set to an NS host, and check the NS records
DKIM WARN, CNAME but no keyThe provider isn’t publishing the key at the targetSee DKIM CNAME Found but No Key at Target
WWW always NONEThe www line was auto-linked when copied ([www.$d](https://...))Replace the line from the full function and see Copying Code Safely
Output looks different on another machineDifferent code loaded, or a different edition, terminal or fontSee Verifying the Running Version
Old version still runs after editing the fileA pasted copy in the session overrides the fileRemove-Item Function:\dnscheck, then dot-source the file again