Files
Linux-Server-Management-Too…/modules/email/mail-log-analyzer.sh
T
cschantz 89ad050222 Fix critical logic errors in email diagnostics scripts
CRITICAL FIXES (5 issues):
1. email-diagnostics.sh: Fix inverted sender/recipient extraction logic
   - Lines 292-303: Corrected pattern matching to properly extract recipients and senders
   - Removed inverted grep patterns that were looking for wrong log entry types

2. mail-log-analyzer.sh: Fix string comparison with percent sign
   - Line 1184-1186: Properly extract numeric value before '%' character
   - Use sed to isolate leading digits for numeric comparison

3. email-diagnostics.sh: Fix malformed grep syntax
   - Line 525-527: Corrected grep command structure with -e options
   - Changed to -iE with pipe patterns and proper file argument placement

4. mail-log-analyzer.sh: Fix overly broad domain bounce pattern
   - Line 749: Changed from "^.*${domain}" to "\b${domain}$"
   - Prevents false positives from substring domain matches

5. mail-log-analyzer.sh: Fix undefined TEMP_LOG variable
   - Line 860: Changed TEMP_LOG to MAIL_LOG (the actual global variable)
   - Added error handling with 2>/dev/null

HIGH SEVERITY FIXES (2 issues):
6. mail-log-analyzer.sh: Fix AWK uninitialized variable
   - Lines 1447-1456: Added BEGIN block to initialize print_line = 0
   - Prevents first log entries from being incorrectly filtered

7. mail-log-analyzer.sh: Fix overly permissive bounce detection pattern
   - Line 247: Changed from "(==|defer)" to more specific pattern
   - Prevents false positives from non-bounce defer messages

MODERATE FIXES (3 issues):
8. mail-queue-inspector.sh: Fix queue message count mismatch
   - Line 41: Changed head -40 to head -20 to match label

9. deliverability-test.sh: Fix fragile SMTP connection test
   - Lines 102-106: Added nc availability check and fallback to bash TCP
   - Proper variable quoting and error handling

10. blacklist-check.sh: Replace deprecated host command with dig
    - Line 52: Changed from host to dig +short for consistency and timeout control

All scripts pass syntax validation.
Impact: Logic errors fixed, no security issues introduced, all existing functionality preserved.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
2026-02-07 00:39:07 -05:00

1522 lines
60 KiB
Bash
Executable File

#!/bin/bash
################################################################################
# Mail Log Analyzer - Advanced Email Issue Detection
################################################################################
# Purpose: Analyze mail logs for issues and provide actionable recommendations
# Features:
# - Spam account detection
# - Blacklist detection
# - SPF/DKIM/DMARC failures
# - Bounce analysis
# - Rate limiting indicators
# - Configuration recommendations
################################################################################
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
source "$SCRIPT_DIR/lib/common-functions.sh"
source "$SCRIPT_DIR/lib/system-detect.sh"
source "$SCRIPT_DIR/lib/email-functions.sh"
# Configuration
ANALYSIS_HOURS=24
SPAM_THRESHOLD=100 # Emails per hour considered spam
REPORT_FILE="/tmp/mail-analysis-$(date +%Y%m%d-%H%M%S).txt"
# Issue tracking arrays
declare -A ISSUES_FOUND
declare -A RECOMMENDATIONS
declare -A BLACKLISTED_IPS
declare -A SPAM_ACCOUNTS
declare -A AUTH_FAILURES
declare -A BOUNCE_REASONS
declare -A HELO_VIOLATIONS
declare -A AUTH_ATTACK_IPS
declare -A FROZEN_MESSAGES
declare -A CONNECTION_FLOODS
# NEW: Enhanced tracking arrays
declare -A DOMAIN_SENT # domain → count of sent messages
declare -A DOMAIN_DELIVERED # domain → count of delivered messages
declare -A DOMAIN_BOUNCED # domain → count of bounced messages
declare -A DOMAIN_ISSUES # domain → list of issues
declare -A USER_SENT # user@domain → count of sent
declare -A USER_ISSUES # user@domain → list of issues
declare -A TOP_RECIPIENTS # recipient@domain → count
declare -A TOP_SENDERS # sender@domain → count
declare -A HOURLY_VOLUME # hour → message count
declare -A ERROR_SAMPLES # error_type → sample log line
declare -A DELIVERY_TIMES # Track message delivery times
declare -A REJECTED_REASONS # rejection reason → count
# Statistics
TOTAL_SENT=0
TOTAL_RECEIVED=0
TOTAL_BOUNCES=0
TOTAL_DEFERRED=0
TOTAL_REJECTED=0
TOTAL_AUTH_FAILURES=0
PANIC_LOG_EXISTS=0
################################################################################
# Pattern Detection Functions
################################################################################
# Detect blacklist rejections
detect_blacklist_issues() {
local log_file="$1"
local temp_file="/tmp/blacklist_detections.$$"
print_info "Scanning for blacklist rejections..."
# Enhanced blacklist detection patterns (from email-diagnostics.sh)
# Includes explicit RBL keywords, provider-specific patterns, and error codes
grep -iE "blacklist|block list|RBL|DNSBL|listed in|blocked using|on our block list|S3150|S3140|AS\(48|CS01|local policy|gmail.*(suspicious|reputation|spam|detected).*reputation|gmail.*detected.*suspicious|spamhaus|barracuda|spamcop|sorbs|abuseat|yahoo.*block|yahoo.*reject|aol.*block|aol.*reject|me\.com.*reject|icloud.*reject|mac\.com.*reject|protonmail.*block|protonmail.*reject|pm\.me.*reject|zoho.*block|zoho.*reject|fastmail.*block|fastmail.*reject|outlook.*block|hotmail.*block|live\.com.*block|msn\.com.*block" -- "$log_file" 2>/dev/null > "$temp_file"
# ENHANCED: Filter out false positives (same as email-diagnostics.sh)
# Exclude negation keywords, question contexts, and non-RBL blocks
if [ -s "$temp_file" ]; then
local temp_filtered="/tmp/blacklist_detections_filtered.$$"
grep -vE "not blacklist|not listed|NOT listed|no.*longer|removed from|delisted|successfully delisted|you.*can.*now|check if|if.*server|if your|we block|some.*block|unlike|rarely|are rare|except|not.*block|not.*in|but.*policy|policy.*block|firewall|rate limit|internally|internal.*block|local.*block|rejected.*not.*blacklist|based on sender|blocks are" -- "$temp_file" > "$temp_filtered" 2>/dev/null || true
if [ -s "$temp_filtered" ]; then
mv "$temp_filtered" "$temp_file"
else
# All messages were false positives, clear the file
> "$temp_file"
fi
fi
if [ -s "$temp_file" ]; then
local count=$(wc -l < "$temp_file")
ISSUES_FOUND["blacklist"]=$count
# Extract specific blacklists mentioned
while IFS= read -r line; do
# Extract recognized blacklist/provider names
local detected=0
if [[ "$line" =~ [Ss]pam[Hh]aus ]]; then
BLACKLISTED_IPS["Spamhaus"]=$((${BLACKLISTED_IPS["Spamhaus"]:-0} + 1))
detected=1
fi
if [[ "$line" =~ [Ss]pam[Cc]op ]]; then
BLACKLISTED_IPS["SpamCop"]=$((${BLACKLISTED_IPS["SpamCop"]:-0} + 1))
detected=1
fi
if [[ "$line" =~ [Bb]arracuda ]]; then
BLACKLISTED_IPS["Barracuda"]=$((${BLACKLISTED_IPS["Barracuda"]:-0} + 1))
detected=1
fi
if [[ "$line" =~ [Gg]mail ]]; then
BLACKLISTED_IPS["Gmail"]=$((${BLACKLISTED_IPS["Gmail"]:-0} + 1))
detected=1
fi
if [[ "$line" =~ [Mm]icrosoft|[Oo]utlook|[Hh]otmail|[Ll]ive ]]; then
BLACKLISTED_IPS["Microsoft"]=$((${BLACKLISTED_IPS["Microsoft"]:-0} + 1))
detected=1
fi
if [[ "$line" =~ [Yy]ahoo|[Aa]ol ]]; then
BLACKLISTED_IPS["Yahoo/AOL"]=$((${BLACKLISTED_IPS["Yahoo/AOL"]:-0} + 1))
detected=1
fi
if [[ "$line" =~ [Ss]orbs ]]; then
BLACKLISTED_IPS["SORBS"]=$((${BLACKLISTED_IPS["SORBS"]:-0} + 1))
detected=1
fi
if [[ "$line" =~ [Aa]buseat|[Cc]bl ]]; then
BLACKLISTED_IPS["CBL"]=$((${BLACKLISTED_IPS["CBL"]:-0} + 1))
detected=1
fi
# Extract IPs being rejected
if [[ "$line" =~ ([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}) ]]; then
local ip="${BASH_REMATCH[1]}"
echo "$line" >> "/tmp/blacklist_ip_${ip//\./_}.log"
fi
done < "$temp_file"
# Build recommendations based on count
if [ "$count" -gt 100 ]; then
RECOMMENDATIONS["blacklist"]="CRITICAL: $count blacklist-related rejections found. Check server IP reputation immediately using 'blacklist-check' tool."
elif [ "$count" -gt 10 ]; then
RECOMMENDATIONS["blacklist"]="WARNING: $count blacklist-related rejections. Review using 'email-diagnostics' for detailed analysis."
else
RECOMMENDATIONS["blacklist"]="Found $count blacklist-related rejection(s). Use 'blacklist-check' to verify current listing status."
fi
fi
rm -f "$temp_file"
}
# Detect spam accounts (high volume senders)
detect_spam_accounts() {
local log_file="$1"
local temp_file="/tmp/sender_counts.$$"
print_info "Analyzing sender volumes..."
# Count messages per sender
grep "<=" -- "$log_file" 2>/dev/null | \
grep -oE 'U=[^ ]+' | \
sort | uniq -c | sort -rn | head -50 > "$temp_file"
# Also count by email address
grep "<=" -- "$log_file" 2>/dev/null | \
grep -oE '\<[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\>' | \
sort | uniq -c | sort -rn | head -50 >> "$temp_file"
local hourly_limit=$((SPAM_THRESHOLD * ANALYSIS_HOURS / 24))
while read -r count identifier; do
if [ "$count" -gt "$hourly_limit" ]; then
# Extract username or email
if [[ "$identifier" =~ U=([^ ]+) ]]; then
local user="${BASH_REMATCH[1]}"
SPAM_ACCOUNTS["$user"]=$count
elif [[ "$identifier" =~ @ ]]; then
SPAM_ACCOUNTS["$identifier"]=$count
fi
fi
done < "$temp_file"
if [ ${#SPAM_ACCOUNTS[@]} -gt 0 ]; then
ISSUES_FOUND["spam_accounts"]=${#SPAM_ACCOUNTS[@]}
RECOMMENDATIONS["spam_accounts"]="Investigate high-volume senders for potential compromise or spam activity."
fi
rm -f "$temp_file"
}
# Detect SPF/DKIM/DMARC failures
detect_auth_failures() {
local log_file="$1"
local temp_file="/tmp/auth_failures.$$"
print_info "Checking email authentication failures..."
# SPF failures
grep -iE "(SPF.*fail|sender SPF authorized)" -- "$log_file" 2>/dev/null > "$temp_file"
if [ -s "$temp_file" ]; then
AUTH_FAILURES["spf"]=$(wc -l < "$temp_file")
fi
# DKIM failures
grep -iE "(DKIM.*fail|dkim.*invalid|no DKIM signature)" -- "$log_file" 2>/dev/null >> "$temp_file"
if grep -q "DKIM" -- "$temp_file"; then
AUTH_FAILURES["dkim"]=$(grep -c "DKIM" -- "$temp_file")
fi
# DMARC failures
grep -iE "(DMARC.*fail|dmarc.*reject)" -- "$log_file" 2>/dev/null >> "$temp_file"
if grep -q "DMARC" -- "$temp_file"; then
AUTH_FAILURES["dmarc"]=$(grep -c "DMARC" -- "$temp_file")
fi
# Check for recipient servers requesting better authentication
grep -iE "(requires.*SPF|requires.*DKIM|improve.*authentication|sender verification)" -- "$log_file" 2>/dev/null > /tmp/auth_requests.$$
if [ -s /tmp/auth_requests.$$ ]; then
local count=$(wc -l < /tmp/auth_requests.$$)
AUTH_FAILURES["auth_requested"]=$count
# Extract which domains are complaining
grep -oE '@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}' /tmp/auth_requests.$$ | \
sed 's/@//' | sort | uniq -c | sort -rn > /tmp/auth_domains.$$
fi
if [ ${#AUTH_FAILURES[@]} -gt 0 ]; then
ISSUES_FOUND["authentication"]=${#AUTH_FAILURES[@]}
local rec="Email authentication issues detected: "
[ -n "${AUTH_FAILURES[spf]}" ] && rec+="SPF failures (${AUTH_FAILURES[spf]}), "
[ -n "${AUTH_FAILURES[dkim]}" ] && rec+="DKIM failures (${AUTH_FAILURES[dkim]}), "
[ -n "${AUTH_FAILURES[dmarc]}" ] && rec+="DMARC failures (${AUTH_FAILURES[dmarc]}), "
RECOMMENDATIONS["authentication"]="${rec%, }. Use SPF/DKIM/DMARC checker tool to verify configuration."
fi
rm -f "$temp_file" /tmp/auth_requests.$$ /tmp/auth_domains.$$
}
# Analyze bounce reasons
analyze_bounces() {
local log_file="$1"
local temp_file="/tmp/bounces.$$"
print_info "Analyzing bounce messages..."
# Extract bounces (==) and temporary deferrals (defer with reason codes)
grep -E "==|^[0-9].*defer[ed]*.*reason" -- "$log_file" 2>/dev/null > "$temp_file"
if [ -s "$temp_file" ]; then
# Categorize bounces
local mailbox_full=$(grep -ciE "(mailbox.*full|quota.*exceed|over quota)" -- "$temp_file")
local user_unknown=$(grep -ciE "(user.*unknown|no such user|recipient.*reject)" -- "$temp_file")
local blocked=$(grep -ciE "(blocked|spam|reject.*content)" -- "$temp_file")
local dns_failure=$(grep -ciE "(DNS|NXDOMAIN|domain.*not.*found)" -- "$temp_file")
local timeout=$(grep -ciE "(timeout|timed out|connection.*fail)" -- "$temp_file")
local greylisting=$(grep -ciE "(greylist|grey.*list|try again later|temporarily reject)" -- "$temp_file")
local tls_failure=$(grep -ciE "(TLS|SSL|certificate)" -- "$temp_file")
[ $mailbox_full -gt 0 ] && BOUNCE_REASONS["mailbox_full"]=$mailbox_full
[ $user_unknown -gt 0 ] && BOUNCE_REASONS["user_unknown"]=$user_unknown
[ $blocked -gt 0 ] && BOUNCE_REASONS["blocked"]=$blocked
[ $dns_failure -gt 0 ] && BOUNCE_REASONS["dns_failure"]=$dns_failure
[ $timeout -gt 0 ] && BOUNCE_REASONS["timeout"]=$timeout
[ $greylisting -gt 0 ] && BOUNCE_REASONS["greylisting"]=$greylisting
[ $tls_failure -gt 0 ] && BOUNCE_REASONS["tls_failure"]=$tls_failure
TOTAL_BOUNCES=$(wc -l < "$temp_file")
ISSUES_FOUND["bounces"]=$TOTAL_BOUNCES
fi
rm -f "$temp_file"
}
# Detect rate limiting issues
detect_rate_limiting() {
local log_file="$1"
print_info "Checking for rate limiting..."
# Look for rate limit messages
local rate_limit_count=$(grep -ciE "(rate limit|too many|throttl|exceed.*limit)" -- "$log_file")
if [ $rate_limit_count -gt 0 ]; then
ISSUES_FOUND["rate_limiting"]=$rate_limit_count
# Check which domains are rate limiting
grep -iE "(rate limit|too many)" -- "$log_file" | \
grep -oE '@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}' | \
sed 's/@//' | sort | uniq -c | sort -rn | head -10 > /tmp/rate_limit_domains.$$
RECOMMENDATIONS["rate_limiting"]="Server is hitting rate limits. Consider implementing email throttling or spreading out bulk sends."
fi
}
# Detect configuration issues
detect_config_issues() {
local log_file="$1"
print_info "Scanning for configuration issues..."
# Missing rDNS
if grep -qiE "(reverse DNS|PTR.*fail|rDNS)" -- "$log_file"; then
ISSUES_FOUND["rdns"]=1
RECOMMENDATIONS["rdns"]="Reverse DNS (PTR) issues detected. Ensure PTR record matches server hostname."
fi
# Certificate problems
local cert_issues=$(grep -ciE "(certificate.*invalid|TLS.*fail|SSL.*error)" -- "$log_file")
if [ $cert_issues -gt 0 ]; then
ISSUES_FOUND["certificate"]=$cert_issues
RECOMMENDATIONS["certificate"]="TLS/SSL certificate issues detected ($cert_issues occurrences). Verify certificate validity."
fi
# Local delivery failures
local local_fails=$(grep -ciE "(local.*delivery.*fail|unable to deliver locally)" -- "$log_file")
if [ $local_fails -gt 0 ]; then
ISSUES_FOUND["local_delivery"]=$local_fails
RECOMMENDATIONS["local_delivery"]="Local delivery failures detected. Check disk space and mailbox permissions."
fi
}
# Detect HELO/EHLO violations
detect_helo_violations() {
local log_file="$1"
local temp_file="/tmp/helo_violations.$$"
print_info "Checking for HELO/EHLO violations..."
# Invalid HELO patterns
grep -iE "(Invalid HELO|rejected.*HELO|HELO.*reject)" -- "$log_file" 2>/dev/null > "$temp_file"
if [ -s "$temp_file" ]; then
local count=$(wc -l < "$temp_file")
ISSUES_FOUND["helo_violations"]=$count
# Extract IPs with HELO violations
while IFS= read -r line; do
if [[ "$line" =~ \[([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})\] ]]; then
local ip="${BASH_REMATCH[1]}"
HELO_VIOLATIONS["$ip"]=$((${HELO_VIOLATIONS["$ip"]:-0} + 1))
fi
# Extract HELO names
if [[ "$line" =~ HELO[[:space:]]+([^[:space:]]+) ]] || [[ "$line" =~ \(([A-Z0-9-]+)\) ]]; then
local helo_name="${BASH_REMATCH[1]}"
# Track Windows machine names and other suspicious HELOs
if [[ "$helo_name" =~ ^WIN- ]] || [[ "$helo_name" =~ ^[0-9.]+$ ]]; then
echo "$helo_name" >> "/tmp/suspicious_helos.$$"
fi
fi
done < "$temp_file"
RECOMMENDATIONS["helo_violations"]="Found $count HELO/EHLO violations. These are often spam bots. Consider tightening HELO checks in Exim configuration."
fi
rm -f "$temp_file"
}
# Detect frozen messages
detect_frozen_messages() {
local log_file="$1"
print_info "Checking for frozen messages..."
# Check for frozen messages in log
local frozen_count=$(grep -ciE "(frozen|message.*frozen)" -- "$log_file")
if [ $frozen_count -gt 0 ]; then
ISSUES_FOUND["frozen_messages"]=$frozen_count
# Try to get actual frozen count from queue
local mta=$(detect_mta)
if [ "$mta" = "exim" ]; then
local queue_frozen=$(exim -bpr 2>/dev/null | grep -c "frozen" || echo "0")
if [ "$queue_frozen" -gt 0 ]; then
FROZEN_MESSAGES["current_queue"]=$queue_frozen
RECOMMENDATIONS["frozen_messages"]="Found $queue_frozen frozen messages in queue. Review with 'exim -bp | grep frozen'. May need manual intervention to delete or retry."
else
RECOMMENDATIONS["frozen_messages"]="Found $frozen_count frozen message events in logs. Check mail queue for current frozen messages."
fi
else
RECOMMENDATIONS["frozen_messages"]="Found $frozen_count frozen message events. Check mail queue for stuck messages."
fi
fi
}
# Check panic log
check_panic_log() {
print_info "Checking for critical errors in panic log..."
local panic_log="/var/log/exim_paniclog"
local alt_panic_log="/var/log/exim/paniclog"
# Check if panic log exists and has content
if [ -f "$panic_log" ] && [ -s "$panic_log" ]; then
PANIC_LOG_EXISTS=1
local panic_lines=$(wc -l < "$panic_log")
ISSUES_FOUND["panic_log"]=$panic_lines
# Get recent panic entries
tail -20 "$panic_log" > "/tmp/recent_panics.$$"
RECOMMENDATIONS["panic_log"]="CRITICAL: Panic log exists with $panic_lines entries! Check /var/log/exim_paniclog immediately. This indicates serious mail system problems."
elif [ -f "$alt_panic_log" ] && [ -s "$alt_panic_log" ]; then
PANIC_LOG_EXISTS=1
local panic_lines=$(wc -l < "$alt_panic_log")
ISSUES_FOUND["panic_log"]=$panic_lines
RECOMMENDATIONS["panic_log"]="CRITICAL: Panic log exists with $panic_lines entries! Check $alt_panic_log immediately."
fi
}
# Detect connection flooding
detect_connection_flooding() {
local log_file="$1"
local temp_file="/tmp/connection_floods.$$"
print_info "Analyzing connection patterns for flooding..."
# Look for rapid connects/disconnects (D=0s or D=1s)
grep -E "connection.*lost D=[01]s" -- "$log_file" 2>/dev/null > "$temp_file"
if [ -s "$temp_file" ]; then
# Count by IP
grep -oE '\[([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})\]' -- "$temp_file" | \
sed 's/\[//;s/\]//' | sort | uniq -c | sort -rn > "/tmp/flood_ips.$$"
# Flag IPs with >20 rapid disconnects
while read count ip; do
if [ "$count" -gt 20 ]; then
CONNECTION_FLOODS["$ip"]=$count
fi
done < "/tmp/flood_ips.$$"
if [ ${#CONNECTION_FLOODS[@]} -gt 0 ]; then
ISSUES_FOUND["connection_flooding"]=${#CONNECTION_FLOODS[@]}
RECOMMENDATIONS["connection_flooding"]="Detected connection flooding from ${#CONNECTION_FLOODS[@]} IPs. These IPs are rapidly connecting and disconnecting. Consider rate limiting or blocking."
fi
fi
rm -f "$temp_file" "/tmp/flood_ips.$$"
}
# Detect SMTP auth brute force attempts
detect_smtp_auth_attacks() {
local log_file="$1"
local temp_file="/tmp/smtp_auth_failures.$$"
print_info "Detecting SMTP authentication failures..."
# Look for auth failures
grep -iE "(authenticator.*failed|authentication failed|535.*authentication|failed.*login)" -- "$log_file" 2>/dev/null > "$temp_file"
if [ -s "$temp_file" ]; then
TOTAL_AUTH_FAILURES=$(wc -l < "$temp_file")
# Extract IPs with auth failures
grep -oE '\[([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})\]' -- "$temp_file" | \
sed 's/\[//;s/\]//' | sort | uniq -c | sort -rn > "/tmp/auth_attack_ips.$$"
# Flag IPs with >10 failures (brute force)
while read count ip; do
if [ "$count" -gt 10 ]; then
AUTH_ATTACK_IPS["$ip"]=$count
fi
done < "/tmp/auth_attack_ips.$$"
if [ ${#AUTH_ATTACK_IPS[@]} -gt 0 ]; then
ISSUES_FOUND["auth_attacks"]=${#AUTH_ATTACK_IPS[@]}
RECOMMENDATIONS["auth_attacks"]="SECURITY ALERT: Detected brute force auth attacks from ${#AUTH_ATTACK_IPS[@]} IPs. Total failures: $TOTAL_AUTH_FAILURES. Block these IPs and enable cPHulk or fail2ban."
elif [ $TOTAL_AUTH_FAILURES -gt 50 ]; then
ISSUES_FOUND["auth_failures_general"]=$TOTAL_AUTH_FAILURES
RECOMMENDATIONS["auth_failures_general"]="Detected $TOTAL_AUTH_FAILURES authentication failures. May indicate password issues or attack attempts."
fi
fi
rm -f "$temp_file" "/tmp/auth_attack_ips.$$"
}
# Detect deferral loops
detect_deferral_loops() {
local log_file="$1"
local temp_file="/tmp/deferrals.$$"
print_info "Checking for deferral loops..."
# Look for retry timeouts and excessive deferrals
grep -iE "(retry timeout exceeded|retry time not reached|too many.*defer)" -- "$log_file" 2>/dev/null > "$temp_file"
if [ -s "$temp_file" ]; then
local deferral_loop_count=$(wc -l < "$temp_file")
# Extract domains with deferral issues
grep -oE '@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}' -- "$temp_file" | \
sed 's/@//' | sort | uniq -c | sort -rn | head -10 > "/tmp/deferral_domains.$$"
ISSUES_FOUND["deferral_loops"]=$deferral_loop_count
RECOMMENDATIONS["deferral_loops"]="Found $deferral_loop_count messages in deferral loops. These will eventually bounce. Check recipient domains and consider manual intervention."
fi
rm -f "$temp_file"
}
#=============================================================================
# ADDITIONAL DETECTION FUNCTIONS - High Priority
#=============================================================================
# Detect TLS/SSL issues
detect_tls_issues() {
local log_file="$1"
local temp_file="/tmp/tls_issues.$$"
print_info "Analyzing TLS/SSL errors..."
# Look for TLS errors
grep -iE "(TLS error|SSL error|SSL_accept|SSL_read|SSL_write|certificate)" -- "$log_file" 2>/dev/null > "$temp_file"
if [ -s "$temp_file" ]; then
local count=$(wc -l < "$temp_file")
ISSUES_FOUND["tls_errors"]=$count
# Categorize TLS errors
local ssl_eof=$(grep -c "unexpected eof" -- "$temp_file" 2>/dev/null | tr -d '\n' || echo "0")
local ssl_broken_pipe=$(grep -c "Broken pipe" -- "$temp_file" 2>/dev/null | tr -d '\n' || echo "0")
local ssl_packet_length=$(grep -c "packet length too long" -- "$temp_file" 2>/dev/null | tr -d '\n' || echo "0")
local ssl_reset=$(grep -c "Connection reset" -- "$temp_file" 2>/dev/null | tr -d '\n' || echo "0")
# Track IPs with TLS issues
declare -A TLS_IPS
while IFS= read -r line; do
if [[ "$line" =~ \[([0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3})\] ]]; then
local ip="${BASH_REMATCH[1]}"
TLS_IPS["$ip"]=$((${TLS_IPS["$ip"]:-0} + 1))
fi
done < "$temp_file"
# Save top TLS error IPs
if [ ${#TLS_IPS[@]} -gt 0 ]; then
for ip in "${!TLS_IPS[@]}"; do
echo "${TLS_IPS[$ip]} $ip"
done | sort -rn | head -10 > "/tmp/tls_error_ips.$$"
fi
RECOMMENDATIONS["tls_errors"]="Found $count TLS/SSL errors. Most common: EOF ($ssl_eof), Broken pipe ($ssl_broken_pipe), Packet length ($ssl_packet_length). These are usually scanner/bot probes and can be safely ignored unless affecting legitimate traffic."
fi
rm -f "$temp_file"
}
# Detect message size rejections
detect_size_rejections() {
local log_file="$1"
local temp_file="/tmp/size_rejections.$$"
print_info "Checking for message size rejections..."
# Look for size-related rejections
grep -iE "(message too big|size exceed|quota exceed|over.*quota)" -- "$log_file" 2>/dev/null > "$temp_file"
if [ -s "$temp_file" ]; then
local count=$(wc -l < "$temp_file")
ISSUES_FOUND["size_rejections"]=$count
# Extract affected users/domains
grep -oE '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}' -- "$temp_file" | \
sort | uniq -c | sort -rn | head -10 > "/tmp/size_reject_users.$$"
RECOMMENDATIONS["size_rejections"]="Found $count message size rejections. Users are trying to send files that exceed size limits. Educate users about limits and suggest file-sharing alternatives (Dropbox, Google Drive, etc.)."
fi
rm -f "$temp_file"
}
# Detect routing/forwarding loops
detect_routing_loops() {
local log_file="$1"
local temp_file="/tmp/routing_loops.$$"
print_info "Detecting mail routing loops..."
# Look for loop indicators
grep -iE "(too many.*Received|routing loop|maximum hops|mail loop)" -- "$log_file" 2>/dev/null > "$temp_file"
if [ -s "$temp_file" ]; then
local count=$(wc -l < "$temp_file")
ISSUES_FOUND["routing_loops"]=$count
# Extract affected addresses
grep -oE '[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}' -- "$temp_file" | \
sort | uniq -c | sort -rn | head -10 > "/tmp/loop_addresses.$$"
RECOMMENDATIONS["routing_loops"]="Found $count routing loops. These are caused by misconfigured email forwards (.forward files, auto-forwards, etc.). Check forwarding rules for affected addresses and break the loops."
fi
rm -f "$temp_file"
}
#=============================================================================
# COMPREHENSIVE ANALYSIS FUNCTIONS - Domain & User Level
#=============================================================================
# Analyze per-domain activity and issues
analyze_domain_performance() {
local log_file="$1"
print_info "Analyzing domain-level performance..."
# Track sent messages per domain
grep "<=" -- "$log_file" 2>/dev/null | while IFS= read -r line; do
# Extract sender domain from F=<user@domain>
if [[ "$line" =~ F=\<[^@]+@([a-zA-Z0-9.-]+)\> ]]; then
local domain="${BASH_REMATCH[1]}"
echo "$domain" >> /tmp/domains_sent.$$
fi
done
# Track delivered messages per domain
grep "=>" -- "$log_file" 2>/dev/null | while IFS= read -r line; do
# Extract recipient domain
if [[ "$line" =~ @([a-zA-Z0-9.-]+\.[a-zA-Z]{2,}) ]]; then
local domain="${BASH_REMATCH[1]}"
echo "$domain" >> /tmp/domains_delivered.$$
fi
done
# Track bounced messages per domain
grep "==" -- "$log_file" 2>/dev/null | while IFS= read -r line; do
if [[ "$line" =~ @([a-zA-Z0-9.-]+\.[a-zA-Z]{2,}) ]]; then
local domain="${BASH_REMATCH[1]}"
echo "$domain" >> /tmp/domains_bounced.$$
# Capture bounce reason
if [[ "$line" =~ (550|551|552|553|554)[[:space:]](.{1,80}) ]]; then
local reason="${BASH_REMATCH[2]}"
echo "$domain|$reason" >> /tmp/domain_bounce_reasons.$$
fi
fi
done
# Summarize domains
if [ -f /tmp/domains_sent.$$ ]; then
sort /tmp/domains_sent.$$ | uniq -c | sort -rn | head -20 > /tmp/top_sending_domains.$$
fi
if [ -f /tmp/domains_delivered.$$ ]; then
sort /tmp/domains_delivered.$$ | uniq -c | sort -rn | head -20 > /tmp/top_recipient_domains.$$
fi
if [ -f /tmp/domains_bounced.$$ ]; then
sort /tmp/domains_bounced.$$ | uniq -c | sort -rn | head -20 > /tmp/top_bouncing_domains.$$
fi
}
# Analyze per-user activity
analyze_user_activity() {
local log_file="$1"
print_info "Analyzing user-level activity..."
# Track messages sent per user
grep "<=" -- "$log_file" 2>/dev/null | while IFS= read -r line; do
# Extract full email address
if [[ "$line" =~ F=\<([a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,})\> ]]; then
local email="${BASH_REMATCH[1]}"
echo "$email" >> /tmp/users_sent.$$
fi
# Also track U= (authenticated user)
if [[ "$line" =~ U=([^[:space:]]+) ]]; then
local user="${BASH_REMATCH[1]}"
echo "$user" >> /tmp/authenticated_users.$$
fi
done
# Summarize top senders
if [ -f /tmp/users_sent.$$ ]; then
sort /tmp/users_sent.$$ | uniq -c | sort -rn | head -20 > /tmp/top_senders.$$
fi
if [ -f /tmp/authenticated_users.$$ ]; then
sort /tmp/authenticated_users.$$ | uniq -c | sort -rn | head -20 > /tmp/top_authenticated_users.$$
fi
}
# Analyze hourly distribution
analyze_hourly_patterns() {
local log_file="$1"
print_info "Analyzing hourly distribution..."
# Extract hour from each message
awk '/^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:/ {
split($2, time, ":")
hour = time[1]
print hour
}' "$log_file" 2>/dev/null | sort | uniq -c | sort -k2 -n > /tmp/hourly_volume.$$
}
# Analyze rejection reasons in detail
analyze_rejection_details() {
local log_file="$1"
print_info "Analyzing rejection reasons..."
# Extract detailed rejection messages
grep -iE "(rejected|denied)" -- "$log_file" 2>/dev/null | head -50 > /tmp/rejection_samples.$$
# Categorize rejections
grep -i "rejected" -- "$log_file" 2>/dev/null | while IFS= read -r line; do
case "$line" in
*"Relay access denied"*)
echo "Relay access denied" >> /tmp/rejection_categories.$$
;;
*"Sender address rejected"*)
echo "Invalid sender" >> /tmp/rejection_categories.$$
;;
*"Recipient address rejected"*)
echo "Invalid recipient" >> /tmp/rejection_categories.$$
;;
*"Greylisted"*)
echo "Greylisted" >> /tmp/rejection_categories.$$
;;
*"Policy"*)
echo "Policy violation" >> /tmp/rejection_categories.$$
;;
*"Spam"*)
echo "Spam filter" >> /tmp/rejection_categories.$$
;;
*)
echo "Other rejection" >> /tmp/rejection_categories.$$
;;
esac
done
if [ -f /tmp/rejection_categories.$$ ]; then
sort /tmp/rejection_categories.$$ | uniq -c | sort -rn > /tmp/rejection_summary.$$
fi
}
# Track message delivery success rate per domain
calculate_domain_success_rates() {
local log_file="$1"
print_info "Calculating domain success rates..."
# For each domain, calculate: (delivered / (delivered + bounced)) * 100
if [ -f /tmp/top_recipient_domains.$$ ]; then
while read count domain; do
local delivered=$count
# Use word boundary to match exact domain, not substrings
local bounced=$(grep -c "\b${domain}$" /tmp/domains_bounced.$$ 2>/dev/null || echo "0")
local total=$((delivered + bounced))
if [ $total -gt 0 ]; then
local success_rate=$(( (delivered * 100) / total ))
echo "$success_rate%|$domain|$delivered/$total" >> /tmp/domain_success_rates.$$
fi
done < /tmp/top_recipient_domains.$$
sort -t'|' -k1 -rn /tmp/domain_success_rates.$$ | head -20 > /tmp/domain_success_rates_sorted.$$
fi
}
# Capture error message samples for troubleshooting
capture_error_samples() {
local log_file="$1"
print_info "Capturing error message samples..."
# Sample of each error type for user troubleshooting
grep -i "SPF.*fail" -- "$log_file" 2>/dev/null | head -3 > /tmp/sample_spf_failures.$$
grep -i "DKIM.*fail" -- "$log_file" 2>/dev/null | head -3 > /tmp/sample_dkim_failures.$$
grep -i "blacklist" -- "$log_file" 2>/dev/null | head -3 > /tmp/sample_blacklist.$$
grep -i "quota.*exceed" -- "$log_file" 2>/dev/null | head -3 > /tmp/sample_quota.$$
grep -i "user.*unknown" -- "$log_file" 2>/dev/null | head -3 > /tmp/sample_unknown_user.$$
grep -i "connection.*timeout" -- "$log_file" 2>/dev/null | head -3 > /tmp/sample_timeout.$$
}
# Gather general statistics
gather_statistics() {
local log_file="$1"
print_info "Gathering statistics..."
# Count sent messages
TOTAL_SENT=$(grep -c "<=" -- "$log_file" 2>/dev/null | tr -d '\n' || echo "0")
# Count received messages
TOTAL_RECEIVED=$(grep -c "=>" -- "$log_file" 2>/dev/null | tr -d '\n' || echo "0")
# Count deferrals
TOTAL_DEFERRED=$(grep -c "defer" -- "$log_file" 2>/dev/null | tr -d '\n' || echo "0")
# Count rejections
TOTAL_REJECTED=$(grep -cE "(reject|denied)" -- "$log_file" 2>/dev/null | tr -d '\n' || echo "0")
}
################################################################################
# Reporting Functions
################################################################################
# Display summary header
display_summary() {
print_banner "Mail Log Analysis Report"
echo -e "${BOLD}Analysis Period:${NC} Last $ANALYSIS_HOURS hours"
echo -e "${BOLD}Log File:${NC} $MAIL_LOG"
echo -e "${BOLD}Analysis Time:${NC} $(date)"
echo ""
# Overall statistics
echo -e "${CYAN}${BOLD}═══════════════════════════════════════════════════${NC}"
echo -e "${CYAN}${BOLD} OVERALL STATISTICS${NC}"
echo -e "${CYAN}${BOLD}═══════════════════════════════════════════════════${NC}"
echo ""
printf " %-30s %s\n" "Messages Sent:" "$TOTAL_SENT"
printf " %-30s %s\n" "Messages Delivered:" "$TOTAL_RECEIVED"
printf " %-30s %s\n" "Bounces/Deferrals:" "$TOTAL_BOUNCES"
printf " %-30s %s\n" "Deferred:" "$TOTAL_DEFERRED"
printf " %-30s %s\n" "Rejected:" "$TOTAL_REJECTED"
echo ""
}
# Display issues found
display_issues() {
if [ ${#ISSUES_FOUND[@]} -eq 0 ]; then
print_success "No significant issues detected!"
echo ""
return
fi
echo -e "${RED}${BOLD}═══════════════════════════════════════════════════${NC}"
echo -e "${RED}${BOLD} ISSUES DETECTED${NC}"
echo -e "${RED}${BOLD}═══════════════════════════════════════════════════${NC}"
echo ""
# Blacklist issues
if [ -n "${ISSUES_FOUND[blacklist]}" ]; then
echo -e "${RED}${BOLD}🚫 BLACKLIST DETECTIONS (${ISSUES_FOUND[blacklist]} occurrences)${NC}"
echo ""
# Show which blacklists
if [ ${#BLACKLISTED_IPS[@]} -gt 0 ]; then
echo " Blacklists mentioned:"
for bl in "${!BLACKLISTED_IPS[@]}"; do
printf " - %-40s %d times\n" "$bl" "${BLACKLISTED_IPS[$bl]}"
done
echo ""
fi
# Show timeline - first and last occurrence
if [ -f "/tmp/blacklist_detections.$$" ]; then
local first_occurrence=$(head -1 "/tmp/blacklist_detections.$$" | awk '{print $1, $2}')
local last_occurrence=$(tail -1 "/tmp/blacklist_detections.$$" | awk '{print $1, $2}')
echo " Timeline:"
echo " First seen: $first_occurrence"
echo " Last seen: $last_occurrence"
# Check if recent (within last hour of log)
local log_end=$(tail -1 "$MAIL_LOG" 2>/dev/null | awk '{print $1, $2}')
if [ "$last_occurrence" == "$log_end" ] || [ -z "$last_occurrence" ]; then
echo -e " ${RED}Status: STILL OCCURRING ⚠️${NC}"
else
echo -e " ${GREEN}Status: May have stopped (last seen earlier in log)${NC}"
fi
echo ""
fi
# Show which domains/users triggered it (top 5)
if [ -f "/tmp/blacklist_detections.$$" ]; then
echo " Affected senders (top 5):"
grep -oE 'F=<[^>]+>' "/tmp/blacklist_detections.$$" 2>/dev/null | \
sed 's/F=<//; s/>//' | sort | uniq -c | sort -rn | head -5 | \
while read count sender; do
printf " - %-45s %d times\n" "$sender" "$count"
done
echo ""
fi
echo -e " ${YELLOW}Action Required:${NC} ${RECOMMENDATIONS[blacklist]}"
echo ""
fi
# Spam accounts
if [ -n "${ISSUES_FOUND[spam_accounts]}" ]; then
echo -e "${RED}${BOLD}⚠️ HIGH-VOLUME SENDERS (${ISSUES_FOUND[spam_accounts]} accounts)${NC}"
echo ""
echo " Top senders exceeding threshold:"
local count=0
for account in "${!SPAM_ACCOUNTS[@]}"; do
printf " - %-50s %d messages\n" "$account" "${SPAM_ACCOUNTS[$account]}"
((count++))
[ $count -ge 10 ] && break
done
echo ""
echo -e " ${YELLOW}Action Required:${NC} ${RECOMMENDATIONS[spam_accounts]}"
echo ""
fi
# Authentication failures
if [ -n "${ISSUES_FOUND[authentication]}" ]; then
echo -e "${RED}${BOLD}🔐 EMAIL AUTHENTICATION ISSUES${NC}"
echo ""
[ -n "${AUTH_FAILURES[spf]}" ] && echo " SPF Failures: ${AUTH_FAILURES[spf]}"
[ -n "${AUTH_FAILURES[dkim]}" ] && echo " DKIM Failures: ${AUTH_FAILURES[dkim]}"
[ -n "${AUTH_FAILURES[dmarc]}" ] && echo " DMARC Failures: ${AUTH_FAILURES[dmarc]}"
echo ""
if [ -f /tmp/auth_domains.$$ ]; then
echo " Domains requesting better authentication:"
head -5 /tmp/auth_domains.$$ | while read count domain; do
printf " - %-40s %d times\n" "$domain" "$count"
done
echo ""
fi
echo -e " ${YELLOW}Action Required:${NC} ${RECOMMENDATIONS[authentication]}"
echo ""
fi
# Bounce analysis
if [ ${#BOUNCE_REASONS[@]} -gt 0 ]; then
echo -e "${YELLOW}${BOLD}📊 BOUNCE ANALYSIS${NC}"
echo ""
echo " Bounce reasons breakdown:"
[ -n "${BOUNCE_REASONS[mailbox_full]}" ] && printf " - %-40s %d\n" "Mailbox Full" "${BOUNCE_REASONS[mailbox_full]}"
[ -n "${BOUNCE_REASONS[user_unknown]}" ] && printf " - %-40s %d\n" "User Unknown" "${BOUNCE_REASONS[user_unknown]}"
[ -n "${BOUNCE_REASONS[blocked]}" ] && printf " - %-40s %d\n" "Blocked/Spam" "${BOUNCE_REASONS[blocked]}"
[ -n "${BOUNCE_REASONS[dns_failure]}" ] && printf " - %-40s %d\n" "DNS Failure" "${BOUNCE_REASONS[dns_failure]}"
[ -n "${BOUNCE_REASONS[timeout]}" ] && printf " - %-40s %d\n" "Timeout" "${BOUNCE_REASONS[timeout]}"
[ -n "${BOUNCE_REASONS[greylisting]}" ] && printf " - %-40s %d\n" "Greylisting" "${BOUNCE_REASONS[greylisting]}"
[ -n "${BOUNCE_REASONS[tls_failure]}" ] && printf " - %-40s %d\n" "TLS/SSL Issues" "${BOUNCE_REASONS[tls_failure]}"
echo ""
# Recommendations based on bounce types
if [ -n "${BOUNCE_REASONS[blocked]}" ] && [ "${BOUNCE_REASONS[blocked]}" -gt 10 ]; then
echo -e " ${YELLOW}Action Required:${NC} High spam/block rate. Check IP reputation and email authentication."
fi
if [ -n "${BOUNCE_REASONS[dns_failure]}" ] && [ "${BOUNCE_REASONS[dns_failure]}" -gt 5 ]; then
echo -e " ${YELLOW}Action Required:${NC} DNS issues detected. Verify domain DNS records and MX records."
fi
echo ""
fi
# Rate limiting
if [ -n "${ISSUES_FOUND[rate_limiting]}" ]; then
echo -e "${YELLOW}${BOLD}⏱️ RATE LIMITING (${ISSUES_FOUND[rate_limiting]} occurrences)${NC}"
echo ""
if [ -f /tmp/rate_limit_domains.$$ ]; then
echo " Domains enforcing rate limits:"
head -5 /tmp/rate_limit_domains.$$ | while read count domain; do
printf " - %-40s %d times\n" "$domain" "$count"
done
echo ""
fi
echo -e " ${YELLOW}Action Required:${NC} ${RECOMMENDATIONS[rate_limiting]}"
echo ""
fi
# Configuration issues
for issue in rdns certificate local_delivery; do
if [ -n "${ISSUES_FOUND[$issue]}" ]; then
local icon="⚙️"
case $issue in
rdns) icon="🌐" ;;
certificate) icon="🔒" ;;
local_delivery) icon="📬" ;;
esac
echo -e "${YELLOW}${BOLD}$icon ${issue^^} ISSUE${NC}"
echo -e " ${YELLOW}Action Required:${NC} ${RECOMMENDATIONS[$issue]}"
echo ""
fi
done
# HELO Violations
if [ -n "${ISSUES_FOUND[helo_violations]}" ]; then
echo -e "${RED}${BOLD}🚫 HELO/EHLO VIOLATIONS (${ISSUES_FOUND[helo_violations]} occurrences)${NC}"
echo ""
if [ ${#HELO_VIOLATIONS[@]} -gt 0 ]; then
echo " Top offending IPs:"
local count=0
for ip in "${!HELO_VIOLATIONS[@]}"; do
printf " - %-40s %d violations\n" "$ip" "${HELO_VIOLATIONS[$ip]}"
((count++))
[ $count -ge 10 ] && break
done
fi
if [ -f "/tmp/suspicious_helos.$$" ]; then
echo ""
echo " Suspicious HELO names detected:"
sort /tmp/suspicious_helos.$$ | uniq -c | sort -rn | head -5 | while read count helo; do
printf " - %-40s %d times\n" "$helo" "$count"
done
fi
echo ""
echo -e " ${YELLOW}Action Required:${NC} ${RECOMMENDATIONS[helo_violations]}"
echo ""
fi
# Panic Log
if [ -n "${ISSUES_FOUND[panic_log]}" ]; then
echo -e "${RED}${BOLD}💥 CRITICAL - PANIC LOG EXISTS (${ISSUES_FOUND[panic_log]} entries)${NC}"
echo ""
if [ -f "/tmp/recent_panics.$$" ]; then
echo " Recent panic log entries:"
cat "/tmp/recent_panics.$$" | head -5 | sed 's/^/ /'
echo ""
fi
echo -e " ${RED}${BOLD}Action Required:${NC} ${RECOMMENDATIONS[panic_log]}"
echo ""
fi
# Frozen Messages
if [ -n "${ISSUES_FOUND[frozen_messages]}" ]; then
echo -e "${YELLOW}${BOLD}❄️ FROZEN MESSAGES (${ISSUES_FOUND[frozen_messages]} detected)${NC}"
echo ""
if [ -n "${FROZEN_MESSAGES[current_queue]}" ]; then
echo " Currently frozen in queue: ${FROZEN_MESSAGES[current_queue]} messages"
echo ""
fi
echo -e " ${YELLOW}Action Required:${NC} ${RECOMMENDATIONS[frozen_messages]}"
echo ""
fi
# Connection Flooding
if [ -n "${ISSUES_FOUND[connection_flooding]}" ]; then
echo -e "${RED}${BOLD}🌊 CONNECTION FLOODING (${ISSUES_FOUND[connection_flooding]} IPs)${NC}"
echo ""
echo " IPs with rapid connect/disconnect:"
local count=0
for ip in "${!CONNECTION_FLOODS[@]}"; do
printf " - %-40s %d rapid connections\n" "$ip" "${CONNECTION_FLOODS[$ip]}"
((count++))
[ $count -ge 10 ] && break
done
echo ""
echo -e " ${YELLOW}Action Required:${NC} ${RECOMMENDATIONS[connection_flooding]}"
echo ""
fi
# Auth Attacks
if [ -n "${ISSUES_FOUND[auth_attacks]}" ]; then
echo -e "${RED}${BOLD}🔓 BRUTE FORCE AUTH ATTACKS (${ISSUES_FOUND[auth_attacks]} IPs)${NC}"
echo ""
echo " IPs attempting brute force:"
local count=0
for ip in "${!AUTH_ATTACK_IPS[@]}"; do
printf " - %-40s %d failed attempts\n" "$ip" "${AUTH_ATTACK_IPS[$ip]}"
((count++))
[ $count -ge 10 ] && break
done
echo ""
echo -e " ${RED}${BOLD}Action Required:${NC} ${RECOMMENDATIONS[auth_attacks]}"
echo ""
fi
# Deferral Loops
if [ -n "${ISSUES_FOUND[deferral_loops]}" ]; then
echo -e "${YELLOW}${BOLD}🔄 DEFERRAL LOOPS (${ISSUES_FOUND[deferral_loops]} messages)${NC}"
echo ""
if [ -f "/tmp/deferral_domains.$$" ]; then
echo " Domains with deferral issues:"
head -5 "/tmp/deferral_domains.$$" | while read count domain; do
printf " - %-40s %d messages\n" "$domain" "$count"
done
echo ""
fi
echo -e " ${YELLOW}Action Required:${NC} ${RECOMMENDATIONS[deferral_loops]}"
echo ""
fi
# TLS/SSL Issues
if [ -n "${ISSUES_FOUND[tls_errors]}" ]; then
echo -e "${YELLOW}${BOLD}🔒 TLS/SSL ERRORS (${ISSUES_FOUND[tls_errors]} occurrences)${NC}"
echo ""
if [ -f "/tmp/tls_error_ips.$$" ]; then
echo " Top IPs with TLS errors:"
head -10 "/tmp/tls_error_ips.$$" | while read count ip; do
printf " - %-40s %d errors\n" "$ip" "$count"
done
echo ""
fi
echo -e " ${YELLOW}Action Required:${NC} ${RECOMMENDATIONS[tls_errors]}"
echo ""
fi
# Message Size Rejections
if [ -n "${ISSUES_FOUND[size_rejections]}" ]; then
echo -e "${YELLOW}${BOLD}📦 MESSAGE SIZE REJECTIONS (${ISSUES_FOUND[size_rejections]} occurrences)${NC}"
echo ""
if [ -f "/tmp/size_reject_users.$$" ]; then
echo " Users affected by size limits:"
head -10 "/tmp/size_reject_users.$$" | while read count user; do
printf " - %-40s %d rejections\n" "$user" "$count"
done
echo ""
fi
echo -e " ${YELLOW}Action Required:${NC} ${RECOMMENDATIONS[size_rejections]}"
echo ""
fi
# Routing Loops
if [ -n "${ISSUES_FOUND[routing_loops]}" ]; then
echo -e "${RED}${BOLD}♻️ ROUTING LOOPS (${ISSUES_FOUND[routing_loops]} detected)${NC}"
echo ""
if [ -f "/tmp/loop_addresses.$$" ]; then
echo " Addresses caught in loops:"
head -10 "/tmp/loop_addresses.$$" | while read count address; do
printf " - %-40s %d times\n" "$address" "$count"
done
echo ""
fi
echo -e " ${RED}Action Required:${NC} ${RECOMMENDATIONS[routing_loops]}"
echo ""
fi
}
# Display actionable recommendations
display_recommendations() {
if [ ${#RECOMMENDATIONS[@]} -eq 0 ]; then
return
fi
echo -e "${GREEN}${BOLD}═══════════════════════════════════════════════════${NC}"
echo -e "${GREEN}${BOLD} RECOMMENDED ACTIONS${NC}"
echo -e "${GREEN}${BOLD}═══════════════════════════════════════════════════${NC}"
echo ""
local priority=1
for issue in blacklist spam_accounts authentication rate_limiting rdns certificate local_delivery helo_violations frozen_messages panic_log connection_flooding auth_attacks deferral_loops tls_errors size_rejections routing_loops; do
if [ -n "${RECOMMENDATIONS[$issue]}" ]; then
echo -e "${CYAN}$priority)${NC} ${BOLD}$(echo $issue | tr '_' ' ' | awk '{for(i=1;i<=NF;i++)sub(/./,toupper(substr($i,1,1)),$i)}1')${NC}"
echo " ${RECOMMENDATIONS[$issue]}"
echo ""
((priority++))
fi
done
# General recommendations
echo -e "${CYAN}${priority})${NC} ${BOLD}Use Email Troubleshooting Tools${NC}"
echo " - SPF/DKIM/DMARC Check (Menu option 4)"
echo " - Blacklist Check (Menu option 5)"
echo " - Mail Queue Inspector (Menu option 2)"
echo ""
}
#=============================================================================
# ENHANCED DISPLAY FUNCTIONS - Domain & User Insights
#=============================================================================
# Display detailed domain analysis
display_domain_analysis() {
# Only show domains with actual problems (< 80% success rate OR > 10 bounces)
local has_issues=0
# Check if we have problem domains
if [ -f /tmp/domain_success_rates_sorted.$$ ] && [ -s /tmp/domain_success_rates_sorted.$$ ]; then
# Check if any domain has < 80% success rate
if awk -F'|' '$1 < 80 {exit 0} END {exit 1}' /tmp/domain_success_rates_sorted.$$ 2>/dev/null; then
has_issues=1
fi
fi
if [ -f /tmp/top_bouncing_domains.$$ ] && [ -s /tmp/top_bouncing_domains.$$ ]; then
# Check if any domain has > 10 bounces
if awk '$1 > 10 {exit 0} END {exit 1}' /tmp/top_bouncing_domains.$$ 2>/dev/null; then
has_issues=1
fi
fi
# Only display section if there are actual problems
if [ $has_issues -eq 0 ]; then
return
fi
echo ""
echo -e "${CYAN}${BOLD}═══════════════════════════════════════════════════${NC}"
echo -e "${CYAN}${BOLD} PROBLEM DOMAINS DETECTED${NC}"
echo -e "${CYAN}${BOLD}═══════════════════════════════════════════════════${NC}"
echo ""
# Show domains with low success rates (< 80%)
if [ -f /tmp/domain_success_rates_sorted.$$ ] && [ -s /tmp/domain_success_rates_sorted.$$ ]; then
local shown=0
while IFS='|' read rate domain stats; do
# Only show if success rate < 80%
# Remove percent sign and decimal portion, keep only integer part
local rate_int=$(echo "$rate" | sed 's/[^0-9].*//')
if [ -n "$rate_int" ] && [ "$rate_int" -lt 80 ]; then
if [ $shown -eq 0 ]; then
echo -e "${RED}${BOLD}⚠️ Domains with Low Delivery Success Rates (<80%):${NC}"
echo ""
fi
printf " %-40s %6s (%s delivered)\n" "$domain" "$rate" "$stats"
shown=1
fi
done < /tmp/domain_success_rates_sorted.$$
[ $shown -eq 1 ] && echo ""
fi
# Show domains with significant bounces (> 10)
if [ -f /tmp/top_bouncing_domains.$$ ] && [ -s /tmp/top_bouncing_domains.$$ ]; then
local shown=0
local count=0
while read num domain; do
# Only show if > 10 bounces
if [ "$num" -gt 10 ]; then
if [ $shown -eq 0 ]; then
echo -e "${RED}${BOLD}⚠️ Domains with High Bounce Counts (>10):${NC}"
echo ""
fi
printf " %-40s %6d bounces\n" "$domain" "$num"
shown=1
((count++))
[ $count -ge 5 ] && break
fi
done < /tmp/top_bouncing_domains.$$
[ $shown -eq 1 ] && echo ""
fi
}
# Display user activity analysis - ONLY show suspicious/high-volume users
display_user_analysis() {
local has_suspicious=0
local threshold=100 # Show users with > 100 messages (potential spam/compromised)
# Check if any users exceed threshold
if [ -f /tmp/top_senders.$$ ] && [ -s /tmp/top_senders.$$ ]; then
if awk -v t=$threshold '$1 > t {exit 0} END {exit 1}' /tmp/top_senders.$$ 2>/dev/null; then
has_suspicious=1
fi
fi
# Only display if there are suspicious users
if [ $has_suspicious -eq 0 ]; then
return
fi
echo ""
echo -e "${CYAN}${BOLD}═══════════════════════════════════════════════════${NC}"
echo -e "${CYAN}${BOLD} HIGH-VOLUME SENDERS (Potential Issues)${NC}"
echo -e "${CYAN}${BOLD}═══════════════════════════════════════════════════${NC}"
echo ""
# Show only high-volume senders (> 100 messages)
if [ -f /tmp/top_senders.$$ ] && [ -s /tmp/top_senders.$$ ]; then
local shown=0
local count=0
while read num email; do
if [ "$num" -gt $threshold ]; then
if [ $shown -eq 0 ]; then
echo -e "${YELLOW}${BOLD}⚠️ Users Sending >$threshold Messages:${NC}"
echo ""
fi
printf " %-45s %6d messages\n" "$email" "$num"
shown=1
((count++))
[ $count -ge 10 ] && break
fi
done < /tmp/top_senders.$$
if [ $shown -eq 1 ]; then
echo ""
echo -e "${YELLOW} Note: High volume may indicate compromised account or spam bot.${NC}"
echo ""
fi
fi
}
# Display hourly distribution - ONLY if suspicious off-hours activity detected
display_hourly_distribution() {
if [ ! -f /tmp/hourly_volume.$$ ] || [ ! -s /tmp/hourly_volume.$$ ]; then
return
fi
# Calculate average and check for off-hours spikes (00:00-06:00)
local max_vol=$(awk '{print $1}' /tmp/hourly_volume.$$ | sort -n | tail -1)
local avg_vol=$(awk '{sum+=$1; count++} END {if(count>0) print int(sum/count); else print 0}' /tmp/hourly_volume.$$)
# Check for off-hours activity (midnight-6am) that's > 2x average
local has_suspicious_hours=0
while read count hour; do
if [ "$hour" -lt 6 ] && [ "$count" -gt $((avg_vol * 2)) ]; then
has_suspicious_hours=1
break
fi
done < /tmp/hourly_volume.$$
# Only show if suspicious activity detected
if [ $has_suspicious_hours -eq 0 ]; then
return
fi
echo ""
echo -e "${CYAN}${BOLD}═══════════════════════════════════════════════════${NC}"
echo -e "${CYAN}${BOLD} ⚠️ SUSPICIOUS HOURLY ACTIVITY DETECTED${NC}"
echo -e "${CYAN}${BOLD}═══════════════════════════════════════════════════${NC}"
echo ""
echo -e "${YELLOW}${BOLD}Unusual off-hours email activity detected (midnight-6am spike)${NC}"
echo -e "${YELLOW}This may indicate a compromised account sending spam.${NC}"
echo ""
echo -e "${BOLD}📈 Message Volume by Hour:${NC}"
echo ""
while read count hour; do
# Create simple bar chart
local bar_length=$((count * 50 / max_vol))
[ $bar_length -lt 1 ] && bar_length=1
local bar=$(printf '█%.0s' $(seq 1 $bar_length))
# Highlight suspicious hours (00-06) in red
if [ "$hour" -lt 6 ] && [ "$count" -gt $((avg_vol * 2)) ]; then
printf " ${RED}%02d:00 %5d %s ← SPIKE${NC}\n" "$hour" "$count" "$bar"
else
printf " %02d:00 %5d %s\n" "$hour" "$count" "$bar"
fi
done < /tmp/hourly_volume.$$
echo ""
}
# Display rejection analysis - ONLY if significant rejections (>10)
display_rejection_analysis() {
if [ ! -f /tmp/rejection_summary.$$ ] || [ ! -s /tmp/rejection_summary.$$ ]; then
return
fi
# Check if any rejection type has > 10 occurrences
local has_significant=0
if awk '$1 > 10 {exit 0} END {exit 1}' /tmp/rejection_summary.$$ 2>/dev/null; then
has_significant=1
fi
if [ $has_significant -eq 0 ]; then
return
fi
echo ""
echo -e "${CYAN}${BOLD}═══════════════════════════════════════════════════${NC}"
echo -e "${CYAN}${BOLD} REJECTION ANALYSIS${NC}"
echo -e "${CYAN}${BOLD}═══════════════════════════════════════════════════${NC}"
echo ""
echo -e "${BOLD}🚫 Top Rejection Reasons (>10 occurrences):${NC}"
echo ""
local count=0
while read num reason; do
if [ "$num" -gt 10 ]; then
printf " %-50s %6d\n" "$reason" "$num"
((count++))
[ $count -ge 5 ] && break
fi
done < /tmp/rejection_summary.$$
echo ""
}
# Display error samples - DISABLED to avoid information overload
display_error_samples() {
# This section is intentionally disabled
# The existing issue detection already shows relevant error details
return
}
# Save report to file
save_report() {
{
display_summary
display_issues
display_recommendations
display_domain_analysis
display_user_analysis
display_hourly_distribution
display_rejection_analysis
display_error_samples
} | tee "$REPORT_FILE" >/dev/null
echo -e "${GREEN}Report saved to: $REPORT_FILE${NC}"
echo ""
}
################################################################################
# Main Function
################################################################################
main() {
print_banner "Mail Log Analyzer"
# Detect mail log location
MAIL_LOG=$(get_mail_log_path)
if [ -z "$MAIL_LOG" ] || [ ! -f "$MAIL_LOG" ]; then
print_error "Mail log not found!"
echo "Expected locations:"
echo " - /var/log/exim_mainlog (cPanel)"
echo " - /var/log/maillog (Plesk/RHEL)"
echo " - /var/log/mail.log (Debian/Ubuntu)"
pause_for_user
return 1
fi
print_info "Found mail log: $MAIL_LOG"
echo ""
# Display time period selection menu
echo -e "${CYAN}${BOLD}Select Analysis Time Period:${NC}"
echo ""
echo " 1) Last 1 hour"
echo " 2) Last 6 hours"
echo " 3) Last 12 hours"
echo " 4) Last 24 hours (recommended)"
echo " 5) Last 48 hours (2 days)"
echo " 6) Last 1 week (7 days)"
echo " 7) Last 1 month (30 days)"
echo " 8) Entire log file"
echo ""
echo -n "Enter choice [4]: "
read -r choice
choice=${choice:-4}
# Map choice to hours
case $choice in
1) ANALYSIS_HOURS=1; ANALYSIS_DESC="1 hour" ;;
2) ANALYSIS_HOURS=6; ANALYSIS_DESC="6 hours" ;;
3) ANALYSIS_HOURS=12; ANALYSIS_DESC="12 hours" ;;
4) ANALYSIS_HOURS=24; ANALYSIS_DESC="24 hours" ;;
5) ANALYSIS_HOURS=48; ANALYSIS_DESC="48 hours" ;;
6) ANALYSIS_HOURS=168; ANALYSIS_DESC="1 week" ;;
7) ANALYSIS_HOURS=720; ANALYSIS_DESC="1 month" ;;
8) ANALYSIS_HOURS=999999; ANALYSIS_DESC="entire log" ;;
*) ANALYSIS_HOURS=24; ANALYSIS_DESC="24 hours" ;;
esac
echo ""
print_info "Analyzing last $ANALYSIS_DESC of mail logs..."
echo ""
# Create temporary log file with time-filtered entries
TEMP_LOG="/tmp/mail_analysis_$$.log"
if [ "$ANALYSIS_HOURS" -eq 999999 ]; then
# Use entire log
cp "$MAIL_LOG" "$TEMP_LOG"
else
# Calculate cutoff timestamp (works with Exim date format)
CUTOFF_TIMESTAMP=$(date -d "$ANALYSIS_HOURS hours ago" '+%Y-%m-%d %H:%M:%S' 2>/dev/null)
if [ -n "$CUTOFF_TIMESTAMP" ]; then
# Filter by actual timestamps
awk -v cutoff="$CUTOFF_TIMESTAMP" '
BEGIN { print_line = 0 }
/^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:[0-9]{2}/ {
timestamp = $1 " " $2
if (timestamp >= cutoff) {
print_line = 1
} else {
print_line = 0
}
}
print_line { print }
' "$MAIL_LOG" > "$TEMP_LOG"
# Fallback to tail if awk filtering produced empty result
if [ ! -s "$TEMP_LOG" ]; then
# Estimate lines based on hours (rough estimate: 1000 lines per hour)
local estimated_lines=$((ANALYSIS_HOURS * 1000))
tail -n "$estimated_lines" "$MAIL_LOG" > "$TEMP_LOG"
fi
else
# Fallback for systems without GNU date
local estimated_lines=$((ANALYSIS_HOURS * 1000))
tail -n "$estimated_lines" "$MAIL_LOG" > "$TEMP_LOG"
fi
fi
# Run all detection functions
detect_blacklist_issues "$TEMP_LOG"
detect_spam_accounts "$TEMP_LOG"
detect_auth_failures "$TEMP_LOG"
analyze_bounces "$TEMP_LOG"
detect_rate_limiting "$TEMP_LOG"
detect_config_issues "$TEMP_LOG"
gather_statistics "$TEMP_LOG"
# Enhanced detection functions
detect_helo_violations "$TEMP_LOG"
detect_frozen_messages "$TEMP_LOG"
check_panic_log
detect_connection_flooding "$TEMP_LOG"
detect_smtp_auth_attacks "$TEMP_LOG"
detect_deferral_loops "$TEMP_LOG"
# Additional high-priority detections
detect_tls_issues "$TEMP_LOG"
detect_size_rejections "$TEMP_LOG"
detect_routing_loops "$TEMP_LOG"
# NEW: Comprehensive analysis functions
analyze_domain_performance "$TEMP_LOG"
analyze_user_activity "$TEMP_LOG"
analyze_hourly_patterns "$TEMP_LOG"
analyze_rejection_details "$TEMP_LOG"
calculate_domain_success_rates "$TEMP_LOG"
capture_error_samples "$TEMP_LOG"
# Display results
clear
display_summary
display_issues
display_recommendations
# Save report
save_report
# Cleanup
rm -f "$TEMP_LOG" /tmp/*.$$ 2>/dev/null
echo ""
echo -n "Press Enter to return to menu..."
read
}
# Run main function
main