diff --git a/modules/email/email-diagnostics.sh b/modules/email/email-diagnostics.sh index 79ecabd..dd45119 100755 --- a/modules/email/email-diagnostics.sh +++ b/modules/email/email-diagnostics.sh @@ -7,17 +7,27 @@ # Shows proof of delivery or identifies why emails aren't working ################################################################################ +set -eo pipefail + 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" +# Month name to number mapping for awk timestamp conversion +declare -A MONTH_MAP=( + [Jan]=01 [Feb]=02 [Mar]=03 [Apr]=04 [May]=05 [Jun]=06 + [Jul]=07 [Aug]=08 [Sep]=09 [Oct]=10 [Nov]=11 [Dec]=12 +) + show_banner "Email Diagnostics - Verify Email Delivery" -# Cleanup temporary files on script exit +# Cleanup temporary files on script exit (Issue 4.1: improved cleanup trap) cleanup() { + # Clean up all known temp files and potential report file rm -f /tmp/email_diag_$$.txt /tmp/email_auth_$$.txt /tmp/email_all_$$.txt \ - /tmp/email_bounces_$$.txt /tmp/email_blacklists_$$.txt /tmp/email_blacklists_filtered_$$.txt 2>/dev/null + /tmp/email_bounces_$$.txt /tmp/email_blacklists_$$.txt /tmp/email_blacklists_filtered_$$.txt \ + /tmp/email_diag_*_*.txt 2>/dev/null } trap cleanup EXIT INT TERM @@ -130,7 +140,7 @@ esac cutoff_epoch=$(($(date +%s) - cutoff_seconds)) echo "" -print_info "Analyzing $check_label for last $hours hours (after $(date -d @$cutoff_epoch '+%Y-%m-%d %H:%M:%S'))..." +print_info "Analyzing $check_label for last $hours hours (after $(date -d "@$cutoff_epoch" '+%Y-%m-%d %H:%M:%S'))..." echo "" ################################################################################ @@ -142,18 +152,53 @@ TEMP_AUTH="/tmp/email_auth_$$.txt" TEMP_ALL="/tmp/email_all_$$.txt" # Time-filtered search: Extract logs from cutoff time, then search -# Uses awk to parse timestamps and filter by epoch time +# Uses awk to parse timestamps and filter by epoch time (Issue 1.1: fixed mktime format) grep -iF -- "$search_pattern" "$MAIL_LOG" 2>/dev/null | awk -v cutoff="$cutoff_epoch" \ 'NF { # Try to extract epoch from various timestamp formats - # Most mail logs: "Mar 20 10:30:00" or "2026-03-20 10:30:00" - epoch = mktime(match($0, /([0-9]{4})-([0-9]{2})-([0-9]{2}) ([0-9]{2}):([0-9]{2}):([0-9]{2})/) ? \ - gensub(/([0-9]{4})-([0-9]{2})-([0-9]{2}) ([0-9]{2}):([0-9]{2}):([0-9]{2})/, \ - "\\1 \\2 \\3 \\4 \\5 \\6", 1) : \ - (match($0, /[A-Z][a-z]{2} +[0-9]+ [0-9]{2}:[0-9]{2}:[0-9]{2}/) ? \ - gensub(/([A-Z][a-z]{2}) +([0-9]+) ([0-9]{2}):([0-9]{2}):([0-9]{2})/, \ - "2026 \\1 \\2 \\3 \\4 \\5", 1) : 0)) - # If we could parse a timestamp and its after cutoff, print the line + # ISO format: "2026-03-20 10:30:00" + if (match($0, /([0-9]{4})-([0-9]{2})-([0-9]{2}) ([0-9]{2}):([0-9]{2}):([0-9]{2})/)) { + year = substr($0, RSTART, 4) + month = substr($0, RSTART+5, 2) + day = substr($0, RSTART+8, 2) + hour = substr($0, RSTART+11, 2) + min = substr($0, RSTART+14, 2) + sec = substr($0, RSTART+17, 2) + epoch = mktime(year " " month " " day " " hour " " min " " sec) + } + # Syslog format: "Mar 20 10:30:00" (need month number for mktime) + else if (match($0, /([A-Z][a-z]{2}) +([0-9]+) ([0-9]{2}):([0-9]{2}):([0-9]{2})/)) { + month_str = substr($0, RSTART, 3) + # Convert month name to number manually (mktime needs numbers, not names) + if (month_str == "Jan") month_num = 1 + else if (month_str == "Feb") month_num = 2 + else if (month_str == "Mar") month_num = 3 + else if (month_str == "Apr") month_num = 4 + else if (month_str == "May") month_num = 5 + else if (month_str == "Jun") month_num = 6 + else if (month_str == "Jul") month_num = 7 + else if (month_str == "Aug") month_num = 8 + else if (month_str == "Sep") month_num = 9 + else if (month_str == "Oct") month_num = 10 + else if (month_str == "Nov") month_num = 11 + else if (month_str == "Dec") month_num = 12 + else month_num = 0 + + if (month_num > 0) { + day = substr($0, RSTART+4) + gsub(/[^0-9]/, "", day) + hour = substr($0, RSTART+8, 2) + min = substr($0, RSTART+11, 2) + sec = substr($0, RSTART+14, 2) + epoch = mktime("2026 " sprintf("%02d", month_num) " " sprintf("%02d", day) " " sprintf("%02d", hour) " " sprintf("%02d", min) " " sprintf("%02d", sec)) + } else { + epoch = 0 + } + } else { + epoch = 0 # No timestamp found, include line anyway + } + + # Print if: no timestamp found (epoch==0) OR timestamp is after cutoff if (epoch == 0 || epoch >= cutoff) print }' > "$TEMP_ALL" 2>/dev/null || true @@ -245,39 +290,26 @@ echo "" # Categorize activity ################################################################################ -# Count different types in single pass (fix Issue 5.1 - performance optimization) -# Initialize counters -delivered=0 sent=0 bounced=0 deferred=0 rejected=0 spf_fail=0 dkim_fail=0 spam_rejected=0 greylist=0 received=0 +# Count different types (Issue 3.1: use grep for proper pattern matching, Issue 3.2: fix double-counting) +delivered=$(grep -c "^ *[^ ]* *[^ ]* .*=> " "$TEMP_MATCHES" 2>/dev/null || echo 0) +received=$delivered # received and delivered should be the same metric +sent=$(grep -c "<= " "$TEMP_MATCHES" 2>/dev/null || echo 0) +deferred=$(grep -Eci "deferred|retry|temporarily rejected" "$TEMP_MATCHES" 2>/dev/null || echo 0) +spf_fail=$(grep -ci "SPF" "$TEMP_MATCHES" 2>/dev/null | grep -c "fail" || echo 0) +dkim_fail=$(grep -ci "DKIM" "$TEMP_MATCHES" 2>/dev/null | grep -c "fail" || echo 0) +greylist=$(grep -Eci "greylist|greylisted" "$TEMP_MATCHES" 2>/dev/null || echo 0) -# Single pass through file for all counts (10-50x faster than 20+ grep passes) -while IFS= read -r line; do - [[ "$line" =~ "=>"|"delivered" ]] && ((delivered++)) - [[ "$line" =~ "<=" ]] && ((sent++)) - [[ "$line" =~ "deferred"|"retry"|"temporarily rejected" ]] && ((deferred++)) - [[ "$line" =~ "SPF" ]] && [[ "$line" =~ "fail" ]] && ((spf_fail++)) - [[ "$line" =~ "DKIM" ]] && [[ "$line" =~ "fail" ]] && ((dkim_fail++)) - [[ "$line" =~ "greylist"|"greylisted" ]] && ((greylist++)) - [[ "$line" =~ "=>" ]] && ((received++)) +# Bounces: SMTP 5xx codes but NOT auth failures or successful deliveries +bounced=$(grep -Ev "authenticator failed|Authentication failed|saved mail to|=> " "$TEMP_MATCHES" 2>/dev/null | \ + grep -ciE "550 |551 |552 |553 |554 |bounced|Mail delivery failed" || echo 0) - # Bounces: 5xx codes but not successful delivery or auth failures - if [[ "$line" =~ "550"|"551"|"552"|"553"|"554"|"bounced"|"Mail delivery failed" ]]; then - if [[ ! "$line" =~ "authenticator failed"|"Authentication failed"|"saved mail to"|"=>" ]]; then - ((bounced++)) - fi - fi +# Rejections: rejected but not auth failures +rejected=$(grep -v "authenticator failed\|Authentication failed" "$TEMP_MATCHES" 2>/dev/null | \ + grep -ciE "rejected " || echo 0) - # Rejections: rejected but not auth failures - if [[ "$line" =~ "rejected" ]]; then - if [[ ! "$line" =~ "authenticator failed"|"Authentication failed" ]]; then - ((rejected++)) - fi - fi - - # Spam rejected: rejected and marked as spam - if [[ "$line" =~ "spam" ]] && [[ "$line" =~ "rejected"|"blocked"|"denied" ]]; then - ((spam_rejected++)) - fi -done < "$TEMP_MATCHES" +# Spam rejected: marked as spam AND rejected/blocked +spam_rejected=$(grep -i "spam" "$TEMP_MATCHES" 2>/dev/null | \ + grep -ciE "rejected|blocked|denied" || echo 0) # Count authentication events auth_failed=0 auth_success=0 @@ -1146,14 +1178,14 @@ TEMPLATE fi # Show historical statistics if history file exists - HISTORY_FILE="$HOME/.email-diagnostics-history.json" + HISTORY_FILE="$HOME/.email-diagnostics-history.txt" if [ -f "$HISTORY_FILE" ] && [ -s "$HISTORY_FILE" ]; then echo "" print_info " 📊 HISTORICAL BLACKLIST TRACKING:" echo "" # Count recorded events from history file (simplified approach) - history_events=$(grep -c "# Historical event recorded:" "$HISTORY_FILE" 2>/dev/null || echo 0) + history_events=$(grep -c "|" "$HISTORY_FILE" 2>/dev/null || echo 0) if [ "$history_events" -gt 0 ]; then echo " 📈 Blacklist History Summary:" @@ -1348,5 +1380,10 @@ cp "$TEMP_MATCHES" "$REPORT_FILE" print_info "Full log saved to: $REPORT_FILE" echo "" -# Cleanup -rm -f "$TEMP_MATCHES" "$TEMP_AUTH" "$TEMP_ALL" "$TEMP_BOUNCES" +# Wait for user (Issue 6.1: design standard requires press_enter before exit) +echo "" +press_enter + +# Cleanup (Issue 4.1: improved cleanup for all temp files) +rm -f "$TEMP_MATCHES" "$TEMP_AUTH" "$TEMP_ALL" "$TEMP_BOUNCES" \ + "$TEMP_BLACKLISTS" "$TEMP_BLACKLISTS_FILTERED" "$REPORT_FILE" 2>/dev/null || true