Private/SharedMailboxes.ps1

function Get-M365SnapshotSharedMailboxes {
    param(
        [Parameter(Mandatory=$true)]
        [hashtable]$GraphHeaders,

        [Parameter(Mandatory=$true)]
        [ValidateRange(1, [int]::MaxValue)]
        [int]$EffectiveMaxSharedMailboxes,

        [Parameter(Mandatory=$true)]
        [switch]$ReturnObjects
    )

    $sharedMailboxes = @()
    $candidateUsers = @()
    $permissionWarningShown = $false
    $mailboxSettingsAccessDenied = $false

    try {
        $usersUri = "https://graph.microsoft.com/v1.0/users?`$select=id,displayName,userPrincipalName,mail,createdDateTime,accountEnabled,userType,assignedLicenses&`$top=999"

        do {
            $usersResponse = Invoke-RestMethod -Uri $usersUri -Headers $GraphHeaders -Method Get -ErrorAction Stop
            if ($usersResponse.value) {
                $candidateUsers += @($usersResponse.value | Where-Object { -not [string]::IsNullOrWhiteSpace([string]$_.mail) })
            }
            $usersUri = $usersResponse.'@odata.nextLink'
        } while ($usersUri)

        foreach ($candidate in $candidateUsers) {
            if ($sharedMailboxes.Count -ge $EffectiveMaxSharedMailboxes) {
                break
            }

            if ([string]::IsNullOrWhiteSpace([string]$candidate.id)) {
                continue
            }

            try {
                $mailboxSettingsUri = "https://graph.microsoft.com/beta/users/$($candidate.id)/mailboxSettings?`$select=userPurpose"
                $mailboxSettings = Invoke-RestMethod -Uri $mailboxSettingsUri -Headers $GraphHeaders -Method Get -ErrorAction Stop
                $userPurpose = [string]$mailboxSettings.userPurpose

                if ($userPurpose -ieq 'shared') {
                    $sharedMailboxes += [PSCustomObject]@{
                        DisplayName = $candidate.displayName
                        PrimarySmtpAddress = $candidate.mail
                        UserPrincipalName = $candidate.userPrincipalName
                        RecipientTypeDetails = 'SharedMailbox'
                        WhenCreatedUTC = $candidate.createdDateTime
                        HiddenFromAddressLists = '(unknown)'
                        DetectionSource = 'GraphMailboxSettings'
                    }
                }
            }
            catch {
                $settingsError = [string]$_.Exception.Message
                if (-not $permissionWarningShown -and ($settingsError -match 'MailboxSettings.Read' -or $settingsError -match 'Insufficient privileges' -or $settingsError -match 'Authorization_RequestDenied' -or $settingsError -match 'Forbidden')) {
                    if (-not $ReturnObjects) {
                        Write-Host "[WARNING] Graph mailbox settings access is missing (MailboxSettings.Read). Shared mailbox detection requires this optional delegated Graph scope." -ForegroundColor Yellow
                        Write-Host "[INFO] Falling back to heuristic detection from Entra user properties (best-effort)." -ForegroundColor DarkGray
                        Write-Host "[INFO] For precise detection, grant delegated MailboxSettings.Read and rerun." -ForegroundColor DarkGray
                        Write-Host "[INFO] If you use an existing app, add delegated Microsoft Graph scope MailboxSettings.Read and grant admin consent." -ForegroundColor DarkGray
                    }
                    $permissionWarningShown = $true
                    $mailboxSettingsAccessDenied = $true
                    break
                }
            }
        }

        if ($mailboxSettingsAccessDenied -and $sharedMailboxes.Count -eq 0) {
            $heuristicShared = @(
                $candidateUsers | Where-Object {
                    -not [string]::IsNullOrWhiteSpace([string]$_.mail) -and
                    ([bool]$_.accountEnabled -eq $false) -and
                    (([string]$_.userType) -eq 'Member') -and
                    (@($_.assignedLicenses).Count -eq 0)
                } | Select-Object -First $EffectiveMaxSharedMailboxes
            )

            foreach ($candidate in $heuristicShared) {
                $sharedMailboxes += [PSCustomObject]@{
                    DisplayName = $candidate.displayName
                    PrimarySmtpAddress = $candidate.mail
                    UserPrincipalName = $candidate.userPrincipalName
                    RecipientTypeDetails = 'LikelySharedMailbox'
                    WhenCreatedUTC = $candidate.createdDateTime
                    HiddenFromAddressLists = '(unknown)'
                    DetectionSource = 'HeuristicUserProperties'
                }
            }

            if (-not $ReturnObjects) {
                Write-Host "[INFO] Heuristic detection identified $($sharedMailboxes.Count) likely shared mailbox(es)." -ForegroundColor DarkGray
            }
        }

        if (-not $ReturnObjects) {
            Write-Host "[OK] Found $($sharedMailboxes.Count) shared mailbox(es)`n" -ForegroundColor Green
        }
    }
    catch {
        if (-not $ReturnObjects) {
            Write-Host "[WARNING] Could not collect shared mailboxes: $($_.Exception.Message)`n" -ForegroundColor Yellow
        }
    }

    return [PSCustomObject]@{
        SharedMailboxes = @($sharedMailboxes)
    }
}