From cb3678ed5ec9c001051d5d600d3321ff31f4ab26 Mon Sep 17 00:00:00 2001 From: robott Date: Mon, 7 Sep 2026 09:26:11 +0200 Subject: [PATCH] update mail-status rework rework script --- mail-status.sh | 390 +++++++++++++++++++++++++++++++------------------ 1 file changed, 246 insertions(+), 144 deletions(-) diff --git a/mail-status.sh b/mail-status.sh index 8736247..1746f6f 100644 --- a/mail-status.sh +++ b/mail-status.sh @@ -1,111 +1,111 @@ #!/bin/bash -# Postfix Mail Summary Tool - Hardened & Domain-Focused Version (v3.0.1) - -# 1. RESOURCE LIMITS & ENVIRONMENT -ulimit -t 15 -ulimit -v 500000 -ulimit -f 102400 +# Postfix Mail Summary Tool - PRODUCTION READY EDITION (v4.0.7) set -euo pipefail IFS=$'\n\t' -# 2. LOCKFILE & PRE-FLIGHT CHECKS -LOCKFILE="/var/lock/postfix_summary.lock" -[ -w /var/lock ] || LOCKFILE="/tmp/postfix_summary.lock" - -exec 9>>"$LOCKFILE" -if ! flock -n 9; then - echo "Error: Another instance is already running." >&2 - exit 1 -fi - -TMP_DATA=$(mktemp /tmp/postfix_log.XXXXXX) -trap 'rm -f "$TMP_DATA" 2>/dev/null' EXIT - -export USE_COLOR=0 -[[ -t 1 ]] && USE_COLOR=1 - -# 3. HELP MENU +# ============================================================================== +# 1. HELP MENU (EXTENDED & DETAILED) +# ============================================================================== show_help() { cat << 'EOF' Usage: mail-stats [OPTIONS] -FILTERING OPTIONS: - -d, --date "MMM DD" Filter by date (e.g., "Sep 4" or "Sep 24"). Default: today. - Use "" (empty string) to search all dates in available logs. - --time "HH:MM" Filter by specific time/hour (e.g., "14:30" or "14:"). - -f, --from EMAIL Filter by sender email or domain (e.g., "admin@" or "domain.com"). - -t, --to EMAIL Filter by recipient email or domain. - -a, --ip IP_ADDRESS Filter by source/relay IP address (e.g., "192.168.1."). - -s, --status STATUS Filter by delivery status (e.g., sent, deferred, reject, bounced). Case-insensitive. +FILTERING OPTIONS (Combine multiple filters to narrow down results): + -d, --date "MMM DD" Filter by exact date. Default is today. + Examples: -d "Sep 4" | -d "Oct 12" + Special: -d "" (Empty string searches ALL available dates) + --time "HH:MM" Filter by exact time or whole hour. + Examples: --time "14:30" (exact minute) | --time "14:" (entire hour) + -f, --from STRING Filter by sender email address or domain. + Examples: -f "admin@domain.com" | -f "paypal" + -t, --to STRING Filter by recipient email address or domain. + Examples: -t "user@local.sk" | -t "gmail.com" + -a, --ip STRING Filter by source/relay IP address (full or partial). + Examples: -a "192.168.1.50" | -a "10.0." + -s, --status STATUS Filter by delivery status (case-insensitive). + Examples: -s sent | -s deferred | -s reject | -s bounced | -s greylist -i, --id QUEUE_ID Search for a specific Postfix Queue ID (Enables Detail View). + Example: -i 4T3bV22xLnz1t + --min-size BYTES Show only emails larger than specified BYTES. + Example: --min-size 10485760 (Find emails larger than ~10MB) + -R, --regex Treat filters (-f, -t, -a, -s) as Regular Expressions. + Example: -R -f "^admin.*@.*\.sk$" -OUTPUT OPTIONS: - -l, --list NUMBER Number of records to show (1-1000, default: 10). - -S, --summary Show aggregate statistics (Top IPs, Senders, Recipients, Statuses) instead of list. - -c, --csv Output data in CSV format (disables colors). +OUTPUT & RUN MODE OPTIONS: + -l, --list NUMBER Number of latest records to display (1-1000). Default: 10. + Example: -l 50 + -S, --summary Show aggregate statistics (Top IPs, Senders, Data usage) instead of list. + -c, --csv Output data in CSV format (disables colors for easy Excel import). + -w, --watch Live Watch Mode (tail -f). Runs continuously and shows new emails in real-time. MISC OPTIONS: --log-dir PATH Custom path to log files (default: /var/log). - -h, --help Display this help message. + -h, --help Display this detailed help message. -EXAMPLES: - # Search for rejected emails today and show summary stats - mail-stats -s reject -S +============================================================================== +PRACTICAL EXAMPLES (Copy & Paste): - # Find all emails sent from a specific IP around 14:30 on Sep 4 - mail-stats -d "Sep 4" --time "14:30" -a "192.168.1.50" + 1. Live monitor all incoming REJECTED emails in real-time: + mail-stats -w -s reject - # Export 50 latest records for a specific target domain to a CSV file - mail-stats -t targetdomain.com -l 50 -c > report.csv + 2. Show statistical summary of all emails sent to 'gmail.com' today: + mail-stats -t gmail.com -S - # Check logs in a custom backup directory - mail-stats --log-dir /mnt/backups/mail_logs/ -d "Aug 15" + 3. Export the last 100 emails from a specific IP to a CSV file: + mail-stats -a 192.168.1.100 -l 100 -c > ip_report.csv + 4. Find all large emails (over 10MB) across all old logs (ignoring date): + mail-stats -d "" --min-size 10485760 + + 5. Find what happened around 14:30 today for a specific sender domain: + mail-stats --time "14:30" -f "@mydomain.sk" -l 50 + + 6. Use REGEX to find senders starting with 'info' or 'admin': + mail-stats -R -f "^(info|admin)@" +============================================================================== EOF exit 0 } -# 4. ARGUMENT PARSING -LIMIT=10; MAX_LIMIT=1000; MAX_INPUT_LEN=100 +# ============================================================================== +# 2. ARGUMENT PARSING +# ============================================================================== +LIMIT=10; MAX_LIMIT=1000 FROM_FILTER=""; TO_FILTER=""; ID_FILTER=""; DATE_FILTER=""; IP_FILTER="" -STATUS_FILTER=""; TIME_FILTER=""; LOG_DIR="/var/log" -DATE_SET=0; CSV_MODE=0; SUMMARY_MODE=0 +STATUS_FILTER=""; TIME_FILTER=""; LOG_DIR="/var/log"; MIN_SIZE=0 +DATE_SET=0; CSV_MODE=0; SUMMARY_MODE=0; WATCH_MODE=0; REGEX_MODE=0 check_arg() { - local opt="$1" - shift - if [[ "$#" -lt 1 ]]; then - echo "Error: Option '$opt' requires an argument." >&2 - exit 1 + if [[ "$2" -lt 1 ]]; then + echo "Error: Option '$1' requires an argument." >&2; exit 1 fi } -sanitize() { echo "${1:0:$MAX_INPUT_LEN}" | tr -cd '[:alnum:]@._ -:'; } - while [[ "$#" -gt 0 ]]; do case $1 in -h|--help) show_help ;; - -l|--list) check_arg "$1" "${2:-}"; if [[ "$2" =~ ^[0-9]+$ ]]; then LIMIT="$2"; fi; shift ;; - -f|--from) check_arg "$1" "${2:-}"; FROM_FILTER=$(sanitize "${2:-}"); shift ;; - -t|--to) check_arg "$1" "${2:-}"; TO_FILTER=$(sanitize "${2:-}"); shift ;; - -i|--id) check_arg "$1" "${2:-}"; ID_FILTER=$(sanitize "${2:-}"); shift ;; - -a|--ip) check_arg "$1" "${2:-}"; IP_FILTER=$(sanitize "${2:-}"); shift ;; - -s|--status) check_arg "$1" "${2:-}"; STATUS_FILTER=$(sanitize "${2:-}"); shift ;; - --time) check_arg "$1" "${2:-}"; TIME_FILTER=$(sanitize "${2:-}"); shift ;; - --log-dir) check_arg "$1" "${2:-}"; LOG_DIR="${2:-}"; shift ;; + -l|--list) check_arg "$1" $#; if [[ "$2" =~ ^[0-9]+$ ]]; then LIMIT="$2"; fi; shift ;; + -f|--from) check_arg "$1" $#; FROM_FILTER="$2"; shift ;; + -t|--to) check_arg "$1" $#; TO_FILTER="$2"; shift ;; + -i|--id) check_arg "$1" $#; ID_FILTER="$2"; shift ;; + -a|--ip) check_arg "$1" $#; IP_FILTER="$2"; shift ;; + -s|--status) check_arg "$1" $#; STATUS_FILTER="$2"; shift ;; + --time) check_arg "$1" $#; TIME_FILTER="$2"; shift ;; + --log-dir) check_arg "$1" $#; LOG_DIR="$2"; shift ;; + --min-size) check_arg "$1" $#; if [[ "$2" =~ ^[0-9]+$ ]]; then MIN_SIZE="$2"; fi; shift ;; -S|--summary) SUMMARY_MODE=1 ;; -c|--csv) CSV_MODE=1 ;; - -d|--date) - if [[ "$#" -lt 2 ]]; then echo "Error: Option '$1' requires an argument."; exit 1; fi - DATE_FILTER=$(sanitize "${2:-}"); DATE_SET=1; shift ;; + -w|--watch) WATCH_MODE=1 ;; + -R|--regex) REGEX_MODE=1 ;; + -d|--date) check_arg "$1" $#; DATE_FILTER="$2"; DATE_SET=1; shift ;; *) echo "Unknown option: $1" >&2; exit 1 ;; esac shift done -(( LIMIT < 1 )) && LIMIT=1 -(( LIMIT > MAX_LIMIT )) && LIMIT=$MAX_LIMIT +if (( LIMIT < 1 )); then LIMIT=1; fi +if (( LIMIT > MAX_LIMIT )); then LIMIT=$MAX_LIMIT; fi if [ "$DATE_SET" -eq 0 ]; then DATE_FILTER=$(date '+%b %e') @@ -114,49 +114,131 @@ else fi readonly DATE_FILTER -DETAIL_VIEW=0; [[ -n "$ID_FILTER" && "$ID_FILTER" != "NOQUEUE" ]] && DETAIL_VIEW=1 +DETAIL_VIEW=0 +if [[ -n "$ID_FILTER" && "$ID_FILTER" != "NOQUEUE" ]]; then + DETAIL_VIEW=1 +fi -# 5. LOG FILE DISCOVERY +export USE_COLOR=0 +if [[ -t 1 ]]; then + USE_COLOR=1 +fi +if [[ "$CSV_MODE" -eq 1 ]]; then + USE_COLOR=0 +fi +export CSV_MODE + +# SECURITY SAFEGUARD: Prevent ReDoS in Watch Mode +if [[ "$WATCH_MODE" -eq 1 && "$REGEX_MODE" -eq 1 ]]; then + echo "Security Warning: Regular Expressions (-R) are disabled in Watch Mode (-w) to prevent CPU locking (ReDoS)." >&2 + exit 1 +fi + +# ============================================================================== +# 3. RESOURCE LIMITS & LOCKFILES (PRODUCTION SAFEGUARDS) +# ============================================================================== +if [[ "$WATCH_MODE" -eq 1 ]]; then + LOCKFILE="/tmp/postfix_watch.lock" + ulimit -v 500000 +else + LOCKFILE="/tmp/postfix_summary.lock" + ulimit -t 15 + ulimit -v 500000 + ulimit -f 102400 +fi + +exec 9>>"$LOCKFILE" +if ! flock -n 9; then + echo "Error: Another instance of this mode is already running." >&2 + exit 1 +fi + +TMP_DATA=$(mktemp /tmp/postfix_log.XXXXXX) +trap 'rm -f "$TMP_DATA" 2>/dev/null' EXIT + +# ============================================================================== +# 4. LOG FILE DISCOVERY & PROCESSING ENGINE +# ============================================================================== if [ ! -d "$LOG_DIR" ]; then echo "Error: Directory $LOG_DIR does not exist." >&2; exit 1 fi -LOG_FILES=() -while IFS= read -r -d '' file; do LOG_FILES+=("$file"); done < <(find "$LOG_DIR" -maxdepth 1 \( -name 'maillog*' -o -name 'mail.log*' \) -type f -print0 2>/dev/null | sort -z -r) -[ ${#LOG_FILES[@]} -eq 0 ] && { echo "Error: No Postfix logs found in $LOG_DIR." >&2; exit 1; } +get_log_stream() { + if [[ "$WATCH_MODE" -eq 1 ]]; then + ACTIVE_LOG="" + if [[ -f "$LOG_DIR/maillog" ]]; then ACTIVE_LOG="$LOG_DIR/maillog"; fi + if [[ -z "$ACTIVE_LOG" && -f "$LOG_DIR/mail.log" ]]; then ACTIVE_LOG="$LOG_DIR/mail.log"; fi -# 6. LOG PROCESSING -process_logs() { - for log in "${LOG_FILES[@]}"; do - [ ! -r "$log" ] && continue - if [[ "$log" =~ \.gz$ ]]; then - [[ -n "$DATE_FILTER" ]] && ! zgrep -m 1 -q "$DATE_FILTER" "$log" 2>/dev/null && continue - zcat -- "$log" 2>/dev/null - else - [[ -n "$DATE_FILTER" ]] && ! grep -m 1 -q "$DATE_FILTER" "$log" 2>/dev/null && continue - cat -- "$log" 2>/dev/null + if [[ -z "$ACTIVE_LOG" ]]; then + echo "Error: No active log found to watch." >&2; exit 1 fi - done + echo "Starting Live Watch Mode on: $ACTIVE_LOG (Ctrl+C to stop)..." >&2 + tail -n 100 -F "$ACTIVE_LOG" + else + LOG_FILES=() + while IFS= read -r -d '' file; do + LOG_FILES+=("$file") + done < <(find "$LOG_DIR" -maxdepth 1 \( -name 'maillog*' -o -name 'mail.log*' \) -type f -print0 2>/dev/null | sort -z -r) + + if [ ${#LOG_FILES[@]} -eq 0 ]; then + echo "Error: No Postfix logs found." >&2; exit 1 + fi + + for log in "${LOG_FILES[@]}"; do + if [ ! -r "$log" ]; then continue; fi + + if [[ "$log" =~ \.gz$ ]]; then + if [[ -n "$DATE_FILTER" ]]; then + if ! zgrep -m 1 -q -e "$DATE_FILTER" -- "$log" 2>/dev/null; then continue; fi + fi + zcat -- "$log" 2>/dev/null + else + if [[ -n "$DATE_FILTER" ]]; then + if ! grep -m 1 -q -e "$DATE_FILTER" -- "$log" 2>/dev/null; then continue; fi + fi + cat -- "$log" 2>/dev/null + fi + done + fi } -process_logs | awk -v d_filt="$DATE_FILTER" -v tm_filt="$TIME_FILTER" -v f_filt="$FROM_FILTER" -v t_filt="$TO_FILTER" -v i_filt="$ID_FILTER" -v ip_filt="$IP_FILTER" -v st_filt="$STATUS_FILTER" ' +AWK_SCRIPT=' BEGIN { split("Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec", n); for(i in n) { m[n[i]]=sprintf("%02d",i); months[n[i]]=1; } db_count = 0; } +function chk(val, filt, is_regex) { + if (filt == "") return 1; + if (is_regex == 1 || is_regex == "1") { + return (tolower(val) ~ tolower(filt)); + } + return (index(tolower(val), tolower(filt)) > 0); +} +function sanitize(str) { gsub(/[^[:print:]]/, "", str); return str; } !($1 in months) { next } -{ if (length($0) < 20 || length($0) > 2048) next; } -(d_filt != "" && substr($0, 1, length(d_filt)) != d_filt) { next } +{ if (length($0) < 20 || length($0) > 4096) next; } +(w_mode == 0 && d_filt != "" && substr($0, 1, length(d_filt)) != d_filt) { next } (tm_filt != "" && index($3, tm_filt) != 1) { next } { - if (db_count > 100000) { split("", db_from); split("", db_time); split("", db_in_ip); db_count = 0; } + if (db_count > 100000) { split("", db_from); split("", db_time); split("", db_in_ip); split("", db_subj); split("", db_size); db_count = 0; } qid = ""; for(i=1; i<=9; i++) if($i ~ /^[A-F0-9]+:$/) { qid=$i; gsub(/:/,"",qid); break; } curr_t = $1"-"$2"_"$3; if (qid != "" && !(qid in db_from)) { db_count++; } + + if (qid != "" && index($0, "warning: header Subject:") > 0) { + if (match($0, /Subject: .* from /)) { + subj = substr($0, RSTART+9, RLENGTH-15); db_subj[qid] = sanitize(subj); + } + } + if (qid != "" && index($0, "size=") > 0) { + if (match($0, /size=[0-9]+/)) db_size[qid] = substr($0, RSTART+5, RLENGTH-5); + } if (index($0, "amavis") > 0 && index($0, "queued_as:") > 0) { - match($0, /queued_as: [A-F0-9]+/); aqid = substr($0, RSTART+11, RLENGTH-11); - if (match($0, /\[[0-9]{1,3}\.[0-9.]+/)) db_in_ip[aqid] = substr($0, RSTART+1, RLENGTH-1); + if(match($0, /queued_as: [A-F0-9]+/)) { + aqid = substr($0, RSTART+11, RLENGTH-11); + if (match($0, /\[[0-9]{1,3}\.[0-9.]+/)) db_in_ip[aqid] = substr($0, RSTART+1, RLENGTH-1); + } } if (qid != "" && index($0, "client=") > 0) { if (match($0, /\[[0-9]{1,3}\.[0-9.]+/)) { @@ -166,7 +248,7 @@ BEGIN { db_time[qid] = curr_t; } if (qid != "" && index($0, "from=<") > 0) { - match($0, /from=<[^>]*>/); if (RLENGTH > 0) db_from[qid] = substr($0, RSTART+6, RLENGTH-7); + if (match($0, /from=<[^>]*>/)) db_from[qid] = substr($0, RSTART+6, RLENGTH-7); } if (qid != "" && index($0, "to=<") > 0 && index($0, "status=") > 0) { if (index($0, "relay=127.0.0.1") > 0 || index($0, "10025") > 0) next; @@ -179,10 +261,13 @@ BEGIN { s_re = "OK"; if (match($0, /\([^)]*\)/)) { s_re = substr($0, RSTART+1, RLENGTH-2); gsub(/ /,"_",s_re); } s_f = (db_from[qid] != "") ? db_from[qid] : "-"; s_tm = (db_time[qid] != "") ? db_time[qid] : curr_t; + s_sz = (db_size[qid] != "") ? db_size[qid] : 0; + s_sub = (db_subj[qid] != "") ? db_subj[qid] : "-"; gsub(/ /,"_",s_sub); - if ((f_filt == "" || index(s_f, f_filt) > 0) && (t_filt == "" || index(s_to, t_filt) > 0) && (i_filt == "" || index(qid, i_filt) > 0) && (ip_filt == "" || index(final_ip, ip_filt) > 0) && (st_filt == "" || index(tolower(s_st), tolower(st_filt)) > 0)) { + if (s_sz >= m_sz && chk(s_f, f_filt, r_mode) && chk(s_to, t_filt, r_mode) && chk(qid, i_filt, r_mode) && chk(final_ip, ip_filt, r_mode) && chk(s_st, st_filt, r_mode)) { sort_k = m[$1] sprintf("%02d", $2); tm_tmp=$3; gsub(/:/,"",tm_tmp); sort_k = sort_k tm_tmp; - print sort_k " " s_tm " " qid " " final_ip " " s_f " " s_to " " s_st " " s_re; + out = sort_k " " s_tm " " qid " " final_ip " " s_f " " s_to " " s_st " " s_sz " " s_sub " " s_re; + if (w_mode == 1) { print out; fflush(); } else { print out > tmp_file; } } } if (index($0, "NOQUEUE: reject:") > 0 && (i_filt == "" || i_filt == "NOQUEUE")) { @@ -193,23 +278,62 @@ BEGIN { s_re = "Rejected"; if (match($0, /: [^;]+; /)) s_re = substr($0, RSTART+2, RLENGTH-4); gsub(/ /,"_",s_re); sort_k = m[$1] sprintf("%02d", $2); tm_tmp=$3; gsub(/:/,"",tm_tmp); sort_k = sort_k tm_tmp; - if ((f_filt == "" || index(f, f_filt) > 0) && (t_filt == "" || index(t, t_filt) > 0) && (ip_filt == "" || index(nip, ip_filt) > 0) && (st_filt == "" || index(tolower(s_st), tolower(st_filt)) > 0)) { - print sort_k " " curr_t " NOQUEUE " nip " " f " " t " " s_st " " s_re; + if (chk(f, f_filt, r_mode) && chk(t, t_filt, r_mode) && chk(nip, ip_filt, r_mode) && chk(s_st, st_filt, r_mode)) { + out = sort_k " " curr_t " NOQUEUE " nip " " f " " t " " s_st " 0 - " s_re; + if (w_mode == 1) { print out; fflush(); } else { print out > tmp_file; } } } -}' > "$TMP_DATA" +}' -# 7. FINAL OUTPUT RENDERING -[[ "$CSV_MODE" -eq 1 ]] && USE_COLOR=0 +# ============================================================================== +# 5. EXECUTION & OUTPUT RENDERING +# ============================================================================== +format_output() { + awk -v u_col="$USE_COLOR" -v csv="$CSV_MODE" -v d_view="$DETAIL_VIEW" ' + function csv_safe(s) { if(s ~ /^[=+\-@]/) { s=" "s }; gsub(/"/,"\"\"",s); return s; } + function d_cut(e, m) { if(length(e)<=m)return e; return ".." substr(e, length(e)-m+2); } + BEGIN { b="\033[1;34m"; r="\033[0m"; g="\033[1;32m"; rd="\033[1;31m"; y="\033[1;33m"; gr="\033[0;90m"; c="\033[0;36m"; if(u_col!="1")b=r=g=rd=y=gr=c=""; + if(csv==1 && d_view==0) print "DATE-TIME,QUEUE_ID,EXT_IP,SENDER,RECIPIENT,STATUS,SIZE_BYTES,SUBJECT,REASON"; } + { + tm=$2; gsub(/_/," ",tm); id=$3; ip=$4; + from=$5; to=$6; st=$7; sz=$8; + subj=$9; gsub(/_/," ",subj); + re=$10; gsub(/_/," ",re); + + if (d_view == 1) { + printf "\n%s--- Detail for ID: %s ---%s\nDate: %s\nIP: %s\nFrom: %s\nTo: %s\nStatus: %s\nSize: %s Bytes\nSubject: %s\nReason: %s\n", b,id,r,tm,ip,from,to,st,sz,subj,re; + fflush(); + } else if (csv == 1) { + printf "\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\"\n", csv_safe(tm), csv_safe(id), csv_safe(ip), csv_safe(from), csv_safe(to), csv_safe(st), csv_safe(sz), csv_safe(subj), csv_safe(re); + fflush(); + } else { + scol = (st == "sent") ? g : (st == "deferred" || st == "GREYLIST" ? y : rd); + s_re = (match(re, /[0-9]{3} [0-9]\.[0-9]\.[0-9]/)) ? substr(re, RSTART, RLENGTH) : substr(re, 1, 15); + printf "%-16s %s%-12s%s %s%-16s%s %-32s %-28s %s%-10s%s %s[%s]%s\n", tm, (id=="NOQUEUE"?rd:b), id, r, gr, sprintf("%.15s", ip), r, d_cut(from, 31), d_cut(to, 27), scol, st, r, (st=="sent"?c:scol), s_re, r; + fflush(); + } + }' +} + +if [[ "$WATCH_MODE" -eq 1 ]]; then + if [[ "$USE_COLOR" -eq 1 ]]; then + printf "\033[1m%-16s %-12s %-16s %-32s %-28s %-10s %s\033[0m\n" "DATE-TIME" "QUEUE_ID" "EXT_IP" "SENDER" "RECIPIENT" "STATUS" "REASON" + fi + get_log_stream | awk -v d_filt="$DATE_FILTER" -v tm_filt="$TIME_FILTER" -v f_filt="$FROM_FILTER" -v t_filt="$TO_FILTER" -v i_filt="$ID_FILTER" -v ip_filt="$IP_FILTER" -v st_filt="$STATUS_FILTER" -v m_sz="$MIN_SIZE" -v w_mode=1 -v r_mode="$REGEX_MODE" -v tmp_file="$TMP_DATA" "$AWK_SCRIPT" | format_output + exit 0 +fi + +# Run normal processing +get_log_stream | awk -v d_filt="$DATE_FILTER" -v tm_filt="$TIME_FILTER" -v f_filt="$FROM_FILTER" -v t_filt="$TO_FILTER" -v i_filt="$ID_FILTER" -v ip_filt="$IP_FILTER" -v st_filt="$STATUS_FILTER" -v m_sz="$MIN_SIZE" -v w_mode=0 -v r_mode="$REGEX_MODE" -v tmp_file="$TMP_DATA" "$AWK_SCRIPT" -# ================= SUMMARY MODE ================= if [[ "$SUMMARY_MODE" -eq 1 ]]; then total_records=$(wc -l < "$TMP_DATA") - [[ "$USE_COLOR" -eq 1 ]] && printf "\033[1;36m" + + if [[ "$USE_COLOR" -eq 1 ]]; then printf "\033[1;36m"; fi echo "==========================================================" echo " SUMMARY STATISTICS (Total records: ${total_records})" echo "==========================================================" - [[ "$USE_COLOR" -eq 1 ]] && printf "\033[0m" + if [[ "$USE_COLOR" -eq 1 ]]; then printf "\033[0m"; fi if [[ "$total_records" -eq 0 ]]; then echo "No data matched your filters." @@ -218,57 +342,35 @@ if [[ "$SUMMARY_MODE" -eq 1 ]]; then print_top() { local col=$1; local title=$2; local max=$3 - [[ "$USE_COLOR" -eq 1 ]] && printf "\n\033[1;33m%s\033[0m\n" "$title" || printf "\n%s\n" "$title" + if [[ "$USE_COLOR" -eq 1 ]]; then + printf "\n\033[1;33m%s\033[0m\n" "$title" + else + printf "\n%s\n" "$title" + fi awk "{print \$$col}" "$TMP_DATA" | sort | uniq -c | sort -nr | head -n "$max" | awk '{printf " %6d %s\n", $1, $2}' } print_top 7 "STATUS BREAKDOWN:" 10 print_top 4 "TOP 10 SOURCE IPs:" 10 print_top 5 "TOP 10 SENDERS:" 10 - print_top 6 "TOP 10 RECIPIENTS:" 10 + + if [[ "$USE_COLOR" -eq 1 ]]; then + printf "\n\033[1;33mBANDWIDTH CONSUMPTION:\033[0m\n" + else + printf "\nBANDWIDTH CONSUMPTION:\n" + fi + awk '{sum+=$8} END {printf " Total Processed Size: %.2f MB\n", sum/1048576}' "$TMP_DATA" exit 0 fi -# ================= DETAIL / LIST MODE ================= if [[ "$DETAIL_VIEW" -eq 1 ]]; then - if [ ! -s "$TMP_DATA" ]; then echo "No records found for ID: $ID_FILTER"; exit 0; fi - sort -n "$TMP_DATA" | awk -v u_col="$USE_COLOR" '{ - blue=(u_col=="1"?"\033[1;34m":""); reset=(u_col=="1"?"\033[0m":""); - tm=$2; gsub(/_/," ",tm); qid=$3; ip=$4; - f=$5; gsub(/[^[:print:]]/, "", f); - t=$6; gsub(/[^[:print:]]/, "", t); - st=$7; - r=$8; gsub(/[^[:print:]]/, "", r); gsub(/_/," ",r); - printf "\n%s--- Detail for ID: %s ---%s\nDate: %s\nExternal IP: %s\nFrom: %s\nTo: %s\nStatus: %s\nReason: %s\n", blue,qid,reset,tm,ip,f,t,st,r - }' -else - if [[ "$CSV_MODE" -eq 1 ]]; then - echo "DATE-TIME,QUEUE_ID,EXT_IP,SENDER,RECIPIENT,STATUS,REASON" - sort -n "$TMP_DATA" | tail -n "$LIMIT" | awk '{ - tm=$2; gsub(/_/," ",tm); id=$3; ip=$4; - from=$5; gsub(/[^[:print:]]/, "", from); - to=$6; gsub(/[^[:print:]]/, "", to); - st=$7; - re=$8; gsub(/[^[:print:]]/, "", re); gsub(/_/," ",re); - printf "\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\"\n", tm, id, ip, from, to, st, re - }' - else - [[ "$USE_COLOR" -eq 1 ]] && printf "\033[1m" - printf "%-16s %-12s %-16s %-36s %-31s %-12s %s\n" "DATE-TIME" "QUEUE_ID" "EXT_IP" "SENDER" "RECIPIENT" "STATUS" "REASON" - [[ "$USE_COLOR" -eq 1 ]] && printf "\033[0m" - - sort -n "$TMP_DATA" | tail -n "$LIMIT" | awk -v u_col="$USE_COLOR" ' - function d_cut(e, m) { if(length(e)<=m)return e; return ".." substr(e, length(e)-m+2); } - BEGIN { b="\033[1;34m"; r="\033[0m"; g="\033[1;32m"; rd="\033[1;31m"; y="\033[1;33m"; gr="\033[0;90m"; c="\033[0;36m"; if(u_col!="1")b=r=g=rd=y=gr=c=""; } - { - tm=$2; gsub(/_/," ",tm); id=$3; ip=$4; - from=$5; gsub(/[^[:print:]]/, "", from); - to=$6; gsub(/[^[:print:]]/, "", to); - st=$7; - re_f=$8; gsub(/[^[:print:]]/, "", re_f); gsub(/_/," ",re_f); - re = (match(re_f, /[0-9]{3} [0-9]\.[0-9]\.[0-9]/)) ? substr(re_f, RSTART, RLENGTH) : substr(re_f, 1, 15); - scol = (st == "sent") ? g : (st == "deferred" || st == "GREYLIST" ? y : rd); - printf "%-16s %s%-12s%s %s%-16s%s %-36s %-31s %s%-12s%s %s[%s]%s\n", tm, (id=="NOQUEUE"?rd:b), id, r, gr, sprintf("%.15s", ip), r, d_cut(from, 35), d_cut(to, 30), scol, st, r, (st=="sent"?c:scol), re, r; - }' + if [ ! -s "$TMP_DATA" ]; then + echo "No records found for ID: $ID_FILTER"; exit 0 fi + sort -n "$TMP_DATA" | format_output +else + if [[ "$USE_COLOR" -eq 1 && "$CSV_MODE" -eq 0 ]]; then + printf "\033[1m%-16s %-12s %-16s %-32s %-28s %-10s %s\033[0m\n" "DATE-TIME" "QUEUE_ID" "EXT_IP" "SENDER" "RECIPIENT" "STATUS" "REASON" + fi + sort -n "$TMP_DATA" | tail -n "$LIMIT" | format_output fi