Outlook VBA Spoof check - checks sender email against originator email - RED banner if fail

Attribute VB_Name = "otl_SpoofCheck" Option Explicit Option Compare Text ' References: ' - Microsoft Outlook XX.X Object Library ' - Microsoft VBScript Regular Expressions 5.5 ' - Microsoft Scripting Runtime Private Const WARNING_MARKER As String = "<!--CC-HEADER-SPOOF-BANNER-->" Private Const LOG_FILE As String = "C:\temp\AntiSpoof_HeaderOnly_Log.csv" Public Sub Run_HeaderSpoof_OnActive() Dim insp As Outlook.Inspector ' Active mail inspector On Error GoTo Quit Set insp = Application.ActiveInspector If insp Is Nothing Then MsgBox "Open an email first.", vbExclamation Exit Sub End If If TypeName(insp.CurrentItem) = "MailItem" Then Call IsSpoofHeader(insp.CurrentItem) 'MsgBox "Header spoof check complete.", vbInformation Else MsgBox "Active item is not a mail item.", vbExclamation End If Quit: End Sub Public Function IsSpoofHeader(ByVal mi As Outlook.mailItem) As Boolean On Error GoTo FailSafe If mi Is Nothing Then Exit Function Dim actualAddr As String ' SMTP address of sender Dim actualDom As String ' Domain of actual sender Dim hdr As String ' Raw header string Dim reFrom As RegExp ' RegExp to extract From header Dim shownDom As String ' Domain shown in header Dim originDom As String ' Originator domain (Return-Path or Received) Dim matches As MatchCollection ' RegExp match results Dim mismatch As Boolean ' Whether domains mismatch Dim bannerText As String ' Text for warning banner Dim bannerHtml As String ' HTML content for warning banner Dim passList As Variant ' Array of allowed domains or relays Dim passItem As Variant ' For each item in allowlist IsSpoofHeader = False ' Default return is False (not spoofed) actualAddr = GetSenderSmtpAddress(mi) ' Get actual SMTP sender address If Len(actualAddr) = 0 Then Exit Function ' Exit if no address found actualDom = LCase$(GetDomainFromEmail(actualAddr)) ' Extract and lowercase domain hdr = GetHeaderRaw(mi) ' Get full message header If Len(hdr) = 0 Then Exit Function ' Exit if no header present '--- Bypass spoof detection if email passed authentication (e.g., SPF, DMARC) or marked internal ''If InStr(1, hdr, "spf=pass", vbTextCompare) > 0 Or If InStr(1, hdr, "dmarc=pass", vbTextCompare) > 0 Or _ InStr(1, hdr, "X-MS-Exchange-Organization-AuthAs: Internal", vbTextCompare) > 0 Or _ InStr(1, hdr, "X-MS-Exchange-CrossTenant-AuthAs: Internal", vbTextCompare) > 0 Then Exit Function End If Set reFrom = New RegExp ' Set up RegExp to extract From header reFrom.pattern = "^\s*From:\s.*?<([^>]+)>" ' Match address inside angle brackets reFrom.IgnoreCase = True reFrom.Multiline = True reFrom.Global = False Set matches = reFrom.Execute(hdr) ' Execute regex on headers If matches.Count > 0 Then shownDom = LCase$(GetDomainFromEmail(matches(0).SubMatches(0))) ' Extract domain from From Else shownDom = "(none)" ' Fallback if no From found End If originDom = GetOriginatorDomainFromHeader(hdr) ' Extract oldest domain in Received headers If Len(originDom) = 0 Then originDom = "(unknown)" ' Fallback if origin not found '--- Allowlist logic: skip spoof check if sender or originator domain is trusted passList = Array("somedom") For Each passItem In passList If InStr(1, originDom, LCase$(passItem)) > 0 Then Exit Function End If Next passItem '--- Compare all 3 domains for mismatch mismatch = Not (EndsWithDomain(shownDom, actualDom) And _ EndsWithDomain(originDom, actualDom)) If mismatch Then IsSpoofHeader = True ' Flag as spoofed bannerText = "----------------------------------------------------------" & vbCrLf & _ "<br>WARNING: Possible header spoofing detected!" & vbCrLf & _ "<br>Displayed From domain: " & shownDom & vbCrLf & _ "<br>Actual sender domain: " & actualDom & vbCrLf & _ "<br>Originator domain: " & originDom & vbCrLf & _ "<br>----------------------------------------------------------" & vbCrLf bannerHtml = WARNING_MARKER & _ "<div style=""padding:10px;margin:0 0 10px 0;background:#c00;color:#fff;" & _ "font-family:Segoe UI,Arial,sans-serif;font-size:14px;font-weight:700;letter-spacing:.2px;" & _ "border-radius:4px;"">" & _ "<p style='margin:0;'>" & bannerText & "</p>" & _ "</div>" If Len(mi.htmlBody) > 0 Then If Not AlreadyTagged(mi.htmlBody) Then mi.htmlBody = InjectBannerIntoHtml(mi.htmlBody, bannerHtml) SafeSave mi End If Else If InStr(mi.body, bannerText) = 0 Then mi.body = bannerText & vbCrLf & String$(Len(bannerText), "=") & vbCrLf & vbCrLf & mi.body SafeSave mi End If End If End If LogMismatch mi, actualAddr, actualDom, shownDom, originDom, mismatch Exit Function FailSafe: MsgBox "ERROR: " & err.Description, vbCritical + vbOKOnly, "ERROR" Resume Next End Function Private Function GetHeaderRaw(ByVal mi As Outlook.mailItem) As String On Error Resume Next Dim pa As Outlook.PropertyAccessor ' Property accessor to read headers Set pa = mi.PropertyAccessor Const PR_HEADERS As String = "http://schemas.microsoft.com/mapi/proptag/0x007D001E" GetHeaderRaw = pa.GetProperty(PR_HEADERS) End Function Private Function GetSenderSmtpAddress(ByVal mi As Outlook.mailItem) As String On Error GoTo TryRaw Dim exu As Outlook.exchangeUser ' Exchange user object Dim raw As String ' Raw sender email Dim pa As Outlook.PropertyAccessor ' Property accessor to read SMTP address If Not mi.Sender Is Nothing Then If mi.Sender.AddressEntryUserType = olExchangeUserAddressEntry Or _ mi.Sender.AddressEntryUserType = olExchangeRemoteUserAddressEntry Then Set exu = mi.Sender.GetExchangeUser If Not exu Is Nothing Then GetSenderSmtpAddress = exu.PrimarySmtpAddress Exit Function End If End If End If TryRaw: raw = mi.SenderEmailAddress If InStr(1, raw, "@") > 0 Then GetSenderSmtpAddress = raw Else Const PR_SMTP As String = "http://schemas.microsoft.com/mapi/proptag/0x39FE001E" Set pa = mi.PropertyAccessor On Error Resume Next GetSenderSmtpAddress = pa.GetProperty(PR_SMTP) On Error GoTo 0 End If End Function Private Function GetDomainFromEmail(ByVal address As String) As String Dim p As Long ' Position of "@" character p = InStrRev(address, "@") If p > 0 Then GetDomainFromEmail = Mid$(address, p + 1) End Function Private Function EndsWithDomain(ByVal candidate As String, ByVal rootDomain As String) As Boolean If candidate = rootDomain Then EndsWithDomain = True Exit Function End If If Right$(candidate, Len(rootDomain) + 1) = "." & rootDomain Then EndsWithDomain = True End If End Function Private Function AlreadyTagged(ByVal htmlOrText As String) As Boolean AlreadyTagged = (InStr(1, htmlOrText, WARNING_MARKER, vbTextCompare) > 0) End Function Private Function InjectBannerIntoHtml(ByVal html As String, ByVal bannerHtml As String) As String Dim lower As String ' Lowercase HTML string for matching Dim pos As Long ' Position of <body> tag Dim gt As Long ' Position of body tag's '>' If AlreadyTagged(html) Then InjectBannerIntoHtml = html Exit Function End If lower = LCase$(html) pos = InStr(1, lower, "<body", vbTextCompare) If pos > 0 Then gt = InStr(pos, html, ">") If gt > 0 Then InjectBannerIntoHtml = Left$(html, gt) & bannerHtml & Mid$(html, gt + 1) Exit Function End If End If InjectBannerIntoHtml = bannerHtml & html End Function Private Sub SafeSave(ByVal mi As Outlook.mailItem) On Error Resume Next mi.Save End Sub Private Sub LogMismatch(ByVal mi As Outlook.mailItem, ByVal actualAddr As String, ByVal actualDom As String, _ ByVal shownDom As String, ByVal originDom As String, ByVal didFlag As Boolean) On Error Resume Next Dim fso As FileSystemObject ' For file operations Dim ts0 As TextStream ' For creating file Dim ts As TextStream ' For appending log Set fso = New FileSystemObject If Not fso.FolderExists("C:\temp") Then fso.CreateFolder "C:\temp" If Not fso.FileExists(LOG_FILE) Then Set ts0 = fso.CreateTextFile(LOG_FILE, True, True) ts0.WriteLine "Timestamp,EntryID,Subject,Sender,SenderDomain,ShownDomain,OriginDomain,Decision" ts0.Close End If Set ts = fso.OpenTextFile(LOG_FILE, ForAppending, True, TristateTrue) ts.WriteLine Format$(Now, "yyyy-mm-dd hh:nn:ss") & "," & mi.EntryID & "," & Replace(mi.Subject, ",", " ") & "," & _ actualAddr & "," & actualDom & "," & shownDom & "," & originDom & "," & IIf(didFlag, "FLAG", "OK") ts.Close End Sub Private Function GetOriginatorDomainFromHeader(ByVal headerText As String) As String ' Parses Received headers line-by-line to safely extract the oldest sending domain Dim lines As Variant ' All lines from header Dim line As String ' Current line Dim block As String ' Accumulated Received block Dim receivedBlocks As Collection ' All Received: blocks Dim i As Long ' Line counter Dim hopLine As String ' Selected Received block Dim reFrom As RegExp ' Extract 'from' host Dim reDom As RegExp ' Extract domain from host Dim matches As MatchCollection ' Regex results Dim domain As String ' Final domain Set receivedBlocks = New Collection lines = Split(headerText, vbCrLf) For i = 0 To UBound(lines) line = Trim$(lines(i)) If LCase$(Left(line, 9)) = "received:" Then If Len(block) > 0 Then receivedBlocks.Add block block = line ElseIf Len(block) > 0 Then If line <> "" Then block = block & " " & line End If Next i If Len(block) > 0 Then receivedBlocks.Add block If receivedBlocks.Count = 0 Then Exit Function hopLine = receivedBlocks(receivedBlocks.Count) Set reFrom = New RegExp reFrom.pattern = "from\s+([^\s\(\);]+)" reFrom.IgnoreCase = True Set matches = reFrom.Execute(hopLine) If matches.Count = 0 Then Exit Function domain = matches(0).SubMatches(0) Set reDom = New RegExp reDom.pattern = "([a-z0-9.-]+\.[a-z]{2,})$" reDom.IgnoreCase = True Set matches = reDom.Execute(domain) If matches.Count > 0 Then GetOriginatorDomainFromHeader = LCase$(matches(0).SubMatches(0)) Else GetOriginatorDomainFromHeader = LCase$(domain) End If End Function

Comments

Popular posts from this blog

Revit area plans adding new types and references (Gross and rentable)

Revit CSV file manager for families and re-exporting to a CSV file