All scripts
Governance 452

Get-MailboxForwardingRules

Scans every mailbox in the tenant for external forwarding, both the ForwardingSmtpAddress set on the mailbox itself and inbox rules that forward or redirect to an outside address. Useful for catching a compromised account or an old forwarding rule someone set up and forgot about.

Get-MailboxForwardingRules.ps1
<#
.SYNOPSIS
    Reports every mailbox in the tenant that is forwarding mail outside the organization.

.DESCRIPTION
    Checks two places mail can leave your tenant without anyone noticing: the
    ForwardingSmtpAddress set on the mailbox itself, and inbox rules that forward
    or redirect messages to an external address. Compromised accounts and old
    "forward to my personal email" rules both show up here. Run it regularly,
    not just once, since a new forwarding rule can appear at any time.

.AUTHOR
    Shehryar Hassan

.EXAMPLE
    Connect-ExchangeOnline
    .\Get-MailboxForwardingRules.ps1 -CsvPath "C:\Reports\ExternalForwarding.csv"

    Connects to Exchange Online first, then runs the report and saves the
    results to a CSV file for review.
#>

[CmdletBinding()]
param(
    [string]$CsvPath = ".\ExternalForwarding-Report.csv"
)

if (-not (Get-Module -ListAvailable -Name ExchangeOnlineManagement)) {
    Write-Error "The ExchangeOnlineManagement module is not installed. Run: Install-Module ExchangeOnlineManagement"
    return
}

try {
    Get-ConnectionInformation -ErrorAction Stop | Out-Null
}
catch {
    Write-Error "Not connected to Exchange Online. Run Connect-ExchangeOnline first."
    return
}

Write-Host "Pulling mailbox list, this can take a while on larger tenants..." -ForegroundColor Cyan
$mailboxes = Get-Mailbox -ResultSize Unlimited -RecipientTypeDetails UserMailbox, SharedMailbox

$findings = New-Object System.Collections.Generic.List[Object]

foreach ($mailbox in $mailboxes) {

    # Mailbox level forwarding, set directly on the mailbox object.
    if ($mailbox.ForwardingSmtpAddress -or $mailbox.ForwardingAddress) {
        $target = if ($mailbox.ForwardingSmtpAddress) { $mailbox.ForwardingSmtpAddress } else { $mailbox.ForwardingAddress }
        $findings.Add([PSCustomObject]@{
            Mailbox       = $mailbox.PrimarySmtpAddress
            Source        = "Mailbox forwarding"
            ForwardsTo    = $target
            DeliverToBoth = $mailbox.DeliverToMailboxAndForward
            RuleName      = $null
        })
    }

    # Inbox rules that forward or redirect to an outside address.
    $rules = Get-InboxRule -Mailbox $mailbox.PrimarySmtpAddress -ErrorAction SilentlyContinue |
        Where-Object { $_.ForwardTo -or $_.RedirectTo -or $_.ForwardAsAttachmentTo }

    foreach ($rule in $rules) {
        $targets = @($rule.ForwardTo) + @($rule.RedirectTo) + @($rule.ForwardAsAttachmentTo) |
            Where-Object { $_ } |
            ForEach-Object { $_.ToString() }

        $externalTargets = $targets | Where-Object { $_ -notmatch [regex]::Escape(($mailbox.PrimarySmtpAddress -split "@")[1]) }

        if ($externalTargets) {
            $findings.Add([PSCustomObject]@{
                Mailbox       = $mailbox.PrimarySmtpAddress
                Source        = "Inbox rule"
                ForwardsTo    = ($externalTargets -join "; ")
                DeliverToBoth = $rule.Enabled
                RuleName      = $rule.Name
            })
        }
    }
}

if ($findings.Count -eq 0) {
    Write-Host "No external forwarding found. Nothing to report." -ForegroundColor Green
    return
}

$findings | Sort-Object Mailbox | Format-Table -AutoSize
$findings | Sort-Object Mailbox | Export-Csv -Path $CsvPath -NoTypeInformation

Write-Host ""
Write-Host "$($findings.Count) forwarding entries found. Saved to $CsvPath" -ForegroundColor Yellow
Write-Host "Check each one, some are legitimate (a shared mailbox forwarding to a ticketing system), but anything you do not recognize is worth a closer look." -ForegroundColor Yellow

Read it before you run it, and test in a safe tenant first.