From 79057c66614aad996c4c627e44e8ae8a95d9af75 Mon Sep 17 00:00:00 2001 From: Martien Date: Thu, 18 Sep 2025 11:17:04 +0200 Subject: [PATCH] Add Exchange/Exchange-Inventory.ps1 --- Exchange/Exchange-Inventory.ps1 | 593 ++++++++++++++++++++++++++++++++ 1 file changed, 593 insertions(+) create mode 100644 Exchange/Exchange-Inventory.ps1 diff --git a/Exchange/Exchange-Inventory.ps1 b/Exchange/Exchange-Inventory.ps1 new file mode 100644 index 0000000..07a5876 --- /dev/null +++ b/Exchange/Exchange-Inventory.ps1 @@ -0,0 +1,593 @@ +<# +.SYNOPSIS + Exchange on-prem inventarisatie t.b.v. migratie – rapport in Word (fallback HTML) + CSV's + Inclusief: werkelijke benodigde mailboxruimte (som TotalItemSize van on-prem mailboxen, excl. whitespace). + +.PARAMETER OutputFolder + Doelmap voor outputbestanden. Default: .\Exchange-Inventory- + +.PARAMETER TopMailboxCount + Aantal grootste mailboxen in rapport (default 30) + +.PARAMETER IncludeCSVs + Exporteer detail-CSV's (default $true) + +.PARAMETER ProgressThrottleMs + Minimale interval (ms) tussen progress updates in intensieve loops (default 75) + +.NOTES + - Uitvoeren in Exchange Management Shell met passende RBAC. + - Getest op Exchange 2013/2016/2019. Secties zonder cmdlets worden overgeslagen. + - Gebruik: + # Exchange Management Shell + .\Exchange-Inventory.ps1 -OutputFolder "D:\Reports\" -TopMailboxCount 50 -IncludeCSVs $true + # Optioneel trager/sneller progress-throttle: + .\Exchange-Inventory.ps1 -ProgressThrottleMs 50 +#> + +[CmdletBinding()] +param( + [string]$OutputFolder = (Join-Path -Path (Get-Location) -ChildPath ("Exchange-Inventory-" + (Get-Date -Format "yyyyMMdd-HHmm"))), + [int]$TopMailboxCount = 30, + [bool]$IncludeCSVs = $true, + [int]$ProgressThrottleMs = 75 +) + +# ------------------------- Helpers: Progress / Output / Word/HTML ------------------------- + +function NowTag { (Get-Date).ToString("yyyy-MM-dd HH:mm:ss") } + +$sections = @( + "Voorbereiding","Organisatie & Servers","Client Access & VDirs","POP/IMAP", + "Certificaten","Domeinen & Policies","Transport & Mailflow","Databases & DAG's", + "Database Copies","Mailboxen (on-prem)","Mailbox Stats","Mailbox Join", + "Benodigde mailboxruimte","Top Mailboxen","Retention & Compliance","Mobile Device Policies", + "Addressing (AL/GAL/OAB)","Public Folders","Throttling Policies", + "Hybrid/Federation/OAuth","Mail Queues","Export CSV","Afronden" +) +[int]$sectionIndex = 0 +[int]$totalSections = $sections.Count + +function Show-Progress([string]$status, [int]$percent = -1) { + $activity = "Exchange Inventory – $($sections[$sectionIndex])" + if($percent -lt 0){ + Write-Progress -Activity $activity -Status $status + } else { + Write-Progress -Activity $activity -Status $status -PercentComplete ([Math]::Min(100,[Math]::Max(0,$percent))) + } +} + +function Next-Section { + if($sectionIndex -lt $totalSections-1){ $sectionIndex++ } + $msg = "[{0}] Start sectie: {1} ({2}/{3})" -f (NowTag), $sections[$sectionIndex], ($sectionIndex+1), $totalSections + Write-Host $msg + Write-Progress -Activity ("Exchange Inventory – {0}" -f $sections[$sectionIndex]) -Status "Bezig..." -PercentComplete ([int](($sectionIndex+1)/$totalSections*100)) +} + +# Outputmap +New-Item -ItemType Directory -Path $OutputFolder -Force | Out-Null +$ReportDocx = Join-Path $OutputFolder "Exchange-Inventory.docx" +$ReportHtml = Join-Path $OutputFolder "Exchange-Inventory.html" + +# Word COM of HTML fallback +$script:Word = $null +$script:Doc = $null +$script:UseHtml = $false +$script:HtmlSb = New-Object System.Text.StringBuilder + +function Start-Report { + try { + $script:Word = New-Object -ComObject Word.Application + $script:Word.Visible = $false + $script:Doc = $script:Word.Documents.Add() + Add-Heading "Exchange Inventarisatie" 1 + Add-Paragraph ("Gegenereerd: " + (Get-Date).ToString("yyyy-MM-dd HH:mm")) + } catch { + $script:UseHtml = $true + [void]$script:HtmlSb.AppendLine("Exchange Inventarisatie") + [void]$script:HtmlSb.AppendLine("") + [void]$script:HtmlSb.AppendLine("

Exchange Inventarisatie

") + [void]$script:HtmlSb.AppendLine("

Gegenereerd: " + (Get-Date).ToString("yyyy-MM-dd HH:mm") + "

") + } +} + +function Add-Heading([string]$Text, [int]$Level=2) { + if($script:UseHtml){ + [void]$script:HtmlSb.AppendLine("$Text") + } else { + $range = $script:Doc.Range() + $range.Collapse(0) | Out-Null + $range.Text = "$Text`r" + $style = switch($Level){ 1 {'Heading 1'} 2 {'Heading 2'} 3 {'Heading 3'} default {'Heading 2'}} + $range.set_Style($style) + $range.InsertParagraphAfter() | Out-Null + } +} + +function Add-Paragraph([string]$Text){ + if($script:UseHtml){ + $enc = [System.Web.HttpUtility]::HtmlEncode($Text) + [void]$script:HtmlSb.AppendLine("

$enc

") + } else { + $range = $script:Doc.Range() + $range.Collapse(0) | Out-Null + $range.Text = $Text + $range.InsertParagraphAfter() | Out-Null + } +} + +function Convert-ToCellText([object]$v){ + if($null -eq $v){ return "" } + if($v -is [DateTime]){ return $v.ToString("yyyy-MM-dd HH:mm") } + if($v -is [string]){ return $v } + if($v -is [System.Collections.IEnumerable] -and -not ($v -is [string])){ + $items = @() + foreach($i in $v){ $items += [string]$i } + return ($items -join ", ") + } + return [string]$v +} + +function Add-Table([object]$Objects, [string[]]$PropOrder, [string]$Title){ + if($null -eq $Objects){ + Add-Paragraph ("{0}: geen data." -f $Title) + return + } + $arr = @() + if($Objects -is [System.Collections.IEnumerable] -and -not ($Objects -is [string])){ + foreach($o in $Objects){ $arr += $o } + } else { $arr = @($Objects) } + if($arr.Count -eq 0){ + Add-Paragraph ("{0}: geen data." -f $Title) + return + } + Add-Heading $Title 3 + + if($script:UseHtml){ + [void]$script:HtmlSb.AppendLine("") + foreach($p in $PropOrder){ [void]$script:HtmlSb.AppendLine("") } + [void]$script:HtmlSb.AppendLine("") + foreach($o in $arr){ + [void]$script:HtmlSb.AppendLine("") + foreach($p in $PropOrder){ + $cell = Convert-ToCellText ($o.$p) + [void]$script:HtmlSb.AppendLine("") + } + [void]$script:HtmlSb.AppendLine("") + } + [void]$script:HtmlSb.AppendLine("
$([System.Web.HttpUtility]::HtmlEncode($p))
$([System.Web.HttpUtility]::HtmlEncode($cell))
") + } else { + $rows = $arr.Count + $cols = $PropOrder.Count + $range = $script:Doc.Range() + $table = $script:Doc.Tables.Add($range, [Math]::Max(1,$rows)+1, $cols) + for($c=1; $c -le $cols; $c++){ $table.Cell(1,$c).Range.Text = $PropOrder[$c-1] } + $table.Rows.Item(1).Range.Bold = $true + for($r=0; $r -lt $rows; $r++){ + for($c=0; $c -lt $cols; $c++){ + $prop = $PropOrder[$c] + $cell = Convert-ToCellText ($arr[$r].$prop) + $table.Cell($r+2, $c+1).Range.Text = $cell + } + } + $table.AutoFitBehavior(2) | Out-Null + $range.InsertParagraphAfter() | Out-Null + } +} + +function End-Report { + if($script:UseHtml){ + [void]$script:HtmlSb.AppendLine("") + $script:HtmlSb.ToString() | Out-File -LiteralPath $ReportHtml -Encoding UTF8 + Write-Host ("[{0}] HTML-rapport: {1}" -f (NowTag), $ReportHtml) + } else { + $wdFormatXMLDocument = 12 + $script:Doc.SaveAs([ref]$ReportDocx, [ref]$wdFormatXMLDocument) + $script:Doc.Close(); $script:Word.Quit() + Write-Host ("[{0}] Word-rapport: {1}" -f (NowTag), $ReportDocx) + } + Write-Progress -Activity "Exchange Inventory – Gereed" -Completed +} + +function Export-IfEnabled($data, $name){ + if($IncludeCSVs -and $data){ + $file = Join-Path $OutputFolder ($name + ".csv") + $data | Export-Csv -NoTypeInformation -Encoding UTF8 -Path $file + Write-Host ("[{0}] CSV geexporteerd: {1}" -f (NowTag), $file) + } +} + +# ------------------------- Start ------------------------- + +Write-Host ("[{0}] Inventarisatie gestart..." -f (NowTag)) +Show-Progress "Initialisatie..." 1 +Start-Report + +# 1) Organisatie & Servers +Next-Section +Show-Progress "Get-OrganizationConfig / Get-ExchangeServer..." +try { $org = Get-OrganizationConfig -ErrorAction SilentlyContinue } catch { $org = $null } +$servers = Get-ExchangeServer -ErrorAction SilentlyContinue | Sort-Object Name +$serversOut = $servers | Select-Object Name, Edition, AdminDisplayVersion, IsClientAccessServer, + IsMailboxServer, IsHubTransportServer, ServerRole, Site, Fqdn, *Build* +Add-Heading "Organisatie & Servers" 2 +if($org){ + $orgRow = [pscustomobject]@{ + Name = $org.Name + DomainName = $org.OriginatingServer + AddressBookPolicyRouting = $org.AddressBookPolicyRoutingEnabled + OAuth2ClientProfile = $org.OAuth2ClientProfileEnabled + PublicFoldersEnabled = $org.PublicFoldersEnabled + IsDehydrated = $org.IsDehydrated + } + Add-Table $orgRow @("Name","DomainName","AddressBookPolicyRouting","OAuth2ClientProfile","PublicFoldersEnabled","IsDehydrated") "Organisatieconfig" +} +Add-Table $serversOut @("Name","Edition","AdminDisplayVersion","ServerRole","Site","Fqdn") "Exchange Servers" + +# 2) Client Access & Virtual Directories +Next-Section +Show-Progress "Virtual directories ophalen..." +try { + $owa = Get-OWAVirtualDirectory -ErrorAction Stop | Select-Object Server,Name,InternalUrl,ExternalUrl,BasicAuthentication +} catch { + $owa = Get-OWAVirtualDirectory -ErrorAction SilentlyContinue | Select-Object Server,Name,InternalUrl,ExternalUrl +} +$ecp = Get-EcpVirtualDirectory -ErrorAction SilentlyContinue | Select-Object Server,Name,InternalUrl,ExternalUrl +$ews = Get-WebServicesVirtualDirectory -ErrorAction SilentlyContinue | Select-Object Server,Name,InternalUrl,ExternalUrl +$oabvd= Get-OabVirtualDirectory -ErrorAction SilentlyContinue | Select-Object Server,Name,InternalUrl,ExternalUrl +$autod= Get-AutodiscoverVirtualDirectory -ErrorAction SilentlyContinue | Select-Object Server,Name,InternalUrl,ExternalUrl +$as = Get-ActiveSyncVirtualDirectory -ErrorAction SilentlyContinue | Select-Object Server,Name,InternalUrl,ExternalUrl +$mapi= Get-MapiVirtualDirectory -ErrorAction SilentlyContinue | Select-Object Server,Name,InternalUrl,ExternalUrl, IISAuthenticationMethods +$psvd= Get-PowerShellVirtualDirectory -ErrorAction SilentlyContinue | Select-Object Server,Name,InternalUrl,ExternalUrl +Add-Table $owa @("Server","Name","InternalUrl","ExternalUrl") "OWA" +Add-Table $ecp @("Server","Name","InternalUrl","ExternalUrl") "ECP" +Add-Table $ews @("Server","Name","InternalUrl","ExternalUrl") "EWS" +Add-Table $oabvd@("Server","Name","InternalUrl","ExternalUrl") "OAB" +Add-Table $autod@("Server","Name","InternalUrl","ExternalUrl") "Autodiscover" +Add-Table $as @("Server","Name","InternalUrl","ExternalUrl") "ActiveSync" +Add-Table $mapi @("Server","Name","InternalUrl","ExternalUrl","IISAuthenticationMethods") "MAPI/HTTP" +Add-Table $psvd @("Server","Name","InternalUrl","ExternalUrl") "Remote PowerShell" + +# 3) POP/IMAP +Next-Section +Show-Progress "POP/IMAP settings..." +$pop = Get-PopSettings -ErrorAction SilentlyContinue | Select-Object Server, LoginType, X509CertificateName, SSLBindings, UnencryptedOrTLSBindings, ProtocolLogEnabled +$imap= Get-ImapSettings -ErrorAction SilentlyContinue | Select-Object Server, LoginType, X509CertificateName, SSLBindings, UnencryptedOrTLSBindings, ProtocolLogEnabled +Add-Table $pop @("Server","LoginType","X509CertificateName","SSLBindings","UnencryptedOrTLSBindings","ProtocolLogEnabled") "POP" +Add-Table $imap @("Server","LoginType","X509CertificateName","SSLBindings","UnencryptedOrTLSBindings","ProtocolLogEnabled") "IMAP" + +# 4) Certificaten +Next-Section +Show-Progress "Exchange certificaten..." +$certs = @() +Get-ExchangeCertificate -ErrorAction SilentlyContinue | ForEach-Object { + $days = ($_.NotAfter - (Get-Date)).TotalDays + $status = if($days -lt 0){"Verlopen"} elseif($days -lt 90){"Verloopt <90d"} else {"OK"} + $certs += [pscustomobject]@{ + Thumbprint = $_.Thumbprint + Services = ($_.Services -join ",") + Subject = $_.Subject + FriendlyName = $_.FriendlyName + NotBefore = $_.NotBefore + NotAfter = $_.NotAfter + Status = $status + } +} +Add-Table $certs @("Thumbprint","Services","Subject","FriendlyName","NotBefore","NotAfter","Status") "Exchange Certificates" + +# 5) Domeinen & Policies +Next-Section +Show-Progress "Accepted/Remote domains, EAP..." +$accDom = Get-AcceptedDomain -ErrorAction SilentlyContinue | Select-Object Name,DomainName,DomainType,Default +$remDom = Get-RemoteDomain -ErrorAction SilentlyContinue | Select-Object Name,DomainName,AllowedOOFType,AutoReplyEnabled,AutoForwardEnabled +$emailp = Get-EmailAddressPolicy -ErrorAction SilentlyContinue | Select-Object Name,Priority,EnabledPrimaryAddressTemplate,RecipientFilter +Add-Table $accDom @("Name","DomainName","DomainType","Default") "Accepted Domains" +Add-Table $remDom @("Name","DomainName","AllowedOOFType","AutoReplyEnabled","AutoForwardEnabled") "Remote Domains" +Add-Table $emailp @("Name","Priority","EnabledPrimaryAddressTemplate","RecipientFilter") "Email Address Policies" + +# 6) Transport & Mailflow +Next-Section +Show-Progress "Transportconfig, rules, connectors..." +try { $tconf = Get-TransportConfig -ErrorAction Stop } catch { $tconf = $null } +if($tconf){ + $trow = [pscustomobject]@{ + TLSReceiveDomainSecureList = ($tconf.TLSReceiveDomainSecureList -join ",") + TlsSendDomainSecureList = ($tconf.TLSSendDomainSecureList -join ",") + ExternalDNSAdapterEnabled = $tconf.ExternalDNSAdapterEnabled + InternalSMTPServers = ($tconf.InternalSMTPServers -join ",") + MaxSendSize = $tconf.MaxSendSize + MaxReceiveSize = $tconf.MaxReceiveSize + } + Add-Table $trow @("TLSReceiveDomainSecureList","TlsSendDomainSecureList","ExternalDNSAdapterEnabled","InternalSMTPServers","MaxSendSize","MaxReceiveSize") "Transport Config" +} +$trules = Get-TransportRule -ErrorAction SilentlyContinue | Select-Object Name,Priority,State,Mode +Add-Table $trules @("Name","Priority","State","Mode") "Transport Rules" +$send = Get-SendConnector -ErrorAction SilentlyContinue | Select-Object Name,AddressSpaces,Enabled,SmartHosts,UseExternalDNSServersEnabled,SourceTransportServers +$recv = Get-ReceiveConnector -ErrorAction SilentlyContinue | Select-Object Server,Name,TransportRole,Bindings,RemoteIPRanges,AuthMechanism,TlsDomainCapabilities,PermissionGroups +Add-Table $send @("Name","AddressSpaces","Enabled","SmartHosts","UseExternalDNSServersEnabled","SourceTransportServers") "Send Connectors" +Add-Table $recv @("Server","Name","TransportRole","Bindings","RemoteIPRanges","AuthMechanism","TlsDomainCapabilities","PermissionGroups") "Receive Connectors" +Export-IfEnabled $send "SendConnectors" +Export-IfEnabled $recv "ReceiveConnectors" + +# 7) Databases & DAG's +Next-Section +Show-Progress "DAG's, DAG-netwerken, databases..." +$dag = Get-DatabaseAvailabilityGroup -ErrorAction SilentlyContinue | Select-Object Name,WitnessServer,WitnessDirectory,OperationalServers,Servers,AutoDagTotalNumberOfServers +Add-Table $dag @("Name","WitnessServer","WitnessDirectory","OperationalServers","Servers","AutoDagTotalNumberOfServers") "Database Availability Groups" +$dagNet = Get-DatabaseAvailabilityGroupNetwork -ErrorAction SilentlyContinue | Select-Object Name,Identity,Description,Subnets,ReplicationEnabled +Add-Table $dagNet @("Name","Identity","Description","Subnets","ReplicationEnabled") "DAG Networks" +$dbs = Get-MailboxDatabase -Status -ErrorAction SilentlyContinue | Sort-Object Name | Select-Object Name,Server,EDBFilePath,LogFolderPath,Mounted,CircularLoggingEnabled,DatabaseSize,AvailableNewMailboxSpace,Recovery +Add-Table $dbs @("Name","Server","EDBFilePath","LogFolderPath","Mounted","CircularLoggingEnabled","DatabaseSize","AvailableNewMailboxSpace","Recovery") "Mailbox Databases" + +# 8) Database Copies +Next-Section +Show-Progress "Database copies ophalen... 0%" +$dbCopies = @() +if($dbs){ + $dbCount = $dbs.Count + $lastTick = Get-Date + for($i=0; $i -lt $dbCount; $i++){ + $db = $dbs[$i] + $copies = Get-MailboxDatabaseCopyStatus $db.Name -ErrorAction SilentlyContinue + $m = Get-MailboxDatabase $db.Name -ErrorAction SilentlyContinue + foreach($c in $copies){ + $mbxSrv = $c.Name.Split("\")[-1] + $dbCopies += [pscustomobject]@{ + Database = $db.Name + MailboxServer = $mbxSrv + Status = $c.Status + ContentIndexState = $c.ContentIndexState + CopyQueueLength = $c.CopyQueueLength + ReplayQueueLength = $c.ReplayQueueLength + LastInspectedLogTime = $c.LastInspectedLogTime + ActivationPreference = ($m.ActivationPreference | Where-Object {$_.Key -eq $mbxSrv}).Value + } + } + if(((Get-Date) - $lastTick).TotalMilliseconds -ge $ProgressThrottleMs){ + $pct = [int](($i+1)/$dbCount*100) + Show-Progress ("Database copies... {0}/{1}" -f ($i+1), $dbCount) $pct + $lastTick = Get-Date + } + } +} +Add-Table $dbCopies @("Database","MailboxServer","Status","ContentIndexState","CopyQueueLength","ReplayQueueLength","LastInspectedLogTime","ActivationPreference") "Database Copies" + +# 9) Mailboxen (on-prem) – filter Remote* en vereis Database +Next-Section +Show-Progress "Mailboxen (on-prem) ophalen..." +$mailboxes = Get-Mailbox -ResultSize Unlimited -ErrorAction SilentlyContinue | + Where-Object { $_.RecipientTypeDetails -notmatch "^Remote" -and $_.Database } | + Select-Object DisplayName,PrimarySmtpAddress,RecipientTypeDetails,Database,ArchiveStatus,RetentionPolicy, LitigationHoldEnabled,WhenCreated +Add-Heading "Mailboxen (on-premises)" 2 + +# 10) Mailbox Stats (per database, voorkomt prompt om Identity) +Next-Section +Show-Progress "Mailbox statistics per database..." +$mbStats = @() +$statsDbs = ($mailboxes.Database | Sort-Object -Unique) +$sdCount = ($statsDbs | Measure-Object).Count +if($sdCount -gt 0){ + $idx=0; $lastTick = Get-Date + foreach($dbName in $statsDbs){ + try{ + Get-MailboxStatistics -Database $dbName -ErrorAction SilentlyContinue | + Select-Object DisplayName,Database,TotalItemSize,ItemCount,LastLogonTime,LastLoggedOnUserAccount | + ForEach-Object { $mbStats += $_ } + } catch {} + $idx++ + if(((Get-Date) - $lastTick).TotalMilliseconds -ge $ProgressThrottleMs){ + Show-Progress ("Mailbox statistics... {0}/{1} databases" -f $idx,$sdCount) ([int]($idx/$sdCount*100)) + $lastTick = Get-Date + } + } +} + +# 11) Mailbox Join +Next-Section +Show-Progress "Mailbox eigenschappen koppelen..." +$sizeLookup = @{} +foreach($s in $mbStats){ $sizeLookup[$s.DisplayName] = $s } +$mbForTop = @() +if($mailboxes){ + $mbCount = $mailboxes.Count + $k=0; $lastTick = Get-Date + foreach($m in $mailboxes){ + $s = $sizeLookup[$m.DisplayName] + $mbForTop += [pscustomobject]@{ + DisplayName = $m.DisplayName + PrimarySmtpAddress = $m.PrimarySmtpAddress + RecipientTypeDetails = $m.RecipientTypeDetails + Database = $m.Database + ArchiveStatus = $m.ArchiveStatus + RetentionPolicy = $m.RetentionPolicy + LitigationHoldEnabled= $m.LitigationHoldEnabled + TotalItemSize = if($s){ $s.TotalItemSize } else { $null } + ItemCount = if($s){ $s.ItemCount } else { $null } + LastLogonTime = if($s){ $s.LastLogonTime } else { $null } + } + $k++ + if($mbCount -gt 0 -and ((Get-Date) - $lastTick).TotalMilliseconds -ge $ProgressThrottleMs){ + Show-Progress ("Join mailboxen... {0}/{1}" -f $k,$mbCount) ([int]($k/$mbCount*100)) + $lastTick = Get-Date + } + } +} + +# 12) Benodigde mailboxruimte (excl. whitespace) – totaal en per database +Next-Section +Show-Progress "Benodigde mailboxruimte berekenen..." +# Helper: haal bytes op uit TotalItemSize veilig +function Get-BytesFromSize($tis){ + try { + if($null -eq $tis){ return 0 } + if($tis.Value -and ($tis.Value.PSObject.Properties.Name -contains "ToBytes")){ + return [int64]$tis.Value.ToBytes() + } + # fallback op string parse (alleen indien nodig) + $s = [string]$tis + if($s -match "([\d\.,]+)\s*(KB|MB|GB|TB)"){ + $num = ($matches[1] -replace '\.','.' -replace ',','.') + switch($matches[2].ToUpper()){ + "KB" { return [double]$num * 1KB } + "MB" { return [double]$num * 1MB } + "GB" { return [double]$num * 1GB } + "TB" { return [double]$num * 1TB } + } + } + } catch { } + return 0 +} + +# Totaal bytes +$totalBytes = 0 +foreach($row in $mbForTop){ + $totalBytes += [int64](Get-BytesFromSize $row.TotalItemSize) +} +$totalGB = [math]::Round($totalBytes / 1GB, 2) + +# Per database aggregatie +$byDb = @() +$groups = $mbForTop | Group-Object -Property Database +foreach($g in $groups){ + $b = 0 + foreach($m in $g.Group){ $b += [int64](Get-BytesFromSize $m.TotalItemSize) } + $byDb += [pscustomobject]@{ + Database = $g.Name + MailboxCount = $g.Count + TotalGB = [math]::Round($b / 1GB, 2) + } +} + +$totalsRow = [pscustomobject]@{ + TotaalMailboxen = ($mbForTop | Measure-Object).Count + TotaleInhoudGB = $totalGB +} +Add-Table $totalsRow @("TotaalMailboxen","TotaleInhoudGB") "Werkelijke benodigde mailboxruimte (excl. whitespace)" +Add-Table ($byDb | Sort-Object -Property Database) @("Database","MailboxCount","TotalGB") "Benodigde mailboxruimte per database" + +Export-IfEnabled ($mbForTop | Select-Object DisplayName,PrimarySmtpAddress,Database,TotalItemSize,ItemCount) "MailboxContentSizes" +Export-IfEnabled ($byDb | Sort-Object Database) "MailboxContentPerDatabase" + +# 13) Samenvatting + Top Mailboxen +Next-Section +Show-Progress "Samenvatting mailboxen..." +$sum = [pscustomobject]@{ + TotaalMailboxen = ($mailboxes | Measure-Object).Count + MetArchive = ($mailboxes | Where-Object {$_.ArchiveStatus -ne "None"} | Measure-Object).Count + LitigationHold = ($mailboxes | Where-Object {$_.LitigationHoldEnabled} | Measure-Object).Count + UniekeDatabases = (($mailboxes.Database | Sort-Object -Unique) -join ", ") +} +Add-Table $sum @("TotaalMailboxen","MetArchive","LitigationHold","UniekeDatabases") "Samenvatting (on-prem)" + +function Parse-Size([string]$val){ + if(-not $val){ return 0 } + if($val -match "([\d\.,]+)\s*(KB|MB|GB|TB)"){ + $num = ($matches[1] -replace '\.','.' -replace ',','.') + switch($matches[2].ToUpper()){ + "KB" { return [double]$num * 1KB } + "MB" { return [double]$num * 1MB } + "GB" { return [double]$num * 1GB } + "TB" { return [double]$num * 1TB } + } + } + return 0 +} + +Next-Section +Show-Progress "Bepalen top mailboxen..." +$topSrc = $mbForTop | Where-Object { $_.TotalItemSize } +$topCount = ($topSrc | Measure-Object).Count +$mbTop = @() +if($topCount -gt 0){ + $sorted = $topSrc | Sort-Object @{Expression={ Parse-Size ([string]$_.TotalItemSize) };Descending=$true} + $chunks = 10 + for($c=1;$c -le $chunks;$c++){ + Start-Sleep -Milliseconds ([Math]::Min($ProgressThrottleMs,150)) + Show-Progress ("Sorteren... stap {0}/{1}" -f $c,$chunks) ([int]($c/$chunks*100)) + } + $mbTop = $sorted | Select-Object -First $TopMailboxCount +} +Add-Table $mbTop @("DisplayName","PrimarySmtpAddress","Database","TotalItemSize","ItemCount","LastLogonTime","ArchiveStatus","RetentionPolicy","LitigationHoldEnabled") ("Top " + $TopMailboxCount + " grootste mailboxen (on-prem)") + +Export-IfEnabled $mbForTop "AllMailboxes" # on-prem only +Export-IfEnabled $mbTop "TopMailboxes" # on-prem only + +# 14) Retention & Compliance +Next-Section +Show-Progress "Retention policies & tags..." +$rets = Get-RetentionPolicy -ErrorAction SilentlyContinue | Select-Object Name,RetentionPolicyTagLinks,IsDefault +$tags = Get-RetentionPolicyTag -ErrorAction SilentlyContinue | Select-Object Name,Type,RetentionAction,AgeLimitForRetention,RetentionEnabled +$holds= Get-Mailbox -ResultSize Unlimited -Filter "LitigationHoldEnabled -eq 'True'" -ErrorAction SilentlyContinue | Select-Object DisplayName,PrimarySmtpAddress,WhenCreated +Add-Table $rets @("Name","RetentionPolicyTagLinks","IsDefault") "Retention Policies" +Add-Table $tags @("Name","Type","RetentionAction","AgeLimitForRetention","RetentionEnabled") "Retention Tags" +Add-Table $holds @("DisplayName","PrimarySmtpAddress","WhenCreated") "Mailboxen met Litigation Hold" + +# 15) Mobile Device Policies +Next-Section +Show-Progress "Mobile Device policies..." +$mdpol = Get-MobileDeviceMailboxPolicy -ErrorAction SilentlyContinue | Select-Object Name,AllowNonProvisionableDevices,AlphanumericDevicePasswordRequired,MaxInactivityTimeDeviceLock,MinDevicePasswordLength,PasswordRecoveryEnabled +Add-Table $mdpol @("Name","AllowNonProvisionableDevices","AlphanumericDevicePasswordRequired","MaxInactivityTimeDeviceLock","MinDevicePasswordLength","PasswordRecoveryEnabled") "Mobile Device Policies" + +# 16) Addressing (AL/GAL/OAB) +Next-Section +Show-Progress "Address lists, GAL, OAB..." +$als = Get-AddressList -ErrorAction SilentlyContinue | Select-Object Name,RecipientFilter +$gals= Get-GlobalAddressList -ErrorAction SilentlyContinue | Select-Object Name,RecipientFilter +$oab = Get-OfflineAddressBook -ErrorAction SilentlyContinue | Select-Object Name,AddressLists,IsDefault +Add-Table $als @("Name","RecipientFilter") "Address Lists" +Add-Table $gals @("Name","RecipientFilter") "Global Address Lists" +Add-Table $oab @("Name","AddressLists","IsDefault") "Offline Address Books" + +# 17) Public Folders +Next-Section +Show-Progress "Public Folders..." +try { + $pfmailboxes = Get-Mailbox -PublicFolder -ErrorAction SilentlyContinue | Select-Object Name,Database,IsRootPublicFolderMailbox + if($pfmailboxes){ Add-Table $pfmailboxes @("Name","Database","IsRootPublicFolderMailbox") "Public Folder Mailboxes (Modern PFs)" } +} catch { } +try { + $pfdb = Get-PublicFolderDatabase -ErrorAction SilentlyContinue | Select-Object Name,Server,DeletedItemRetention,DistinguishedName + if($pfdb){ Add-Table $pfdb @("Name","Server","DeletedItemRetention","DistinguishedName") "Public Folder Database (Legacy)" } +} catch { } + +# 18) Throttling Policies +Next-Section +Show-Progress "Throttling policies..." +$throt = Get-ThrottlingPolicy -ErrorAction SilentlyContinue | Select-Object Name,IsDefault,GlobalThrottlingPolicy +Add-Table $throt @("Name","IsDefault","GlobalThrottlingPolicy") "Throttling Policies" + +# 19) Hybrid / Federation / OAuth +Next-Section +Show-Progress "Hybrid/Federation/OAuth..." +try { $fed = Get-FederationTrust -ErrorAction SilentlyContinue | Select-Object Name,ApplicationUri,TokenIssuerUri,OrgContact } catch { $fed = $null } +try { $auth= Get-AuthConfig -ErrorAction SilentlyContinue | Select-Object ServiceName,Issuer,EvoStsMetadataUrl,CurrentCertificateThumbprint,PreviousCertificateThumbprint } catch { $auth = $null } +try { $hyb = Get-HybridConfiguration -ErrorAction SilentlyContinue | Select-Object Features,Domains,ClientAccessServers,ReceivingTransportServers,SendingTransportServers } catch { $hyb = $null } +if($fed){ Add-Table $fed @("Name","ApplicationUri","TokenIssuerUri","OrgContact") "Federation Trust" } +if($auth){ Add-Table $auth @("ServiceName","Issuer","EvoStsMetadataUrl","CurrentCertificateThumbprint","PreviousCertificateThumbprint") "OAuth Config" } +if($hyb){ Add-Table $hyb @("Features","Domains","ClientAccessServers","ReceivingTransportServers","SendingTransportServers") "Hybrid Configuration" } + +# 20) Mail Queues +Next-Section +Show-Progress "Mail queues per server..." +$qdata = foreach($sv in $servers){ + try{ + $qs = Get-Queue -Server $sv.Name -ErrorAction Stop | Measure-Object -Property MessageCount -Sum + [pscustomobject]@{ Server=$sv.Name; TotalQueued=$qs.Sum } + } catch { + [pscustomobject]@{ Server=$sv.Name; TotalQueued="N/A" } + } +} +Add-Table $qdata @("Server","TotalQueued") "Queue per server" + +# 21) CSV Export +Next-Section +Show-Progress "CSV export..." +Export-IfEnabled $dbs "MailboxDatabases" +Export-IfEnabled $serversOut "Servers" +Export-IfEnabled $dbCopies "DatabaseCopies" + +# 22) Afronden +Next-Section +Show-Progress "Rapport opslaan..." +End-Report +Write-Host ("[{0}] Inventarisatie gereed. Map: {1}" -f (NowTag), $OutputFolder) \ No newline at end of file