Public/Get-SDMailboxPermissionAudit.ps1

function Get-SDMailboxPermissionAudit {
    <#
    .SYNOPSIS
        Audits Exchange Online mailbox permissions (FullAccess, SendAs, SendOnBehalf) and writes CSV and HTML.

    .DESCRIPTION
        Collects explicitly granted permissions for all mailboxes of the selected types, or for the
        mailboxes given with -Identity:

          - FullAccess Get-EXOMailboxPermission (inherited entries and NT AUTHORITY\SELF are ignored)
          - SendAs Get-EXORecipientPermission (NT AUTHORITY\SELF is ignored)
          - SendOnBehalf GrantSendOnBehalfTo of the mailbox

        Trustees that only appear as a SID (S-1-5-21-...) usually belong to deleted accounts. They are
        marked as Orphaned and highlighted in the HTML report - a typical clean-up finding.

        If a single mailbox cannot be read, the error is logged and the audit continues.
        This function only reads data. It never changes permissions.

        Requirements:
          - Module: ExchangeOnlineManagement 3.x
          - Connect-ExchangeOnline with a role that can read recipients and permissions,
            e.g. View-Only Organization Management or Exchange Recipient Administrator.

    .PARAMETER Identity
        Mailbox(es) to audit (UPN, primary SMTP address or other Exchange identity).
        Accepts pipeline input. Without -Identity all mailboxes of -RecipientTypeDetails are audited.

    .PARAMETER RecipientTypeDetails
        Mailbox types to include when -Identity is not used.
        Default: UserMailbox, SharedMailbox, RoomMailbox, EquipmentMailbox.

    .PARAMETER PermissionType
        Permission types to collect. Default: FullAccess, SendAs, SendOnBehalf.

    .PARAMETER OutputDirectory
        Folder for the CSV and HTML files. Defaults to the current directory.
        File names: MailboxPermissionAudit_yyyy-MM-dd_HHmm.csv / .html

    .PARAMETER Delimiter
        CSV delimiter. Default ','. Use ';' for German Excel.

    .PARAMETER LogDirectory
        Folder for Log_yyyy-MM-dd.log. Defaults to %LOCALAPPDATA%\ServiceDeskToolkit\Logs.

    .EXAMPLE
        Connect-ExchangeOnline -UserPrincipalName admin@contoso.com
        Get-SDMailboxPermissionAudit -RecipientTypeDetails SharedMailbox -OutputDirectory C:\Reports -Verbose

        Audits all shared mailboxes and writes the report to C:\Reports.

    .EXAMPLE
        'info@contoso.com', 'buchhaltung@contoso.com' | Get-SDMailboxPermissionAudit -PermissionType FullAccess, SendAs

        Audits two mailboxes, FullAccess and SendAs only.

    .EXAMPLE
        Get-SDMailboxPermissionAudit -Delimiter ';' | Where-Object Orphaned

        Full audit with a CSV for German Excel; shows only permissions of deleted accounts.

    .INPUTS
        System.String. Mailbox identities, also by property name (Identity, PrimarySmtpAddress, UserPrincipalName).

    .OUTPUTS
        ServiceDeskToolkit.MailboxPermission

    .NOTES
        Author: Artur Warkentin
        Version: 1.0.0
        Date: 2026-10-01
        Changelog:
          1.0.0 (2026-10-01) First PowerShell Gallery release, no functional changes.
          0.1.0 (2026-09-30) Initial version.

    .LINK
        https://learn.microsoft.com/powershell/module/exchange/get-exomailboxpermission
    #>

    [CmdletBinding()]
    [OutputType([pscustomobject])]
    param(
        [Parameter(Position = 0, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [Alias('PrimarySmtpAddress', 'UserPrincipalName')]
        [ValidateNotNullOrEmpty()]
        [string[]]$Identity,

        [ValidateSet('UserMailbox', 'SharedMailbox', 'RoomMailbox', 'EquipmentMailbox')]
        [string[]]$RecipientTypeDetails = @('UserMailbox', 'SharedMailbox', 'RoomMailbox', 'EquipmentMailbox'),

        [ValidateSet('FullAccess', 'SendAs', 'SendOnBehalf')]
        [string[]]$PermissionType = @('FullAccess', 'SendAs', 'SendOnBehalf'),

        [ValidateNotNullOrEmpty()]
        [string]$OutputDirectory = (Get-Location).Path,

        [char]$Delimiter = ',',

        [string]$LogDirectory = $script:SDLogDirectory
    )

    begin {
        $ErrorActionPreference = 'Stop'
        $PSDefaultParameterValues = @{ 'Write-SDLog:LogDirectory' = $LogDirectory }
        $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
        $requested = New-Object -TypeName 'System.Collections.Generic.List[string]'
        $mailboxProperties = 'DisplayName', 'UserPrincipalName', 'PrimarySmtpAddress', 'RecipientTypeDetails', 'GrantSendOnBehalfTo'

        try {
            Write-SDLog -Message 'Get-SDMailboxPermissionAudit started.'
            Assert-SDExchangeConnection
        }
        catch {
            Write-SDLog -Level ERROR -Message $_.Exception.Message
            throw
        }
    }

    process {
        foreach ($id in @($Identity | Where-Object { $_ })) { $requested.Add($id) }
    }

    end {
        try {
            $failedMailboxes = 0
            if ($requested.Count -gt 0) {
                $mailboxes = New-Object -TypeName 'System.Collections.Generic.List[object]'
                foreach ($id in $requested) {
                    try {
                        $mailboxes.Add((Get-EXOMailbox -Identity $id -Properties $mailboxProperties -ErrorAction Stop))
                    }
                    catch {
                        $failedMailboxes++
                        Write-SDLog -Level ERROR -Message ("Mailbox '{0}' not found: {1}" -f $id, $_.Exception.Message)
                    }
                }
                $mailboxes = $mailboxes.ToArray()
            }
            else {
                $mailboxes = @(Get-EXOMailbox -ResultSize Unlimited -RecipientTypeDetails $RecipientTypeDetails -Properties $mailboxProperties)
            }
            Write-SDLog -Message ('{0} mailbox(es) to audit; permission types: {1}.' -f $mailboxes.Count, ($PermissionType -join ', '))

            $rows = New-Object -TypeName 'System.Collections.Generic.List[object]'
            $index = 0
            foreach ($mailbox in $mailboxes) {
                $index++
                $address = [string]$mailbox.PrimarySmtpAddress
                Write-Progress -Activity 'Auditing mailbox permissions' -Status $address -PercentComplete ([int](100 * $index / [math]::Max($mailboxes.Count, 1)))

                $addRow = {
                    param($Permission, $Trustee, $AccessType)
                    $info = Get-SDTrusteeInfo -Trustee ([string]$Trustee)
                    if ($info.IsBuiltIn) { return }
                    $rows.Add([pscustomobject]@{
                            PSTypeName  = 'ServiceDeskToolkit.MailboxPermission'
                            Mailbox     = $address
                            DisplayName = [string]$mailbox.DisplayName
                            MailboxType = [string]$mailbox.RecipientTypeDetails
                            Permission  = $Permission
                            Trustee     = [string]$Trustee
                            AccessType  = $AccessType
                            Orphaned    = $info.IsOrphaned
                        })
                }

                try {
                    if ($PermissionType -contains 'FullAccess') {
                        foreach ($entry in @(Get-EXOMailboxPermission -Identity $address)) {
                            if ($entry.IsInherited) { continue }
                            if ((@($entry.AccessRights) -join ',') -notmatch 'FullAccess') { continue }
                            $accessType = if ($entry.Deny) { 'Deny' } else { 'Allow' }
                            & $addRow 'FullAccess' $entry.User $accessType
                        }
                    }
                    if ($PermissionType -contains 'SendAs') {
                        foreach ($entry in @(Get-EXORecipientPermission -Identity $address -AccessRights SendAs)) {
                            if ($entry.IsInherited) { continue }
                            & $addRow 'SendAs' $entry.Trustee ([string]$entry.AccessControlType)
                        }
                    }
                    if ($PermissionType -contains 'SendOnBehalf') {
                        foreach ($trustee in @(Get-SDPropertyValue -InputObject $mailbox -Name 'GrantSendOnBehalfTo')) {
                            if ($trustee) { & $addRow 'SendOnBehalf' $trustee 'Allow' }
                        }
                    }
                }
                catch {
                    $failedMailboxes++
                    Write-SDLog -Level ERROR -Message ('Permissions of {0} could not be read: {1}' -f $address, $_.Exception.Message)
                }
            }
            Write-Progress -Activity 'Auditing mailbox permissions' -Completed

            $sorted = @($rows | Sort-Object -Property Mailbox, Permission, Trustee)
            $summary = [ordered]@{
                'Mailboxes'   = $mailboxes.Count
                'Permissions' = $sorted.Count
            }
            foreach ($type in $PermissionType) { $summary[$type] = @($sorted | Where-Object Permission -EQ $type).Count }
            $summary['Orphaned'] = @($sorted | Where-Object Orphaned).Count
            if ($failedMailboxes -gt 0) { $summary['Failed mailboxes'] = $failedMailboxes }

            $columns = 'Mailbox', 'DisplayName', 'MailboxType', 'Permission', 'Trustee', 'AccessType', 'Orphaned'
            $files = Export-SDReport -InputObject $sorted -Property $columns -OutputDirectory $OutputDirectory -BaseName 'MailboxPermissionAudit' `
                -Title 'Exchange Online - mailbox permissions' -Summary $summary -HighlightProperty 'Orphaned' -Delimiter $Delimiter `
                -Description 'Explicit FullAccess, SendAs and SendOnBehalf permissions. Inherited and built-in system entries are excluded. Orphaned = trustee is an unresolved SID (deleted account).'

            Write-SDLog -Message ('CSV report: {0}' -f $files.CsvPath)
            Write-SDLog -Message ('HTML report: {0}' -f $files.HtmlPath)
            if ($failedMailboxes -gt 0) {
                Write-SDLog -Level WARN -Message ('{0} mailbox(es) could not be audited - see log.' -f $failedMailboxes)
            }

            $sorted | ForEach-Object { $_ }
        }
        catch {
            Write-SDLog -Level ERROR -Message ('Get-SDMailboxPermissionAudit failed: {0}' -f $_.Exception.Message)
            throw
        }
        finally {
            $stopwatch.Stop()
            Write-SDLog -Message ('Get-SDMailboxPermissionAudit finished: {0:N1} s.' -f $stopwatch.Elapsed.TotalSeconds)
        }
    }
}