From 04fff874e376e83994a2f80e5f53c503607266c2 Mon Sep 17 00:00:00 2001 From: robott Date: Fri, 27 Mar 2026 18:54:49 +0100 Subject: [PATCH] Update mail-status.sh --- mail-status.sh | 106 ++++++++++++++++++++++++++++++++++--------------- 1 file changed, 75 insertions(+), 31 deletions(-) diff --git a/mail-status.sh b/mail-status.sh index db0c4ce..a9d74ae 100644 --- a/mail-status.sh +++ b/mail-status.sh @@ -1,11 +1,39 @@ #!/bin/bash -# Postfix Mail Summary Script (Final Hardened & Portable Version) +# Postfix Mail Summary Script (Hardened, Secure & Memory-Safe Version) + +# ============================================================================== +# 0. RESOURCE LIMITS (Anti-DoS Protection) +# ============================================================================== +ulimit -t 15 # Max CPU time (seconds) +ulimit -v 500000 # Max virtual memory ~500MB +ulimit -f 102400 # Max file size created by script (100MB) + +# ============================================================================== +# 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' # ============================================================================== -# 1. HELP MESSAGE +# 2. HELP MESSAGE # ============================================================================== show_help() { echo "Usage: $0 [OPTIONS]" @@ -21,14 +49,8 @@ show_help() { } # ============================================================================== -# 2. PRIVILEGE CHECK & TTY COLOR DETECTION +# 3. INITIALIZATION & ARGUMENT PARSING # ============================================================================== -if [[ $EUID -ne 0 ]]; then - echo "Error: This script must be run as root (use sudo)." >&2 - exit 1 -fi - -# Detect TTY for color support export USE_COLOR=0 [[ -t 1 ]] && USE_COLOR=1 @@ -40,7 +62,6 @@ ID_FILTER="" DATE_FILTER="" MAX_INPUT_LEN=100 -# Parse arguments using Bash-native slicing (no fork) while [[ "$#" -gt 0 ]]; do case $1 in -h|--help) show_help ;; @@ -58,16 +79,14 @@ while [[ "$#" -gt 0 ]]; do done # ============================================================================== -# 3. SAFE LOG FILE COLLECTION (Fixed sort -z bug & portability) +# 4. LOG FILE COLLECTION # ============================================================================== LOG_FILES=() -# Detect sort -z support and collect files safely 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 - # POSIX Fallback (assumes no newlines in log filenames, which is safe in /var/log) while IFS= read -r -d '' file; do LOG_FILES+=("$file") done < <(find /var/log -maxdepth 1 -name 'maillog*' -type f -print0 | sort) @@ -78,26 +97,29 @@ if [ ${#LOG_FILES[@]} -eq 0 ]; then exit 1 fi -TMP_DATA=$(mktemp /tmp/postfix_log.XXXXXX) -trap 'rm -f -- "$TMP_DATA"' EXIT - # ============================================================================== -# 4. OPTIMIZED LOG READING +# 5. OPTIMIZED LOG READING # ============================================================================== read_logs() { - if [[ -n "$DATE_FILTER" || -n "$ID_FILTER" ]]; then - zcat -f -- "${LOG_FILES[@]}" - else - if [[ -f /var/log/maillog ]]; then - tail -n 50000 /var/log/maillog - else - zcat -f -- "${LOG_FILES[@]}" | tail -n 50000 - 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 } # ============================================================================== -# 5. LOG ANALYSIS (Hardened AWK with Memory Cleanup) +# 6. SECURE LOG ANALYSIS (With Memory Garbage Collection) # ============================================================================== read_logs | awk -v f_filt="$FROM_FILTER" -v t_filt="$TO_FILTER" -v i_filt="$ID_FILTER" -v d_filt="$DATE_FILTER" ' function shorten(addr, max_len) { @@ -113,15 +135,28 @@ function shorten(addr, max_len) { 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; } { - if (d_filt != "" && index($1 " " $2, d_filt) == 0) next; + # Basic validation: Skip lines that are too short or suspicious + if (length($0) < 20 || length($0) > 1000) next; full_time = $1 "-" $2 "-" $3; qid = ""; @@ -130,19 +165,26 @@ 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); + if (RLENGTH > 0) { + client_ip[qid] = substr($0, RSTART+1, RLENGTH-2); + num_ids++; + gc(); + } } + # 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]; + delete client_ip[qid]; delete from[qid]; num_ids--; next; } @@ -164,15 +206,17 @@ BEGIN { if (length(s_reason) > 25) s_reason = substr(s_reason, 1, 22) ".."; } + # 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; } - delete client_ip[qid]; delete from[qid]; + delete client_ip[qid]; delete from[qid]; num_ids--; } + # 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) : "-"; @@ -189,7 +233,7 @@ BEGIN { }' > "$TMP_DATA" # ============================================================================== -# 6. FINAL OUTPUT (POSIX-Safe Process Optimization) +# 7. FINAL OUTPUT # ============================================================================== head -n 1 "$TMP_DATA" sed '1d' "$TMP_DATA" | tail -n "$LIMIT" \ No newline at end of file