Exchange-Online/Get-MailFlowReport.ps1

<#
.SYNOPSIS
    Reports on accepted domains, inbound/outbound connectors, and transport rules in Exchange Online.
.DESCRIPTION
    Collects mail flow configuration details including accepted domains, inbound connectors,
    outbound connectors, and transport rules. Consolidates key properties into a single
    report for M365 security assessments, migration planning, and mail routing reviews.
 
    Requires ExchangeOnlineManagement module and an active Exchange Online connection.
.PARAMETER OutputPath
    Optional path to export results as CSV. If not specified, results are returned
    to the pipeline.
.EXAMPLE
    PS> . .\Common\Connect-Service.ps1
    PS> Connect-Service -Service ExchangeOnline
    PS> .\Exchange-Online\Get-MailFlowReport.ps1
 
    Displays all accepted domains, connectors, and transport rules.
.EXAMPLE
    PS> .\Exchange-Online\Get-MailFlowReport.ps1 -OutputPath '.\mail-flow-report.csv'
 
    Exports the full mail flow configuration report to CSV.
.EXAMPLE
    PS> .\Exchange-Online\Get-MailFlowReport.ps1 -Verbose
 
    Displays the mail flow report with detailed progress messages for each item type.
#>

[CmdletBinding()]
param(
    [Parameter()]
    [ValidateNotNullOrEmpty()]
    [string]$OutputPath
)

$ErrorActionPreference = 'Stop'

# Verify EXO connection
try {
    $null = Get-OrganizationConfig -ErrorAction Stop
}
catch {
    Write-Error "Not connected to Exchange Online. Run Connect-Service -Service ExchangeOnline first."
    return
}

$results = [System.Collections.Generic.List[PSCustomObject]]::new()

# Accepted Domains
Write-Verbose "Retrieving accepted domains..."
try {
    $acceptedDomains = @(Get-AcceptedDomain)
    Write-Verbose "Found $($acceptedDomains.Count) accepted domains"

    foreach ($domain in $acceptedDomains) {
        $details = @(
            "DomainType=$($domain.DomainType)"
            "Default=$($domain.Default)"
        )

        $results.Add([PSCustomObject]@{
            ItemType = 'Domain'
            Name     = $domain.DomainName
            Status   = if ($domain.Default) { 'Default' } else { 'Active' }
            Details  = $details -join '; '
        })
    }
}
catch {
    Write-Warning "Failed to retrieve accepted domains: $_"
}

# Inbound Connectors
Write-Verbose "Retrieving inbound connectors..."
try {
    $inboundConnectors = @(Get-InboundConnector)
    Write-Verbose "Found $($inboundConnectors.Count) inbound connectors"

    foreach ($connector in $inboundConnectors) {
        $details = @(
            "ConnectorType=$($connector.ConnectorType)"
            "SenderDomains=$($connector.SenderDomains -join ', ')"
            "RequireTls=$($connector.RequireTls)"
            "RestrictDomainsToCertificate=$($connector.RestrictDomainsToCertificate)"
        )

        if ($connector.SenderIPAddresses.Count -gt 0) {
            $details += "SenderIPAddresses=$($connector.SenderIPAddresses -join ', ')"
        }

        if ($connector.TlsSenderCertificateName) {
            $details += "TlsSenderCertificateName=$($connector.TlsSenderCertificateName)"
        }

        $results.Add([PSCustomObject]@{
            ItemType = 'InboundConnector'
            Name     = $connector.Name
            Status   = if ($connector.Enabled) { 'Enabled' } else { 'Disabled' }
            Details  = $details -join '; '
        })
    }
}
catch {
    Write-Warning "Failed to retrieve inbound connectors: $_"
}

# Outbound Connectors
Write-Verbose "Retrieving outbound connectors..."
try {
    $outboundConnectors = @(Get-OutboundConnector)
    Write-Verbose "Found $($outboundConnectors.Count) outbound connectors"

    foreach ($connector in $outboundConnectors) {
        $details = @(
            "ConnectorType=$($connector.ConnectorType)"
            "RecipientDomains=$($connector.RecipientDomains -join ', ')"
            "UseMXRecord=$($connector.UseMXRecord)"
            "TlsSettings=$($connector.TlsSettings)"
        )

        if ($connector.SmartHosts.Count -gt 0) {
            $details += "SmartHosts=$($connector.SmartHosts -join ', ')"
        }

        $results.Add([PSCustomObject]@{
            ItemType = 'OutboundConnector'
            Name     = $connector.Name
            Status   = if ($connector.Enabled) { 'Enabled' } else { 'Disabled' }
            Details  = $details -join '; '
        })
    }
}
catch {
    Write-Warning "Failed to retrieve outbound connectors: $_"
}

# Transport Rules
Write-Verbose "Retrieving transport rules..."
try {
    $transportRules = @(Get-TransportRule)
    Write-Verbose "Found $($transportRules.Count) transport rules"

    foreach ($rule in $transportRules) {
        $details = @(
            "Priority=$($rule.Priority)"
            "Mode=$($rule.Mode)"
        )

        if ($rule.SentTo) {
            $details += "SentTo=$($rule.SentTo -join ', ')"
        }
        if ($rule.SentToMemberOf) {
            $details += "SentToMemberOf=$($rule.SentToMemberOf -join ', ')"
        }
        if ($rule.FromMemberOf) {
            $details += "FromMemberOf=$($rule.FromMemberOf -join ', ')"
        }
        if ($rule.From) {
            $details += "From=$($rule.From -join ', ')"
        }
        if ($rule.SubjectContainsWords) {
            $details += "SubjectContains=$($rule.SubjectContainsWords -join ', ')"
        }
        if ($rule.HasAttachment) {
            $details += "HasAttachment=$($rule.HasAttachment)"
        }

        # Capture the actions applied by this rule
        $actionParts = @()
        if ($rule.AddToRecipients) {
            $actionParts += "AddToRecipients"
        }
        if ($rule.BlindCopyTo) {
            $actionParts += "BlindCopyTo"
        }
        if ($rule.ModerateMessageByUser) {
            $actionParts += "ModerateMessageByUser"
        }
        if ($rule.RejectMessageReasonText) {
            $actionParts += "RejectMessage"
        }
        if ($rule.DeleteMessage) {
            $actionParts += "DeleteMessage"
        }
        if ($rule.PrependSubject) {
            $actionParts += "PrependSubject=$($rule.PrependSubject)"
        }
        if ($rule.SetHeaderName) {
            $actionParts += "SetHeader=$($rule.SetHeaderName):$($rule.SetHeaderValue)"
        }
        if ($rule.ApplyHtmlDisclaimerText) {
            $actionParts += "ApplyDisclaimer"
        }

        if ($actionParts.Count -gt 0) {
            $details += "Actions=$($actionParts -join ', ')"
        }

        $results.Add([PSCustomObject]@{
            ItemType = 'TransportRule'
            Name     = $rule.Name
            Status   = if ($rule.State -eq 'Enabled') { 'Enabled' } else { 'Disabled' }
            Details  = $details -join '; '
        })
    }
}
catch {
    Write-Warning "Failed to retrieve transport rules: $_"
}

Write-Verbose "Mail flow report complete: $($results.Count) total items"

if ($OutputPath) {
    $results | Export-Csv -Path $OutputPath -NoTypeInformation -Encoding UTF8
    Write-Output "Exported mail flow report ($($results.Count) items) to $OutputPath"
}
else {
    Write-Output $results
}