<# .SYNOPSIS Script to update recurring meeting with no end date to have an end date using Graph via Powershell. .DESCRIPTION Script to update meeting items using Graph via Powershell. It can run on a single mailbox, or multiple mailboxes. If it runs on a single mailbox, the module can pop-up and request the authenticated user to consent Graph permissions. The script will run against the authenticated mailbox. If it runs against multiple mailboxes, an AzureAD Registered App is needed, with the appropriate Application permissions (requires 'Calendars.ReadWrite' API permission granted). .PARAMETER ClientID This is an optional parameter. String parameter with the ClientID (or AppId) of your AzureAD Registered App. .PARAMETER TenantID This is an optional parameter. String parameter with the TenantID your AzureAD tenant. .PARAMETER CertificateThumbprint This is an optional parameter. Certificate thumbprint which is uploaded to the AzureAD App. .PARAMETER Subject This is a mandatory parameter. The exact subject text to filter meeting items. This parameter cannot be used together with the "FromAddress" parameter. .PARAMETER Mailboxes This is an optional parameter. This is a list of SMTP Addresses. If this parameter is ommitted, the script will run against the authenticated user mailbox. .PARAMETER EventEndDate This is a required parameter. This is the end date we will update on the Recurrent meeting items. By Default will be 1 year forward from the current date. .PARAMETER StartDate This is an optional parameter. The script will search for meeting items starting based on this StartDate onwards. If this parameter is ommitted, by default will consider the current date. .PARAMETER EndDate This is a required parameter. The script will search for meeting items ending based on this EndDate backwards. If this parameter is ommitted, by default will consider 2 year forward from the current date. .PARAMETER DisableTranscript This is an optional parameter. Transcript is enabled by default. Use this parameter to not write the powershell Transcript. .PARAMETER ListOnly This is an optional parameter. Use this parameter to list the calendar events found, without deleting them. This is a good parameter to use, to actually see the current found events and double check these are the ones to be deleted. .PARAMETER DisconnectMgGraph This is an optional parameter. Use this parameter to disconnect from MgGraph when it finishes. .EXAMPLE PS C:\> .\Update-GraphUserCalendarEventsEndDate.ps1 -Subject "Yearly Team Meeting" -StartDate 06/20/2022 -Verbose The script will install required modules if not already installed. Later it will request the user credential, and ask for permissions consent if not granted already. Then it will search for all meeting items matching exact subject "Yearly Team Meeting" starting on 06/20/2022 forward. It will set the end date on the recurrent meetings on 1 year forward from the current date. .EXAMPLE PS C:\> $mailboxes = Get-EXOMailbox -Filter {Office -eq "Staff"} -Properties PrimarySMTPAddress | Select-Object PrimarySMTPAddress PS C:\> .\Update-GraphUserCalendarEventsEndDate.ps1 -Subject "Yearly Team Meeting" -Mailboxes $mailboxes.PrimarySMTPAddress -ClientID "12345678" -TenantId "abcdefg" -CertificateThumbprint "a1b2c3d4" -Verbose The script will install required modules if not already installed. Later it will connect to MgGraph using AzureAD App details (requires ClientID, TenantID and CertificateThumbprint). Then it will search for all meeting items matching exact subject "Yearly Team Meeting" starting on the current date forward, for all mailboxes belonging to the "Staff" Office. It will set the end date on the recurrent meetings on 1 year forward from the current date. .NOTES Author: Agustin Gallegos Contributor: Corey Schneider #> [CmdletBinding(SupportsShouldProcess = $True, ConfirmImpact = 'Low')] param ( [String] $ClientID, [String] $TenantID, [String] $CertificateThumbprint, [parameter(Mandatory = $true)] [String] $Subject, [String[]] $Mailboxes, [DateTime] $EventEndDate = (Get-Date).AddYears(1), [DateTime] $StartDate = (Get-date), [DateTime] $EndDate = (Get-Date).AddYears(2), [Switch] $DisableTranscript, [Switch] $DisconnectMgGraph ) begin { if ( -not($DisableTranscript) ) { Start-Transcript } # Downloading required Graph modules @( 'Microsoft.Graph.Users' 'Microsoft.Graph.Calendar' 'Microsoft.Graph.Authentication' ) | ForEach-Object { if ( -not(Get-Module $_ -ListAvailable)) { Write-Verbose "'$_' Module not found. Installing it..." Install-Module $_ -Scope CurrentUser -Force } } Import-Module Microsoft.Graph.Users, Microsoft.Graph.Calendar -Verbose:$false # Connect to Graph if there is no current context $conn = Get-MgContext if ( $null -eq $conn -or $conn.Scopes -notcontains "Calendars.ReadWrite" ) { Write-Host "[$((Get-Date).ToString("HH:mm:ss"))] There is currently no active connection to MgGraph or current connection is missing required 'Calendars.ReadWrite' Scope." if ( -not($PSBoundParameters.ContainsKey('Mailboxes')) ) { # Connecting to graph with the user account Write-Host "[$((Get-Date).ToString("HH:mm:ss"))] Connecting to graph with the user account" Connect-MgGraph -Scopes "Calendars.ReadWrite" } else { # Connecting to graph using Azure App if ( $clientID -eq '' -or $TenantID -eq '' -or $CertificateThumbprint -eq '' ) { Write-Host "[$((Get-Date).ToString("HH:mm:ss"))] ERROR: Required 'ClientID', 'TenantID' and 'CertificateThumbprint' parameters are missing to connect using App Authentication." -ForegroundColor Red Exit } Write-Host "[$((Get-Date).ToString("HH:mm:ss"))] Connecting to graph with Azure AppId: $ClientID" Connect-MgGraph -ClientId $ClientID -TenantId $TenantID -CertificateThumbprint $CertificateThumbprint } } else { if ( $null -eq $conn.Account ) { Write-Host "[$((Get-Date).ToString("HH:mm:ss"))] Currently connect with App Account: $($conn.AppName)" } else { Write-Host "[$((Get-Date).ToString("HH:mm:ss"))] Currently connected with User Account: $($conn.Account)" } } $mbxs = (Get-MgContext).Account if ( $PSBoundParameters.ContainsKey('Mailboxes') ) { $mbxs = $Mailboxes } } process { $i = 0 foreach ( $mb in $mbxs ) { $i++ Write-Progress -activity "Scanning Users: $i out of $($mbxs.Count)" -status "Percent scanned: " -PercentComplete ($i * 100 / $($mbxs.Count)) -ErrorAction SilentlyContinue Write-Host "[$((Get-Date).ToString("HH:mm:ss"))] Working on mailbox $mb" Write-Host "[$((Get-Date).ToString("HH:mm:ss"))] Collecting events based on exact subject: '$Subject' starting $startDate." $events = Get-MgUserCalendarView -UserId $mb -Filter "Subject eq '$subject'" -StartDateTime $StartDate -EndDateTime $EndDate -All if ( $events.Count -eq 0 ) { Write-Host "[$((Get-Date).ToString("HH:mm:ss"))] No events found based on parameters criteria. Please double check and try again." Continue } # Exporting found events to Verbose console if ( $PSBoundParameters.ContainsKey('Verbose') ) { #$events | Select-Object subject,@{N="Mailbox";E={$mb}},@{N="organizer";E={$_.Organizer.EmailAddress.Address}},@{N="Attendees";E={$_.Attendees | ForEach-Object {$_.EmailAddress.Address -join ";"}}},@{N="StartTime";E={$_.Start.DateTime}},@{N="EndTime";E={$_.End.DateTime}},id $mainEvent = Get-MgUserEvent -UserId $mb -EventId $events[0].SeriesMasterId Write-Verbose "Displaying Master event details:" $mainEvent | Select-Object subject, @{N = "Mailbox"; E = { $mb } }, @{N = "organizer"; E = { $_.Organizer.EmailAddress.Address } }, @{N = "Attendees"; E = { $_.Attendees | ForEach-Object { $_.EmailAddress.Address -join ";" } } }, @{N = "StartDate"; E = { $_.Recurrence.Range.StartDate } }, @{N = "EndDate"; E = { $_.Recurrence.Range.EndDate } }, id } #get master event id $masterId = $events[0].SeriesMasterId #get main event $mainEvent = Get-MgUserEvent -UserId $mb -EventId $masterId #modify the main event end date to specified date $mainevent.Recurrence.Range.EndDate = $EventEndDate #modify the main event type from 'noEnd' to 'endDate $mainevent.Recurrence.Range.Type = 'endDate' #update the main event on calendar to clear 'remaining ocurrences' Update-MgUserEvent -EventId $mainEvent.Id -UserId $mb -BodyParameter $mainEvent } } end { if ( -not($DisableTranscript) ) { Stop-Transcript } if ( $DisconnectMgGraph ) { Disconnect-MgGraph } }