Thursday, February 6, 2020

PowerShell Scripts - Get all Mailbox and Mailbox Folder permissions in O365 (New Exchange PowerShell)

Script updated several times between 2020-02-10 and 2020-02-13 to tweak different aspects when using the new Exchange Online powershell cmdlets that is currently in preview but is generally much more efficient for this task. I received confirmation that getting mailbox folder permissions will return something more than a displayname and that we will eventually be able to use the FolderId instead of folder path since folder paths have problems with backslashes and other foreign characters.

You need to document, monitor, and manage mailbox and mailbox folder permissions across an entire O365 tenant. When trying with PowerShell, we can't even pipe Get-Mailbox to Get-MailboxFolderStatistics to Get-MailboxFolderPermissions. When attempting these things in PowerShell, it has traditionally been extremely slow.

This gets weird when it comes to mailbox permissions (FullAccess, SendAs, SendOnBehalf), Calendar Permissions, and Mailbox Folder Permissions (tons of options) and HR wants you to verify that an employee does not have access to that mailbox even if they are rehired later. The old Exchange PowerShell commands transmitted too much data so the results of pulling large numbers of mailboxes and folders were too slow to be feasible. One could search the Unified Audit Log for folder permissions changes but that doesn't give the baseline.

You can download my interactive script that does the retrieval, partial retrieval, and resuming here: I have updated several times and maintain updates on it for tweaking special scenarios and will update once the new cmdlets offer different data.

No matter how you try, you will need to make a local copy of the mailbox/folder permissions somewhere because attempting to query this information is too time-intensive to be useful. With the advent of the new Exchange Online V2 powershell cmdlets, the performance of getting the mailbox, mailbox permissions, and folder permissions is not nearly as horrible as it was before.
Make sure you have an Exchange Admin account and you have installed the new Exchange module (Install-Module ExchangeOnlineManagement) - you can connect and even support MFA using Connect-ExchangeOnline and all the old ExchangeOnline commands still work. From there, the more straightforward version for mailbox permissions for 5 mailboxes is something like this:

Get-EXOMailbox -ResultSize 5 | Get-EXOMailboxPermissions | Where-Object { $_.IsInherited -eq $false -and $_.Deny -eq $false } | Select-Object Identity,User,@{Label="AccessRights";Expression={$_.AccessRights -join ","}} | Export-CSV -Path "C:\someFolder\MailboxPermissions.csv" -Append

Or for the Mailbox Folder permissions (for 5 mailboxes):

Get-EXOMailbox -ResultSize 5 | Get-EXOMailboxFolderStatistics | Where-Object { $_.SearchFolder -eq $false -and @("Root","Calendar","Inbox","User Created") -contains $_.FolderType -and (@("IPF.Note","IPF.Appointment",$null) -contains $_.ContainerClass -or $_.Name -eq "Top of Information Store")} | Select-Object @{Label="Identity";Expression={ if($_.Name -eq "Top of Information Store"){ $_.Identity.Substring(0,$_.Identity.IndexOf("\")) } else { $_.Identity.Substring(0,$_.Identity.IndexOf("\"))+':'+$_.Identity.Substring($_.Identity.IndexOf("\")).Replace([char]63743,"/")}}} | Get-EXOMailboxFolderPermissions | Where-Object { $_.AccessRights -ne "None" } | Select-Object Identity,FolderPath,User,@{Label="AccessRights";Expression={$_.AccessRights -join ","}} | Export-CSV -Path "C:\someFolder\MailboxFolderPermissions.csv" -Append

Or combine them effectively (for 5 mailboxes):

Get-EXOMailbox -ResultSize 5 | Tee-Object -Variable "myMailboxes" | Get-EXOMailboxPermissions | Where-Object { $_.IsInherited -eq $false -and $_.Deny -eq $false } | Select-Object Identity,User,@{Label="AccessRights";Expression={$_.AccessRights -join ","}} | Export-CSV -Path "C:\someFolder\MailboxPermissions.csv" -Append
     $myMailboxes | Get-EXOMailboxFolderStatistics | Where-Object { $_.SearchFolder -eq $false -and @("Root","Calendar","Inbox","User Created") -contains $_.FolderType -and (@("IPF.Note","IPF.Appointment",$null) -contains $_.ContainerClass -or $_.Name -eq "Top of Information Store")} | Select-Object @{Label="Identity";Expression={ if($_.Name -eq "Top of Information Store"){ $_.Identity.Substring(0,$_.Identity.IndexOf("\")) } else { $_.Identity.Substring(0,$_.Identity.IndexOf("\"))+':'+$_.Identity.Substring($_.Identity.IndexOf("\")).Replace([char]63743,"/")}}} | Get-EXOMailboxFolderPermissions | Where-Object { $_.AccessRights -ne "None" } | Select-Object Identity,FolderPath,User,@{Label="AccessRights";Expression={$_.AccessRights -join ","}} | Export-CSV -Path "C:\someFolder\MailboxFolderPermissions.csv" -Append

If you are a PowerShell person, you may notice 3 oddities with my combining:
  1. I did not use a foreach-object
  2. I used a Tee-Object
  3. There are a ton of filters and some weird Select-Object stuff going on in the middle with MailboxFolderStatistics

Foreach-Object breaks the new Exchange Online cmdlets' multithreading (speed) so I can't say Get-Mailboxes | foreach-object { Get-Mailboxpermissions; Get-MailboxFolderPermissions} ... It was actually faster to run each command separately. To keep from having to retrieve the mailboxes twice, Tee-Object allows you to take the output from the Get-EXOMailboxes, store it in a variable, and use it a second time once your first full command is over - and it does NOT break the PowerShell pipeline so speed stays happy!

The reason for all the weirdness with Get-EXOMailboxFolderStatistics is that when someone tries Get-MailboxFolderPermission but only supplies the email address of the mailbox, it only retrieves the folder permissions for the folder "Top of Information Store". So one might think - I will just use the Get-EXOMailboxFolderStatistics to get the list of all the folders within the mailbox and send THAT over to the Get-EXOMailboxFolderPermission...well, it fails miserably because the Identity of the folder from Statistics looks like\NameOfFolder but the MailboxFolderPermissions cmdlet is expecting\NameOfFolder\NameofSubfolder. Right now, the FolderID is not supported in the new cmdlets, only the path. Either way, the lack of nice pairing is stupid, but fixable by Selecting the identity of folders and then fixing the name and then passing that along the pipeline. The other filters I throw in there are so that I don't ask about permissions for every dumb folder in a mailbox that might be some Teams, Yammer, etc. system folder and subfolder but I do want Calendars.

Wednesday, September 18, 2019

PowerShell Scripts - Connect to Azure AD with MFA - reusing your existing connection

Situation: You are a very good user and have Multi-factor Authentication enabled for your account and you do things in Azure often. You want to connect to Azure without having to be prompted AGAIN for your MFA once you have done so for something else (e.g. the Exchange Online PowerShell module). You could also be creating a script that many people have to run many times a day and you don't want them re-authenticating over MFA every...single...time.


  1. Make sure you have the latest and greatest of the Exchange Online PowerShell Module (you can get it at
  2. Make sure you have the latest version of the AzureAD Module (I'm using at least version at the time of this writing)
  3. Make a script similar to this (or add to your script...this assumes the current user is the one that installed the EXO module):

        [string]$O365TenantId = "YOUR-TENANT-ID-GOES-HERE",
        [string]$O365ClientId = "1b730954-1685-4b74-9bfd-dac224a7b894",
        [string]$O365ResourceUrl = "",
        [uri]$O365URI = "urn:ietf:wg:oauth:2.0:oob",
        [string]$DefaultDomain = ""
    Import-Module AzureAD
    #Load the MFA module.
        $getChildItemSplat = @{
            Path = "$Env:LOCALAPPDATA\Apps\2.0\*\CreateExoPSSession.ps1"
            Recurse = $true
            ErrorAction = 'Stop'
            Verbose = $false
        $MFAPath = ((Get-ChildItem @getChildItemSplat | Sort-Object LastWriteTime -Descending | where-object {(Test-Path "$($_.PSParentPath)\Microsoft.Exchange.Management.ExoPowershellModule.dll") -eq $true} | Select-Object -First 1 | Select-Object -ExpandProperty fullname).Replace("\CreateExoPSSession.ps1", ""))
        . "$MFAPath\CreateExoPSSession.ps1" 3>$null
        Write-Verbose -Message "MFA module found in this folder - $MFAPath"
    } catch {
        Read-Host "MFA Module not found inside the local appdata folder for $ENV:USERNAME. If it is installed for another user already, run powershell as that user. To install the latest module, go to Press any key to exit"
    try {
        #Since the MFA module has a file that holds all of the authentication processes, let's use that to authenticate silently
        $IdentityPath = Get-ChildItem -Path "$MFAPath\Microsoft.IdentityModel.Clients.ActiveDirectory.dll" -Recurse -Verbose:$false | Sort-Object LastWriteTime -Descending | Select-Object -First 1 -ExpandProperty FullName
        Add-Type -Path $IdentityPath
        #Using the documentation for the AuthenticationContext, you need a resource url, client ID, URI (special), PromptBehavior (special), and UserIdentifier (special)
        #This means that we create a basic context pointing to a common login url for O365 but the AzureAD graph url as the resource url
        $authContext = New-Object "Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext" -ArgumentList "$O365TenantId"
        #Specifying Auto for this allows MFA to check for an existing token and use it if possible, otherwise prompt for MFA
        $MFAPromptBehavior = [Microsoft.IdentityModel.Clients.ActiveDirectory.PromptBehavior]::Auto
        #Using an identifier type of 2 is required where the displayId is required for token to pass (ie the email is the username)
        $AADcredential = [Microsoft.IdentityModel.Clients.ActiveDirectory.UserIdentifier]::new(("$ENV:USERNAME@$DefaultDomain"),2)
        #Get the authentication...let's hope the result is successful :)
        $authResult = $authContext.AcquireToken($O365ResourceUrl,$O365ClientId,$O365URI,$MFAPromptBehavior,$AADcredential)
        #Now that all the weird auth stuff has completed, connect to azuread using the resulting data
        Connect-azuread -TenantId $authResult.TenantId -AadAccessToken $authResult.AccessToken -AccountId $authResult.UserInfo.DisplayableId
    } catch {
        write-output "Failure to connect to O365/Azure MSOLService for user account management. Here is the error from MS: $_"
        Read-Host "Press any key to exit script"

I learned this from a smattering of blog posts that you may also find helpful (one I found after I figured all this out on my own sadly):

Tuesday, May 21, 2019

O365 Spam Remover Script - now with a GUI and supports MFA

Problem: A spam campaign has hit your company and you want to remove the email from all inboxes in the tenant to help prevent people clicking bad links, freaking out, etc.

Solution: If you have less than 50k mailboxes, use Office 365 Compliance Center's Search and Purge feature. If not, you can use their discovery tools to generate a search but then you can't see the progress and it'll be a bit slow. So, if the campaign is less than 10 days old, here's a script that obtains as many Exchange Admin creds (now supporting MFA and Non-MFA) you can supply, tries to load a GUI for you or fails back to interactive command-line requests, and will use multiple powershell windows to run the necessary mailbox searches while you watch the progress. As with any script you get from the internet, no warranty is expressed or implied for this script so test it and tweak to your environment. I have tried to make it use UTC and avoid hard-coding any regional settings but your mileage may vary.

Update 6/17/2019 - Moved the code to GitHub for easier updating. DO NOT WORRY - my github does not look like some giant mess of folders with cryptic things...the powershell files are right there on the screen and you can click any of them to view them in their entirety.

Update on May 22, 2019 - I have added some support to attempt to auto-load the Exchange Online for Powershell module and use it as priority over basic authentication.