From 2c4ac98ff8dbaba2a01349d223df049fa5c6304b Mon Sep 17 00:00:00 2001 From: robott Date: Fri, 27 Mar 2026 16:55:12 +0100 Subject: [PATCH] add mail-status file --- mail-status.sh | 195 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) create mode 100644 mail-status.sh diff --git a/mail-status.sh b/mail-status.sh new file mode 100644 index 0000000..db0c4ce --- /dev/null +++ b/mail-status.sh @@ -0,0 +1,195 @@ +#!/bin/bash +# Postfix Mail Summary Script (Final Hardened & Portable Version) + +set -euo pipefail +IFS=$'\n\t' + +# ============================================================================== +# 1. 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 +} + +# ============================================================================== +# 2. PRIVILEGE CHECK & TTY COLOR DETECTION +# ============================================================================== +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 + +LIMIT=10 +MAX_LIMIT=1000 +FROM_FILTER="" +TO_FILTER="" +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 ;; + -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 + +# ============================================================================== +# 3. SAFE LOG FILE COLLECTION (Fixed sort -z bug & portability) +# ============================================================================== +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) +fi + +if [ ${#LOG_FILES[@]} -eq 0 ]; then + echo "Error: No Postfix logs found in /var/log/." >&2 + exit 1 +fi + +TMP_DATA=$(mktemp /tmp/postfix_log.XXXXXX) +trap 'rm -f -- "$TMP_DATA"' EXIT + +# ============================================================================== +# 4. 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 + fi +} + +# ============================================================================== +# 5. LOG ANALYSIS (Hardened AWK with Memory Cleanup) +# ============================================================================== +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) "..."; +} + +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" +} +{ + if (d_filt != "" && index($1 " " $2, d_filt) == 0) 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; + + 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 (index($0, "from=<") > 0) { + match($0, /from=<[^>]+>/); + if (RLENGTH > 0) from[qid] = substr($0, RSTART+6, RLENGTH-7); + } + + 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]; + 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) ".."; + } + + 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]; + } + + 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" + +# ============================================================================== +# 6. FINAL OUTPUT (POSIX-Safe Process Optimization) +# ============================================================================== +head -n 1 "$TMP_DATA" +sed '1d' "$TMP_DATA" | tail -n "$LIMIT" \ No newline at end of file