Shore Up
A row of mail slots on a wall, each with a small arrow redirecting a letter into a different slot, and a clerk reading from a checklist to set each arrow.
MailWindows

Bulk-Update Exchange Mailbox Forwarding from a CSV

Ketan Aagja8 min read
No ratings yet

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-Mailbox parameters 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-Mailbox accepts (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 ForwardingAddress instead, which takes a recipient the tenant knows; this guide uses ForwardingSmtpAddress because bulk forwarding to outside addresses is the common case. If you need the internal-recipient variant, check the Set-Mailbox reference on Microsoft Learn for -ForwardingAddress.
  • DeliverToMailboxAndForwardTRUE keeps a copy in the mailbox and forwards; FALSE forwards 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's TRUE/FALSE text into an actual $true/$false, because -DeliverToMailboxAndForward expects a boolean, not a string.
  • -WhatIf:$WhatIf passes the switch through from the top variable, so one flag controls the whole run.
  • -ErrorAction Stop inside the try means 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.

Written by
Ketan Aagja

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.

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.

8 min read

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.

8 min read

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.

9 min read

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.

10 min read