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,38 +185,62 @@ function Get-FoldersToPurge {
[string[]]$FolderNamePurgeList = @(), [string[]]$FolderNamePurgeList = @(),
[Parameter(Mandatory = $False, HelpMessage = "Purge folders with these prefixes")] [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"
$foldersToDelete = @()
$traversal = "Deep"
if ($PurgeTraversalShallow) {
$traversal = "Shallow"
}
$offset = 0
$moreToList = $true
# get all folder pages
while ($moreToList) {
# SOAP message for getting the folders # SOAP message for getting the folders
$body = @" $body = @"
<FindFolder Traversal="Deep" xmlns="http://schemas.microsoft.com/exchange/services/2006/messages"> <FindFolder Traversal="$traversal" xmlns="http://schemas.microsoft.com/exchange/services/2006/messages">
<FolderShape> <FolderShape>
<t:BaseShape>Default</t:BaseShape> <t:BaseShape>Default</t:BaseShape>
<t:AdditionalProperties> <t:AdditionalProperties>
<t:ExtendedFieldURI PropertyTag="0x3007" PropertyType="SystemTime"/> <t:ExtendedFieldURI PropertyTag="0x3007" PropertyType="SystemTime"/>
</t:AdditionalProperties> </t:AdditionalProperties>
</FolderShape> </FolderShape>
<m:IndexedPageFolderView MaxEntriesReturned="1000" Offset="$offset" BasePoint="Beginning" />
<ParentFolderIds> <ParentFolderIds>
<t:DistinguishedFolderId Id="$WellKnownRoot"/> <t:DistinguishedFolderId Id="$WellKnownRoot"/>
</ParentFolderIds> </ParentFolderIds>
</FindFolder> </FindFolder>
"@ "@
Write-Host "`nLooking for folders under well-known folder: $WellKnownRoot matching folders: $FolderNamePurgeList or prefixes: $FolderPrefixPurgeList for user: $User" try {
Write-Host "`nRetrieving folders starting from offset: $offset"
$getFolderIdMsg = Initialize-SOAPMessage -User $User -Body $body $getFolderIdMsg = Initialize-SOAPMessage -User $User -Body $body
$response = Invoke-SOAPRequest -Token $Token -Message $getFolderIdMsg $response = Invoke-SOAPRequest -Token $Token -Message $getFolderIdMsg
# 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
# Are there more folders to list # Are there more folders to list
$rootFolder = $response | Select-Xml -XPath "//m:RootFolder" -Namespace @{m = "http://schemas.microsoft.com/exchange/services/2006/messages" } | $rootFolder = $response | Select-Xml -XPath "//m:RootFolder" -Namespace @{m = "http://schemas.microsoft.com/exchange/services/2006/messages" } |
Select-Object -ExpandProperty Node Select-Object -ExpandProperty Node
$moreToList = ![System.Convert]::ToBoolean($rootFolder.IncludesLastItemInRange) $moreToList = ![System.Convert]::ToBoolean($rootFolder.IncludesLastItemInRange)
}
catch {
Write-Host "Error retrieving folders"
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
# Loop through folders # Loop through folders
foreach ($folder in $folders) { foreach ($folder in $folders) {
@ -254,8 +278,16 @@ function Get-FoldersToPurge {
} }
} }
if (!$moreToList -or $null -eq $folders) {
Write-Host "Retrieved all folders."
}
else {
$offset += $folders.count
}
}
# powershel does not do well when returning empty arrays # powershel does not do well when returning empty arrays
return $foldersToDelete, $moreToList return , $foldersToDelete
} }
function Empty-Folder { function Empty-Folder {
@ -355,7 +387,10 @@ function Purge-Folders {
[string[]]$FolderPrefixPurgeList = @(), [string[]]$FolderPrefixPurgeList = @(),
[Parameter(Mandatory = $False, HelpMessage = "Purge folders before this date time (UTC)")] [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 if (($FolderNamePurgeList.count -eq 0) -and
@ -364,9 +399,6 @@ function Purge-Folders {
Exit Exit
} }
Write-Host "`nPurging CI-produced folders..."
Write-Host "--------------------------------"
if ($FolderNamePurgeList.count -gt 0) { if ($FolderNamePurgeList.count -gt 0) {
Write-Host "Folders with names: $FolderNamePurgeList" Write-Host "Folders with names: $FolderNamePurgeList"
} }
@ -382,20 +414,18 @@ function Purge-Folders {
'WellKnownRoot' = $WellKnownRoot; 'WellKnownRoot' = $WellKnownRoot;
'FolderNamePurgeList' = $FolderNamePurgeList; 'FolderNamePurgeList' = $FolderNamePurgeList;
'FolderPrefixPurgeList' = $FolderPrefixPurgeList; 'FolderPrefixPurgeList' = $FolderPrefixPurgeList;
'PurgeBeforeTimestamp' = $PurgeBeforeTimestamp 'PurgeBeforeTimestamp' = $PurgeBeforeTimestamp;
'PurgeTraversalShallow' = $PurgeTraversalShallow
} }
$moreToList = $True $foldersToDelete = Get-FoldersToPurge @foldersToDeleteParams
# 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 $foldersToDeleteCount = $foldersToDelete.count
$foldersToDeleteIds = @() $foldersToDeleteIds = @()
$folderNames = @() $folderNames = @()
if ($foldersToDeleteCount -eq 0) { if ($foldersToDeleteCount -eq 0) {
Write-Host "`nNo folders to purge matching the criteria" Write-Host "`nNo folders to purge matching the criteria"
break return
} }
foreach ($folder in $foldersToDelete) { foreach ($folder in $foldersToDelete) {
@ -405,7 +435,6 @@ function Purge-Folders {
Empty-Folder -FolderIdList $foldersToDeleteIds -FolderNameList $folderNames Empty-Folder -FolderIdList $foldersToDeleteIds -FolderNameList $folderNames
Delete-Folder -FolderIdList $foldersToDeleteIds -FolderNameList $folderNames Delete-Folder -FolderIdList $foldersToDeleteIds -FolderNameList $folderNames
}
} }
function Create-Contact { function Create-Contact {
@ -459,7 +488,7 @@ function Get-ItemsToPurge {
$foldersToSearchBody = "<t:DistinguishedFolderId Id='$WellKnownRoot'/>" $foldersToSearchBody = "<t:DistinguishedFolderId Id='$WellKnownRoot'/>"
if (![String]::IsNullOrEmpty($SubFolderName)) { 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 ) { if ($subFolders.count -gt 0 ) {
$foldersToSearchBody = "" $foldersToSearchBody = ""
@ -615,6 +644,8 @@ function Purge-Items {
} }
} }
### MAIN ####
Write-Host 'Authenticating with Exchange Web Services ...' Write-Host 'Authenticating with Exchange Web Services ...'
$global:Token = Get-AccessToken | ConvertTo-SecureString -AsPlainText -Force $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()) } $FolderNamePurgeList = $FolderNamePurgeList | ForEach-Object { @($_.Split(',').Trim()) }
$FolderPrefixPurgeList = $FolderPrefixPurgeList | ForEach-Object { @($_.Split(',').Trim()) } $FolderPrefixPurgeList = $FolderPrefixPurgeList | ForEach-Object { @($_.Split(',').Trim()) }
Write-Host "`nPurging CI-produced folders under 'msgfolderroot' ..."
Write-Host "--------------------------------------------------------"
$purgeFolderParams = @{ $purgeFolderParams = @{
'WellKnownRoot' = "root"; 'WellKnownRoot' = "msgfolderroot";
'FolderNamePurgeList' = $FolderNamePurgeList; 'FolderNamePurgeList' = $FolderNamePurgeList;
'FolderPrefixPurgeList' = $FolderPrefixPurgeList; 'FolderPrefixPurgeList' = $FolderPrefixPurgeList;
'PurgeBeforeTimestamp' = $PurgeBeforeTimestamp 'PurgeBeforeTimestamp' = $PurgeBeforeTimestamp
} }
#purge older prefix folders #purge older prefix folders from msgfolderroot
Purge-Folders @purgeFolderParams Purge-Folders @purgeFolderParams
#purge older contacts #purge older contacts
@ -647,4 +681,20 @@ Purge-Items -ItemsFolder "calendar" -ItemsSubFolder "Birthdays" -PurgeBeforeTime
# -/Recoverable Items/SubstrateHolds # -/Recoverable Items/SubstrateHolds
Write-Host "`nProcess well-known folders that are always purged" Write-Host "`nProcess well-known folders that are always purged"
Write-Host "---------------------------------------------------" 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"