Purge emails from a mailbox that are protected by a Organizational Hold


Scenario: You need to remove messages from a mailbox, but the mailbox is protected under an Organizational Hold (but it could also be a LitHold or a InPlaceHold). Since the message is currently protected, it will not purge out of the mailbox until the hold is expired, or ever, depending on what type of hold is on the mailbox.

Solution:

1. Disable the Hold:
You need to remove the mailbox from the Hold. If its an Organizational hold, add them to the exclusion. If its a LitHold or InPlace Hold, disable the hold.

get-mailbox steveman | Select *hold*
get-organizationconfig | Select *Hold*

2. Get the message to the Purges Folder:
Move the email items into the Purges folder. You can do this by any one of the following:

Manual Purging: Deleting the message from the mailbox within the client, then ‘deleting ‘purge’ the message from the Recoverable Deleted Items (Deletions folder).

Search Mailbox – You could use search-mailbox with -deletecontent -force.

New-ComplianceSearch & New-ComplianceSearchAction : You could use a new-compliancesearch with a new-compliancesearchaction -purge -purgetype HardDelete.

MFCMAPI: You could use MFCMAPI to move the message directly to the purges folder.

3. Disable SingleItemRecovery and RetainDeletedItemsFor:
Run the following:
set-mailbox steveman -singleitemrecoveryenabled:$false – retaindeleteditemsfor 0.00:00:00

4. Start the Mailbox Folder Assistant:
start-mailboxfolderassistant steveman

Note: For steps 3 and 4, give time in between the steps for Replication and settings to go into effect.

Advertisement

Check Microsoft, Security and Compliance, Azure, and Exchange Role Membership

Scenario: You want to quickly gather membership information on all of Microsoft Online, AzureAD, and Exchange Online Roles.

Scriptlets:

Check Microsoft Online for Role Membership

#Connect to Microsoft Online: Connect-MSOLService
$MSrole_user = @()
$MSroles = Get-MsolRole
$MSroles | Sort name | %{
$n = $_.name
“Checking Role: $n”
$MSrole_User += Get-MsolRoleMember -RoleObjectId $_.ObjectId | Select @{Name=”RoleName”;Expression={“$n”}},DisplayName, EmailAddress,RoleMemberType
}
$MSRole_User


Check Azure AD for Role Membership

#Connect to Azure AD: Connect-AzureAD
$AZrole_user = @()
$AZroles = Get-AzureADDirectoryRole
$AZroles | Sort DisplayName | %{
$n = $_.DisplayName
“Checking Role: $n”
$AZrole_User += Get-AzureADDirectoryRoleMember -ObjectId $_.ObjectId | Select @{Name=”RoleName”;Expression={“$n”}},DisplayName,ObjectType,Mail,SecurityEnabled
}
$AZRole_User

Check for Security and Compliance Role Membership

#Connect to IPPS Sessions Online: Connect-IPPSSession
$SCrole_user = @()
$SCroles = Get-RoleGroup
$SCRoles | Sort Name | %{
$n = $_.name
“Checking Role: $n”
$SCRole_User += get-rolegroupmember $N | select @{Name=”RoleName”;Expression={“$N”},name,windowsliveid
}
$SCRole_User

Check Exchange Online Online

#Connect to Exchange Online: Connect-ExchangeOnline
$Exchange_Roles = get-managementroleassignment -geteffectiveusers
$exchange_Roles_Unique = $Exchange_Roles | Select RoleAssigneeName,EffectiveUserName
$Exchange_Roles_Unique = $exchange_Roles_Unique | Select -Unique RoleAssigneeName,EffectiveUserName
$Exchange_Roles_Unique

Another DoWhile Loop method for performing an action on large datasets in smaller subsets

Scenario: You have a large data set, maybe a large amount of mailboxes, and you need to set the RoleAssignmentPolicy to a policy that does not allow email forwarding.

Solution:

Collect Mailboxes

$mbx = get-mailbox -resultsize unlimited | where RoleAssignmentPolicy -ne PolicyWithNoEmailForward

Create DoWhile Variables

$start = 0
$inc = 10
$end = $inc
$totalCount = $mbx.count

Loop It

Do{
“Running on $start..$end out of $totalCount”
$mbx[$start..$end] | Set-mailbox -RoleAssignmentPolicy PolicyWithNoEmailForward
$start = $start + $inc
$end = $end + $inc
}While($start -le $totalCount)

Use Paging within a Select statement when running PowerShell commands to avoid Throttling

Scenario: You have a large dataset that you want to use a Do-While Loop with a Paging system to separate the data into smaller data sets. In this example, I will use a Do-While loop to set a roleassignmentpolicy against may mailboxes:

Solution: Run the following commands

Collect the Mailboxes

$mbx = get-mailbox -resultsize unlimited | where RoleAssignmentPolicy -ne “PolicyWithNoEmailForward”

Create the Page Variables

$total_Count = $mbx.count
$Sleep = 60 #in seconds
$page = 10 #the amount of records you want to go through per set
$page_Count = $page #The amount the page will increase

Buffer for DoWhile Loop

$mbx | Select -first $page | %{
$N = $_.name
“Setting RoleAssignmentPolicy on Mbx: $N”
Set-mailbox $n -roleassignmentPolicy “PolicyWithNoEmailForward”
}

Run the DoWhile Loop

Do{

#Run through the first set of Mailboxes
$mbx | Select -first $page -skip $page | %{
    $N = $_.name
    "Setting RoleAssignmentPolicy on Mbx: $N" 
    Set-mailbox $n -roleassignmentPolicy "PolicyWithNoEmailForward"
  }

#Display a separator between each set
 "

  Next"

#Increase the Page and SKip Count
  $Page = $page + $page_Count

#Sleep Duration
  Sleep $Sleep

} While($Page -lt $Total_Count)





Search-Mailbox: Easy way to convert String values to Integers

Scenario: Need an easy way to convert the [string] value into a number? Specifically for the Search-Mailbox command for the ResultItemsCount and ResultItemsSize?

Solution:

Command:
$Search = Search-Mailbox $mbx -SearchDumpster -SearchQuery Received:1/1/1900..1/1/2013 -EstimateResultOnly

Now convert the results and store in a variable:
Note: $ItemSize is in Bytes.

$ItemCount = ($Search.ResultItemsCount) -as [int]

$ItemSize = (([regex]::match($Search.ResultItemsSize, $regex).Groups[1].value) -replace " bytes","" -as [int])

Configure your Exchange Online PowerShell Script to leverage OAUTH/Modern Authentication and Authenticate Silently

Scenario: You need to connect to Exchange Online PowerShell via a script that will silently authenticate using Modern Authentication/OAuth.

Microsoft is deprecating Legacy/Basic Authentication when connecting to Exchange Online. Your existing scripts that leverages a username/password to authenticate silently, either by hardcoding a username or password into the script OR using encrypted keys that PowerShell calls in, will break when legacy authentication is officially disabled. You will need to convert the logic us to now start connecting to Exchange Online via Modern Authentication/Oauth.

If you haven’t yet asked ‘How to do we do that’? I am glad I asked for you….

1. You need the latest release of the ExchangeOnlineManagement (Connect-ExchangeOnline) module installed in PowerShell : Install-Module -name ExchangeOnlineManagement.

2. Setup App-only application in Azure.

2a. Register a new application object in Azure Active Directory

2b. Provide the Azure applications the following API permission: Exchange.ManageAsApp

2c. Create a self-signed cert in PowerShell that will be used to authenticate to the Azure App.
# Create certificate
$mycert = New-SelfSignedCertificate -DnsName “whatever.com” -CertStoreLocation “cert:\LocalMachine\My” -NotAfter (Get-Date).AddYears(3) -KeySpec KeyExchange

#Export certificate to .pfx file
$mycert | Export-PfxCertificate -FilePath c:\temp\mycert.pfx -Password $(ConvertTo-SecureString -String “PasswordForCert!” -Force -AsPlainText)

#Export certificate to .cer file
$mycert | Export-Certificate -FilePath c:\temp\mycert.cer


2d. Upload the Certificate in the Certificates & Secrets of the Azure App.

3. Assign the Exchange Administrator role (via Azure Roles or MSOL Roles) to the new Registered App.

4. Now that the app is configured with Exchange Permissions, Exchange Access, and the Certificate uploaded, connect to Exchange Online using the pfx Cert from PowerShell:

Connect-ExchangeOnline -CertificateFilePath “C:\temp\mycert.pfx” -CertificatePassword (ConvertTo-SecureString -String “PasswordForCert!” -AsPlainText -Force) -AppID “<AppID of your new registered app>” -Organization “<your tenant organization name>”





Running PowerShell commands against a large group of mailboxes or returning large data returns

Scenario: While attempting to run Exchange Online PowerShell commands against a large group of mailboxes and/or returning large datasets, you may run into mico delays or other errors once you breach any number of throttles that Microsoft Online imposes against your PowerShell session. Although throttles protect Microsoft Online for good reason, your still left with the question “WHY ME?” and how do we get around these strict restrictions.

Solutions: There’s a few ways to execute commands in Exchange Online PowerShell, but each have their own advantages and disadvantages. Below are a few examples with running the command: get-mobiledevicestastics which is naturally heavy in processing and subject to breaching throttles.


Standard Command – This may work if you only have a small number of mailboxes in your environment. Its quick, but also dangerous.
$AllMailboxes.alias | Get-mobiledevicestatistics -mailbox $_



ForEach with a Mini-Sleep – This command will help with some of the throttling, its not difficult to execute by implementing a 2-command loop and will help with the recharge rate for how many commands you are allowed to execute within a time period.

$AllMailboxes.alias | %{ Get-mobiledevicestatistics -mailbox $_; Start-Sleep -milliseconds 500}
-or-
$AllMailboxes.alias | ForEach { Get-mobiledevicestatistics -mailbox $_; Start-Sleep -milliseconds 500}


Invoke-Command to help with Large Data Return – Invoke with Select-Object forces the O365 servers run the command and limits the data returned. This takes your local client out of the loop helping to ease local client throttling. HOWEVER invoke command doesn’t allow complex PowerShell commands either.

Invoke-Command -session (Get-Pssession) -scriptblock {Get-mobiledevicestatistics -mailbox $_ | select-object identity, Device*,LastSuccessSync}
-or-
Invoke-Command -session (Get-Pssession) -scriptblock {Get-Mailbox -resultsize unlimited | select-object -property Displayname,Identity,PrimarySMTPAddress}


Microsoft Module: RobustCloudCommand – Microsoft created a module that can be used, originally it used to be a script. The module is designed to stay under the throttling limitations imposed by Microsoft. The module keeps an eye on the amount of commands issued, the amount of time or data collected, throttling errors, and so on. The module will tear down and rebuild the PowerShell session when needed and pickup where the script left off. AND one of the coolest things is that it supports Modern Auth and will renew your OAUTH tokens automatically. Let those big scripts fly! The only issue I see is that its limited in complex PowerShell commands. However if you want to run a single command, such as adding a BlockEverything Authentication Policy for every single one my theoretical 400K users, the workhorse below will do the trick

Install-Module -Name RobustCloudCommand #https://www.powershellgallery.com/packages/RobustCloudCommand

$users = Import-csv C:\temp\AllMailboxes.csv

Start-RobustCloudCommand -recipients $Users -logfile C:\temp\out.log -ScriptBlock {Get-user $input.userprincipalname -AuthenticationPolicy BlockEverything}











Dumpster of Exchange Online Mailboxes that are on a Hold are maxed out due to bloated recurring meetings

Scenario: We noticed that a handful of mailboxes that are on a hold had their dumpsters maxed out hitting their 110GB RecoverableItemsQuota. The RecoverableItemsQuota increased from 100GB to 110GB when we enabled the AutoExpandingArchiveEnabled property. The users were experiencing these symptoms:
– Users cannot delete items from the Deleted Items folder
-ManagedFolderAssistant may have been hanging on large items, preventing smaller items from moving into the archive via our retention policies
-Users cannot managed or accept updated meeting requests
-Weird intermittent sending/bounceback issues for each user.

Investigating: We discovered a bloated recurring calendar item stored in the Versions folder (Recoverable Items/Dumpster folder). These mailboxes have archive mailboxes as well as retention policies, but it seemed if the retention policy was not moving the items out of the Versions folder into the Archive Versions folder. What also stuck out was there were only 400-500 items in the Versions folder, yet the total size of the Versions folder was near the 100GB mark. We found this by running a command similar to: get-mailboxfolderstatistics steve | Select Name, ItemsinFolder,FolderSize

We also used MFCMAPI to enter into the mailbox to view the contents of the Versions folder. Of the hundreds of items listed in the Versions folder, it was all the same meeting (just a different version of it) and the size of each item ranged from 200MB to 300MB a piece. This means that native Exchange functionality such as Retention Policies would not move the email items into the archive mailbox because they exceeded the MaxSendSize and MaxReceiveSize.

Further investigation, we asked the meeting owner how they were managing the meeting. The meeting owner would manage each occurrence of the meeting separately by updating it with a daily agenda and attachments. This meeting happens every day, meaning it did not take too long for this recurring meeting to bloat past the size that Microsoft can handle.

The Fix: The Holds placed on the mailbox put us into a bind, because at the time there wasn’t an easy way of getting rid of these items by simply deleting them via a Search And Destroy Script, MFCMAPI, or Native Exchange Retention Policies. However it was discovered that we could run this one-liner which was able to remove all of the excess versions for the recurring meeting:

Start-ManagedFolderAssistant steve -HoldCleanup

In prep for the fix, we made sure we put all the items in the versions folder because we were moving them around experimenting. Then ran the command. A few of the users made immediate progress, while others didn’t budge. For those that didnt budge, we ran the -StopHoldCleanup switch and reissued the -HoldCleanup switch. Eventually within a few days, they were cleaned up too.



Determine when a user can start sending email again after hitting the 10,000 Recipient Rate Limit in Exchange Online

Scenario: Recently we had a user who hit the 10K Recipient Rate Limit in a 24 hour period within Exchange Online. This user was performing two types of emailing from their ExOnline mailbox: 1. Sending mass mailings from their Mailbox using their from address and 2. Sending mass mailings from their Mailbox and setting another From Address (Delegated Send-as Permissions for a Service Account). Both types of sending impacted the recipient rate limit for the persons mailbox.

BTW – Do not mass mail directly to recipients from an ExOnline mailbox. Instead use distribution groups or other third party mailing systems to send mass communications.

Scriptlet: Here is a scriptlet to help determine when the user should be able to send from their mailbox again. The results provide hourly recipient counts for the sender which may help in determine the timeframe when a user should no longer be impacted by the 24 hour period:

Edit the Variables in the scriptlet and paste it into Exchange Online PowerShell.

#Edit these variables
$sender= “SteveTheMassMailer@domain.com”
$start = “1/7/2021”
$end = “1/9/2021”
$pageSize = 1000
$P = 1
$messages = $null
$report = @()
$totalRecipients = 0

#Loop for All Messages
do
{
Write-Host “Message Trace – Page $P…”
$temp_Messages = Get-MessageTrace -senderaddress $sender -startdate $start -enddate $end -PageSize $pagesize -Page $P
$P++
$Messages += $temp_Messages
}until ($temp_Messages -eq $null)

#Display messages or Message Count
#$Messages
$Messages.count

#Build the Loop Parameters
$Loop_start = get-date($messages | Sort Received | Select -expandproperty Received -First 1) -format “MM/dd/yyyy HH:00:00”
$Loop_end = get-date($messages | Sort Received | Select -expandproperty Received -last 1) -format “MM/dd/yyyy HH:00:00”
$1Hour_end = get-date($Loop_start)
$1Hour_end = $1Hour_end.AddHours(1)
$1Hour_end = get-date($1Hour_end) -format “MM/dd/yyyy HH:00:00”


#Now Loop It!
“Starting Loop for $sender”
Do{
“Checking $Loop_Start and $1Hour_end”
$RecipientCount = $messages | Where {($_.Received -gt $loop_start) -and ($_.Received -lt $1hour_end)} | Select RecipientAddress
$totalrecipients = $totalRecipients + $recipientCount.count
$recipientCount = $RecipientCount.count

#Report it
$obj = New-Object PSObject
$obj | Add-Member NoteProperty -Name StartTime -Value $Loop_Start
$obj | Add-Member NoteProperty -Name EndTime -Value $1hour_End
$obj | Add-Member NoteProperty -Name RecipientCount -Value $RecipientCount
$Report += $obj

#New Variables
$Loop_Start = $1hour_end
$1Hour_end = get-date($Loop_start)
$1Hour_end = $1Hour_end.AddHours(1)
$1Hour_end = get-date($1Hour_end) -format “MM/dd/yyyy HH:00:00”
}While( (get-date($Loop_start)) -lt (Get-date($Loop_End)))

#Summary
$report
“#Summary######################################################”
“-Results are reported in GMT”
“- Total Recipient Count for $Sender between $start – $end : $TotalRecipients”





Microsoft Example of the 24 hour Recipient Rate Limit:
After the recipient rate limit is reached, messages can’t be sent from the mailbox until the number of recipients that were sent messages in the past 24 hours drops below the limit. For example, a user sends an email message to 5000 recipients at 09:00 AM, then sends another message to 2500 recipients at 10:00 AM, and then sends another message to 2500 recipients at 11:00 AM, hitting the limit of 10,000 messages. The user won’t be able to send messages again until 09:00 AM the next day. 






Exchange Online OWA “500 Something went wrong. Repeating redirects detected”

Scenario: When attempting to access Outlook on the Web (OWA), you receive the following error:


Solution: This was a Microsoft issue. Their frontend servers within the Exchange Online environment were erroring out. In our case we were impacted well before the global Microsoft incident/advisory was issued (Ex231181). We did notice that it was the same frontend server being reported in the error details amongst all impacted clients, it just took a while for Microsoft to acknowledge the issue. I did see other articles suggesting it could be Microsoft Admin rights and roles assigned to the user, but in our case these were just normal users that were being impacted.