
Bulk-Update Exchange Mailbox Forwarding from a CSV
Before you run this
This guide sets mailbox-level forwarding (the ForwardingSmtpAddress / ForwardingAddress and DeliverToMailboxAndForward properties) on many mailboxes at once, reading the target address for each mailbox from a CSV file. It uses Set-Mailbox in Exchange Online PowerShell. This is not the same as an Outlook inbox rule — it is the server-side forwarding an admin sets, and it applies regardless of whether the user's Outlook is open.
Privileges: You need to connect to Exchange Online with an account holding the Exchange Administrator role (or a custom role with Recipient Management). You do not need a local elevated (Administrator) PowerShell session to run the script — but installing the module for all users does need one. Installing it with -Scope CurrentUser avoids that.
Test first. Read the script before you run it. The script below has a -WhatIf dry-run switch on by default — leave it on for the first pass and run it against a single test mailbox before you point it at a real list. Do not paste-and-run it against a live tenant blind.
This changes mail flow, and one setting is a foot-gun. If DeliverToMailboxAndForward is $false, incoming mail is forwarded away and not kept in the mailbox — the user stops seeing their own mail, and there is no copy to recover. Forwarding is also a classic data-exfiltration path, which is why it is worth doing deliberately and auditing afterward. The setting is reversible (you can clear it), but any mail that was forwarded while it was live is already gone. Before you make any change, run the backup step below to capture the current forwarding state of every mailbox you are about to touch, so you can restore it.
What I'm assuming
- Exchange Online (Microsoft 365). The same
Set-Mailboxparameters exist in on-prem Exchange Management Shell, so the logic carries over, but I'm writing for EXO. - The ExchangeOnlineManagement module, v3 or later (this is the module that provides
Connect-ExchangeOnline). - PowerShell 5.1 or 7 on Windows. The module supports both.
If you don't have the module:
# CurrentUser scope avoids needing an elevated session
Install-Module ExchangeOnlineManagement -Scope CurrentUser
The CSV format
Keep the CSV simple and explicit. One row per mailbox:
UserPrincipalName,ForwardingSmtpAddress,DeliverToMailboxAndForward
alice@example.com,alice.archive@partner.example.net,TRUE
bob@example.com,bob.backup@partner.example.net,FALSE
- UserPrincipalName — the mailbox to change. Any value
Get-Mailboxaccepts (UPN or primary SMTP) works, but UPN is unambiguous. - ForwardingSmtpAddress — the external SMTP address to forward to. For forwarding to another internal mailbox you'd use
ForwardingAddressinstead, which takes a recipient the tenant knows; this guide usesForwardingSmtpAddressbecause bulk forwarding to outside addresses is the common case. If you need the internal-recipient variant, check theSet-Mailboxreference on Microsoft Learn for-ForwardingAddress. - DeliverToMailboxAndForward —
TRUEkeeps a copy in the mailbox and forwards;FALSEforwards only. Set this per row on purpose.
Save it as C:\path\to\forwarding.csv (substitute your own path).
Connect and back up first
Connect once at the top of your session:
Connect-ExchangeOnline -UserPrincipalName admin@example.com
Now capture the current state of every mailbox in the CSV, before changing anything. This file is your undo:
$csv = Import-Csv "C:\path\to\forwarding.csv"
$backup = foreach ($row in $csv) {
Get-Mailbox -Identity $row.UserPrincipalName |
Select-Object UserPrincipalName,
ForwardingSmtpAddress,
ForwardingAddress,
DeliverToMailboxAndForward
}
# Timestamped so you never overwrite a previous backup
$stamp = Get-Date -Format 'yyyyMMdd-HHmmss'
$backup | Export-Csv "C:\path\to\forwarding-backup-$stamp.csv" -NoTypeInformation
Open that backup and confirm it has a row for every mailbox you expect before you go further.
The update script
Safe by default: $WhatIf at the top is $true, so the first run only reports what it would do. Read the output, then set it to $false and run for real.
# ---- settings ----
$CsvPath = "C:\path\to\forwarding.csv" # replace with your file
$WhatIf = $true # $true = dry run; set $false to apply
# ------------------
$csv = Import-Csv $CsvPath
foreach ($row in $csv) {
# Convert the CSV text "TRUE"/"FALSE" into a real boolean
$deliverAndForward = [System.Convert]::ToBoolean($row.DeliverToMailboxAndForward)
try {
Set-Mailbox -Identity $row.UserPrincipalName `
-ForwardingSmtpAddress $row.ForwardingSmtpAddress `
-DeliverToMailboxAndForward $deliverAndForward `
-WhatIf:$WhatIf `
-ErrorAction Stop
$action = if ($WhatIf) { "WOULD SET" } else { "SET" }
Write-Host "$action $($row.UserPrincipalName) -> $($row.ForwardingSmtpAddress) (keep copy: $deliverAndForward)"
}
catch {
Write-Warning "FAILED $($row.UserPrincipalName): $_"
}
}
Notes on the non-obvious lines:
[System.Convert]::ToBoolean(...)turns the CSV'sTRUE/FALSEtext into an actual$true/$false, because-DeliverToMailboxAndForwardexpects a boolean, not a string.-WhatIf:$WhatIfpasses the switch through from the top variable, so one flag controls the whole run.-ErrorAction Stopinside thetrymeans a bad row (a mailbox that doesn't exist, a malformed address) is caught and logged, and the loop keeps going instead of dying on the first error.
Run it once with $WhatIf = $true, read every line of output, fix any FAILED rows in the CSV, then change $WhatIf to $false and run again.
Verify it worked
Pull the forwarding properties back for the mailboxes you touched and eyeball them:
Import-Csv "C:\path\to\forwarding.csv" | ForEach-Object {
Get-Mailbox -Identity $_.UserPrincipalName
} | Select-Object UserPrincipalName,
ForwardingSmtpAddress,
DeliverToMailboxAndForward |
Format-Table -AutoSize
Each row should show the address from your CSV and the matching DeliverToMailboxAndForward value. It can take a short while for the change to be visible if the connection is being throttled, but Get-Mailbox reads the same directory you wrote to, so it's usually immediate.
For a wider audit — catching forwarding you didn't set — you can list every mailbox in the tenant that has forwarding configured:
Get-Mailbox -ResultSize Unlimited |
Where-Object { $_.ForwardingSmtpAddress -or $_.ForwardingAddress } |
Select-Object UserPrincipalName, ForwardingSmtpAddress, ForwardingAddress, DeliverToMailboxAndForward
Undo
To clear forwarding on a single mailbox, set the address to $null. It's good practice to also put DeliverToMailboxAndForward back to $true so nothing is silently dropped while you sort things out:
Set-Mailbox -Identity alice@example.com `
-ForwardingSmtpAddress $null `
-DeliverToMailboxAndForward $true
To restore the exact state you captured earlier, drive it from the backup CSV. Note that empty cells in the backup come back as empty strings, so translate those to $null:
$restore = Import-Csv "C:\path\to\forwarding-backup-20240101-120000.csv" # your backup file
foreach ($row in $restore) {
$fwd = if ([string]::IsNullOrWhiteSpace($row.ForwardingSmtpAddress)) { $null } else { $row.ForwardingSmtpAddress }
$deliver = [System.Convert]::ToBoolean($row.DeliverToMailboxAndForward)
Set-Mailbox -Identity $row.UserPrincipalName `
-ForwardingSmtpAddress $fwd `
-DeliverToMailboxAndForward $deliver `
-WhatIf # remove -WhatIf once you've confirmed the output
}
This restore only handles ForwardingSmtpAddress; if any mailbox originally used ForwardingAddress (internal recipient), restore that property the same way after checking the exact parameter usage in the Set-Mailbox reference on Microsoft Learn.
When you're done, close the session cleanly:
Disconnect-ExchangeOnline -Confirm:$false
For the exact, current parameter list — Microsoft occasionally adjusts these — see the Set-Mailbox and Connect-ExchangeOnline reference pages on Microsoft Learn.
Runs enterprise networks and security for a living, and writes Shore Up to turn two decades of hands-on Linux, Windows and mail-server work into guides you can actually use.
More about the author →Was this article helpful?
Tap a star — no sign-in needed.
Be the first to rate this article.
Related guides
Bulk-Export Exchange Distribution List Membership for Compliance
An auditor asks the same question every year: who was in which distribution list on this date? Clicking through each group in the admin center does not scale past a handful of groups, and it produces nothing you can hand over. This guide scripts a clean, point-in-time CSV of every distribution group and its members.
Bulk-Set Out-of-Office Auto-Replies with Exchange PowerShell
This guide sets the Automatic Replies (out-of-office) configuration on multiple mailboxes at once, using Set-MailboxAutoReplyConfiguration in Exchange Online PowerShell. It turns auto-reply on (or schedules it), and writes the internal and external message text. It does not delete mail, move anything, or change mailbox permissions — but it does overwrite whatever auto-reply text and state each affected mailbox currently has, and there is no built-in "undo" that restores the previous message. If a user had their own carefully worded reply set, this replaces it. So capture the current state first (I show how below) and treat the change list as production data.
Automate User Offboarding in Active Directory with PowerShell
This script offboards one leaving user in a single pass: it disables their AD account, records and removes their group memberships (except the primary group), and moves the account into a disabled-users OU. A separate, clearly marked step sets mail forwarding on their mailbox. The point is a consistent, logged procedure so nothing gets missed and you can reconstruct exactly what changed.
Automate New-User Onboarding in Active Directory
This guide builds a PowerShell script that onboards one new employee in four steps: it creates an Active Directory user account , adds them to security groups , provisions an on-premises Exchange mailbox , and creates their home folder on a file server and sets NTFS permissions . The purpose is to replace the error-prone click-through in Active Directory Users and Computers with one repeatable, reviewable run.




