Files
linux-scripts/windows-exchange-smoke-tests.ps1
T
chiefgeek a1a17e81a1 Sync all scripts from website downloads — 352 scripts total
Includes updated JS challenge scripts with Claude-User whitelist,
same-site referer bypass, Blackbox-Exporter allowed bot, and all
new exporters, cheat sheets, and automation scripts.
2026-05-25 03:31:08 +02:00

680 lines
23 KiB
PowerShell

###############################################################################
#### windows-exchange-smoke-tests.ps1 — Verify Exchange Server health ####
#### Checks Exchange services, OWA, ECP, Autodiscover, ActiveSync, EWS, ####
#### MAPI/HTTP, mail flow, queues, DAG, databases, certs, and event logs. ####
#### ####
#### Author: Phil Connor ####
#### Contact: contact@mylinux.work ####
#### License: MIT ####
#### Version: 1.0 ####
#### ####
#### Usage: .\windows-exchange-smoke-tests.ps1 ####
#### .\windows-exchange-smoke-tests.ps1 -ExchangeServer mail01 ####
#### .\windows-exchange-smoke-tests.ps1 -OutputFormat tap ####
#### ####
#### See -Help for all options. ####
###############################################################################
[CmdletBinding()]
param(
[string]$ExchangeServer = "localhost",
[int]$CertExpiryDays = 30,
[int]$MaxQueueLength = 100,
[ValidateSet("text","tap")]
[string]$OutputFormat = "text",
[switch]$NoColor,
[switch]$Help
)
$ErrorActionPreference = "Continue"
# ============================================================================
# HELP
# ============================================================================
if ($Help) {
@"
Usage: .\windows-exchange-smoke-tests.ps1 [OPTIONS]
Smoke-test Exchange Server infrastructure. PowerShell 5.1+.
Designed for Exchange servers or machines with Exchange Management Shell.
Parameters:
-ExchangeServer HOST Target Exchange server hostname or IP (default: localhost)
-CertExpiryDays N Certificate expiry warning threshold in days (default: 30)
-MaxQueueLength N Max messages per queue before failure (default: 100)
-OutputFormat FORMAT Output: text (default), tap
-NoColor Disable coloured output
-Verbose Show debug output
-Help Show this help
Examples:
.\windows-exchange-smoke-tests.ps1
.\windows-exchange-smoke-tests.ps1 -ExchangeServer mail01.corp.local
.\windows-exchange-smoke-tests.ps1 -CertExpiryDays 60 -MaxQueueLength 50
.\windows-exchange-smoke-tests.ps1 -OutputFormat tap -NoColor
"@
exit 0
}
# ============================================================================
# STATE
# ============================================================================
$script:Pass = 0
$script:Fail = 0
$script:Skip = 0
$script:Total = 0
$script:Results = @()
$script:StartTime = Get-Date
$script:EMSLoaded = $false
# ============================================================================
# COLORS
# ============================================================================
function Write-Color {
param([string]$Text, [string]$Color = "White")
if ($NoColor) {
Write-Host $Text
} else {
Write-Host $Text -ForegroundColor $Color
}
}
function Write-Log { param([string]$Msg) Write-Color "[INFO] $Msg" "Cyan" }
function Write-Warn { param([string]$Msg) Write-Color "[WARN] $Msg" "Yellow" }
function Write-Err { param([string]$Msg) Write-Color "[ERROR] $Msg" "Red" }
# ============================================================================
# TEST RESULT RECORDING
# ============================================================================
function Record-Pass {
param([string]$Name, [string]$Detail = "")
$script:Pass++
$script:Total++
$script:Results += [PSCustomObject]@{ Status="PASS"; Name=$Name; Detail=$Detail }
if ($OutputFormat -eq "tap") {
Write-Host "ok $($script:Total) - $Name$(if($Detail){" ($Detail)"})"
} else {
$mark = if ($NoColor) { "[PASS]" } else { [char]0x2713 }
$msg = " $mark $Name"
if ($Detail) { $msg += " - $Detail" }
Write-Color $msg "Green"
}
}
function Record-Fail {
param([string]$Name, [string]$Detail = "")
$script:Fail++
$script:Total++
$script:Results += [PSCustomObject]@{ Status="FAIL"; Name=$Name; Detail=$Detail }
if ($OutputFormat -eq "tap") {
Write-Host "not ok $($script:Total) - $Name"
if ($Detail) { Write-Host " # $Detail" }
} else {
$mark = if ($NoColor) { "[FAIL]" } else { [char]0x2717 }
$msg = " $mark $Name"
if ($Detail) { $msg += " - $Detail" }
Write-Color $msg "Red"
}
}
function Record-Skip {
param([string]$Name, [string]$Reason = "")
$script:Skip++
$script:Total++
$script:Results += [PSCustomObject]@{ Status="SKIP"; Name=$Name; Detail=$Reason }
if ($OutputFormat -eq "tap") {
Write-Host "ok $($script:Total) - $Name # SKIP $Reason"
} else {
$mark = if ($NoColor) { "[SKIP]" } else { [char]0x2298 }
$msg = " $mark $Name"
if ($Reason) { $msg += " - $Reason" }
Write-Color $msg "Yellow"
}
}
# ============================================================================
# HELPERS
# ============================================================================
function Test-CommandExists {
param([string]$Command)
$null -ne (Get-Command $Command -ErrorAction SilentlyContinue)
}
function Write-Section {
param([string]$Name)
if ($OutputFormat -eq "text") {
Write-Host ""
Write-Color $Name "White"
}
}
function Test-HttpEndpoint {
param([string]$Url, [string]$Name)
try {
# Ignore certificate errors for self-signed Exchange certs
try {
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = { $true }
} catch {}
$response = Invoke-WebRequest -Uri $Url -UseBasicParsing -TimeoutSec 15 -ErrorAction Stop
Record-Pass $Name "$Url - HTTP $($response.StatusCode)"
} catch {
$ex = $_.Exception
if ($ex.Response) {
$statusCode = [int]$ex.Response.StatusCode
# 401/403 means the endpoint is alive but requires auth — that's fine
if ($statusCode -eq 401 -or $statusCode -eq 403) {
Record-Pass $Name "$Url - HTTP $statusCode (auth required)"
} else {
Record-Fail $Name "$Url - HTTP $statusCode"
}
} else {
Record-Fail $Name "$Url - $($ex.Message)"
}
} finally {
try {
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = $null
} catch {}
}
}
# ============================================================================
# EXCHANGE MANAGEMENT SHELL
# ============================================================================
function Initialize-ExchangeShell {
# Check if Exchange cmdlets are already available
if (Test-CommandExists "Get-ExchangeServer") {
$script:EMSLoaded = $true
Write-Verbose "Exchange Management Shell already loaded"
return
}
# Try loading the Exchange snap-in
try {
Add-PSSnapin Microsoft.Exchange.Management.PowerShell.SnapIn -ErrorAction Stop
$script:EMSLoaded = $true
Write-Verbose "Exchange Management Shell loaded via snap-in"
return
} catch {
Write-Verbose "Snap-in load failed: $($_.Exception.Message)"
}
# Try the Exchange module path
$exchangeShell = "$env:ExchangeInstallPath\bin\RemoteExchange.ps1"
if ($exchangeShell -and (Test-Path $exchangeShell -ErrorAction SilentlyContinue)) {
try {
. $exchangeShell
Connect-ExchangeServer -auto -ClientApplication:ManagementShell
$script:EMSLoaded = $true
Write-Verbose "Exchange Management Shell loaded via RemoteExchange.ps1"
return
} catch {
Write-Verbose "RemoteExchange.ps1 failed: $($_.Exception.Message)"
}
}
Write-Verbose "Exchange Management Shell not available — falling back to connectivity tests"
}
# ============================================================================
# TESTS
# ============================================================================
# -- 1. Exchange Core Services -----------------------------------------------
function Test-ExchangeServices {
Write-Section "Services"
$services = @(
"MSExchangeADTopology",
"MSExchangeIS",
"MSExchangeTransport",
"MSExchangeServiceHost",
"MSExchangeMailboxAssistants",
"MSExchangeDelivery",
"MSExchangeSubmission",
"MSExchangeRPC",
"MSExchangeFastSearch",
"MSExchangeRepl"
)
$running = 0
$stopped = 0
$missing = 0
$stoppedNames = @()
foreach ($svc in $services) {
try {
$service = Get-Service -Name $svc -ComputerName $ExchangeServer -ErrorAction Stop
if ($service.Status -eq "Running") {
$running++
} else {
$stopped++
$stoppedNames += "$svc ($($service.Status))"
}
} catch {
$missing++
Write-Verbose "Service $svc not found on $ExchangeServer"
}
}
if ($missing -eq $services.Count) {
Record-Skip "Exchange services running" "no Exchange services found on $ExchangeServer"
} elseif ($stopped -gt 0) {
Record-Fail "Exchange services running" "$stopped stopped: $($stoppedNames -join ', ')"
} else {
Record-Pass "Exchange services running" "$running of $($services.Count) services running"
}
}
# -- 2. Exchange Management Shell -------------------------------------------
function Test-ExchangeManagementShell {
if ($script:EMSLoaded) {
try {
$server = Get-ExchangeServer $ExchangeServer -ErrorAction Stop
Record-Pass "Exchange Management Shell" "loaded — $($server.Name) ($($server.ServerRole))"
} catch {
Record-Pass "Exchange Management Shell" "loaded"
}
} else {
Record-Skip "Exchange Management Shell" "snap-in not available"
}
}
# -- 3. OWA -----------------------------------------------------------------
function Test-OWA {
Write-Section "Client Access Endpoints"
$url = "https://$ExchangeServer/owa/"
Test-HttpEndpoint -Url $url -Name "OWA responding"
}
# -- 4. ECP -----------------------------------------------------------------
function Test-ECP {
$url = "https://$ExchangeServer/ecp/"
Test-HttpEndpoint -Url $url -Name "ECP responding"
}
# -- 5. Autodiscover ---------------------------------------------------------
function Test-Autodiscover {
$url = "https://$ExchangeServer/Autodiscover/Autodiscover.xml"
Test-HttpEndpoint -Url $url -Name "Autodiscover endpoint"
}
# -- 6. ActiveSync -----------------------------------------------------------
function Test-ActiveSync {
$url = "https://$ExchangeServer/Microsoft-Server-ActiveSync"
Test-HttpEndpoint -Url $url -Name "ActiveSync endpoint"
}
# -- 7. EWS ------------------------------------------------------------------
function Test-EWS {
$url = "https://$ExchangeServer/EWS/Exchange.asmx"
Test-HttpEndpoint -Url $url -Name "EWS endpoint"
}
# -- 8. MAPI/HTTP ------------------------------------------------------------
function Test-MAPIHTTP {
$url = "https://$ExchangeServer/mapi/"
Test-HttpEndpoint -Url $url -Name "MAPI/HTTP endpoint"
}
# -- 9. Mail Flow (Transport Queues) -----------------------------------------
function Test-MailFlow {
Write-Section "Mail Flow"
if (-not $script:EMSLoaded) {
Record-Skip "Mail flow" "Exchange Management Shell not loaded"
return
}
try {
$queues = Get-Queue -Server $ExchangeServer -ErrorAction Stop
if (-not $queues) {
Record-Pass "Mail flow" "no queues returned (transport idle)"
return
}
$deferred = $queues | Where-Object { $_.Status -eq "Retry" -or $_.DeliveryType -eq "Unreachable" }
if ($deferred) {
$count = ($deferred | Measure-Object).Count
Record-Fail "Mail flow" "$count queue(s) in retry or unreachable state"
} else {
$queueCount = ($queues | Measure-Object).Count
Record-Pass "Mail flow" "$queueCount queue(s), none in retry/unreachable"
}
} catch {
Record-Fail "Mail flow" "Get-Queue error - $($_.Exception.Message)"
}
}
# -- 10. Mail Queue Length ---------------------------------------------------
function Test-MailQueueLength {
if (-not $script:EMSLoaded) {
Record-Skip "Mail queue length" "Exchange Management Shell not loaded"
return
}
try {
$queues = Get-Queue -Server $ExchangeServer -ErrorAction Stop
if (-not $queues) {
Record-Pass "Mail queue length" "no queues (transport idle)"
return
}
$overThreshold = $queues | Where-Object { $_.MessageCount -gt $MaxQueueLength }
if ($overThreshold) {
$worst = ($overThreshold | Sort-Object MessageCount -Descending | Select-Object -First 1)
Record-Fail "Mail queue length" "$($worst.Identity) has $($worst.MessageCount) messages (threshold $MaxQueueLength)"
} else {
$totalMessages = ($queues | Measure-Object -Property MessageCount -Sum).Sum
Record-Pass "Mail queue length" "$totalMessages total messages, threshold $MaxQueueLength"
}
} catch {
Record-Fail "Mail queue length" "Get-Queue error - $($_.Exception.Message)"
}
}
# -- 11. DAG Health ----------------------------------------------------------
function Test-DAGHealth {
Write-Section "Database Availability Group"
if (-not $script:EMSLoaded) {
Record-Skip "DAG health" "Exchange Management Shell not loaded"
return
}
try {
$server = Get-ExchangeServer $ExchangeServer -ErrorAction Stop
$dagMembership = Get-DatabaseAvailabilityGroup -ErrorAction Stop | Where-Object {
$_.Servers -contains $server.Name -or $_.Servers -contains $server.Identity
}
if (-not $dagMembership) {
Record-Skip "DAG health" "not a DAG member"
return
}
$dagName = $dagMembership.Name
$memberCount = ($dagMembership.Servers | Measure-Object).Count
# Check replication health
try {
$replHealth = Test-ReplicationHealth -Server $ExchangeServer -ErrorAction Stop
$failures = $replHealth | Where-Object { $_.Result -ne "Passed" }
if ($failures) {
$failCount = ($failures | Measure-Object).Count
$failNames = ($failures | Select-Object -ExpandProperty Check) -join ", "
Record-Fail "DAG health" "$dagName - $failCount check(s) failed: $failNames"
} else {
Record-Pass "DAG health" "$dagName, $memberCount members, all checks passed"
}
} catch {
Record-Pass "DAG health" "$dagName, $memberCount members"
}
} catch {
Record-Fail "DAG health" "error - $($_.Exception.Message)"
}
}
# -- 12. Database Copy Status ------------------------------------------------
function Test-DatabaseCopyStatus {
if (-not $script:EMSLoaded) {
Record-Skip "Database copy status" "Exchange Management Shell not loaded"
return
}
try {
$copies = Get-MailboxDatabaseCopyStatus -Server $ExchangeServer -ErrorAction Stop
if (-not $copies) {
Record-Skip "Database copy status" "no database copies found"
return
}
$unhealthy = $copies | Where-Object {
$_.Status -notin @("Healthy", "Mounted")
}
if ($unhealthy) {
$count = ($unhealthy | Measure-Object).Count
$details = ($unhealthy | ForEach-Object { "$($_.DatabaseName): $($_.Status)" }) -join ", "
Record-Fail "Database copy status" "$count unhealthy: $details"
} else {
$copyCount = ($copies | Measure-Object).Count
Record-Pass "Database copy status" "$copyCount copies, all healthy/mounted"
}
} catch {
Record-Fail "Database copy status" "error - $($_.Exception.Message)"
}
}
# -- 13. Database Mount Status -----------------------------------------------
function Test-DatabaseMountStatus {
Write-Section "Databases"
if (-not $script:EMSLoaded) {
Record-Skip "Database mount status" "Exchange Management Shell not loaded"
return
}
try {
$databases = Get-MailboxDatabase -Server $ExchangeServer -Status -ErrorAction Stop
if (-not $databases) {
Record-Skip "Database mount status" "no databases found on $ExchangeServer"
return
}
$dismounted = $databases | Where-Object { -not $_.Mounted }
if ($dismounted) {
$count = ($dismounted | Measure-Object).Count
$names = ($dismounted | Select-Object -ExpandProperty Name) -join ", "
Record-Fail "Database mount status" "$count dismounted: $names"
} else {
$dbCount = ($databases | Measure-Object).Count
Record-Pass "Database mount status" "$dbCount database(s), all mounted"
}
} catch {
Record-Fail "Database mount status" "error - $($_.Exception.Message)"
}
}
# -- 14. Certificate Status --------------------------------------------------
function Test-CertificateStatus {
Write-Section "Certificates"
if (-not $script:EMSLoaded) {
Record-Skip "Certificate expiry" "Exchange Management Shell not loaded"
return
}
try {
$certs = Get-ExchangeCertificate -Server $ExchangeServer -ErrorAction Stop
if (-not $certs) {
Record-Skip "Certificate expiry" "no Exchange certificates found"
return
}
$now = Get-Date
$threshold = $now.AddDays($CertExpiryDays)
$expired = @()
$expiring = @()
foreach ($cert in $certs) {
if ($cert.NotAfter -lt $now) {
$expired += $cert
} elseif ($cert.NotAfter -lt $threshold) {
$expiring += $cert
}
}
if ($expired.Count -gt 0) {
$names = ($expired | ForEach-Object { "$($_.Subject) (expired $($_.NotAfter.ToString('yyyy-MM-dd')))" }) -join ", "
Record-Fail "Certificate expiry" "$($expired.Count) expired: $names"
} elseif ($expiring.Count -gt 0) {
$nearest = ($expiring | Sort-Object NotAfter | Select-Object -First 1)
$daysLeft = [math]::Floor(($nearest.NotAfter - $now).TotalDays)
Record-Fail "Certificate expiry" "$($expiring.Count) expiring within $CertExpiryDays days (nearest: $daysLeft days)"
} else {
$certCount = ($certs | Measure-Object).Count
$nearest = ($certs | Sort-Object NotAfter | Select-Object -First 1)
$daysLeft = [math]::Floor(($nearest.NotAfter - $now).TotalDays)
Record-Pass "Certificate expiry" "$certCount cert(s), nearest expires in $daysLeft days"
}
} catch {
Record-Fail "Certificate expiry" "error - $($_.Exception.Message)"
}
}
# -- 15. Outlook Anywhere ---------------------------------------------------
function Test-OutlookAnywhere {
Write-Section "Outlook Anywhere"
if (-not $script:EMSLoaded) {
Record-Skip "Outlook Anywhere" "Exchange Management Shell not loaded"
return
}
try {
$oa = Get-OutlookAnywhere -Server $ExchangeServer -ErrorAction Stop
if (-not $oa) {
Record-Skip "Outlook Anywhere" "not configured on $ExchangeServer"
return
}
$external = $oa.ExternalHostname
$authMethod = $oa.ExternalClientAuthenticationMethod
if ($external) {
Record-Pass "Outlook Anywhere" "external: $external ($authMethod)"
} else {
Record-Pass "Outlook Anywhere" "configured (no external hostname)"
}
} catch {
Record-Fail "Outlook Anywhere" "error - $($_.Exception.Message)"
}
}
# -- 16. Exchange Event Log --------------------------------------------------
function Test-EventLog {
Write-Section "Event Log"
try {
$since = (Get-Date).AddHours(-24)
$events = Get-WinEvent -FilterHashtable @{
LogName = "Application"
ProviderName = "MSExchange*"
Level = 2 # Error
StartTime = $since
} -ComputerName $ExchangeServer -MaxEvents 50 -ErrorAction Stop
if ($events) {
$count = ($events | Measure-Object).Count
$sources = ($events | Group-Object ProviderName | Sort-Object Count -Descending |
Select-Object -First 3 | ForEach-Object { "$($_.Name) ($($_.Count))" }) -join ", "
Record-Fail "Exchange event log" "$count error(s) in last 24h: $sources"
} else {
Record-Pass "Exchange event log" "no errors in last 24h"
}
} catch {
if ($_.Exception.Message -match "No events were found") {
Record-Pass "Exchange event log" "no errors in last 24h"
} else {
Record-Skip "Exchange event log" "cannot read event log - $($_.Exception.Message)"
}
}
}
# ============================================================================
# OUTPUT
# ============================================================================
function Write-Header {
if ($OutputFormat -eq "tap") {
Write-Host "TAP version 13"
} else {
Write-Host ""
Write-Color "Windows Exchange Smoke Tests" "White"
Write-Host "Server: $ExchangeServer"
Write-Host "EMS: $(if ($script:EMSLoaded) { 'loaded' } else { 'not available' })"
Write-Host "Time: $(Get-Date -Format 'yyyy-MM-ddTHH:mm:ssZ')"
}
}
function Write-Summary {
$duration = [math]::Floor(((Get-Date) - $script:StartTime).TotalSeconds)
if ($OutputFormat -eq "tap") {
Write-Host "1..$($script:Total)"
Write-Host "# pass $($script:Pass)"
Write-Host "# fail $($script:Fail)"
Write-Host "# skip $($script:Skip)"
} else {
Write-Host ""
$separator = [string]::new([char]0x2500, 40)
Write-Color $separator "White"
Write-Color "Summary $ExchangeServer" "White"
$summaryLine = " $($script:Pass) passed $($script:Fail) failed $($script:Skip) skipped (${duration}s)"
Write-Host $summaryLine
Write-Color $separator "White"
if ($script:Fail -eq 0) {
Write-Color "All tests passed." "Green"
} else {
Write-Color "$($script:Fail) test(s) failed." "Red"
}
}
}
# ============================================================================
# MAIN
# ============================================================================
# Load Exchange Management Shell if available
Initialize-ExchangeShell
Write-Header
# Run all tests
Test-ExchangeServices
Test-ExchangeManagementShell
Test-OWA
Test-ECP
Test-Autodiscover
Test-ActiveSync
Test-EWS
Test-MAPIHTTP
Test-MailFlow
Test-MailQueueLength
Test-DAGHealth
Test-DatabaseCopyStatus
Test-DatabaseMountStatus
Test-CertificateStatus
Test-OutlookAnywhere
Test-EventLog
Write-Summary
if ($script:Fail -eq 0) { exit 0 } else { exit 1 }