#!/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= 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=//' | 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 'BEGIN{i=0} {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 'BEGIN {sum=0; count=0} {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