Public/NC.Statistics.ps1

#Requires -Version 5.0
using namespace System.Management.Automation

# Nebula.Core: Statistics ===========================================================================================================================

function Export-MboxStatistics {
    <#
    .SYNOPSIS
        Exports mailbox (and archive) size/quota statistics.
    .DESCRIPTION
        Ensures an Exchange Online session, retrieves either all mailboxes or a single identity,
        calculates usage/quota information (optionally rounding quotas), and writes to CSV or
        returns objects to the pipeline.
    .PARAMETER UserPrincipalName
        Optional single mailbox identity. When omitted, exports all mailboxes to CSV.
    .PARAMETER CsvFolder
        Destination folder for the CSV file (defaults to current directory when exporting all mailboxes).
    .PARAMETER Round
        Round quota values up to the nearest integer GB.
    .PARAMETER BatchSize
        Number of processed mailboxes before flushing partial CSV output (defaults to 25).
    #>

    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('User', 'Identity')]
        [string]$UserPrincipalName,
        [string]$CsvFolder,
        [switch]$Round,
        [ValidateRange(1, 500)]
        [int]$BatchSize = 25
    )

    Set-ProgressAndInfoPreferences
    try {
        if (-not (Test-EOLConnection)) {
            Add-EmptyLine
            Write-NCMessage "Can't connect or use Microsoft Exchange Online Management module. Please check logs." -Level ERROR
            return
        }

        $exportAll = [string]::IsNullOrWhiteSpace($UserPrincipalName)
        $mailboxes = @()

        try {
            if ($exportAll) {
                $mailboxes = Get-Mailbox -ResultSize Unlimited -WarningAction SilentlyContinue
            }
            else {
                $mailboxes = @(Get-Mailbox -Identity $UserPrincipalName -ErrorAction Stop)
            }
        }
        catch {
            Write-NCMessage "Failed to retrieve mailbox information: $($_.Exception.Message)" -Level ERROR
            return
        }

        if (-not $mailboxes -or $mailboxes.Count -eq 0) {
            Write-NCMessage "No mailboxes found matching the provided criteria." -Level WARNING
            return
        }

        $folder = if ($CsvFolder) { Test-Folder $CsvFolder } else { Test-Folder $null }
        $statsBuffer = New-Object System.Collections.Generic.List[object]
        $processedCount = 0
        $totalMailboxes = $mailboxes.Count
        $writeToCsv = $exportAll
        $csvPath = $null
        $csvInitialized = $false

        if ($writeToCsv) {
            $csvPath = New-File("$($folder)\$((Get-Date -Format $NCVars.DateTimeString_CSV))_M365-MailboxStatistics.csv")
            Write-NCMessage "Saving report to $csvPath" -Level DEBUG
        }

        foreach ($mailbox in $mailboxes) {
            $processedCount++
            $percentComplete = (($processedCount / $totalMailboxes) * 100)
            Write-Progress -Activity "Processing $($mailbox.DisplayName)" -Status "$processedCount of $totalMailboxes ($($percentComplete.ToString('0.00'))%)" -PercentComplete $percentComplete

            $stats = Get-MailboxStatisticsSafe -Identity $mailbox.UserPrincipalName
            $mailboxSizeGb = if ($stats) { Convert-MbxSizeToGB -SizeObject $stats.TotalItemSize } else { "Error" }

            $hasArchive = ($mailbox.ArchiveStatus -eq 'Active') -or ($mailbox.ArchiveGuid -and $mailbox.ArchiveGuid -ne [guid]::Empty)
            $archiveSize = $null
            if ($hasArchive) {
                $archiveStats = Get-MailboxStatisticsSafe -Identity $mailbox.UserPrincipalName -Archive
                $archiveSize = if ($archiveStats) { Convert-MbxSizeToGB -SizeObject $archiveStats.TotalItemSize } else { "Error" }
            }

            $record = [pscustomobject][ordered]@{
                UserName                     = $mailbox.DisplayName
                ServerName                   = $mailbox.ServerName
                Database                     = $mailbox.Database
                RecipientTypeDetails         = $mailbox.RecipientTypeDetails
                PrimarySmtpAddress           = $mailbox.PrimarySmtpAddress
                "Mailbox Size (GB)"          = $mailboxSizeGb
                "Issue Warning Quota (GB)"   = Resolve-MbxQuotaValue -RawValue $mailbox.IssueWarningQuota -Round:$Round
                "Prohibit Send Quota (GB)"   = Resolve-MbxQuotaValue -RawValue $mailbox.ProhibitSendQuota -Round:$Round
                "Archive Database"           = if ($mailbox.ArchiveDatabase) { $mailbox.ArchiveDatabase } else { $null }
                "Archive Name"               = if ($hasArchive) { $mailbox.ArchiveName } else { $null }
                "Archive State"              = if ($hasArchive) { $mailbox.ArchiveState } else { $null }
                "Archive Mailbox Size (GB)"  = $archiveSize
                "Archive Warning Quota (GB)" = if ($hasArchive) {
                    Resolve-MbxQuotaValue -RawValue $mailbox.ArchiveWarningQuota -Round:$Round
                }
                else { $null }
                "Archive Quota (GB)"         = if ($hasArchive) {
                    Resolve-MbxQuotaValue -RawValue $mailbox.ArchiveQuota -Round:$Round
                }
                else { $null }
                AutoExpandingArchiveEnabled  = $mailbox.AutoExpandingArchiveEnabled
            }

            $statsBuffer.Add($record) | Out-Null

            if ($writeToCsv -and (($processedCount % $BatchSize) -eq 0)) {
                if ($csvInitialized) {
                    $statsBuffer | Export-CSV -LiteralPath $csvPath -NoTypeInformation -Encoding $NCVars.CSV_Encoding -Delimiter $($NCVars.CSV_DefaultLimiter) -Append
                }
                else {
                    $statsBuffer | Export-CSV -LiteralPath $csvPath -NoTypeInformation -Encoding $NCVars.CSV_Encoding -Delimiter $($NCVars.CSV_DefaultLimiter)
                    $csvInitialized = $true
                }
                Write-NCMessage "Processed $processedCount / $totalMailboxes mailboxes, flushed batch to CSV." -Level VERBOSE
                $statsBuffer.Clear()
            }
        }

        if ($writeToCsv) {
            if ($statsBuffer.Count -gt 0) {
                if ($csvInitialized) {
                    $statsBuffer | Export-CSV -LiteralPath $csvPath -NoTypeInformation -Encoding $NCVars.CSV_Encoding -Delimiter $($NCVars.CSV_DefaultLimiter) -Append
                }
                else {
                    $statsBuffer | Export-CSV -LiteralPath $csvPath -NoTypeInformation -Encoding $NCVars.CSV_Encoding -Delimiter $($NCVars.CSV_DefaultLimiter)
                }
            }

            Write-NCMessage "Mailbox statistics exported to $csvPath." -Level SUCCESS
        }
        else {
            $statsBuffer
        }

        Write-Progress -Activity "Export complete" -Completed
    }
    finally {
        Restore-ProgressAndInfoPreferences
    }
}

function Get-MboxStatistics {
    <#
    .SYNOPSIS
        Returns simplified mailbox statistics.
    .DESCRIPTION
        Ensures an Exchange Online session, retrieves mailbox statistics and returns
        a concise set of key fields (size, quotas, basic usage info, latest message trace,
        and oldest mailbox item metadata).
    .PARAMETER UserPrincipalName
        Optional single mailbox identity. When omitted, returns all mailboxes.
    .PARAMETER IncludeArchive
        When present, includes archive size and archive usage percentage (if available).
    .PARAMETER IncludeMessageActivity
        When present, includes latest message trace info and oldest mailbox item metadata
        (LastReceived, LastSent, OldestItemReceivedDate, OldestItemFolderPath).
    .PARAMETER Round
        Round quota values up to the nearest integer GB (default: $true).
    #>

    [CmdletBinding()]
    param(
        [Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('User', 'Identity')]
        [string]$UserPrincipalName,
        [switch]$IncludeArchive,
        [switch]$IncludeMessageActivity,
        [bool]$Round = $true
    )

    begin {
        Set-ProgressAndInfoPreferences
        $pipelineUpns = [System.Collections.Generic.List[string]]::new()
    }

    process {
        if (-not [string]::IsNullOrWhiteSpace($UserPrincipalName)) {
            [void]$pipelineUpns.Add($UserPrincipalName)
        }
    }

    end {
        try {
            if (-not (Test-EOLConnection)) {
                Add-EmptyLine
                Write-NCMessage "Can't connect or use Microsoft Exchange Online Management module. Please check logs." -Level ERROR
                return
            }

            $mailboxes = @()
            try {
                if ($pipelineUpns.Count -eq 0) {
                    $mailboxes = Get-Mailbox -ResultSize Unlimited -WarningAction SilentlyContinue
                }
                else {
                    foreach ($upn in ($pipelineUpns | Select-Object -Unique)) {
                        try {
                            $mailboxes += @(Get-Mailbox -Identity $upn -ErrorAction Stop)
                        }
                        catch {
                            Write-NCMessage "Mailbox not found for '$upn'. Skipping. $($_.Exception.Message)" -Level WARNING
                        }
                    }
                }
            }
            catch {
                Write-NCMessage "Failed to retrieve mailbox information: $($_.Exception.Message)" -Level ERROR
                return
            }

            if (-not $mailboxes -or $mailboxes.Count -eq 0) {
                Write-NCMessage "No mailboxes found matching the provided criteria." -Level WARNING
                return
            }

            $processedCount = 0
            $totalMailboxes = $mailboxes.Count

            foreach ($mailbox in $mailboxes) {
                $processedCount++
                $percentComplete = (($processedCount / $totalMailboxes) * 100)
                Write-Progress -Activity "Processing $($mailbox.DisplayName)" -Status "$processedCount of $totalMailboxes ($($percentComplete.ToString('0.00'))%)" -PercentComplete $percentComplete

                $stats = Get-MailboxStatisticsSafe -Identity $mailbox.UserPrincipalName
                if (-not $stats) {
                    continue
                }

                $mailboxSizeGb = Convert-MbxSizeToGB -SizeObject $stats.TotalItemSize
                $prohibitSendQuota = Resolve-MbxQuotaValue -RawValue $mailbox.ProhibitSendQuota -Round:$Round
                $warningQuota = Resolve-MbxQuotaValue -RawValue $mailbox.IssueWarningQuota -Round:$Round
                $oldestItemReceivedDate = $null
                $oldestItemFolderPath = $null
                $lastTrace = $null
                if ($IncludeMessageActivity) {
                    $lastTrace = Get-MboxLastMessageTrace -SourceMailbox $mailbox.UserPrincipalName

                    try {
                        $oldestItem = Get-MailboxFolderStatistics -Identity $mailbox.UserPrincipalName -IncludeOldestAndNewestItems -ErrorAction Stop |
                            Where-Object { $_.OldestItemReceivedDate -ne $null } |
                            Sort-Object -Property OldestItemReceivedDate |
                            Select-Object -First 1

                        if ($oldestItem) {
                            $oldestItemReceivedDate = $oldestItem.OldestItemReceivedDate
                            $oldestItemFolderPath = $oldestItem.FolderPath
                        }
                    }
                    catch {
                        Write-NCMessage ("Unable to retrieve oldest mailbox item details for '{0}'. {1}" -f $mailbox.PrimarySmtpAddress, $_.Exception.Message) -Level WARNING
                    }
                }

                $percentUsed = $null
                if ($prohibitSendQuota -is [double] -and $prohibitSendQuota -gt 0) {
                    $percentUsed = [Math]::Round(($mailboxSizeGb / $prohibitSendQuota) * 100, 2)
                }

                $archiveSize = $null
                $archivePercentUsed = $null
                $hasArchive = ($mailbox.ArchiveStatus -eq 'Active') -or ($mailbox.ArchiveGuid -and $mailbox.ArchiveGuid -ne [guid]::Empty)
                if ($IncludeArchive -and $hasArchive) {
                    $archiveStats = Get-MailboxStatisticsSafe -Identity $mailbox.UserPrincipalName -Archive
                    if ($archiveStats) {
                        $archiveSize = Convert-MbxSizeToGB -SizeObject $archiveStats.TotalItemSize
                        $archiveQuota = Resolve-MbxQuotaValue -RawValue $mailbox.ArchiveQuota -Round:$Round
                        if ($archiveQuota -is [double] -and $archiveQuota -gt 0) {
                            $archivePercentUsed = [Math]::Round(($archiveSize / $archiveQuota) * 100, 2)
                        }
                    }
                }

                $mailboxTypeDetail = if ($stats.PSObject.Properties.Match('MailboxTypeDetail').Count -gt 0) {
                    $stats.MailboxTypeDetail
                }
                elseif ($stats.PSObject.Properties.Match('RecipientTypeDetails').Count -gt 0) {
                    $stats.RecipientTypeDetails
                }
                else {
                    $mailbox.RecipientTypeDetails
                }

                $mailboxCreated = $null
                if ($mailbox.PSObject.Properties.Match('WhenCreatedUTC').Count -gt 0 -and $mailbox.WhenCreatedUTC) {
                    $mailboxCreated = $mailbox.WhenCreatedUTC
                }
                elseif ($mailbox.PSObject.Properties.Match('WhenCreated').Count -gt 0 -and $mailbox.WhenCreated) {
                    $mailboxCreated = $mailbox.WhenCreated
                }
                elseif ($stats.PSObject.Properties.Match('WhenMailboxCreated').Count -gt 0 -and $stats.WhenMailboxCreated) {
                    $mailboxCreated = $stats.WhenMailboxCreated
                }
                elseif ($stats.PSObject.Properties.Match('DateCreated').Count -gt 0 -and $stats.DateCreated) {
                    $mailboxCreated = $stats.DateCreated
                }
                elseif ($stats.PSObject.Properties.Match('Created').Count -gt 0 -and $stats.Created) {
                    $mailboxCreated = $stats.Created
                }

                $record = [ordered]@{
                    DisplayName          = $mailbox.DisplayName
                    UserPrincipalName    = $mailbox.UserPrincipalName
                    PrimarySmtpAddress   = $mailbox.PrimarySmtpAddress
                    MailboxTypeDetail    = $mailboxTypeDetail
                    ArchiveEnabled       = [bool]$hasArchive
                    MailboxSizeGB        = $mailboxSizeGb
                    ItemCount            = $stats.ItemCount
                    MailboxCreated       = $mailboxCreated
                    LastLogonTime        = $stats.LastLogonTime
                    WarningQuotaGB       = $warningQuota
                    ProhibitSendQuotaGB  = $prohibitSendQuota
                    PercentUsed          = $percentUsed
                }

                if ($IncludeMessageActivity) {
                    $record.LastReceived = if ($lastTrace) { $lastTrace.LastReceived } else { $null }
                    $record.LastSent = if ($lastTrace) { $lastTrace.LastSent } else { $null }
                    $record.OldestItemReceivedDate = $oldestItemReceivedDate
                    $record.OldestItemFolderPath = $oldestItemFolderPath
                }

                if ($IncludeArchive) {
                    $record.ArchiveSizeGB = $archiveSize
                    $record.ArchivePercentUsed = $archivePercentUsed
                }

                [pscustomobject]$record
            }

            Write-Progress -Activity "Export complete" -Completed
        }
        finally {
            Restore-ProgressAndInfoPreferences
        }
    }
}