Back to script library
Entra / Microsoft 365 · Exchange Online

Analyze mail traffic users

Using the Exchange Online message trace log to analyze messages sent to external and internal domains by mailboxes.

Connect & set up

Run these once per session. All scopes are read-only unless the script makes changes.

Connect-ExchangeOnline

Run it

The main script. Copy it, or download the .ps1 and run it from your console.

param(
[int] $LookbackDays = 10,
[string] $StartDate = (Get-Date).AddDays(-$LookbackDays),
[string] $EndDate = (Get-Date),
[int] $BatchSize = 2000
)
If ($Null -eq (Get-ConnectionInformation)) {
Connect-ExchangeOnline
}
[array]$Messages = $Null
# Message trace date is kept for a maximum of 10 days
Write-Host ("Message trace data will be analyzed between {0} and {1}" -f $StartDate, $EndDate)
[array]$Messages = $Null
# Define the page size to fetch messages.
[int]$BatchSizeForMessages = 1000
# original code [array]$MessagePage = Get-MessageTrace -StartDate $StartDate -EndDate $EndDate -PageSize 1000 -Page $i -Status "Delivered"
Try {
# The warning action is suppressed here because we don't want to see warnings when more data is available
[array]$MessagePage = Get-MessageTraceV2 -StartDate $StartDate -EndDate $EndDate `
-ResultSize $BatchSizeForMessages -Status "Delivered" -ErrorAction Stop -WarningAction SilentlyContinue
$Messages += $MessagePage
# Very basic throttling in an attempt to avoid Exchange Online not being able to return pages of message trace data.
Start-Sleep -Milliseconds 250
} Catch {
Write-Host ("Error fetching message trace data: {0}" -f $_.Exception.Message)
Break
}
If ($MessagePage.count -eq $BatchSizeForMessages) {
Do {
Write-Host ("Fetched {0} messages so far" -f $Messages.count)
$LastMessageFetched = $MessagePage[-1]
$LastMessageFetchedDate = $LastMessageFetched.Received.ToString("O")
$LastMessageFetchedRecipient = $LastMessageFetched.RecipientAddress
# Fetch the next page of messages
[array]$MessagePage = Get-MessageTraceV2 -StartDate $StartDate -EndDate $LastMessageFetchedDate `
-StartingRecipientAddress $LastMessageFetchedRecipient -ResultSize $BatchSizeForMessages -Status "Delivered" -ErrorAction Stop -WarningAction SilentlyContinue
If ($MessagePage) {
$Messages += $MessagePage
}
} While ($MessagePage.count -eq $BatchSizeForMessages)
}
# Remove Exchange Online public folder hierarchy synchronization messages
$Messages = $Messages | Where-Object {$_.Subject -NotLike "*HierarchySync*"}
# Get a list of vertified domains for the tenant so we can differentiate between internal
# and external email based on recipient address
[array]$Domains = Get-AcceptedDomain | Select-Object -ExpandProperty DomainName
# Fetch a list of user and shared mailboxes to process
[array]$Mbx = Get-ExoMailbox -RecipientTypeDetails UserMailbox, SharedMailbox -ResultSize Unlimited | Sort-Object DisplayName
$MessageReport = [System.Collections.Generic.List[Object]]::new()
ForEach ($User in $Mbx) {
Write-Host ("Processing email for {0}" -f $User.DisplayName)
# Get messages sent by the user
[array]$UserMessages = $Messages | Where-Object {$_.SenderAddress -eq $User.PrimarySmtpAddress}
If ($UserMessages) {
# We’ve found some messages to process, so let’s do that
[int]$ExternalEmail = 0; [int]$InternalEmail = 0; [array]$ExternalDomains = $Null
ForEach ($M in $UserMessages) {
$MsgRecipientDomain = $M.RecipientAddress.Split('@')[1]
If ($MsgRecipientDomain -in $Domains) {
$InternalEmail++
} Else {
$ExternalEmail++
$ExternalDomains += $MsgRecipientDomain
}
} # End Foreach message
$ExternalDomains = $ExternalDomains | Sort-Object -Unique
$PercentInternal = "N/A"; $PercentExternal = "N/A"
If ($InternalEmail -gt 0) {
$PercentInternal = ($InternalEmail/($UserMessages.count)).toString("P") }
If ($ExternalEmail -gt 0) {
$PercentExternal = ($ExternalEmail/($UserMessages.count)).toString("P") }
Switch ($User.RecipientTypeDetails) {
"UserMailbox" { $Type = "User"}
"SharedMailbox" { $Type = "Shared"}
}
$ReportLine = [PSCustomObject]@{
User = $User.UserPrincipalName
Name = ("{0} ({1})" -f $User.DisplayName, $Type)
Internal = $InternalEmail
"% Internal" = $PercentInternal
External = $ExternalEmail
"% External" = $PercentExternal
"External Domains" = $ExternalDomains -Join ", "
}
$MessageReport.Add($ReportLine)
} # End if user (has some messages)
} # End ForEach mailboxes
# Generate a report
$ReportFile = "c:\temp\UserMailTraffic.html"
$CSVFile = "c:\temp\UserMailTraffic.csv"
$HtmlHead="<html>
<style>
BODY{font-family: Arial; font-size: 8pt;}
H1{font-size: 22px; font-family: 'Segoe UI Light','Segoe UI','Lucida Grande',Verdana,Arial,Helvetica,sans-serif;}
H2{font-size: 18px; font-family: 'Segoe UI Light','Segoe UI','Lucida Grande',Verdana,Arial,Helvetica,sans-serif;}
H3{font-size: 16px; font-family: 'Segoe UI Light','Segoe UI','Lucida Grande',Verdana,Arial,Helvetica,sans-serif;}
TABLE{border: 1px solid black; border-collapse: collapse; font-size: 8pt;}
TH{border: 1px solid #969595; background: #dddddd; padding: 5px; color: #000000;}
TD{border: 1px solid #969595; padding: 5px; }
td.pass{background: #B7EB83;}
td.warn{background: #FFF275;}
td.fail{background: #FF2626; color: #ffffff;}
td.info{background: #85D4FF;}
</style>
<body>
<div align=center>
<p><h1>Message Traffic User Analysis</h1></p>
<p><h3>Generated: " + (Get-Date -format 'dd-MMM-yyyy hh:mm') + " for " + (Get-OrganizationConfig | Select-Object -ExpandProperty DisplayName) + "</h3></p></div>"
$HtmlBody = $MessageReport | ConvertTo-Html -Fragment
"</body></html><p>" + $HtmlHead + $Htmlbody + "<p>" | Out-File $ReportFile -Encoding UTF8
$MessageReport | Export-Csv -NoTypeInformation $CSVFile
Write-Host ("All done - the HTML report is available in {0} and CSV in {1}" -f $ReportFile, $CSVFile)

Parameters

ParameterDefaultNotes
-LookbackDays10Number of days to include in the message trace search window.
-StartDate(Get-Date).AddDays(-10)Start of the reporting window.
-EndDate(Get-Date)End of the reporting window.
-BatchSize2000Maximum number of records to fetch per query batch.
Attribution