123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400
# ================================================================================================
# 365Fanatic - Mailbox Archiving & Custom Attributes Report for Exchange Online
# ------------------------------------------------------------------------------------------------
# Exports ONE CSV with, per mailbox (User, Shared and Microsoft 365 Group mailboxes):
# - Identity info
# - Archive details (status, state, name, quotas, auto-expanding, archive size / item count)
# - Retention (policy, retention hold, MRM processing, move-to-archive rules of the policy)
# - Holds (litigation, in-place, compliance tag, delay hold) + Single Item Recovery
# - Primary mailbox size, item count, last logon / activity
# - CustomAttribute1 - 15 and ExtensionCustomAttribute1 - 5
# A second CSV with the retention policies and their move-to-archive tags is also created.
#
# Before connecting, the script checks the ExchangeOnlineManagement module against the
# PowerShell Gallery and asks whether you want to update it (skip with -SkipModuleCheck).
# ================================================================================================
[CmdletBinding()]
param(
# Optional. If omitted, an existing session is reused or you are prompted to sign in.
[string]$UserPrincipalName,
[string]$OutputDirectory = 'C:\Temp',
[ValidateSet('UserMailbox', 'SharedMailbox', 'GroupMailbox')]
[string[]]$MailboxType = @('UserMailbox', 'SharedMailbox', 'GroupMailbox'),
# Also report inactive (soft-deleted, on hold) mailboxes
[switch]$IncludeInactiveMailboxes,
# Much faster: skips size / item count / last logon lookups
[switch]$SkipStatistics,
# Skip the "update the Exchange Online module?" check (e.g. for scheduled runs)
[switch]$SkipModuleCheck
)
$ErrorActionPreference = 'Stop'
# ------------------------------------------------------------------------------------------------
# Banner
# ------------------------------------------------------------------------------------------------
function Show-Banner {
$art = @'
_____ __ ____ _____ _ _
|___ / / /_ | ___| | ___| __ _ _ __ __ _ | |_ (_) ___
|_ \ | '_ \ |___ \ | |_ / _` || '_ \ / _` || __|| | / __|
___) || (_) | ___) || _| | (_| || | | || (_| || |_ | || (__
|____/ \___/ |____/ |_| \__,_||_| |_| \__,_| \__||_| \___|
'@
Write-Host $art -ForegroundColor Cyan
Write-Host ' Mailbox Archiving & Custom Attributes Report for Exchange Online' -ForegroundColor Yellow
Write-Host ''
}
# ------------------------------------------------------------------------------------------------
# Helper functions
# ------------------------------------------------------------------------------------------------
# Retries only on throttling / transient errors; real errors (not found, bad parameter) fail fast.
function Invoke-WithRetry {
param([scriptblock]$ScriptBlock, [int]$MaxAttempts = 3)
for ($attempt = 1; $attempt -le $MaxAttempts; $attempt++) {
try { return & $ScriptBlock }
catch {
$transient = $_.Exception.Message -match 'throttl|429|too many requests|timed out|timeout|temporar|try again'
if (-not $transient -or $attempt -eq $MaxAttempts) { throw }
Start-Sleep -Seconds (5 * $attempt)
}
}
}
# Converts "1.5 GB (1,610,612,736 bytes)" (string or ByteQuantifiedSize) to bytes. $null if unlimited/empty.
function ConvertTo-Bytes {
param($Value)
if ($null -eq $Value) { return $null }
$text = $Value.ToString()
if ($text -match 'Unlimited') { return $null }
if ($text -match '\(([\d,\.\s]+)\s*bytes\)') { return [int64]($Matches[1] -replace '[^\d]', '') }
return $null
}
function ConvertTo-GB {
param($Value)
if ($null -eq $Value) { return $null }
if ($Value.ToString() -match 'Unlimited') { return 'Unlimited' }
$bytes = ConvertTo-Bytes $Value
if ($null -eq $bytes) { return $null }
return [math]::Round($bytes / 1GB, 3)
}
function Format-DateValue {
param($Value)
if ($null -eq $Value -or $Value -eq '') { return $null }
try { return ([datetime]$Value).ToString('yyyy-MM-dd HH:mm:ss') } catch { return $Value.ToString() }
}
$script:TotalSteps = 7
$script:StartTime = Get-Date
# Timestamped status line
function Write-Step {
param([string]$Message, [string]$Color = 'Gray')
Write-Host ("[{0}] {1}" -f (Get-Date -Format 'HH:mm:ss'), $Message) -ForegroundColor $Color
}
# Step header, e.g. "=== Step 3/7: Reading retention policies ==="
function Write-Phase {
param([int]$Number, [string]$Title)
Write-Host ''
Write-Host ("[{0}] === Step {1}/{2}: {3} ===" -f (Get-Date -Format 'HH:mm:ss'), $Number, $script:TotalSteps, $Title) -ForegroundColor Cyan
}
# Mailbox statistics by ExchangeGuid. Tries Get-ExoMailboxStatistics first (REST, fast),
# then falls back to Get-MailboxStatistics (e.g. for Microsoft 365 Group mailboxes).
function Get-StatsSafe {
param([guid]$Guid, [switch]$Archive, [string[]]$Properties)
$archiveArgs = @{}
if ($Archive) { $archiveArgs['Archive'] = $true }
$errors = @()
try { return Get-ExoMailboxStatistics -ExchangeGuid $Guid -Properties $Properties @archiveArgs -ErrorAction Stop }
catch { $errors += $_.Exception.Message }
try { return Get-MailboxStatistics -Identity $Guid.ToString() @archiveArgs -ErrorAction Stop }
catch { $errors += $_.Exception.Message }
throw ($errors -join ' || ')
}
# Checks the installed ExchangeOnlineManagement module against the PowerShell Gallery and asks to update.
function Confirm-ExoModule {
$installed = Get-Module -ListAvailable -Name ExchangeOnlineManagement |
Sort-Object Version -Descending | Select-Object -First 1
if (-not $installed) {
Write-Host 'The ExchangeOnlineManagement module is not installed.' -ForegroundColor Yellow
if ((Read-Host 'Install it now from the PowerShell Gallery? (Y/N)') -match '^[Yy]') {
Install-Module -Name ExchangeOnlineManagement -Scope CurrentUser -Force -AllowClobber
Import-Module ExchangeOnlineManagement -Force
Write-Host 'Module installed.' -ForegroundColor Green
return
}
throw 'The ExchangeOnlineManagement module is required.'
}
Write-Host "Installed ExchangeOnlineManagement module : v$($installed.Version)"
try { $latest = Find-Module -Name ExchangeOnlineManagement -ErrorAction Stop }
catch {
Write-Warning "Could not check the PowerShell Gallery for updates: $($_.Exception.Message)"
return
}
Write-Host "Latest version on PowerShell Gallery : v$($latest.Version)"
if ([version]$latest.Version -le $installed.Version) {
Write-Host 'The module is up to date.' -ForegroundColor Green
return
}
# Module 3.10.0+ needs PowerShell 7.6.0+ when running in PowerShell 7 (Windows PowerShell 5.1 is fine)
if ($PSVersionTable.PSEdition -eq 'Core' -and
$PSVersionTable.PSVersion -lt [version]'7.6.0' -and
[version]$latest.Version -ge [version]'3.10.0') {
Write-Warning "v$($latest.Version) requires PowerShell 7.6.0 or later (you are on $($PSVersionTable.PSVersion)). Update PowerShell first, or run this script in Windows PowerShell 5.1. Continuing with v$($installed.Version)."
return
}
Write-Host "A newer version is available." -ForegroundColor Yellow
if ((Read-Host "Update the module to v$($latest.Version) now? (Y/N)") -match '^[Yy]') {
try {
Update-Module -Name ExchangeOnlineManagement -Force -ErrorAction Stop
Remove-Module ExchangeOnlineManagement -Force -ErrorAction SilentlyContinue
Import-Module ExchangeOnlineManagement -Force -ErrorAction Stop
Write-Host "Module updated - now using v$((Get-Module ExchangeOnlineManagement).Version)." -ForegroundColor Green
}
catch {
Write-Warning "Module update failed: $($_.Exception.Message)"
Write-Warning "If it was installed for all users, re-run PowerShell as Administrator. Continuing with v$($installed.Version)."
}
}
else {
Write-Host "Keeping v$($installed.Version)." -ForegroundColor Gray
}
}
# ------------------------------------------------------------------------------------------------
# Start
# ------------------------------------------------------------------------------------------------
Clear-Host
Show-Banner
Write-Phase 1 'Checking the Exchange Online PowerShell module'
if (-not $SkipModuleCheck -and [Environment]::UserInteractive) {
Write-Step 'Looking up the installed version and comparing it with the PowerShell Gallery...' 'Yellow'
try { Confirm-ExoModule }
catch { Write-Host $_.Exception.Message -ForegroundColor Red; exit 1 }
}
else {
Write-Step 'Module check skipped.' 'DarkGray'
}
# ------------------------------------------------------------------------------------------------
# Connect
# ------------------------------------------------------------------------------------------------
Write-Phase 2 'Connecting to Exchange Online'
Write-Step 'Checking for an existing Exchange Online session...' 'Yellow'
$weConnected = $false
$existing = try { @(Get-ConnectionInformation | Where-Object { $_.State -eq 'Connected' }) } catch { @() }
if ($existing.Count -gt 0) {
Write-Host "Re-using existing Exchange Online session ($($existing[0].UserPrincipalName))." -ForegroundColor Green
}
else {
try {
if ($UserPrincipalName) {
Write-Host "Connecting to Exchange Online as: $UserPrincipalName" -ForegroundColor Yellow
Connect-ExchangeOnline -UserPrincipalName $UserPrincipalName -ShowBanner:$false -ErrorAction Stop
}
else {
Write-Host 'Connecting to Exchange Online (sign-in prompt)...' -ForegroundColor Yellow
Connect-ExchangeOnline -ShowBanner:$false -ErrorAction Stop
}
$weConnected = $true
Write-Host 'Connected.' -ForegroundColor Green
}
catch {
Write-Host "Failed to connect to Exchange Online: $($_.Exception.Message)" -ForegroundColor Red
exit 1
}
}
try {
# --------------------------------------------------------------------------------------------
# Retention policy -> "move to archive" rules (looked up once, reused per mailbox)
# --------------------------------------------------------------------------------------------
Write-Phase 3 'Reading retention policies and retention tags'
$policyRules = @{}
$policyReport = [System.Collections.Generic.List[object]]::new()
try {
$tagLookup = @{}
Write-Step 'Running Get-RetentionPolicyTag...' 'Yellow'
$allTags = @(Get-RetentionPolicyTag)
foreach ($tag in $allTags) { $tagLookup[$tag.Name] = $tag }
Write-Step "Found $($allTags.Count) retention tags." 'Gray'
Write-Step 'Running Get-RetentionPolicy...' 'Yellow'
$allPolicies = @(Get-RetentionPolicy)
Write-Step "Found $($allPolicies.Count) retention policies - resolving their move-to-archive rules..." 'Gray'
foreach ($policy in $allPolicies) {
$moveRules = [System.Collections.Generic.List[string]]::new()
foreach ($tagName in @($policy.RetentionPolicyTagLinks)) {
$tag = $tagLookup[[string]$tagName]
if (-not $tag) { continue }
$days = $null
if ($tag.AgeLimitForRetention) {
try { $days = [int]([timespan]::Parse($tag.AgeLimitForRetention.ToString())).TotalDays } catch { }
}
$policyReport.Add([PSCustomObject]@{
RetentionPolicy = $policy.Name
IsDefaultPolicy = $policy.IsDefault
TagName = $tag.Name
TagType = $tag.Type
RetentionAction = $tag.RetentionAction
AgeLimitDays = $days
RetentionEnabled = $tag.RetentionEnabled
})
if ($tag.RetentionAction -eq 'MoveToArchive' -and $tag.RetentionEnabled) {
$moveRules.Add("$($tag.Name) ($days days)")
}
}
$policyRules[$policy.Name] = ($moveRules -join '; ')
}
Write-Step "Retention lookup complete ($(@($policyRules.Keys).Count) policies, $($policyReport.Count) policy/tag links)." 'Green'
}
catch {
Write-Warning "Could not read retention policies/tags (missing permission?): $($_.Exception.Message)"
}
# --------------------------------------------------------------------------------------------
# Mailboxes
# User / Shared mailboxes : Get-ExoMailbox (fast REST cmdlet, explicit property list)
# Microsoft 365 Groups : Get-Mailbox -GroupMailbox (Get-ExoMailbox does not list group mailboxes)
# --------------------------------------------------------------------------------------------
$properties = @(
'DisplayName', 'UserPrincipalName', 'PrimarySmtpAddress', 'RecipientTypeDetails',
'ExternalDirectoryObjectId', 'ExchangeGuid', 'WhenCreated', 'IsInactiveMailbox',
# Archive
'ArchiveStatus', 'ArchiveState', 'ArchiveName', 'ArchiveGuid',
'ArchiveQuota', 'ArchiveWarningQuota', 'AutoExpandingArchiveEnabled',
# Retention
'RetentionPolicy', 'ElcProcessingDisabled', 'RetentionHoldEnabled',
'StartDateForRetentionHold', 'EndDateForRetentionHold', 'RetentionComment', 'RetentionUrl',
# Holds / recovery
'LitigationHoldEnabled', 'LitigationHoldDate', 'LitigationHoldOwner', 'LitigationHoldDuration',
'InPlaceHolds', 'ComplianceTagHoldApplied', 'DelayHoldApplied',
'SingleItemRecoveryEnabled', 'RetainDeletedItemsFor'
) + (1..15 | ForEach-Object { "CustomAttribute$_" }) + (1..5 | ForEach-Object { "ExtensionCustomAttribute$_" })
Write-Phase 4 'Retrieving mailboxes'
$mailboxes = [System.Collections.Generic.List[object]]::new()
$exoTypes = @($MailboxType | Where-Object { $_ -ne 'GroupMailbox' })
if ($exoTypes.Count -gt 0) {
Write-Step "Running Get-ExoMailbox for: $($exoTypes -join ', ') (can take a few minutes on large tenants)..." 'Yellow'
$getParams = @{
RecipientTypeDetails = $exoTypes
ResultSize = 'Unlimited'
Properties = $properties
}
if ($IncludeInactiveMailboxes) { $getParams['IncludeInactiveMailbox'] = $true }
# If this tenant/module rejects a property, drop it and retry instead of failing the whole run
$exoResult = $null
$retries = 0
while ($null -eq $exoResult -and $retries -le 5) {
try { $exoResult = @(Get-ExoMailbox @getParams -ErrorAction Stop) }
catch {
if ($_.Exception.Message -match 'InvalidProperties\s*=\s*([^\r\n]+)') {
$bad = @($Matches[1].Split(',') | ForEach-Object { $_.Trim().TrimEnd('.') } | Where-Object { $_ })
Write-Warning "Skipping properties not supported here: $($bad -join ', ')"
$getParams['Properties'] = @($getParams['Properties'] | Where-Object { $_ -notin $bad })
$retries++
}
else { throw }
}
}
if ($null -eq $exoResult) { throw 'Could not retrieve mailboxes with Get-ExoMailbox.' }
$mailboxes.AddRange([object[]]$exoResult)
Write-Step "Get-ExoMailbox returned $($exoResult.Count) mailboxes." 'Green'
}
if ($MailboxType -contains 'GroupMailbox') {
Write-Step 'Running Get-Mailbox -GroupMailbox for Microsoft 365 Groups (can be slow)...' 'Yellow'
try {
$groupResult = @(Get-Mailbox -GroupMailbox -ResultSize Unlimited -ErrorAction Stop)
if ($groupResult.Count -gt 0) { $mailboxes.AddRange([object[]]$groupResult) }
Write-Step "Get-Mailbox -GroupMailbox returned $($groupResult.Count) group mailboxes." 'Green'
}
catch { Write-Warning "Could not retrieve Microsoft 365 Group mailboxes: $($_.Exception.Message)" }
}
$total = $mailboxes.Count
Write-Step "Total mailboxes to process: $total" 'Green'
Write-Phase 5 "Collecting archive, retention, hold, statistics and custom attribute data ($total mailboxes)"
$report = [System.Collections.Generic.List[object]]::new()
$counter = 0
foreach ($mailbox in $mailboxes) {
$counter++
Write-Progress -Activity 'Processing mailboxes' `
-Status "$counter of $total - $($mailbox.DisplayName)" `
-PercentComplete ([math]::Round(($counter / [math]::Max($total, 1)) * 100, 1))
$guid = [guid]$mailbox.ExchangeGuid
$statsError = $null
$stats = $null
$archiveStats = $null
$hasArchive = ($mailbox.ArchiveStatus -eq 'Active')
# Live status line for this mailbox (finished at the end of the iteration)
Write-Host ("[{0}] [{1}/{2}] {3} ({4}) ... " -f (Get-Date -Format 'HH:mm:ss'), $counter, $total, $mailbox.DisplayName, $mailbox.RecipientTypeDetails) -NoNewline
if ($SkipStatistics) { Write-Host 'statistics skipped; ' -NoNewline -ForegroundColor DarkGray }
# --- Statistics (primary + archive) ---
if (-not $SkipStatistics) {
try {
$stats = Invoke-WithRetry {
Get-StatsSafe -Guid $guid `
-Properties ItemCount, TotalItemSize, TotalDeletedItemSize, LastLogonTime, LastUserActionTime, LastInteractionTime
}
}
catch { $statsError = "Primary: $($_.Exception.Message)"; Write-Host 'mailbox stats FAILED; ' -NoNewline -ForegroundColor Red }
if ($stats) { Write-Host 'mailbox stats OK; ' -NoNewline -ForegroundColor Green }
if ($hasArchive) {
try {
$archiveStats = Invoke-WithRetry {
Get-StatsSafe -Guid $guid -Archive `
-Properties ItemCount, TotalItemSize, TotalDeletedItemSize
}
}
catch {
$statsError = (@($statsError, "Archive: $($_.Exception.Message)") | Where-Object { $_ }) -join ' | '
Write-Host 'archive stats FAILED; ' -NoNewline -ForegroundColor Red
}
if ($archiveStats) { Write-Host 'archive stats OK; ' -NoNewline -ForegroundColor Green }
}
}
# --- Holds / Single Item Recovery ---
$inPlaceHolds = @($mailbox.InPlaceHolds | Where-Object { $_ })
$litigationHold = [bool]$mailbox.LitigationHoldEnabled
$complianceTagHold = [bool]$mailbox.ComplianceTagHoldApplied
$hasActiveHold = $litigationHold -or ($inPlaceHolds.Count -gt 0) -or $complianceTagHold
$sirStatus = if ($hasActiveHold) { 'Enabled (enforced by hold)' }
elseif ($mailbox.SingleItemRecoveryEnabled) { "Enabled (retain $($mailbox.RetainDeletedItemsFor))" }
else { 'Disabled' }
# --- Build the row (ordered so the CSV columns stay in a logical order) ---
$row = [ordered]@{
DisplayName = $mailbox.DisplayName
UserPrincipalName = $mailbox.UserPrincipalName
PrimarySMTP = $mailbox.PrimarySmtpAddress
MailboxType = $mailbox.RecipientTypeDetails
IsInactiveMailbox = $mailbox.IsInactiveMailbox
CreationDate = Format-DateValue $mailbox.WhenCreated
ExternalDirectoryObjectId = $mailbox.ExternalDirectoryObjectId
# Archive
ArchiveStatus = $mailbox.ArchiveStatus
ArchiveEnabled = if ($hasArchive) { 'Enabled' } else { 'Disabled' }
ArchiveState = $mailbox.ArchiveState
ArchiveName = $mailbox.ArchiveName
ArchiveGuid = $mailbox.ArchiveGuid
AutoExpandingArchive = $mailbox.AutoExpandingArchiveEnabled
ArchiveQuotaGB = ConvertTo-GB $mailbox.ArchiveQuota
ArchiveWarningQuotaGB = ConvertTo-GB $mailbox.ArchiveWarningQuota
ArchiveItemCount = if ($archiveStats) { $archiveStats.ItemCount } else { $null }
ArchiveSizeGB = if ($archiveStats) { ConvertTo-GB $archiveStats.TotalItemSize } else { $null }
ArchiveDeletedItemsSizeGB = if ($archiveStats) { ConvertTo-GB $archiveStats.TotalDeletedItemSize } else { $null }
# Retention
RetentionPolicy = $mailbox.RetentionPolicy
MoveToArchiveRules = if ($mailbox.RetentionPolicy) { $policyRules[[string]$mailbox.RetentionPolicy] } else { $null }
ElcProcessingDisabled = $mailbox.ElcProcessingDisabled
RetentionHoldEnabled = $mailbox.RetentionHoldEnabled
RetentionHoldStart = Format-DateValue $mailbox.StartDateForRetentionHold
RetentionHoldEnd = Format-DateValue $mailbox.EndDateForRetentionHold
RetentionComment = $mailbox.RetentionComment
RetentionUrl = $mailbox.RetentionUrl
# Holds & Single Item Recovery
HasActiveHold = $hasActiveHold
LitigationHoldEnabled = $litigationHold
LitigationHoldDate = Format-DateValue $mailbox.LitigationHoldDate
LitigationHoldOwner = $mailbox.LitigationHoldOwner
LitigationHoldDuration = $mailbox.LitigationHoldDuration
InPlaceHoldsCount = $inPlaceHolds.Count
InPlaceHolds = ($inPlaceHolds -join ', ')
ComplianceTagHoldApplied = $complianceTagHold
DelayHoldApplied = $mailbox.DelayHoldApplied
SingleItemRecoveryEnabled = $mailbox.SingleItemRecoveryEnabled
RetainDeletedItemsFor = $mailbox.RetainDeletedItemsFor
SingleItemRecoveryStatus = $sirStatus
# Primary mailbox statistics
ItemCount = if ($stats) { $stats.ItemCount } else { $null }
TotalItemSizeGB = if ($stats) { ConvertTo-GB $stats.TotalItemSize } else { $null }
DeletedItemsSizeGB = if ($stats) { ConvertTo-GB $stats.TotalDeletedItemSize } else { $null }
LastLogonTime = if ($stats) { Format-DateValue $stats.LastLogonTime } else { $null }
LastUserActionTime = if ($stats) { Format-DateValue $stats.LastUserActionTime } else { $null }
LastInteractionTime = if ($stats) { Format-DateValue $stats.LastInteractionTime } else { $null }
}
# Custom attributes 1 - 15 and extension attributes 1 - 5
foreach ($i in 1..15) { $row["CustomAttribute$i"] = $mailbox."CustomAttribute$i" }
foreach ($i in 1..5) { $row["ExtensionCustomAttribute$i"] = $mailbox."ExtensionCustomAttribute$i" }
$row['StatisticsError'] = $statsError
$report.Add([PSCustomObject]$row)
$sizeText = if ($stats) { " | size $(ConvertTo-GB $stats.TotalItemSize) GB" } else { '' }
Write-Host ("done | archive: {0}{1}" -f $(if ($hasArchive) { 'yes' } else { 'no' }), $sizeText) -ForegroundColor Gray
}
Write-Progress -Activity 'Processing mailboxes' -Completed
# --------------------------------------------------------------------------------------------
# Export CSV
# --------------------------------------------------------------------------------------------
Write-Phase 6 'Exporting the CSV report'
Write-Step "Collection finished: $($report.Count) mailboxes in the report." 'Green'
if (-not (Test-Path $OutputDirectory)) {
Write-Step "Creating output folder $OutputDirectory..." 'Yellow'
New-Item -ItemType Directory -Path $OutputDirectory -Force | Out-Null
}
$timestamp = Get-Date -Format 'yyyy-MM-dd_HH-mm'
$outputPath = Join-Path $OutputDirectory "MailboxArchiving_Report_$timestamp.csv"
Write-Step "Writing mailbox report ($($report.Count) rows)..." 'Yellow'
$report | Export-Csv -Path $outputPath -NoTypeInformation -Encoding UTF8
Write-Step 'Mailbox report written.' 'Green'
if ($policyReport.Count -gt 0) {
$policyPath = Join-Path $OutputDirectory "RetentionPolicies_Report_$timestamp.csv"
Write-Step "Writing retention policy report ($($policyReport.Count) rows)..." 'Yellow'
$policyReport | Export-Csv -Path $policyPath -NoTypeInformation -Encoding UTF8
Write-Step 'Retention policy report written.' 'Green'
}
Write-Host "`nMailbox report : $outputPath" -ForegroundColor Cyan
if ($policyPath) { Write-Host "Policy report : $policyPath" -ForegroundColor Cyan }
# --------------------------------------------------------------------------------------------
# Summary
# --------------------------------------------------------------------------------------------
Write-Phase 7 'Summary'
Write-Host "Total mailboxes processed : $($report.Count)"
$report | Group-Object MailboxType | ForEach-Object { Write-Host (" {0,-28}: {1}" -f $_.Name, $_.Count) }
Write-Host "Archive enabled : $(@($report | Where-Object { $_.ArchiveEnabled -eq 'Enabled' }).Count)"
Write-Host "Archive disabled : $(@($report | Where-Object { $_.ArchiveEnabled -eq 'Disabled' }).Count)"
Write-Host "Auto-expanding archive : $(@($report | Where-Object { $_.AutoExpandingArchive -eq $true }).Count)"
Write-Host "With active holds : $(@($report | Where-Object { $_.HasActiveHold }).Count)"
Write-Host "Retention hold enabled : $(@($report | Where-Object { $_.RetentionHoldEnabled }).Count)"
Write-Host "Single Item Recovery enabled : $(@($report | Where-Object { $_.SingleItemRecoveryStatus -like 'Enabled*' }).Count)"
Write-Host "Inactive mailboxes : $(@($report | Where-Object { $_.IsInactiveMailbox }).Count)"
$withErrors = @($report | Where-Object { $_.StatisticsError }).Count
if ($withErrors -gt 0) { Write-Host "Mailboxes with statistics errors: $withErrors (see StatisticsError column)" -ForegroundColor Yellow }
Write-Step ("Finished in {0}." -f ((Get-Date) - $script:StartTime).ToString('hh\:mm\:ss')) 'Green'
}
finally {
if ($weConnected) {
Write-Step 'Disconnecting from Exchange Online...' 'Yellow'
Disconnect-ExchangeOnline -Confirm:$false -ErrorAction SilentlyContinue
Write-Step 'Disconnected.' 'Gray'
}
}