Speedy PowerShell Access to Office 365 Groups with Get-ExoRecipient

Cranking Up Speed in Old PowerShell Scripts

Now that Microsoft has released the set of REST-based Exchange Online cmdlets in the Exchange Online Management module, people are experimenting with the new cmdlets to discover what needs to be done to upgrade scripts.

When reviewing Chapter 13 for the December 2019 update of the Office 365 for IT Pros eBook to add information about PowerShell support for private channels in Teams, I also took the chance to update one of the examples showing how to use Exchange Online cmdlets with Office 365 Groups. Some interesting gotchas were encountered. Before getting into the details of what those gotchas were, let’s think about how quickly we can fetch a set of group objects to process.

The Slowness of Get-UnifiedGroup

When you connect a PowerShell session to Exchange Online, the set of cmdlets designed to manage Office 365 like Get-UnifiedGroup, Get-UnifiedGroupLinks, and so on become available for use. The Get-UnifiedGroup cmdlet fetches details about Office 365 Groups and is used extensively across in scripts published in blogs, GitHub, or the TechNet Gallery. Although Get-UnifiedGroup is effective at what it does, it’s slow. Very slow. And that slowness gets worse as the number of groups grows.

For this reason, my advice has long been to avoid using Get-UnifiedGroup to create a set of group mailboxes for processing and to use the Get-Recipient cmdlet instead. Get-Recipient is much faster because it doesn’t have to do as much processing to fetch the properties of each group object. To create a set of groups for processing, you could run a command like:

$Groups = Get-UnifiedGroup -ResultSize Unlimited

Running Get-Recipient to fetch the set of group mailboxes is much faster:

$Groups = Get-Recipient -RecipientTypeDetails GroupMailbox -ResultSize Unlimited

Try it and see just how quick Get-Recipient is.

In any case, like all the new REST cmdlets, the Get-ExoRecipient cmdlet is faster than its Remote PowerShell equivalent, especially when you need to process large quantities of objects. The Get-ExoRecipient cmdlet is the fastest way to create a list of Office 365 Groups and should be used for this purpose whenever possible.

Fetching Inbox Statistics for Group Mailboxes

Which brings us to an example of how to convert some code from the Remote PowerShell cmdlets to REST. This example uses Get-Recipient to find Office 365 Groups and pipes the objects through Get-MailboxFolderStatistics to report the number of items in the Inbox folder of the group mailbox. We also report the date of the newest item created in the Inbox. Calculated properties are used to create more readable output.

Here’s the original code:

Get-Recipient -RecipientTypeDetails GroupMailbox -ResultSize Unlimited | Sort DisplayName | Get-MailboxFolderStatistics -FolderScope Inbox -IncludeOldestAndNewestItems | Format-Table  @{"Name"="Group";"Expression"={$_.Identity.Split("\")[0]}}, ItemsinFolder, @{"Name"="Size"; "Expression"={$_.FolderSize.Split("(")[0]}}, @{"Name"="Newest Item"; "Expression"={Get-Date($_.NewestItemReceivedDate) -format g}}

The output is something like this:

Group                                              ItemsInFolder Size      Newest Item
-----                                              ------------- ----      -----------
2021Edition_c832eaa2-f5b8-4f2e-a745-0595074a5                 52 196.92 KB 12 Mar 2019 14:43
academydocuments_b3192809-1a22-4d31-8538-80e7350de           447 45.12 MB  14 May 2018 11:33

The group name looks and is strange. It’s the identity returned by Get-MailboxFolderStatistics less the name of the folder (Inbox). The identity comes from the name given to the group object when it is created.

Looking for items in a folder is a convenient way to check the activity of Outlook-based Office 365 Groups which use email for conversations. If new items aren’t be posted into the Inbox, the group isn’t active. The same technique can be used to examine items in the Team Chat folder to discover the last time someone posted a message to a channel conversation in a team-enabled group.

Converting to REST

The converted code uses Get-ExoRecipient to fetch the set of groups and then pipes each mailbox to Get-ExoMailboxFolderStatistics to return the number of items and the date of the latest item in the Inbox folder. Here’s the updated code:

Get-ExoRecipient -RecipientTypeDetails GroupMailbox -ResultSize Unlimited | Select-Object -Property @{Name = 'UserPrincipalName'; Expression = {$_.PrimarySmtpAddress}} | Get-ExoMailboxFolderStatistics -FolderScope Inbox -IncludeOldestAndNewestItems | Format-Table  @{"Name"="Group";"Expression"={$_.Identity.Split("\")[0]}}, ItemsinFolder, @{"Name"="Size"; "Expression"={$_.FolderSize.Value.ToString('a')}}, @{"Name"="Newest Item"; "Expression"={ReturnEuroDate($_.NewestItemReceivedDate)}}

Explaining the Differences

Even allowing for the replacement of Remote PowerShell cmdlets by REST cmdlets, the code is obviously different. Here’s why:

  • The REST cmdlets sort objects by name before processing as part of their preparation to be able to restore on failure. The older cmdlets sort objects by creation timestamp unless another sort is applied.
  • The Get-ExoMailboxFolderStatistics cmdlet accepts a User Principal Name as an identity for a mailbox. Unlike Get-ExoMailboxStatistics, it doesn’t accept the account object identifier (GUID). The behavior across the two cmdlets is inconsistent, but to make the cmdlet happy, we calculate a User Principal Name from the primary SMTP address. This will work if the primary SMTP address matches the User Principal Name but won’t if this isn’t the case.
  • To allow for accurate calculations and comparisons, Get-ExoMailboxFolderStatistics returns a ByteQuantifiedSize object for size properties where Get-MailboxFolderStatistics returns a string (which isn’t so good for calculations). This removes the need to do string manipulations to get numeric values for folder and mailbox sizes. For instance, here’s some code to take the TotalItemSize property returned by Get-MailboxStatistics and convert a string like “3.952 GB (4,243,014,142 bytes)” into the number of megabytes occupied by the mailbox (4,046):
If ($MbxStats.TotalItemSize.Value -ne $NULL) {$MbxSizeMB = (($MbxStats.TotalItemSize.ToString().Split('(')[1].Split(')')[0].Replace(',','').Split(' ')[0])/1MB).ToUInt32($NULL)}

Reporting the folder size returned by Get-ExoMailboxFolderStatistics in megabytes is much simpler:

  • The dates returned by the REST cmdlets are in U.S. format and don’t respect the locale of the workstation. This means that you get values like “3/11/2019 11:20:11 AM” that the Get-Date cmdlet can’t handle when formatting a date. I wrote a simple function to parse the U.S. format dates (see below). You can use a similar function or just return the U.S. dates.
  • The identity for a folder returned by Get-ExoMailboxFolderStatistics uses the user principal name and is different to that returned by Get-MailboxFolderStatistics.

The same output is generated:

Group                                               ItemsInFolder Size      Newest Item
-----                                               ------------- ----      -----------
[email protected]                                52 196.92 KB 12 Mar 2019 14:33
[email protected]                          447  45.12 MB 14 May 2018.11:33

Here’s the function to convert U.S. dates to a local format.

Function ReturnEuroDate([String]$InputDate) {
    # Input date is something like 10/12/2017 9:15 PM
    [String]$Return = $Null
    $SplitBits = $InputDate.Split(" ")
    # Convert from U.S. date
    $DateParts = $SplitBits[0] -split "/"
    $EuroDate = "$($dateparts[1])/$($dateParts[0])/$($dateParts[2])" 
    [int]$Hours = $SplitBits[1].Split(":")[0]; [int]$Minutes = $SplitBits[1].Split(":")[1]
    If ($SplitBits[2] -eq "PM" -and $Hours -ne 12) {$Hours+= 12} #Add 12 to hours if PM
    $EuroDate = Get-Date($Eurodate + " " + $Hours + ":" + $Minutes) -format g
    $OutputDate = $EuroDate
    Return $OutputDate }

Updating Scripts Will Take Time

Converting code in old scripts to use the REST cmdlets seems straightforward on the surface and sometimes is the case. However, small and important differences await the unprepared, which means that you should set aside a reasonable amount of time to rewrite, test, and measure the performance of scripts converted to REST.

Although the REST cmdlets include auto-retry to make them less susceptible to transient errors, don’t forget to add some error handling into your code. Old routines handle the errors generated by Remote PowerShell cmdlets; the new cmdlets bring their own errors. While fewer, they still exist.