Src/Private/Get-AbrExoClientAccess.ps1

function Get-AbrExoClientAccess {
    <#
    .SYNOPSIS
    Documents Exchange Online client access protocols (OWA, MAPI, ActiveSync,
    POP, IMAP), authentication policies, and modern authentication status.
    .NOTES
        Version: 0.1.0
        Author: Pai Wei Sing
    #>

    [CmdletBinding()]
    param (
        [Parameter(Position = 0, Mandatory)]
        [string]$TenantId
    )

    begin {
        Write-PScriboMessage -Message "Collecting Exchange Online Client Access configuration for $TenantId."
        Show-AbrDebugExecutionTime -Start -TitleMessage 'ClientAccess'
    }

    process {
        Section -Style Heading2 'Protocols & Authentication' {
            Paragraph "The following section documents client access protocols, authentication policies, and how users connect to Exchange Online in tenant $TenantId."
            BlankLine

            #region Modern Auth Status
            try {
                Write-Host " - Retrieving modern authentication configuration..."
                $OrgConfig = Get-OrganizationConfig -ErrorAction Stop

                $ModernAuthEnabled   = $OrgConfig.OAuth2ClientProfileEnabled -eq $true
                $BasicAuthBlocked    = $OrgConfig.DefaultPublicFolderMailbox   # proxy check; use auth policies below

                Section -Style Heading3 'Modern Authentication Status' {
                    Paragraph "Modern Authentication (OAuth 2.0) is the recommended authentication method for Exchange Online. Legacy Basic Authentication has been retired by Microsoft for most protocols."
                    BlankLine

                    $MaObj = [System.Collections.ArrayList]::new()
                    $maInObj = [ordered] @{
                        'Modern Authentication (OAuth2) Enabled'    = $ModernAuthEnabled
                        'Recommendation'                            = if ($ModernAuthEnabled) { 'OK: Modern Auth is enabled.' } else { 'CRITICAL: Enable Modern Auth immediately.' }
                    }
                    $MaObj.Add([pscustomobject](ConvertTo-HashToYN $maInObj)) | Out-Null

                    $null = (& {
                        if ($HealthCheck.ExchangeOnline.Mailboxes) {
                            $null = ($MaObj | Where-Object { $_.'Modern Authentication (OAuth2) Enabled' -eq 'No' } | Set-Style -Style Critical | Out-Null)
                        }
                    })

                    $MaTableParams = @{ Name = "Modern Authentication - $TenantId"; List = $true; ColumnWidths = 50, 50 }
                    if ($Report.ShowTableCaptions) { $MaTableParams['Caption'] = "- $($MaTableParams.Name)" }
                    $MaObj | Table @MaTableParams
                }
            } catch {
                Write-ExoError 'ClientAccess' "Unable to retrieve modern auth status: $($_.Exception.Message)"
            }
            #endregion

            #region Authentication Policies
            try {
                Write-Host " - Retrieving authentication policies..."
                $AuthPolicies = Get-AuthenticationPolicy -ErrorAction Stop | Sort-Object Name

                if ($AuthPolicies -and @($AuthPolicies).Count -gt 0) {
                    Section -Style Heading3 'Authentication Policies' {
                        Paragraph "Authentication policies control which legacy protocols are permitted per user or tenant-wide. $(@($AuthPolicies).Count) policy/policies are configured."
                        BlankLine

                        $ApObj = [System.Collections.ArrayList]::new()
                        foreach ($Policy in $AuthPolicies) {
                            $apInObj = [ordered] @{
                                'Policy Name'                   = $Policy.Name
                                'Allow Basic - SMTP'            = $Policy.AllowBasicAuthSmtp
                                'Allow Basic - IMAP'            = $Policy.AllowBasicAuthImap
                                'Allow Basic - POP'             = $Policy.AllowBasicAuthPop
                                'Allow Basic - OWA'             = $Policy.AllowBasicAuthWebServices
                                'Allow Basic - EAS'             = $Policy.AllowBasicAuthActiveSync
                                'Allow Basic - EWS'             = $Policy.AllowBasicAuthWebServices
                                'Allow Basic - MAPI'            = $Policy.AllowBasicAuthMapi
                                'Allow Basic - PowerShell'      = $Policy.AllowBasicAuthPowershell
                            }
                            $ApObj.Add([pscustomobject](ConvertTo-HashToYN $apInObj)) | Out-Null
                        }

                        $null = (& {
                            if ($HealthCheck.ExchangeOnline.Mailboxes) {
                                # Flag any policy that allows basic auth on any protocol
                                $null = ($ApObj | Where-Object {
                                    $_.'Allow Basic - SMTP' -eq 'Yes' -or
                                    $_.'Allow Basic - IMAP' -eq 'Yes' -or
                                    $_.'Allow Basic - POP'  -eq 'Yes' -or
                                    $_.'Allow Basic - MAPI' -eq 'Yes'
                                } | Set-Style -Style Critical | Out-Null)
                            }
                        })

                        $ApTableParams = @{ Name = "Authentication Policies - $TenantId"; List = $false; ColumnWidths = 22, 9, 9, 9, 9, 9, 9, 9, 7, 8 }
                        # 9 properties: use List=$true for wide policy tables to avoid column overflow
                        $ApTableParams = @{ Name = "Authentication Policies - $TenantId"; List = $false; ColumnWidths = 24, 9, 9, 9, 9, 9, 9, 9, 13 }
                        if ($Report.ShowTableCaptions) { $ApTableParams['Caption'] = "- $($ApTableParams.Name)" }
                        if ($ApObj.Count -gt 0) { $ApObj | Table @ApTableParams }
                        $script:ExcelSheets['Auth Policies'] = $ApObj
                    }
                } else {
                    Paragraph "No custom authentication policies configured. Tenant-wide defaults apply."
                }
            } catch {
                Write-ExoError 'ClientAccess' "Unable to retrieve authentication policies: $($_.Exception.Message)"
            }
            #endregion

            #region CAS Mailbox Plans (per-plan protocol enablement)
            try {
                Write-Host " - Retrieving CAS mailbox plans..."
                $CasPlans = Get-CASMailboxPlan -ErrorAction Stop | Sort-Object DisplayName

                if ($CasPlans -and @($CasPlans).Count -gt 0) {
                    Section -Style Heading3 'Client Access Protocol Settings (by Plan)' {
                        Paragraph "CAS Mailbox Plans define which protocols are enabled per licence plan. The following $(@($CasPlans).Count) plan(s) are configured."
                        BlankLine

                        $CpObj = [System.Collections.ArrayList]::new()
                        foreach ($Plan in $CasPlans) {
                            $cpInObj = [ordered] @{
                                'Plan Name'             = $Plan.DisplayName
                                'OWA Enabled'           = $Plan.OwaMailboxPolicy -ne $null
                                'EAS Enabled'           = $Plan.ActiveSyncEnabled
                                'IMAP Enabled'          = $Plan.ImapEnabled
                                'POP Enabled'           = $Plan.PopEnabled
                                'MAPI Enabled'          = $Plan.MAPIEnabled
                                'EWS Enabled'           = $Plan.EwsEnabled
                            }
                            $CpObj.Add([pscustomobject](ConvertTo-HashToYN $cpInObj)) | Out-Null
                        }

                        $null = (& {
                            if ($HealthCheck.ExchangeOnline.Mailboxes) {
                                $null = ($CpObj | Where-Object { $_.'IMAP Enabled' -eq 'Yes' -or $_.'POP Enabled' -eq 'Yes' } | Set-Style -Style Warning | Out-Null)
                            }
                        })

                        $CpTableParams = @{ Name = "CAS Mailbox Plans - $TenantId"; List = $false; ColumnWidths = 28, 12, 12, 12, 12, 12, 12 }
                        if ($Report.ShowTableCaptions) { $CpTableParams['Caption'] = "- $($CpTableParams.Name)" }
                        if ($CpObj.Count -gt 0) { $CpObj | Table @CpTableParams }
                        $script:ExcelSheets['CAS Plans'] = $CpObj
                    }
                }
            } catch {
                Write-ExoError 'ClientAccess' "Unable to retrieve CAS mailbox plans: $($_.Exception.Message)"
            }
            #endregion

            #region OWA Policies
            try {
                Write-Host " - Retrieving OWA mailbox policies..."
                $OwaPolicies = Get-OwaMailboxPolicy -ErrorAction Stop | Sort-Object IsDefault -Descending

                if ($OwaPolicies -and @($OwaPolicies).Count -gt 0) {
                    Section -Style Heading3 'Outlook Web App (OWA) Policies' {
                        Paragraph "The following $(@($OwaPolicies).Count) OWA mailbox policy/policies control the Outlook Web App experience for users."
                        BlankLine

                        $OwaObj = [System.Collections.ArrayList]::new()
                        foreach ($Policy in $OwaPolicies) {
                            $owaInObj = [ordered] @{
                                'Policy Name'               = $Policy.Name
                                'Default Policy'            = $Policy.IsDefault
                                'Public Attachments'        = $Policy.DirectFileAccessOnPublicComputersEnabled
                                'Private Attachments'       = $Policy.DirectFileAccessOnPrivateComputersEnabled
                                'Instant Messaging'         = $Policy.InstantMessagingEnabled
                                'Text Messaging'            = $Policy.TextMessagingEnabled
                                'Theme Selection'           = $Policy.ThemeSelectionEnabled
                                'Phishing Reporting'        = $Policy.ReportJunkEmailEnabled
                                'ActiveSync Integration'    = $Policy.ActiveSyncIntegrationEnabled
                            }
                            $OwaObj.Add([pscustomobject](ConvertTo-HashToYN $owaInObj)) | Out-Null
                        }

                        # 9 columns = 100%
                        $OwaTableParams = @{ Name = "OWA Mailbox Policies - $TenantId"; List = $false; ColumnWidths = 20, 8, 10, 10, 10, 10, 9, 9, 8, 6 }
                        # Fix: 9 props → 9 widths = 100
                        $OwaTableParams = @{ Name = "OWA Mailbox Policies - $TenantId"; List = $false; ColumnWidths = 20, 9, 10, 10, 11, 10, 10, 10, 10 }
                        if ($Report.ShowTableCaptions) { $OwaTableParams['Caption'] = "- $($OwaTableParams.Name)" }
                        if ($OwaObj.Count -gt 0) { $OwaObj | Table @OwaTableParams }
                        $script:ExcelSheets['OWA Policies'] = $OwaObj
                    }
                }
            } catch {
                Write-ExoError 'ClientAccess' "Unable to retrieve OWA policies: $($_.Exception.Message)"
            }
            #endregion

            #region Per-mailbox protocol exceptions (InfoLevel 2)
            if ($InfoLevel.ClientAccess -ge 2) {
                try {
                    Write-Host " - Retrieving per-mailbox CAS settings (exceptions only)..."
                    $CasMailboxes = Get-CASMailbox -ResultSize Unlimited -ErrorAction Stop |
                        Where-Object {
                            $_.ImapEnabled -eq $true -or $_.PopEnabled -eq $true -or
                            $_.MAPIEnabled -eq $false -or $_.ActiveSyncEnabled -eq $false
                        } | Sort-Object DisplayName

                    if ($CasMailboxes -and @($CasMailboxes).Count -gt 0) {
                        Section -Style Heading3 'Per-Mailbox Protocol Exceptions' {
                            Paragraph "The following $(@($CasMailboxes).Count) mailbox(es) have non-standard protocol settings (IMAP/POP enabled, MAPI/ActiveSync disabled)."
                            BlankLine

                            $CmObj = [System.Collections.ArrayList]::new()
                            foreach ($Cas in $CasMailboxes) {
                                $cmInObj = [ordered] @{
                                    'Display Name'          = $Cas.DisplayName
                                    'UPN'                   = $Cas.PrimarySmtpAddress
                                    'MAPI Enabled'          = $Cas.MAPIEnabled
                                    'EAS Enabled'           = $Cas.ActiveSyncEnabled
                                    'OWA Enabled'           = $Cas.OWAEnabled
                                    'IMAP Enabled'          = $Cas.ImapEnabled
                                    'POP Enabled'           = $Cas.PopEnabled
                                    'EWS Enabled'           = $Cas.EwsEnabled
                                }
                                $CmObj.Add([pscustomobject](ConvertTo-HashToYN $cmInObj)) | Out-Null
                            }

                            $null = (& {
                                if ($HealthCheck.ExchangeOnline.Mailboxes) {
                                    $null = ($CmObj | Where-Object { $_.'IMAP Enabled' -eq 'Yes' -or $_.'POP Enabled' -eq 'Yes' } | Set-Style -Style Warning | Out-Null)
                                    $null = ($CmObj | Where-Object { $_.'MAPI Enabled' -eq 'No' } | Set-Style -Style Warning | Out-Null)
                                }
                            })

                            $CmTableParams = @{ Name = "Per-Mailbox Protocol Exceptions - $TenantId"; List = $false; ColumnWidths = 16, 22, 9, 9, 9, 9, 9, 9, 8 }
                            if ($Report.ShowTableCaptions) { $CmTableParams['Caption'] = "- $($CmTableParams.Name)" }
                            if ($CmObj.Count -gt 0) { $CmObj | Table @CmTableParams }
                            $script:ExcelSheets['Protocol Exceptions'] = $CmObj
                        }
                    } else {
                        Paragraph "No per-mailbox protocol exceptions detected. All mailboxes use plan defaults."
                    }
                } catch {
                    Write-ExoError 'ClientAccess' "Unable to retrieve per-mailbox CAS settings: $($_.Exception.Message)"
                }
            }
            #endregion
        }
    }

    end {
        Show-AbrDebugExecutionTime -End -TitleMessage 'ClientAccess'
    }
}