From ebf6e16cfd18a7dfd1b72e03c8b0f53c7a87a918 Mon Sep 17 00:00:00 2001 From: robott Date: Fri, 27 Mar 2026 22:11:45 +0100 Subject: [PATCH] new top --- mail-status.sh | 312 ++++++++++++++++++++++--------------------------- 1 file changed, 137 insertions(+), 175 deletions(-) diff --git a/mail-status.sh b/mail-status.sh index 93e1a63..4e19d53 100644 --- a/mail-status.sh +++ b/mail-status.sh @@ -1,241 +1,203 @@ #!/bin/bash -# Postfix Mail Summary Script (Hardened, Secure & Memory-Safe Version) +# Postfix Mail Summary Tool - Hardened & Domain-Focused Version (v2026.1.1) # ============================================================================== -# 0. RESOURCE LIMITS +# 1. RESOURCE LIMITS & ENVIRONMENT # ============================================================================== ulimit -t 15 ulimit -v 500000 ulimit -f 102400 -# ============================================================================== -# 1. LOCKFILE & PRE-FLIGHT CHECKS -# ============================================================================== -LOCKFILE="/tmp/postfix_summary.lock" -if [ -f "$LOCKFILE" ]; then - PID=$(cat "$LOCKFILE" 2>/dev/null) - if [ -n "$PID" ] && ps -p "$PID" > /dev/null 2>&1; then - echo "Error: Another instance (PID $PID) is already running." >&2 - exit 1 - else - rm -f "$LOCKFILE" - fi -fi - -find /tmp -name "postfix_log.*" -mmin +10 -user root -delete 2>/dev/null || true -TMP_DATA=$(mktemp /tmp/postfix_log.XXXXXX) - -trap 'rm -f "$LOCKFILE" "$TMP_DATA" 2>/dev/null' EXIT - -echo $$ > "$LOCKFILE" - set -euo pipefail IFS=$'\n\t' # ============================================================================== -# 2. HELP MESSAGE +# 2. LOCKFILE & PRE-FLIGHT CHECKS +# ============================================================================== +# Lockfile fallback if /var/lock is not writable +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 +readonly USE_COLOR + +# ============================================================================== +# 3. HELP MENU # ============================================================================== show_help() { echo "Usage: $0 [OPTIONS]" echo "" 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" - 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\")" + echo " -l, --list NUMBER Number of records to show (1-1000, default: 10)" + echo " -f, --from EMAIL/DOMAIN Filter by sender address" + echo " -t, --to EMAIL/DOMAIN Filter by recipient address" + echo " -i, --id QUEUE_ID Search for a specific ID (Detail View)" + echo " -d, --date \"MMM DD\" Filter by date (e.g., \"Mar 27\")" + echo " Use \"\" to search through all available logs" exit 0 } # ============================================================================== -# 3. INITIALIZATION & ARGUMENT PARSING +# 4. ARGUMENT PARSING # ============================================================================== -export USE_COLOR=0 -[[ -t 1 ]] && USE_COLOR=1 +LIMIT=10; readonly MAX_LIMIT=1000; readonly MAX_INPUT_LEN=100 +FROM_FILTER=""; TO_FILTER=""; ID_FILTER=""; DATE_FILTER=""; DATE_SET=0 -LIMIT=10 -MAX_LIMIT=1000 -FROM_FILTER="" -TO_FILTER="" -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 + local opt="$1" + shift + if [[ "$#" -lt 1 ]]; then + echo "Error: Option '$opt' requires an argument (can be \"\")." >&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"; else LIMIT=10; fi - (( LIMIT > MAX_LIMIT )) && LIMIT=$MAX_LIMIT - 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 ;; + -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 ;; -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 ;; + if [[ "$#" -lt 2 ]]; then echo "Error: Option '$1' requires an argument."; exit 1; fi + DATE_FILTER=$(sanitize "${2:-}"); DATE_SET=1; shift ;; + *) echo "Unknown option: $1" >&2; exit 1 ;; esac shift done -DETAIL_VIEW=0 -if [[ -n "$ID_FILTER" && "$ID_FILTER" != "NOQUEUE" ]]; then DETAIL_VIEW=1; fi +# Limit validation +(( LIMIT < 1 )) && LIMIT=1 +(( LIMIT > MAX_LIMIT )) && LIMIT=$MAX_LIMIT + +if [ "$DATE_SET" -eq 0 ]; then DATE_FILTER=$(date '+%b %e'); fi +readonly DATE_FILTER +DETAIL_VIEW=0; [[ -n "$ID_FILTER" && "$ID_FILTER" != "NOQUEUE" ]] && DETAIL_VIEW=1 # ============================================================================== -# 4. LOG FILE COLLECTION +# 5. LOG FILE DISCOVERY # ============================================================================== LOG_FILES=() -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 - exit 1 -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 -r) +[ ${#LOG_FILES[@]} -eq 0 ] && { echo "Error: No Postfix logs found." >&2; exit 1; } # ============================================================================== -# 5. OPTIMIZED LOG READING +# 6. LOG PROCESSING (AWK ENGINE) # ============================================================================== -read_logs() { - if [[ -z "$DATE_FILTER" ]]; then DATE_FILTER=$(date '+%b %e'); fi +process_logs() { for log in "${LOG_FILES[@]}"; do - if [[ ! -r "$log" ]]; then continue; fi + [ ! -r "$log" ] && continue if [[ "$log" =~ \.gz$ ]]; then - zcat -f -- "$log" | awk -v d_filt="$DATE_FILTER" 'substr($0,1,length(d_filt)) == d_filt' + gzip -t "$log" 2>/dev/null && zcat -- "$log" else - cat -- "$log" | awk -v d_filt="$DATE_FILTER" 'substr($0,1,length(d_filt)) == d_filt' + cat -- "$log" fi done } -# ============================================================================== -# 6. LOG ANALYSIS (AWK) -# ============================================================================== -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; - return substr(addr, 1, max_len-3) "..."; +process_logs | awk -v d_filt="$DATE_FILTER" -v f_filt="$FROM_FILTER" -v t_filt="$TO_FILTER" -v i_filt="$ID_FILTER" ' +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; } - -BEGIN { - 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" -} - +!($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) > 2000) next; - full_time = $1 "-" $2 "-" $3; - qid = ""; - if ($6 ~ /^[A-F0-9]+:$/) { qid = $6; gsub(/:/, "", qid); } - else if ($5 ~ /^[A-F0-9]+:$/) { qid = $5; gsub(/:/, "", qid); } - - if (qid == "" && index($0, "NOQUEUE") == 0) next; - - 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); + if (db_count > 100000) { split("", db_from); split("", db_time); split("", db_in_ip); 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 (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 (index($0, "from=<") > 0) { - match($0, /from=<[^>]+>/); - if (RLENGTH > 0) from[qid] = substr($0, RSTART+6, RLENGTH-7); + if (qid != "" && index($0, "client=") > 0) { + if (match($0, /\[[0-9]{1,3}\.[0-9.]+/)) { + cip = substr($0, RSTART+1, RLENGTH-1); + if (cip != "127.0.0.1" && db_in_ip[qid] == "") db_in_ip[qid] = cip; + } + db_time[qid] = curr_t; } - - 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]; next; } - - s_from = (from[qid] != "") ? from[qid] : "-"; - 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"; - - 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) ".."; - } - } - - 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]; + if (qid != "" && index($0, "from=<") > 0) { + match($0, /from=<[^>]*>/); if (RLENGTH > 0) db_from[qid] = substr($0, RSTART+6, RLENGTH-7); } - - 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); - - 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); - - # --- NOVÁ LOGIKA PRE GREYLIST --- - s_stat = "REJECT"; scol = red; - if (index($0, "Greylisted") > 0) { - s_stat = "GREYLIST"; - scol = yellow; + if (qid != "" && index($0, "to=<") > 0 && index($0, "status=") > 0) { + if (index($0, "relay=127.0.0.1") > 0 || index($0, "10025") > 0) next; + match($0, /to=<[^>]*>/); s_to = substr($0, RSTART+4, RLENGTH-5); + match($0, /status=[a-z]+/); s_st = substr($0, RSTART+7, RLENGTH-7); + rip = ""; if (match($0, /relay=[^ ]+\[[0-9]{1,3}\.[0-9.]+/)) { + temp_m = substr($0, RSTART, RLENGTH); match(temp_m, /\[[0-9.]+/); rip = substr(temp_m, RSTART+1, RLENGTH-1); } - # -------------------------------- - - 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), scol, s_stat, reset, yellow, s_reason, reset; + final_ip = (rip != "" && rip != "127.0.0.1") ? rip : (db_in_ip[qid] != "" ? db_in_ip[qid] : "local"); + 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; + if ((f_filt == "" || index(s_f, f_filt) > 0) && (t_filt == "" || index(s_to, t_filt) > 0) && (i_filt == "" || index(qid, i_filt) > 0)) { + 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; } } + if (index($0, "NOQUEUE: reject:") > 0 && (i_filt == "" || i_filt == "NOQUEUE")) { + match($0, /from=<[^>]*>/); f=(RLENGTH>0)?substr($0, RSTART+6, RLENGTH-7):"-"; + match($0, /to=<[^>]*>/); t=(RLENGTH>0)?substr($0, RSTART+4, RLENGTH-5):"-"; + if (match($0, /\[[0-9]{1,3}\.[0-9.]+/)) nip = substr($0, RSTART+1, RLENGTH-1); else nip = "unknown"; + s_st = (index($0, "Greylisted") > 0) ? "GREYLIST" : "REJECT"; + 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)) { + print sort_k " " curr_t " NOQUEUE " nip " " f " " t " " s_st " " s_re; + } + } }' > "$TMP_DATA" # ============================================================================== -# 7. FINAL OUTPUT +# 7. FINAL OUTPUT RENDERING # ============================================================================== 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 + 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 - head -n 1 "$TMP_DATA" - sed '1d' "$TMP_DATA" | tail -n "$LIMIT" + [[ "$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; + }' fi \ No newline at end of file