This script prompts you for the path to the users folder. It will recursively prune every calendar.mrk file it finds in the directory provided and in any child folders. If you only want to prune the calendars in the c:\mdaemon\users\domain.com\user1\calendar.IMAP folder, provide that full path. The script does make a backup of every MRK file before it starts pruning. On line 227 of the script, you set how many years to keep. For example if you enter -2, then any event that is older than 2 years from today will be removed. Recurring events are only removed if the last occurence of the event is more than X years ago, where X is the value provided.
The script will also remove ICS, TNEF, and attachments associated with an event that is being pruned.
If quotas are enabled, the script will also force MDaemon to recalculate the quotas for any account that has the calendar pruned.
I would highly reccomend that you test this script out in a test environment to be sure you it is going to do what you want and provided the desired outcome.
MDaemon Technologies offers no warranty, provides no support, and is not responsible for any damages that may arise from the use of this script. Use at your own risk.
#########################################################################################################################
#########################################################################################################################
## Prune Calendar Events V1.4 ##
## March 5, 2024 ##
## Copyright MDaemon Technologies 2024 ##
## ##
## This script is designed prune events from the Calendar.mrk file that are older than X years. It searches the default ##
## mail path configured for New Account templates and checks each Calendar.mrk file for old events. It checks for recurrence ##
## removes the associated .MSG files, attachments, and the associated .ICS files. A backup is made of the Calendar.mrk file ##
## before any changes are made to it. It is reccomended that you backup your entire user folder structure before running ##
## this script. ##
## ##
## MDaemon Technologies offers no warranty, provides no support, and is not responsible for any damages that may arise from ##
## the use of this script. Use at your own risk. ##
## ##
#########################################################################################################################
#########################################################################################################################
Function Get-IniContent {
<#
.Synopsis
Gets the content of an INI file
.Description
Gets the content of an INI file and returns it as a hashtable
.Notes
Author : Oliver Lipkau <oliver@lipkau.net>
Source : https://github.com/lipkau/PsIni
http://gallery.technet.microsoft.com/scriptcenter/ea40c1ef-c856-434b-b8fb-ebd7a76e8d91
Version : 1.0.0 - 2010/03/12 - OL - Initial release
1.0.1 - 2014/12/11 - OL - Typo (Thx SLDR)
Typo (Thx Dave Stiff)
1.0.2 - 2015/06/06 - OL - Improvment to switch (Thx Tallandtree)
1.0.3 - 2015/06/18 - OL - Migrate to semantic versioning (GitHub issue#4)
1.0.4 - 2015/06/18 - OL - Remove check for .ini extension (GitHub Issue#6)
1.1.0 - 2015/07/14 - CB - Improve round-tripping and be a bit more liberal (GitHub Pull #7)
OL - Small Improvments and cleanup
1.1.1 - 2015/07/14 - CB - changed .outputs section to be OrderedDictionary
#Requires -Version 2.0
.Inputs
System.String
.Outputs
System.Collections.Specialized.OrderedDictionary
.Parameter FilePath
Specifies the path to the input file.
.Parameter CommentChar
Specify what characters should be describe a comment.
Lines starting with the characters provided will be rendered as comments.
Default: ";"
.Parameter IgnoreComments
Remove lines determined to be comments from the resulting dictionary.
.Example
$FileContent = Get-IniContent "C:\myinifile.ini"
-----------
Description
Saves the content of the c:\myinifile.ini in a hashtable called $FileContent
.Example
$inifilepath | $FileContent = Get-IniContent
-----------
Description
Gets the content of the ini file passed through the pipe into a hashtable called $FileContent
.Example
C:\PS>$FileContent = Get-IniContent "c:\settings.ini"
C:\PS>$FileContent["Section"]["Key"]
-----------
Description
Returns the key "Key" of the section "Section" from the C:\settings.ini file
.Link
Out-IniFile
#>
[CmdletBinding()]
Param(
[ValidateNotNullOrEmpty()]
[ValidateScript({(Test-Path $_)})]
[Parameter(ValueFromPipeline=$True,Mandatory=$True)]
[string]$FilePath,
[char[]]$CommentChar = @(";"),
[switch]$IgnoreComments
)
Begin
{
Write-Verbose "$($MyInvocation.MyCommand.Name):: Function started"
$commentRegex = "^([$($CommentChar -join '')].*)$"
}
Process
{
Write-Verbose "$($MyInvocation.MyCommand.Name):: Processing file: $Filepath"
$ini = New-Object System.Collections.Specialized.OrderedDictionary([System.StringComparer]::OrdinalIgnoreCase)
$commentCount = 0
switch -regex -file $FilePath
{
"^\s*\[(.+)\]\s*$" # Section
{
$section = $matches[1]
$ini[$section] = New-Object System.Collections.Specialized.OrderedDictionary([System.StringComparer]::OrdinalIgnoreCase)
$CommentCount = 0
continue
}
$commentRegex # Comment
{
if (!$IgnoreComments)
{
if (!(test-path "variable:section"))
{
$section = "_"
$ini[$section] = New-Object System.Collections.Specialized.OrderedDictionary([System.StringComparer]::OrdinalIgnoreCase)
}
$value = $matches[1]
$CommentCount++
$name = "Comment" + $CommentCount
$ini[$section][$name] = $value
}
continue
}
"(.+?)\s*=\s*(.*)" # Key
{
if (!(test-path "variable:section"))
{
$section = "_"
$ini[$section] = New-Object System.Collections.Specialized.OrderedDictionary([System.StringComparer]::OrdinalIgnoreCase)
}
$name,$value = $matches[1..2]
$ini[$section][$name] = $value
continue
}
}
Write-Verbose "$($MyInvocation.MyCommand.Name):: Finished Processing file: $FilePath"
Return $ini
}
End
{Write-Verbose "$($MyInvocation.MyCommand.Name):: Function ended"}
}
function ConverttoSystemDateTime($DTString){
#Split the EndDateTime variable to a Date and a Time, the Time is Trash here.
#$EndDate,$Trash = Split-String -Input $DTString -Separator " "
$EndDate,$Trash = $DTString -split " "
#Split the Date into year, month, and day values.
#$EndYear,$EndMonth,$EndDay = Split-String -Input $EndDate -Separator "-"
$EndYear,$EndMonth,$EndDay = $EndDate -split "-"
#Using the year, month, and day values create a new EndDate object of type System.DateTime
$EndDate = New-Object System.DateTime $EndYear,$EndMonth,$EndDay
return $EndDate
}
function RemoveTNEFFile($File, $Event){
if($Event.TNEF)
{
if($Event.TNEF.'#cdata-section')
{
#Write-Host "Found CDATA section setting the EndDateTime variable to the CDATA value."
$TNEFFile = Join-Path $File.DirectoryName $Event.TNEF.'#cdata-section'
}
else
{
#Write-Host "If there is no CDATA section then just get the value."
$TNEFFile = Join-Path $File.DirectoryName $Event.TNEF
}
if(Test-Path $TNEFFile)
{
Write-Host "Removing $TNEFFile"
Remove-Item $TNEFFile -force -ErrorAction SilentlyContinue
}
}
}
function RemoveICSFile($File, $Event){
if($Event.PersistentData.DAVDataFile)
{
$ICSFileDir = Join-Path $File.DirectoryName "_DAV"
if($Event.PersistentData.DAVDataFile.'#cdata-section')
{
#Write-Host "Found CDATA section setting the EndDateTime variable to the CDATA value."
$ICSFile = Join-Path $ICSFileDir $Event.PersistentData.DAVDataFile.'#cdata-section'
}
else
{
#Write-Host "If there is no CDATA section then just get the value."
$ICSFile = Join-Path $ICSFileDir $Event.PersistentData.DAVDataFile
}
if(Test-Path $ICSFile)
{
Write-Host "Removing $ICSFile"
Remove-Item $ICSFile -force -ErrorAction SilentlyContinue
}
}
}
function RemoveAttachmentFile($File, $Event){
if($Event.Attachment)
{
$AttachmentDirectory = Join-Path $File.DirectoryName "_attachments"
ForEach($Attachment in $Event.Attachment){
#Write-Host ""
$AttachmentFile = Join-Path $AttachmentDirectory $Attachment.LocalFile
if(Test-Path $AttachmentFile)
{
Write-Host "Removing $AttachmentFile"
Remove-Item $AttachmentFile -force -ErrorAction SilentlyContinue
}
}
}
}
#Get the path to the MDaemon\app folder.
if(Test-Path "HKLM:\SOFTWARE\Alt-N Technologies\MDaemon")
{
$MDAPPPath = (Get-ItemProperty "HKLM:\SOFTWARE\Alt-N Technologies\MDaemon" -Name AppPath).AppPath
$MDINIPath = (Get-ItemProperty "HKLM:\SOFTWARE\Alt-N Technologies\MDaemon" -Name IniPath).IniPath
}
elseif(Test-Path "HKLM:\SOFTWARE\Wow6432Node\Alt-N Technologies\MDaemon")
{
$MDAPPPath = (Get-ItemProperty "HKLM:\SOFTWARE\Wow6432Node\Alt-N Technologies\MDaemon" -Name AppPath).AppPath
$MDINIPath = (Get-ItemProperty "HKLM:\SOFTWARE\Wow6432Node\Alt-N Technologies\MDaemon" -Name IniPath).IniPath
}
else
{
Write-Host "Oh No, I couldn't find the registry keys for MDaemon!"
Write-Host "The script will now exit."
Exit
}
$AccountTemplatePath = Join-Path $MDAPPPath "AccountTemplates.dat"
$MDINIContent = Get-IniContent $MDINIPath
$AccounteTemplatesContent = Get-IniContent $AccountTemplatePath
#Path to the MDaemon\Users folder.
$PathtoUserFolders = $AccounteTemplatesContent["New Accounts"]["RootMailPath"]
$PathtoUserFolders = Split-Path $PathtoUserFolders
$PathtoUserFolders = Split-Path $PathtoUserFolders
$PathtoUserFolders = "c:\bad"
While(!(Test-Path $PathtoUserFolders))
{
Write-Host "The path to the user folders does not exist."
$PathtoUserFolders = Read-Host "Please enter the path to the user folders, for example, C:\MDaemon\Users. Any Calendar.MRK files found in the path entered will be processsed and content could be removed. If you need to exit enter Exit"
if($PathtoUserFolders -like "Exit"){
Exit
}
}
#Number of years to keep. This should be a negative value. If you wnat to remove events that ended more than 2 years
#ago set the value to -2."
$Years = -3
if($Years -ge 0)
{
Write-Host "The Years variable needs to be set to a negative value. $Years"
Exit
}
$ClearQuotaCounts = ""
#Searches the directory structure for Calendar.mrk files.
ForEach ($File in Get-ChildItem -Path $PathtoUserFolders -Include Calendar.mrk -Recurse -File)
{
$AddUsertoClearQuotaCountsSEM = "No"
#Locking the MRK file so it can't be changed by anything else."
$LockMRKFile = Join-Path $File.DirectoryName "Calendar.lck"
Write-Host "Creating lock file for Calendar.mrk"
$null = New-Item $LockMRKFile -type File -Force
#Making a backup copy of the MRK file, just in case."
$BackupMRK = Join-Path $File.DirectoryName "Calendar.mrk.bck"
Write-Host "Making a backup of the Calendar.mrk file."
Copy-Item $File $BackupMRK -Force
Write-Host "Loading $File"
$Data = [Xml] (Get-Content $File)
$MyDate = Get-Date
$EndDateTime = $null
$EndDate = $null
#Looks at each event in the calendar to determine if it should be removed.
foreach($Event in $Data.Calendar.event)
{
#$Event.ID
if($Event.End.'#cdata-section')
{
#Write-Host "Found CDATA section setting the EndDateTime variable to the CDATA value."
$EndDateTime = $Event.End.'#cdata-section'
}
else
{
#Write-Host "If there is no CDATA section then just get the value."
$EndDateTime = $Event.End
}
$EndDate = ConverttoSystemDateTime $EndDateTime
#if the end date on the event is more than X years ago.
if($EndDate -lt ($MyDate.AddYears($Years)))
{
#If the event is recurring
if($Event.Recurrence)
{
$RecurrEndDate = ConverttoSystemDateTime $Event.Recurrence.End
#If the recurrence ends more than X years ago
if($RecurrEndDate -lt ($MyDate.AddYears($Years)))
{
$AddUsertoClearQuotaCountsSEM = "Yes"
#Removing event and associated files because the last ocurrence of the event was more than 3 years ago.
Write-Host "Removing recurring event" $Event.ID
$Event.ParentNode.RemoveChild($Event)
if($Event.TNEF)
{
RemoveTNEFFile $File $Event
}
if($Event.PersistentData.DAVDataFile)
{
RemoveICSFile $File $Event
}
if($Event.Attachment){
RemoveAttachmentFile $File $Event
}
}
#If the recurrence do nothing.
else
{
#Not doing anything here because the recurring event is not older than the date set.
}
}
else
{
$AddUsertoClearQuotaCountsSEM = "Yes"
#Removing Event and associated files because the event was more than 3 years ago.
Write-Host "Removing event" $Event.ID
$Event.ParentNode.RemoveChild($Event)
if($Event.TNEF)
{
RemoveTNEFFile $File $Event
}
if($Event.PersistentData.DAVDataFile)
{
RemoveICSFile $File $Event
}
if($Event.Attachment){
RemoveAttachmentFile $File $Event
}
}
}
}
$Output = Join-Path $File.DirectoryName "MRKOutput.mrk"
Write-Host "Writing out new file at $File"
$Data.save($File)
Remove-Item $LockMRKFile -Force
if($AddUsertoClearQuotaCountsSEM -eq "Yes")
{
#Building the email address of the user.
$Mailbox = Split-Path -Path (Split-Path -Path $File.Directory) -Leaf
$Domain = Split-Path -Path (Split-Path -Path (Split-Path -Path $File.Directory)) -Leaf
$Email = $Mailbox + "@" + $Domain
Write-Host "$Email will be added to the reloadquotacounts.sem file."
$ClearQuotaCounts = $ClearQuotaCounts + $Email + "`r`n"
}
}
$QuataCountsSub = $MDINIContent["Special"]["QuotaCountsSubFolders"]
$CountGroupwareFolders = $MDINIContent["Special"]["CountGroupwareFolders"]
if($QuataCountsSub -eq "Yes" -and $CountGroupwareFolders -eq "Yes")
{
$SemPath = Join-Path $MDAPPPath "ClearQuotaCounts.sem"
Write-Host "Creating SEM files to update Quota Caches."
New-Item -Path $SemPath -type File -Value $ClearQuotaCounts -Force
}
else
{
Write-Host "Quotas are not configured to count Groupware folder or sub folders so the cache will not be updated."
}
Let us know if you have any questions or if there is anything we can do to improve the script.