<# .SYNOPSIS Compare health metrics across all Exchange mailbox databases .DESCRIPTION Generates a comparison report of database sizes, whitespace, mount status, backup age, and circular logging settings. Alerts on databases exceeding thresholds or with configuration issues. .PARAMETER WhitespaceThresholdGB Alert if available whitespace exceeds this value (default: 50) .PARAMETER BackupAgeThresholdDays Alert if last backup is older than this many days (default: 2) .PARAMETER OutputFolder Destination folder for reports. Default: .\DatabaseComparison- .NOTES ⚠️ AI-GENERATED SCRIPT - UNTESTED This script was generated by Claude AI and has not been tested in production. Review and test thoroughly in a non-production environment before use. - Run in Exchange Management Shell with appropriate RBAC permissions - Requires -Status parameter support (Get-MailboxDatabase -Status) - Tested compatibility: Exchange 2013/2016/2019 (not validated) .EXAMPLE .\Compare-MailboxDatabases.ps1 .EXAMPLE .\Compare-MailboxDatabases.ps1 -WhitespaceThresholdGB 100 -BackupAgeThresholdDays 1 #> [CmdletBinding()] param( [int]$WhitespaceThresholdGB = 50, [int]$BackupAgeThresholdDays = 2, [string]$OutputFolder = (Join-Path -Path (Get-Location) -ChildPath ("DatabaseComparison-" + (Get-Date -Format "yyyyMMdd-HHmm"))) ) function NowTag { (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") } function Convert-BytesToGB([string]$sizeStr) { if (-not $sizeStr) { return 0 } try { if ($sizeStr -match "([\d\.,]+)\s*(KB|MB|GB|TB)") { $num = [double]($matches[1] -replace ',', '.') $bytes = switch ($matches[2].ToUpper()) { "KB" { $num * 1KB } "MB" { $num * 1MB } "GB" { $num * 1GB } "TB" { $num * 1TB } } return [math]::Round($bytes / 1GB, 2) } } catch {} return 0 } Write-Host "[$(NowTag)] ⚠️ AI-GENERATED SCRIPT - UNTESTED" -ForegroundColor Yellow Write-Host "[$(NowTag)] Starting database comparison analysis..." -ForegroundColor Green # Create output folder New-Item -ItemType Directory -Path $OutputFolder -Force | Out-Null # Get all databases with status Write-Host "[$(NowTag)] Retrieving mailbox databases..." $databases = Get-MailboxDatabase -Status -ErrorAction SilentlyContinue | Sort-Object Name $dbCount = ($databases | Measure-Object).Count Write-Host "[$(NowTag)] Found $dbCount databases" # Analyze each database $dbAnalysis = @() $alerts = @() foreach ($db in $databases) { Write-Host "[$(NowTag)] Analyzing $($db.Name)..." $dbSizeGB = Convert-BytesToGB ([string]$db.DatabaseSize) $whitespaceGB = Convert-BytesToGB ([string]$db.AvailableNewMailboxSpace) $backupAge = $null $backupStatus = "Unknown" if ($db.LastFullBackup) { $backupAge = [int]((Get-Date) - $db.LastFullBackup).TotalDays $backupStatus = if ($backupAge -le $BackupAgeThresholdDays) { "OK" } else { "OLD" } } else { $backupStatus = "NEVER" } # Generate alerts $dbAlerts = @() if (-not $db.Mounted) { $dbAlerts += "Database is DISMOUNTED" $alerts += [PSCustomObject]@{ Database = $db.Name Severity = "CRITICAL" Alert = "Database is dismounted" } } if ($whitespaceGB -gt $WhitespaceThresholdGB) { $dbAlerts += "Excessive whitespace: $whitespaceGB GB" $alerts += [PSCustomObject]@{ Database = $db.Name Severity = "WARNING" Alert = "Excessive whitespace: $whitespaceGB GB (threshold: $WhitespaceThresholdGB GB)" } } if ($backupStatus -eq "NEVER") { $dbAlerts += "Never backed up" $alerts += [PSCustomObject]@{ Database = $db.Name Severity = "CRITICAL" Alert = "Database has never been backed up" } } elseif ($backupStatus -eq "OLD") { $dbAlerts += "Backup is $backupAge days old" $alerts += [PSCustomObject]@{ Database = $db.Name Severity = "WARNING" Alert = "Last backup is $backupAge days old (threshold: $BackupAgeThresholdDays days)" } } if ($db.CircularLoggingEnabled) { $dbAlerts += "Circular logging enabled" $alerts += [PSCustomObject]@{ Database = $db.Name Severity = "INFO" Alert = "Circular logging is enabled" } } $dbAnalysis += [PSCustomObject]@{ Database = $db.Name Server = $db.Server Mounted = $db.Mounted DatabaseSizeGB = $dbSizeGB WhitespaceGB = $whitespaceGB WhitespacePercent = if ($dbSizeGB -gt 0) { [math]::Round(($whitespaceGB / $dbSizeGB) * 100, 2) } else { 0 } CircularLogging = $db.CircularLoggingEnabled LastFullBackup = $db.LastFullBackup BackupAgeDays = $backupAge BackupStatus = $backupStatus EdbFilePath = $db.EdbFilePath LogFolderPath = $db.LogFolderPath Alerts = ($dbAlerts -join "; ") } } # Export full analysis $csvFile = Join-Path $OutputFolder "Database-Comparison.csv" $dbAnalysis | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $csvFile Write-Host "[$(NowTag)] Database analysis exported: $csvFile" -ForegroundColor Green # Export alerts if ($alerts.Count -gt 0) { $alertsFile = Join-Path $OutputFolder "Database-Alerts.csv" $alerts | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $alertsFile Write-Host "[$(NowTag)] Alerts exported: $alertsFile" -ForegroundColor Yellow Write-Host "`nALERTS FOUND:" -ForegroundColor Yellow $alerts | Group-Object Severity | ForEach-Object { Write-Host " $($_.Name): $($_.Count) alert(s)" -ForegroundColor $( switch ($_.Name) { "CRITICAL" { "Red" } "WARNING" { "Yellow" } default { "Cyan" } } ) } } else { Write-Host "`n[$(NowTag)] No alerts found - all databases healthy!" -ForegroundColor Green } # Summary statistics Write-Host "`nDATABASE SUMMARY:" -ForegroundColor Cyan Write-Host " Total Databases: $dbCount" Write-Host " Mounted: $(($dbAnalysis | Where-Object Mounted).Count)" Write-Host " Dismounted: $(($dbAnalysis | Where-Object { -not $_.Mounted }).Count)" Write-Host " Total Size: $([math]::Round(($dbAnalysis | Measure-Object DatabaseSizeGB -Sum).Sum, 2)) GB" Write-Host " Total Whitespace: $([math]::Round(($dbAnalysis | Measure-Object WhitespaceGB -Sum).Sum, 2)) GB" Write-Host " Circular Logging Enabled: $(($dbAnalysis | Where-Object CircularLogging).Count)" Write-Host "`n[$(NowTag)] Analysis complete! Output folder: $OutputFolder"