Public/NC.Mailboxes.ps1

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

# Nebula.Core: Mailboxes helpers ====================================================================================================================

function Add-MboxAlias {
    <#
    .SYNOPSIS
        Adds a new alias to a mailbox or mail-enabled recipient.
    .DESCRIPTION
        Validates Exchange Online connectivity, checks the recipient, and adds the alias when it does not already exist.
    .PARAMETER SourceMailbox
        Mailbox or recipient identity. Accepts pipeline input.
    .PARAMETER MailboxAlias
        Alias to add (SMTP address).
    .EXAMPLE
        Add-MboxAlias -SourceMailbox user@contoso.com -MailboxAlias alias@contoso.com
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('Identity')]
        [string]$SourceMailbox,
        [Parameter(Mandatory)]
        [string]$MailboxAlias
    )

    begin { Set-ProgressAndInfoPreferences }

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

        try {
            $recipient = Get-Recipient -Identity $SourceMailbox -ErrorAction Stop
        }
        catch {
            Write-NCMessage "Mailbox or recipient '$SourceMailbox' not found. $($_.Exception.Message)" -Level ERROR
            return
        }

        $normalizedAlias = $MailboxAlias.ToLowerInvariant()
        $existingAliases = $recipient.EmailAddresses | ForEach-Object { ($_ -replace '^smtp:', '').ToLowerInvariant() }
        if ($existingAliases -contains $normalizedAlias) {
            Write-NCMessage ("Alias '{0}' already exists for '{1}'. No action taken." -f $MailboxAlias, $recipient.PrimarySmtpAddress) -Level WARNING
            return
        }

        try {
            switch ($recipient.RecipientTypeDetails) {
                'MailContact' { Set-MailContact -Identity $recipient.Identity -EmailAddresses @{ add = $MailboxAlias } -ErrorAction Stop }
                'MailUser' { Set-MailUser -Identity $recipient.Identity -EmailAddresses @{ add = $MailboxAlias } -ErrorAction Stop }
                {($_ -eq 'MailUniversalDistributionGroup') -or ($_ -eq 'DynamicDistributionGroup') -or ($_ -eq 'MailUniversalSecurityGroup')} {
                    Set-DistributionGroup -Identity $recipient.Identity -EmailAddresses @{ add = $MailboxAlias } -ErrorAction Stop
                }
                default { Set-Mailbox -Identity $recipient.Identity -EmailAddresses @{ add = $MailboxAlias } -ErrorAction Stop }
            }

            Write-NCMessage ("Alias '{0}' added to {1}." -f $MailboxAlias, $recipient.PrimarySmtpAddress) -Level SUCCESS
        }
        catch {
            Write-NCMessage "Unable to add alias '$MailboxAlias' to '$($recipient.PrimarySmtpAddress)'. $($_.Exception.Message)" -Level ERROR
            return
        }

        Get-MboxAlias -SourceMailbox $recipient.PrimarySmtpAddress
    }

    end { Restore-ProgressAndInfoPreferences }
}

function Add-MboxPermission {
    <#
    .SYNOPSIS
        Grants mailbox permissions to users.
    .DESCRIPTION
        Ensures Exchange Online connectivity, validates target and user mailboxes, and assigns FullAccess,
        SendAs, SendOnBehalfTo, or both (All). Returns the applied permissions.
    .PARAMETER SourceMailbox
        Mailbox identity to update.
    .PARAMETER UserMailbox
        One or more users to grant permissions to. Accepts pipeline input.
    .PARAMETER AccessRights
        Permission type: All, FullAccess, SendAs, SendOnBehalfTo. Defaults to All (FullAccess + SendAs).
    .PARAMETER AutoMapping
        Enable Outlook automapping when granting FullAccess.
    .PARAMETER PassThru
        When specified, emits detailed permission objects to the pipeline.
    .EXAMPLE
        Add-MboxPermission -SourceMailbox user@contoso.com -UserMailbox user@contoso.com -AccessRights FullAccess -AutoMapping
    .EXAMPLE
        Add-MboxPermission -SourceMailbox user@contoso.com -UserMailbox user@contoso.com -PassThru
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipelineByPropertyName = $true)]
        [Alias('Identity')]
        [string]$SourceMailbox,
        [Parameter(Mandatory, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [string[]]$UserMailbox,
        [ValidateSet('All', 'FullAccess', 'SendAs', 'SendOnBehalfTo')]
        [string]$AccessRights = 'All',
        [switch]$AutoMapping,
        [switch]$PassThru
    )

    begin { Set-ProgressAndInfoPreferences }

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

        try {
            $targetMailbox = Get-Mailbox -Identity $SourceMailbox -ErrorAction Stop
        }
        catch {
            Write-NCMessage "Mailbox '$SourceMailbox' not found. $($_.Exception.Message)" -Level ERROR
            return
        }

        foreach ($user in $UserMailbox) {
            try {
                $userObject = Get-User -Identity $user -ErrorAction Stop
            }
            catch {
                Add-EmptyLine
                Write-NCMessage "The mailbox '$user' does not exist. Please check the provided e-mail address." -Level ERROR
                continue
            }

            $userIdentity = $userObject.UserPrincipalName
            switch ($AccessRights) {
                'FullAccess' {
                    $existingPermission = Get-MailboxPermission -Identity $targetMailbox.PrimarySmtpAddress -User $userIdentity -ErrorAction SilentlyContinue | Where-Object { $_.AccessRights -contains 'FullAccess' -and -not $_.IsInherited }
                    if ($existingPermission) {
                        Write-NCMessage ("{0} already has FullAccess permission to {1}, skipping." -f $userIdentity, $targetMailbox.PrimarySmtpAddress) -Level WARNING
                        continue
                    }

                    $added = Add-MailboxPermission -Identity $targetMailbox.PrimarySmtpAddress -User $userIdentity -AccessRights FullAccess -AutoMapping:$AutoMapping.IsPresent -Confirm:$false
                    Write-NCMessage ("Added FullAccess for {0} on {1}." -f $userIdentity, $targetMailbox.PrimarySmtpAddress) -Level SUCCESS
                    if ($PassThru) {
                        [pscustomobject]@{
                            Identity     = $added.Identity
                            User         = $added.User
                            DisplayName  = $userObject.DisplayName
                            AccessRights = $added.AccessRights
                            IsInherited  = $added.IsInherited
                            Deny         = $added.Deny
                        }
                    }
                }
                'SendAs' {
                    $existingPermission = Get-RecipientPermission -Identity $targetMailbox.PrimarySmtpAddress -Trustee $userIdentity -ErrorAction SilentlyContinue | Where-Object { $_.AccessRights -contains 'SendAs' }
                    if ($existingPermission) {
                        Write-NCMessage ("{0} already has SendAs permission to {1}, skipping." -f $userIdentity, $targetMailbox.PrimarySmtpAddress) -Level WARNING
                        continue
                    }

                    $added = Add-RecipientPermission -Identity $targetMailbox.PrimarySmtpAddress -Trustee $userIdentity -AccessRights SendAs -Confirm:$false
                    Write-NCMessage ("Added SendAs for {0} on {1}." -f $userIdentity, $targetMailbox.PrimarySmtpAddress) -Level SUCCESS
                    if ($PassThru) {
                        [pscustomobject]@{
                            Identity          = $added.Identity
                            Trustee           = $added.Trustee
                            DisplayName       = $userObject.DisplayName
                            AccessControlType = $added.AccessControlType
                            AccessRights      = $added.AccessRights
                        }
                    }
                }
                'SendOnBehalfTo' {
                    $existingPermission = $targetMailbox.GrantSendOnBehalfTo | Where-Object { $_ -eq $userIdentity }
                    if ($existingPermission) {
                        Write-NCMessage ("{0} already has SendOnBehalfTo permission to {1}, skipping." -f $userIdentity, $targetMailbox.PrimarySmtpAddress) -Level WARNING
                        continue
                    }

                    Set-Mailbox -Identity $targetMailbox.PrimarySmtpAddress -GrantSendOnBehalfTo @{ add = $userIdentity } -Confirm:$false | Out-Null
                    Write-NCMessage ("Added SendOnBehalfTo for {0} on {1}." -f $userIdentity, $targetMailbox.PrimarySmtpAddress) -Level SUCCESS
                    if ($PassThru) {
                        [pscustomobject]@{
                            Identity     = $targetMailbox.PrimarySmtpAddress
                            Trustee      = $userIdentity
                            DisplayName  = $userObject.DisplayName
                            AccessRights = 'SendOnBehalfTo'
                        }
                    }
                }
                'All' {
                    $created = @()

                    $existingFullAccess = Get-MailboxPermission -Identity $targetMailbox.PrimarySmtpAddress -User $userIdentity -ErrorAction SilentlyContinue | Where-Object { $_.AccessRights -contains 'FullAccess' -and -not $_.IsInherited }
                    if (-not $existingFullAccess) {
                        $added = Add-MailboxPermission -Identity $targetMailbox.PrimarySmtpAddress -User $userIdentity -AccessRights FullAccess -AutoMapping:$AutoMapping.IsPresent -Confirm:$false
                        Write-NCMessage ("Added FullAccess for {0} on {1}." -f $userIdentity, $targetMailbox.PrimarySmtpAddress) -Level SUCCESS
                        $created += [pscustomobject]@{
                            Identity     = $added.Identity
                            User         = $added.User
                            DisplayName  = $userObject.DisplayName
                            AccessRights = $added.AccessRights
                            IsInherited  = $added.IsInherited
                            Deny         = $added.Deny
                        }
                    }
                    else {
                        Write-NCMessage ("{0} already has FullAccess permission to {1}, skipping." -f $userIdentity, $targetMailbox.PrimarySmtpAddress) -Level WARNING
                    }

                    $existingSendAs = Get-RecipientPermission -Identity $targetMailbox.PrimarySmtpAddress -Trustee $userIdentity -ErrorAction SilentlyContinue | Where-Object { $_.AccessRights -contains 'SendAs' }
                    if (-not $existingSendAs) {
                        $addedSendAs = Add-RecipientPermission -Identity $targetMailbox.PrimarySmtpAddress -Trustee $userIdentity -AccessRights SendAs -Confirm:$false
                        Write-NCMessage ("Added SendAs for {0} on {1}." -f $userIdentity, $targetMailbox.PrimarySmtpAddress) -Level SUCCESS
                        $created += [pscustomobject]@{
                            Identity          = $addedSendAs.Identity
                            Trustee           = $addedSendAs.Trustee
                            DisplayName       = $userObject.DisplayName
                            AccessControlType = $addedSendAs.AccessControlType
                            AccessRights      = $addedSendAs.AccessRights
                        }
                    }
                    else {
                        Write-NCMessage ("{0} already has SendAs permission to {1}, skipping." -f $userIdentity, $targetMailbox.PrimarySmtpAddress) -Level WARNING
                    }

                    if ($PassThru) {
                        $created
                    }
                }
            }
        }
    }

    end { Restore-ProgressAndInfoPreferences }
}

function Export-MboxPermission {
    <#
    .SYNOPSIS
        Exports mailbox permissions for selected recipient types.
    .DESCRIPTION
        Gathers FullAccess, SendAs, and SendOnBehalfTo permissions for user, shared, room, or all mailboxes
        and writes them to a CSV report.
    .PARAMETER RecipientType
        Recipient type to analyze: User, Shared, Room, All.
    .PARAMETER CsvFolder
        Destination folder for the CSV file. Defaults to current directory.
    .PARAMETER BatchSize
        Number of processed mailboxes before flushing partial CSV output.
    .PARAMETER Resume
        Resume from the latest matching CSV in the target folder or from -CsvPath.
    .PARAMETER CsvPath
        Explicit CSV file to resume. When omitted, the most recent matching CSV in the target folder is used.
    .PARAMETER MaxConsecutiveErrors
        Stop after this many consecutive mailbox-level failures.
    .EXAMPLE
        Export-MboxPermission -RecipientType All -CsvFolder C:\Temp
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [ValidateSet('User', 'Shared', 'Room', 'All')]
        [string]$RecipientType,
        [string]$CsvFolder,
        [ValidateRange(1, 500)]
        [int]$BatchSize = 25,
        [switch]$Resume,
        [string]$CsvPath,
        [ValidateRange(1, 100)]
        [int]$MaxConsecutiveErrors = 5
    )

    begin {
        Set-ProgressAndInfoPreferences
        $permissions = [System.Collections.Generic.List[object]]::new()
        $processedSinceFlush = 0
        $consecutiveErrors = 0
        $aborted = $false
    }

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

        switch ($RecipientType) {
            'User' { $mailboxes = Get-Recipient -ResultSize Unlimited -WarningAction SilentlyContinue | Where-Object { $_.RecipientTypeDetails -eq 'UserMailbox' } }
            'Shared' { $mailboxes = Get-Recipient -ResultSize Unlimited -WarningAction SilentlyContinue | Where-Object { $_.RecipientTypeDetails -eq 'SharedMailbox' } }
            'Room' { $mailboxes = Get-Recipient -ResultSize Unlimited -WarningAction SilentlyContinue | Where-Object { $_.RecipientTypeDetails -eq 'RoomMailbox' } }
            'All' {
                Write-NCMessage "No recipient type specified, scanning User, Shared, and Room mailboxes." -Level WARNING
                $mailboxes = Get-Recipient -ResultSize Unlimited -WarningAction SilentlyContinue | Where-Object { $_.RecipientTypeDetails -in @('UserMailbox', 'SharedMailbox', 'RoomMailbox') }
            }
        }

        $normalizeText = {
            param($value)
            return Get-NormalizedText -Value $value
        }
        $buildPermissionKey = {
            param($row)

            $mailbox = & $normalizeText $row.'Mailbox Address'
            if (-not $mailbox) {
                $mailbox = & $normalizeText $row.Mailbox
            }
            $recipientType = & $normalizeText $row.'Recipient Type'
            return "{0}|{1}" -f $mailbox, $recipientType
        }
        $existingPermissionKeys = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
        $csvPathResolved = $null
        $folderForExport = Test-Folder $CsvFolder
        $defaultCsvPath = New-File "$folderForExport\$((Get-Date -Format $NCVars.DateTimeString_CSV))_M365-MboxPermissions-Report.csv"
        $csvPathResolved = $defaultCsvPath

        if ($Resume) {
            $resumePath = $null
            if (-not [string]::IsNullOrWhiteSpace($CsvPath)) {
                $resumePath = $CsvPath
            }
            else {
                $existingCsv = Get-ChildItem -LiteralPath $folderForExport -File -Filter "*_M365-MboxPermissions-Report.csv" |
                    Sort-Object LastWriteTime -Descending |
                    Select-Object -First 1
                if ($existingCsv) {
                    $resumePath = $existingCsv.FullName
                }
            }

            if ($resumePath) {
                $csvPathResolved = $resumePath
                if (Test-Path -LiteralPath $csvPathResolved) {
                    try {
                        foreach ($row in (Import-CSV -LiteralPath $csvPathResolved -Delimiter $NCVars.CSV_DefaultLimiter -ErrorAction Stop)) {
                            $null = $existingPermissionKeys.Add((& $buildPermissionKey $row))
                        }
                        Write-NCMessage ("Resuming mailbox permissions export from {0}; {1} row(s) already recorded." -f $csvPathResolved, $existingPermissionKeys.Count) -Level INFO
                    }
                    catch {
                        Write-NCMessage ("Unable to read existing CSV '{0}' for resume. {1}" -f $csvPathResolved, $_.Exception.Message) -Level WARNING
                        $existingPermissionKeys.Clear()
                        $csvPathResolved = $defaultCsvPath
                    }
                }
                else {
                    Write-NCMessage ("Resume requested for '{0}', but the file does not exist. Starting a new report at that path." -f $csvPathResolved) -Level INFO
                }
            }
            else {
                Write-NCMessage ("Resume requested, but no existing CSV was found. Starting a new report at {0}." -f $csvPathResolved) -Level INFO
            }
        }

        Write-NCMessage ("Mailbox permission export will flush every {0} mailbox(es). Resume: {1}. Stop after {2} consecutive error(s)." -f $BatchSize, $Resume.IsPresent, $MaxConsecutiveErrors) -Level INFO
        Write-NCMessage "Saving report to $csvPathResolved" -Level DEBUG

        $writeBuffer = {
            param([System.Collections.Generic.List[object]]$buffer)

            if ($buffer.Count -eq 0) {
                return
            }

            $exportRows = $buffer
            if ((Test-Path -LiteralPath $csvPathResolved) -and ((Get-Item -LiteralPath $csvPathResolved).Length -gt 0)) {
                $exportRows | Export-Csv -LiteralPath $csvPathResolved -NoTypeInformation -Encoding $NCVars.CSV_Encoding -Delimiter $NCVars.CSV_DefaultLimiter -Append
            }
            else {
                $exportRows | Export-Csv -LiteralPath $csvPathResolved -NoTypeInformation -Encoding $NCVars.CSV_Encoding -Delimiter $NCVars.CSV_DefaultLimiter
            }
            $buffer.Clear()
        }

        $counter = 0
        foreach ($mailbox in $mailboxes) {
            $counter++
            $Percentage = Get-NCProgressPercent -Current $counter -Total $mailboxes.Count
            Write-Progress -Activity "Processing $($mailbox.PrimarySmtpAddress)" -Status "$counter of $($mailboxes.Count) - $Percentage%" -PercentComplete $Percentage

            try {
                $exoMailbox = Get-EXOMailbox -Identity $mailbox.Identity -ErrorAction Stop
                $sendAs = Get-RecipientPermission -Identity $exoMailbox.PrimarySmtpAddress -AccessRights SendAs -ErrorAction Stop | Where-Object { $_.Trustee.ToString() -ne 'NT AUTHORITY\SELF' -and $_.Trustee.ToString() -notlike 'S-1-5*' } | ForEach-Object { $_.Trustee.ToString() }
                $fullAccess = Get-MailboxPermission -Identity $exoMailbox.PrimarySmtpAddress -ErrorAction Stop | Where-Object { $_.AccessRights -eq 'FullAccess' -and -not $_.IsInherited } | ForEach-Object { $_.User.ToString() }

                $row = [pscustomobject]@{
                    Mailbox           = $exoMailbox.DisplayName
                    'Mailbox Address' = $exoMailbox.PrimarySmtpAddress
                    'Recipient Type'  = $exoMailbox.RecipientTypeDetails
                    FullAccess        = ($fullAccess -join ', ')
                    SendAs            = ($sendAs -join ', ')
                    SendOnBehalfTo    = $exoMailbox.GrantSendOnBehalfTo -join ', '
                }
            }
            catch {
                Write-NCMessage "Failed to process mailbox '$($mailbox.PrimarySmtpAddress)': $($_.Exception.Message)" -Level ERROR
                $consecutiveErrors++
                if ($MaxConsecutiveErrors -gt 0 -and $consecutiveErrors -ge $MaxConsecutiveErrors) {
                    $aborted = $true
                    break
                }
                continue
            }

            if ($Resume -and $existingPermissionKeys.Contains((& $buildPermissionKey $row))) {
                continue
            }

            $null = $existingPermissionKeys.Add((& $buildPermissionKey $row))
            $permissions.Add($row) | Out-Null
            $consecutiveErrors = 0

            $processedSinceFlush++
            if ($BatchSize -gt 0 -and $permissions.Count -gt 0 -and $processedSinceFlush -ge $BatchSize) {
                & $writeBuffer $permissions
                $processedSinceFlush = 0
            }
        }
    }

    end {
        try {
            if ($permissions.Count -eq 0) {
                if ($Resume -and (Test-Path -LiteralPath $csvPathResolved) -and ((Get-Item -LiteralPath $csvPathResolved).Length -gt 0)) {
                    Write-NCMessage "No new mailbox permissions found. Existing CSV at $csvPathResolved already contains the requested rows." -Level INFO
                }
                else {
                    Write-NCMessage "No mailbox permissions were collected." -Level WARNING
                }
                return
            }

            & $writeBuffer $permissions
            if ($aborted) {
                Write-NCMessage "Mailbox permissions export stopped early. Partial data kept at $csvPathResolved." -Level ERROR
            }
            else {
                Write-NCMessage "Mailbox permissions exported to $csvPathResolved" -Level SUCCESS
            }
        }
        finally {
            Write-Progress -Activity "Processing mailbox permissions" -Completed
            Restore-ProgressAndInfoPreferences
        }
    }
}

function Get-MboxAlias {
    <#
    .SYNOPSIS
        Lists primary and secondary SMTP addresses for a recipient.
    .DESCRIPTION
        Ensures Exchange Online connectivity and returns aliases with a flag for the primary address.
        Use -Csv for single mailbox exports.
        Use -All or -Domain to export a tenant-wide or domain-scoped CSV report with one row per alias.
        CSV exports include DisplayName and Name, omit recipients that have only the primary address unless -IncludePrimaryOnly is used,
        and hide MOERA addresses unless -IncludeMoera is used.
    .PARAMETER SourceMailbox
        Mailbox or recipient identity. Accepts pipeline input.
    .PARAMETER Csv
        Export a single mailbox result set to CSV.
    .PARAMETER CsvFolder
        Destination folder for the CSV file. Defaults to current directory.
    .PARAMETER All
        Enumerate every non-guest recipient and export the aliases to CSV.
    .PARAMETER Domain
        Enumerate recipients whose addresses match the provided domain and export the aliases to CSV.
    .PARAMETER IncludePrimaryOnly
        Include recipients that have no secondary aliases in CSV exports.
    .PARAMETER IncludeMoera
        Include MOERA addresses in CSV exports.
    .PARAMETER BatchSize
        Number of processed recipients before flushing partial CSV output.
    .PARAMETER Resume
        Resume from the latest matching CSV in the target folder or from -CsvPath.
    .PARAMETER CsvPath
        Explicit CSV file to resume. When omitted, the most recent matching CSV in the target folder is used.
    .PARAMETER MaxConsecutiveErrors
        Stop after this many consecutive recipient-level failures.
    .EXAMPLE
        Get-MboxAlias -SourceMailbox user@contoso.com
    .EXAMPLE
        Get-MboxAlias user@contoso.com
    .EXAMPLE
        Get-MboxAlias -All -CsvFolder C:\Temp
    .EXAMPLE
        Get-MboxAlias -SourceMailbox user@contoso.com -Csv -IncludeMoera -CsvFolder C:\Temp
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'Single')]
        [Alias('Identity')]
        [string]$SourceMailbox,
        [Parameter(ParameterSetName = 'Single')]
        [Parameter(ParameterSetName = 'All')]
        [Parameter(ParameterSetName = 'Domain')]
        [switch]$Csv,
        [Parameter(ParameterSetName = 'Single')]
        [Parameter(ParameterSetName = 'All')]
        [Parameter(ParameterSetName = 'Domain')]
        [string]$CsvFolder,
        [Parameter(ParameterSetName = 'Single')]
        [Parameter(ParameterSetName = 'All')]
        [Parameter(ParameterSetName = 'Domain')]
        [switch]$IncludePrimaryOnly,
        [Parameter(ParameterSetName = 'Single')]
        [Parameter(ParameterSetName = 'All')]
        [Parameter(ParameterSetName = 'Domain')]
        [switch]$IncludeMoera,
        [Parameter(ParameterSetName = 'Single')]
        [Parameter(ParameterSetName = 'All')]
        [Parameter(ParameterSetName = 'Domain')]
        [ValidateRange(1, 500)]
        [int]$BatchSize = 25,
        [Parameter(ParameterSetName = 'Single')]
        [Parameter(ParameterSetName = 'All')]
        [Parameter(ParameterSetName = 'Domain')]
        [switch]$Resume,
        [Parameter(ParameterSetName = 'Single')]
        [Parameter(ParameterSetName = 'All')]
        [Parameter(ParameterSetName = 'Domain')]
        [string]$CsvPath,
        [Parameter(ParameterSetName = 'Single')]
        [Parameter(ParameterSetName = 'All')]
        [Parameter(ParameterSetName = 'Domain')]
        [ValidateRange(1, 100)]
        [int]$MaxConsecutiveErrors = 5,
        [Parameter(Mandatory, ParameterSetName = 'All')]
        [switch]$All,
        [Parameter(Mandatory, ParameterSetName = 'Domain')]
        [string]$Domain
    )

    begin {
        Set-ProgressAndInfoPreferences
        $aliasRows = [System.Collections.Generic.List[object]]::new()
        $resolvedCsvFolder = $null
        $processedSinceFlush = 0
        $consecutiveErrors = 0
        $aborted = $false
    }

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

        if ($PSCmdlet.ParameterSetName -eq 'Single') {
            try {
                $recipient = Get-Recipient -Identity $SourceMailbox -ErrorAction Stop
            }
            catch {
                Write-NCMessage "Recipient '$SourceMailbox' not available or not found." -Level ERROR
                $consecutiveErrors++
                return
            }

            $recipients = @($recipient)
        }
        else {
            if ($All) {
                Write-NCMessage "No mailbox specified; scanning all recipients. This may take a while." -Level WARNING
                Write-Progress -Activity "Processing aliases" -Status "Retrieving recipient list ..." -PercentComplete 0
                $recipients = Get-Recipient -ResultSize Unlimited | Where-Object { $_.RecipientTypeDetails -ne 'GuestMailUser' }
                Write-Progress -Activity "Processing aliases" -Status "Recipient list loaded." -PercentComplete 5
            }
            else {
                $normalizedDomain = $Domain.Trim().ToLowerInvariant().TrimStart('@')
                if ([string]::IsNullOrWhiteSpace($normalizedDomain)) {
                    Write-NCMessage "Domain filter cannot be empty." -Level ERROR
                    return
                }

                Write-Progress -Activity "Processing aliases" -Status "Retrieving recipient list for domain '$normalizedDomain' ..." -PercentComplete 0
                $recipients = Get-Recipient -ResultSize Unlimited | Where-Object {
                    $_.RecipientTypeDetails -ne 'GuestMailUser' -and $_.EmailAddresses -like "*@$normalizedDomain"
                }
                Write-Progress -Activity "Processing aliases" -Status "Recipient list loaded." -PercentComplete 5
            }
        }

        $willExport = $Csv -or $PSCmdlet.ParameterSetName -ne 'Single'

        if ($willExport -and -not $resolvedCsvFolder) {
            try {
                $resolvedCsvFolder = Test-Folder $CsvFolder
            }
            catch {
                Write-NCMessage "Invalid CSV folder. $($_.Exception.Message)" -Level ERROR
                return
            }
        }

        $recipientCount = @($recipients).Count
        $addressCount = 0
        foreach ($recipient in $recipients) {
            $addressCount += @($recipient.EmailAddresses).Count
        }

        $baseProgress = if ($PSCmdlet.ParameterSetName -eq 'Single') { 0 } else { 5 }
        $progressSpan = if ($willExport) { 94 } else { 95 }
        $totalWorkUnits = [Math]::Max($recipientCount + $addressCount, 1)
        $completedWorkUnits = 0
        $recipientIndex = 0
        $normalizeText = {
            param($value)
            return Get-NormalizedText -Value $value
        }
        $buildAliasKey = {
            param($row)

            $primary = & $normalizeText $row.PrimarySmtpAddress
            $alias = & $normalizeText $row.Alias
            $isPrimary = if ($null -ne $row.IsPrimary) { [string]$row.IsPrimary } else { $null }
            $isMoera = if ($null -ne $row.IsMoera) { [string]$row.IsMoera } else { $null }
            return "{0}|{1}|{2}|{3}" -f $primary, $alias, $isPrimary, $isMoera
        }
        $existingAliasKeys = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::OrdinalIgnoreCase)
        $csvPathResolved = $null

        if ($willExport) {
            $folderForExport = if ($resolvedCsvFolder) { $resolvedCsvFolder } else { Test-Folder $CsvFolder }
            $defaultCsvPath = New-File "$folderForExport\$((Get-Date -Format $NCVars.DateTimeString_CSV))_M365-Alias-Report.csv"
            $csvPathResolved = $defaultCsvPath

            if ($Resume) {
                $resumePath = $null
                if (-not [string]::IsNullOrWhiteSpace($CsvPath)) {
                    $resumePath = $CsvPath
                }
                else {
                    $existingCsv = Get-ChildItem -LiteralPath $folderForExport -File -Filter "*_M365-Alias-Report.csv" |
                        Sort-Object LastWriteTime -Descending |
                        Select-Object -First 1
                    if ($existingCsv) {
                        $resumePath = $existingCsv.FullName
                    }
                }

                if ($resumePath) {
                    $csvPathResolved = $resumePath
                    if (Test-Path -LiteralPath $csvPathResolved) {
                        try {
                        foreach ($row in (Import-CSV -LiteralPath $csvPathResolved -Delimiter $NCVars.CSV_DefaultLimiter -ErrorAction Stop)) {
                                $null = $existingAliasKeys.Add((& $buildAliasKey $row))
                            }
                            Write-NCMessage ("Resuming alias export from {0}; {1} row(s) already recorded." -f $csvPathResolved, $existingAliasKeys.Count) -Level INFO
                        }
                        catch {
                            Write-NCMessage ("Unable to read existing CSV '{0}' for resume. {1}" -f $csvPathResolved, $_.Exception.Message) -Level WARNING
                            $existingAliasKeys.Clear()
                            $csvPathResolved = $defaultCsvPath
                        }
                    }
                    else {
                        Write-NCMessage ("Resume requested for '{0}', but the file does not exist. Starting a new report at that path." -f $csvPathResolved) -Level INFO
                    }
                }
                else {
                    Write-NCMessage ("Resume requested, but no existing CSV was found. Starting a new report at {0}." -f $csvPathResolved) -Level INFO
                }
            }

            Write-NCMessage ("Alias export will flush every {0} recipient(s). Resume: {1}. Stop after {2} consecutive error(s)." -f $BatchSize, $Resume.IsPresent, $MaxConsecutiveErrors) -Level INFO
            Write-NCMessage "Saving report to $csvPathResolved" -Level DEBUG
        }
        $updateAliasProgress = {
            param(
                [string]$Status,
                [switch]$Advance
            )

            if ($Advance) {
                $completedWorkUnits++
            }

            $percentage = [Math]::Min(99, [Math]::Round($baseProgress + (($completedWorkUnits / $totalWorkUnits) * $progressSpan), 2))
            Write-Progress -Activity "Processing aliases" -Status $Status -PercentComplete $percentage
        }

        foreach ($recipient in $recipients) {
            $recipientIndex++
            & $updateAliasProgress -Status "Recipient $recipientIndex of $recipientCount" -Advance

            $rows = [System.Collections.Generic.List[object]]::new()
            foreach ($address in $recipient.EmailAddresses) {
                & $updateAliasProgress -Status "Recipient $recipientIndex of $recipientCount" -Advance

                if ($address -clike 'SMTP:*') {
                    if ($willExport) {
                        $rows.Add([pscustomobject]@{
                                DisplayName        = $recipient.DisplayName
                                Name               = $recipient.Name
                                UserPrincipalName  = $recipient.PrimarySmtpAddress
                                PrimarySmtpAddress = $recipient.PrimarySmtpAddress
                                Alias              = $address.Replace('SMTP:', '')
                                IsPrimary          = $true
                                IsMoera            = $address.Replace('SMTP:', '') -match '@[^@]+\.onmicrosoft\.com$'
                            }) | Out-Null
                    }
                    else {
                        $rows.Add([pscustomobject]@{
                                PrimarySmtpAddress = $recipient.PrimarySmtpAddress
                                Alias              = $address.Replace('SMTP:', '')
                                IsPrimary          = $true
                            }) | Out-Null
                    }
                }
                elseif ($address -clike 'smtp:*') {
                    if ($willExport) {
                        $rows.Add([pscustomobject]@{
                                DisplayName        = $recipient.DisplayName
                                Name               = $recipient.Name
                                UserPrincipalName  = $recipient.PrimarySmtpAddress
                                PrimarySmtpAddress = $recipient.PrimarySmtpAddress
                                Alias              = $address.Replace('smtp:', '')
                                IsPrimary          = $false
                                IsMoera            = $address.Replace('smtp:', '') -match '@[^@]+\.onmicrosoft\.com$'
                            }) | Out-Null
                    }
                    else {
                        $rows.Add([pscustomobject]@{
                                PrimarySmtpAddress = $recipient.PrimarySmtpAddress
                                Alias              = $address.Replace('smtp:', '')
                                IsPrimary          = $false
                            }) | Out-Null
                    }
                }
            }

            if ($rows.Count -eq 0) {
                Write-NCMessage "No aliases found for '$($recipient.PrimarySmtpAddress)'." -Level WARNING
                continue
            }

            if ($willExport) {
                $exportRows = $rows
                    if (-not $IncludeMoera) {
                        $exportRows = @($exportRows | Where-Object { -not $_.IsMoera })
                    }

                if ($exportRows.Count -eq 0) {
                    continue
                }

                if (-not $IncludePrimaryOnly -and $exportRows.Count -eq 1 -and $exportRows[0].IsPrimary -and $exportRows[0].Alias -eq $exportRows[0].PrimarySmtpAddress) {
                    continue
                }

                foreach ($row in $exportRows) {
                    $rowKey = & $buildAliasKey $row
                    if ($Resume -and $existingAliasKeys.Contains($rowKey)) {
                        continue
                    }

                    $aliasRows.Add($row) | Out-Null
                }

                $processedSinceFlush++
                if ($aliasRows.Count -gt 0 -and $processedSinceFlush -ge $BatchSize) {
                    $aliasRows |
                        Select-Object DisplayName, Name, UserPrincipalName, PrimarySmtpAddress, Alias, IsPrimary, IsMoera |
                        Export-Csv -LiteralPath $csvPathResolved -NoTypeInformation -Encoding $NCVars.CSV_Encoding -Delimiter $NCVars.CSV_DefaultLimiter -Append
                    $aliasRows.Clear()
                    $processedSinceFlush = 0
                }
            }
            else {
                $aliasRows.AddRange($rows)
            }

            if ($MaxConsecutiveErrors -gt 0 -and $consecutiveErrors -ge $MaxConsecutiveErrors) {
                $aborted = $true
                break
            }
        }

        if ($aliasRows.Count -eq 0) {
            if ($willExport -and -not $IncludePrimaryOnly) {
                if ((Test-Path -LiteralPath $csvPathResolved) -and ((Get-Item -LiteralPath $csvPathResolved).Length -gt 0)) {
                    Write-NCMessage "No new aliases found. Existing CSV at $csvPathResolved already contains the requested rows." -Level INFO
                }
                else {
                    Write-NCMessage "No secondary aliases found. Use -IncludePrimaryOnly to include recipients that only have a primary SMTP address." -Level WARNING
                }
            }
            return
        }

        if ($willExport) {
            & $updateAliasProgress -Status "Writing CSV export ..." -Advance
            $exportAliases = $aliasRows | Sort-Object -Property PrimarySmtpAddress, @{ Expression = 'IsPrimary'; Descending = $true }, Alias

            if ($exportAliases.Count -eq 0) {
                Write-NCMessage "No exportable aliases found. Use -IncludeMoera to include MOERA addresses or -IncludePrimaryOnly to include recipients with only a primary SMTP address." -Level WARNING
                return
            }

            $finalExport = $exportAliases | Select-Object DisplayName, Name, UserPrincipalName, PrimarySmtpAddress, Alias, IsPrimary, IsMoera
            if ((Test-Path -LiteralPath $csvPathResolved) -and ((Get-Item -LiteralPath $csvPathResolved).Length -gt 0)) {
                $finalExport | Export-Csv -LiteralPath $csvPathResolved -NoTypeInformation -Encoding $NCVars.CSV_Encoding -Delimiter $NCVars.CSV_DefaultLimiter -Append
            }
            else {
                $finalExport | Export-Csv -LiteralPath $csvPathResolved -NoTypeInformation -Encoding $NCVars.CSV_Encoding -Delimiter $NCVars.CSV_DefaultLimiter
            }
            if ($aborted) {
                Write-NCMessage "Alias report export stopped early. Partial data kept at $csvPathResolved." -Level ERROR
            }
            else {
                Write-NCMessage "Alias report exported to $csvPathResolved" -Level SUCCESS
            }
        }
        else {
            $aliasRows | Sort-Object -Property PrimarySmtpAddress, @{ Expression = 'IsPrimary'; Descending = $true }, Alias
        }
    }

    end {
        Write-Progress -Activity "Processing aliases" -Completed
        Restore-ProgressAndInfoPreferences
    }
}

function Get-MboxPrimarySmtpAddress {
    <#
    .SYNOPSIS
        Returns the PrimarySmtpAddress for a mailbox or recipient.
    .DESCRIPTION
        Ensures Exchange Online connectivity and resolves the recipient to its PrimarySmtpAddress.
    .PARAMETER SourceMailbox
        Mailbox or recipient identity. Accepts pipeline input.
    .PARAMETER Raw
        Return only the PrimarySmtpAddress values.
    .PARAMETER Detailed
        Return additional recipient fields (Identity and RecipientTypeDetails).
    .EXAMPLE
        Get-MboxPrimarySmtpAddress -SourceMailbox user@contoso.com
    .EXAMPLE
        Get-MboxPrimarySmtpAddress -SourceMailbox user@contoso.com -Detailed
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('Identity')]
        [string[]]$SourceMailbox,
        [switch]$Raw,
        [switch]$Detailed
    )

    begin { Set-ProgressAndInfoPreferences }

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

        foreach ($entry in $SourceMailbox) {
            try {
                $recipient = Get-Recipient -Identity $entry -ErrorAction Stop
            }
            catch {
                Write-NCMessage "Recipient '$entry' not available or not found. $($_.Exception.Message)" -Level ERROR
                continue
            }

            if ($Raw) {
                $recipient.PrimarySmtpAddress
            }
            else {
                if ($Detailed) {
                    [pscustomobject]@{
                        DisplayName          = $recipient.DisplayName
                        PrimarySmtpAddress   = $recipient.PrimarySmtpAddress
                        Identity             = $recipient.Identity
                        RecipientTypeDetails = $recipient.RecipientTypeDetails
                    }
                }
                else {
                    [pscustomobject]@{
                        DisplayName        = $recipient.DisplayName
                        PrimarySmtpAddress = $recipient.PrimarySmtpAddress
                    }
                }
            }
        }
    }

    end { Restore-ProgressAndInfoPreferences }
}

Set-Alias -Name gpa -Value Get-MboxPrimarySmtpAddress -Description "Get Primary Address (function)"

function Get-MboxPermission {
    <#
    .SYNOPSIS
        Retrieves mailbox permissions for a single mailbox.
    .DESCRIPTION
        Shows FullAccess, SendAs, and SendOnBehalfTo permissions with optional summary counts.
    .PARAMETER SourceMailbox
        Mailbox identity to inspect.
    .PARAMETER IncludeSummary
        Display a short summary of counts.
    .EXAMPLE
        Get-MboxPermission -SourceMailbox user@contoso.com -IncludeSummary
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('Identity')]
        [string]$SourceMailbox,
        [switch]$IncludeSummary
    )

    begin { Set-ProgressAndInfoPreferences }

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

        try {
            $mailbox = Get-Mailbox -Identity $SourceMailbox -ErrorAction Stop
        }
        catch {
            Write-NCMessage "Mailbox '$SourceMailbox' not found." -Level ERROR
            return
        }

        $results = [System.Collections.Generic.List[object]]::new()
        $fullAccessCount = 0
        $sendAsCount = 0
        $sendOnBehalfToCount = 0

        $addAccessRight = {
            param(
                [psobject]$Entry,
                [string]$Right
            )

            if (-not $Entry -or [string]::IsNullOrWhiteSpace($Right)) {
                return
            }

            $existingRights = @($Entry.AccessRights -split '\s*,\s*' | Where-Object { -not [string]::IsNullOrWhiteSpace($_) })
            if ($existingRights -notcontains $Right) {
                $Entry.AccessRights = (@($existingRights + $Right) | Select-Object -Unique) -join ', '
            }
        }

        Get-MailboxPermission -Identity $mailbox.PrimarySmtpAddress -ErrorAction SilentlyContinue | Where-Object { $_.AccessRights -eq 'FullAccess' -and -not $_.IsInherited } | ForEach-Object {
            $userMailbox = $_.User.ToString()
            $primary = (Get-Mailbox -Identity $userMailbox -ErrorAction SilentlyContinue).PrimarySmtpAddress
            $display = (Get-User -Identity $userMailbox -ErrorAction SilentlyContinue).DisplayName

            if ($primary) {
                $existing = $results | Where-Object { $_.UserMailbox -eq $primary } | Select-Object -First 1
                if ($existing) {
                    & $addAccessRight $existing 'FullAccess'
                }
                else {
                    $results.Add([pscustomobject]@{
                            User         = $display
                            UserMailbox  = $primary
                            AccessRights = 'FullAccess'
                        }) | Out-Null
                }
                $fullAccessCount++
            }
        }
        Write-Progress -Activity "Gathered FullAccess permissions for $($mailbox.PrimarySmtpAddress) ..." -Status "35% Complete" -PercentComplete 35

        Get-RecipientPermission -Identity $mailbox.PrimarySmtpAddress -AccessRights SendAs -ErrorAction SilentlyContinue | Where-Object { $_.Trustee.ToString() -ne 'NT AUTHORITY\SELF' -and $_.Trustee.ToString() -notlike 'S-1-5*' } | ForEach-Object {
            $userMailbox = $_.Trustee.ToString()
            $primary = (Get-Mailbox -Identity $userMailbox -ErrorAction SilentlyContinue).PrimarySmtpAddress
            $display = (Get-User -Identity $userMailbox -ErrorAction SilentlyContinue).DisplayName

            if ($primary) {
                $existing = $results | Where-Object { $_.UserMailbox -eq $primary } | Select-Object -First 1
                if ($existing) {
                    & $addAccessRight $existing 'SendAs'
                }
                else {
                    $results.Add([pscustomobject]@{
                            User         = $display
                            UserMailbox  = $primary
                            AccessRights = 'SendAs'
                        }) | Out-Null
                }
                $sendAsCount++
            }
        }
        Write-Progress -Activity "Gathered SendAs permissions for $($mailbox.PrimarySmtpAddress) ..." -Status "50% Complete" -PercentComplete 50

        foreach ($userMailbox in $mailbox.GrantSendOnBehalfTo) {
            $primary = (Get-Mailbox -Identity $userMailbox -ErrorAction SilentlyContinue).PrimarySmtpAddress
            $display = (Get-User -Identity $userMailbox -ErrorAction SilentlyContinue).DisplayName

            if ($primary) {
                $existing = $results | Where-Object { $_.UserMailbox -eq $primary } | Select-Object -First 1
                if ($existing) {
                    & $addAccessRight $existing 'SendOnBehalfTo'
                }
                else {
                    $results.Add([pscustomobject]@{
                            User         = $display
                            UserMailbox  = $primary
                            AccessRights = 'SendOnBehalfTo'
                        }) | Out-Null
                }
                $sendOnBehalfToCount++
            }
        }
        Write-Progress -Activity "Gathered SendOnBehalfTo permissions for $($mailbox.PrimarySmtpAddress) ..." -Status "90% Complete" -PercentComplete 90

        Add-EmptyLine
        Write-NCMessage ("Access Rights on {0} ({1})" -f $mailbox.DisplayName, $mailbox.PrimarySmtpAddress) -Level WARNING
        if ($PSCmdlet.MyInvocation.PipelineLength -gt 1) {
            $results
        }
        else {
            $results | Format-Table -AutoSize -Wrap
        }

        if ($IncludeSummary) {
            Add-EmptyLine
            Write-NCMessage "Summary of Permissions Found:" -Level INFO
            Write-NCMessage ("FullAccess: {0}" -f $fullAccessCount) -Level SUCCESS
            Write-NCMessage ("SendAs: {0}" -f $sendAsCount) -Level SUCCESS
            Write-NCMessage ("SendOnBehalfTo: {0}" -f $sendOnBehalfToCount) -Level SUCCESS
        }
    }

    end { Restore-ProgressAndInfoPreferences }
}

function Get-UserLastSeen {
    <#
    .SYNOPSIS
        Returns the most recent activity timestamps for a user mailbox.
    .DESCRIPTION
        Combines Exchange Online LastUserActionTime with Entra ID (Microsoft Graph) interactive sign-in logs.
        Falls back to mailbox activity if Graph sign-in logs are unavailable or permissions are missing.
    .PARAMETER User
        Mailbox identity (UPN, SMTP address, alias). Accepts pipeline input.
    .EXAMPLE
        Get-UserLastSeen -User user@contoso.com
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('Identity', 'UserPrincipalName')]
        [string]$User
    )

    begin { Set-ProgressAndInfoPreferences }

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

        try {
            $mailbox = Get-Mailbox -Identity $User -ErrorAction Stop
        }
        catch {
            Write-NCMessage "Mailbox '$User' not found or not accessible. $($_.Exception.Message)" -Level ERROR
            return
        }

        $mailboxIdentity = $mailbox.PrimarySmtpAddress
        $lastMailboxAction = $null
        $lastSignIn = $null

        $stats = Get-MailboxStatisticsSafe -Identity $mailboxIdentity
        if ($stats) {
            $lastMailboxAction = $stats.LastUserActionTime
        }

        $graphReady = Test-MgGraphConnection -Scopes @('AuditLog.Read.All', 'Directory.Read.All') -EnsureExchangeOnline:$false
        if ($graphReady) {
            try {
                $signIns = Get-MgAuditLogSignIn -Filter "userId eq '$($mailbox.ExternalDirectoryObjectId)'" -All:$true -Top 20 -ErrorAction Stop
                if ($signIns) {
                    $lastSignIn = $signIns | Sort-Object -Property CreatedDateTime -Descending | Select-Object -First 1 -ExpandProperty CreatedDateTime
                }
            }
            catch {
                Write-NCMessage ("Unable to retrieve sign-in logs for {0}. {1}" -f $mailboxIdentity, $_.Exception.Message) -Level WARNING
            }
        }
        else {
            Write-NCMessage "Microsoft Graph AuditLog.Read.All is not available. Returning mailbox activity only." -Level WARNING
        }

        $sources = @()
        if ($lastMailboxAction) { $sources += 'MailboxAction' }
        if ($lastSignIn) { $sources += 'SignInLog' }

        $lastSeen = $null
        if ($sources.Count -gt 0) {
            $timestamps = @()
            if ($lastMailboxAction) { $timestamps += $lastMailboxAction }
            if ($lastSignIn) { $timestamps += $lastSignIn }
            $lastSeen = $timestamps | Sort-Object -Descending | Select-Object -First 1
        }

        [pscustomobject][ordered]@{
            DisplayName           = $mailbox.DisplayName
            PrimarySmtpAddress    = $mailboxIdentity
            LastUserActionTime    = $lastMailboxAction
            LastInteractiveSignIn = if ($lastSignIn) { [datetime]$lastSignIn } else { $null }
            LastSeen              = if ($lastSeen) { [datetime]$lastSeen } else { $null }
            Source                = if ($sources.Count -gt 0) { $sources -join ',' } else { 'None' }
        }
    }

    end { Restore-ProgressAndInfoPreferences }
}

function Get-MboxLastMessageTrace {
    <#
    .SYNOPSIS
        Returns the most recent received and sent message traces for a mailbox.
    .DESCRIPTION
        Resolves the mailbox to its PrimarySmtpAddress and queries Get-MessageTraceV2 for the latest
        received and sent messages, returning the trace objects and their timestamps.
    .PARAMETER SourceMailbox
        Mailbox or recipient identity (UPN, SMTP address, alias). Accepts pipeline input.
    .PARAMETER IncludeTrace
        Include raw message trace objects in the output.
    .EXAMPLE
        Get-MboxLastMessageTrace -SourceMailbox user@contoso.com
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('Identity', 'UserPrincipalName', 'Mailbox', 'UPN')]
        [string]$SourceMailbox,
        [switch]$IncludeTrace
    )

    begin { Set-ProgressAndInfoPreferences }

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

        try {
            $recipient = Get-Recipient -Identity $SourceMailbox -ErrorAction Stop
        }
        catch {
            Write-NCMessage "Mailbox or recipient '$SourceMailbox' not found. $($_.Exception.Message)" -Level ERROR
            return
        }

        $primaryAddress = $recipient.PrimarySmtpAddress
        $receivedTrace = $null
        $sentTrace = $null

        try {
            $receivedTrace = Get-MessageTraceV2 -RecipientAddress $primaryAddress -ErrorAction Stop |
                Sort-Object Received -Descending |
                Select-Object -First 1
        }
        catch {
            Write-NCMessage ("Unable to retrieve received message trace for '{0}'. {1}" -f $primaryAddress, $_.Exception.Message) -Level WARNING
        }

        try {
            $sentTrace = Get-MessageTraceV2 -SenderAddress $primaryAddress -ErrorAction Stop |
                Sort-Object Received -Descending |
                Select-Object -First 1
        }
        catch {
            Write-NCMessage ("Unable to retrieve sent message trace for '{0}'. {1}" -f $primaryAddress, $_.Exception.Message) -Level WARNING
        }

        $result = [ordered]@{
            DisplayName            = $recipient.DisplayName
            PrimarySmtpAddress     = $primaryAddress
            LastReceived           = if ($receivedTrace) { $receivedTrace.Received } else { $null }
            LastReceivedSubject    = if ($receivedTrace) { $receivedTrace.Subject } else { $null }
            LastReceivedSender     = if ($receivedTrace) { $receivedTrace.SenderAddress } else { $null }
            LastReceivedRecipient  = if ($receivedTrace) { $receivedTrace.RecipientAddress } else { $null }
            LastReceivedStatus     = if ($receivedTrace) { $receivedTrace.Status } else { $null }
            LastSent               = if ($sentTrace) { $sentTrace.Received } else { $null }
            LastSentSubject        = if ($sentTrace) { $sentTrace.Subject } else { $null }
            LastSentSender         = if ($sentTrace) { $sentTrace.SenderAddress } else { $null }
            LastSentRecipient      = if ($sentTrace) { $sentTrace.RecipientAddress } else { $null }
            LastSentStatus         = if ($sentTrace) { $sentTrace.Status } else { $null }
        }

        if ($IncludeTrace.IsPresent) {
            $result.LastReceivedTrace = $receivedTrace
            $result.LastSentTrace = $sentTrace
        }

        [pscustomobject]$result
    }

    end { Restore-ProgressAndInfoPreferences }
}

function New-SharedMailbox {
    <#
    .SYNOPSIS
        Creates a shared mailbox with opinionated defaults.
    .DESCRIPTION
        Ensures Exchange Online connectivity, creates the shared mailbox, enables copies of sent messages, and sets deleted items retention.
    .PARAMETER SharedMailboxSMTPAddress
        Primary SMTP address for the new shared mailbox.
    .PARAMETER SharedMailboxDisplayName
        Display name for the mailbox.
    .PARAMETER SharedMailboxAlias
        Alias for the mailbox.
    .EXAMPLE
        New-SharedMailbox -SharedMailboxSMTPAddress user@contoso.com -SharedMailboxDisplayName "Contoso - Info" -SharedMailboxAlias contoso_info
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [string]$SharedMailboxSMTPAddress,
        [Parameter(Mandatory)]
        [string]$SharedMailboxDisplayName,
        [Parameter(Mandatory)]
        [string]$SharedMailboxAlias
    )

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

    try {
        New-Mailbox -Name $SharedMailboxDisplayName -Alias $SharedMailboxAlias -Shared -PrimarySmtpAddress $SharedMailboxSMTPAddress -ErrorAction Stop
        Write-NCMessage ("`nSet outgoing e-mail copy save for {0}" -f $SharedMailboxSMTPAddress) -Level INFO
        Set-Mailbox -Identity $SharedMailboxSMTPAddress -MessageCopyForSentAsEnabled $true
        Set-Mailbox -Identity $SharedMailboxSMTPAddress -MessageCopyForSendOnBehalfEnabled $true
        Set-Mailbox -Identity $SharedMailboxSMTPAddress -RetainDeletedItemsFor 30
        Write-NCMessage "All done, remember to set access and editing rights to the new mailbox." -Level SUCCESS
    }
    catch {
        Write-NCMessage "Unable to create shared mailbox. $($_.Exception.Message)" -Level ERROR
    }
}

function Remove-MboxAlias {
    <#
    .SYNOPSIS
        Removes an alias from a mailbox or mail-enabled recipient.
    .DESCRIPTION
        Validates Exchange Online connectivity, resolves the recipient, and removes the specified alias.
    .PARAMETER SourceMailbox
        Mailbox or recipient identity. Accepts pipeline input.
    .PARAMETER MailboxAlias
        Alias to remove (SMTP address).
    .EXAMPLE
        Remove-MboxAlias -SourceMailbox user@contoso.com -MailboxAlias alias@contoso.com
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('Identity')]
        [string]$SourceMailbox,
        [Parameter(Mandatory)]
        [string]$MailboxAlias
    )

    begin { Set-ProgressAndInfoPreferences }

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

        $normalizedAlias = $MailboxAlias.Trim().ToLowerInvariant()

        function Test-ProxyAddressPresent {
            param(
                [Parameter(Mandatory)]
                [object[]]$EmailAddresses,
                [Parameter(Mandatory)]
                [string]$TargetAddress
            )

            foreach ($proxy in $EmailAddresses) {
                $proxyText = $proxy.ToString()
                $separatorIndex = $proxyText.IndexOf(':')
                $addressPart = if ($separatorIndex -ge 0) { $proxyText.Substring($separatorIndex + 1) } else { $proxyText }
                if ($addressPart.Trim().ToLowerInvariant() -eq $TargetAddress) {
                    return $true
                }
            }

            return $false
        }

        try {
            $recipient = Get-Recipient -Identity $SourceMailbox -ErrorAction Stop
        }
        catch {
            Write-NCMessage "Mailbox or recipient '$SourceMailbox' not found. $($_.Exception.Message)" -Level ERROR
            return
        }

        try {
            switch ($recipient.RecipientTypeDetails) {
                'MailContact' { Set-MailContact -Identity $recipient.Identity -EmailAddresses @{ remove = $MailboxAlias } -ErrorAction Stop }
                'MailUser' { Set-MailUser -Identity $recipient.Identity -EmailAddresses @{ remove = $MailboxAlias } -ErrorAction Stop }
                { ($_ -eq 'MailUniversalDistributionGroup') -or ($_ -eq 'DynamicDistributionGroup') -or ($_ -eq 'MailUniversalSecurityGroup') } {
                    Set-DistributionGroup -Identity $recipient.Identity -EmailAddresses @{ remove = $MailboxAlias } -ErrorAction Stop
                }
                default { Set-Mailbox -Identity $recipient.Identity -EmailAddresses @{ remove = $MailboxAlias } -ErrorAction Stop }
            }
        }
        catch {
            Write-NCMessage "Unable to remove alias '$MailboxAlias' from '$($recipient.PrimarySmtpAddress)'. $($_.Exception.Message)" -Level ERROR
            return
        }

        try {
            $updatedRecipient = Get-Recipient -Identity $recipient.Identity -ErrorAction Stop
        }
        catch {
            Write-NCMessage "Alias removal attempted, but unable to validate final state for '$($recipient.PrimarySmtpAddress)'. $($_.Exception.Message)" -Level WARNING
            return
        }

        if (Test-ProxyAddressPresent -EmailAddresses $updatedRecipient.EmailAddresses -TargetAddress $normalizedAlias) {
            Write-NCMessage ("Alias '{0}' is still present on {1}. It may be protected (for example, used as WindowsLiveId)." -f $MailboxAlias, $updatedRecipient.PrimarySmtpAddress) -Level WARNING
        }
        else {
            Write-NCMessage ("Alias '{0}' removed from {1}." -f $MailboxAlias, $updatedRecipient.PrimarySmtpAddress) -Level SUCCESS
        }

        Get-MboxAlias -SourceMailbox $updatedRecipient.PrimarySmtpAddress
    }

    end { Restore-ProgressAndInfoPreferences }
}

function Remove-MboxPermission {
    <#
    .SYNOPSIS
        Removes mailbox permissions from users.
    .DESCRIPTION
        Ensures Exchange Online connectivity, validates target and user mailboxes, and removes FullAccess,
        SendAs, SendOnBehalfTo, or all of them.
    .PARAMETER SourceMailbox
        Mailbox identity to update.
    .PARAMETER UserMailbox
        One or more users to remove permissions from. Accepts pipeline input.
    .PARAMETER AccessRights
        Permission type to remove: All, FullAccess, SendAs, SendOnBehalfTo. Defaults to All.
    .PARAMETER ClearAll
        Removes any non-inherited FullAccess, SendAs, and SendOnBehalfTo permissions for the source mailbox.
    .EXAMPLE
        Remove-MboxPermission -SourceMailbox user@contoso.com -UserMailbox user@contoso.com -AccessRights SendAs
    .EXAMPLE
        Remove-MboxPermission -SourceMailbox user@contoso.com -ClearAll
    #>

    [CmdletBinding(DefaultParameterSetName = 'User')]
    param(
        [Parameter(Mandatory, Position = 0, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'User')]
        [Parameter(Mandatory, Position = 0, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'All')]
        [Alias('Identity')]
        [string]$SourceMailbox,
        [Parameter(Mandatory, Position = 1, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, ParameterSetName = 'User')]
        [string[]]$UserMailbox,
        [Parameter(ParameterSetName = 'User')]
        [ValidateSet('All', 'FullAccess', 'SendAs', 'SendOnBehalfTo')]
        [string]$AccessRights = 'All',
        [Parameter(Mandatory, ParameterSetName = 'All')]
        [switch]$ClearAll
    )

    begin { Set-ProgressAndInfoPreferences }

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

        try {
            $targetMailbox = Get-Mailbox -Identity $SourceMailbox -ErrorAction Stop
        }
        catch {
            Write-NCMessage "Mailbox '$SourceMailbox' not found. $($_.Exception.Message)" -Level ERROR
            return
        }

        if ($PSCmdlet.ParameterSetName -eq 'All') {
            $targetAddress = $targetMailbox.PrimarySmtpAddress

            $fullAccessUsers = Get-MailboxPermission -Identity $targetAddress -ErrorAction SilentlyContinue |
                Where-Object { $_.AccessRights -eq 'FullAccess' -and -not $_.IsInherited -and $_.User.ToString() -ne 'NT AUTHORITY\SELF' -and $_.User.ToString() -notlike 'S-1-5*' } |
                ForEach-Object { $_.User.ToString() } | Sort-Object -Unique

            foreach ($userIdentity in $fullAccessUsers) {
                Write-NCMessage ("Removing FullAccess for {0} from {1} ..." -f $userIdentity, $targetAddress) -Level INFO
                Remove-MailboxPermission -Identity $targetAddress -User $userIdentity -AccessRights FullAccess -Confirm:$false
            }

            $sendAsUsers = Get-RecipientPermission -Identity $targetAddress -AccessRights SendAs -ErrorAction SilentlyContinue |
                Where-Object { $_.Trustee.ToString() -ne 'NT AUTHORITY\SELF' -and $_.Trustee.ToString() -notlike 'S-1-5*' } |
                ForEach-Object { $_.Trustee.ToString() } | Sort-Object -Unique

            foreach ($userIdentity in $sendAsUsers) {
                Write-NCMessage ("Removing SendAs for {0} from {1} ..." -f $userIdentity, $targetAddress) -Level INFO
                Remove-RecipientPermission -Identity $targetAddress -Trustee $userIdentity -AccessRights SendAs -Confirm:$false
            }

            $sendOnBehalfUsers = @($targetMailbox.GrantSendOnBehalfTo) | ForEach-Object { $_.ToString() } | Sort-Object -Unique
            foreach ($userIdentity in $sendOnBehalfUsers) {
                Write-NCMessage ("Removing SendOnBehalfTo for {0} from {1} ..." -f $userIdentity, $targetAddress) -Level INFO
                Set-Mailbox -Identity $targetAddress -GrantSendOnBehalfTo @{ remove = $userIdentity } -Confirm:$false | Out-Null
            }
        }
        else {
            foreach ($user in $UserMailbox) {
                try {
                    $userObject = Get-User -Identity $user -ErrorAction Stop
                }
                catch {
                    Add-EmptyLine
                    Write-NCMessage "The mailbox '$user' does not exist. Please check the provided e-mail address." -Level ERROR
                    continue
                }

                $userIdentity = $userObject.UserPrincipalName
                switch ($AccessRights) {
                    'FullAccess' {
                        Write-NCMessage ("Removing FullAccess for {0} from {1} ..." -f $userIdentity, $targetMailbox.PrimarySmtpAddress) -Level INFO
                        Remove-MailboxPermission -Identity $targetMailbox.PrimarySmtpAddress -User $userIdentity -AccessRights FullAccess -Confirm:$false
                    }
                    'SendAs' {
                        Write-NCMessage ("Removing SendAs for {0} from {1} ..." -f $userIdentity, $targetMailbox.PrimarySmtpAddress) -Level INFO
                        Remove-RecipientPermission -Identity $targetMailbox.PrimarySmtpAddress -Trustee $userIdentity -AccessRights SendAs -Confirm:$false
                    }
                    'SendOnBehalfTo' {
                        Write-NCMessage ("Removing SendOnBehalfTo for {0} from {1} ..." -f $userIdentity, $targetMailbox.PrimarySmtpAddress) -Level INFO
                        Set-Mailbox -Identity $targetMailbox.PrimarySmtpAddress -GrantSendOnBehalfTo @{ remove = $userIdentity } -Confirm:$false | Out-Null
                    }
                    'All' {
                        Write-NCMessage ("Removing FullAccess for {0} from {1} ..." -f $userIdentity, $targetMailbox.PrimarySmtpAddress) -Level INFO
                        Remove-MailboxPermission -Identity $targetMailbox.PrimarySmtpAddress -User $userIdentity -AccessRights FullAccess -Confirm:$false
                        Write-NCMessage ("Removing SendAs for {0} from {1} ..." -f $userIdentity, $targetMailbox.PrimarySmtpAddress) -Level INFO
                        Remove-RecipientPermission -Identity $targetMailbox.PrimarySmtpAddress -Trustee $userIdentity -AccessRights SendAs -Confirm:$false
                        Write-NCMessage ("Removing SendOnBehalfTo for {0} from {1} ..." -f $userIdentity, $targetMailbox.PrimarySmtpAddress) -Level INFO
                        Set-Mailbox -Identity $targetMailbox.PrimarySmtpAddress -GrantSendOnBehalfTo @{ remove = $userIdentity } -Confirm:$false | Out-Null
                    }
                }
            }
        }
    }

    end { Restore-ProgressAndInfoPreferences }
}

function Set-MboxLanguage {
    <#
    .SYNOPSIS
        Sets mailbox regional language.
    .DESCRIPTION
        Changes language for a single mailbox or a list provided via CSV (EmailAddress column).
    .PARAMETER SourceMailbox
        Mailbox to update. Accepts pipeline input. Ignored when -Csv is provided.
    .PARAMETER Language
        Language tag (default it-IT).
    .PARAMETER Csv
        CSV file with EmailAddress column containing mailboxes to update.
    .EXAMPLE
        Set-MboxLanguage -SourceMailbox user@contoso.com -Language en-US
    .EXAMPLE
        Set-MboxLanguage -Csv C:\temp\mailboxes.csv -Language it-IT
    #>

    [CmdletBinding(DefaultParameterSetName = 'Mailbox')]
    param(
        [Parameter(ParameterSetName = 'Mailbox', Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('Identity')]
        [string[]]$SourceMailbox,
        [Parameter()]
        [string]$Language = 'it-IT',
        [Parameter(ParameterSetName = 'Csv', Mandatory = $true)]
        [string]$Csv
    )

    begin { Set-ProgressAndInfoPreferences }

    process {
        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 = @()
        if ($PSCmdlet.ParameterSetName -eq 'Csv') {
            if (-not (Test-Path -LiteralPath $Csv)) {
                Write-NCMessage "CSV file '$Csv' not found." -Level ERROR
                return
            }

            try {
                $mailboxes = (Import-Csv -LiteralPath $Csv -Delimiter $NCVars.CSV_DefaultLimiter) | Where-Object { $_.EmailAddress }
            }
            catch {
                Write-NCMessage "Unable to read CSV file '$Csv'. $($_.Exception.Message)" -Level ERROR
                return
            }
        }
        else {
            $mailboxes = $SourceMailbox
        }

        $counter = 0
        $total = $mailboxes.Count
        foreach ($entry in $mailboxes) {
            $address = if ($entry.PSObject.Properties.Match('EmailAddress')) { $entry.EmailAddress } else { $entry }
            if (-not $address) { continue }

            $counter++
            $Percentage = Get-NCProgressPercent -Current $counter -Total $total
            Write-Progress -Activity "Changing language to $Language" -Status "$counter of $total - $Percentage%" -PercentComplete $Percentage

            try {
                Set-MailboxRegionalConfiguration -Identity $address -LocalizeDefaultFolderName:$true -Language $Language -ErrorAction Stop
                $result = Get-MailboxRegionalConfiguration -Identity $address -ErrorAction Stop
                [pscustomobject]@{
                    PrimarySmtpAddress = $result.Identity
                    Language           = $result.Language
                    TimeZone           = $result.TimeZone
                }
            }
            catch {
                Write-NCMessage "Failed to update mailbox '$address'. $($_.Exception.Message)" -Level ERROR
            }
        }
    }

    end {
        Write-Progress -Activity "Changing mailbox language" -Completed
        Restore-ProgressAndInfoPreferences
    }
}

function Set-MboxRulesQuota {
    <#
    .SYNOPSIS
        Sets mailbox rules quota to 256KB.
    .DESCRIPTION
        Iterates the provided mailboxes, sets the RulesQuota to 256KB, and returns the updated values.
    .PARAMETER SourceMailbox
        Mailboxes to update. Accepts pipeline input.
    .EXAMPLE
        Set-MboxRulesQuota -SourceMailbox user1@contoso.com, user2@contoso.com
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('Identity')]
        [string[]]$SourceMailbox
    )

    begin {
        Set-ProgressAndInfoPreferences
        $results = [System.Collections.Generic.List[object]]::new()
        $counter = 0
    }

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

        foreach ($mailbox in $SourceMailbox) {
            try {
                $recipient = Get-Recipient -Identity $mailbox -ErrorAction Stop
            }
            catch {
                Write-NCMessage "Mailbox '$mailbox' not found. $($_.Exception.Message)" -Level ERROR
                continue
            }

            $counter++
            $Percentage = Get-NCProgressPercent -Current $counter -Total $SourceMailbox.Count
            Write-Progress -Activity "Processing $($recipient.PrimarySmtpAddress)" -Status "$counter of $($SourceMailbox.Count) - $Percentage%" -PercentComplete $Percentage

            try {
                Set-Mailbox -Identity $recipient.PrimarySmtpAddress -RulesQuota 256KB
                $results.Add([pscustomobject]@{
                        PrimarySmtpAddress = $recipient.PrimarySmtpAddress
                        'Rules Quota'      = (Get-Mailbox -Identity $recipient.PrimarySmtpAddress).RulesQuota
                    }) | Out-Null
            }
            catch {
                Write-NCMessage $_.Exception.Message -Level ERROR
            }
        }
    }

    end {
        Write-Progress -Activity "Processing mailbox rules quota" -Completed
        Restore-ProgressAndInfoPreferences
        $results
    }
}

function Set-SharedMboxCopyForSent {
    <#
    .SYNOPSIS
        Enables sent-item copy options on shared mailboxes.
    .DESCRIPTION
        For each shared mailbox, enables MessageCopyForSentAsEnabled and MessageCopyForSendOnBehalfEnabled,
        returning the updated status and listing any errors.
    .PARAMETER SourceMailbox
        Shared mailboxes to update. Accepts pipeline input.
    .EXAMPLE
        Set-SharedMboxCopyForSent -SourceMailbox user@contoso.com
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('Identity')]
        [string[]]$SourceMailbox
    )

    begin {
        Set-ProgressAndInfoPreferences
        $results = [System.Collections.Generic.List[object]]::new()
        $errors = [System.Collections.Generic.List[string]]::new()
        $counter = 0
    }

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

        foreach ($mailbox in $SourceMailbox) {
            try {
                $recipient = Get-Recipient -Identity $mailbox -ErrorAction Stop
                if ($recipient.RecipientTypeDetails -ne 'SharedMailbox') {
                    $errors.Add("{0} is not a Shared Mailbox." -f $mailbox) | Out-Null
                    continue
                }

                $counter++
                $Percentage = Get-NCProgressPercent -Current $counter -Total $SourceMailbox.Count
                Write-Progress -Activity "Processing $($recipient.PrimarySmtpAddress)" -Status "$counter of $($SourceMailbox.Count) - $Percentage%" -PercentComplete $Percentage

                Set-Mailbox -Identity $recipient.PrimarySmtpAddress -MessageCopyForSentAsEnabled $true
                Set-Mailbox -Identity $recipient.PrimarySmtpAddress -MessageCopyForSendOnBehalfEnabled $true

                $updated = Get-Mailbox -Identity $recipient.PrimarySmtpAddress
                $results.Add([pscustomobject]@{
                        PrimarySmtpAddress      = $recipient.PrimarySmtpAddress
                        'Copy for SentAs'       = $updated.MessageCopyForSentAsEnabled
                        'Copy for SendOnBehalf' = $updated.MessageCopyForSendOnBehalfEnabled
                    }) | Out-Null
            }
            catch {
                Write-NCMessage $_.Exception.Message -Level ERROR
            }
        }
    }

    end {
        Write-Progress -Activity "Processing shared mailbox sent items copy" -Completed
        Restore-ProgressAndInfoPreferences

        $results
        if ($errors.Count -gt 0) {
            Write-NCMessage ($errors -join [Environment]::NewLine) -Level WARNING
        }
    }
}

function Test-SharedMailboxCompliance {
    <#
    .SYNOPSIS
        Reports shared mailbox sign-in activity and licensing.
    .DESCRIPTION
        Uses Microsoft Graph sign-in logs and assigned plans to flag shared mailboxes with successful sign-ins and missing licenses.
    .PARAMETER GridView
        Show the result in Out-GridView (default behaviour). Specify -GridView:$false to return objects.
    .EXAMPLE
        Test-SharedMailboxCompliance
    #>

    [CmdletBinding()]
    param(
        [switch]$GridView
    )

    begin { Set-ProgressAndInfoPreferences }

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

        $graphConnected = Test-MgGraphConnection -Scopes @('AuditLog.Read.All', 'Directory.Read.All') -EnsureExchangeOnline:$false
        if (-not $graphConnected) {
            Add-EmptyLine
            Write-NCMessage "Can't connect or use Microsoft Graph modules. Please check logs." -Level ERROR
            return
        }

        Add-EmptyLine
        Write-NCMessage "Finding shared mailboxes ..." -NoNewline
        $mailboxes = Get-ExoMailbox -RecipientTypeDetails SharedMailbox -ResultSize Unlimited | Sort-Object DisplayName
        if (-not $mailboxes) {
            Write-NCMessage "No shared mailboxes found." -Level WARNING
            return
        }

        $mailboxLabel = if ($mailboxes.Count -eq 1) { 'shared mailbox found' } else { 'shared mailboxes found' }
        Write-NCMessage (" {0} {1}." -f $mailboxes.Count, $mailboxLabel) -Level SUCCESS
        $exoPlan1 = '9aaf7827-d63c-4b61-89c3-182f06f82e5c'
        $exoPlan2 = 'efb87545-963c-4e0d-99df-69c6916d9eb0'
        $report = [System.Collections.Generic.List[object]]::new()
        $counter = 0

        foreach ($mbx in $mailboxes) {
            $counter++
            $Percentage = Get-NCProgressPercent -Current $counter -Total $mailboxes.Count
            Write-Progress -Activity "Checking $($mbx.DisplayName)" -Status "$counter of $($mailboxes.Count) - $Percentage%" -PercentComplete $Percentage

            $logsFound = $false
            $exoPlan1Found = $false
            $exoPlan2Found = $false

            try {
                $signIns = Get-MgAuditLogSignIn -Filter "userid eq '$($mbx.ExternalDirectoryObjectId)'" -All -Top 20 -ErrorAction Stop
                if ($signIns) {
                    foreach ($log in $signIns) {
                        if ($log.Status.ErrorCode -eq 0) {
                            $logsFound = $true
                            break
                        }
                    }
                }
            }
            catch {
                Write-NCMessage ("Unable to retrieve sign-in records for {0}. {1}" -f $mbx.DisplayName, $_.Exception.Message) -Level ERROR
            }

            if ($logsFound) {
                Write-NCMessage ("Sign-in records found for shared mailbox {0}" -f $mbx.DisplayName) -Level WARNING
                try {
                    $user = Get-MgUser -UserId $mbx.ExternalDirectoryObjectId -Property UserPrincipalName, assignedPlans
                    $exoPlans = @($user.AssignedPlans | Where-Object { $_.Service -eq 'exchange' -and $_.capabilityStatus -eq 'Enabled' })
                    $exoPlan1Found = $exoPlan1 -in $exoPlans.ServicePlanId
                    $exoPlan2Found = $exoPlan2 -in $exoPlans.ServicePlanId
                }
                catch {
                    Write-NCMessage ("Unable to read license info for {0}. {1}" -f $mbx.DisplayName, $_.Exception.Message) -Level ERROR
                }
            }
            else {
                Write-NCMessage ("No successful sign-in records found for shared mailbox {0}" -f $mbx.DisplayName) -Level SUCCESS
            }

            $report.Add([pscustomobject]@{
                    DisplayName               = $mbx.DisplayName
                    ExternalDirectoryObjectId = $mbx.ExternalDirectoryObjectId
                    'Sign in Record Found'    = if ($logsFound) { 'Yes' } else { 'No' }
                    'Exchange Online Plan 1'  = $exoPlan1Found
                    'Exchange Online Plan 2'  = $exoPlan2Found
                }) | Out-Null
        }

        Write-Progress -Activity "Checking shared mailboxes" -Completed

        $showGrid = if ($PSBoundParameters.ContainsKey('GridView')) { $GridView.IsPresent } else { $true }
        if ($showGrid) {
            $report | Out-GridView -Title "Shared Mailbox Sign-In Records and Licensing Status"
        }
        else {
            $report
        }
    }

    end { Restore-ProgressAndInfoPreferences }
}