Update mail-status.sh

This commit is contained in:
2026-03-27 18:54:49 +01:00
parent 584e0ffe3f
commit 04fff874e3

View File

@@ -1,11 +1,39 @@
#!/bin/bash #!/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 set -euo pipefail
IFS=$'\n\t' IFS=$'\n\t'
# ============================================================================== # ==============================================================================
# 1. HELP MESSAGE # 2. HELP MESSAGE
# ============================================================================== # ==============================================================================
show_help() { show_help() {
echo "Usage: $0 [OPTIONS]" 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 export USE_COLOR=0
[[ -t 1 ]] && USE_COLOR=1 [[ -t 1 ]] && USE_COLOR=1
@@ -40,7 +62,6 @@ ID_FILTER=""
DATE_FILTER="" DATE_FILTER=""
MAX_INPUT_LEN=100 MAX_INPUT_LEN=100
# Parse arguments using Bash-native slicing (no fork)
while [[ "$#" -gt 0 ]]; do while [[ "$#" -gt 0 ]]; do
case $1 in case $1 in
-h|--help) show_help ;; -h|--help) show_help ;;
@@ -58,16 +79,14 @@ while [[ "$#" -gt 0 ]]; do
done done
# ============================================================================== # ==============================================================================
# 3. SAFE LOG FILE COLLECTION (Fixed sort -z bug & portability) # 4. LOG FILE COLLECTION
# ============================================================================== # ==============================================================================
LOG_FILES=() LOG_FILES=()
# Detect sort -z support and collect files safely
if sort -z </dev/null >/dev/null 2>&1; then 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 | sort -z) done < <(find /var/log -maxdepth 1 -name 'maillog*' -type f -print0 | sort -z)
else else
# POSIX Fallback (assumes no newlines in log filenames, which is safe in /var/log)
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 | sort) done < <(find /var/log -maxdepth 1 -name 'maillog*' -type f -print0 | sort)
@@ -78,26 +97,29 @@ if [ ${#LOG_FILES[@]} -eq 0 ]; then
exit 1 exit 1
fi fi
TMP_DATA=$(mktemp /tmp/postfix_log.XXXXXX)
trap 'rm -f -- "$TMP_DATA"' EXIT
# ============================================================================== # ==============================================================================
# 4. OPTIMIZED LOG READING # 5. OPTIMIZED LOG READING
# ============================================================================== # ==============================================================================
read_logs() { read_logs() {
if [[ -n "$DATE_FILTER" || -n "$ID_FILTER" ]]; then if [[ -z "$DATE_FILTER" ]]; then
zcat -f -- "${LOG_FILES[@]}" DATE_FILTER=$(date '+%b %e')
else
if [[ -f /var/log/maillog ]]; then
tail -n 50000 /var/log/maillog
else
zcat -f -- "${LOG_FILES[@]}" | tail -n 50000
fi 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 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" ' 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) { function shorten(addr, max_len) {
@@ -113,15 +135,28 @@ function shorten(addr, max_len) {
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"; cyan="\033[0;36m"; gray="\033[0;90m"
if (ENVIRON["USE_COLOR"] != "1") { if (ENVIRON["USE_COLOR"] != "1") {
blue=""; bold=""; reset=""; green=""; red=""; yellow=""; cyan=""; gray=""; 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" 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; full_time = $1 "-" $2 "-" $3;
qid = ""; qid = "";
@@ -130,19 +165,26 @@ 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) 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) { 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]; delete client_ip[qid]; delete from[qid]; num_ids--;
next; next;
} }
@@ -164,15 +206,17 @@ BEGIN {
if (length(s_reason) > 25) s_reason = substr(s_reason, 1, 22) ".."; if (length(s_reason) > 25) s_reason = substr(s_reason, 1, 22) "..";
} }
# Apply Filters
if ((f_filt == "" || index(s_from, f_filt) > 0) && if ((f_filt == "" || index(s_from, f_filt) > 0) &&
(t_filt == "" || index(s_to, t_filt) > 0) && (t_filt == "" || index(s_to, t_filt) > 0) &&
(i_filt == "" || index(qid, i_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", 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; 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) { 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) : "-";
@@ -189,7 +233,7 @@ BEGIN {
}' > "$TMP_DATA" }' > "$TMP_DATA"
# ============================================================================== # ==============================================================================
# 6. FINAL OUTPUT (POSIX-Safe Process Optimization) # 7. FINAL OUTPUT
# ============================================================================== # ==============================================================================
head -n 1 "$TMP_DATA" head -n 1 "$TMP_DATA"
sed '1d' "$TMP_DATA" | tail -n "$LIMIT" sed '1d' "$TMP_DATA" | tail -n "$LIMIT"