With the growing number of people migrating from the Azure AD module to Microsoft Graph API, it’s great to see some features finally become available via the command line interface .aka. PowerShell. Today we’ll cover step by step on how to check if an email was read using Graph API PowerShell SDK.
Table Of Contents
Requirements
The use case may vary, sometimes you might want to check how many people read a recent communication that was sent by the organization. Other times you may need statistics for general read status. Whatever the case may be, it can be helpful to get this kind of data.
In order to set this up and check to see if a specific email was read or not, there are certain requirements that need to be put in place in order to successfully query a mailbox. Let’s cover those now.
- Microsoft.Graph PowerShell Module
- EXACT subject line of the email you’re searching for
- An Azure App Registration setup with following API permissions
- Directory.Read.All
- Mail.ReadBasic.All
How To Check If An Email Was Read using Graph API PowerShell SDK
Now that we have the Azure App Registration and Service Principal created with the above permissions, let’s look at how we can start getting message read status for our mail items.
Get-MSGraphMailReadStatus PowerShell Script
To give you some insight, this script uses Get-MgUserMessage
graph cmdlet under the hood to get the actual message. There is also a parameter to see which folder the mail item is currently located. This helps if a user says they’ve lost an email, when in reality they’ve accidently dragged it into another folder without realizing it.
Function Get-MgMailReadStatus { <# .SYNOPSIS Get Email read status for users using Graph Api. A session using Connect-Graph must be open as a requirement. .NOTES Name: Get-MgMailReadStatus Author: Paul Contreras Version: 1.3 .EXAMPLE Get-MgMailReadStatus -UserId [email protected], [email protected] -Subject 'Exact Subject Line' .EXAMPLE Get-MgMailReadStatus -UserId [email protected] -Subject 'Exact Subject Line' -SenderAddress [email protected] -StartDate 5/25/2020 -EndDate 10/28/2022 -ShowMailFolder .PARAMETER UserId Checks against this persons mailbox. .PARAMETER Subject Queries the mailbox using the exact text specified .PARAMETER SenderAddress Specify the 'From' Address in your search. Format should be [email protected] .PARAMETER StartDate Specify the date when the query should start filtering for. Format should be MM/DD/YYYY .PARAMETER EndDate Specify the date when the query should end the filter. Format should be MM/DD/YYYY .PARAMETER ShowMailFolder When this switch is used, it will display what folder the email is currently located in. This makes the overall query slower so use only when needed. .PARAMETER Top Defaulted to 1. This is the limit of emails per mailbox that you would like to find #> [CmdletBinding()] param( [Parameter( Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 0 )] [Alias('UserPrincipalName')] [string[]] $UserId, [Parameter( Mandatory = $false, ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false )] [string] $Subject, [Parameter( Mandatory = $false, ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false )] [string] $SenderAddress, [Parameter( Mandatory = $false, ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false )] [string] $StartDate, [Parameter( Mandatory = $false, ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false )] [string] $EndDate, [Parameter( Mandatory = $false, ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false )] [switch] $ShowMailFolder, [Parameter( Mandatory = $false, ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false )] [int] $Top = 1, [Parameter( Mandatory = $false, ValueFromPipeline = $false, ValueFromPipelineByPropertyName = $false )] [switch] $IncludeRepliesandForwards ) BEGIN { $ConnectionGraph = Get-MgContext if (-not $ConnectionGraph) { Write-Error "Please connect to Microsoft Graph" -ErrorAction Stop } if ($PSBoundParameters.ContainsKey('StartDate') -and -not $PSBoundParameters.ContainsKey('EndDate')) { Write-Error "EndDate is required when a StartDate is entered" -ErrorAction Stop } if ($PSBoundParameters.ContainsKey('EndDate') -and -not $PSBoundParameters.ContainsKey('StartDate')) { Write-Error "StartDate is required when an EndDate is entered" -ErrorAction Stop } if ($PSBoundParameters.ContainsKey('IncludeRepliesandForwards') -and -not $PSBoundParameters.ContainsKey('Subject')) { Write-Error "A Subject is required to use IncludeRepliesandForwards" -ErrorAction Stop } $Date = Get-Date -Format g } PROCESS { foreach ($User in $UserId) { try { $GraphUser = Get-MgUser -UserId $User -ErrorAction Stop | select Id, DisplayName, UserPrincipalName Write-Verbose "Checking Status for $($GraphUser.DisplayName)" #Building the filter query if ($PSBoundParameters.ContainsKey('Subject')) { if ($Subject -match "'") { $Subject = $Subject -replace "'","''" } if (-not $PSBoundParameters.ContainsKey('IncludeRepliesandForwards')) { $FilterQuery = "Subject eq '$Subject'" } } if ($PSBoundParameters.ContainsKey('SenderAddress')) { if ($FilterQuery) { $FilterQuery = $FilterQuery + " AND from/emailAddress/address eq '$SenderAddress'" } else { $FilterQuery = "from/emailAddress/address eq '$SenderAddress'" } } if ($PSBoundParameters.ContainsKey('StartDate') -and $PSBoundParameters.ContainsKey('EndDate')) { $BeginDateFilter = (Get-Date $StartDate).AddDays(-1).ToString('yyyy-MM-dd') #Adding a day to either side to account for UTC time and MS operators. $EndDateFilter = (Get-Date $EndDate).AddDays(1).ToString('yyyy-MM-dd') if ($FilterQuery) { $FilterQuery = $FilterQuery + " AND ReceivedDateTime ge $BeginDateFilter AND ReceivedDateTime le $EndDateFilter" } else { $FilterQuery = "ReceivedDateTime ge $BeginDateFilter AND ReceivedDateTime le $EndDateFilter" } } if ($FilterQuery) { if ($Top -gt 10) { $Message = Get-MgUserMessage -UserId $User -Filter $FilterQuery -Top $Top -ErrorAction Stop | Sort-Object ReceivedDateTime -Descending } else { $Message = Get-MgUserMessage -UserId $User -Filter $FilterQuery -ErrorAction Stop | Sort-Object ReceivedDateTime -Descending | select -First $Top } if ($PSBoundParameters.ContainsKey('IncludeRepliesandForwards')) { $Message = $Message | Where-Object {$_.Subject -like "* $Subject"} } } else { $Message = Get-MgUserMessage -UserId $User -ErrorAction Stop | Sort-Object ReceivedDateTime -Descending | select -First $Top } #Building output object $Object = [Ordered]@{ RunDate = $Date MailboxDisplayName = $GraphUser.DisplayName Mailbox = $GraphUser.UserPrincipalName UserId = $GraphUser.Id SenderAddress = $null SenderName = $null RecipientAddress = $null RecipientName = $null Subject = $null IsRead = $null HasAttachment = $null ReceivedDate = $null MessageId = $null } if ($PSBoundParameters.ContainsKey('ShowMailFolder')) { $Object.Add( 'MailFolder', $null ) $Object.Add( 'PSTypeName', 'GraphMailReadStatus.MailFolder') } else { $Object.Add( 'PSTypeName', 'GraphMailReadStatus') } if (-not $Message) { Write-Verbose "0 Messages with the subject: '$Subject' were found on Mailbox: '$($GraphUser.DisplayName)'" #Output object [PSCustomObject]$Object } else { Write-Verbose "$($Message.Count) Message(s) with the subject: '$Subject' were returned on Mailbox: '$($GraphUser.DisplayName)'" foreach ($MailItem in $Message) { $Object.SenderAddress = $MailItem.sender.emailaddress | select -ExpandProperty Address $Object.SenderName = $MailItem.sender.emailaddress | select -ExpandProperty Name $Object.RecipientAddress = $MailItem.toRecipients.emailaddress | select -ExpandProperty Address $Object.RecipientName = $MailItem.toRecipients.emailaddress | select -ExpandProperty Name $Object.Subject = $MailItem.Subject $Object.IsRead = $MailItem.IsRead $Object.HasAttachment = $MailItem.HasAttachments $Object.ReceivedDate = $MailItem.ReceivedDateTime.ToLocalTime() $Object.MessageId = $MailItem.Id if ($PSBoundParameters.ContainsKey('ShowMailFolder')) { $MailFolder = Get-MgUserMailFolder -MailFolderId $MailItem.ParentFolderId -UserId $GraphUser.UserPrincipalName | select -ExpandProperty DisplayName $Object.MailFolder = $MailFolder $Object.PSTypeName = 'GraphMailReadStatus.MailFolder' } else { $Object.PSTypeName = 'GraphMailReadStatus' } [PSCustomObject]$Object $Message = $null $MailItem = $null $MailFolder = $null } } } catch { Write-Error $_.Exception.Message } #end foreach block $Object = $null } } END {} }
Script Parameters
-UserId
DataType: string/array
Description: Checks against this persons mailbox. Multiple UPNs/ObjectIds separated by a comma are acceptable.
-Subject
DataType: string
Description: Queries the mailbox using the EXACT text specified
-SenderAddress
DataType: string
Description: Specify the ‘From’ Address in your search. Format should be [email protected]
-StartDate
DataType: string
Description: Specify the date when the query should start filtering for. Format should be MM/DD/YYYY
-EndDate
DataType: string
Description: Specify the date when the query should end the filter. Format should be MM/DD/YYYY
-ShowMailFolder
DataType: switch
Description: When this switch is used, it will display what folder the email is currently located in. This makes the overall query slower so use only when needed
-Top
DataType: int
Description: Defaulted to 1. This is the limit of emails per mailbox that you would like to find
Example 1 – Specifying a user, a subject and the parent folder
PS C:\> Get-MSGraphMailReadStatus -UserId [email protected] ` -Subject "You’ve renewed your Office 365 E1 subscription" -ShowMailFolder
Conclusion
Hopefully this article was able to show you how to check if an email was read using Graph API PowerShell. Since this utilizes Graph API, it supports PowerShell 7 and can be incredible fast when using Foreach-Object -Parallel. I’ve used this several times in my environment and its great addition to my tool belt.
For those of us struggling with even getting access to USE Graph API, let alone understanding it, this is quite difficult and disappointing. As an Exchange Admin and Security Admin, we should be able to capture this using a PowerShell command in Exchange, one would think.
Your script is not working as I get the result Get-MSGraphMailReadStatus is not recognized
Can you show how to run it with Foreach-Object -Parallel for collecting emails from several mailboxes?
also i am getting internalservererror when running it for a large number of mailboxes (10000+)
First of all, this was an incredibly well thought out tutorial and also very helpful.
For those starting out and wanting to run the example command, do the following:
In a new PowerShell window, execute Import-Module -Name MSAL.PS -Force before anything else.
Then:
$AppID = ‘appid’
$TenantID = ‘tenid’
$ClientSecret = ‘somesecret’
$MsalToken = Get-MsalToken -TenantId $TenantID -ClientId $AppID -ClientSecret ($ClientSecret | ConvertTo-SecureString -AsPlainText -Force)
Connect-Graph -AccessToken $MsalToken.AccessToken
Once that is complete, navigate to folder where you stored Get-MSGraphMailReadStatus.ps1
Load the script into the PowerShell window (there is a space between those two dots).
PS C:\ExhangePowerShell>. .\Get-MSGraphMailReadStatus.ps1
PS C:\ExchangePowerShell> Get-MSGraphMailReadStatus -UserId myid@some_corp.com -Subject “Lunch Menu” -ShowMailFolder