commit 0a59e028c9743341f264274ddddb7c16950fd26e Author: Kumi Date: Sat Aug 26 15:08:29 2023 +0200 Current version diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..a3449a7 --- /dev/null +++ b/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2020-2023 Kumi Mitterer + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..1bd4092 --- /dev/null +++ b/README.md @@ -0,0 +1,28 @@ +# Office 365 email purger + +This PowerShell script will use Compliance Search to purge all content older than a specified date involving one or more specified users from all mailboxes in an Office 365 tenant. + +## Usage + +1. Download the script and save it to your computer. +2. Edit the script and change the following lines to match your environment: + + ```powershell + $Addresses = @("user@example.com", "user2@example.com") + $MaxDate = "2016-01-01" + ``` + +3. Run the script. You will be prompted for your Office 365 credentials. +4. Get a cup of coffee, and then another. This script will take a while to run. As in, hours or days, depending on the size of your organization. + +## Notes + +* This script will delete all content older than the specified date, including items in the user's mailbox that were sent to or received from other users in the organization. +* The larger your organization, the longer this script will take to run. +* This is quite a dangerous script. Use it at your own risk. +* This script may delete more or less than you expect. +* The script is provided as-is. I am not responsible for any damage caused by running it. + +## License + +This script is licensed under the [MIT License](LICENSE). diff --git a/purger.ps1 b/purger.ps1 new file mode 100644 index 0000000..7b15616 --- /dev/null +++ b/purger.ps1 @@ -0,0 +1,136 @@ +#!/usr/bin/env pwsh + +# Add any email addresses that should be purged to the $Addresses array + +$Addresses = @() +$MaxDate = "2023-06-30" + +# No modifications below this point. + +function Write-Log { + [CmdletBinding()] + param + ( + [Parameter(Mandatory = $true)] + [string]$Message + ) + + $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" + Write-Output "${timestamp}: $Message" +} + +# Build query for ComplianceSearch + +if (!$Addresses) { + Write-Output "Search query is empty - make sure that Addresses array is not empty - exiting" + exit 1 +} + +$From = "from:" + ($Addresses -join " OR from:") +$To = "to:" + ($Addresses -join " OR to:") +$Participants = "$From OR $To" + +$Query = "($Participants) AND date<=$MaxDate AND (kind:email)" + +# Ask user for admin credentials + +$UserCredential = Get-Credential -Title "Office 365 Credentials" -Message "Enter your credentials for Office 365" + +# Load PowerShell session for Microsoft Exchange + +Import-Module ExchangeOnlineManagement +Connect-IPPSSession -Credential $UserCredential +Connect-ExchangeOnline -Credential $UserCredential + +# Define folders that should be ignored by ComplianceSearch + +$folders = "/Recoverable Items", "/Deletions", "/Purges", "/Versions" + +Write-Output "Finding folders to exclude, this may take a while..." + +$mailboxes = Get-Mailbox + +$folderQueries = @() + +foreach ($mailbox in $mailboxes) { + $emailAddress = $mailbox.PrimarySmtpAddress + $folderStatistics = Get-EXOMailboxFolderStatistics $emailAddress + foreach ($folderStatistic in $folderStatistics) { + $folderId = $folderStatistic.FolderId; + $folderPath = $folderStatistic.FolderPath; + + if ($folders -contains $folderPath) { + + # Based on / shamelessly stolen from: + # https://practical365.com/targeted-collection-content-search/ + + $encoding = [System.Text.Encoding]::GetEncoding("us-ascii") + $nibbler = $encoding.GetBytes("0123456789ABCDEF"); + $folderIdBytes = [Convert]::FromBase64String($folderId); + $indexIdBytes = New-Object byte[] 48; + $indexIdIdx = 0; + $folderIdBytes | Select-Object -skip 23 -First 24 | % { $indexIdBytes[$indexIdIdx++] = $nibbler[$_ -shr 4]; + $indexIdBytes[$indexIdIdx++] = $nibbler[$_ -band 0xF] } + $folderQuery = "folderid:$($encoding.GetString($indexIdBytes))"; + + $folderStat = New-Object PSObject + Add-Member -InputObject $folderStat -MemberType NoteProperty -Name Mailbox -Value $emailAddress + Add-Member -InputObject $folderStat -MemberType NoteProperty -Name FolderPath -Value $folderPath + Add-Member -InputObject $folderStat -MemberType NoteProperty -Name FolderQuery -Value $folderQuery + + $folderQueries += $folderStat + + } + } +} + +$Excluded = "" + +foreach ($folder in $folderQueries) { + $q = $folder.folderQuery + $Excluded += " NOT $q" +} + +$Query += $Excluded + +$Looper = $true + +while ($Looper) { + try { + Write-Output "Creating new search" + New-ComplianceSearch -Name "WAT Remover" -ContentMatchQuery $Query -ExchangeLocation all -AllowNotFoundExchangeLocationsEnabled $true -Force | Out-Null + Start-ComplianceSearch -Identity "WAT Remover" | Out-Null + + Write-Output "Waiting for search to complete" + while (!(Get-ComplianceSearch -Identity "WAT Remover" | Format-List -Property Status | Out-String | Select-String -Pattern "Completed")) { + Start-Sleep -s 1 + } + + $Results = (Get-ComplianceSearch "WAT Remover").Items + if ($Results) { + Write-Output "$Results results found, deleting" + for ($i = 0; $i -lt 10; $i++) { + Write-Output "Starting deletion run $i" + New-ComplianceSearchAction -SearchName "WAT Remover" -Purge -PurgeType HardDelete -Force -Confirm:$false | Out-Null + while (!(Get-ComplianceSearchAction -Identity "WAT Remover_Purge" | Out-String | Select-String -Pattern "Completed")) { + Start-Sleep -s 1 + } + Write-Output "Results deleted" + Remove-ComplianceSearchAction -Confirm:$false -Identity "WAT Remover_Purge" + } + } + else { + Write-Output "No results found, exiting after this run" + $Looper = $false + } + } + finally { + Write-Output "Cleaning up" + Remove-ComplianceSearchAction -Confirm:$false -Identity "WAT Remover_Purge" + Remove-ComplianceSearch -Identity "WAT Remover" -Confirm:$false + } +} + +Write-Output "Done - exiting" + +