diff --git a/modules/email/mail-log-analyzer.sh b/modules/email/mail-log-analyzer.sh index 0d1448b..959dcb9 100755 --- a/modules/email/mail-log-analyzer.sh +++ b/modules/email/mail-log-analyzer.sh @@ -21,7 +21,11 @@ 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" +TEMP_DIR=$(mktemp -d) || { print_error "Failed to create temp directory"; exit 1; } +REPORT_FILE=""$TEMP_DIR/"mail-analysis-$(date +%Y%m%d-%H%M%S).txt" + +# Cleanup trap - runs on EXIT or SIGINT +trap 'rm -rf "$TEMP_DIR" 2>/dev/null' EXIT INT TERM # Issue tracking arrays declare -A ISSUES_FOUND @@ -65,7 +69,7 @@ PANIC_LOG_EXISTS=0 # Detect blacklist rejections detect_blacklist_issues() { local log_file="$1" - local temp_file="/tmp/blacklist_detections.$$" + local temp_file=""$TEMP_DIR/"blacklist_detections.$$" print_info "Scanning for blacklist rejections..." @@ -76,7 +80,7 @@ detect_blacklist_issues() { # 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.$$" + local temp_filtered=""$TEMP_DIR/"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 @@ -152,7 +156,7 @@ detect_blacklist_issues() { # Detect spam accounts (high volume senders) detect_spam_accounts() { local log_file="$1" - local temp_file="/tmp/sender_counts.$$" + local temp_file=""$TEMP_DIR/"sender_counts.$$" print_info "Analyzing sender volumes..." @@ -191,7 +195,7 @@ detect_spam_accounts() { # Detect SPF/DKIM/DMARC failures detect_auth_failures() { local log_file="$1" - local temp_file="/tmp/auth_failures.$$" + local temp_file=""$TEMP_DIR/"auth_failures.$$" print_info "Checking email authentication failures..." @@ -214,14 +218,14 @@ detect_auth_failures() { 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.$$) + grep -iE "(requires.*SPF|requires.*DKIM|improve.*authentication|sender verification)" -- "$log_file" 2>/dev/null > "$TEMP_DIR/"auth_requests.$$ + if [ -s "$TEMP_DIR/"auth_requests.$$ ]; then + local count=$(wc -l < "$TEMP_DIR/"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.$$ + grep -oE '@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}' "$TEMP_DIR/"auth_requests.$$ | \ + sed 's/@//' | sort | uniq -c | sort -rn > "$TEMP_DIR/"auth_domains.$$ fi if [ ${#AUTH_FAILURES[@]} -gt 0 ]; then @@ -233,13 +237,13 @@ detect_auth_failures() { RECOMMENDATIONS["authentication"]="${rec%, }. Use SPF/DKIM/DMARC checker tool to verify configuration." fi - rm -f "$temp_file" /tmp/auth_requests.$$ /tmp/auth_domains.$$ + rm -f "$temp_file" "$TEMP_DIR/"auth_requests.$$ "$TEMP_DIR/"auth_domains.$$ } # Analyze bounce reasons analyze_bounces() { local log_file="$1" - local temp_file="/tmp/bounces.$$" + local temp_file=""$TEMP_DIR/"bounces.$$" print_info "Analyzing bounce messages..." @@ -286,7 +290,7 @@ detect_rate_limiting() { # 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.$$ + sed 's/@//' | sort | uniq -c | sort -rn | head -10 > "$TEMP_DIR/"rate_limit_domains.$$ RECOMMENDATIONS["rate_limiting"]="Server is hitting rate limits. Consider implementing email throttling or spreading out bulk sends." fi @@ -322,7 +326,7 @@ detect_config_issues() { # Detect HELO/EHLO violations detect_helo_violations() { local log_file="$1" - local temp_file="/tmp/helo_violations.$$" + local temp_file=""$TEMP_DIR/"helo_violations.$$" print_info "Checking for HELO/EHLO violations..." @@ -345,7 +349,7 @@ detect_helo_violations() { 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.$$" + echo "$helo_name" >> ""$TEMP_DIR/"suspicious_helos.$$" fi fi done < "$temp_file" @@ -398,7 +402,7 @@ check_panic_log() { ISSUES_FOUND["panic_log"]=$panic_lines # Get recent panic entries - tail -20 "$panic_log" > "/tmp/recent_panics.$$" + tail -20 "$panic_log" > ""$TEMP_DIR/"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 @@ -412,7 +416,7 @@ check_panic_log() { # Detect connection flooding detect_connection_flooding() { local log_file="$1" - local temp_file="/tmp/connection_floods.$$" + local temp_file=""$TEMP_DIR/"connection_floods.$$" print_info "Analyzing connection patterns for flooding..." @@ -422,14 +426,14 @@ detect_connection_flooding() { 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.$$" + sed 's/\[//;s/\]//' | sort | uniq -c | sort -rn > ""$TEMP_DIR/"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.$$" + done < ""$TEMP_DIR/"flood_ips.$$" if [ ${#CONNECTION_FLOODS[@]} -gt 0 ]; then ISSUES_FOUND["connection_flooding"]=${#CONNECTION_FLOODS[@]} @@ -437,13 +441,13 @@ detect_connection_flooding() { fi fi - rm -f "$temp_file" "/tmp/flood_ips.$$" + rm -f "$temp_file" ""$TEMP_DIR/"flood_ips.$$" } # Detect SMTP auth brute force attempts detect_smtp_auth_attacks() { local log_file="$1" - local temp_file="/tmp/smtp_auth_failures.$$" + local temp_file=""$TEMP_DIR/"smtp_auth_failures.$$" print_info "Detecting SMTP authentication failures..." @@ -455,14 +459,14 @@ detect_smtp_auth_attacks() { # 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.$$" + sed 's/\[//;s/\]//' | sort | uniq -c | sort -rn > ""$TEMP_DIR/"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.$$" + done < ""$TEMP_DIR/"auth_attack_ips.$$" if [ ${#AUTH_ATTACK_IPS[@]} -gt 0 ]; then ISSUES_FOUND["auth_attacks"]=${#AUTH_ATTACK_IPS[@]} @@ -473,13 +477,13 @@ detect_smtp_auth_attacks() { fi fi - rm -f "$temp_file" "/tmp/auth_attack_ips.$$" + rm -f "$temp_file" ""$TEMP_DIR/"auth_attack_ips.$$" } # Detect deferral loops detect_deferral_loops() { local log_file="$1" - local temp_file="/tmp/deferrals.$$" + local temp_file=""$TEMP_DIR/"deferrals.$$" print_info "Checking for deferral loops..." @@ -491,7 +495,7 @@ detect_deferral_loops() { # 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.$$" + sed 's/@//' | sort | uniq -c | sort -rn | head -10 > ""$TEMP_DIR/"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." @@ -507,7 +511,7 @@ detect_deferral_loops() { # Detect TLS/SSL issues detect_tls_issues() { local log_file="$1" - local temp_file="/tmp/tls_issues.$$" + local temp_file=""$TEMP_DIR/"tls_issues.$$" print_info "Analyzing TLS/SSL errors..." @@ -537,7 +541,7 @@ detect_tls_issues() { 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.$$" + done | sort -rn | head -10 > ""$TEMP_DIR/"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." @@ -549,7 +553,7 @@ detect_tls_issues() { # Detect message size rejections detect_size_rejections() { local log_file="$1" - local temp_file="/tmp/size_rejections.$$" + local temp_file=""$TEMP_DIR/"size_rejections.$$" print_info "Checking for message size rejections..." @@ -562,7 +566,7 @@ detect_size_rejections() { # 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.$$" + sort | uniq -c | sort -rn | head -10 > ""$TEMP_DIR/"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 @@ -573,7 +577,7 @@ detect_size_rejections() { # Detect routing/forwarding loops detect_routing_loops() { local log_file="$1" - local temp_file="/tmp/routing_loops.$$" + local temp_file=""$TEMP_DIR/"routing_loops.$$" print_info "Detecting mail routing loops..." @@ -586,7 +590,7 @@ detect_routing_loops() { # 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.$$" + sort | uniq -c | sort -rn | head -10 > ""$TEMP_DIR/"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 @@ -609,7 +613,7 @@ analyze_domain_performance() { # Extract sender domain from F= if [[ "$line" =~ F=\<[^@]+@([a-zA-Z0-9.-]+)\> ]]; then local domain="${BASH_REMATCH[1]}" - echo "$domain" >> /tmp/domains_sent.$$ + echo "$domain" >> "$TEMP_DIR/"domains_sent.$$ fi done @@ -618,7 +622,7 @@ analyze_domain_performance() { # Extract recipient domain if [[ "$line" =~ @([a-zA-Z0-9.-]+\.[a-zA-Z]{2,}) ]]; then local domain="${BASH_REMATCH[1]}" - echo "$domain" >> /tmp/domains_delivered.$$ + echo "$domain" >> "$TEMP_DIR/"domains_delivered.$$ fi done @@ -626,27 +630,27 @@ analyze_domain_performance() { 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.$$ + echo "$domain" >> "$TEMP_DIR/"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.$$ + echo "$domain|$reason" >> "$TEMP_DIR/"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.$$ + if [ -f "$TEMP_DIR/"domains_sent.$$ ]; then + sort "$TEMP_DIR/"domains_sent.$$ | uniq -c | sort -rn | head -20 > "$TEMP_DIR/"top_sending_domains.$$ fi - if [ -f /tmp/domains_delivered.$$ ]; then - sort /tmp/domains_delivered.$$ | uniq -c | sort -rn | head -20 > /tmp/top_recipient_domains.$$ + if [ -f "$TEMP_DIR/"domains_delivered.$$ ]; then + sort "$TEMP_DIR/"domains_delivered.$$ | uniq -c | sort -rn | head -20 > "$TEMP_DIR/"top_recipient_domains.$$ fi - if [ -f /tmp/domains_bounced.$$ ]; then - sort /tmp/domains_bounced.$$ | uniq -c | sort -rn | head -20 > /tmp/top_bouncing_domains.$$ + if [ -f "$TEMP_DIR/"domains_bounced.$$ ]; then + sort "$TEMP_DIR/"domains_bounced.$$ | uniq -c | sort -rn | head -20 > "$TEMP_DIR/"top_bouncing_domains.$$ fi } @@ -661,23 +665,23 @@ analyze_user_activity() { # 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.$$ + echo "$email" >> "$TEMP_DIR/"users_sent.$$ fi # Also track U= (authenticated user) if [[ "$line" =~ U=([^[:space:]]+) ]]; then local user="${BASH_REMATCH[1]}" - echo "$user" >> /tmp/authenticated_users.$$ + echo "$user" >> "$TEMP_DIR/"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.$$ + if [ -f "$TEMP_DIR/"users_sent.$$ ]; then + sort "$TEMP_DIR/"users_sent.$$ | uniq -c | sort -rn | head -20 > "$TEMP_DIR/"top_senders.$$ fi - if [ -f /tmp/authenticated_users.$$ ]; then - sort /tmp/authenticated_users.$$ | uniq -c | sort -rn | head -20 > /tmp/top_authenticated_users.$$ + if [ -f "$TEMP_DIR/"authenticated_users.$$ ]; then + sort "$TEMP_DIR/"authenticated_users.$$ | uniq -c | sort -rn | head -20 > "$TEMP_DIR/"top_authenticated_users.$$ fi } @@ -692,7 +696,7 @@ analyze_hourly_patterns() { split($2, time, ":") hour = time[1] print hour - }' "$log_file" 2>/dev/null | sort | uniq -c | sort -k2 -n > /tmp/hourly_volume.$$ + }' "$log_file" 2>/dev/null | sort | uniq -c | sort -k2 -n > "$TEMP_DIR/"hourly_volume.$$ } # Analyze rejection reasons in detail @@ -702,37 +706,37 @@ analyze_rejection_details() { print_info "Analyzing rejection reasons..." # Extract detailed rejection messages - grep -iE "(rejected|denied)" -- "$log_file" 2>/dev/null | head -50 > /tmp/rejection_samples.$$ + grep -iE "(rejected|denied)" -- "$log_file" 2>/dev/null | head -50 > "$TEMP_DIR/"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.$$ + echo "Relay access denied" >> "$TEMP_DIR/"rejection_categories.$$ ;; *"Sender address rejected"*) - echo "Invalid sender" >> /tmp/rejection_categories.$$ + echo "Invalid sender" >> "$TEMP_DIR/"rejection_categories.$$ ;; *"Recipient address rejected"*) - echo "Invalid recipient" >> /tmp/rejection_categories.$$ + echo "Invalid recipient" >> "$TEMP_DIR/"rejection_categories.$$ ;; *"Greylisted"*) - echo "Greylisted" >> /tmp/rejection_categories.$$ + echo "Greylisted" >> "$TEMP_DIR/"rejection_categories.$$ ;; *"Policy"*) - echo "Policy violation" >> /tmp/rejection_categories.$$ + echo "Policy violation" >> "$TEMP_DIR/"rejection_categories.$$ ;; *"Spam"*) - echo "Spam filter" >> /tmp/rejection_categories.$$ + echo "Spam filter" >> "$TEMP_DIR/"rejection_categories.$$ ;; *) - echo "Other rejection" >> /tmp/rejection_categories.$$ + echo "Other rejection" >> "$TEMP_DIR/"rejection_categories.$$ ;; esac done - if [ -f /tmp/rejection_categories.$$ ]; then - sort /tmp/rejection_categories.$$ | uniq -c | sort -rn > /tmp/rejection_summary.$$ + if [ -f "$TEMP_DIR/"rejection_categories.$$ ]; then + sort "$TEMP_DIR/"rejection_categories.$$ | uniq -c | sort -rn > "$TEMP_DIR/"rejection_summary.$$ fi } @@ -743,20 +747,22 @@ calculate_domain_success_rates() { print_info "Calculating domain success rates..." # For each domain, calculate: (delivered / (delivered + bounced)) * 100 - if [ -f /tmp/top_recipient_domains.$$ ]; then + if [ -f "$TEMP_DIR/"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") + # Escape domain for safe use in grep pattern (fix regex injection risk) + local escaped_domain=$(printf '%s\n' "$domain" | sed 's/[[\.*^$/]/\\&/g') + local bounced=$(grep -c "\b${escaped_domain}$" "$TEMP_DIR/"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.$$ + echo "$success_rate%|$domain|$delivered/$total" >> "$TEMP_DIR/"domain_success_rates.$$ fi - done < /tmp/top_recipient_domains.$$ + done < "$TEMP_DIR/"top_recipient_domains.$$ - sort -t'|' -k1 -rn /tmp/domain_success_rates.$$ | head -20 > /tmp/domain_success_rates_sorted.$$ + sort -t'|' -k1 -rn "$TEMP_DIR/"domain_success_rates.$$ | head -20 > "$TEMP_DIR/"domain_success_rates_sorted.$$ fi } @@ -767,12 +773,12 @@ capture_error_samples() { 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.$$ + grep -i "SPF.*fail" -- "$log_file" 2>/dev/null | head -3 > "$TEMP_DIR/"sample_spf_failures.$$ + grep -i "DKIM.*fail" -- "$log_file" 2>/dev/null | head -3 > "$TEMP_DIR/"sample_dkim_failures.$$ + grep -i "blacklist" -- "$log_file" 2>/dev/null | head -3 > "$TEMP_DIR/"sample_blacklist.$$ + grep -i "quota.*exceed" -- "$log_file" 2>/dev/null | head -3 > "$TEMP_DIR/"sample_quota.$$ + grep -i "user.*unknown" -- "$log_file" 2>/dev/null | head -3 > "$TEMP_DIR/"sample_unknown_user.$$ + grep -i "connection.*timeout" -- "$log_file" 2>/dev/null | head -3 > "$TEMP_DIR/"sample_timeout.$$ } # Gather general statistics @@ -848,9 +854,9 @@ display_issues() { 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}') + if [ -f ""$TEMP_DIR/"blacklist_detections.$$" ]; then + local first_occurrence=$(head -1 ""$TEMP_DIR/"blacklist_detections.$$" | awk '{print $1, $2}') + local last_occurrence=$(tail -1 ""$TEMP_DIR/"blacklist_detections.$$" | awk '{print $1, $2}') echo " Timeline:" echo " First seen: $first_occurrence" @@ -867,9 +873,9 @@ display_issues() { fi # Show which domains/users triggered it (top 5) - if [ -f "/tmp/blacklist_detections.$$" ]; then + if [ -f ""$TEMP_DIR/"blacklist_detections.$$" ]; then echo " Affected senders (top 5):" - grep -oE 'F=<[^>]+>' "/tmp/blacklist_detections.$$" 2>/dev/null | \ + grep -oE 'F=<[^>]+>' ""$TEMP_DIR/"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" @@ -905,9 +911,9 @@ display_issues() { [ -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 + if [ -f "$TEMP_DIR/"auth_domains.$$ ]; then echo " Domains requesting better authentication:" - head -5 /tmp/auth_domains.$$ | while read count domain; do + head -5 "$TEMP_DIR/"auth_domains.$$ | while read count domain; do printf " - %-40s %d times\n" "$domain" "$count" done echo "" @@ -944,9 +950,9 @@ display_issues() { 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 + if [ -f "$TEMP_DIR/"rate_limit_domains.$$ ]; then echo " Domains enforcing rate limits:" - head -5 /tmp/rate_limit_domains.$$ | while read count domain; do + head -5 "$TEMP_DIR/"rate_limit_domains.$$ | while read count domain; do printf " - %-40s %d times\n" "$domain" "$count" done echo "" @@ -983,10 +989,10 @@ display_issues() { [ "$count" -ge 10 ] && break done fi - if [ -f "/tmp/suspicious_helos.$$" ]; then + if [ -f ""$TEMP_DIR/"suspicious_helos.$$" ]; then echo "" echo " Suspicious HELO names detected:" - sort /tmp/suspicious_helos.$$ | uniq -c | sort -rn | head -5 | while read count helo; do + sort "$TEMP_DIR/"suspicious_helos.$$ | uniq -c | sort -rn | head -5 | while read count helo; do printf " - %-40s %d times\n" "$helo" "$count" done fi @@ -999,9 +1005,9 @@ display_issues() { 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 + if [ -f ""$TEMP_DIR/"recent_panics.$$" ]; then echo " Recent panic log entries:" - cat "/tmp/recent_panics.$$" | head -5 | sed 's/^/ /' + cat ""$TEMP_DIR/"recent_panics.$$" | head -5 | sed 's/^/ /' echo "" fi echo -e " ${RED}${BOLD}Action Required:${NC} ${RECOMMENDATIONS[panic_log]}" @@ -1056,9 +1062,9 @@ display_issues() { 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 + if [ -f ""$TEMP_DIR/"deferral_domains.$$" ]; then echo " Domains with deferral issues:" - head -5 "/tmp/deferral_domains.$$" | while read count domain; do + head -5 ""$TEMP_DIR/"deferral_domains.$$" | while read count domain; do printf " - %-40s %d messages\n" "$domain" "$count" done echo "" @@ -1071,9 +1077,9 @@ display_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 + if [ -f ""$TEMP_DIR/"tls_error_ips.$$" ]; then echo " Top IPs with TLS errors:" - head -10 "/tmp/tls_error_ips.$$" | while read count ip; do + head -10 ""$TEMP_DIR/"tls_error_ips.$$" | while read count ip; do printf " - %-40s %d errors\n" "$ip" "$count" done echo "" @@ -1086,9 +1092,9 @@ display_issues() { 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 + if [ -f ""$TEMP_DIR/"size_reject_users.$$" ]; then echo " Users affected by size limits:" - head -10 "/tmp/size_reject_users.$$" | while read count user; do + head -10 ""$TEMP_DIR/"size_reject_users.$$" | while read count user; do printf " - %-40s %d rejections\n" "$user" "$count" done echo "" @@ -1101,9 +1107,9 @@ display_issues() { 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 + if [ -f ""$TEMP_DIR/"loop_addresses.$$" ]; then echo " Addresses caught in loops:" - head -10 "/tmp/loop_addresses.$$" | while read count address; do + head -10 ""$TEMP_DIR/"loop_addresses.$$" | while read count address; do printf " - %-40s %d times\n" "$address" "$count" done echo "" @@ -1127,7 +1133,7 @@ display_recommendations() { 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 -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++)) @@ -1152,16 +1158,16 @@ display_domain_analysis() { local has_issues=0 # Check if we have problem domains - if [ -f /tmp/domain_success_rates_sorted.$$ ] && [ -s /tmp/domain_success_rates_sorted.$$ ]; then + if [ -f "$TEMP_DIR/"domain_success_rates_sorted.$$ ] && [ -s "$TEMP_DIR/"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 + if awk -F'|' '$1 < 80 {exit 0} END {exit 1}' "$TEMP_DIR/"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 + if [ -f "$TEMP_DIR/"top_bouncing_domains.$$ ] && [ -s "$TEMP_DIR/"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 + if awk '$1 > 10 {exit 0} END {exit 1}' "$TEMP_DIR/"top_bouncing_domains.$$ 2>/dev/null; then has_issues=1 fi fi @@ -1178,7 +1184,7 @@ display_domain_analysis() { echo "" # Show domains with low success rates (< 80%) - if [ -f /tmp/domain_success_rates_sorted.$$ ] && [ -s /tmp/domain_success_rates_sorted.$$ ]; then + if [ -f "$TEMP_DIR/"domain_success_rates_sorted.$$ ] && [ -s "$TEMP_DIR/"domain_success_rates_sorted.$$ ]; then local shown=0 while IFS='|' read rate domain stats; do # Only show if success rate < 80% @@ -1192,12 +1198,12 @@ display_domain_analysis() { printf " %-40s %6s (%s delivered)\n" "$domain" "$rate" "$stats" shown=1 fi - done < /tmp/domain_success_rates_sorted.$$ + done < "$TEMP_DIR/"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 + if [ -f "$TEMP_DIR/"top_bouncing_domains.$$ ] && [ -s "$TEMP_DIR/"top_bouncing_domains.$$ ]; then local shown=0 local count=0 while read num domain; do @@ -1212,7 +1218,7 @@ display_domain_analysis() { ((count++)) [ "$count" -ge 5 ] && break fi - done < /tmp/top_bouncing_domains.$$ + done < "$TEMP_DIR/"top_bouncing_domains.$$ [ "$shown" -eq 1 ] && echo "" fi } @@ -1223,8 +1229,8 @@ display_user_analysis() { 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 + if [ -f "$TEMP_DIR/"top_senders.$$ ] && [ -s "$TEMP_DIR/"top_senders.$$ ]; then + if awk -v t=$threshold '$1 > t {exit 0} END {exit 1}' "$TEMP_DIR/"top_senders.$$ 2>/dev/null; then has_suspicious=1 fi fi @@ -1241,7 +1247,7 @@ display_user_analysis() { echo "" # Show only high-volume senders (> 100 messages) - if [ -f /tmp/top_senders.$$ ] && [ -s /tmp/top_senders.$$ ]; then + if [ -f "$TEMP_DIR/"top_senders.$$ ] && [ -s "$TEMP_DIR/"top_senders.$$ ]; then local shown=0 local count=0 while read num email; do @@ -1255,7 +1261,7 @@ display_user_analysis() { ((count++)) [ "$count" -ge 10 ] && break fi - done < /tmp/top_senders.$$ + done < "$TEMP_DIR/"top_senders.$$ if [ "$shown" -eq 1 ]; then echo "" @@ -1267,13 +1273,16 @@ display_user_analysis() { # Display hourly distribution - ONLY if suspicious off-hours activity detected display_hourly_distribution() { - if [ ! -f /tmp/hourly_volume.$$ ] || [ ! -s /tmp/hourly_volume.$$ ]; then + if [ ! -f "$TEMP_DIR/"hourly_volume.$$ ] || [ ! -s "$TEMP_DIR/"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.$$) + local max_vol=$(awk '{print $1}' "$TEMP_DIR/"hourly_volume.$$ | sort -n | tail -1) + max_vol=${max_vol:-1} # Prevent division by zero + [ "$max_vol" -le 0 ] && max_vol=1 + + local avg_vol=$(awk 'BEGIN {sum=0; count=0} {sum+=$1; count++} END {if(count>0) print int(sum/count); else print 0}' "$TEMP_DIR/"hourly_volume.$$) # Check for off-hours activity (midnight-6am) that's > 2x average local has_suspicious_hours=0 @@ -1282,7 +1291,7 @@ display_hourly_distribution() { has_suspicious_hours=1 break fi - done < /tmp/hourly_volume.$$ + done < "$TEMP_DIR/"hourly_volume.$$ # Only show if suspicious activity detected if [ $has_suspicious_hours -eq 0 ]; then @@ -1313,19 +1322,19 @@ display_hourly_distribution() { else printf " %02d:00 %5d %s\n" "$hour" "$count" "$bar" fi - done < /tmp/hourly_volume.$$ + done < "$TEMP_DIR/"hourly_volume.$$ echo "" } # Display rejection analysis - ONLY if significant rejections (>10) display_rejection_analysis() { - if [ ! -f /tmp/rejection_summary.$$ ] || [ ! -s /tmp/rejection_summary.$$ ]; then + if [ ! -f "$TEMP_DIR/"rejection_summary.$$ ] || [ ! -s "$TEMP_DIR/"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 + if awk '$1 > 10 {exit 0} END {exit 1}' "$TEMP_DIR/"rejection_summary.$$ 2>/dev/null; then has_significant=1 fi @@ -1348,7 +1357,7 @@ display_rejection_analysis() { ((count++)) [ "$count" -ge 5 ] && break fi - done < /tmp/rejection_summary.$$ + done < "$TEMP_DIR/"rejection_summary.$$ echo "" } @@ -1440,11 +1449,11 @@ main() { echo "" # Create temporary log file with time-filtered entries - TEMP_LOG="/tmp/mail_analysis_$$.log" + TEMP_LOG=""$TEMP_DIR/"mail_analysis_$$.log" if [ "$ANALYSIS_HOURS" -eq 999999 ]; then # Use entire log - cp "$MAIL_LOG" "$TEMP_LOG" + cp "$MAIL_LOG" "$TEMP_LOG" || { print_error "Failed to copy mail log"; exit 1; } 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) @@ -1462,21 +1471,27 @@ main() { } } print_line { print } - ' "$MAIL_LOG" > "$TEMP_LOG" + ' "$MAIL_LOG" > "$TEMP_LOG" || { print_error "Failed to filter mail log"; exit 1; } # 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" + tail -n "$estimated_lines" "$MAIL_LOG" > "$TEMP_LOG" || { print_error "Failed to read tail of mail log"; exit 1; } fi else # Fallback for systems without GNU date local estimated_lines=$((ANALYSIS_HOURS * 1000)) - tail -n "$estimated_lines" "$MAIL_LOG" > "$TEMP_LOG" + tail -n "$estimated_lines" "$MAIL_LOG" > "$TEMP_LOG" || { print_error "Failed to read tail of mail log"; exit 1; } fi fi + # Verify we have data to analyze + if [ ! -s "$TEMP_LOG" ]; then + print_error "No mail log data found for analysis period" + exit 1 + fi + # Run all detection functions detect_blacklist_issues "$TEMP_LOG" detect_spam_accounts "$TEMP_LOG" @@ -1516,12 +1531,10 @@ main() { # Save report save_report - # Cleanup - rm -f "$TEMP_LOG" /tmp/*.$$ 2>/dev/null + # Note: Cleanup handled by trap handler on script exit echo "" - echo -n "Press Enter to return to menu..." - read + press_enter } # Run main function