Get-MailRecords.psm1
|
<#
.SYNOPSIS Performs DNS lookups (A, MX, NS, SPF, DMARC, DKIM) for a domain, email, or URL. .DESCRIPTION Checks for common mail-related DNS records on a given domain, email address, or URL. Supports record types TXT, CNAME, or BOTH for SPF, DMARC, and DKIM. If a DKIM selector is not provided, common selectors are tried automatically. Function alias: GMR. Parameter aliases: -d (Domain), -s (Sub), -js (JustSub), -sel (Selector), -r (RecordType), -srv (Server), -e (Export). .PARAMETER Domain The full domain name, email address, or URL to query. Mandatory. Alias: -d .PARAMETER Sub Query both the subdomain and the base domain. For example, mail.facebook.com will return results for mail.facebook.com AND facebook.com. Alias: -s .PARAMETER JustSub Query only the subdomain — skips the base domain lookup. For example, mail.facebook.com returns results for mail.facebook.com only. Alias: -js .PARAMETER Selector The DKIM selector to use. If not provided, common selectors are tried automatically. Alias: -sel .PARAMETER RecordType Record type(s) to query for SPF, DMARC, and DKIM. Valid options: 'TXT', 'CNAME', 'BOTH'. Default: 'TXT'. Alias: -r .PARAMETER Server DNS server to query. Default: 8.8.8.8. Alias: -srv .PARAMETER Export Export results to file. Provide a filename (e.g., 'results.csv', 'output.json') or just the format ('CSV', 'JSON') for auto-generated timestamped filename. Alias: -e .EXAMPLE # Get basic mail records for facebook.com Get-MailRecords -Domain facebook.com GMR -Domain facebook.com GMR -d facebook.com .EXAMPLE # Query both the subdomain and the base domain Get-MailRecords -Domain mail.facebook.com -Sub GMR -d mail.facebook.com -s .EXAMPLE # Query only the subdomain, skip the base domain Get-MailRecords -Domain mail.facebook.com -JustSub GMR -d mail.facebook.com -js .EXAMPLE # Get DKIM record with explicit selector Get-MailRecords -Domain cnn.facebook.com -Selector face GMR -d cnn.facebook.com -sel face .EXAMPLE # Use a custom DNS server Get-MailRecords -Domain cnn.com -Server 1.1.1.1 GMR -d cnn.com -srv 1.1.1.1 .EXAMPLE # Get CNAME records for SPF/DMARC/DKIM Get-MailRecords -Domain cnn.com -RecordType CNAME GMR -d cnn.com -r CNAME .EXAMPLE # Get only subdomain records (exclude parent domain) Get-MailRecords -Domain mail.facebook.com -JustSub GMR -d mail.facebook.com -js .EXAMPLE # Export results to a specific CSV file Get-MailRecords -Domain example.com -Export results.csv GMR -d example.com -e results.csv .EXAMPLE # Export with auto-generated timestamped filename Get-MailRecords -Domain example.com -Export CSV GMR -d example.com -e CSV .EXAMPLE # Check multiple domains via pipeline and export to JSON "google.com", "microsoft.com", "amazon.com" | Get-MailRecords -Export output.json .EXAMPLE # Bulk check from CSV file and export results Import-Csv domains.csv | Get-MailRecords -Export results.csv .EXAMPLE # Prompt for domain interactively GMR .LINK https://github.com/dcazman/Get-MailRecords .NOTES Author: Dan Casmas (07/2023) Tested on Windows PowerShell 5.1 and PowerShell 7 (Windows, Linux, macOS). Minimum required version: 5.1. Requires Resolve-DnsName (Windows built-in) or dig (Linux/macOS: install bind-utils or dnsutils). Function alias: GMR. Parameter aliases: -d (Domain), -s (Sub), -js (JustSub), -sel (Selector), -r (RecordType), -srv (Server), -e (Export). To add more DKIM selectors, edit $DkimSelectors near the top of the script. Only the first two NS results are returned. CNAME record types will follow the CNAME chain to retrieve the final TXT record value. Note: Multi-part TLDs (e.g., .co.uk, .com.au) are handled for common cases, but use -Sub for complex domains. Portions of code adapted from Jordan W. #> function Get-MailRecords { [Alias("GMR")] [CmdletBinding()] param ( [parameter(Mandatory = $true, HelpMessage = "Enter the full domain name, email address, or URL.", Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [ValidateScript({ if ($_ -like "*.*") { return $true } else { throw [System.Management.Automation.ValidationMetadataException] "Enter the full domain name, email address, or URL." } })] [alias ('d')] [string]$Domain, [parameter(Mandatory = $false, HelpMessage = "Query both the subdomain and the base domain. Example: mail.facebook.com returns results for mail.facebook.com AND facebook.com.")] [alias ('s')] [switch]$Sub, [parameter(Mandatory = $false, HelpMessage = "DKIM selector. DKIM won't be checked without this string.")] [ValidateNotNullOrEmpty()] [alias ('sel')] [string]$Selector = 'unprovided', [parameter(Mandatory = $false, HelpMessage = "Looks for record type TXT or CNAME or BOTH for SPF, DMARC, and DKIM if -Selector is used. The default record type is TXT.")] [ValidateSet('TXT', 'CNAME', 'BOTH')] [ValidateNotNullOrEmpty()] [alias ('r')] [string]$RecordType = 'TXT', [parameter(Mandatory = $false, HelpMessage = "Server to query. The default is 8.8.8.8")] [ValidateNotNullOrEmpty()] [alias ('srv')] [string]$Server = '8.8.8.8', [parameter(Mandatory = $false, HelpMessage = "Query only the subdomain, skip the base domain. Example: mail.facebook.com returns results for mail.facebook.com only.")] [alias ('js')] [switch]$JustSub, [parameter(Mandatory = $false, HelpMessage = "Export results to file. Provide a filename (e.g., 'results.csv', 'output.json') or just the format ('CSV', 'JSON') for auto-generated timestamped filename.")] [alias ('e')] [string]$Export ) begin { # Determine DNS resolution method first, before any early returns. # Resolve-DnsName is Windows built-in; dig is used as a fallback on Linux/macOS. if (Get-Command -Name Resolve-DnsName -ErrorAction SilentlyContinue) { $script:DnsMethod = 'ResolveDnsName' } elseif (Get-Command -Name dig -ErrorAction SilentlyContinue) { $script:DnsMethod = 'dig' } else { $script:DnsMethod = 'none' Write-Error "Neither Resolve-DnsName nor dig is available. On Linux/macOS, install bind-utils (RHEL/CentOS) or dnsutils (Debian/Ubuntu) to get dig." } # Initialize collection for export functionality $ExportFormat = $null $OutputPath = $null if ($Export) { $script:AllResults = @() # Determine if Export is a filename or just a format (case-insensitive) if ($Export -match '\.(csv|json)$') { # It's a filename with extension $OutputPath = $Export $ExportFormat = ($Export -split '\.')[-1].ToUpper() } elseif ($Export -match '^(csv|json)$') { # It's just a format, generate timestamped filename $ExportFormat = $Export.ToUpper() $timestamp = Get-Date -Format "yyyyMMdd_HHmm" $extension = $ExportFormat.ToLower() $OutputPath = Join-Path (Get-Location).Path "MailRecords_$timestamp.$extension" } else { Write-Error "Export parameter must be either a filename with .csv or .json extension, or 'CSV'/'JSON' for auto-generated filename." return } } } process { # Abort early if no DNS tool is available if ($script:DnsMethod -eq 'none') { return $null } # Initialize DKIM selectors $DkimSelectors = @( "default", "s", "s1", "s2", "selector1", "selector2", "pps1", "google", "everlytickey1", "everlytickey2", "eversrv", "k1", "mxvault", "dkim", "mail", "s1024", "s2048", "s4096" ) # Cross-platform DNS query wrapper. # Uses Resolve-DnsName on Windows; falls back to dig on Linux/macOS. # Returns objects with consistent key properties: Type, TTL, Strings, NameHost, NameExchange, Preference, IPAddress. function Invoke-DnsQuery { param( [Parameter(Mandatory = $true)] [string]$Name, [Parameter(Mandatory = $true)] [string]$Type, [Parameter(Mandatory = $true)] [string]$Server ) if ($script:DnsMethod -eq 'ResolveDnsName') { return Resolve-DnsName -Name $Name -Type $Type -Server $Server -DnsOnly -ErrorAction SilentlyContinue } # dig fallback for Linux/macOS $digArgs = "@$Server", "+noall", "+answer", "-t", $Type.ToUpper(), $Name $digOutput = & dig @digArgs 2>$null if (-not $digOutput) { return $null } $results = [System.Collections.Generic.List[object]]::new() foreach ($line in $digOutput) { if ([string]::IsNullOrWhiteSpace($line) -or $line -match '^\s*;') { continue } # Parse dig answer line: NAME TTL CLASS TYPE DATA if ($line -match '^(\S+)\s+(\d+)\s+IN\s+(\S+)\s+(.+)$') { $recordName = $Matches[1].TrimEnd('.') $ttl = [int]$Matches[2] $recordType = $Matches[3].ToUpper() $data = $Matches[4].Trim() $obj = [PSCustomObject]@{ Name = $recordName Type = $recordType TTL = $ttl } switch ($recordType) { 'A' { $obj | Add-Member -NotePropertyName 'IPAddress' -NotePropertyValue $data } 'MX' { if ($data -match '^(\d+)\s+(\S+)$') { $obj | Add-Member -NotePropertyName 'Preference' -NotePropertyValue ([int]$Matches[1]) $obj | Add-Member -NotePropertyName 'NameExchange' -NotePropertyValue $Matches[2].TrimEnd('.') } } 'NS' { $obj | Add-Member -NotePropertyName 'NameHost' -NotePropertyValue $data.TrimEnd('.') } 'CNAME' { $obj | Add-Member -NotePropertyName 'NameHost' -NotePropertyValue $data.TrimEnd('.') } 'TXT' { # Extract each quoted string segment (handles multi-part TXT records like long DKIM keys) $parts = [regex]::Matches($data, '"([^"]*)"') | ForEach-Object { $_.Groups[1].Value } if (-not $parts) { $parts = @($data) } $obj | Add-Member -NotePropertyName 'Strings' -NotePropertyValue @($parts) } } $results.Add($obj) } } return $results.ToArray() } # NameServer lookup function Get-NS { param ( [Parameter(Mandatory = $true)] [string]$Domain, [Parameter(Mandatory = $true)] [string]$Server ) $NS = Invoke-DnsQuery -Name $Domain -Type 'NS' -Server $Server if ([string]::IsNullOrWhiteSpace($NS.NameHost)) { return $false } $OutNS = foreach ($Item in $NS) { $Item | Select-Object NameHost, TTL } [string]$resultsNS = ($OutNS | Select-Object -First 2 | Out-String).TrimEnd("`r`n").Trim() return $resultsNS } # SPF record lookup function Get-SPF { param ( [Parameter(Mandatory = $true)] [string]$Domain, [Parameter(Mandatory = $true)] [string]$Server, [Parameter(Mandatory = $true)] [string]$Type ) $SPF = Invoke-DnsQuery -Name $Domain -Type $Type -Server $Server if ($Type -eq 'TXT') { $spfRecord = $SPF.Strings | Where-Object { $_ -like "v=spf1*" } if ([string]::IsNullOrWhiteSpace($spfRecord)) { return $false } return $spfRecord } elseif ($Type -eq 'CNAME') { $cnameRecord = $SPF | Where-Object { $_.Type -eq 'CNAME' } if ($cnameRecord) { $targetDomain = $cnameRecord.NameHost $targetSPF = Invoke-DnsQuery -Name $targetDomain -Type 'TXT' -Server $Server $spfRecord = $targetSPF.Strings | Where-Object { $_ -like "v=spf1*" } if ($spfRecord) { return "CNAME -> $targetDomain : $spfRecord" } return "CNAME -> $targetDomain (no SPF found)" } return $false } } # Normalize selector to lowercase for case-insensitive matching if ($Selector -ne 'unprovided') { $Selector = $Selector.ToLowerInvariant() } # Validate and parse the input domain $TestDomain = try { ([System.Uri]$Domain).Host.TrimStart('www.') } catch { try { ([Net.Mail.MailAddress]$Domain).Host } catch { $Domain } } # Final cleanup and lowercase normalization if ($TestDomain) { try { $TestDomain = $TestDomain.Replace('@', '').Trim().ToLowerInvariant() } catch { Write-Error "Problem with $Domain as entered. Please read the command help." return $null } } else { Write-Error "Problem with $Domain as entered. Please read the command help." return $null } # Extract the base domain (handles most cases, but not multi-part TLDs like .co.uk) # For complex TLDs, use the -Sub parameter to preserve the full domain if (-not $Sub -and -not $JustSub) { $parts = $TestDomain.Split(".") # Basic handling: if more than 3 parts and second-to-last is short (2 chars), keep 3 parts # This catches common cases like example.co.uk, but not all scenarios if ($parts.Count -gt 2 -and $parts[-2].Length -eq 2 -and $parts[-1].Length -le 3) { $TestDomain = $parts[-3..-1] -join "." } else { $TestDomain = $parts[-2, -1] -join "." } } # Initialize DKIM result $resultdkim = $false # Normalize record type to uppercase $RecordTypeTest = @() if ($RecordType -eq 'BOTH') { $RecordTypeTest = @('TXT', 'CNAME') } else { $RecordTypeTest = $RecordType.ToUpper() } # Check if A record exists $resultA = $null -ne (Invoke-DnsQuery -Name $TestDomain -Type 'A' -Server $Server | Where-Object { $_.Type -eq 'A' }) try { # Query the DNS server for MX records $mxRecords = Invoke-DnsQuery -Name $TestDomain -Type 'MX' -Server $Server | Sort-Object -Property Preference } catch { # Handle errors during the query Write-Error "An error occurred while resolving DNS: $_" $mxRecords = $null } # Validate and format MX results outside the try/catch so that # Write-Warning is not silently swallowed when -WarningAction Stop is used. if ($mxRecords -and $mxRecords.Type -contains 'MX') { $formattedRecords = $mxRecords | Where-Object { -not [string]::IsNullOrWhiteSpace($_.NameExchange) } | Select-Object @{n = "Name"; e = { $_.NameExchange } }, @{n = "Preference"; e = { $_.Preference } }, @{n = "TTL"; e = { $_.TTL } } $resultmx = ($formattedRecords | Out-String).TrimEnd("`r`n").Trim() } else { Write-Warning "No MX records found for domain: $Domain" $resultmx = $false } # Hold the original selector value $SelectorHold = $Selector # Loop through specified record types $Output = $RecordTypeTest | ForEach-Object { $TempType = $_ # Get NS records $resultsNS = Get-NS -Domain $TestDomain -Server $Server # Get SPF record $resultspf = Get-SPF -Domain $TestDomain -Server $Server -Type $TempType # Get DMARC record $DMARC = Invoke-DnsQuery -Name "_dmarc.$TestDomain" -Type $TempType -Server $Server if (-not $DMARC) { $resultdmarc = $false } else { if ($TempType -eq 'TXT') { $resultdmarc = ($DMARC.Strings -like "v=DMARC1*") -join ' ' if ([string]::IsNullOrWhiteSpace($resultdmarc)) { $resultdmarc = $false } } elseif ($TempType -eq 'CNAME') { $cnameRecord = $DMARC | Where-Object { $_.Type -eq 'CNAME' } if ($cnameRecord) { $targetDomain = $cnameRecord.NameHost $targetDMARC = Invoke-DnsQuery -Name $targetDomain -Type 'TXT' -Server $Server $dmarcRecord = ($targetDMARC.Strings -like "v=DMARC1*") -join ' ' if ($dmarcRecord) { $resultdmarc = "CNAME -> $targetDomain : $dmarcRecord" } else { $resultdmarc = "CNAME -> $targetDomain (no DMARC found)" } } else { $resultdmarc = $false } } } # Start of DKIM checking if ($Selector -ne 'unprovided') { # Get DKIM record if it exists $DKIM = Invoke-DnsQuery -Name "$($Selector)._domainkey.$($TestDomain)" -Type $TempType -Server $Server | Where-Object { $_.Type -eq $TempType } if (-not $DKIM) { $resultdkim = $false } else { if ($TempType -eq 'TXT') { foreach ($Item in $DKIM) { if ($Item.Type -eq 'TXT' -and $Item.Strings -match "v=DKIM1") { $resultdkim = [string]$Item.Strings break } } } elseif ($TempType -eq 'CNAME') { $cnameRecord = $DKIM | Where-Object { $_.Type -eq 'CNAME' } if ($cnameRecord) { $targetDomain = $cnameRecord.NameHost $targetDKIM = Invoke-DnsQuery -Name $targetDomain -Type 'TXT' -Server $Server $dkimRecord = $targetDKIM | Where-Object { $_.Strings -match "v=DKIM1" } if ($dkimRecord) { $resultdkim = "CNAME -> $targetDomain : $([string]$dkimRecord.Strings)" } else { $resultdkim = "CNAME -> $targetDomain (no DKIM found)" } } else { $resultdkim = $false } } } } # Auto-discover DKIM selector if not provided if ($Selector -eq 'unprovided') { # Break the loop if DKIM is found $BreakFlag = $false foreach ($line in $DkimSelectors) { $DKIM = $null $DKIM = Invoke-DnsQuery -Name "$($line)._domainkey.$($TestDomain)" -Type $TempType -Server $Server | Where-Object { $_.Type -eq $TempType } if ($TempType -eq 'TXT') { $DKIM = $DKIM | Where-Object { $_.Strings -match "v=DKIM1" } foreach ($Item in $DKIM) { [string]$resultdkim = $Item.Strings $Selector = $line $BreakFlag = $true break } } elseif ($TempType -eq 'CNAME') { $cnameRecord = $DKIM | Where-Object { $_.Type -eq 'CNAME' } if ($cnameRecord) { $targetDomain = $cnameRecord.NameHost $targetDKIM = Invoke-DnsQuery -Name $targetDomain -Type 'TXT' -Server $Server $dkimRecord = $targetDKIM | Where-Object { $_.Strings -match "v=DKIM1" } if ($dkimRecord) { $resultdkim = "CNAME -> $targetDomain : $([string]$dkimRecord.Strings)" $Selector = $line $BreakFlag = $true } } } if ($BreakFlag) { break } } } # Reset selector if DKIM not found if ($resultdkim -eq $false) { $Selector = $SelectorHold } [PSCustomObject]@{ A = $resultA MX = $resultmx "SPF_$TempType" = $resultspf "DMARC_$TempType" = $resultdmarc "DKIM_$TempType" = $resultdkim SELECTOR = $Selector DOMAIN = $TestDomain RECORDTYPE = $TempType SERVER = $Server NS_First2 = $resultsNS } } if ($JustSub) { if ($Export) { $script:AllResults += $Output } else { return $Output } } else { if ($Export) { $script:AllResults += $Output } else { $Output } # If Sub is true, also query the base/parent domain if ($Sub -eq $true -and ($TestDomain.Split('.').count -gt 2)) { # Derive the parent domain explicitly from the already-parsed domain $tParts = $TestDomain.Split('.') $parentDomain = if ($tParts.Count -gt 2 -and $tParts[-2].Length -eq 2 -and $tParts[-1].Length -le 3) { $tParts[-3..-1] -join '.' } else { $tParts[-2, -1] -join '.' } # Skip if stripping produced the same domain (e.g. multi-part TLDs like .co.uk) if ($parentDomain -ne $TestDomain) { $subOutput = Get-MailRecords -Domain $parentDomain -Server $Server -RecordType $RecordType -Selector $SelectorHold if ($Export) { $script:AllResults += $subOutput } else { $subOutput } } } } } end { # Export results if requested if ($ExportFormat -and $script:AllResults.Count -gt 0) { try { switch ($ExportFormat) { 'CSV' { $script:AllResults | Export-Csv -Path $OutputPath -NoTypeInformation -Force Write-Host "Results exported to: $OutputPath" -ForegroundColor Green } 'JSON' { $script:AllResults | ConvertTo-Json -Depth 10 | Out-File -FilePath $OutputPath -Force Write-Host "Results exported to: $OutputPath" -ForegroundColor Green } } } catch { Write-Error "Failed to export results: $_" } } } } |