PowerShell 7 Script -
<# FAST Exchange 2016 SMTP Protocol Log Analyzer (Internal + Local with Sender/Recipient)
- Robust timestamp parsing (no TryParse)
- StreamReader + hash tables (fast)
- Outputs one row per recipient with sender, connector, session, IP, and timestamp
#>
$logPath = "C:\New_Folder\SMTP-Receive-Logs\6-9-2026\SmtpReceive" # <-- update to your folder
Write-Host "Fast processing logs from: $logPath" -ForegroundColor Cyan
# -------- Settings --------
# Local hosts (Exchange nodes + loopback). Add/remove your server IPs here.
$localIPs = @{
"127.0.0.1" = $true
"10.100.201.248" = $true # EXHYBRID1
"10.100.201.197" = $true # EXHYBRID2
}
# Internal prefixes
$internalPrefixes = @(
"10.",
"172.16.","172.17.","172.18.","172.19.","172.20.",
"172.21.","172.22.","172.23.","172.24.","172.25.",
"172.26.","172.27.","172.28.","172.29.","172.30.","172.31.",
"192.168."
)
function Test-InternalIP {
param([string]$ip)
foreach ($p in $internalPrefixes) {
if ($ip.StartsWith($p)) { return $true }
}
return $false
}
# Parse ISO-ish timestamps robustly; return [datetime] (UTC) or $null
function Convert-ToTimestamp {
param([string]$s)
if ([string]::IsNullOrWhiteSpace($s)) { return $null }
try {
# Handles 2026-01-14T23:00:17.776Z and similar ISO formats
$dto = [datetimeoffset]::Parse($s, [System.Globalization.CultureInfo]::InvariantCulture)
return $dto.UtcDateTime
}
catch {
try {
# Fallback (e.g., without Z or ms)
return [datetime]::Parse($s, [System.Globalization.CultureInfo]::InvariantCulture)
}
catch {
return $null
}
}
}
# -------- Session State Model --------
class SessionState {
[string] $Connector
[string] $ClientIP
[string] $Category # LOCAL/INTERNAL
[datetime] $LastTs
[string] $CurrentMailFrom
[System.Collections.Generic.List[string]] $Recipients
SessionState([string]$connector, [string]$clientIP, [string]$category, [datetime]$ts) {
$this.Connector = $connector
$this.ClientIP = $clientIP
$this.Category = $category
$this.LastTs = $ts
$this.CurrentMailFrom = $null
$this.Recipients = [System.Collections.Generic.List[string]]::new()
}
}
# -------- Data Stores --------
$sessions = @{} # key: session-id -> SessionState
$internalRows = New-Object System.Collections.Generic.List[object]
$localRows = New-Object System.Collections.Generic.List[object]
# Commit the current message (1 row per recipient)
function Commit-Message {
param(
[string]$sessionId,
[SessionState]$state
)
if ([string]::IsNullOrWhiteSpace($state.CurrentMailFrom)) { return }
if ($state.Recipients.Count -eq 0) { return }
foreach ($rcpt in $state.Recipients) {
$row = [PSCustomObject]@{
Timestamp = $state.LastTs
ClientIP = $state.ClientIP
Connector = $state.Connector
SessionId = $sessionId
Sender = $state.CurrentMailFrom
Recipient = $rcpt
}
if ($state.Category -eq 'LOCAL') {
$localRows.Add($row) | Out-Null
} else {
$internalRows.Add($row) | Out-Null
}
}
# Reset for next message within the same session
$state.CurrentMailFrom = $null
$state.Recipients.Clear()
}
# Compiled regex for speed
$reMailFrom = [regex]::new('(?i)^\s*MAIL FROM:\s*<([^>]*)>', 'Compiled')
$reRcptTo = [regex]::new('(?i)^\s*RCPT TO:\s*<([^>]*)>', 'Compiled')
# -------- Process Logs --------
Get-ChildItem -Path $logPath -Filter *.log | Sort-Object Name | ForEach-Object {
Write-Host "Processing $($_.Name)..." -ForegroundColor Yellow
$reader = [System.IO.StreamReader]::new($_.FullName)
while (-not $reader.EndOfStream) {
$line = $reader.ReadLine()
if (-not $line) { continue }
# Skip non-data lines quickly (e.g., "#Fields", blanks)
$c = $line[0]
if ($c -lt '0' -or $c -gt '9') { continue }
# date-time,connector-id,session-id,sequence-number,local-endpoint,remote-endpoint,event,data,context
$cols = $line.Split(',', 9)
if ($cols.Count -lt 8) { continue }
$ts = Convert-ToTimestamp $cols[0]
if (-not $ts) { continue } # robust: skip if timestamp can’t be parsed
$connector = $cols[1]
$sessionId = $cols[2]
$remoteEp = $cols[5]
$event = $cols[6] # "<", ">", "+", "-"
$data = $cols[7]
# Extract client IP (IPv4) from remote-endpoint "A.B.C.D:port"
if (-not $remoteEp -or ($remoteEp -notmatch '^(\d{1,3}\.){3}\d{1,3}')) { continue }
$clientIP = $remoteEp.Split(':')[0]
# Resolve/initialize session only for LOCAL/INTERNAL
$state = $sessions[$sessionId]
if (-not $state) {
if ($localIPs.ContainsKey($clientIP)) {
$state = [SessionState]::new($connector, $clientIP, 'LOCAL', $ts)
$sessions[$sessionId] = $state
}
elseif (Test-InternalIP $clientIP) {
$state = [SessionState]::new($connector, $clientIP, 'INTERNAL', $ts)
$sessions[$sessionId] = $state
}
else {
# External — skip entirely (perf)
continue
}
}
else {
$state.LastTs = $ts
}
# On disconnect, commit any pending message and close session
if ($event -eq '-') {
Commit-Message -sessionId $sessionId -state $state
$sessions.Remove($sessionId) | Out-Null
continue
}
# Only parse client-sent commands
if ($event -ne '<') { continue }
if ([string]::IsNullOrWhiteSpace($data)) { continue }
# Start of a new message: MAIL FROM
$m = $reMailFrom.Match($data)
if ($m.Success) {
# Commit the previous message (if any)
Commit-Message -sessionId $sessionId -state $state
$state.CurrentMailFrom = $m.Groups[1].Value.Trim()
$state.Recipients.Clear()
continue
}
# Add recipient to current message
$r = $reRcptTo.Match($data)
if ($r.Success) {
$addr = $r.Groups[1].Value.Trim()
if ($addr) { $state.Recipients.Add($addr) | Out-Null }
continue
}
# Optional: commit on DATA if you want message row as soon as data starts
# if ($data.StartsWith('DATA', [System.StringComparison]::OrdinalIgnoreCase)) {
# Commit-Message -sessionId $sessionId -state $state
# }
}
$reader.Close()
}
# Flush open sessions (e.g., if logs end mid-session)
foreach ($kv in @($sessions.GetEnumerator())) {
Commit-Message -sessionId $kv.Key -state $kv.Value
}
$sessions.Clear()
# -------- Output --------
$internalRows | Export-Csv ".\InternalRelayDetails.csv" -NoTypeInformation
$localRows | Export-Csv ".\LocalConnectionsDetails.csv" -NoTypeInformation
Write-Host "`nFAST PROCESSING COMPLETE!" -ForegroundColor Green
Write-Host " InternalRelayDetails.csv"
Write-Host " LocalConnectionsDetails.csv"
No comments:
Post a Comment