Delete messages older than N days
Posted: 2020-11-01 18:49
There's a few of these around here, so this is nothing new, of course. But I do think I've improved on it, so I'm sharing it here.
As part of my hmailserver backup offsite routine I also made a fairly simplistic cleardown script, which was basically Jimimaseye's backup and cleardown script but rewritten in powershell. My first go at it was very linear in fashion which looked for a foldername match at the top level, then pruned messages older than N days for that folder plus all subfolders 3 levels down. There are big limitations on that method, of course. What if a you want to prune a subfolder, but the top level doesn't match your list of folders?
I've been playing around with this for a couple of days and have come up with *almost* perfection. The logic of it goes like this:
* Looks down every level of subfolders for name matches. For example, if you have a folder called PRUNE located at TOPLEVELFOLDER > SECONDLEVELFOLDER > PRUNE, it will find the folder and prune messages from it.
* CONFIG:PruneSubFolders - If you set this to true, it will prune all subfolders WITHIN the matching folder. For example, if your folder structure contains TOPLEVELFOLDER > SECONDLEVELFOLDER > PRUNE > FOURTHLEVELFOLDER, it will prune both PRUNE and FOURTHLEVELFOLDER
* CONFIG:DeleteEmptySubFolders - If you set this to true, it will delete empty subfolders WITHIN the matching folder UNLESS there is a subfolder of the empty subfolder that contains messages. The idea behind this is that message pruning is for trash and spam, mainly. Therefore if you delete a folder which then ends up in the trash folder, at some point the folder should also be deleted. This function will never delete name matching folders - it only deletes empty subfolders of matching folders if there are no folders within the empty folder that contain messages. Of course, if you delete a folder, all messages within the folder and all subfolders and the messages they contain will be deleted as well, so that's why it only works when there are no subfolders with messages.
* All searching is infinite - it looks down as many levels as exist to find what it needs.
* Folder name matching uses regex, so if you have, for example, automatically created folders you will be able to find them via regex match.
Above, I said *almost* perfection because there's one thing that's nagging me. In my original simplistic approach, which only looked down specified levels, I was able to output breadcrumb folder names like this: "Deleted 4 messages in user@mydomain.tld > Trash > hoohah > ddd". For the life of me, I cannot figure out how to breadcrumb the folders using the approach I took on this script. Maybe its not a big deal, but I found breadcrumbs to be useful information. Currently the output goes like this: "Deleted 1 messages from 3rdlevel in user@mydomain.tld". Of course, in this instance, I know its a third level folder but only because that's how I named the folder when testing things out. It could be a subfolder 8 levels down from the matching folder and you'd never know what level it is unless you had the folder structure memorized.
I think the solution to the breadcrumb could be in exploding the com command but I haven't figured out how to do it yet. Maybe someone has a tip that can put me in the right direction.
Anyway, here's the script. For testing, set $DoDelete to false and you will get the report without actually deleting any messages or folders. I haven't put this into my hMailServer Offsite Backup script yet because I want to test it some more before implementing it on a live server. Everything works for me so far. Any feedback would be greatly appreciated.
As part of my hmailserver backup offsite routine I also made a fairly simplistic cleardown script, which was basically Jimimaseye's backup and cleardown script but rewritten in powershell. My first go at it was very linear in fashion which looked for a foldername match at the top level, then pruned messages older than N days for that folder plus all subfolders 3 levels down. There are big limitations on that method, of course. What if a you want to prune a subfolder, but the top level doesn't match your list of folders?
I've been playing around with this for a couple of days and have come up with *almost* perfection. The logic of it goes like this:
* Looks down every level of subfolders for name matches. For example, if you have a folder called PRUNE located at TOPLEVELFOLDER > SECONDLEVELFOLDER > PRUNE, it will find the folder and prune messages from it.
* CONFIG:PruneSubFolders - If you set this to true, it will prune all subfolders WITHIN the matching folder. For example, if your folder structure contains TOPLEVELFOLDER > SECONDLEVELFOLDER > PRUNE > FOURTHLEVELFOLDER, it will prune both PRUNE and FOURTHLEVELFOLDER
* CONFIG:DeleteEmptySubFolders - If you set this to true, it will delete empty subfolders WITHIN the matching folder UNLESS there is a subfolder of the empty subfolder that contains messages. The idea behind this is that message pruning is for trash and spam, mainly. Therefore if you delete a folder which then ends up in the trash folder, at some point the folder should also be deleted. This function will never delete name matching folders - it only deletes empty subfolders of matching folders if there are no folders within the empty folder that contain messages. Of course, if you delete a folder, all messages within the folder and all subfolders and the messages they contain will be deleted as well, so that's why it only works when there are no subfolders with messages.
* All searching is infinite - it looks down as many levels as exist to find what it needs.
* Folder name matching uses regex, so if you have, for example, automatically created folders you will be able to find them via regex match.
Above, I said *almost* perfection because there's one thing that's nagging me. In my original simplistic approach, which only looked down specified levels, I was able to output breadcrumb folder names like this: "Deleted 4 messages in user@mydomain.tld > Trash > hoohah > ddd". For the life of me, I cannot figure out how to breadcrumb the folders using the approach I took on this script. Maybe its not a big deal, but I found breadcrumbs to be useful information. Currently the output goes like this: "Deleted 1 messages from 3rdlevel in user@mydomain.tld". Of course, in this instance, I know its a third level folder but only because that's how I named the folder when testing things out. It could be a subfolder 8 levels down from the matching folder and you'd never know what level it is unless you had the folder structure memorized.
I think the solution to the breadcrumb could be in exploding the com command but I haven't figured out how to do it yet. Maybe someone has a tip that can put me in the right direction.
Anyway, here's the script. For testing, set $DoDelete to false and you will get the report without actually deleting any messages or folders. I haven't put this into my hMailServer Offsite Backup script yet because I want to test it some more before implementing it on a live server. Everything works for me so far. Any feedback would be greatly appreciated.
Code: Select all
<#
.SYNOPSIS
Prune Messages
.DESCRIPTION
Delete messages in specified folders older than N days
.FUNCTIONALITY
Looks for folder name match at any folder level and if found, deletes all messages older than N days within that folder and all subfolders within
Deletes empty subfolders within matching folders if DeleteEmptySubFolders set to True in config
.PARAMETER
.NOTES
Folder name matching occurs at any level folder
Empty folders are assumed to be trash if they're located in this script
Only empty folders found in levels BELOW matching level will be deleted
.EXAMPLE
#>
<### USER VARIABLES ###>
$hMSAdminPass = "secretpassword" # hMailServer Admin password
$DoDelete = $False # FOR TESTING - set to false to run and report results without deleting messages and folders
$PruneSubFolders = $True # True will prune all folders in levels below name matching folders
$DeleteEmptySubFolders = $True # True will delete empty subfolders below the matching level unless a subfolder within contains messages
$DaysBeforeDelete = 30 # Number of days to keep messages in cleanup folders
$PruneFolders = "2ndlevel|Trash|Deleted|Junk|Spam|(2020-[0-1][0-9]-[0-3][0-9])$|ListMail|Unsubscribes" # Names of IMAP folders you want to cleanup - uses regex
$Error.Clear()
Set-Variable -Name TotalDeletedMessages -Value 0 -Option AllScope
Set-Variable -Name TotalDeletedFolders -Value 0 -Option AllScope
Function Debug ($DebugOutput) {Write-Host $DebugOutput}
Function ElapsedTime ($EndTime) {
$TimeSpan = New-Timespan $EndTime
If (([int]($TimeSpan).Hours) -eq 0) {$Hours = ""} ElseIf (([int]($TimeSpan).Hours) -eq 1) {$Hours = "1 hour "} Else {$Hours = "$([int]($TimeSpan).Hours) hours "}
If (([int]($TimeSpan).Minutes) -eq 0) {$Minutes = ""} ElseIf (([int]($TimeSpan).Minutes) -eq 1) {$Minutes = "1 minute "} Else {$Minutes = "$([int]($TimeSpan).Minutes) minutes "}
If (([int]($TimeSpan).Seconds) -eq 1) {$Seconds = "1 second"} Else {$Seconds = "$([int]($TimeSpan).Seconds) seconds"}
If (($TimeSpan).TotalSeconds -lt 1) {
$Return = "less than 1 second"
} Else {
$Return = "$Hours$Minutes$Seconds"
}
Return $Return
}
Function GetSubFolders ($Folder) {
$IterateFolder = 0
$ArrayDeletedFolders = @()
If ($Folder.SubFolders.Count -gt 0) {
Do {
$SubFolder = $Folder.SubFolders.Item($IterateFolder)
$SubFolderName = $SubFolder.Name
$SubFolderID = $SubFolder.ID
If ($SubFolder.Subfolders.Count -gt 0) {GetSubFolders $SubFolder}
If ($SubFolder.Messages.Count -gt 0) {
If ($PruneSubFolders) {GetMessages $SubFolder}
} Else {
If ($DeleteEmptySubFolders) {$ArrayDeletedFolders += $SubFolderID}
}
$IterateFolder++
} Until ($IterateFolder -eq $Folder.SubFolders.Count)
}
If ($DeleteEmptySubFolders) {
$ArrayDeletedFolders | ForEach {
$ASFName = $Folder.SubFolders.ItemByDBID($_).Name
If (SubFoldersEmpty $Subfolder) {
Try {
If ($DoDelete) {$Folder.SubFolders.DeleteByDBID($_)}
$TotalDeletedFolders++
Debug "Deleted empty subfolder $ASFName in $AccountAddress"
}
Catch {
Debug "[ERROR] Deleting empty subfolder $ASFName in $AccountAddress"
Debug "[ERROR] : $Error"
}
$Error.Clear()
}
}
}
$ArrayDeletedFolders.Clear()
}
Function SubFoldersEmpty ($Folder) {
$IterateFolder = 0
$Return = $False
If ($Folder.SubFolders.Count -gt 0) {
Do {
$SubFolder = $Folder.SubFolders.Item($IterateFolder)
If ($SubFolder.Messages.Count -gt 0) {
$Return = $True
} Else {
SubFoldersEmpty $SubFolder
}
$IterateFolder++
} Until ($IterateFolder -eq $Folder.SubFolders.Count)
}
Return $Return
}
Function GetMatchFolders ($Folder) {
$IterateFolder = 0
If ($Folder.SubFolders.Count -gt 0) {
Do {
$SubFolder = $Folder.SubFolders.Item($IterateFolder)
$SubFolderName = $SubFolder.Name
If ($SubFolderName -match [regex]$PruneFolders) {
GetSubFolders $SubFolder
GetMessages $SubFolder
} Else {
GetMatchFolders $SubFolder
}
$IterateFolder++
} Until ($IterateFolder -eq $Folder.SubFolders.Count)
}
}
Function GetMessages ($Folder) {
$IterateMessage = 0
$ArrayDeletedMessages = @()
$DeletedMessages = 0
If ($Folder.Messages.Count -gt 0) {
Do {
$Message = $Folder.Messages.Item($IterateMessage)
If ($Message.InternalDate -lt ((Get-Date).AddDays(-$DaysBeforeDelete))) {
$ArrayDeletedMessages += $Message.ID
$ArrayCountDeletedMessages += $Message.ID
}
$IterateMessage++
} Until ($IterateMessage -eq $Folder.Messages.Count)
}
$ArrayDeletedMessages | ForEach {
$AFolderName = $Folder.Name
Try {
If ($DoDelete) {$Folder.Messages.DeleteByDBID($_)}
$DeletedMessages++
$TotalDeletedMessages++
}
Catch {
Debug "[ERROR] Deleting messages from folder $AFolderName in $AccountAddress"
Debug "[ERROR] $Error"
}
$Error.Clear()
}
If ($DeletedMessages -gt 0) {
Debug "Deleted $DeletedMessages messages from $AFolderName in $AccountAddress"
}
$ArrayDeletedMessages.Clear()
}
Function DeleteOldMessages {
$BeginDeletingOldMessages = Get-Date
Debug "----------------------------"
Debug "Begin deleting messages older than $DaysBeforeDelete days"
<# Authenticate hMailServer COM #>
$hMS = New-Object -COMObject hMailServer.Application
$hMS.Authenticate("Administrator", $hMSAdminPass) | Out-Null
$EnumDomain = 0
Do {
$hMSDomain = $hMS.Domains.Item($EnumDomain)
If ($hMSDomain.Active) {
$EnumAccount = 0
Do {
$hMSAccount = $hMSDomain.Accounts.Item($EnumAccount)
If ($hMSAccount.Active) {
$AccountAddress = $hMSAccount.Address
$EnumFolder = 0
If ($hMSAccount.IMAPFolders.Count -gt 0) {
Do {
$hMSIMAPFolder = $hMSAccount.IMAPFolders.Item($EnumFolder)
If ($hMSIMAPFolder.Name -match [regex]$PruneFolders) {
If ($hMSIMAPFolder.SubFolders.Count -gt 0) {
GetSubFolders $hMSIMAPFolder
} # IF SUBFOLDER COUNT > 0
GetMessages $hMSIMAPFolder
} # IF FOLDERNAME MATCH REGEX
Else {GetMatchFolders $hMSIMAPFolder}
$EnumFolder++
} Until ($EnumFolder -eq $hMSAccount.IMAPFolders.Count)
} # IF IMAPFOLDER COUNT > 0
} #IF ACCOUNT ACTIVE
$EnumAccount++
} Until ($EnumAccount -eq $hMSDomain.Accounts.Count)
} # IF DOMAIN ACTIVE
$EnumDomain++
} Until ($EnumDomain -eq $hMS.Domains.Count)
If ($TotalDeletedMessages -gt 0) {
Debug "[OK] Finished deleting $TotalDeletedMessages messages in $(ElapsedTime $BeginDeletingOldMessages)"
} Else {
Debug "[OK] No messages older than $DaysBeforeDelete days to delete"
}
If ($TotalDeletedFolders -gt 0) {
Debug "[OK] Deleted $TotalDeletedFolders empty subfolders"
}
} # END FUNCTION
DeleteOldMessages