Forward a user's mail to another address, or stop forwarding, with approval

Build a workflow action that forwards a user's incoming mail to another address in your organisation, or stops forwarding, so MSPs and IT teams can handle leaver and cover requests by asking SuperIT, with an admin approving every change.

Runs in Cloud sandbox (PowerShell)
Uses Microsoft Graph credential with an Exchange Online scope
Suggested approval Require approval from superit:admins for every run
Category Email

Test it with SuperIT before you rely on it

This is an example, not a ready-made action, and its script hasn't been tested end to end in a SuperIT tenant. The Exchange Online credential it depends on hasn't been tested with SuperIT either, so it may not work as written. Before you use it, work through it with SuperIT: ask SuperIT to review the script against your environment and requirements, adjust it together, then test it on a test account or device. Test in the action editor runs the script for real.

Why use an action instead of asking SuperIT

SuperIT reads Microsoft 365 through your Microsoft 365 connection, but that connection is read-only, and mailbox changes like this one need Exchange Online, which SuperIT can't call in a chat. Starting or stopping mailbox forwarding needs an action with an Exchange Online credential. Keep approval on every run: forwarding is a common way data is stolen from a compromised mailbox.

To check whether a user has inbox rules that forward mail, just ask SuperIT in chat.

When to use it

  • "Tom has left, forward his email to his manager Sarah."
  • "Forward Jane's mail to [email protected] while she's on leave, and keep a copy in Jane's mailbox."
  • "Stop forwarding on Sam's mailbox."

What the action does

  1. Checks the inputs.
  2. Connects to Exchange Online with the credential.
  3. To start: checks that the address you give is in one of your tenant's accepted domains, and refuses any other address. Then it sets mailbox forwarding to that address, clears any forwarding to a named recipient that was set before, and either keeps a copy in the mailbox or not.
  4. To stop: clears mailbox forwarding.
  5. Prints the mailbox's forwarding settings after the change.

What it doesn't do: it doesn't forward to addresses outside your organisation, create or remove inbox rules that forward mail, forward mail that has already arrived, or change your tenant's outbound spam policy. Microsoft 365 also blocks automatic forwarding to external addresses by default, in the outbound spam filter policy.

Mailbox forwarding is set with Exchange Online because Microsoft Graph has no setting for it.

Before you begin

  • A Microsoft Graph credential for Exchange Online actions, named M365 Exchange, with the scope https://outlook.office365.com/Exchange.Manage, signed in with an account that holds Exchange Administrator. Link it only to Exchange Online actions. This credential setup hasn't been tested with SuperIT; if the credential won't register or the script can't connect, see Exchange Online tasks.
  • An authorization policy with one rule: Specific groups superit:engineers, superit:admins → Require approval from superit:admins. Forwarding is a common way to steal data from a compromised mailbox, so don't let anyone forward mail without approval. Don't add an Anyone rule: the script doesn't receive the requester's identity, so an end user could forward someone else's mail.
  • The Engineer or Admin role to build the action in Workflows → Actions. Only admins can create actions in chat.

Build it with SuperIT

Create a workflow action called "Set up mail forwarding".
Runtime: PowerShell in the cloud sandbox. Credential: M365 Exchange
(Exchange Online, connect with Connect-ExchangeOnline -AccessToken).
Inputs:
- mailbox (email, required)
- operation (select, required, choices: start, stop)
- forward_to (email, optional) - required when starting
- keep_copy (boolean, optional) - keep a copy in the mailbox
It should refuse a forward_to address whose domain isn't in Get-AcceptedDomain,
use Set-Mailbox ForwardingSmtpAddress and DeliverToMailboxAndForward,
clear ForwardingAddress when starting (it overrides ForwardingSmtpAddress),
and print the forwarding settings after the change.
Authorization policy: engineers and admins, every run approved by superit:admins.

SuperIT repeats the brief back, drafts the script, checks it, and creates the action as a draft. Review the script in Workflows → Actions, test it, then publish it.

Or build it yourself

Name Type Required Example
mailbox email Yes [email protected]
operation select (choices start, stop) Yes start
forward_to email No [email protected]
keep_copy boolean No on
# Goal: start forwarding on a mailbox to an address in one of the tenant's own domains, or stop it,
# in Exchange Online, then print the forwarding settings Exchange reports back.
$token = $env:CREDENTIAL_M365_EXCHANGE_TOKEN

function Invoke-MailForwardingUpdate {
    param([string]$Mailbox, [string]$Operation, [string]$ForwardTo, [string]$KeepCopy)

    $Mailbox = $Mailbox.Trim()
    $Operation = $Operation.Trim().ToLower()
    $ForwardTo = $ForwardTo.Trim()
    Assert-Inputs -Mailbox $Mailbox -Operation $Operation -ForwardTo $ForwardTo

    Connect-ExchangeWithToken
    try {
        if ($Operation -eq 'start') {
            Assert-InternalRecipient -ForwardTo $ForwardTo
            Start-MailForwarding -Mailbox $Mailbox -ForwardTo $ForwardTo -KeepCopy ($KeepCopy -eq 'true')
        } else {
            Stop-MailForwarding -Mailbox $Mailbox
        }
        Write-ForwardingSummary -Mailbox $Mailbox -Settings (Get-Mailbox -Identity $Mailbox)
    } finally {
        Disconnect-ExchangeOnline -Confirm:$false
    }
}

function Assert-Inputs {
    param([string]$Mailbox, [string]$Operation, [string]$ForwardTo)

    if (-not $token) { throw "Link the M365 Exchange credential to this action." }
    if ($Mailbox -notmatch '^[^@\s]+@[^@\s]+$') { throw "mailbox must be an email address." }
    if ($Operation -notin 'start', 'stop') { throw "operation must be start or stop." }
    if ($Operation -eq 'start' -and $ForwardTo -notmatch '^[^@\s]+@[^@\s]+$') { throw "forward_to is required to start forwarding." }
    if ($ForwardTo -eq $Mailbox) { throw "A mailbox can't forward to itself." }
}

function Connect-ExchangeWithToken {
    $upn = Get-TokenUpn
    Import-Module ExchangeOnlineManagement
    try {
        # A delegated token must be paired with the signed-in admin's UPN, read from the token itself.
        Connect-ExchangeOnline -AccessToken $token -UserPrincipalName $upn -ShowBanner:$false
    } catch {
        Write-Error "Failed to connect to Exchange Online as '$upn': $_"
        exit 1
    }
}

function Get-TokenUpn {
    $part = $token.Split('.')[1].Replace('-', '+').Replace('_', '/')
    $part += '=' * ((4 - $part.Length % 4) % 4)
    ([Text.Encoding]::UTF8.GetString([Convert]::FromBase64String($part)) | ConvertFrom-Json).upn
}

function Assert-InternalRecipient {
    param([string]$ForwardTo)

    # Forwarding outside the organisation is the classic way mail is stolen, so only the tenant's own domains are allowed.
    try {
        $domains = @(Get-AcceptedDomain | ForEach-Object { "$($_.DomainName)" })
    } catch {
        Write-Error "Failed to read the tenant's accepted domains: $_"
        exit 1
    }
    $domain = $ForwardTo.Split('@')[-1]
    if ($domains -notcontains $domain) {
        throw "$ForwardTo isn't in one of this tenant's accepted domains ($($domains -join ', ')). This action only forwards inside the organisation. Nothing was changed."
    }
}

function Start-MailForwarding {
    param([string]$Mailbox, [string]$ForwardTo, [bool]$KeepCopy)

    try {
        # ForwardingAddress wins over ForwardingSmtpAddress, so a leftover one would keep mail going to the old recipient.
        # -ErrorAction Stop: Exchange cmdlets run in their own module and ignore the script's $ErrorActionPreference.
        Set-Mailbox -Identity $Mailbox -ForwardingAddress $null -ForwardingSmtpAddress "smtp:$ForwardTo" -DeliverToMailboxAndForward $KeepCopy -ErrorAction Stop
    } catch {
        Write-Error "Failed to forward '$Mailbox' to '$ForwardTo': $_"
        exit 1
    }
}

function Stop-MailForwarding {
    param([string]$Mailbox)

    try {
        Set-Mailbox -Identity $Mailbox -ForwardingSmtpAddress $null -ForwardingAddress $null -DeliverToMailboxAndForward $false -ErrorAction Stop
    } catch {
        Write-Error "Failed to stop forwarding for '$Mailbox': $_"
        exit 1
    }
}

function Write-ForwardingSummary {
    param([string]$Mailbox, [object]$Settings)

    Write-Output "=== MAIL FORWARDING ==="
    Write-Output "Mailbox    : $Mailbox"
    Write-Output "Forwarding : $(if ($Settings.ForwardingSmtpAddress) { $Settings.ForwardingSmtpAddress } else { 'off' })"
    Write-Output "Recipient  : $(if ($Settings.ForwardingAddress) { $Settings.ForwardingAddress } else { 'off' })"
    Write-Output "Keep copy  : $($Settings.DeliverToMailboxAndForward)"
    # $script: so the runner's wrapper sees it; a plain assignment stays local to this function.
    $script:result = @{ mailbox = $Mailbox; forwarding_to = "$($Settings.ForwardingSmtpAddress)"; forwarding_recipient = "$($Settings.ForwardingAddress)"; keep_copy = $Settings.DeliverToMailboxAndForward }
}

Invoke-MailForwardingUpdate -Mailbox $ctx.inputs.mailbox -Operation $ctx.inputs.operation `
    -ForwardTo $ctx.inputs.forward_to -KeepCopy $ctx.inputs.keep_copy

Test it

Test runs for real

Test skips authorization and approval, uses the real M365 Exchange credential, and really forwards mail. Use a test mailbox and an internal test address.

The script is untested, so treat your first test runs as debugging, and don't publish the action until a test run does exactly what you expect.

  1. On the action, select Test. Enter a test mailbox, start, an internal address and switch on keep_copy.
  2. Read the This is not a dry run. warning, tick "I understand this is a real execution that may change real systems." and select Start test run.
  3. Send the test mailbox an email and check it arrives at both addresses.
  4. Test again with stop.
  5. Test with an external address, such as a personal email address, and check the run is refused.

Use it

Publish the action. Engineers ask SuperIT, for example "Forward Tom's mail to Sarah and keep a copy". The run waits for an admin to approve.

For a suspected compromise, ask SuperIT to check first, for example "Does Sam's mailbox forward mail anywhere, or have inbox rules that do?". SuperIT reads mailbox settings and inbox rules through the Microsoft 365 connection, then you can stop forwarding with this action.

For MSPs, publish to managed teams from the Publish dialog; an engineer or admin in each team authorizes its own M365 Exchange credential.

Variations

  • If you must forward outside the organisation for a specific case, make a separate copy of the action with a required allow_external acknowledgement, keep approval on it, and allow the domain in your outbound spam filter policy only for that mailbox.
  • Forward to an internal recipient by name with -ForwardingAddress instead of an SMTP address.
  • Convert a leaver's mailbox to shared (Set-Mailbox -Type Shared) and give the manager access instead. See Grant shared mailbox access.