Another interesting task came up, which is a continuation of one of the previous ones. It was necessary to create a script that would remove attachments from old past meetings.
The script is based on the same approach as before — attachments will be removed using EWS. Accordingly, the account running the script must have impersonation rights.
For example, you can add permissions like this:
New-ManagementRoleAssignment -Name "AllowImpersonation" -Role ApplicationImpersonation -User sanglyb@yourorg.com
New-RoleGroup -name ImpersonationGroup -Roles ApplicationImpersonation
More details about permissions and roles can be found in the reference guide.
As usual, I uploaded the script to GitHub — if anything changes, that version will most likely be the latest.
When running the script, you must specify the required parameter -Mailbox — the mailbox where the script will search for appointments, in the format user@yourorg.ru.
You can also set report mode (-report $true), in which case only statistics will be displayed, without deleting attachments.
Additionally, you can set the search depth in days and your EWS address either in the script or through parameters.
Example:
./remove_attachements.ps -Mailbox user@yourorg.ru $false -DaysBack 90
It’s important to understand how the script works: it finds an event in the specified user’s calendar, checks who the organizer is, and if the organizer is not the user, impersonates the organizer via EWS, removes attachments (if the meeting is in the past, or if it’s a completed recurring meeting), and sends updates to all attendees.
This means the update may be sent to multiple users. Therefore, use the script with maximum caution and at your own risk!
Below is the script itself:
param(
[Parameter(Mandatory=$true)]
[string]$Mailbox,
[switch]$Report=$false,
[int]$DaysBack = 60,
# [string]$Output = "past_meetings.csv",
[string]$EwsUrl="https://exchangeserver/EWS/Exchange.asmx"
)
Write-Host "> Mailbox: $Mailbox" -ForegroundColor Cyan
Write-Host "> Report: $Report" -ForegroundColor Cyan
Write-Host "> EWS URL: $EwsUrl" -ForegroundColor Cyan
# bypass certificate errors
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {
param($sender, $certificate, $chain, $sslPolicyErrors)
return $true
}
function New-EwsService {
param(
[string]$ImpersonateMailbox,
[string]$Url
)
$svc = New-Object Microsoft.Exchange.WebServices.Data.ExchangeService(
[Microsoft.Exchange.WebServices.Data.ExchangeVersion]::Exchange2019
)
$svc.UseDefaultCredentials = $true
$svc.Url = $Url
if ($ImpersonateMailbox) {
$svc.ImpersonatedUserId = New-Object Microsoft.Exchange.WebServices.Data.ImpersonatedUserId(
[Microsoft.Exchange.WebServices.Data.ConnectingIdType]::SmtpAddress,
$ImpersonateMailbox
)
}
return $svc
}
function Get-Appointments {
param(
[Parameter(Mandatory = $true)]
[Microsoft.Exchange.WebServices.Data.ExchangeService]$Service,
[Parameter(Mandatory = $true)]
[DateTime]$Start,
[Parameter(Mandatory = $true)]
[DateTime]$End
)
# splitting the range into 30-day chunks
$step = 30
$calendarFolders = Get-CalendarFolders -Service $Service
$allAppointments = @()
foreach ($folder in $calendarFolders) {
$chunkStart = $Start
while ($chunkStart -lt $End) {
$chunkEnd = $chunkStart.AddDays($step)
if ($chunkEnd -gt $End) { $chunkEnd = $End }
# CalendarView for 30 days
$view = New-Object Microsoft.Exchange.WebServices.Data.CalendarView(
$chunkStart, $chunkEnd, 1000
)
$view.PropertySet = [Microsoft.Exchange.WebServices.Data.PropertySet]::FirstClassProperties
try {
$page = $Service.FindAppointments($folder.Id, $view)
if ($page.Items.Count -gt 0) {
$allAppointments += $page.Items
}
}
catch {
Write-Warning "Error reading folder $($folder.DisplayName): $_"
}
# next chunk
$chunkStart = $chunkEnd
}
}
return $allAppointments
}
function Get-OriginalAppointment {
param(
[Microsoft.Exchange.WebServices.Data.ExchangeService]$Service,
[string]$ICalUid,
[DateTime]$Start,
[DateTime]$End
)
$appointments=Get-Appointments -service $service -start $start -end $end
foreach ($appt in $appointments) {
try { $appt.Load() } catch {}
if ($appt.ICalUid -eq $ICalUid) {
return $appt
}
}
return $null
}
function get-calendarFolders {
param(
[Microsoft.Exchange.WebServices.Data.ExchangeService]$Service
)
$root = [Microsoft.Exchange.WebServices.Data.WellKnownFolderName]::MsgFolderRoot
$folderView = New-Object Microsoft.Exchange.WebServices.Data.FolderView(10000)
$folderView.Traversal = [Microsoft.Exchange.WebServices.Data.FolderTraversal]::Deep
# filter by folder type
$searchFilter = New-Object Microsoft.Exchange.WebServices.Data.SearchFilter+IsEqualTo(
[Microsoft.Exchange.WebServices.Data.FolderSchema]::FolderClass,
"IPF.Appointment"
)
return $service.FindFolders($root, $searchFilter, $folderView)
}
function Get-Master {
param(
[Microsoft.Exchange.WebServices.Data.Appointment]$OrgItem,
[Microsoft.Exchange.WebServices.Data.ExchangeService]$OrgService
)
if ($OrgItem.AppointmentType -eq "Occurrence") {
return [Microsoft.Exchange.WebServices.Data.Appointment]::BindToRecurringMaster(
$OrgService,
$OrgItem.Id
)
}
return $OrgItem
}
function Save-OrgItemAndSendUpdate {
param(
[Microsoft.Exchange.WebServices.Data.Appointment]$OrgItem,
[Microsoft.Exchange.WebServices.Data.ExchangeService]$OrgService
)
Write-Host "`nRemoving attachments from event $($OrgItem.Subject)..." -ForegroundColor green
try {
$OrgItem.Attachments.Clear()
$OrgItem.Update(
[Microsoft.Exchange.WebServices.Data.ConflictResolutionMode]::AlwaysOverwrite,
[Microsoft.Exchange.WebServices.Data.SendInvitationsOrCancellationsMode]::SendToNone
)
$OrgItem.Load()
# modify meeting body to ensure proper update notifications
#$OrgItem.Body = New-Object Microsoft.Exchange.WebServices.Data.MessageBody (
# $OrgItem.Body.Text + "<p>Attachement removed by script</p>"
#)
$body = $OrgItem.Body.Text
$body = if ($body.EndsWith(" ")) {
$body.TrimEnd()
} else {
$body + " "
}
$OrgItem.Body = New-Object Microsoft.Exchange.WebServices.Data.MessageBody $body
$OrgItem.Update(
[Microsoft.Exchange.WebServices.Data.ConflictResolutionMode]::AlwaysOverwrite,
[Microsoft.Exchange.WebServices.Data.SendInvitationsOrCancellationsMode]::SendToAllAndSaveCopy
)
Write-Host "Event update completed" -ForegroundColor Green
} catch {
Write-Warning "Error updating event: $($_.Exception.Message)"
if ($_.Exception.InnerException) {
Write-Warning "Inner: $($_.Exception.InnerException.Message)"
}
}
}
$service = New-EwsService -ImpersonateMailbox $Mailbox -Url $EwsUrl
$startDate = (Get-Date).AddDays(-$DaysBack)
$endDate = (Get-Date).AddDays(1)
$appointments=Get-Appointments -service $service -start $startDate -end $endDate
$groups = $appointments | where-object {$_.HasAttachments -eq $true} | Group-Object ICalUid,AppointmentType
Write-Host "`nPreparing data..." -ForegroundColor Green
$allItems=@()
$results = foreach ($grp in $groups) {
$item = $grp.Group | Select-Object -First 1
$item.Load()
if ($item.End -ge (Get-Date)) { continue }
if (-not $item.HasAttachments) { continue }
if ($item.AppointmentType -eq "RecurringMaster" -and $item.LastOccurrence.End -ge (get-date)) { continue }
if ($item.AppointmentType -eq "Occurrence") {
if ($item.Attachments.Count -ge 1){
$itemTMP = Get-Master $item $service
if ($itemTMP.AppointmentType -eq "RecurringMaster" -and $itemTMP.LastOccurrence.End -ge (get-date)) {
$test="skipped"
continue
} else {
$test="not skipped"
$item=$itemTMP
}
}
}
$allItems+=$item
[pscustomobject]@{
Mailbox = $Mailbox
Subject = $item.Subject
Start = $item.Start
End = $item.End
Organizer = $item.Organizer.Address
HasAttachments = $item.HasAttachments
AttachmentsCount = $item.Attachments.Count
Attachments = $item.Attachments
ItemType = $item.AppointmentType
ICalUid = $item.ICalUid
IsCancelled = $item.IsCancelled
LastOccurrence = $item.LastOccurrence.End
skipped = $skipped
}
}
$allItems = $allItems | sort-object {$_.Organizer.Address}
if ($allItems.count -eq 0){
Write-host "No calendar events with attachments found." -ForegroundColor green
exit
} else {
write-host "Old calendar events with attachments found" -ForegroundColor green
$results
}
# if Report is specified — only report, no cleanup
if ($Report) {
Write-Host "`nReport mode. Attachments will NOT be removed." -ForegroundColor Cyan
exit
}
# Clean
$item=$null
foreach ($item in $allItems){
$organizer = $item.Organizer.Address
# --- if the organizer is another person ---
if ($organizer -and $organizer -ne $Mailbox) {
try {
if ($prevorganizer -ne $organizer) {
Write-Host "Impersonating $organizer" -ForegroundColor Yellow
$orgService = New-EwsService -ImpersonateMailbox $organizer -Url $EwsUrl
$prevorganizer=$Organizer
}
$origItem = Get-OriginalAppointment -Service $orgService -ICalUid $item.ICalUid -start $item.Start.AddHours(-2) -end $item.End.AddHours(2)
if (-not $origItem) {
Write-Warning "`n`n`nOriginal meeting not found in organizer's mailbox. Removing attachment from user $mailbox ..."
Save-OrgItemAndSendUpdate $item $service
continue
}
$origItem.Load()
} catch {
Write-Warning "Impersonation error: $($_.Exception.Message)"
continue
}
if ($origItem.AppointmentType -eq "Occurrence") {
$itemTMP = $null
$itemTMP = Get-Master $origItem $orgService
if ($itemTMP.AppointmentType -eq "RecurringMaster" -and $itemTMP.LastOccurrence.end -ge (get-date)) {
Write-Warning "Last occurrence date has not yet passed..."
continue
} else {
$origItem=$itemTMP
}
}
Save-OrgItemAndSendUpdate $origItem $orgService
}
else {
Save-OrgItemAndSendUpdate $item $service
}
}
Write-Host "`nCompleted" -ForegroundColor Green
