Skip to content
www.mytechnote.ru
Menu
  • Administration
    • Scripts
  • Equipment
    • Reviews
    • Instructions
  • Other
  • English
    • English
    • Русский
Menu

How to remove attachments from old calendar events in Exchange using a script

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
Posted on 11.12.202511.12.2025 by sanglyb

Post navigation

← How to download and install Office 2024 LTSC
Problem Using PassFiltEx on Older CPUs →

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

Подписывайтесь

Youtube
vk
telegram
github
twitter
rss
  • Форум
  • Об авторе
  • Поддержать автора

Популярные записи

Новые записи

  • How to Extend an LVM Partition in Linux
  • SCCM Reporting Errors After Migrating from SQL Server 2016 to 2019
  • Problem Using PassFiltEx on Older CPUs
  • How to remove attachments from old calendar events in Exchange using a script
  • How to download and install Office 2024 LTSC

Архивы

  • 2026 (3)
  • 2025 (5)
  • 2024 (7)
  • 2023 (30)
  • 2022 (4)
  • 2020 (17)
  • 2017 (82)
  • 2016 (5)
© 2015-2026 www.mytechnote.ru
Использование материалов сайта возможно только с разрешения владельца или обязательна индексируемая, прямая, передающая вес ссылка на материал.