From 1cafd05a66993b43c786f12e9a46d08ef77a3066 Mon Sep 17 00:00:00 2001 From: robott Date: Mon, 7 Sep 2026 08:41:49 +0200 Subject: [PATCH] update mail-status add -a IP add -c export csv fix logs redhat/debian --- mail-status.sh | 116 ++++++++++++++++++++++++++++++------------------- 1 file changed, 72 insertions(+), 44 deletions(-) diff --git a/mail-status.sh b/mail-status.sh index 39ea28d..8db0711 100644 --- a/mail-status.sh +++ b/mail-status.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Postfix Mail Summary Tool - Hardened & Domain-Focused Version (v2026.1.1) +# Postfix Mail Summary Tool - Hardened & Domain-Focused Version (v2026.2.0) # ============================================================================== # 1. RESOURCE LIMITS & ENVIRONMENT @@ -14,7 +14,6 @@ IFS=$'\n\t' # ============================================================================== # 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" @@ -29,7 +28,6 @@ trap 'rm -f "$TMP_DATA" 2>/dev/null' EXIT export USE_COLOR=0 [[ -t 1 ]] && USE_COLOR=1 -readonly USE_COLOR # ============================================================================== # 3. HELP MENU @@ -39,12 +37,15 @@ show_help() { echo "" echo "Options:" echo " -h, --help Display this help message" - 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 " -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 " -a, --ip IP_ADDRESS Filter by source/relay IP address" + echo " -d, --date \"MMM DD\" Filter by date (e.g., \"Sep 4\" or \"Sep 24\")" echo " Use \"\" to search through all available logs" + echo " -c, --csv Output data in CSV format (disables colors)" + echo " mail-stats -d ""Sep 4"" -c > report.csv" exit 0 } @@ -52,7 +53,8 @@ show_help() { # 4. ARGUMENT PARSING # ============================================================================== LIMIT=10; readonly MAX_LIMIT=1000; readonly MAX_INPUT_LEN=100 -FROM_FILTER=""; TO_FILTER=""; ID_FILTER=""; DATE_FILTER=""; DATE_SET=0 +FROM_FILTER=""; TO_FILTER=""; ID_FILTER=""; DATE_FILTER=""; IP_FILTER="" +DATE_SET=0; CSV_MODE=0 check_arg() { local opt="$1" @@ -74,7 +76,9 @@ while [[ "$#" -gt 0 ]]; do -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) + -a|--ip) check_arg "$1" "${2:-}"; IP_FILTER=$(sanitize "${2:-}"); shift ;; + -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 ;; *) echo "Unknown option: $1" >&2; exit 1 ;; @@ -86,11 +90,11 @@ done (( LIMIT < 1 )) && LIMIT=1 (( LIMIT > MAX_LIMIT )) && LIMIT=$MAX_LIMIT +# Date Formatting Fix for single-digit days if [ "$DATE_SET" -eq 0 ]; then - # If no date was provided, use today's (with an automatic space for single-digit days) DATE_FILTER=$(date '+%b %e') else - # If a date was provided manually, fix the format for single-digit days (changes "Sep 4" to "Sep 4") + # Automatically add the extra space for dates like "Sep 4" -> "Sep 4" DATE_FILTER=$(echo "$DATE_FILTER" | sed -E 's/^([a-zA-Z]{3})[[:space:]]+([0-9])$/\1 \2/') fi readonly DATE_FILTER @@ -98,29 +102,32 @@ readonly DATE_FILTER DETAIL_VIEW=0; [[ -n "$ID_FILTER" && "$ID_FILTER" != "NOQUEUE" ]] && DETAIL_VIEW=1 # ============================================================================== -# 5. LOG FILE DISCOVERY +# 5. LOG FILE DISCOVERY (Universal for Debian/Ubuntu & RHEL/CentOS) # ============================================================================== 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 -r) +while IFS= read -r -d '' file; do LOG_FILES+=("$file"); done < <(find /var/log -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." >&2; exit 1; } # ============================================================================== -# 6. LOG PROCESSING (AWK ENGINE) +# 6. LOG PROCESSING (SMART GREP & AWK ENGINE) # ============================================================================== process_logs() { for log in "${LOG_FILES[@]}"; do [ ! -r "$log" ] && continue + # Fast skipping: If date filter is set, check if log contains the date before extracting if [[ "$log" =~ \.gz$ ]]; then - gzip -t "$log" 2>/dev/null && zcat -- "$log" + [[ -n "$DATE_FILTER" ]] && ! zgrep -m 1 -q "$DATE_FILTER" "$log" 2>/dev/null && continue + zcat -- "$log" 2>/dev/null else - cat -- "$log" + [[ -n "$DATE_FILTER" ]] && ! grep -m 1 -q "$DATE_FILTER" "$log" 2>/dev/null && continue + cat -- "$log" 2>/dev/null fi done } -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); +process_logs | awk -v d_filt="$DATE_FILTER" -v f_filt="$FROM_FILTER" -v t_filt="$TO_FILTER" -v i_filt="$ID_FILTER" -v ip_filt="$IP_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; } @@ -157,7 +164,9 @@ 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; - if ((f_filt == "" || index(s_f, f_filt) > 0) && (t_filt == "" || index(s_to, t_filt) > 0) && (i_filt == "" || index(qid, i_filt) > 0)) { + + # Applying all filters including IP filter + 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)) { 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; } @@ -169,7 +178,9 @@ BEGIN { 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)) { + + # Applying filters for NOQUEUE items including IP + if ((f_filt == "" || index(f, f_filt) > 0) && (t_filt == "" || index(t, t_filt) > 0) && (ip_filt == "" || index(nip, ip_filt) > 0)) { print sort_k " " curr_t " NOQUEUE " nip " " f " " t " " s_st " " s_re; } } @@ -178,33 +189,50 @@ BEGIN { # ============================================================================== # 7. FINAL OUTPUT RENDERING # ============================================================================== +# Disable color if in CSV mode +[[ "$CSV_MODE" -eq 1 ]] && USE_COLOR=0 + 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" '{ + 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; + 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 + 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 - [[ "$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" + if [[ "$CSV_MODE" -eq 1 ]]; then + # CSV FORMAT OUTPUT + 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 + # COLORED FORMAT OUTPUT (Standard) + [[ "$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 + 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 +fi