-
Notifications
You must be signed in to change notification settings - Fork 346
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #2227 from cboonham/chris-CheckMailboxExtendedProp…
…erty New scripts to help manage mailbox extended property quota issues
- Loading branch information
Showing
17 changed files
with
552 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
# Copyright (c) Microsoft Corporation. | ||
# Licensed under the MIT License. | ||
|
||
#Requires -Modules @{ ModuleName="ExchangeOnlineManagement"; ModuleVersion="3.4.0" } | ||
#Requires -Modules @{ ModuleName="Microsoft.Graph.Users"; ModuleVersion="2.24.0" } | ||
#Requires -Modules @{ ModuleName="Microsoft.Graph.Mail"; ModuleVersion="2.24.0" } | ||
|
||
<# | ||
.SYNOPSIS | ||
Removes the extended property (aka named property) the message. | ||
.DESCRIPTION | ||
Takes as input the result from Search-MailboxExtendedProperty.ps1 and removes the extended property from the message. | ||
.PARAMETER MessagesWithExtendedProperty | ||
A list of mailbox items and their extended property, that are to be removed. | ||
.EXAMPLE | ||
$mailboxExtendedProperty = Get-MailboxExtendedProperty -Identity [email protected] | Where-Object { $_.PropertyName -like '*Some Pattern*' } | ||
$messagesWithExtendedProperty = .\Search-MailboxExtendedProperty.ps1 -MailboxExtendedProperty $mailboxExtendedProperty | ||
.\Remove-MailboxExtendedProperty.ps1 -MessagesWithExtendedProperty $messagesWithExtendedProperty | ||
#> | ||
[CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] | ||
param( | ||
[Parameter(Mandatory = $true, Position = 0)] | ||
[ValidateScript({ | ||
if ($_.GetType().FullName -eq 'System.Management.Automation.PSCustomObject' -or $_.GetType().FullName -eq 'System.Object[]') { | ||
$true | ||
} else { | ||
throw "The parameter MailboxExtendedProperty doesn't appear to be the result from running 'Search-MailboxExtendedProperty'." | ||
} | ||
})] | ||
$MessagesWithExtendedProperty | ||
) | ||
|
||
process { | ||
# Get the current Microsoft Graph context | ||
$context = Get-MgContext | ||
if ($null -eq $context) { | ||
Write-Host -ForegroundColor Red "No valid context. Please connect to Microsoft Graph first." | ||
return | ||
} | ||
|
||
# Get the user information for the context | ||
$user = Get-MgUser -UserId $context.Account -Select 'displayName, id, mail, userPrincipalName' | ||
if ($null -eq $user) { | ||
Write-Host -ForegroundColor Red "No valid user. Please check the Microsoft Graph connection." | ||
return | ||
} | ||
|
||
Write-Host "Attempting to remove $($MessagesWithExtendedProperty.Count) extended properties from the mailbox of $($user.UserPrincipalName)." | ||
|
||
foreach ($message in $MessagesWithExtendedProperty) { | ||
if ($message.SingleValueExtendedProperties.Count -eq 1) { | ||
# Url encode the extended property | ||
$extendedProperty = [System.Uri]::EscapeDataString($message.SingleValueExtendedProperties.Id) | ||
|
||
# Construct the URL to remove the extended property from the message | ||
$url = "https://graph.microsoft.com/v1.0/users/$($user.UserPrincipalName)/messages/$($message.ID)/singleValueExtendedProperties/$extendedProperty" | ||
|
||
if ($PSCmdlet.ShouldProcess("Extended property '$($message.SingleValueExtendedProperties.Id)' on the message '$($message.Subject)'.", "Remove")) { | ||
# Remove the extended property from the message (fire and forget) | ||
Invoke-MgGraphRequest -Method DELETE -Uri $url -Headers @{ Authorization = "Bearer $($context.AccessToken)" } | ||
|
||
Write-Host -ForegroundColor Green "Removed the extended property '$($message.SingleValueExtendedProperties.Id)' on the message '$($message.Subject)'." | ||
} | ||
} else { | ||
Write-Host -ForegroundColor Red "Invalid extended property format: $($message.SingleValueExtendedProperties)." | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,145 @@ | ||
# Copyright (c) Microsoft Corporation. | ||
# Licensed under the MIT License. | ||
|
||
#Requires -Modules @{ ModuleName="ExchangeOnlineManagement"; ModuleVersion="3.4.0" } | ||
#Requires -Modules @{ ModuleName="Microsoft.Graph.Users"; ModuleVersion="2.24.0" } | ||
#Requires -Modules @{ ModuleName="Microsoft.Graph.Mail"; ModuleVersion="2.24.0" } | ||
|
||
<# | ||
.SYNOPSIS | ||
Searches for mailbox items with a specified extended property (aka named property). | ||
.DESCRIPTION | ||
For each of the specified mailbox extended properties, this script searches for mailbox items that have the property set. | ||
It returns a list of mailbox items and information about the item and the extended property. | ||
The list of mailbox items returned can be used to further process the items, such as removing the extended property. | ||
There are some limitations: the search is limited to messages (extended properties can exist on folder, contact, calendar instances etc), single value extended properties (not multi-value), and the property value must be a non-null string. | ||
.PARAMETER MailboxExtendedProperty | ||
One of more mailbox extended properties to search for in the mailbox, returned by Get-MailboxExtendedProperty. | ||
.EXAMPLE | ||
$mailboxExtendedProperty = Get-MailboxExtendedProperty -Identity [email protected] | Where-Object { $_.PropertyName -like '*Some Pattern*' } | ||
$messagesWithExtendedProperty = .\Search-MailboxExtendedProperty.ps1 -MailboxExtendedProperty $mailboxExtendedProperty | ||
#> | ||
param( | ||
[Parameter(Mandatory = $true, Position = 0)] | ||
[ValidateScript({ | ||
if ($_.GetType().FullName -eq 'System.Management.Automation.PSObject' -or $_.GetType().FullName -eq 'System.Object[]') { | ||
$true | ||
} else { | ||
throw "The parameter MailboxExtendedProperty doesn't appear to be the result from running 'Get-MailboxExtendedProperty'." | ||
} | ||
})] | ||
$MailboxExtendedProperty | ||
) | ||
|
||
process { | ||
function Get-FolderPath { | ||
param ( | ||
[string]$userId, | ||
[string]$folderId | ||
) | ||
|
||
$folderPath = @() | ||
$currentFolderId = $folderId | ||
|
||
# Get the folder path from the target folder to the root | ||
do { | ||
$folder = Get-MgUserMailFolder -UserId $userId -MailFolderId $currentFolderId -Select 'DisplayName, ParentFolderId' | ||
if ($null -eq $folder) { | ||
break | ||
} else { | ||
$folderPath += $folder.DisplayName | ||
$currentFolderId = $folder.ParentFolderId | ||
} | ||
} | ||
while ($folder.DisplayName -ne "") | ||
|
||
# Reverse the array to get the path from root to the target folder | ||
$fullPath = ($folderPath | Sort-Object -Descending) -join "\" | ||
|
||
# Ensure the path starts with a backslash and does not end with one | ||
return "\" + $fullPath.TrimEnd("\") | ||
} | ||
|
||
# Folder paths already looked up | ||
$mailboxFolderPath = @{} | ||
|
||
# Messages found with the extended property | ||
$message = @() | ||
|
||
# Get the current Microsoft Graph context | ||
$context = Get-MgContext | ||
if ($null -eq $context) { | ||
Write-Host -ForegroundColor Red "No valid context. Please connect to Microsoft Graph first." | ||
return | ||
} | ||
|
||
# Get the user information for the context | ||
$user = Get-MgUser -UserId $context.Account -Select 'displayName, id, mail, userPrincipalName' | ||
if ($null -eq $user) { | ||
Write-Host -ForegroundColor Red "No valid user. Please check the Microsoft Graph connection." | ||
return | ||
} | ||
|
||
Write-Host "Searching for mailbox items with the specified $($MailboxExtendedProperty.Count) extended properties in the mailbox of $($user.UserPrincipalName)." | ||
|
||
# For each of the specified mailbox extended properties | ||
foreach ($property in $MailboxExtendedProperty) { | ||
# Get the mailbox extended property identity again to check if it still exists and parse the identity | ||
$parsedProperty = Get-MailboxExtendedProperty -Identity $property.Identity | ||
|
||
if ($null -eq $parsedProperty) { | ||
Write-Host -ForegroundColor Yellow "Mailbox extended property no longer present in mailbox $($property.Identity)." | ||
continue | ||
} else { | ||
if ($parsedProperty.PropertyType -eq "StringProperty") { | ||
$property = "String {$($parsedProperty.PropertyNamespace.Guid)} Name $($parsedProperty.PropertyName)" | ||
} elseif ($parsedProperty.PropertyType -eq "IdProperty") { | ||
$property = "String {$($parsedProperty.PropertyNamespace.Guid)} Id 0x$($parsedProperty.PropertyId.ToString('X'))" | ||
} else { | ||
Write-Host -ForegroundColor Red "Mailbox extended property type $($parsedProperty.PropertyType) not supported." | ||
continue | ||
} | ||
|
||
Write-Host "Searching for mailbox items with the extended property $property." | ||
|
||
# Url encode the extended property | ||
$urlEncodedProperty = [System.Uri]::UnescapeDataString($property) | ||
# Filter for messages with the extended property set | ||
$filter = "singleValueExtendedProperties/Any(ep: ep/id eq '$urlEncodedProperty' and ep/value ne null)" | ||
# Expand the extended property to get the value | ||
$expandProperty = "singleValueExtendedProperties(`$filter=id eq '$property')" | ||
|
||
# Search for mailbox items with the extended property | ||
$mailboxItem = Get-MgUserMessage -UserId $user.UserPrincipalName -Filter $filter -Property "Subject,ParentFolderId,SingleValueExtendedProperties,InternetMessageId" -ExpandProperty $expandProperty | ||
foreach ($item in $mailboxItem) { | ||
|
||
# Get the folder path for the item (it doesn't exist as a property) | ||
if ($mailboxFolderPath.ContainsKey($item.ParentFolderId)) { | ||
$folderPath = $mailboxFolderPath[$item.ParentFolderId] | ||
} else { | ||
$folderPath = Get-FolderPath -userId $user.UserPrincipalName -folderId $item.ParentFolderId | ||
$mailboxFolderPath[$item.ParentFolderId] = $folderPath | ||
} | ||
|
||
# Add the item to the list of messages | ||
$message += New-Object PSObject -Property @{ | ||
Id = $item.Id | ||
SingleValueExtendedProperties = $item.SingleValueExtendedProperties | ||
Subject = $item.Subject | ||
InternetMessageId = $item.InternetMessageId | ||
ParentFolderId = $item.ParentFolderId | ||
FolderPath = $folderPath | ||
} | ||
} | ||
|
||
Write-Host "Found $($mailboxItem.Count) mailbox items with the extended property $property." | ||
} | ||
} | ||
|
||
Write-Host "Found a total of $($message.Count) mailbox items with the specified $($MailboxExtendedProperty.Count) extended properties in the mailbox of $($user.UserPrincipalName)." | ||
|
||
return $message | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,134 @@ | ||
# Copyright (c) Microsoft Corporation. | ||
# Licensed under the MIT License. | ||
|
||
#Requires -Modules @{ ModuleName="ExchangeOnlineManagement"; ModuleVersion="3.4.0" } | ||
|
||
<# | ||
.SYNOPSIS | ||
Checks what mailbox extended properties (aka named properties) exist in the mailbox and if they are near to any limits. | ||
.DESCRIPTION | ||
This script retrieves the mailbox extended properties for a specified user identity. | ||
.PARAMETER Identity | ||
The identity of the user whose mailbox extended properties are to be retrieved. | ||
.PARAMETER Threshold | ||
The quota threshold to check for having exceeded. Default is 0.9, which is 90% of the allowed quota. | ||
.PARAMETER SelectFirst | ||
The number of sorted descending results to select, when checking any namespace or same name prefix quota. Default is 10. | ||
.EXAMPLE | ||
.\Test-MailboxExtendedProperty.ps1 -Identity [email protected] | ||
.\Test-MailboxExtendedProperty.ps1 -Identity [email protected] -Threshold 0.95 | ||
.\Test-MailboxExtendedProperty.ps1 -Identity [email protected] -Threshold 0.7 -SelectFirst 20 | ||
#> | ||
param( | ||
[Parameter(Mandatory = $true, Position = 0)] | ||
$Identity, | ||
[Parameter(Mandatory = $false, Position = 1)] | ||
[ValidateRange(0.0, 1.0)] | ||
[double]$Threshold = 0.9, | ||
[Parameter(Mandatory = $false, Position = 2)] | ||
$SelectFirst = 10 | ||
) | ||
|
||
process { | ||
Write-Host -ForegroundColor Blue "Checking the mailbox $Identity for having exceeded the threshold of $($Threshold * 100)% of any named properties quota." | ||
|
||
# Flag to indicate if the mailbox has exceeded the threshold of a named properties quota. | ||
$exceededThresholdQuota = $false | ||
# The Guid of the PublicStrings namespace. | ||
$publicStringsNamespace = "00020329-0000-0000-c000-000000000046" | ||
# The Guid of the InternetHeaders namespace. | ||
$internetHeadersNamespace = "00020386-0000-0000-C000-000000000046" | ||
# The length of the prefix to check for named properties with the same name. | ||
$prefixLength = 10 | ||
|
||
# Retrieve the named properties. | ||
$namedProps = Get-MailboxExtendedProperty -Identity $Identity | ||
# Retrieve the named properties quota. | ||
$namedPropsQuota = Get-MailboxStatistics -Identity $Identity | Select-Object -ExpandProperty NamedPropertiesCountQuota | ||
|
||
# The PublicStrings namespace is allowed to be 20% of the named properties quota. | ||
$publicStringsQuota = [int](0.2 * $namedPropsQuota) | ||
# The InternetHeaders namespace is allowed to be 60% of the named properties quota. | ||
$internetHeadersQuota = [int](0.6 * $namedPropsQuota) | ||
# Any namespace is allowed to be 20% of the named properties quota. | ||
$anyNamespaceQuota = [int](0.2 * $namedPropsQuota) | ||
# The same 10 character name prefix is allowed to be 10% of the named properties quota. | ||
$sameNamePrefixQuota = [int](0.1 * $namedPropsQuota) | ||
|
||
Write-Host -ForegroundColor Gray "The total named properties quota is $namedPropsQuota." | ||
Write-Host -ForegroundColor Gray "The PublicStrings namespace named properties quota is $publicStringsQuota." | ||
Write-Host -ForegroundColor Gray "The InternetHeaders namespace named properties quota is $internetHeadersQuota." | ||
Write-Host -ForegroundColor Gray "The any namespace named properties quota is $anyNamespaceQuota." | ||
Write-Host -ForegroundColor Gray "The same name prefix named properties quota is $sameNamePrefixQuota." | ||
|
||
Write-Host -ForegroundColor Blue "Checking if the mailbox has exceeded the threshold of total named properties quota." | ||
if ($namedProps.Count -ge [int]($Threshold * $namedPropsQuota)) { | ||
Write-Host -ForegroundColor Yellow "The mailbox has $($namedProps.Count) named properties. The quota is $namedPropsQuota." | ||
$exceededThresholdQuota = $true | ||
} else { | ||
Write-Host -ForegroundColor Green "The mailbox is under quota with $($namedProps.Count) named properties." | ||
} | ||
|
||
$namedPropsPublicStrings = $namedProps | Where-Object { $_.PropertyNamespace -eq $publicStringsNamespace } | ||
$namedPropsInternetHeaders = $namedProps | Where-Object { $_.PropertyNamespace -eq $internetHeadersNamespace } | ||
|
||
Write-Host -ForegroundColor Blue "Checking if the mailbox has exceeded the threshold of PublicStrings namespace named properties quota." | ||
if ($namedPropsPublicStrings.Count -ge [int]($Threshold * $publicStringsQuota)) { | ||
Write-Host -ForegroundColor Yellow "The PublicStrings namespace has $($namedPropsPublicStrings.Count) named properties. The quota is $publicStringsQuota." | ||
$exceededThresholdQuota = $true | ||
} else { | ||
Write-Host -ForegroundColor Green "The PublicStrings namespace is under quota with $($namedPropsPublicStrings.Count) named properties." | ||
} | ||
|
||
Write-Host -ForegroundColor Blue "Checking if the mailbox has exceeded the threshold of InternetHeaders namespace named properties quota." | ||
if ($namedPropsInternetHeaders.Count -ge [int]($Threshold * $internetHeadersQuota)) { | ||
Write-Host -ForegroundColor Yellow "The InternetHeaders namespace has $($namedPropsInternetHeaders.Count) named properties. The quota is $internetHeadersQuota." | ||
$exceededThresholdQuota = $true | ||
} else { | ||
Write-Host -ForegroundColor Green "The InternetHeaders namespace is under quota with $($namedPropsInternetHeaders.Count) named properties." | ||
} | ||
|
||
Write-Host -ForegroundColor Blue "Checking if the mailbox has exceeded the threshold of any other namespace named properties quota." | ||
$namespaces = $namedProps | Where-Object { $_.PropertyNamespace -ne $publicStringsNamespace -or $_.PropertyNamespace -ne $internetHeadersNamespace } | Group-Object PropertyNamespace -NoElement | Sort-Object Count -Descending | Select-Object -First $SelectFirst | ||
foreach ($namespace in $namespaces) { | ||
if ($namespace.Count -ge [int]($Threshold * $anyNamespaceQuota)) { | ||
Write-Host -ForegroundColor Yellow "The $($namespace.Name) namespace has $($namespace.Count) named properties. The quota is $anyNamespaceQuota." | ||
$exceededThresholdQuota = $true | ||
} else { | ||
Write-Host -ForegroundColor Green "The $($namespace.Name) namespace is under quota with $($namespace.Count) named properties." | ||
} | ||
} | ||
|
||
Write-Host -ForegroundColor Blue "Checking if the mailbox has exceeded the threshold of named properties with the same name prefix quota." | ||
$propPrefix=@{} | ||
$namedProps | Where-Object { $_.PropertyType -eq "StringProperty" -and $_.PropertyName -ne $null } | ForEach-Object { | ||
$propPrefixKey = $_.PropertyName | ||
if ($propPrefixKey.Length -gt $prefixLength) { | ||
$propPrefixKey=$propPrefixKey.Substring(0, $prefixLength) | ||
} | ||
$propPrefix[$propPrefixKey]++ | ||
} | ||
$topPropPrefix = $propPrefix.GetEnumerator() | Sort-Object -Property Value -Descending | Select-Object -First $SelectFirst | ||
|
||
foreach ($prefix in $topPropPrefix) { | ||
if ($prefix.Value -ge [int]($Threshold * $sameNamePrefixQuota)) { | ||
Write-Host -ForegroundColor Yellow "The $($prefix.Name) prefix has $($prefix.Value) named properties. The quota is $sameNamePrefixQuota." | ||
$exceededThresholdQuota = $true | ||
} else { | ||
Write-Host -ForegroundColor Green "The $($prefix.Name) prefix is under quota with $($prefix.Value) named properties." | ||
} | ||
} | ||
|
||
Write-Host -ForegroundColor Blue "Summary, checking $SelectFirst result(s) and threshold of $($Threshold * 100)%." | ||
if ($exceededThresholdQuota) { | ||
Write-Host -ForegroundColor Red "The mailbox has exceeded the threshold of a named properties quota. See above for which quota(s) have been exceeded." | ||
} else { | ||
Write-Host -ForegroundColor Green "The mailbox is under the threshold of each named properties quota." | ||
} | ||
} |
Oops, something went wrong.