Exchange cleanup improvements (#3551)

Exchange cleanup improvements:
* Explicitly clean-up folders in DeletedItems - Empty folder does not seem to work reliably
* Cleanup any folders in DeletedItems - helps get rid of random manual deletions 
* Harden paging - issues where if no items were deleted initial page, we'd never make progress 
* More focused search on `msgfolderroot`
---

#### Does this PR need a docs update or release note?

- [ ]  Yes, it's included
- [ ] 🕐 Yes, but in a later PR
- [x]  No

#### Type of change

<!--- Please check the type of change your PR introduces: --->
- [ ] 🌻 Feature
- [x] 🐛 Bugfix
- [ ] 🗺️ Documentation
- [ ] 🤖 Supportability/Tests
- [x] 💻 CI/Deployment
- [ ] 🧹 Tech Debt/Cleanup

#### Issue(s)

<!-- Can reference multiple issues. Use one of the following "magic words" - "closes, fixes" to auto-close the Github issue. -->
* #<issue>

#### Test Plan

<!-- How will this be tested prior to merging.-->
- [x] 💪 Manual
- [ ]  Unit test
- [ ] 💚 E2E
This commit is contained in:
Georgi Matev 2023-06-01 10:46:12 -07:00 committed by GitHub
parent 6fd453a699
commit aa271fe09b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

View File

@ -185,77 +185,109 @@ function Get-FoldersToPurge {
[string[]]$FolderNamePurgeList = @(),
[Parameter(Mandatory = $False, HelpMessage = "Purge folders with these prefixes")]
[string[]]$FolderPrefixPurgeList = @()
[string[]]$FolderPrefixPurgeList = @(),
[Parameter(Mandatory = $False, HelpMessage = "Perform shallow traversal only")]
[bool]$PurgeTraversalShallow = $false
)
$foldersToDelete = @()
Write-Host "`nLooking for folders under well-known folder: $WellKnownRoot matching folders: $FolderNamePurgeList or prefixes: $FolderPrefixPurgeList for user: $User"
# SOAP message for getting the folders
$body = @"
<FindFolder Traversal="Deep" xmlns="http://schemas.microsoft.com/exchange/services/2006/messages">
$foldersToDelete = @()
$traversal = "Deep"
if ($PurgeTraversalShallow) {
$traversal = "Shallow"
}
$offset = 0
$moreToList = $true
# get all folder pages
while ($moreToList) {
# SOAP message for getting the folders
$body = @"
<FindFolder Traversal="$traversal" xmlns="http://schemas.microsoft.com/exchange/services/2006/messages">
<FolderShape>
<t:BaseShape>Default</t:BaseShape>
<t:AdditionalProperties>
<t:ExtendedFieldURI PropertyTag="0x3007" PropertyType="SystemTime"/>
</t:AdditionalProperties>
</FolderShape>
<m:IndexedPageFolderView MaxEntriesReturned="1000" Offset="$offset" BasePoint="Beginning" />
<ParentFolderIds>
<t:DistinguishedFolderId Id="$WellKnownRoot"/>
</ParentFolderIds>
</FindFolder>
"@
Write-Host "`nLooking for folders under well-known folder: $WellKnownRoot matching folders: $FolderNamePurgeList or prefixes: $FolderPrefixPurgeList for user: $User"
$getFolderIdMsg = Initialize-SOAPMessage -User $User -Body $body
$response = Invoke-SOAPRequest -Token $Token -Message $getFolderIdMsg
try {
Write-Host "`nRetrieving folders starting from offset: $offset"
# Get the folders from the response
$folders = $response | Select-Xml -XPath "//t:Folders/*" -Namespace @{t = "http://schemas.microsoft.com/exchange/services/2006/types" } |
Select-Object -ExpandProperty Node
$getFolderIdMsg = Initialize-SOAPMessage -User $User -Body $body
$response = Invoke-SOAPRequest -Token $Token -Message $getFolderIdMsg
# Are there more folders to list
$rootFolder = $response | Select-Xml -XPath "//m:RootFolder" -Namespace @{m = "http://schemas.microsoft.com/exchange/services/2006/messages" } |
Select-Object -ExpandProperty Node
$moreToList = ![System.Convert]::ToBoolean($rootFolder.IncludesLastItemInRange)
# Are there more folders to list
$rootFolder = $response | Select-Xml -XPath "//m:RootFolder" -Namespace @{m = "http://schemas.microsoft.com/exchange/services/2006/messages" } |
Select-Object -ExpandProperty Node
$moreToList = ![System.Convert]::ToBoolean($rootFolder.IncludesLastItemInRange)
}
catch {
Write-Host "Error retrieving folders"
# Loop through folders
foreach ($folder in $folders) {
$folderName = $folder.DisplayName
$folderCreateTime = $folder.ExtendedProperty
| Where-Object { $_.ExtendedFieldURI.PropertyTag -eq "0x3007" }
| Select-Object -ExpandProperty Value
| Get-Date
Write-Host $response.OuterXml
Exit
}
# Get the folders from the response
$folders = $response | Select-Xml -XPath "//t:Folders/*" -Namespace @{t = "http://schemas.microsoft.com/exchange/services/2006/types" } |
Select-Object -ExpandProperty Node
if ($FolderNamePurgeList.count -gt 0) {
$IsNameMatchParams = @{
'FolderName' = $folderName;
'FolderNamePurgeList' = $FolderNamePurgeList
}
# Loop through folders
foreach ($folder in $folders) {
$folderName = $folder.DisplayName
$folderCreateTime = $folder.ExtendedProperty
| Where-Object { $_.ExtendedFieldURI.PropertyTag -eq "0x3007" }
| Select-Object -ExpandProperty Value
| Get-Date
if ((IsNameMatch @IsNameMatchParams)) {
Write-Host "• Found name match: $folderName ($folderCreateTime)"
$foldersToDelete += $folder
continue
if ($FolderNamePurgeList.count -gt 0) {
$IsNameMatchParams = @{
'FolderName' = $folderName;
'FolderNamePurgeList' = $FolderNamePurgeList
}
if ((IsNameMatch @IsNameMatchParams)) {
Write-Host "• Found name match: $folderName ($folderCreateTime)"
$foldersToDelete += $folder
continue
}
}
if ($FolderPrefixPurgeList.count -gt 0) {
$IsPrefixAndAgeMatchParams = @{
'FolderName' = $folderName;
'FolderCreateTime' = $folderCreateTime;
'FolderPrefixPurgeList' = $FolderPrefixPurgeList;
'PurgeBeforeTimestamp' = $PurgeBeforeTimestamp;
}
if ((IsPrefixAndAgeMatch @IsPrefixAndAgeMatchParams)) {
Write-Host "• Found prefix match: $folderName ($folderCreateTime)"
$foldersToDelete += $folder
}
}
}
if ($FolderPrefixPurgeList.count -gt 0) {
$IsPrefixAndAgeMatchParams = @{
'FolderName' = $folderName;
'FolderCreateTime' = $folderCreateTime;
'FolderPrefixPurgeList' = $FolderPrefixPurgeList;
'PurgeBeforeTimestamp' = $PurgeBeforeTimestamp;
}
if ((IsPrefixAndAgeMatch @IsPrefixAndAgeMatchParams)) {
Write-Host "• Found prefix match: $folderName ($folderCreateTime)"
$foldersToDelete += $folder
}
if (!$moreToList -or $null -eq $folders) {
Write-Host "Retrieved all folders."
}
else {
$offset += $folders.count
}
}
# powershel does not do well when returning empty arrays
return $foldersToDelete, $moreToList
return , $foldersToDelete
}
function Empty-Folder {
@ -355,7 +387,10 @@ function Purge-Folders {
[string[]]$FolderPrefixPurgeList = @(),
[Parameter(Mandatory = $False, HelpMessage = "Purge folders before this date time (UTC)")]
[datetime]$PurgeBeforeTimestamp
[datetime]$PurgeBeforeTimestamp,
[Parameter(Mandatory = $False, HelpMessage = "Perform shallow traversal only")]
[bool]$PurgeTraversalShallow = $false
)
if (($FolderNamePurgeList.count -eq 0) -and
@ -364,9 +399,6 @@ function Purge-Folders {
Exit
}
Write-Host "`nPurging CI-produced folders..."
Write-Host "--------------------------------"
if ($FolderNamePurgeList.count -gt 0) {
Write-Host "Folders with names: $FolderNamePurgeList"
}
@ -382,30 +414,27 @@ function Purge-Folders {
'WellKnownRoot' = $WellKnownRoot;
'FolderNamePurgeList' = $FolderNamePurgeList;
'FolderPrefixPurgeList' = $FolderPrefixPurgeList;
'PurgeBeforeTimestamp' = $PurgeBeforeTimestamp
'PurgeBeforeTimestamp' = $PurgeBeforeTimestamp;
'PurgeTraversalShallow' = $PurgeTraversalShallow
}
$moreToList = $True
# only get max of 1000 results so we may need to iterate over eligible folders
while ($moreToList) {
$foldersToDelete, $moreToList = Get-FoldersToPurge @foldersToDeleteParams
$foldersToDeleteCount = $foldersToDelete.count
$foldersToDeleteIds = @()
$folderNames = @()
$foldersToDelete = Get-FoldersToPurge @foldersToDeleteParams
$foldersToDeleteCount = $foldersToDelete.count
$foldersToDeleteIds = @()
$folderNames = @()
if ($foldersToDeleteCount -eq 0) {
Write-Host "`nNo folders to purge matching the criteria"
break
}
foreach ($folder in $foldersToDelete) {
$foldersToDeleteIds += $folder.FolderId.Id
$folderNames += $folder.DisplayName
}
Empty-Folder -FolderIdList $foldersToDeleteIds -FolderNameList $folderNames
Delete-Folder -FolderIdList $foldersToDeleteIds -FolderNameList $folderNames
if ($foldersToDeleteCount -eq 0) {
Write-Host "`nNo folders to purge matching the criteria"
return
}
foreach ($folder in $foldersToDelete) {
$foldersToDeleteIds += $folder.FolderId.Id
$folderNames += $folder.DisplayName
}
Empty-Folder -FolderIdList $foldersToDeleteIds -FolderNameList $folderNames
Delete-Folder -FolderIdList $foldersToDeleteIds -FolderNameList $folderNames
}
function Create-Contact {
@ -459,7 +488,7 @@ function Get-ItemsToPurge {
$foldersToSearchBody = "<t:DistinguishedFolderId Id='$WellKnownRoot'/>"
if (![String]::IsNullOrEmpty($SubFolderName)) {
$subFolders, $moreToList = Get-FoldersToPurge -WellKnownRoot $WellKnownRoot -FolderNamePurgeList $SubFolderName -PurgeBeforeTimestamp $PurgeBeforeTimestamp
$subFolders = Get-FoldersToPurge -WellKnownRoot $WellKnownRoot -FolderNamePurgeList $SubFolderName -PurgeBeforeTimestamp $PurgeBeforeTimestamp
if ($subFolders.count -gt 0 ) {
$foldersToSearchBody = ""
@ -615,6 +644,8 @@ function Purge-Items {
}
}
### MAIN ####
Write-Host 'Authenticating with Exchange Web Services ...'
$global:Token = Get-AccessToken | ConvertTo-SecureString -AsPlainText -Force
@ -622,14 +653,17 @@ $global:Token = Get-AccessToken | ConvertTo-SecureString -AsPlainText -Force
$FolderNamePurgeList = $FolderNamePurgeList | ForEach-Object { @($_.Split(',').Trim()) }
$FolderPrefixPurgeList = $FolderPrefixPurgeList | ForEach-Object { @($_.Split(',').Trim()) }
Write-Host "`nPurging CI-produced folders under 'msgfolderroot' ..."
Write-Host "--------------------------------------------------------"
$purgeFolderParams = @{
'WellKnownRoot' = "root";
'WellKnownRoot' = "msgfolderroot";
'FolderNamePurgeList' = $FolderNamePurgeList;
'FolderPrefixPurgeList' = $FolderPrefixPurgeList;
'PurgeBeforeTimestamp' = $PurgeBeforeTimestamp
}
#purge older prefix folders
#purge older prefix folders from msgfolderroot
Purge-Folders @purgeFolderParams
#purge older contacts
@ -647,4 +681,20 @@ Purge-Items -ItemsFolder "calendar" -ItemsSubFolder "Birthdays" -PurgeBeforeTime
# -/Recoverable Items/SubstrateHolds
Write-Host "`nProcess well-known folders that are always purged"
Write-Host "---------------------------------------------------"
Empty-Folder -WellKnownRoot "deleteditems", "recoverableitemsroot"
# We explicitly also clean direct folders under Deleted Items since there is some evidence
# that suggests that emptying alone may not be reliable
Write-Host "`nExplicit delete of all folders under 'DeletedItems' ..."
Write-Host "----------------------------------------------------------"
$purgeFolderParams = @{
'WellKnownRoot' = "deleteditems";
'FolderNamePurgeList' = $FolderNamePurgeList;
'FolderPrefixPurgeList' = @('*');
'PurgeBeforeTimestamp' = (Get-Date);
'PurgeTraversalShallow' = $true
}
Purge-Folders @purgeFolderParams
Empty-Folder -WellKnownRootList "deleteditems", "recoverableitemsroot"