Export Mailboxes with Forwarding Configured (Exchange Online)
Audit every Exchange Online mailbox for forwarding and export the ones that forward to a CSV, using app-only certificate authentication. A fast check for the auto-forwarding attackers use to exfiltrate mail.
by Emanuel De Almeida
TL;DR
- Audits every Exchange Online user mailbox for forwarding and exports the ones that forward to a CSV under C:\Temp\Export.
- Reports the internal forwarding address, the external SMTP forwarding address and whether a local copy is kept.
- Connects app-only with a certificate, so it runs unattended for scheduled audits.
- A quick way to catch the silent auto-forwarding rules attackers use to exfiltrate mail.
- Needs an Entra app with the Exchange.ManageAsApp role and a read-level Exchange directory role.
What does this script do?
It connects to Exchange Online, lists every user mailbox, and checks each one for a forwarding address: ForwardingAddress for an internal recipient, ForwardingSmtpAddress for an external one. Any mailbox that forwards is written to a CSV, along with whether Exchange also keeps a local copy (DeliverToMailboxAndForward). Each run is logged, so it drops cleanly into a scheduled task.
Why audit mailbox forwarding?
Auto-forwarding is one of the oldest tricks in business email compromise: an attacker who gains a mailbox sets a quiet forward to an external address and keeps reading everything that arrives, long after the password is reset. Exporting forwarding configuration on a schedule turns that invisible setting into a line on a report you actually review, and it doubles as evidence for compliance and offboarding checks.
What do you need before running it?
- PowerShell 5.1 or later.
- The ExchangeOnlineManagement module.
- An Entra ID app registration with the Exchange.ManageAsApp application role (admin consent), plus the Exchange Administrator or Global Reader directory role.
- A client certificate uploaded to that app, with its thumbprint available on the machine that runs the script.
Install the Exchange Online module if you do not already have it:
Install-Module ExchangeOnlineManagement -Scope CurrentUserHow do you set up app-only authentication?
- Register (or reuse) an application in the Entra admin center.
- Under API permissions, add the Office 365 Exchange Online application permission Exchange.ManageAsApp and grant admin consent.
- Assign the app the Exchange Administrator (or Global Reader) role so it can read mailbox settings.
- Upload a certificate to the app, then copy the tenant, application (client) id and certificate thumbprint into the three variables at the top of the script.
How do you run the script?
Set the three connection variables, then run it from a session that can read the certificate's private key:
.\Export-MailboxForwarding.ps1Mailboxes that forward are written to C:\Temp\Export\Forwarding_Configured.csv, with a log under C:\Temp\Log. An empty file means nothing forwards, which is the result you want.
What is in the CSV output?
- UserPrincipalName: the mailbox owner.
- PrimarySmtpAddress: their primary address.
- DeliverAndForward: whether Exchange also keeps a local copy (DeliverToMailboxAndForward).
- ForwardingAddress: an internal forwarding recipient, if set.
- ForwardingSmtpAddress: an external SMTP forwarding target, if set.
FAQ
Does this change any mailbox settings?
No. It only reads mailbox properties and writes a CSV. It never sets, changes or removes forwarding.
What is the difference between ForwardingAddress and ForwardingSmtpAddress?
ForwardingAddress points to another recipient inside your organization; ForwardingSmtpAddress is a raw SMTP address, typically external. External forwarding is the higher risk and the one worth alerting on.
It does not catch inbox rules that forward. Why?
This checks mailbox-level forwarding. Users can also forward with inbox rules, which live elsewhere; audit those separately with Get-InboxRule if you need full coverage.
The script
#Requires -Version 5.1
#Requires -Modules ExchangeOnlineManagement
<#
.SYNOPSIS
Exports every Exchange Online mailbox that has forwarding configured.
.DESCRIPTION
Connects to Exchange Online with app-only (certificate) authentication,
checks every user mailbox for forwarding (ForwardingAddress for an internal
recipient, ForwardingSmtpAddress for an external one) and writes the ones
that forward to a CSV, including whether a local copy is kept. Logs and the
export are written under C:\Temp.
Fill in your tenant, application (client) id and certificate thumbprint
below. The app registration needs the Exchange.ManageAsApp role plus the
Exchange Administrator (or Global Reader) directory role.
.NOTES
Author : Emanuel De Almeida - https://www.navanem.com
Version: 1.0
#>
If ([Net.SecurityProtocolType]::Tls12 -bor $False) {
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Write-Host "`t Forced TLS 1.2 since it is not the server default"
}
$Global:ErrorActionPreference = 'Stop'
# ─── Connection variables (replace with your own) ───
$Tenant = '<your-tenant>.onmicrosoft.com'
$Application_ID = '<your-application-id>'
$Certificate_Thumb_Print = '<your-certificate-thumbprint>'
# Log helper
Function Write-Log {
Param(
[Parameter(Mandatory = $true)][String]$Message,
[Parameter(Mandatory = $true)][String]$Type
)
$Date = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
"$Date - $Type - $Message" |
Out-File -FilePath "C:\Temp\Log\$(Get-Date -Format 'yyyy-MM-dd').log" -Append -Encoding UTF8
}
# Make sure the output folders exist
Function CheckFilePath {
If (-not (Test-Path -Path 'C:\Temp\Log')) { New-Item 'C:\Temp\Log' -ItemType Directory | Out-Null }
If (-not (Test-Path -Path 'C:\Temp\Export')) { New-Item 'C:\Temp\Export' -ItemType Directory | Out-Null }
}
CheckFilePath
Try {
Write-Log -Message 'Connecting to Exchange Online' -Type 'Information'
Connect-ExchangeOnline -AppId $Application_ID -CertificateThumbprint $Certificate_Thumb_Print -Organization $Tenant -ShowBanner:$false
Write-Log -Message 'Connected' -Type 'Success'
} Catch {
Write-Host "`n`t$($_.InvocationInfo.InvocationName) [Line:$($_.InvocationInfo.ScriptLineNumber)]: $($_.Exception.Message)" -ForegroundColor Yellow
Write-Log -Message "$($_.InvocationInfo.InvocationName) [Line:$($_.InvocationInfo.ScriptLineNumber)]: $($_.Exception.Message)" -Type 'Error'
Write-Log -Message 'Unable to connect' -Type 'Error'
Break
}
Try {
Write-Log -Message 'Checking mailboxes for forwarding' -Type 'Information'
$Mailboxes = Get-Mailbox -RecipientTypeDetails UserMailbox -ResultSize Unlimited |
Select-Object UserPrincipalName, PrimarySmtpAddress, DeliverToMailboxAndForward, ForwardingAddress, ForwardingSmtpAddress
Foreach ($Mailbox in $Mailboxes) {
if ($Mailbox.ForwardingAddress -or $Mailbox.ForwardingSmtpAddress) {
[PSCustomObject][Ordered]@{
'UserPrincipalName' = $Mailbox.UserPrincipalName
'PrimarySmtpAddress' = $Mailbox.PrimarySmtpAddress
'DeliverAndForward' = $Mailbox.DeliverToMailboxAndForward
'ForwardingAddress' = $Mailbox.ForwardingAddress
'ForwardingSmtpAddress' = $Mailbox.ForwardingSmtpAddress
} | Export-Csv -Path 'C:\Temp\Export\Forwarding_Configured.csv' -Delimiter ',' -Encoding UTF8 -NoTypeInformation -Append -Force
}
}
Write-Log -Message 'Exported all information' -Type 'Success'
} Catch {
Write-Host "`n`t$($_.InvocationInfo.InvocationName) [Line:$($_.InvocationInfo.ScriptLineNumber)]: $($_.Exception.Message)" -ForegroundColor Yellow
Write-Log -Message "$($_.InvocationInfo.InvocationName) [Line:$($_.InvocationInfo.ScriptLineNumber)]: $($_.Exception.Message)" -Type 'Error'
Write-Log -Message 'Unable to export information' -Type 'Error'
}
Review before running. Test in a non-production environment first.
