Report on Microsoft 365 mailbox forwarding (all methods) via PowerShell

For this week’s “updated PowerShell script” entry, we will cover the scenario of generating a report of all Microsoft 365 mailboxes with some form of mailbox forwarding configured. The original script was published back in 2017, and since then, some changes have occurred in the service, making it harder to have undesired forwarding to external addresses. Still, even with these new controls in mind, it’s important to know whose emails are being forwarded and to whom, even if only internal recipients are involved.

If you are not familiar with the plethora of methods that can be used to forward one’s emails in Exchange Online, which properties to look at or the controls you can use to control forwarding, you can find a good summary in this EHLO blog article. The current version of the script will cover the following:

  • Mailbox level forwarding via the ForwardingSmtpAddress  or ForwardingAddress  property – covered by default
  • Forwarding via Inbox rules (Outlook rules) – covered when you run the script with the -CheckInboxRules switch
  • Calendar-level (delegate) forwarding – covered when you run the script with the -CheckCalendarDelegates switch
  • Forwarding configured via Transport rules – covered when you run he script with the -CheckTransportRules switch

The first method is the easiest one to cover, as all the information we need can be obtained via a single call to the Get-Mailbox cmdlet. In fact, a single call is all we need in order to cover all mailboxes within the company, and thanks to the support for server-side filtering for both the ForwardingSmtpAddress and ForwardingAddress  properties, the call is even more efficient. But wait, there is more! As this script update is also trying to address the incoming deprecation of the Remote PowerShell session connectivity to Exchange Online, we are now leveraging the V3 Exchange Online module, and the Get-EXOMailbox cmdlet. The use of said cmdlet allows us to minimize the set of properties returned, further adding to the script’s effectiveness.

Get-EXOMailbox -Filter {ForwardingSmtpAddress -ne $null -or ForwardingAddress -ne $null} -ResultSize Unlimited -Properties ForwardingAddress,ForwardingSMTPAddress,DeliverToMailboxAndForward

When covering the next method, namely Inbox rules, things start to get more complicated. There is no easy way to get all the mailboxes with such form of forwarding configured, so instead we have to fetch the full set and iterate over each mailbox, issuing a call to the Get-InboxRule cmdlet as we go. Because of this overhead, the script will not check for Inbox rules, unless you specify the -CheckInboxRules switch when invoking it. As the V3 version of the Exchange PowerShell modules proxies all cmdlets through a RESTful endpoint, performance should be improved compared to the previous version of the script. Still, it might be a good idea to add some delay in-between executions, as a simple anti-throttling measure, especially if you tenant has thousands of mailboxes.

Get-InboxRule -Mailbox vasil -IncludeHidden | ? {$_.ForwardTo -ne $null -or $_.ForwardAsAttachmentTo -ne $null -or $_.RedirectTo -ne $null} | Select Name,ForwardTo,ForwardAsAttachmentTo,RedirectTo

The next form of forwarding to consider is that for Calendar delegates, who depending on the configuration will receive a copy of any meeting requests addresses to a given mailbox, or get the original request redirected to them. Luckily, this scenario is also covered by the above cmdlet, as it is effectively governed by a hidden rule within the owner’s mailbox. This is in fact one of the new additions to the script, made available thanks to the introduction of the –IncludeHidden switch parameter for the Get-InboxRule cmdlet.

Speaking of delegates, a resource mailbox can have its own set of resource delegates configured, information about which you can obtain via the Get-CalendarProcessing cmdlet. Even though this type of forwarding only affects calendar items, like in the previous scenario, the methods are different and thus distinct entries are returned. To cover this type of forwarding, run the script with the -CheckCalendarDelegates switch.

Get-Mailbox -ResultSize Unlimited -RecipientTypeDetails UserMailbox | % {Get-CalendarProcessing $_.PrimarySmtpAddress | select Identity,ResourceDelegates }

A note is due here. As mentioned above, this setting only applies to resource mailboxes, i.e. room and equipment mailboxes. In Exchange Online, the Set-CalendarProcessing cmdlet will not allow you to configure the corresponding ResourceDelegates property on a user or a shared mailbox. Interestingly, the property cannot be configured on Booking/Scheduling mailboxes either. Nevertheless, this is a nice segway to introducing another improvement to the script, namely the added support for Booking mailboxes.

The last form of forwarding to consider is that via Transport rules, which can be included if you run the script with the –CheckTransportRules switch. While this is purely admin-controlled, it is still something you should check, if nothing else to avoid issues with your mail flow. Luckily, a single call should be sufficient to cover all transport rules in the organization:

Get-TransportRule | ? {$_.RedirectMessageTo -ne $null -or $_.BlindCopyTo -ne $null -or $_.AddToRecipients -ne $null -or $_.CopyTo -ne $null -or $_.AddManagerAsRecipientType -ne $null}

Another new addition to the script is the –CheckTenantControls switch, which if specified, will provide insights about the admin controls used to limit forwarding. First, the script will enumerate all outbound spam policy objects in the organization, via the Get-HostedOutboundSpamFilterPolicy cmdlet, and highlight any that have the AutoForwardingMode property set to allow external forwarding. Apart from the outbound spam policy, a set of controls exists as part of the remote domains configuration, which the script will cover next (via the Get-RemoteDomain cmdlet). If any remote domains have the AutoForwardEnabled property set to allow forwarding, they will be highlighted in the output. Lastly, using the –CheckTenantControls switch will also allow the script to enumerate all accepted domains within the company, in an attempt to distinguish between internal and external forwarding addresses, when producing the output. The Get-AcceptedDomain cmdlet will be used for this.

As the new version of the script is introducing some additional checks and leveraging additional cmdlets, please make sure you’re running it with a user with sufficient permissions. To find out which role is required to run a specific Exchange Online cmdlet, you can use the following:

Get-ManagementRole -Cmdlet Get-AcceptedDomain

As mentioned above, the script now relies on the V3 Exchange module and will fail to run if it’s not installed. A simple logic is included to detect existing connectivity to Exchange Online PowerShell, and if no session is detected, the script will use the Connect-ExchangeOnline cmdlet to establish a fresh one. In order to minimize the script footprint, only the following cmdlets will be loaded: Get-InboxRule, Get-TransportRule, Get-CalendarProcessing, Get-HostedOutboundSpamFilterPolicy, Get-HostedOutboundSpamFilterRule, Get-RemoteDomain, Get-AcceptedDomain.

The last set of parameter you can use with the script include those governing the set of mailboxes to check. As mentioned already, whenever we want to include Inbox rules or calendar processing details, the script will have to run additional cmdlets against each and every mailbox in the tenant. In order to minimize the impact, you can instead instruct it to run against only mailboxes of specific type, i.e. user mailboxes. The following switched can be used:

  • IncludeUserMailboxes – specify this switch to include user mailboxes
  • IncludeSharedMailboxes – specify this switch to include shared mailboxes
  • IncludeRoomMailboxes – specify this switch to include room, equipment and Booking mailboxes
  • IncludeAll – specify this switch to include all mailbox types (the default setting if you don’t specify any of the -Include* parameters)

Compared to earlier versions of the script, few changes have been made here. First, the -IncludeRoomMailboxes switch will now also cover any Booking/Scheduling mailboxes created in the tenant. Apart from that, the switches to cover Discovery and Teams mailboxes have been removed, as those mailbox types are a thing of the past. Regardless, if any such mailboxes still exist in your tenant, they will be included when you run the script with the –IncludeAll switch.

And with that, the building blocks of the script are covered, and you can download it from my GitHub repo. Here are few examples on how to run it. First, to check all user mailboxes for mailbox-level forwarding. The second example covers all mailboxes within the company, and will also check for any Inbox rules configured to forward/redirect messages. The last example shows the most comprehensive method to run the script – include all mailboxes and all supported forwarding types, while also providing information about the tenant-level controls and an educated guess whether a given forwarding address is external or internal.

#Cover user mailboxes only
.\Mailbox_Forwarding_inventoryV2.ps1 -IncludeUserMailboxes

#Cover all mailbox types and include Inbox rules
.\Mailbox_Forwarding_inventoryV2.ps1 -IncludeAll -CheckInboxRules

#Cover all mailbox types and all forwarding methods
.\Mailbox_Forwarding_inventoryV2.ps1 -IncludeAll -CheckInboxRules -CheckCalendarDelegates -CheckTransportRules -CheckTenantControls

Few notes about the output of the script. By default, a CSV file will be generated, with each line representing a mailbox/forwarding method combo. For each entry, the following details will be shown: Mailbox address, Mailbox type, the type of forwarding, the forwarding address, whether the forwarding address is external and whether the original message will be kept. Depending on the type of forwarding configured, some of the details might be blank or missing. The prime example here would be transport rules, where you can have a plethora of conditions triggering a forward action, without directly specifying a mailbox object. A sample of the output is shown below:

Sample output of the mailbox forwarding scriptBecause of the peculiarities of the output, my assumption is that you would likely want to modify it. For this purpose, output will also be written to the console and saved in the global variable $varForwarding. Use said variable to filter or enrich the output as needed, or if you prefer, make changes directly in the script code itself.

Few more notes before closing the article. Take the information presented in the IsExternal field with a grain of salt. The method used to populate said field is by grabbing the set of accepted domains within your tenant and comparing it against the “forward to” entry. This should address most scenarios, but most definitely not all possible ones. Similarly, for the tenant-wide controls check, only the value of the auto-forwarding parameter is taken into consideration, not whether the policy is actually enabled, or scoped to any mailboxes with forwarding configured. Consider the report as “best effort” guesstimate, and combine it with other methods, such as the forwarded messages report in the new EAC.

Another very important thing to note is that “alternative” methods to forward/exfiltrate messages exist. One such method is outlined in the following article, and you can use this EWS-based script to cover the scenarios. Other methods, such as forwarding via Flow are subject to different set of controls, as detailed for example here. Then we have other types of admin-level functionality such as Supervision/Communication compliance, which can also be considered.

As always, feel free to modify the script to best suit your specific needs, and let me know if you run into any issues. I’m open to suggestions for improving the script as well, such as including other recipient types (do you care about inactive mailboxes for example) or forwarding methods. Or perhaps you’d prefer to run it against a set of users from a CSV file, instead of all mailboxes?

9 thoughts on “Report on Microsoft 365 mailbox forwarding (all methods) via PowerShell

  1. JJ says:

    When I execute the script, I can see that it’s running. However, after getting about 1/4 of the way through the total number of accounts, it throws this error:

    ConvertFrom-Json: Conversion from JSON failed with error: Unexpected character encountered while parsing value: U. Path ”, line 0,
    position 0.

    There’s no output to the console and no csv file created. I’m not sure what it’s trying to convert. Am I missing something totally obvious? Thanks!

    1. Vasil Michev says:

      The script doesn’t call the ConvertFrom-Json cmdlet directly, so I’m guessing some of the V2 cmdlets is failing… Try running Get-ExOMailbox and Get-ExORecipient against your environment, see if they throw similar errors.

  2. Jesse says:

    Great script, thank you! Is there a way to run this against a single domain in a multi-agency tenant?

    1. Vasil Michev says:

      Sure, you can adjust the filter on the Get-EXOMailbox cmdlet (lines 96-97), or replace it altogether with Import from CSV, etc.

      1. Jesse says:

        Hm. Not sure where I’ve gone wrong here. I’ve updated the referenced section as follows:

        #Filterable, but if we are going to include all the methods, we need to cycle all mailboxes anyway
        if (!$CheckInboxRules -and !$CheckCalendarDelegates) { $MBList = import-csv -path .\mailboxes.csv | ForEach {Get-EXOMailbox -Identity $_.UserPrincipalName -ResultSize Unlimited -RecipientTypeDetails $included -Properties ForwardingAddress,ForwardingSMTPAddress,DeliverToMailboxAndForward }}
        else { $MBList = import-csv -path .\mailboxes.csv | ForEach {Get-EXOMailbox -Identity $_.UserPrincipalName -ResultSize Unlimited -RecipientTypeDetails $included -Properties ForwardingAddress,ForwardingSMTPAddress,DeliverToMailboxAndForward }}

        But it’s throwing this error:

        Get-MailboxForwardingInventory : No matching mailboxes found, specify different criteria.
        At xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
        + Get-MailboxForwardingInventory @PSBoundParameters -OutVariable global …
        + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
        + CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
        + FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Get-MailboxForwardingInventory

        Any suggestions?

        1. Vasil Michev says:

          If you are going with a CSV, you will have to fetch each mailbox props anyway, so do something like this:

          (replaces lines 95-96)

          $MBList = Import-CSV blabla.csv

          Then, within the foreach loop (say line 113), add

          $MB = Get-ExOMailbox $MB.UserPrincipalName -Properties ForwardingAddress,ForwardingSMTPAddress,DeliverToMailboxAndForward
          if (!$MB) { continue }

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.