Files
GoNetKit/parser/analyzer.go

1133 lines
31 KiB
Go

package parser
import (
"fmt"
"mime"
"net/mail"
"regexp"
"strings"
"time"
"gonetkit/resolver"
)
type Report struct {
// Basic Information
From string
To string
Subject string
Date string
MessageID string
UserAgent string
Priority string
// Authentication
SPFRecord string
SPFPass bool
SPFDetails string
DMARCRecord string
DMARCPass bool
DMARCDetails string
DKIM string
DKIMPass bool
DKIMDetails string
// Routing & Security
Received []string
ReturnPath string
ReplyTo string
Blacklists []string
XMailer string
// Content Analysis
ContentType string
Encoding string
Authentication string
SecurityFlags []string
// Potential Issues
Warnings []string
SecurityScore int // 0-100
DeliveryStatus string
// Sender Identification
EnvelopeSender string // Return-Path
FromDomain string // Domain from From header
SendingServer string // Host/IP from first Received header
// Header details for security analysis
SPFHeader string
DMARCHeader string
DKIMHeader string
// Encryption
Encrypted bool
EncryptionDetail string
// All headers for advanced view
AllHeaders map[string]string
// Enhanced Analysis
SpamScore string
SpamFlags []string
VirusInfo string
ARC []string // ARC chain for forwarded emails
BIMI string // Brand Indicators for Message Identification
ListInfo []string // Mailing list information
AutoReply bool // Auto-reply/vacation message detection
BulkEmail bool // Bulk/marketing email detection
PhishingRisk string // Phishing risk assessment
SpoofingRisk string // Spoofing risk assessment
DeliveryDelay string // Time analysis between hops
GeoLocation string // Geographic analysis of sending servers
Compliance []string // Compliance flags (GDPR, CAN-SPAM, etc.)
ThreadInfo string // Message threading information
Attachments []string // Attachment analysis
URLs []string // URL analysis
SenderRep string // Sender reputation summary
}
func decodeMIMEHeader(encoded string) string {
decoder := &mime.WordDecoder{}
decoded, err := decoder.DecodeHeader(encoded)
if err != nil {
// If decoding fails, return the original string
return encoded
}
return decoded
}
func Analyze(raw string) *Report {
msg, err := mail.ReadMessage(strings.NewReader(raw))
if err != nil {
return &Report{
Warnings: []string{"Failed to parse email headers"},
}
}
h := msg.Header
allHeaders := make(map[string]string)
for k, v := range h {
allHeaders[k] = strings.Join(v, "\n")
}
report := &Report{
From: decodeMIMEHeader(h.Get("From")),
To: decodeMIMEHeader(h.Get("To")),
Subject: decodeMIMEHeader(h.Get("Subject")),
Date: h.Get("Date"),
MessageID: h.Get("Message-ID"),
UserAgent: getUserAgent(h),
Priority: getPriority(h),
ReturnPath: h.Get("Return-Path"),
ReplyTo: h.Get("Reply-To"),
ContentType: h.Get("Content-Type"),
Encoding: h.Get("Content-Transfer-Encoding"),
Received: h["Received"],
XMailer: h.Get("X-Mailer"),
Authentication: h.Get("Authentication-Results"),
AllHeaders: allHeaders,
}
// Extract domain and IP
domain := extractDomain(report.From)
ip := extractSenderIP(report.Received)
// Sender identification
report.EnvelopeSender = report.ReturnPath
report.FromDomain = domain
report.SendingServer = extractSendingServer(report.Received)
// Security Checks
report.SecurityFlags = getSecurityFlags(h)
report.Warnings = analyzeWarnings(h)
// SPF, DMARC, DKIM header details
report.SPFHeader = getFirstHeader(h, []string{"Authentication-Results", "Received-SPF"})
report.DMARCHeader = getFirstHeader(h, []string{"Authentication-Results", "ARC-Authentication-Results"})
report.DKIMHeader = getFirstHeader(h, []string{"DKIM-Signature", "Authentication-Results"})
// SPF Check - first try to parse from Authentication-Results header
report.SPFPass, report.SPFDetails = checkSPFResult(h)
if !report.SPFPass {
// Fallback to DNS lookup if not found in headers
report.SPFRecord, report.SPFPass = resolver.CheckSPF(domain)
if report.SPFDetails == "" {
report.SPFDetails = analyzeSPF(report.SPFRecord)
}
} else {
// If SPF passed in headers, still get the record for display
report.SPFRecord, _ = resolver.CheckSPF(domain)
}
// DMARC Check - first try to parse from Authentication-Results header
report.DMARCPass, report.DMARCDetails = checkDMARCResult(h)
if !report.DMARCPass {
// Fallback to DNS lookup if not found in headers
report.DMARCRecord, report.DMARCPass = resolver.CheckDMARC(domain)
if report.DMARCDetails == "" {
report.DMARCDetails = analyzeDMARC(report.DMARCRecord)
}
} else {
// If DMARC passed in headers, still get the record for display
report.DMARCRecord, _ = resolver.CheckDMARC(domain)
}
// DKIM Check
report.DKIM = h.Get("DKIM-Signature")
report.DKIMPass, report.DKIMDetails = checkDKIMResult(h, report.DKIM)
// Blacklist Check
if ip != "" {
report.Blacklists = resolver.CheckBlacklists(ip)
}
// Encryption analysis
report.Encrypted, report.EncryptionDetail = analyzeEncryption(report.Received)
// Calculate Security Score
report.SecurityScore = calculateSecurityScore(report)
// Analyze Delivery Status
report.DeliveryStatus = analyzeDeliveryStatus(report)
// --- Enhanced Analysis --- //
// Spam detection (simple heuristic based on subject and content)
if strings.Contains(strings.ToLower(report.Subject), "free") || strings.Contains(strings.ToLower(report.Subject), "win") {
report.SpamScore = "High"
report.SpamFlags = append(report.SpamFlags, "Contains common spam keywords")
} else {
report.SpamScore = "Low"
}
// Virus scanning (placeholder for integration with virus scanning service)
if strings.Contains(report.Authentication, "virus") {
report.VirusInfo = "Virus detected in the message"
} else {
report.VirusInfo = "No virus detected"
}
// Enhanced Analysis
report.SpamScore = extractSpamScore(h)
report.SpamFlags = extractSpamFlags(h)
report.VirusInfo = extractVirusInfo(h)
report.ARC = extractARCInfo(h)
report.ListInfo = extractListInfo(h)
report.AutoReply = detectAutoReply(h)
report.BulkEmail = detectBulkEmail(h)
report.PhishingRisk = assessPhishingRisk(report)
report.SpoofingRisk = assessSpoofingRisk(report)
report.DeliveryDelay = analyzeDeliveryDelay(report.Received)
report.Compliance = extractComplianceFlags(h)
report.ThreadInfo = extractThreadInfo(h)
report.Attachments = extractAttachmentInfo(h)
report.URLs = extractURLInfo(h)
// BIMI check (placeholder for now)
report.BIMI = h.Get("BIMI-Location")
if report.BIMI == "" {
report.BIMI = "No BIMI record found"
}
// Geo location (enhanced analysis)
if ip != "" {
geoInfo := analyzeIPGeography(ip, h)
report.GeoLocation = geoInfo
}
// Sender reputation (simplified for now)
report.SenderRep = assessSenderReputation(report)
return report
}
func getUserAgent(h mail.Header) string {
ua := h.Get("User-Agent")
if ua == "" {
ua = h.Get("X-Mailer")
}
return ua
}
func getPriority(h mail.Header) string {
priority := h.Get("X-Priority")
if priority == "" {
priority = h.Get("Importance")
}
return priority
}
func getSecurityFlags(h mail.Header) []string {
var flags []string
if h.Get("X-Spam-Flag") != "" {
flags = append(flags, "Spam Flag Present")
}
if h.Get("X-Virus-Scanned") != "" {
flags = append(flags, "Virus Scanned")
}
return flags
}
func analyzeWarnings(h mail.Header) []string {
var warnings []string
if h.Get("From") != h.Get("Reply-To") && h.Get("Reply-To") != "" {
warnings = append(warnings, "Reply-To address differs from From address")
}
if h.Get("X-Spam-Score") != "" {
warnings = append(warnings, "Message was flagged by spam filters")
}
return warnings
}
func analyzeSPF(record string) string {
if record == "" {
return "No SPF record found - This may cause delivery issues"
}
if strings.Contains(record, "~all") {
return "Soft fail configuration - Some non-authorized servers may send mail"
}
if strings.Contains(record, "-all") {
return "Strict configuration - Only authorized servers can send mail"
}
return "SPF record present but not strict"
}
func analyzeDMARC(record string) string {
if record == "" {
return "No DMARC record found - This may affect deliverability"
}
if strings.Contains(record, "p=none") {
return "Monitor-only mode - No enforcement"
}
if strings.Contains(record, "p=quarantine") {
return "Suspicious messages may be quarantined"
}
if strings.Contains(record, "p=reject") {
return "Strict enforcement - Failed messages are rejected"
}
return "DMARC record present"
}
func checkSPFResult(h mail.Header) (bool, string) {
// Try to parse Authentication-Results for SPF result
ar := h.Get("Authentication-Results")
if ar != "" {
// Split by semicolon and look for spf= entries
arParts := strings.Split(ar, ";")
for _, part := range arParts {
part = strings.TrimSpace(part)
if strings.HasPrefix(strings.ToLower(part), "spf=") {
// Example: spf=pass reason=mailfrom (ip=205.220.166.231, headerfrom=marshcommercial.co.uk)
if strings.Contains(strings.ToLower(part), "spf=pass") {
return true, "SPF passed according to Authentication-Results: " + part
}
if strings.Contains(strings.ToLower(part), "spf=fail") {
return false, "SPF failed according to Authentication-Results: " + part
}
if strings.Contains(strings.ToLower(part), "spf=neutral") {
return false, "SPF neutral according to Authentication-Results: " + part
}
if strings.Contains(strings.ToLower(part), "spf=softfail") {
return false, "SPF soft fail according to Authentication-Results: " + part
}
if strings.Contains(strings.ToLower(part), "spf=temperror") {
return false, "SPF temporary error according to Authentication-Results: " + part
}
if strings.Contains(strings.ToLower(part), "spf=permerror") {
return false, "SPF permanent error according to Authentication-Results: " + part
}
// If we found spf= but no recognized result, return the raw text
return false, "SPF result found but not recognized: " + part
}
}
}
// Also check Received-SPF header as fallback
receivedSPF := h.Get("Received-SPF")
if receivedSPF != "" {
if strings.Contains(strings.ToLower(receivedSPF), "pass") {
return true, "SPF passed according to Received-SPF: " + receivedSPF
}
if strings.Contains(strings.ToLower(receivedSPF), "fail") {
return false, "SPF failed according to Received-SPF: " + receivedSPF
}
}
return false, ""
}
func checkDMARCResult(h mail.Header) (bool, string) {
// Try to parse Authentication-Results for DMARC result
ar := h.Get("Authentication-Results")
if ar != "" {
// Split by semicolon and look for dmarc= entries
arParts := strings.Split(ar, ";")
for _, part := range arParts {
part = strings.TrimSpace(part)
if strings.HasPrefix(strings.ToLower(part), "dmarc=") {
// Example: dmarc=pass hse.action=pass header.from=marshcommercial.co.uk
if strings.Contains(strings.ToLower(part), "dmarc=pass") {
return true, "DMARC passed according to Authentication-Results: " + part
}
if strings.Contains(strings.ToLower(part), "dmarc=fail") {
return false, "DMARC failed according to Authentication-Results: " + part
}
if strings.Contains(strings.ToLower(part), "dmarc=temperror") {
return false, "DMARC temporary error according to Authentication-Results: " + part
}
if strings.Contains(strings.ToLower(part), "dmarc=permerror") {
return false, "DMARC permanent error according to Authentication-Results: " + part
}
// If we found dmarc= but no recognized result, return the raw text
return false, "DMARC result found but not recognized: " + part
}
}
}
return false, ""
}
func checkDKIMResult(h mail.Header, dkimHeader string) (bool, string) {
// Try to parse Authentication-Results for DKIM result
ar := h.Get("Authentication-Results")
if ar != "" {
arLines := strings.Split(ar, ";")
for _, line := range arLines {
line = strings.TrimSpace(line)
if strings.HasPrefix(strings.ToLower(line), "dkim=") {
// Example: dkim=pass (signature was verified) header.d=example.com;
if strings.Contains(line, "pass") {
return true, line
}
if strings.Contains(line, "fail") {
return false, line
}
if strings.Contains(line, "neutral") {
return false, line
}
}
}
}
// Fallback: if DKIM header exists, assume present but not verified
if dkimHeader != "" {
return true, "DKIM-Signature header present, but no Authentication-Results found"
}
return false, "No DKIM signature found"
}
func calculateSecurityScore(r *Report) int {
score := 0
// Base score from authentication
if r.SPFPass {
score += 20
}
if r.DMARCPass {
score += 20
}
if r.DKIMPass {
score += 20
}
// Penalty for blacklists
score -= len(r.Blacklists) * 15
// Bonus for security features
if len(r.SecurityFlags) > 0 {
score += 10
}
// Penalty for warnings
score -= len(r.Warnings) * 5
// Ensure score stays within 0-100
if score < 0 {
score = 0
}
if score > 100 {
score = 100
}
return score
}
func analyzeDeliveryStatus(r *Report) string {
if len(r.Blacklists) > 0 {
return "High Risk - Listed on blacklists"
}
if r.SecurityScore >= 80 {
return "Excellent - Should deliver reliably"
} else if r.SecurityScore >= 60 {
return "Good - May have minor delivery issues"
} else if r.SecurityScore >= 40 {
return "Fair - Could face delivery problems"
} else {
return "Poor - Likely to have delivery issues"
}
}
func extractDomain(from string) string {
r := regexp.MustCompile(`@([a-zA-Z0-9.-]+)`)
m := r.FindStringSubmatch(from)
if len(m) >= 2 {
return m[1]
}
return ""
}
func extractSenderIP(received []string) string {
r := regexp.MustCompile(`\b(\d{1,3}\.){3}\d{1,3}\b`)
for _, line := range received {
m := r.FindString(line)
if m != "" && !strings.HasPrefix(m, "127.") {
return m
}
}
return ""
}
func extractSendingServer(received []string) string {
if len(received) > 0 {
r := regexp.MustCompile(`from\s+([^\s]+)`)
m := r.FindStringSubmatch(received[0])
if len(m) > 1 {
return m[1]
}
}
return ""
}
func getFirstHeader(h mail.Header, keys []string) string {
for _, k := range keys {
if v := h.Get(k); v != "" {
return v
}
}
return ""
}
func analyzeEncryption(received []string) (bool, string) {
for _, line := range received {
if strings.Contains(strings.ToLower(line), "tls") || strings.Contains(line, "ESMTPS") || strings.Contains(line, "with ESMTPS") || strings.Contains(line, "with TLS") {
return true, line
}
}
return false, "No evidence of encryption (TLS) found in Received headers"
}
func extractTimeFromReceived(received string) *time.Time {
// Try to extract timestamp from Received header
// Format 1: "Tue, 15 Jul 2025 14:47:49 +0200"
re1 := regexp.MustCompile(`;\s*([A-Za-z]{3},\s*\d{1,2}\s+[A-Za-z]{3}\s+\d{4}\s+\d{2}:\d{2}:\d{2}\s*[+-]\d{4})`)
matches := re1.FindStringSubmatch(received)
if len(matches) > 1 {
t, err := time.Parse("Mon, 2 Jan 2006 15:04:05 -0700", matches[1])
if err == nil {
return &t
}
}
// Format 2: "Tue, 15 Jul 2025 12:47:33 GMT"
re2 := regexp.MustCompile(`;\s*([A-Za-z]{3},\s*\d{1,2}\s+[A-Za-z]{3}\s+\d{4}\s+\d{2}:\d{2}:\d{2}\s+GMT)`)
matches = re2.FindStringSubmatch(received)
if len(matches) > 1 {
t, err := time.Parse("Mon, 2 Jan 2006 15:04:05 GMT", matches[1])
if err == nil {
return &t
}
}
// Format 3: Extract any timestamp pattern at the end of line
re3 := regexp.MustCompile(`(\d{1,2}\s+[A-Za-z]{3}\s+\d{4}\s+\d{2}:\d{2}:\d{2})`)
matches = re3.FindStringSubmatch(received)
if len(matches) > 1 {
t, err := time.Parse("2 Jan 2006 15:04:05", matches[1])
if err == nil {
return &t
}
}
// Format 4: Try with day name prefix
re4 := regexp.MustCompile(`([A-Za-z]{3},?\s*\d{1,2}\s+[A-Za-z]{3}\s+\d{4}\s+\d{2}:\d{2}:\d{2})`)
matches = re4.FindStringSubmatch(received)
if len(matches) > 1 {
// Try different formats
formats := []string{
"Mon, 2 Jan 2006 15:04:05",
"Mon 2 Jan 2006 15:04:05",
"2 Jan 2006 15:04:05",
}
for _, format := range formats {
t, err := time.Parse(format, matches[1])
if err == nil {
return &t
}
}
}
return nil
}
// Enhanced Analysis Functions
func extractSpamScore(h mail.Header) string {
// Check various spam score headers
if score := h.Get("X-Spam-Score"); score != "" {
return "X-Spam-Score: " + score
}
if score := h.Get("X-SpamAssassin-Score"); score != "" {
return "SpamAssassin Score: " + score
}
if score := h.Get("X-Microsoft-Antispam-Mailbox-Delivery"); score != "" {
return "Microsoft Antispam: " + score
}
if score := h.Get("X-Barracuda-Spam-Score"); score != "" {
return "Barracuda Score: " + score
}
// AntiSpamEurope specific (from your example)
if reason := h.Get("X-antispameurope-REASON"); reason != "" {
status := h.Get("X-antispameurope-Spamstatus")
if status != "" {
return fmt.Sprintf("AntiSpamEurope: %s (%s)", status, reason)
}
return "AntiSpamEurope: " + reason
}
// Hornetsecurity specific
if h.Get("X-hornetsecurity-identifier") != "" {
return "Hornetsecurity Security Gateway processed"
}
return "No spam score found"
}
func extractSpamFlags(h mail.Header) []string {
var flags []string
// Standard spam flags
if h.Get("X-Spam-Flag") == "YES" {
flags = append(flags, "Marked as Spam")
}
if h.Get("X-Spam-Status") != "" {
flags = append(flags, "Spam Status: "+h.Get("X-Spam-Status"))
}
if h.Get("X-Spam-Level") != "" {
flags = append(flags, "Spam Level: "+h.Get("X-Spam-Level"))
}
// Microsoft-specific
if h.Get("X-MS-Exchange-Organization-SCL") != "" {
flags = append(flags, "Exchange SCL: "+h.Get("X-MS-Exchange-Organization-SCL"))
}
if h.Get("X-Microsoft-Antispam") != "" {
flags = append(flags, "Microsoft Antispam detected")
}
// Google-specific
if h.Get("X-Gm-Message-State") != "" {
flags = append(flags, "Gmail State: "+h.Get("X-Gm-Message-State"))
}
// Hornetsecurity/AntiSpamEurope specific (from your example)
if h.Get("X-antispameurope-Spamstatus") != "" {
flags = append(flags, "AntiSpamEurope Status: "+h.Get("X-antispameurope-Spamstatus"))
}
if h.Get("X-antispameurope-REASON") != "" {
flags = append(flags, "AntiSpamEurope Reason: "+h.Get("X-antispameurope-REASON"))
}
if h.Get("X-antispameurope-Virusscan") != "" {
flags = append(flags, "AntiSpamEurope Virus Scan: "+h.Get("X-antispameurope-Virusscan"))
}
// Generic detection
if h.Get("X-Quarantine-ID") != "" {
flags = append(flags, "Message was quarantined")
}
return flags
}
func extractVirusInfo(h mail.Header) string {
if scan := h.Get("X-Virus-Scanned"); scan != "" {
status := "Virus Scanned: " + scan
if clean := h.Get("X-Virus-Status"); clean != "" {
status += " | Status: " + clean
}
return status
}
if clam := h.Get("X-Clam-Scanned"); clam != "" {
return "ClamAV Scanned: " + clam
}
if mcafee := h.Get("X-McAfee-Virus-Scanned"); mcafee != "" {
return "McAfee Scanned: " + mcafee
}
return "No virus scanning information found"
}
func extractListInfo(h mail.Header) []string {
var info []string
if listId := h.Get("List-ID"); listId != "" {
info = append(info, "List-ID: "+listId)
}
if listPost := h.Get("List-Post"); listPost != "" {
info = append(info, "List-Post: "+listPost)
}
if listUnsubscribe := h.Get("List-Unsubscribe"); listUnsubscribe != "" {
info = append(info, "Unsubscribe: "+listUnsubscribe)
}
if precedence := h.Get("Precedence"); precedence != "" {
info = append(info, "Precedence: "+precedence)
}
return info
}
func detectAutoReply(h mail.Header) bool {
// Check for auto-reply indicators
if h.Get("X-Autoreply") != "" || h.Get("Auto-Submitted") != "" {
return true
}
if h.Get("X-Auto-Response-Suppress") != "" {
return true
}
if strings.Contains(strings.ToLower(h.Get("Subject")), "out of office") {
return true
}
if strings.Contains(strings.ToLower(h.Get("Subject")), "automatic reply") {
return true
}
return false
}
func detectBulkEmail(h mail.Header) bool {
// Check for bulk email indicators
if h.Get("Precedence") == "bulk" || h.Get("Precedence") == "junk" {
return true
}
if h.Get("X-Bulk") != "" || h.Get("X-Campaign-ID") != "" {
return true
}
if h.Get("List-Unsubscribe") != "" {
return true
}
if strings.Contains(strings.ToLower(h.Get("X-Mailer")), "mailchimp") ||
strings.Contains(strings.ToLower(h.Get("X-Mailer")), "constant contact") {
return true
}
return false
}
func assessPhishingRisk(r *Report) string {
risk := 0
reasons := []string{}
// Check for domain mismatches
if r.EnvelopeSender != "" && r.From != "" {
envDomain := extractDomain(r.EnvelopeSender)
fromDomain := extractDomain(r.From)
if envDomain != "" && fromDomain != "" && envDomain != fromDomain {
risk += 30
reasons = append(reasons, "Envelope and From domain mismatch")
}
}
// Check for failed authentication
if !r.SPFPass {
risk += 20
reasons = append(reasons, "SPF failed")
}
if !r.DMARCPass {
risk += 20
reasons = append(reasons, "DMARC failed")
}
if !r.DKIMPass {
risk += 15
reasons = append(reasons, "DKIM missing/failed")
}
// Check for suspicious reply-to
if r.ReplyTo != "" && r.ReplyTo != r.From {
risk += 10
reasons = append(reasons, "Reply-To differs from From")
}
// Assess risk level
if risk >= 50 {
return "HIGH RISK: " + strings.Join(reasons, ", ")
} else if risk >= 25 {
return "MEDIUM RISK: " + strings.Join(reasons, ", ")
} else if risk > 0 {
return "LOW RISK: " + strings.Join(reasons, ", ")
}
return "LOW RISK: No significant phishing indicators detected"
}
func assessSpoofingRisk(r *Report) string {
risks := []string{}
// Check for display name spoofing
if strings.Contains(r.From, "<") && strings.Contains(r.From, ">") {
// Extract display name and email
re := regexp.MustCompile(`^([^<]+)<([^>]+)>`)
matches := re.FindStringSubmatch(r.From)
if len(matches) == 3 {
displayName := strings.TrimSpace(matches[1])
email := strings.TrimSpace(matches[2])
// Check if display name looks like it's trying to spoof another domain
if strings.Contains(strings.ToLower(displayName), "@") {
risks = append(risks, "Display name contains @ symbol")
}
// Check for common spoofing patterns
commonDomains := []string{"gmail", "yahoo", "outlook", "hotmail", "microsoft", "apple", "amazon", "paypal", "ebay"}
for _, domain := range commonDomains {
if strings.Contains(strings.ToLower(displayName), domain) &&
!strings.Contains(strings.ToLower(email), domain) {
risks = append(risks, "Display name suggests "+domain+" but email domain differs")
}
}
}
}
// Check for authentication failures
if !r.SPFPass || !r.DMARCPass {
risks = append(risks, "Authentication failures increase spoofing risk")
}
if len(risks) > 0 {
return "POTENTIAL SPOOFING: " + strings.Join(risks, ", ")
}
return "No obvious spoofing indicators detected"
}
func analyzeDeliveryDelay(received []string) string {
if len(received) < 2 {
return "Insufficient data for delay analysis (need at least 2 Received headers)"
}
// Try to extract timestamps from received headers
var timestamps []struct {
time time.Time
index int
}
for i, header := range received {
if t := extractTimeFromReceived(header); t != nil {
timestamps = append(timestamps, struct {
time time.Time
index int
}{*t, i})
}
}
if len(timestamps) < 2 {
return "Could not extract enough timestamps for delay analysis"
}
// Calculate delays between consecutive hops
var delays []string
totalDelay := time.Duration(0)
// Sort timestamps by index (received headers are in reverse chronological order)
for i := 0; i < len(timestamps)-1; i++ {
// Since Received headers are added in reverse order, we calculate from later to earlier
var delay time.Duration
if timestamps[i].index < timestamps[i+1].index {
delay = timestamps[i].time.Sub(timestamps[i+1].time)
} else {
delay = timestamps[i+1].time.Sub(timestamps[i].time)
}
if delay > 0 {
delays = append(delays, fmt.Sprintf("Hop %d→%d: %v", timestamps[i+1].index+1, timestamps[i].index+1, delay))
totalDelay += delay
}
}
if len(delays) == 0 {
return "Could not calculate meaningful delivery delays"
}
result := fmt.Sprintf("Total delivery time: %v | ", totalDelay)
result += strings.Join(delays, ", ")
// Add analysis
if totalDelay > 24*time.Hour {
result += " | ⚠️ Unusually long delivery time"
} else if totalDelay > 1*time.Hour {
result += " | ⚠️ Slow delivery"
} else if totalDelay < 1*time.Minute {
result += " | ✅ Very fast delivery"
} else {
result += " | ✅ Normal delivery time"
}
return result
}
func extractComplianceFlags(h mail.Header) []string {
var flags []string
// CAN-SPAM compliance
if h.Get("List-Unsubscribe") != "" {
flags = append(flags, "CAN-SPAM: Unsubscribe link provided")
}
// GDPR indicators
if strings.Contains(strings.ToLower(h.Get("Subject")), "gdpr") ||
strings.Contains(strings.ToLower(h.Get("Subject")), "privacy policy") {
flags = append(flags, "GDPR: Privacy-related content detected")
}
// Marketing compliance
if h.Get("X-Campaign-ID") != "" {
flags = append(flags, "Marketing: Campaign tracking detected")
}
// Auto-suppression compliance
if h.Get("X-Auto-Response-Suppress") != "" {
flags = append(flags, "Auto-response suppression headers present")
}
return flags
}
func extractThreadInfo(h mail.Header) string {
var info []string
if threadIndex := h.Get("Thread-Index"); threadIndex != "" {
info = append(info, "Thread-Index: "+threadIndex)
}
if threadTopic := h.Get("Thread-Topic"); threadTopic != "" {
info = append(info, "Thread-Topic: "+threadTopic)
}
if inReplyTo := h.Get("In-Reply-To"); inReplyTo != "" {
info = append(info, "In-Reply-To: "+inReplyTo)
}
if references := h.Get("References"); references != "" {
info = append(info, "References: "+references)
}
if len(info) == 0 {
return "No threading information available"
}
return strings.Join(info, " | ")
}
func extractAttachmentInfo(h mail.Header) []string {
var attachments []string
// Look for attachment indicators in headers
if disposition := h.Get("Content-Disposition"); strings.Contains(strings.ToLower(disposition), "attachment") {
attachments = append(attachments, "Content-Disposition indicates attachment")
}
// Check for common attachment-related headers
if h.Get("X-Attachment-Id") != "" {
attachments = append(attachments, "Attachment ID present")
}
// Look for MIME boundaries which might indicate attachments
if contentType := h.Get("Content-Type"); strings.Contains(strings.ToLower(contentType), "multipart") {
attachments = append(attachments, "Multipart content (may contain attachments)")
}
return attachments
}
func extractURLInfo(h mail.Header) []string {
var urls []string
// Look for URL-related headers
if h.Get("X-Originating-URL") != "" {
urls = append(urls, "Originating URL: "+h.Get("X-Originating-URL"))
}
// Check for tracking URLs in headers
if h.Get("X-Campaign-ID") != "" {
urls = append(urls, "Campaign tracking detected")
}
if h.Get("List-Unsubscribe") != "" {
urls = append(urls, "Unsubscribe URL present")
}
return urls
}
func assessSenderReputation(r *Report) string {
score := 0
factors := []string{}
// Positive factors
if r.SPFPass {
score += 25
factors = append(factors, "+SPF")
}
if r.DMARCPass {
score += 25
factors = append(factors, "+DMARC")
}
if r.DKIMPass {
score += 20
factors = append(factors, "+DKIM")
}
if r.Encrypted {
score += 10
factors = append(factors, "+TLS")
}
// Negative factors
if len(r.Blacklists) > 0 {
score -= 50
factors = append(factors, "-Blacklisted")
}
if len(r.SpamFlags) > 0 {
score -= 20
factors = append(factors, "-Spam flags")
}
if r.PhishingRisk != "" && strings.HasPrefix(r.PhishingRisk, "HIGH") {
score -= 30
factors = append(factors, "-High phishing risk")
}
// Ensure score stays within bounds
if score < 0 {
score = 0
}
if score > 100 {
score = 100
}
reputation := ""
if score >= 80 {
reputation = "EXCELLENT"
} else if score >= 60 {
reputation = "GOOD"
} else if score >= 40 {
reputation = "FAIR"
} else if score >= 20 {
reputation = "POOR"
} else {
reputation = "VERY POOR"
}
return fmt.Sprintf("%s (%d/100) - Factors: %s", reputation, score, strings.Join(factors, ", "))
}
func extractARCInfo(h mail.Header) []string {
var arc []string
// Collect all ARC-related headers
if arcAuth := h.Get("ARC-Authentication-Results"); arcAuth != "" {
arc = append(arc, "ARC-Authentication-Results: "+arcAuth)
}
if arcSig := h.Get("ARC-Message-Signature"); arcSig != "" {
arc = append(arc, "ARC-Message-Signature: "+arcSig)
}
if arcSeal := h.Get("ARC-Seal"); arcSeal != "" {
arc = append(arc, "ARC-Seal: "+arcSeal)
}
// If no specific ARC headers, check for multiple ARC headers with instance numbers
for key, values := range h {
if strings.HasPrefix(key, "ARC-") {
for _, value := range values {
arcEntry := key + ": " + value
// Avoid duplicates
found := false
for _, existing := range arc {
if strings.Contains(existing, value) {
found = true
break
}
}
if !found {
arc = append(arc, arcEntry)
}
}
}
}
return arc
}
func analyzeIPGeography(ip string, h mail.Header) string {
var info []string
info = append(info, "IP: "+ip)
// Extract server information from headers
if mailgunIP := h.Get("X-Mailgun-Sending-Ip"); mailgunIP != "" && mailgunIP == ip {
info = append(info, "Service: Mailgun Email Service")
}
// Look for hostname clues in Received headers
for _, received := range h["Received"] {
if strings.Contains(received, ip) {
// Extract hostname from received header
re := regexp.MustCompile(`from\s+([^\s\[\(]+)`)
matches := re.FindStringSubmatch(received)
if len(matches) > 1 {
hostname := matches[1]
info = append(info, "Hostname: "+hostname)
// Analyze hostname for geographic/service clues
hostname = strings.ToLower(hostname)
if strings.Contains(hostname, ".eu.") || strings.Contains(hostname, "europe") {
info = append(info, "Region: Europe (based on hostname)")
}
if strings.Contains(hostname, ".us.") || strings.Contains(hostname, "america") {
info = append(info, "Region: Americas (based on hostname)")
}
if strings.Contains(hostname, ".asia.") || strings.Contains(hostname, "asia") {
info = append(info, "Region: Asia (based on hostname)")
}
// Service detection
if strings.Contains(hostname, "mailgun") {
info = append(info, "Service: Mailgun Email Service")
}
if strings.Contains(hostname, "sendgrid") {
info = append(info, "Service: SendGrid Email Service")
}
if strings.Contains(hostname, "amazonses") || strings.Contains(hostname, "aws") {
info = append(info, "Service: Amazon SES")
}
if strings.Contains(hostname, "outlook") || strings.Contains(hostname, "microsoft") {
info = append(info, "Service: Microsoft Exchange/Outlook")
}
if strings.Contains(hostname, "google") || strings.Contains(hostname, "gmail") {
info = append(info, "Service: Google/Gmail")
}
break
}
}
}
// IP range analysis (basic)
if strings.HasPrefix(ip, "10.") || strings.HasPrefix(ip, "192.168.") || strings.HasPrefix(ip, "172.") {
info = append(info, "Type: Private/Internal IP")
} else {
info = append(info, "Type: Public IP")
// Basic geographic hints from IP ranges (very basic)
if strings.HasPrefix(ip, "161.38.") {
info = append(info, "ISP: Likely European hosting provider")
}
}
info = append(info, "(Full geographic lookup requires external GeoIP service)")
return strings.Join(info, " | ")
}