Using Microsoft Graph to report on User calendar information
Calendar item reporting and purges are typically the result of leavers having important invites that need to be shared, or hosts recurring meetings that must be removed.
Here is an advanced Graph API function that I use to search mailboxes and return a spreadsheet report. You must have the Microsoft.Graph.Authentication and the Microsoft.Graph.Calendar modules installed
The Script can be found at this Gist and is embedded below. Let me know if this doesn’t work for you, we can troubleshoot!
| #Requires -Version 5.0 | |
| Function Get-MGUserCalendarEvents { | |
| <# | |
| .SYNOPSIS | |
| This script will connect to the Microsoft Graph API and retrieve all calendar events for a list of users. | |
| .DESCRIPTION | |
| This script will connect to the Microsoft Graph API and retrieve all calendar events for a list of users. | |
| .PARAMETER ClientID | |
| The Client ID of the Azure AD Application | |
| .PARAMETER CertificateThumbprint | |
| The Certificate Thumbprint of the Azure AD Application | |
| .PARAMETER TenantId | |
| The Tenant ID of the Azure AD Application | |
| .PARAMETER ImportFile | |
| The file containing the list of users to retrieve calendar events for | |
| The file MUST BE A TEXT FILE WITH ONE USER ID PER LINE. User ID format must be either: | |
| - UserPrincipalName | |
| - Graph Object ID | |
| .PARAMETER ExportFile | |
| The file to export the calendar events to | |
| The file will be a CSV file with the following columns: | |
| - User | |
| - UserPrincipalName | |
| - EventSubject | |
| - EventStart | |
| - EventEnd | |
| - EventOrganizer | |
| - IsOnlineMeeting | |
| - OnlineMeetingProvider | |
| - Attendees | |
| - IsAllDay | |
| - IsCancelled | |
| - IsOrganizer | |
| - Locations | |
| - MeetingType | |
| - Recurring | |
| .PARAMETER StartDate | |
| The start date to retrieve calendar events from | |
| The date format must be yyyy-MM-dd | |
| .PARAMETER EndDate | |
| The end date to retrieve calendar events to | |
| The date format must be yyyy-MM-dd | |
| .EXAMPLE | |
| Get-MGUserCalendarEvents -ClientID '****************' -CertificateThumbprint ************ -TenantId 'tenant.onmicrosoft.com' -ImportFile ".\Users.txt" -ExportFile ".\Users_Events.csv" -StartDate "2023-07-01" -EndDate "2023-08-01" | |
| This example will connect to the Microsoft Graph API using the specified Client ID, Client Secret and Tenant ID. | |
| It will then retrieve all calendar events for the users specified in the Users.txt file and export the results to the Users_Events.csv file. | |
| .EXAMPLE | |
| Get-MGUserCalendarEvents -Input ".\CustomInputFile.csv" -Output ".\CustomOutputFile.csv" -StartDate "2023-07-01" -EndDate "2023-08-01" | |
| This example will connect to the Microsoft Graph API using the default Client ID, Certificate Thumbprint and Tenant ID | |
| .NOTES | |
| Graph API Documentation: https://docs.microsoft.com/en-us/graph/api/resources/calendar?view=graph-rest-1.0 | |
| Client ID of the Azure AD Application must have "Calendars.ReadBasic","Calendars.Read.Shared","Calendars.ReadWrite","Calendars.Read" permissions | |
| #> | |
| [CmdletBinding()] | |
| param ( | |
| [Parameter(Mandatory = $true, HelpMessage = 'Client ID of the Azure AD Application must have "Calendars.ReadBasic","Calendars.Read.Shared","Calendars.ReadWrite","Calendars.Read" permissions')] | |
| $AppId, | |
| [Parameter(Mandatory = $true, HelpMessage = 'Certificate Thumbprint of the Azure AD Application')] | |
| $CertificateThumbprint, | |
| [Parameter(Mandatory = $true, HelpMessage = 'Tenant ID of the Azure AD Application')] | |
| $TenantId, | |
| [Parameter(Mandatory = $true, HelpMessage = 'Input file needs to be text file with one userprincipalname per line')] | |
| [ValidateScript({ Test-Path -Path $_ -PathType Leaf })] | |
| $ImportFile, | |
| [Parameter(Mandatory = $true, HelpMessage = 'Output file will be a CSV file with the following columns: User, Email, UserPrincipalName, EventSubject, EventStart, EventEnd, EventOrganizer, IsOnlineMeeting, OnlineMeetingProvider, Attendees, IsAllDay, IsCancelled, IsOrganizer, Locations, MeetingType, Recurring')] | |
| $ExportFile, | |
| [Parameter(Mandatory = $true, HelpMessage = 'Start Date format: yyyy-MM-dd')] | |
| [ValidatePattern('^\d{4}-\d{2}-\d{2}$')] | |
| $StartDate, | |
| [Parameter(Mandatory = $true, HelpMessage = 'End Date format: yyyy-MM-dd')] | |
| [ValidatePattern('^\d{4}-\d{2}-\d{2}$')] | |
| $EndDate | |
| ) | |
| Begin { | |
| $ErrorActionPreference = 'Stop' | |
| #Connect to Microsoft Graph | |
| Connect-MgGraph -AppId $AppId -CertificateThumbprint $CertificateThumbprint -TenantId $TenantId -NoWelcome | |
| #Create an array to store the data | |
| $Data = [System.Collections.ArrayList]@() | |
| #Import list of users from import file | |
| $Users = Get-Content $ImportFile | |
| #Create variables for progress bar and timer | |
| [INT]$TotalIDs = $Users.Count | |
| If ($TotalIDs -eq "0") { [INT]$TotalIDs = 1 } | |
| [INT]$x = 0 | |
| $timerx = Get-Date | |
| [nullable[double]]$secondsRemaining = $null | |
| } | |
| Process { | |
| ForEach ($User in $Users) { | |
| Try { | |
| #Get the user details | |
| $MGUser = Get-MGUser -UserID $User | |
| # Increment the progress bar variables and display | |
| $x++ | |
| $secondsElapsedx = (Get-Date) - $timerx | |
| $secondsRemaining = ($secondsElapsedx.TotalSeconds / $x) * ($TotalIDs - $x) | |
| Write-Progress -ID 1 -Activity "$($secondsElapsedx.ToString('hh\:mm\:ss')) | Processing User $($MGUser.GivenName + " " + $MGUser.Surname) " -Status " $x / $TotalIDs " -PercentComplete ($x / $TotalIDs * 100) -SecondsRemaining $secondsRemaining | |
| # Get all events for the user within the date range | |
| $Events = Get-MgUserCalendarView -UserId $MGUser.Id -StartDateTime $StartDate -EndDateTime $EndDate -All | Sort-Object -Property { $_.Start.DateTime } | |
| # Create variables for new progress bar and timer | |
| [INT]$Total = $Events.Count | |
| [INT]$i = 0 | |
| $timeri = Get-Date | |
| ForEach ($Event in $Events) { | |
| Try { | |
| # Increment the progress bar variables and display | |
| $i++ | |
| $secondsElapsedi = (Get-Date) - $timeri | |
| $secondsRemaining = ($secondsElapsedi.TotalSeconds / $i) * ($Total - $i) | |
| Write-Progress -ID 2 -Activity "$($secondsElapsedi.ToString('hh\:mm\:ss')) | Recording Mailbox Event `"$($($Event.Start.DateTime) + " - " + $($Event.Subject))`" " -PercentComplete ($i / $Total * 100) -SecondsRemaining $secondsRemaining | |
| # Create strings for custom object properties | |
| $EventStart = $Event.Start.DateTime | |
| $EventEnd = $Event.End.DateTime | |
| $EventOrganizer = $Event.Organizer.EmailAddress.Name | |
| $EventMaster = if ($null -ne $Event.SeriesMasterId){ | |
| Get-MGUserEvent -UserId $MGUser.Id -EventId $Event.SeriesMasterId | |
| } else { $Event } | |
| # Recurring events are represented by different collections of properties. The following switch statement keys off of the recurrence type, and builds readable strings for the custom object. | |
| $RecurString = Switch ($EventMaster.Recurrence.Pattern.Type) { | |
| "absoluteMonthly" { | |
| "Occurs on the " + $EventMaster.Recurrence.Pattern.DayOfMonth + " day of every " + $EventMaster.Recurrence.Pattern.Interval + " month(s)" | |
| } | |
| "relativeMonthly" { | |
| "Occurs on the " + $EventMaster.Recurrence.Pattern.Index + " " + $EventMaster.Recurrence.Pattern.DaysOfWeek + " of every " + $EventMaster.Recurrence.Pattern.Interval + " month(s)" | |
| } | |
| "absoluteYearly" { | |
| "Occurs on the " + $EventMaster.Recurrence.Pattern.Month + "/" + $EventMaster.Recurrence.Pattern.DayOfMonth + " every " + $EventMaster.Recurrence.Pattern.Interval + " year(s)" | |
| } | |
| "relativeYearly" { | |
| "Occurs on the " + $EventMaster.Recurrence.Pattern.Index + " " + $EventMaster.Recurrence.Pattern.DaysOfWeek + " of " + $EventMaster.Recurrence.Pattern.Month + " every " + $EventMaster.Recurrence.Pattern.Interval + " year(s)" | |
| } | |
| "Weekly" { | |
| "Occurs on " + $EventMaster.Recurrence.Pattern.DaysOfWeek + " every " + $EventMaster.Recurrence.Pattern.Interval + " week(s)" | |
| } | |
| "Daily" { | |
| "Occurs every " + $EventMaster.Recurrence.Pattern.Interval + " day(s)" | |
| } | |
| Default { | |
| "Does not recur" | |
| } | |
| } | |
| If ($RecurString -ne "Does not recur") { | |
| $String += " from " + $Event.Recurrence.Range.StartDate + " to " + $Event.Recurrence.Range.EndDate | |
| } | |
| # Add event and user properties new custom object to the data array | |
| $Data += [PSCustomObject]@{ | |
| 'User' = ($MGUser.GivenName + " " + $MGUser.Surname) | |
| 'Email' = $MGUser.Mail | |
| 'UserPrincipalName' = $MGUser.UserPrincipalName | |
| 'EventSubject' = $Event.Subject | |
| 'EventStart' = $EventStart | |
| 'EventEnd' = $EventEnd | |
| 'EventOrganizer' = $EventOrganizer | |
| 'IsOnlineMeeting' = $Event.IsOnlineMeeting | |
| 'OnlineMeetingProvider' = $Event.OnlineMeetingProvider | |
| 'Attendees' = [string]$Event.Attendees.EmailAddress.Name | |
| 'IsAllDay' = $Event.IsAllDay | |
| 'IsCancelled' = $Event.IsCancelled | |
| 'IsOrganizer' = $Event.IsOrganizer | |
| 'Locations' = $($Event.Locations.DisplayName) | |
| 'Recurring' = $RecurString | |
| } | |
| } | |
| Catch { | |
| # If an error occurs, write the error to the console and continue to the next event | |
| Write-Host "Event Error: $EventStart $($_.Exception.Message)" -ForegroundColor Red | |
| Continue | |
| } | |
| } | |
| Write-Progress -ID 2 -Activity "Completed" -Completed | |
| } | |
| Catch { | |
| # If an error occurs, write the error to the console and continue to the next user | |
| Write-Host "User Error: $User - $($_.Exception.Message)" -ForegroundColor Red | |
| Continue | |
| } | |
| } | |
| Write-Progress -ID 1 -Activity "Completed" -Completed | |
| } | |
| End { | |
| # Export the data array to a CSV file | |
| $Data | Export-CSV -Notypeinformation -path $ExportFile | |
| Return $Data | |
| } | |
| } |

