4

Check If An Email Was Read using Graph API PowerShell SDK

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.
 

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

Mail Read Status Graph API Permissions
 

If you have never created an Azure App or need help getting started with Microsoft Graph API, please follow this guide on how to Connect To Microsoft Graph API Using PowerShell. It will take you from zero knowledge to getting everything up and running in no time.

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

Get Mail Read Status Example 1

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.

5/5 - (6 votes)

Paul Contreras

Hi, my name is Paul and I am a Sysadmin who enjoys working on various technologies from Microsoft, VMWare, Cisco and many others. Join me as I document my trials and tribulations of the daily grind of System Administration.

4 Comments

  1. 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.

  2. Your script is not working as I get the result Get-MSGraphMailReadStatus is not recognized

  3. 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+)

  4. 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

Leave a Reply

Your email address will not be published.