Update mail-status.sh

This commit is contained in:
2026-03-27 19:19:00 +01:00
parent 04fff874e3
commit b2fec3f7e8

View File

@@ -2,11 +2,11 @@
# Postfix Mail Summary Script (Hardened, Secure & Memory-Safe Version) # 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 -t 15
ulimit -v 500000 # Max virtual memory ~500MB ulimit -v 500000
ulimit -f 102400 # Max file size created by script (100MB) ulimit -f 102400
# ============================================================================== # ==============================================================================
# 1. LOCKFILE & PRE-FLIGHT CHECKS # 1. LOCKFILE & PRE-FLIGHT CHECKS
@@ -41,9 +41,9 @@ show_help() {
echo "Options:" echo "Options:"
echo " -h, --help Display this help message" echo " -h, --help Display this help message"
echo " -l, --list NUMBER Show the last X records (default: 10, max: 1000)" echo " -l, --list NUMBER Show the last X records (default: 10, max: 1000)"
echo " -f, --from EMAIL/DOMAIN Filter by SENDER (substring match)" echo " -f, --from EMAIL/DOMAIN Filter by SENDER"
echo " -t, --to EMAIL/DOMAIN Filter by RECIPIENT (substring match)" echo " -t, --to EMAIL/DOMAIN Filter by RECIPIENT"
echo " -i, --id QUEUE_ID Search for a specific Queue ID" 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 " -d, --date \"MMM DD\" Filter by date (e.g., \"Mar 27\")"
exit 0 exit 0
} }
@@ -62,35 +62,49 @@ ID_FILTER=""
DATE_FILTER="" DATE_FILTER=""
MAX_INPUT_LEN=100 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 while [[ "$#" -gt 0 ]]; do
case $1 in case $1 in
-h|--help) show_help ;; -h|--help) show_help ;;
-l|--list) -l|--list)
check_arg "$1" "${2:-}"
if [[ "$2" =~ ^[0-9]+$ ]]; then LIMIT="$2"; else LIMIT=10; fi if [[ "$2" =~ ^[0-9]+$ ]]; then LIMIT="$2"; else LIMIT=10; fi
(( LIMIT > MAX_LIMIT )) && LIMIT=$MAX_LIMIT (( LIMIT > MAX_LIMIT )) && LIMIT=$MAX_LIMIT
shift ;; shift ;;
-f|--from) FROM_FILTER="${2:0:$MAX_INPUT_LEN}"; shift ;; -f|--from)
-t|--to) TO_FILTER="${2:0:$MAX_INPUT_LEN}"; shift ;; check_arg "$1" "${2:-}"
-i|--id) ID_FILTER="${2:0:$MAX_INPUT_LEN}"; shift ;; FROM_FILTER="${2:0:$MAX_INPUT_LEN}"; shift ;;
-d|--date) DATE_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 ;; *) echo "Unknown parameter: $1. Use -h for help." >&2; exit 1 ;;
esac esac
shift shift
done done
DETAIL_VIEW=0
if [[ -n "$ID_FILTER" && "$ID_FILTER" != "NOQUEUE" ]]; then DETAIL_VIEW=1; fi
# ============================================================================== # ==============================================================================
# 4. LOG FILE COLLECTION # 4. LOG FILE COLLECTION
# ============================================================================== # ==============================================================================
LOG_FILES=() LOG_FILES=()
if sort -z </dev/null >/dev/null 2>&1; then while IFS= read -r -d '' file; do
while IFS= read -r -d '' file; do LOG_FILES+=("$file")
LOG_FILES+=("$file") done < <(find /var/log -maxdepth 1 -name 'maillog*' -type f -print0 2>/dev/null | sort -z)
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
if [ ${#LOG_FILES[@]} -eq 0 ]; then if [ ${#LOG_FILES[@]} -eq 0 ]; then
echo "Error: No Postfix logs found in /var/log/." >&2 echo "Error: No Postfix logs found in /var/log/." >&2
@@ -101,63 +115,37 @@ fi
# 5. OPTIMIZED LOG READING # 5. OPTIMIZED LOG READING
# ============================================================================== # ==============================================================================
read_logs() { read_logs() {
if [[ -z "$DATE_FILTER" ]]; then if [[ -z "$DATE_FILTER" ]]; then DATE_FILTER=$(date '+%b %e'); fi
DATE_FILTER=$(date '+%b %e')
fi
for log in "${LOG_FILES[@]}"; do for log in "${LOG_FILES[@]}"; do
if [[ ! -r "$log" ]]; then continue; fi if [[ ! -r "$log" ]]; then continue; fi
if [[ "$log" =~ \.gz$ ]]; then 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' zcat -f -- "$log" | awk -v d_filt="$DATE_FILTER" 'substr($0,1,length(d_filt)) == d_filt'
else 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' cat -- "$log" | awk -v d_filt="$DATE_FILTER" 'substr($0,1,length(d_filt)) == d_filt'
fi fi
done 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) { function shorten(addr, max_len) {
if (d_mode == 1) return addr;
if (length(addr) <= max_len) 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) "..."; 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 { 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" 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") { if (ENVIRON["USE_COLOR"] != "1") { blue=""; bold=""; reset=""; green=""; red=""; yellow=""; gray=""; cyan=""; }
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;
}
{
# Basic validation: Skip lines that are too short or suspicious
if (length($0) < 20 || length($0) > 1000) next;
if (d_mode == 0)
printf "%-16s %-12s %-16s %-36s %-31s %-12s %s\n", "DATE-TIME", "ID", "CLIENT_IP", "SENDER", "RECIPIENT", "STATUS", "REASON"
}
{
if (length($0) < 20 || length($0) > 2000) next;
full_time = $1 "-" $2 "-" $3; full_time = $1 "-" $2 "-" $3;
qid = ""; qid = "";
if ($6 ~ /^[A-F0-9]+:$/) { qid = $6; gsub(/:/, "", qid); } if ($6 ~ /^[A-F0-9]+:$/) { qid = $6; gsub(/:/, "", qid); }
@@ -165,69 +153,66 @@ BEGIN {
if (qid == "" && index($0, "NOQUEUE") == 0) next; if (qid == "" && index($0, "NOQUEUE") == 0) next;
# Capture Client IP
if (index($0, "client=") > 0) { if (index($0, "client=") > 0) {
match($0, /\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\]/); match($0, /\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\]/);
if (RLENGTH > 0) { if (RLENGTH > 0) client_ip[qid] = substr($0, RSTART+1, RLENGTH-2);
client_ip[qid] = substr($0, RSTART+1, RLENGTH-2);
num_ids++;
gc();
}
} }
# Capture Sender
if (index($0, "from=<") > 0) { if (index($0, "from=<") > 0) {
match($0, /from=<[^>]+>/); match($0, /from=<[^>]+>/);
if (RLENGTH > 0) from[qid] = substr($0, RSTART+6, RLENGTH-7); if (RLENGTH > 0) from[qid] = substr($0, RSTART+6, RLENGTH-7);
} }
# Final Transaction Processing
if (index($0, "to=<") > 0 && index($0, "status=") > 0) { if (index($0, "to=<") > 0 && index($0, "status=") > 0) {
s_ip = (client_ip[qid] != "") ? client_ip[qid] : "local"; s_ip = (client_ip[qid] != "") ? client_ip[qid] : "local";
if (s_ip == "127.0.0.1") { if (s_ip == "127.0.0.1") { delete client_ip[qid]; delete from[qid]; next; }
delete client_ip[qid]; delete from[qid]; num_ids--;
next;
}
s_from = (from[qid] != "") ? from[qid] : "-"; s_from = (from[qid] != "") ? from[qid] : "-";
match($0, /to=<[^>]+>/); match($0, /to=<[^>]+>/); s_to = (RLENGTH > 0) ? substr($0, RSTART+4, RLENGTH-5) : "-";
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_reason = "OK"; scol = green; r_col = cyan;
s_stat = (RLENGTH > 0) ? substr($0, RSTART+7, RLENGTH-7) : "unknown"; if (s_stat != "sent") {
if (s_stat == "deferred") { scol = yellow; r_col = yellow; }
s_reason = "-"; r_col = reset; scol = reset; else { scol = red; r_col = red; }
if (match($0, /\([^)]*\)/)) {
if (s_stat == "sent") { scol = green; s_reason = "OK"; r_col = cyan; } s_reason = substr($0, RSTART+1, RLENGTH-2);
else if (s_stat == "deferred") { scol = yellow; r_col = yellow; } if (d_mode == 0 && length(s_reason) > 30) s_reason = substr(s_reason, 1, 27) "..";
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) "..";
} }
# 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)) {
if ((f_filt == "" || index(s_from, f_filt) > 0) && if (d_mode == 1) {
(t_filt == "" || index(s_to, t_filt) > 0) && printf "\n%s%s--- Detail for ID: %s ---%s\n", bold, blue, qid, reset;
(i_filt == "" || index(qid, i_filt) > 0)) { printf "%-12s : %s\n", "Date", full_time;
printf "%s%-16s %s%-12s%s %s%-16s%s %-36s %-31s %s%-12s%s %s[%s]%s\n", printf "%-12s : %s\n", "Client IP", s_ip;
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; 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) { if (index($0, "NOQUEUE: reject:") > 0) {
match($0, /from=<[^>]+>/); f = (RLENGTH > 0) ? substr($0, RSTART+6, RLENGTH-7) : "-"; match($0, /from=<[^>]+>/); f = (RLENGTH > 0) ? substr($0, RSTART+6, RLENGTH-7) : "-";
match($0, /to=<[^>]+>/); t = (RLENGTH > 0) ? substr($0, RSTART+4, RLENGTH-5) : "-"; match($0, /to=<[^>]+>/); t = (RLENGTH > 0) ? substr($0, RSTART+4, RLENGTH-5) : "-";
n_ip = "unknown"; 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 (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) && s_reason = "Blocked";
(t_filt == "" || index(t, t_filt) > 0) && if (match($0, /[0-9](\.[0-9])+ [^;]+/)) s_reason = substr($0, RSTART, RLENGTH);
(i_filt == "" || i_filt == "NOQUEUE")) { else if (match($0, /[0-9]{3} [^;]+/)) s_reason = substr($0, RSTART, RLENGTH);
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; 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" }' > "$TMP_DATA"
@@ -235,5 +220,14 @@ BEGIN {
# ============================================================================== # ==============================================================================
# 7. FINAL OUTPUT # 7. FINAL OUTPUT
# ============================================================================== # ==============================================================================
head -n 1 "$TMP_DATA" if [[ "$DETAIL_VIEW" -eq 1 ]]; then
sed '1d' "$TMP_DATA" | tail -n "$LIMIT" # 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