239 lines
9.1 KiB
Bash
239 lines
9.1 KiB
Bash
#!/bin/bash
|
|
# 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'
|
|
|
|
# ==============================================================================
|
|
# 2. HELP MESSAGE
|
|
# ==============================================================================
|
|
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 (substring match)"
|
|
echo " -t, --to EMAIL/DOMAIN Filter by RECIPIENT (substring match)"
|
|
echo " -i, --id QUEUE_ID Search for a specific Queue ID"
|
|
echo " -d, --date \"MMM DD\" Filter by date (e.g., \"Mar 27\")"
|
|
exit 0
|
|
}
|
|
|
|
# ==============================================================================
|
|
# 3. INITIALIZATION & ARGUMENT PARSING
|
|
# ==============================================================================
|
|
export USE_COLOR=0
|
|
[[ -t 1 ]] && USE_COLOR=1
|
|
|
|
LIMIT=10
|
|
MAX_LIMIT=1000
|
|
FROM_FILTER=""
|
|
TO_FILTER=""
|
|
ID_FILTER=""
|
|
DATE_FILTER=""
|
|
MAX_INPUT_LEN=100
|
|
|
|
while [[ "$#" -gt 0 ]]; do
|
|
case $1 in
|
|
-h|--help) show_help ;;
|
|
-l|--list)
|
|
if [[ "$2" =~ ^[0-9]+$ ]]; then LIMIT="$2"; else LIMIT=10; fi
|
|
(( LIMIT > MAX_LIMIT )) && LIMIT=$MAX_LIMIT
|
|
shift ;;
|
|
-f|--from) FROM_FILTER="${2:0:$MAX_INPUT_LEN}"; shift ;;
|
|
-t|--to) TO_FILTER="${2:0:$MAX_INPUT_LEN}"; shift ;;
|
|
-i|--id) ID_FILTER="${2:0:$MAX_INPUT_LEN}"; shift ;;
|
|
-d|--date) DATE_FILTER="${2:0:$MAX_INPUT_LEN}"; shift ;;
|
|
*) echo "Unknown parameter: $1. Use -h for help." >&2; exit 1 ;;
|
|
esac
|
|
shift
|
|
done
|
|
|
|
# ==============================================================================
|
|
# 4. LOG FILE COLLECTION
|
|
# ==============================================================================
|
|
LOG_FILES=()
|
|
if sort -z </dev/null >/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
|
|
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
|
|
echo "Error: No Postfix logs found in /var/log/." >&2
|
|
exit 1
|
|
fi
|
|
|
|
# ==============================================================================
|
|
# 5. OPTIMIZED LOG READING
|
|
# ==============================================================================
|
|
read_logs() {
|
|
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
|
|
}
|
|
|
|
# ==============================================================================
|
|
# 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) {
|
|
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) "...";
|
|
}
|
|
|
|
# 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;
|
|
}
|
|
{
|
|
# Basic validation: Skip lines that are too short or suspicious
|
|
if (length($0) < 20 || length($0) > 1000) 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;
|
|
|
|
# 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);
|
|
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]; num_ids--;
|
|
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 = "-"; r_col = reset; scol = reset;
|
|
|
|
if (s_stat == "sent") { scol = green; s_reason = "OK"; r_col = cyan; }
|
|
else if (s_stat == "deferred") { scol = yellow; r_col = yellow; }
|
|
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)) {
|
|
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--;
|
|
}
|
|
|
|
# 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) : "-";
|
|
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 ((f_filt == "" || index(f, f_filt) > 0) &&
|
|
(t_filt == "" || index(t, t_filt) > 0) &&
|
|
(i_filt == "" || i_filt == "NOQUEUE")) {
|
|
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;
|
|
}
|
|
}
|
|
}' > "$TMP_DATA"
|
|
|
|
# ==============================================================================
|
|
# 7. FINAL OUTPUT
|
|
# ==============================================================================
|
|
head -n 1 "$TMP_DATA"
|
|
sed '1d' "$TMP_DATA" | tail -n "$LIMIT" |