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 NameIf (!($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 NameIf (!($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 unicodeForEach ($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 = $RecipientOutcome = $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 outcomeIf (!($RecipientOutcome -in $BadOutcomes)) {$ReportLine = [PSCustomObject]@{Timestamp = $Line.origin_timestamp_utcSender = $Line.sender_addressSubject = $Line.message_subjectRecipient = $RecipientInfo.RecipientRecipientDomain = $RecipientInfo.Recipient.Split("@")[1]RecipientStatus = $Line.Recipient_StatusRecipientInfo = $RecipientInfoOutcome = $RecipientOutcomeBytes = $Line.total_bytesMessage_id = $Line.message_idSender_Domain = $SenderDomainClient_IP = $Line.original_client_ipDirection = $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 TimestampWrite-Host ("{0} records found for inbound email" -f $InboundEmail.count)Write-Host ("{0} records found for outbound email" -f $OutboundEmail.count)
Attribution
Author
Office365itpros