Recently, as part of a tenant-to-tenant migration, I ran into a problem where a customer had included all of their resource calendars (conference rooms, reservable equipment) in calendar migrations.
Unfortunately, once you’re no longer in the source tenant, you don’t have a way to cancel the meetings.
Until now.
[cmdletbinding()]
param(
[Parameter(Mandatory=$True)][string[]]$UserId,
[Parameter(Mandatory=$True)][string]$ItemsBeforeDate,
[string]$Logfile = (Get-Date -Format yyyy-MM-dd) + "_CalendarEntryProcessing.txt"
)
## Prerequisites
# Graph Calendar module
If (Get-Module -ListAvailable Microsoft.Graph.Calendar) { Import-Module Microsoft.Graph.Calendar }
Else { Throw "Install the Microsoft.Graph.Calendar module before continuing."; Break }
# Graph scopes
[string[]]$CurrentContextScopes = (Get-MgContext).Scopes
[string[]]$RequiredScopes = @('User.Read.All','Calendars.ReadWrite.Shared','Calendars.ReadWrite')
$Intersection =[object[]][Linq.Enumerable]::Intersect($RequiredScopes,$CurrentContextScopes)
If ($Intersection.Count -ne 3) { "Disconnect-MgGraph, then run Connect-MgGraph -Scopes User.Read.All,Calendard.ReadWrite.Shared,Calendars.ReadWrite." }
function Write-Log([string[]]$Message, [string]$LogFile = $Script:LogFile, [switch]$ConsoleOutput, [ValidateSet("SUCCESS", "INFO", "WARN", "ERROR", "MANUAL")][string]$LogLevel)
{
$Message = $Message + $Input
If (!$LogLevel) { $LogLevel = "INFO" }
switch ($LogLevel)
{
SUCCESS { $Color = "Green" }
INFO { $Color = "White" }
WARN { $Color = "Yellow" }
ERROR { $Color = "Red" }
MANUAL { $Color = "Gray" }
}
if ($Message -ne $null -and $Message.Length -gt 0)
{
$TimeStamp = [System.DateTime]::Now.ToString("yyyy-MM-dd HH:mm:ss")
if ($LogFile -ne $null -and $LogFile -ne [System.String]::Empty)
{
Out-File -Append -FilePath $LogFile -InputObject "[$TimeStamp] [$LogLevel] $Message"
}
if ($ConsoleOutput -eq $true)
{
Write-Host "[$TimeStamp] [$LogLevel] :: $Message" -ForegroundColor $Color
}
}
}
$WatershedDate = [DateTime]::Parse($ItemsBeforeDate)
$UserIdTotal = $UserId.Count
$UserIdCounter = 1
Write-Host "Watershed date set to $($WatershedDate)."
Foreach ($User in $UserId)
{
Write-Log -LogFile INFO -ConsoleOutput -Message "Processing $($User)"
# Get events in mailbox
$UserAlias = $User.Split("@")[0]
try { $Events = Get-MgUserEvent -UserId $User -All -ErrorAction SilentlyContinue }
catch { Write-Log -Message "No calendar events found in mailbox $($User)" -LogFile $Logfile -LogLevel INFO }
$EventTotal = $Events.Count
$EventCounter = 1
If ($EventTotal -ge 1)
{
Foreach ($Entry in $Events)
{
Write-Log -ConsoleOutput -LogLevel INFO -Message "Processing calendar entry [$($EventCounter)/$($EventTotal)]"
$EntryOrganizer = $Entry.Organizer.Emailaddress.Address.Split("CN=")[2]
[datetime]$EntryEndDate = $Entry.End.DateTime
If (($Entry.CreatedDateTime -lt $WatershedDate) -and ($EntryEndDate -ge $WatershedDate))
{
Write-Log -ConsoleOutput -LogLevel INFO -Message "MAILBOX: $($User) || ENTRYENDDATE: $($EntryEndDate)"
If (($EntryOrganizer -ne $UserAlias) -or ($IsOrganizer -eq $False))
{
Write-Log -ConsoleOutput -LogLevel INFO $Logfile -Message "[$($EventCounter)/$($EventTotal)] Calendar Entry SUBJECT: $($Entry.Subject) in MAILBOX $($User) with ID $($Entry.Id) should be deleted because it is older than the watershed date of $($WatershedDate)."
$Remove = Remove-MgUserEvent -UserId $User -EventId $($Entry.Id) -confirm:$false -erroraction SilentlyContinue -PassThru
If ($Test = Get-MgUserEvent -UserId $User -EventId $Entry.Id -ErrorAction SilentlyContinue) { Write-Log -ConsoleOutput -LogLevel ERROR -LogFile $Logfile -Message "Calendar Entry SUBJECT: $($Entry.Subject) in MAILBOX $($User) with ID $($Entry.Id) must be deleted manually." }
}
Else
{
Write-Log -Logfile $Logfile -LogLevel MANUAL -ConsoleOutput -Message "Calendar Entry SUBJECT: $($Entry.Subject) in MAILBOX $($User) with ID $($Entry.Id) must be deleted manually because $($User) is the ORGANIZER."
}
}
Else
{
Write-Log -ConsoleOutput -Message "Calendar Entry $($Entry.Subject) with ID $($Entry.Id) in mailbox $($User) was not deleted because it was newer than the watershed date of $($WatershedDate) or was filtered out because its occurrence was older than the watershed date of $($WatershedDate)." -LogLevel INFO -LogFile $Logfile
}
$EventCounter++
}
}
$UserIdCounter++
}
There are a couple of key parameters here:
UserIdparameter is typed as[string[]], so you can provide one or more comma-separated SMTP addresses, an array object, pipe in a CSV, add it to a foreach loop, or however you can get the data in.ItemsBeforeDateis the watershed date (in my case, it needed to be the migration cutover date), and it has the following logic:- All calendar entries before this date will be ignored
- Calendar entries created before this date with an instance ending after this date will be deleted
- If the entry is marked
IsOrganizer -eq $Trueit will be skipped. Entries markedIsOrganizer -eq $Truemeans that the resource mailbox is the organizer, and currently, the Graph cmdlets don’t have an option to suppress sending a cancellation. Due to the way the cmdlet is written, there’s no override or ignore the error, so it’s a terminating failure. As long as the conference rooms is added as a resource, though, this should be an edge case.
Delete those calendar items with impunity.


Thanks AAron.
How to Delete the Departed user Scheduled reoccurring meeting from users calendar where user is deleted from entra id