From b2fec3f7e81a617a6b21b884fcec7bf7af7f7318 Mon Sep 17 00:00:00 2001 From: robott Date: Fri, 27 Mar 2026 19:19:00 +0100 Subject: [PATCH] Update mail-status.sh --- mail-status.sh | 194 ++++++++++++++++++++++++------------------------- 1 file changed, 94 insertions(+), 100 deletions(-) diff --git a/mail-status.sh b/mail-status.sh index a9d74ae..202a733 100644 --- a/mail-status.sh +++ b/mail-status.sh @@ -2,11 +2,11 @@ # Postfix Mail Summary Script (Hardened, Secure & Memory-Safe Version) # ============================================================================== -# 0. RESOURCE LIMITS (Anti-DoS Protection) +# 0. RESOURCE LIMITS # ============================================================================== -ulimit -t 15 # Max CPU time (seconds) -ulimit -v 500000 # Max virtual memory ~500MB -ulimit -f 102400 # Max file size created by script (100MB) +ulimit -t 15 +ulimit -v 500000 +ulimit -f 102400 # ============================================================================== # 1. LOCKFILE & PRE-FLIGHT CHECKS @@ -41,9 +41,9 @@ show_help() { echo "Options:" echo " -h, --help Display this help message" echo " -l, --list NUMBER Show the last X records (default: 10, max: 1000)" - echo " -f, --from EMAIL/DOMAIN Filter by SENDER (substring match)" - echo " -t, --to EMAIL/DOMAIN Filter by RECIPIENT (substring match)" - echo " -i, --id QUEUE_ID Search for a specific Queue ID" + echo " -f, --from EMAIL/DOMAIN Filter by SENDER" + echo " -t, --to EMAIL/DOMAIN Filter by RECIPIENT" + echo " -i, --id QUEUE_ID Search for a specific ID (Shows Detail View)" echo " -d, --date \"MMM DD\" Filter by date (e.g., \"Mar 27\")" exit 0 } @@ -62,35 +62,49 @@ ID_FILTER="" DATE_FILTER="" MAX_INPUT_LEN=100 +# Helper function to validate arguments +check_arg() { + if [[ -z "${2:-}" || "$2" == -* ]]; then + echo "Error: Option '$1' requires an argument." >&2 + exit 1 + fi +} + while [[ "$#" -gt 0 ]]; do case $1 in -h|--help) show_help ;; -l|--list) + check_arg "$1" "${2:-}" if [[ "$2" =~ ^[0-9]+$ ]]; then LIMIT="$2"; else LIMIT=10; fi (( LIMIT > MAX_LIMIT )) && LIMIT=$MAX_LIMIT shift ;; - -f|--from) FROM_FILTER="${2:0:$MAX_INPUT_LEN}"; shift ;; - -t|--to) TO_FILTER="${2:0:$MAX_INPUT_LEN}"; shift ;; - -i|--id) ID_FILTER="${2:0:$MAX_INPUT_LEN}"; shift ;; - -d|--date) DATE_FILTER="${2:0:$MAX_INPUT_LEN}"; shift ;; + -f|--from) + check_arg "$1" "${2:-}" + FROM_FILTER="${2:0:$MAX_INPUT_LEN}"; shift ;; + -t|--to) + check_arg "$1" "${2:-}" + TO_FILTER="${2:0:$MAX_INPUT_LEN}"; shift ;; + -i|--id) + check_arg "$1" "${2:-}" + ID_FILTER="${2:0:$MAX_INPUT_LEN}"; shift ;; + -d|--date) + check_arg "$1" "${2:-}" + DATE_FILTER="${2:0:$MAX_INPUT_LEN}"; shift ;; *) echo "Unknown parameter: $1. Use -h for help." >&2; exit 1 ;; esac shift done +DETAIL_VIEW=0 +if [[ -n "$ID_FILTER" && "$ID_FILTER" != "NOQUEUE" ]]; then DETAIL_VIEW=1; fi + # ============================================================================== # 4. LOG FILE COLLECTION # ============================================================================== LOG_FILES=() -if sort -z /dev/null 2>&1; then - while IFS= read -r -d '' file; do - LOG_FILES+=("$file") - done < <(find /var/log -maxdepth 1 -name 'maillog*' -type f -print0 | sort -z) -else - while IFS= read -r -d '' file; do - LOG_FILES+=("$file") - done < <(find /var/log -maxdepth 1 -name 'maillog*' -type f -print0 | sort) -fi +while IFS= read -r -d '' file; do + LOG_FILES+=("$file") +done < <(find /var/log -maxdepth 1 -name 'maillog*' -type f -print0 2>/dev/null | sort -z) if [ ${#LOG_FILES[@]} -eq 0 ]; then echo "Error: No Postfix logs found in /var/log/." >&2 @@ -101,63 +115,37 @@ fi # 5. OPTIMIZED LOG READING # ============================================================================== read_logs() { -if [[ -z "$DATE_FILTER" ]]; then - DATE_FILTER=$(date '+%b %e') - fi - + if [[ -z "$DATE_FILTER" ]]; then DATE_FILTER=$(date '+%b %e'); fi for log in "${LOG_FILES[@]}"; do if [[ ! -r "$log" ]]; then continue; fi - if [[ "$log" =~ \.gz$ ]]; then - # zcat už sám o sebe číta celý komprimovaný súbor zcat -f -- "$log" | awk -v d_filt="$DATE_FILTER" 'substr($0,1,length(d_filt)) == d_filt' else - # ZMENA: Namiesto tail použijeme cat, aby sme prešli celý súbor cat -- "$log" | awk -v d_filt="$DATE_FILTER" 'substr($0,1,length(d_filt)) == d_filt' fi done } # ============================================================================== -# 6. SECURE LOG ANALYSIS (With Memory Garbage Collection) +# 6. LOG ANALYSIS (AWK) # ============================================================================== -read_logs | awk -v f_filt="$FROM_FILTER" -v t_filt="$TO_FILTER" -v i_filt="$ID_FILTER" -v d_filt="$DATE_FILTER" ' +read_logs | awk -v f_filt="$FROM_FILTER" -v t_filt="$TO_FILTER" -v i_filt="$ID_FILTER" -v d_mode="$DETAIL_VIEW" ' function shorten(addr, max_len) { + if (d_mode == 1) return addr; if (length(addr) <= max_len) return addr; - if (index(addr, "@") > 0) { - split(addr, p, "@"); - user = p[1]; domain = "@" p[2]; - if (length(domain) > (max_len - 10)) return ".." substr(domain, length(domain)-(max_len-5)); - user_limit = max_len - length(domain) - 2; - if (user_limit < 3) user_limit = 3; - return substr(user, 1, user_limit) ".." domain; - } return substr(addr, 1, max_len-3) "..."; } -# Garbage collector to prevent RAM exhaustion from abandoned Queue IDs -function gc() { - if (num_ids > 2000) { - for (idx in client_ip) { - delete client_ip[idx]; delete from[idx]; - num_ids--; - if (num_ids < 1000) break; - } - } -} - BEGIN { - blue="\033[1;34m"; bold="\033[1m"; reset="\033[0m"; green="\033[1;32m"; red="\033[1;31m"; yellow="\033[1;33m"; cyan="\033[0;36m"; gray="\033[0;90m" - if (ENVIRON["USE_COLOR"] != "1") { - blue=""; bold=""; reset=""; green=""; red=""; yellow=""; cyan=""; gray=""; - } - printf "%-16s %-12s %-16s %-36s %-31s %-12s %s\n", "DATE-TIME", "ID", "CLIENT_IP", "SENDER", "RECIPIENT", "STATUS", "REASON" - num_ids = 0; + blue="\033[1;34m"; bold="\033[1m"; reset="\033[0m"; green="\033[1;32m"; red="\033[1;31m"; yellow="\033[1;33m"; gray="\033[0;90m"; cyan="\033[0;36m"; + if (ENVIRON["USE_COLOR"] != "1") { blue=""; bold=""; reset=""; green=""; red=""; yellow=""; gray=""; cyan=""; } + + if (d_mode == 0) + printf "%-16s %-12s %-16s %-36s %-31s %-12s %s\n", "DATE-TIME", "ID", "CLIENT_IP", "SENDER", "RECIPIENT", "STATUS", "REASON" } -{ - # Basic validation: Skip lines that are too short or suspicious - if (length($0) < 20 || length($0) > 1000) next; +{ + if (length($0) < 20 || length($0) > 2000) next; full_time = $1 "-" $2 "-" $3; qid = ""; if ($6 ~ /^[A-F0-9]+:$/) { qid = $6; gsub(/:/, "", qid); } @@ -165,69 +153,66 @@ BEGIN { if (qid == "" && index($0, "NOQUEUE") == 0) next; - # Capture Client IP if (index($0, "client=") > 0) { match($0, /\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\]/); - if (RLENGTH > 0) { - client_ip[qid] = substr($0, RSTART+1, RLENGTH-2); - num_ids++; - gc(); - } + if (RLENGTH > 0) client_ip[qid] = substr($0, RSTART+1, RLENGTH-2); } - # Capture Sender if (index($0, "from=<") > 0) { match($0, /from=<[^>]+>/); if (RLENGTH > 0) from[qid] = substr($0, RSTART+6, RLENGTH-7); } - # Final Transaction Processing if (index($0, "to=<") > 0 && index($0, "status=") > 0) { s_ip = (client_ip[qid] != "") ? client_ip[qid] : "local"; - if (s_ip == "127.0.0.1") { - delete client_ip[qid]; delete from[qid]; num_ids--; - next; - } - + if (s_ip == "127.0.0.1") { delete client_ip[qid]; delete from[qid]; next; } + s_from = (from[qid] != "") ? from[qid] : "-"; - match($0, /to=<[^>]+>/); - s_to = (RLENGTH > 0) ? substr($0, RSTART+4, RLENGTH-5) : "-"; + match($0, /to=<[^>]+>/); s_to = (RLENGTH > 0) ? substr($0, RSTART+4, RLENGTH-5) : "-"; + match($0, /status=[a-z]+/); s_stat = (RLENGTH > 0) ? substr($0, RSTART+7, RLENGTH-7) : "unknown"; - match($0, /status=[a-z]+/); - s_stat = (RLENGTH > 0) ? substr($0, RSTART+7, RLENGTH-7) : "unknown"; - - s_reason = "-"; r_col = reset; scol = reset; - - if (s_stat == "sent") { scol = green; s_reason = "OK"; r_col = cyan; } - else if (s_stat == "deferred") { scol = yellow; r_col = yellow; } - else if (s_stat == "bounced") { scol = red; r_col = red; } - - if (s_stat != "sent" && match($0, /\([^)]*\)/)) { - s_reason = substr($0, RSTART+1, RLENGTH-2); - if (length(s_reason) > 25) s_reason = substr(s_reason, 1, 22) ".."; + s_reason = "OK"; scol = green; r_col = cyan; + if (s_stat != "sent") { + if (s_stat == "deferred") { scol = yellow; r_col = yellow; } + else { scol = red; r_col = red; } + if (match($0, /\([^)]*\)/)) { + s_reason = substr($0, RSTART+1, RLENGTH-2); + if (d_mode == 0 && length(s_reason) > 30) s_reason = substr(s_reason, 1, 27) ".."; + } } - # Apply Filters - if ((f_filt == "" || index(s_from, f_filt) > 0) && - (t_filt == "" || index(s_to, t_filt) > 0) && - (i_filt == "" || index(qid, i_filt) > 0)) { - printf "%s%-16s %s%-12s%s %s%-16s%s %-36s %-31s %s%-12s%s %s[%s]%s\n", - reset, full_time, blue, qid, reset, gray, s_ip, reset, shorten(s_from, 35), shorten(s_to, 30), scol, s_stat, reset, r_col, s_reason, reset; + if ((f_filt == "" || index(s_from, f_filt) > 0) && (t_filt == "" || index(s_to, t_filt) > 0) && (i_filt == "" || index(qid, i_filt) > 0)) { + if (d_mode == 1) { + printf "\n%s%s--- Detail for ID: %s ---%s\n", bold, blue, qid, reset; + printf "%-12s : %s\n", "Date", full_time; + printf "%-12s : %s\n", "Client IP", s_ip; + printf "%-12s : %s\n", "Sender", s_from; + printf "%-12s : %s\n", "Recipient", s_to; + printf "%-12s : %s%s%s\n", "Status", scol, s_stat, reset; + printf "%-12s : %s%s%s\n", "Reason", r_col, s_reason, reset; + printf "%s--------------------------------%s\n", gray, reset; + } else { + printf "%s%-16s %s%-12s%s %s%-16s%s %-36s %-31s %s%-12s%s %s[%s]%s\n", + reset, full_time, blue, qid, reset, gray, s_ip, reset, shorten(s_from, 35), shorten(s_to, 30), scol, s_stat, reset, r_col, s_reason, reset; + } } - delete client_ip[qid]; delete from[qid]; num_ids--; + delete client_ip[qid]; delete from[qid]; } - - # Handle Blocked Connections (NOQUEUE) + if (index($0, "NOQUEUE: reject:") > 0) { match($0, /from=<[^>]+>/); f = (RLENGTH > 0) ? substr($0, RSTART+6, RLENGTH-7) : "-"; match($0, /to=<[^>]+>/); t = (RLENGTH > 0) ? substr($0, RSTART+4, RLENGTH-5) : "-"; n_ip = "unknown"; if (match($0, /\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\]/)) n_ip = substr($0, RSTART+1, RLENGTH-2); - if ((f_filt == "" || index(f, f_filt) > 0) && - (t_filt == "" || index(t, t_filt) > 0) && - (i_filt == "" || i_filt == "NOQUEUE")) { - printf "%s%-16s %s%-12s%s %s%-16s%s %-36s %-31s %s%-12s%s %s[Blocked]%s\n", - reset, full_time, red, "NOQUEUE", reset, gray, n_ip, reset, shorten(f, 35), shorten(t, 30), red, "REJECT", reset, yellow, "Policy", reset; + s_reason = "Blocked"; + if (match($0, /[0-9](\.[0-9])+ [^;]+/)) s_reason = substr($0, RSTART, RLENGTH); + else if (match($0, /[0-9]{3} [^;]+/)) s_reason = substr($0, RSTART, RLENGTH); + + if (d_mode == 0 && length(s_reason) > 30) s_reason = substr(s_reason, 1, 27) ".."; + + if ((f_filt == "" || index(f, f_filt) > 0) && (t_filt == "" || index(t, t_filt) > 0) && (i_filt == "" || i_filt == "NOQUEUE" || index("NOQUEUE", i_filt) > 0)) { + printf "%s%-16s %s%-12s%s %s%-16s%s %-36s %-31s %s%-12s%s %s[%s]%s\n", + reset, full_time, red, "NOQUEUE", reset, gray, n_ip, reset, shorten(f, 35), shorten(t, 30), red, "REJECT", reset, yellow, s_reason, reset; } } }' > "$TMP_DATA" @@ -235,5 +220,14 @@ BEGIN { # ============================================================================== # 7. FINAL OUTPUT # ============================================================================== -head -n 1 "$TMP_DATA" -sed '1d' "$TMP_DATA" | tail -n "$LIMIT" \ No newline at end of file +if [[ "$DETAIL_VIEW" -eq 1 ]]; then + # If Detail View was requested but no matching ID was found + if [ ! -s "$TMP_DATA" ]; then + echo "No records found for ID: $ID_FILTER" + else + cat "$TMP_DATA" + fi +else + head -n 1 "$TMP_DATA" + sed '1d' "$TMP_DATA" | tail -n "$LIMIT" +fi \ No newline at end of file