public/Get-MsecExchangeMailbox.ps1

function Get-MsecExchangeMailbox {
    <#
    .SYNOPSIS
        Mailboxes with the two things that actually leak mail: where it is forwarded, and which
        legacy protocols are open.

    .DESCRIPTION
        FORWARDING IS THE EXFILTRATION CONTROL, and it comes in two shapes that are NOT the same
        risk. ForwardingSmtpAddress is a raw address that can point anywhere, including outside
        the tenant. ForwardingAddress must resolve to an existing recipient object, so it cannot
        name an arbitrary stranger. Reporting them as one column loses that, so both are on the
        row and ForwardingKind says which is in play. Measured on one tenant: 34 of 305 mailboxes
        forwarded, 11 by raw SMTP, four of those to addresses outside every accepted domain.

        THE POINT IS NOT THAT FORWARDING IS BAD. Most of it is deliberate - ticketing systems,
        Teams, product feedback tools. The point is that nothing tells you when a new one
        appears, and an attacker's forward looks exactly like a legitimate one in the portal.
        This command exists to be diffed, not to be read once.

        DeliverToMailboxAndForward DECIDES WHETHER THE USER EVER SEES THE MAIL. False means the
        message leaves and no copy stays behind - the mailbox owner has no way to notice. That
        is the more dangerous configuration and it is a separate column for that reason.

        SMTP AUTH IS RESOLVED, NOT REPORTED RAW. The per-mailbox setting is often $null, meaning
        "inherit the tenant default", and $null read as a boolean is false - so an unresolved
        value claims SMTP AUTH is off on every mailbox that never set it. SmtpAuthEnabled is the
        EFFECTIVE answer and SmtpAuthSource says whether it came from the mailbox or the tenant.
        It is the protocol worth caring about most, because it bypasses multi-factor
        authentication outright.

        POP, IMAP and ActiveSync are reported as found. On most tenants they are enabled
        everywhere because that is the default nobody changed, which is attack surface rather
        than a misconfiguration - judge it against whether anyone actually uses them.

    .PARAMETER RecipientTypeDetails
        Mailbox types to include. Defaults to All, unlike Get-MsecExchangeMailboxPermission -
        forwarding matters on every mailbox, not only shared ones.

    .PARAMETER ForwardingOnly
        Only mailboxes that forward somewhere.

    .EXAMPLE
        Connect-Msec -KeyVaultName kv-msec
        Get-MsecExchangeMailbox -ForwardingOnly |
            Format-Table UserPrincipalName, ForwardingKind, ForwardingTarget, IsForwardingExternal, DeliverToMailboxAndForward

    .EXAMPLE
        # Mail leaving the tenant with no copy left behind - the owner cannot notice.
        Get-MsecExchangeMailbox -ForwardingOnly |
            Where-Object { $_.IsForwardingExternal -and -not $_.DeliverToMailboxAndForward }

    .EXAMPLE
        # SMTP AUTH is the one that bypasses MFA.
        Get-MsecExchangeMailbox | Where-Object SmtpAuthEnabled |
            Format-Table UserPrincipalName, SmtpAuthEnabled, SmtpAuthSource

    .OUTPUTS
        One PSCustomObject per mailbox, PSTypeName 'MsecExchangeMailbox'.

    .NOTES
        Needs Connect-Msec. The Exchange session is opened on first use.

        Two bulk calls (Get-EXOMailbox, Get-EXOCasMailbox) rather than one per mailbox, so this
        stays workable on a large tenant. Read-only.
    #>

    [CmdletBinding()]
    [OutputType([PSCustomObject])]
    param(
        [ValidateSet('SharedMailbox', 'UserMailbox', 'RoomMailbox', 'EquipmentMailbox', 'All')]
        [string[]] $RecipientTypeDetails = 'All',

        [switch] $ForwardingOnly
    )

    Initialize-MsecExoSession -Endpoint Exchange

    $mailboxParams = @{ ErrorAction = 'Stop' }
    if ($RecipientTypeDetails -notcontains 'All') {
        $mailboxParams['RecipientTypeDetails'] = $RecipientTypeDetails
    }

    # PropertySets keeps the response small; Delivery carries the forwarding fields.
    $mailboxes = @(Get-EXOMailbox @mailboxParams -PropertySets Minimum, Delivery -ResultSize Unlimited)

    # Accepted domains decide what "external" means. If this fails, IsForwardingExternal goes
    # $null rather than $false - claiming a forward is internal when it was never checked is
    # the one answer that would matter and be wrong.
    $domains = $null
    try {
        $domains = @((Get-AcceptedDomain -ErrorAction Stop).DomainName | ForEach-Object { "$_".ToLower() })
    }
    catch {
        Write-Warning "Could not read accepted domains, so IsForwardingExternal is null rather than false: $($_.Exception.Message)"
    }

    # The tenant default that a $null per-mailbox SMTP AUTH setting inherits.
    $tenantSmtpAuthDisabled = $null
    try { $tenantSmtpAuthDisabled = (Get-TransportConfig -ErrorAction Stop).SmtpClientAuthenticationDisabled }
    catch {
        Write-Warning "Could not read the tenant SMTP AUTH default, so SmtpAuthEnabled is null where a mailbox inherits it: $($_.Exception.Message)"
    }

    $cas = @{}
    try {
        foreach ($c in @(Get-EXOCasMailbox -ResultSize Unlimited -ErrorAction Stop)) {
            $cas[[string] $c.ExternalDirectoryObjectId] = $c
        }
    }
    catch {
        Write-Warning "Could not read CAS mailbox settings, so the protocol columns are null rather than false: $($_.Exception.Message)"
    }

    foreach ($mailbox in $mailboxes) {
        $smtpTarget = "$($mailbox.ForwardingSmtpAddress)" -replace '^smtp:', ''
        $objTarget  = "$($mailbox.ForwardingAddress)"

        $kind = if ($smtpTarget) { 'SmtpAddress' } elseif ($objTarget) { 'Recipient' } else { 'None' }
        if ($ForwardingOnly -and $kind -eq 'None') { continue }

        # Only a raw SMTP address can be judged here. A ForwardingAddress names a recipient
        # object, which needs a lookup this command does not do - so it is $null, not $false.
        $isExternal = $null
        if ($kind -eq 'SmtpAddress' -and $null -ne $domains) {
            $suffix = ($smtpTarget -split '@')[-1]
            $isExternal = [bool] ($suffix -and $suffix.ToLower() -notin $domains)
        }

        $c = $cas[[string] $mailbox.ExternalDirectoryObjectId]

        # $null when the mailbox does not set it: the tenant default applies, and reading $null
        # as a boolean would report "disabled" for every mailbox that simply never set it.
        $smtpAuthEnabled = $null
        $smtpAuthSource  = $null
        if ($c) {
            if ($null -ne $c.SmtpClientAuthenticationDisabled) {
                $smtpAuthEnabled = -not $c.SmtpClientAuthenticationDisabled
                $smtpAuthSource  = 'Mailbox'
            }
            elseif ($null -ne $tenantSmtpAuthDisabled) {
                $smtpAuthEnabled = -not $tenantSmtpAuthDisabled
                $smtpAuthSource  = 'Tenant'
            }
        }

        [PSCustomObject]@{
            PSTypeName                 = 'MsecExchangeMailbox'
            DisplayName                = [string] $mailbox.DisplayName
            UserPrincipalName          = [string] $mailbox.UserPrincipalName
            PrimarySmtpAddress         = [string] $mailbox.PrimarySmtpAddress
            MailboxType                = [string] $mailbox.RecipientTypeDetails
            ForwardingKind             = $kind
            ForwardingTarget           = $(if ($smtpTarget) { $smtpTarget } elseif ($objTarget) { $objTarget } else { $null })
            ForwardingSmtpAddress      = $(if ($smtpTarget) { $smtpTarget } else { $null })
            ForwardingAddress          = $(if ($objTarget) { $objTarget } else { $null })
            # $null for a Recipient forward - see the comment above, it is not "internal".
            IsForwardingExternal       = $isExternal
            # False means no copy stays behind, so the owner cannot notice the mail leaving.
            DeliverToMailboxAndForward = $(if ($kind -eq 'None') { $null } else { [bool] $mailbox.DeliverToMailboxAndForward })
            SmtpAuthEnabled            = $smtpAuthEnabled
            SmtpAuthSource             = $smtpAuthSource
            PopEnabled                 = $(if ($c) { [bool] $c.PopEnabled } else { $null })
            ImapEnabled                = $(if ($c) { [bool] $c.ImapEnabled } else { $null })
            ActiveSyncEnabled          = $(if ($c) { [bool] $c.ActiveSyncEnabled } else { $null })
            OwaEnabled                 = $(if ($c) { [bool] $c.OWAEnabled } else { $null })
        }
    }
}