Create Report from Exchange Protocol logs (Script)

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: