Back to script library
Entra / Microsoft 365 · Exchange Online

Analyze message trace historical logs

Analyzes historical Exchange Online message tracking logs exported to CSV files and summarizes traffic patterns.

Connect & set up

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

Connect-ExchangeOnline -ShowBanner:$false -ErrorAction Stop

Run it

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

[array]$Modules = Get-Module | Select-Object -ExpandProperty Name
If (!($Modules -contains "ExchangeOnlineManagement")) {
Write-Host "Loading ExchangeOnlineManagement module"
Connect-ExchangeOnline -ShowBanner:$false -ErrorAction Stop
}
# Folder where the historical message tracking logs downloaded from Exchange Online are stored
$DataFolder = "c:\temp\MtData\"
$CSVFile = "c:\temp\HistoricalMessageTrace.CSV"
# Get accepted domains
[array]$Domains = Get-AcceptedDomain | Select-Object -ExpandProperty DomainName
[array]$DataFiles = Get-ChildItem -Path $DataFolder | Select-Object -ExpandProperty Name
If (!($DataFiles)) {
Write-Host "No historical message tracking logs to analyze - exiting"
Break
}
Write-Host ("Preparing to process {0} historical message trace data files..." -f $DataFiles.count)
$Report = [System.Collections.Generic.List[Object]]::new() # Create output file for report
[array]$BadOutcomes = "Receive, Fail", "Receive, Deliver, Quarantined", "Receive, Deliver, FilteredAsSpam"
ForEach ($File in $DataFiles) {
$MtDataFile = $DataFolder + $File
[array]$MtData = Import-CSV -Path $MtDataFile -Encoding unicode
ForEach ($Line in $MtData) {
If (!([string]::IsNullOrEmpty($Line.origin_timestamp_utc))){
[array]$RecipientStatus = $Line.Recipient_Status.split(";")
# array of individual recipients for a message
$RecipientInfo = [System.Collections.Generic.List[Object]]::new()
ForEach ($RecipientDetail in $RecipientStatus) {
$Recipient = $RecipientDetail.Split("##")[0]
$RecipientOutcome = $RecipientDetail.Split("##")[1]
$RecipientLine = [PSCustomObject]@{
Recipient = $Recipient
Outcome = $RecipientOutcome
}
$RecipientInfo.Add($RecipientLine)
}
$SenderDomain = $Line.Sender_address.Split("@")[1]
If ($SenderDomain -in $Domains) {
$Direction = "Originating"
} Else {
$Direction = "Incoming"
}
# Only report on messages with a good outcome
If (!($RecipientOutcome -in $BadOutcomes)) {
$ReportLine = [PSCustomObject]@{
Timestamp = $Line.origin_timestamp_utc
Sender = $Line.sender_address
Subject = $Line.message_subject
Recipient = $RecipientInfo.Recipient
RecipientDomain = $RecipientInfo.Recipient.Split("@")[1]
RecipientStatus = $Line.Recipient_Status
RecipientInfo = $RecipientInfo
Outcome = $RecipientOutcome
Bytes = $Line.total_bytes
Message_id = $Line.message_id
Sender_Domain = $SenderDomain
Client_IP = $Line.original_client_ip
Direction = $Direction
}
$Report.Add($ReportLine)
}
}
}
}
$Report = $Report | Sort-Object Sender, @{Expression = { $_.Timestamp -as [datetime] }; Descending = $true}
$Report | Select-Object Timestamp, Sender, Subject, Recipient | Out-GridView
$Report | Export-CSV -NoTypeInformation $CSVFile
# Split into outbound and inbound files (when we have data files containing both types of data)
$OutboundEmail = $Report | Where-Object {$_.Direction -eq 'Originating'} | Sort-Object Timestamp
$InboundEmail = $Report | Where-Object {$_.Direction -eq 'Incoming'} | Sort-Object Timestamp
Write-Host ("{0} records found for inbound email" -f $InboundEmail.count)
Write-Host ("{0} records found for outbound email" -f $OutboundEmail.count)
Attribution