2025-06-04 10:12:06 +05:30
<#
= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
Name : Trace Inbound External Emails in Exchange Online
2025-06-09 10:30:53 +05:30
Version : 1.1
2025-06-04 10:12:06 +05:30
Website : o365reports . com
Script Highlights :
~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~ ~
1 . Exports emails received from external domains into a CSV file .
2025-06-09 10:30:53 +05:30
2 . Supports exporting emails within a specified 90 -day time range .
2025-06-04 10:12:06 +05:30
3 . Lists emails received from a specific external domain .
4 . Finds emails received from a specific external user .
5 . Audit emails received by a specific user from external domains .
6 . Allows filter ing the mail flow report based on mail status .
7 . The script automatically verifies and installs the Exchange Online PowerShell module ( if not installed already ) upon your confirmation .
8 . The script can be executed with an MFA-enabled account .
9 . It can be executed with Certificate-based Authentication ( CBA ) too .
10 . The script is scheduler-friendly .
For detailed Script execution : https : / / o365reports . com / 2025 / 06 / 03 / trace-emails -received -from -external -domains -in -exchange -online
2025-06-09 10:30:53 +05:30
Change Log
~ ~ ~ ~ ~ ~ ~ ~ ~ ~
V1 . 0 ( Jun 03 , 2025 ) - File created
V1 . 1 ( Jun 09 , 2025 ) - Extended message tracing period from 30 to 90 days .
2025-06-04 10:12:06 +05:30
= = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
#>
param (
[ Parameter ( Mandatory = $false ) ]
[ Nullable[DateTime] ] $StartDate ,
[ Nullable[DateTime] ] $EndDate ,
[ string ] $UserName ,
[ string ] $Password ,
[ string ] $SenderAddress = " " ,
[ string ] $RecipientAddress = " " ,
[ ValidateSet ( " Delivered " , " Failed " , " Gettingstatus " , " FilteredAsSpam " , " Quarantined " ) ]
[ string ] $MailStatus ,
[ string ] $ExternalDomainName ,
[ string ] $Organization ,
[ string ] $ClientId ,
[ string ] $CertificateThumbprint
)
#--------------------------------------------------- Date Time Checking ---------------------------------------------------
2025-06-09 10:30:53 +05:30
$MaxStartDate = ( ( Get-Date ) . AddDays ( -90 ) ) . Date
2025-06-04 10:12:06 +05:30
if ( -not $StartDate -and -not $EndDate ) {
$EndDate = ( Get-Date ) . Date
$StartDate = $MaxStartDate
}
try {
if ( $StartDate ) { $StartDate = [ DateTime ] $StartDate }
if ( $EndDate ) { $EndDate = [ DateTime ] $EndDate }
if ( $StartDate -lt $MaxStartDate ) {
2025-06-09 10:30:53 +05:30
Write-Host " Error: MessageTrace can only be retrieved for the past 90 days. Select a date after $MaxStartDate " -ForegroundColor Red
2025-06-04 10:12:06 +05:30
Exit
}
if ( $EndDate -lt $StartDate ) {
Write-Host " Error: End date must be later than start date. " -ForegroundColor Red
Exit
}
}
catch {
Write-Host " Error: Invalid date format. Please enter a valid date. " -ForegroundColor Red
Exit
}
$CurrentStart = $StartDate
$CurrentEnd = $EndDate . AddDays ( 1 ) . AddSeconds ( -1 )
#-------------------------------------------------- Function: Connect Exchange Online --------------------------------------
Function Connect_Exo {
$installedModule = Get-Module ExchangeOnlineManagement -ListAvailable | Where-Object { $_ . Version -ge [ version ] " 3.0 " }
if ( -not $installedModule ) {
Write-Host " Exchange Online PowerShell module is not available or unsupported version is available " -ForegroundColor Yellow
$confirm = Read-Host " Do you want to install the latest module? [Y] Yes [N] No "
if ( $confirm -match " [yY] " ) {
Install-Module ExchangeOnlineManagement -Repository PSGallery -AllowClobber -Force
}
else {
Write-Host " EXO module is required. Please install it manually using 'Install-Module ExchangeOnlineManagement'. "
exit
}
}
Write-Host " Connecting to Exchange Online... "
if ( $UserName -and $Password ) {
$SecuredPassword = ConvertTo-SecureString -AsPlainText $Password -Force
$Credential = New-Object System . Management . Automation . PSCredential ( $UserName , $SecuredPassword )
Connect-ExchangeOnline -Credential $Credential -ShowBanner: $false
}
elseif ( $Organization -ne " " -and $ClientId -ne " " -and $CertificateThumbprint -ne " " )
{
Connect-ExchangeOnline -AppId $ClientId -CertificateThumbprint $CertificateThumbprint -Organization $Organization -ShowBanner: $false
}
else {
Connect-ExchangeOnline -ShowBanner: $false
}
}
#-------------------------------------------------- Function: Export To CSV -----------------------------------------------
Function Export-ToCSV {
param (
[ Parameter ( Mandatory = $true ) ]
$Results
)
$Results |
Select-Object MessageTraceID ,
@ { Name = " Received Time " ; Expression = { ( $_ . Received ) . ToLocalTime ( ) . ToString ( " yyyy-MM-dd HH:mm:ss " ) } } ,
@ { Name = " Sender Address " ; Expression = { $_ . SenderAddress } } ,
@ { Name = " Recipient Address " ; Expression = { $_ . RecipientAddress } } ,
Subject , Status ,
@ { Name = " Sender Domain " ; Expression = { ( $_ . SenderAddress -split " @ " ) [ 1 ] } } ,
@ { Name = " Recipient Domain " ; Expression = { ( $_ . RecipientAddress -split " @ " ) [ 1 ] } } ,
@ { Name = " Sender IP " ; Expression = { $_ . FromIP } } , #FromIP
@ { Name = " Receipient IP " ; Expression = { $_ . ToIP } } , #TOIP
@ { Name = " Mail Size(KB) " ; Expression = { [ math ] :: Round ( $_ . Size / 1 KB , 2 ) } } |
Export-Csv -Path $OutputCSV -Append -Force -NoTypeInformation
}
#-------------------------------------------------- Function: Filter Data --------------------------------------------------
Function Filterdata {
$FilteredResults = @ ( )
foreach ( $queryResult in $queryResults ) {
$senderDomain = ( $queryResult . SenderAddress -split " @ " ) [ -1 ]
$recipientDomain = ( $queryResult . RecipientAddress -split " @ " ) [ -1 ]
if (
( $InternalDomains -contains $recipientDomain ) -and
( -not ( $InternalDomains -contains $senderDomain ) ) -and
( [ string ] :: IsNullOrEmpty ( $SenderAddress ) -or $queryResult . SenderAddress -eq $SenderAddress ) -and
( [ string ] :: IsNullOrEmpty ( $RecipientAddress ) -or $queryResult . RecipientAddress -eq $RecipientAddress ) -and
( [ string ] :: IsNullOrEmpty ( $ExternalDomainName ) -or $ExternalDomainName -eq $SenderDomain ) -and
( [ string ] :: IsNullOrEmpty ( $MailStatus ) -or $queryResult . Status -eq $MailStatus )
) {
$FilteredResults + = $queryResult
}
}
if ( $FilteredResults . Count -gt 0 ) {
$Global:FilteredCount + = $FilteredResults . Count
Export-ToCSV -Results $FilteredResults
}
}
#-------------------------------------------------- Initialization ---------------------------------------------------------
Connect_Exo
$batchSize = 5000
$InternalDomains = ( Get-AcceptedDomain ) . DomainName # Gett all Internal Domains
$Location = Get-Location
$Timestamp = ( Get-Date -Format " yyyy-MM-dd_HH-mm-ss " )
$OutputCSV = " $Location \MailsReceivedFromExternalDomain_Report_ $Timestamp .csv "
$queryResults = $null
$ProcessedCount = 0
$Global:FilteredCount = 0
#-------------------------------------------------- Core Processing Loop ---------------------------------------------------
while ( $CurrentEnd -ge $CurrentStart ) {
$IntervalStartDate = $CurrentEnd . AddDays ( -10 )
if ( $IntervalStartDate -lt $CurrentStart ) { $IntervalStartDate = $CurrentStart } # To check currentstartdate not go beyond Actualstartdate
if ( $IntervalStartDate -ge $CurrentEnd ) { break } # To Terminate if DateInterval reaches enddate
try {
$queryResults = Get-MessageTraceV2 -StartDate $IntervalStartDate -EndDate $CurrentEnd -ResultSize $batchSize -ErrorAction Stop
$ProcessedCount + = $queryResults . Count
if ( $queryResults . Count -eq 0 ) {
# If the current Quary has No result change date to next Quary ,Skip the current Quary
$CurrentEnd = $IntervalStartDate
continue
}
Filter data
}
catch {
Write-Host " Error fetching message trace data: $_ " -ForegroundColor Red
Disconnect-ExchangeOnline -Confirm: $false -ErrorAction SilentlyContinue
Exit
}
Write-Progress -Activity " Retrieving Mail Record from $StartDate to $EndDate " -Status " Processed count: $ProcessedCount "
## #To Fetch Subsequant Data in the current quary
while ( $queryResults . Count -eq $batchSize ) {
$lastMessage = $queryResults [ -1 ]
$LastEndDate = $lastMessage . Received . ToString ( " O " )
$StartingRecipientAddress = $lastMessage . RecipientAddress
try {
$queryResults = Get-MessageTraceV2 -StartDate $IntervalStartDate -EndDate $LastEndDate -StartingRecipientAddress $StartingRecipientAddress -ResultSize $batchSize -ErrorAction Stop
$ProcessedCount + = $queryResults . Count
Write-Progress -Activity " Retrieving Mail Record from $StartDate to $EndDate " -Status " Processed count: $ProcessedCount "
# To break the loop if $quaryresult is zero
if ( $queryResults . Count -eq 0 ) { break }
Filter data
}
catch {
Write-Host " Error fetching additional message trace data: $_ " -ForegroundColor Red
Disconnect-ExchangeOnline -Confirm: $false -ErrorAction SilentlyContinue
Exit
}
}
$CurrentEnd = $IntervalStartDate # Adjusted the End date Last IntervalStartdate
}
#-------------------------------------------------- Final Output -----------------------------------------------------------
Write-Host ` n ~ ~ Script prepared by AdminDroid Community ~ ~ ` n -ForegroundColor Green
Write-Host " ~~ Check out " -NoNewline -ForegroundColor Green ; Write-Host " admindroid.com " -ForegroundColor Yellow -NoNewline ; Write-Host " to get access to 1900+ Microsoft 365 reports. ~~ " -ForegroundColor Green ` n ` n
If ( $FilteredCount -eq 0 ) {
Write-Host " No records found "
}
else {
Write-Host " `n The output file contains $FilteredCount Mail records. "
if ( Test-Path -Path $OutputCSV ) {
Write-Host " `n The Output file is available at: " -NoNewline -ForegroundColor Yellow
Write-Host $OutputCSV
$Prompt = New-Object -ComObject wscript . shell
$UserInput = $Prompt . popup ( " Do you want to open the output file? " , 0 , " Open Output File " , 4 )
If ( $UserInput -eq 6 ) {
Invoke-Item " $OutputCSV "
}
}
}
Disconnect-ExchangeOnline -Confirm: $false