Private/Expand-METRuleRecipients.ps1

function Expand-METRuleRecipients {
    # Resolves which mailboxes in $AllMailboxes are covered by a single policy rule,
    # respecting SentTo / SentToMemberOf / RecipientDomainIs include conditions and
    # their ExceptIf counterparts. When no include conditions are present the rule is
    # treated as a catch-all and covers all mailboxes before exceptions are applied.
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [object]    $Rule,
        [Parameter(Mandatory)] [AllowEmptyCollection()] [string[]] $AllMailboxes,
        [Parameter(Mandatory)] [hashtable] $GroupCache,
        [ValidateSet('Recipient','Sender')] [string] $ScopeType = 'Recipient',
        [System.Collections.Generic.List[string]] $RetrievalErrors
    )

    function New-AddressSet {
        return ,([System.Collections.Generic.HashSet[string]]::new(
            [System.StringComparer]::OrdinalIgnoreCase))
    }

    function Test-RecipientDomain {
        param([string] $Address, [object[]] $Domains)

        if ($Address -notmatch '@(?<Domain>[^@]+)$') { return $false }
        $recipientDomain = $Matches.Domain.TrimEnd('.')
        foreach ($configuredDomain in @($Domains)) {
            $candidate = ([string]$configuredDomain).Trim().TrimStart('@').TrimEnd('.')
            if (-not $candidate) { continue }
            if ($recipientDomain.Equals($candidate, [System.StringComparison]::OrdinalIgnoreCase) -or
                $recipientDomain.EndsWith(".$candidate", [System.StringComparison]::OrdinalIgnoreCase)) {
                return $true
            }
        }
        return $false
    }

    $covered = New-AddressSet
    $directProperty = if ($ScopeType -eq 'Sender') { 'From' } else { 'SentTo' }
    $groupProperty = if ($ScopeType -eq 'Sender') { 'FromMemberOf' } else { 'SentToMemberOf' }
    $domainProperty = if ($ScopeType -eq 'Sender') { 'SenderDomainIs' } else { 'RecipientDomainIs' }
    $exceptDirectProperty = if ($ScopeType -eq 'Sender') { 'ExceptIfFrom' } else { 'ExceptIfSentTo' }
    $exceptGroupProperty = if ($ScopeType -eq 'Sender') { 'ExceptIfFromMemberOf' } else { 'ExceptIfSentToMemberOf' }
    $exceptDomainProperty = if ($ScopeType -eq 'Sender') { 'ExceptIfSenderDomainIs' } else { 'ExceptIfRecipientDomainIs' }

    # ── Include conditions ────────────────────────────────────────────────────
    $hasInclude = $false

    if ($Rule.$directProperty) {
        $hasInclude = $true
        $conditionMatches = New-AddressSet
        foreach ($addr in @($Rule.$directProperty)) { $null = $conditionMatches.Add([string]$addr) }
        $conditionMatches.IntersectWith([string[]]$AllMailboxes)
        $covered = $conditionMatches
    }

    if ($Rule.$groupProperty) {
        $conditionMatches = New-AddressSet
        foreach ($grp in @($Rule.$groupProperty)) {
            $members = @(Expand-METGroupMembership -Identity $grp -Cache $GroupCache -RetrievalErrors $RetrievalErrors)
            foreach ($m in $members) { $null = $conditionMatches.Add([string]$m) }
        }
        $conditionMatches.IntersectWith([string[]]$AllMailboxes)
        if ($hasInclude) { $covered.IntersectWith($conditionMatches) } else { $covered = $conditionMatches }
        $hasInclude = $true
    }

    if ($Rule.$domainProperty) {
        $conditionMatches = New-AddressSet
        foreach ($mbx in $AllMailboxes) {
            if (Test-RecipientDomain -Address $mbx -Domains @($Rule.$domainProperty)) {
                $null = $conditionMatches.Add($mbx)
            }
        }
        if ($hasInclude) { $covered.IntersectWith($conditionMatches) } else { $covered = $conditionMatches }
        $hasInclude = $true
    }

    # No include conditions = catch-all rule; covers every mailbox before exceptions are applied
    if (-not $hasInclude) {
        foreach ($mbx in $AllMailboxes) { $null = $covered.Add($mbx) }
    }

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

    # Intersect with actual mailboxes - rules may reference addresses outside the tenant
    $mailboxSet = [System.Collections.Generic.HashSet[string]]::new(
        [string[]]$AllMailboxes, [System.StringComparer]::OrdinalIgnoreCase)
    $null = $covered.IntersectWith($mailboxSet)

    # ── Exception conditions ──────────────────────────────────────────────────
    if ($Rule.$exceptDirectProperty) {
        foreach ($addr in @($Rule.$exceptDirectProperty)) { $null = $covered.Remove($addr) }
    }

    if ($Rule.$exceptGroupProperty) {
        foreach ($grp in @($Rule.$exceptGroupProperty)) {
            $members = @(Expand-METGroupMembership -Identity $grp -Cache $GroupCache -RetrievalErrors $RetrievalErrors)
            foreach ($m in $members) { $null = $covered.Remove($m) }
        }
    }

    if ($Rule.$exceptDomainProperty) {
        $toRemove = @($covered | Where-Object {
            Test-RecipientDomain -Address $_ -Domains @($Rule.$exceptDomainProperty)
        })
        foreach ($addr in $toRemove) { $null = $covered.Remove($addr) }
    }

    return [string[]]$covered
}