update mail-status
add -a IP add -c export csv fix logs redhat/debian
This commit is contained in:
+43
-15
@@ -1,5 +1,5 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# Postfix Mail Summary Tool - Hardened & Domain-Focused Version (v2026.1.1)
|
# Postfix Mail Summary Tool - Hardened & Domain-Focused Version (v2026.2.0)
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# 1. RESOURCE LIMITS & ENVIRONMENT
|
# 1. RESOURCE LIMITS & ENVIRONMENT
|
||||||
@@ -14,7 +14,6 @@ IFS=$'\n\t'
|
|||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# 2. LOCKFILE & PRE-FLIGHT CHECKS
|
# 2. LOCKFILE & PRE-FLIGHT CHECKS
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# Lockfile fallback if /var/lock is not writable
|
|
||||||
LOCKFILE="/var/lock/postfix_summary.lock"
|
LOCKFILE="/var/lock/postfix_summary.lock"
|
||||||
[ -w /var/lock ] || LOCKFILE="/tmp/postfix_summary.lock"
|
[ -w /var/lock ] || LOCKFILE="/tmp/postfix_summary.lock"
|
||||||
|
|
||||||
@@ -29,7 +28,6 @@ trap 'rm -f "$TMP_DATA" 2>/dev/null' EXIT
|
|||||||
|
|
||||||
export USE_COLOR=0
|
export USE_COLOR=0
|
||||||
[[ -t 1 ]] && USE_COLOR=1
|
[[ -t 1 ]] && USE_COLOR=1
|
||||||
readonly USE_COLOR
|
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# 3. HELP MENU
|
# 3. HELP MENU
|
||||||
@@ -43,8 +41,11 @@ show_help() {
|
|||||||
echo " -f, --from EMAIL/DOMAIN Filter by sender address"
|
echo " -f, --from EMAIL/DOMAIN Filter by sender address"
|
||||||
echo " -t, --to EMAIL/DOMAIN Filter by recipient address"
|
echo " -t, --to EMAIL/DOMAIN Filter by recipient address"
|
||||||
echo " -i, --id QUEUE_ID Search for a specific ID (Detail View)"
|
echo " -i, --id QUEUE_ID Search for a specific ID (Detail View)"
|
||||||
echo " -d, --date \"MMM DD\" Filter by date (e.g., \"Mar 27\")"
|
echo " -a, --ip IP_ADDRESS Filter by source/relay IP address"
|
||||||
|
echo " -d, --date \"MMM DD\" Filter by date (e.g., \"Sep 4\" or \"Sep 24\")"
|
||||||
echo " Use \"\" to search through all available logs"
|
echo " Use \"\" to search through all available logs"
|
||||||
|
echo " -c, --csv Output data in CSV format (disables colors)"
|
||||||
|
echo " mail-stats -d ""Sep 4"" -c > report.csv"
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,7 +53,8 @@ show_help() {
|
|||||||
# 4. ARGUMENT PARSING
|
# 4. ARGUMENT PARSING
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
LIMIT=10; readonly MAX_LIMIT=1000; readonly MAX_INPUT_LEN=100
|
LIMIT=10; readonly MAX_LIMIT=1000; readonly MAX_INPUT_LEN=100
|
||||||
FROM_FILTER=""; TO_FILTER=""; ID_FILTER=""; DATE_FILTER=""; DATE_SET=0
|
FROM_FILTER=""; TO_FILTER=""; ID_FILTER=""; DATE_FILTER=""; IP_FILTER=""
|
||||||
|
DATE_SET=0; CSV_MODE=0
|
||||||
|
|
||||||
check_arg() {
|
check_arg() {
|
||||||
local opt="$1"
|
local opt="$1"
|
||||||
@@ -74,6 +76,8 @@ while [[ "$#" -gt 0 ]]; do
|
|||||||
-f|--from) check_arg "$1" "${2:-}"; FROM_FILTER=$(sanitize "${2:-}"); shift ;;
|
-f|--from) check_arg "$1" "${2:-}"; FROM_FILTER=$(sanitize "${2:-}"); shift ;;
|
||||||
-t|--to) check_arg "$1" "${2:-}"; TO_FILTER=$(sanitize "${2:-}"); shift ;;
|
-t|--to) check_arg "$1" "${2:-}"; TO_FILTER=$(sanitize "${2:-}"); shift ;;
|
||||||
-i|--id) check_arg "$1" "${2:-}"; ID_FILTER=$(sanitize "${2:-}"); shift ;;
|
-i|--id) check_arg "$1" "${2:-}"; ID_FILTER=$(sanitize "${2:-}"); shift ;;
|
||||||
|
-a|--ip) check_arg "$1" "${2:-}"; IP_FILTER=$(sanitize "${2:-}"); shift ;;
|
||||||
|
-c|--csv) CSV_MODE=1 ;;
|
||||||
-d|--date)
|
-d|--date)
|
||||||
if [[ "$#" -lt 2 ]]; then echo "Error: Option '$1' requires an argument."; exit 1; fi
|
if [[ "$#" -lt 2 ]]; then echo "Error: Option '$1' requires an argument."; exit 1; fi
|
||||||
DATE_FILTER=$(sanitize "${2:-}"); DATE_SET=1; shift ;;
|
DATE_FILTER=$(sanitize "${2:-}"); DATE_SET=1; shift ;;
|
||||||
@@ -86,11 +90,11 @@ done
|
|||||||
(( LIMIT < 1 )) && LIMIT=1
|
(( LIMIT < 1 )) && LIMIT=1
|
||||||
(( LIMIT > MAX_LIMIT )) && LIMIT=$MAX_LIMIT
|
(( LIMIT > MAX_LIMIT )) && LIMIT=$MAX_LIMIT
|
||||||
|
|
||||||
|
# Date Formatting Fix for single-digit days
|
||||||
if [ "$DATE_SET" -eq 0 ]; then
|
if [ "$DATE_SET" -eq 0 ]; then
|
||||||
# If no date was provided, use today's (with an automatic space for single-digit days)
|
|
||||||
DATE_FILTER=$(date '+%b %e')
|
DATE_FILTER=$(date '+%b %e')
|
||||||
else
|
else
|
||||||
# If a date was provided manually, fix the format for single-digit days (changes "Sep 4" to "Sep 4")
|
# Automatically add the extra space for dates like "Sep 4" -> "Sep 4"
|
||||||
DATE_FILTER=$(echo "$DATE_FILTER" | sed -E 's/^([a-zA-Z]{3})[[:space:]]+([0-9])$/\1 \2/')
|
DATE_FILTER=$(echo "$DATE_FILTER" | sed -E 's/^([a-zA-Z]{3})[[:space:]]+([0-9])$/\1 \2/')
|
||||||
fi
|
fi
|
||||||
readonly DATE_FILTER
|
readonly DATE_FILTER
|
||||||
@@ -98,27 +102,30 @@ readonly DATE_FILTER
|
|||||||
DETAIL_VIEW=0; [[ -n "$ID_FILTER" && "$ID_FILTER" != "NOQUEUE" ]] && DETAIL_VIEW=1
|
DETAIL_VIEW=0; [[ -n "$ID_FILTER" && "$ID_FILTER" != "NOQUEUE" ]] && DETAIL_VIEW=1
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# 5. LOG FILE DISCOVERY
|
# 5. LOG FILE DISCOVERY (Universal for Debian/Ubuntu & RHEL/CentOS)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
LOG_FILES=()
|
LOG_FILES=()
|
||||||
while IFS= read -r -d '' file; do LOG_FILES+=("$file"); done < <(find /var/log -maxdepth 1 -name 'maillog*' -type f -print0 2>/dev/null | sort -z -r)
|
while IFS= read -r -d '' file; do LOG_FILES+=("$file"); done < <(find /var/log -maxdepth 1 \( -name 'maillog*' -o -name 'mail.log*' \) -type f -print0 2>/dev/null | sort -z -r)
|
||||||
[ ${#LOG_FILES[@]} -eq 0 ] && { echo "Error: No Postfix logs found." >&2; exit 1; }
|
[ ${#LOG_FILES[@]} -eq 0 ] && { echo "Error: No Postfix logs found." >&2; exit 1; }
|
||||||
|
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# 6. LOG PROCESSING (AWK ENGINE)
|
# 6. LOG PROCESSING (SMART GREP & AWK ENGINE)
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
process_logs() {
|
process_logs() {
|
||||||
for log in "${LOG_FILES[@]}"; do
|
for log in "${LOG_FILES[@]}"; do
|
||||||
[ ! -r "$log" ] && continue
|
[ ! -r "$log" ] && continue
|
||||||
|
# Fast skipping: If date filter is set, check if log contains the date before extracting
|
||||||
if [[ "$log" =~ \.gz$ ]]; then
|
if [[ "$log" =~ \.gz$ ]]; then
|
||||||
gzip -t "$log" 2>/dev/null && zcat -- "$log"
|
[[ -n "$DATE_FILTER" ]] && ! zgrep -m 1 -q "$DATE_FILTER" "$log" 2>/dev/null && continue
|
||||||
|
zcat -- "$log" 2>/dev/null
|
||||||
else
|
else
|
||||||
cat -- "$log"
|
[[ -n "$DATE_FILTER" ]] && ! grep -m 1 -q "$DATE_FILTER" "$log" 2>/dev/null && continue
|
||||||
|
cat -- "$log" 2>/dev/null
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
process_logs | awk -v d_filt="$DATE_FILTER" -v f_filt="$FROM_FILTER" -v t_filt="$TO_FILTER" -v i_filt="$ID_FILTER" '
|
process_logs | awk -v d_filt="$DATE_FILTER" -v f_filt="$FROM_FILTER" -v t_filt="$TO_FILTER" -v i_filt="$ID_FILTER" -v ip_filt="$IP_FILTER" '
|
||||||
BEGIN {
|
BEGIN {
|
||||||
split("Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec", n);
|
split("Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec", n);
|
||||||
for(i in n) { m[n[i]]=sprintf("%02d",i); months[n[i]]=1; }
|
for(i in n) { m[n[i]]=sprintf("%02d",i); months[n[i]]=1; }
|
||||||
@@ -157,7 +164,9 @@ BEGIN {
|
|||||||
s_re = "OK"; if (match($0, /\([^)]*\)/)) { s_re = substr($0, RSTART+1, RLENGTH-2); gsub(/ /,"_",s_re); }
|
s_re = "OK"; if (match($0, /\([^)]*\)/)) { s_re = substr($0, RSTART+1, RLENGTH-2); gsub(/ /,"_",s_re); }
|
||||||
s_f = (db_from[qid] != "") ? db_from[qid] : "-";
|
s_f = (db_from[qid] != "") ? db_from[qid] : "-";
|
||||||
s_tm = (db_time[qid] != "") ? db_time[qid] : curr_t;
|
s_tm = (db_time[qid] != "") ? db_time[qid] : curr_t;
|
||||||
if ((f_filt == "" || index(s_f, f_filt) > 0) && (t_filt == "" || index(s_to, t_filt) > 0) && (i_filt == "" || index(qid, i_filt) > 0)) {
|
|
||||||
|
# Applying all filters including IP filter
|
||||||
|
if ((f_filt == "" || index(s_f, f_filt) > 0) && (t_filt == "" || index(s_to, t_filt) > 0) && (i_filt == "" || index(qid, i_filt) > 0) && (ip_filt == "" || index(final_ip, ip_filt) > 0)) {
|
||||||
sort_k = m[$1] sprintf("%02d", $2); tm_tmp=$3; gsub(/:/,"",tm_tmp); sort_k = sort_k tm_tmp;
|
sort_k = m[$1] sprintf("%02d", $2); tm_tmp=$3; gsub(/:/,"",tm_tmp); sort_k = sort_k tm_tmp;
|
||||||
print sort_k " " s_tm " " qid " " final_ip " " s_f " " s_to " " s_st " " s_re;
|
print sort_k " " s_tm " " qid " " final_ip " " s_f " " s_to " " s_st " " s_re;
|
||||||
}
|
}
|
||||||
@@ -169,7 +178,9 @@ BEGIN {
|
|||||||
s_st = (index($0, "Greylisted") > 0) ? "GREYLIST" : "REJECT";
|
s_st = (index($0, "Greylisted") > 0) ? "GREYLIST" : "REJECT";
|
||||||
s_re = "Rejected"; if (match($0, /: [^;]+; /)) s_re = substr($0, RSTART+2, RLENGTH-4); gsub(/ /,"_",s_re);
|
s_re = "Rejected"; if (match($0, /: [^;]+; /)) s_re = substr($0, RSTART+2, RLENGTH-4); gsub(/ /,"_",s_re);
|
||||||
sort_k = m[$1] sprintf("%02d", $2); tm_tmp=$3; gsub(/:/,"",tm_tmp); sort_k = sort_k tm_tmp;
|
sort_k = m[$1] sprintf("%02d", $2); tm_tmp=$3; gsub(/:/,"",tm_tmp); sort_k = sort_k tm_tmp;
|
||||||
if ((f_filt == "" || index(f, f_filt) > 0) && (t_filt == "" || index(t, t_filt) > 0)) {
|
|
||||||
|
# Applying filters for NOQUEUE items including IP
|
||||||
|
if ((f_filt == "" || index(f, f_filt) > 0) && (t_filt == "" || index(t, t_filt) > 0) && (ip_filt == "" || index(nip, ip_filt) > 0)) {
|
||||||
print sort_k " " curr_t " NOQUEUE " nip " " f " " t " " s_st " " s_re;
|
print sort_k " " curr_t " NOQUEUE " nip " " f " " t " " s_st " " s_re;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -178,6 +189,9 @@ BEGIN {
|
|||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# 7. FINAL OUTPUT RENDERING
|
# 7. FINAL OUTPUT RENDERING
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
|
# Disable color if in CSV mode
|
||||||
|
[[ "$CSV_MODE" -eq 1 ]] && USE_COLOR=0
|
||||||
|
|
||||||
if [[ "$DETAIL_VIEW" -eq 1 ]]; then
|
if [[ "$DETAIL_VIEW" -eq 1 ]]; then
|
||||||
if [ ! -s "$TMP_DATA" ]; then echo "No records found for ID: $ID_FILTER"; exit 0; fi
|
if [ ! -s "$TMP_DATA" ]; then echo "No records found for ID: $ID_FILTER"; exit 0; fi
|
||||||
sort -n "$TMP_DATA" | awk -v u_col="$USE_COLOR" '{
|
sort -n "$TMP_DATA" | awk -v u_col="$USE_COLOR" '{
|
||||||
@@ -190,6 +204,19 @@ if [[ "$DETAIL_VIEW" -eq 1 ]]; then
|
|||||||
printf "\n%s--- Detail for ID: %s ---%s\nDate: %s\nExternal IP: %s\nFrom: %s\nTo: %s\nStatus: %s\nReason: %s\n", blue,qid,reset,tm,ip,f,t,st,r
|
printf "\n%s--- Detail for ID: %s ---%s\nDate: %s\nExternal IP: %s\nFrom: %s\nTo: %s\nStatus: %s\nReason: %s\n", blue,qid,reset,tm,ip,f,t,st,r
|
||||||
}'
|
}'
|
||||||
else
|
else
|
||||||
|
if [[ "$CSV_MODE" -eq 1 ]]; then
|
||||||
|
# CSV FORMAT OUTPUT
|
||||||
|
echo "DATE-TIME,QUEUE_ID,EXT_IP,SENDER,RECIPIENT,STATUS,REASON"
|
||||||
|
sort -n "$TMP_DATA" | tail -n "$LIMIT" | awk '{
|
||||||
|
tm=$2; gsub(/_/," ",tm); id=$3; ip=$4;
|
||||||
|
from=$5; gsub(/[^[:print:]]/, "", from);
|
||||||
|
to=$6; gsub(/[^[:print:]]/, "", to);
|
||||||
|
st=$7;
|
||||||
|
re=$8; gsub(/[^[:print:]]/, "", re); gsub(/_/," ",re);
|
||||||
|
printf "\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\",\"%s\"\n", tm, id, ip, from, to, st, re
|
||||||
|
}'
|
||||||
|
else
|
||||||
|
# COLORED FORMAT OUTPUT (Standard)
|
||||||
[[ "$USE_COLOR" -eq 1 ]] && printf "\033[1m"
|
[[ "$USE_COLOR" -eq 1 ]] && printf "\033[1m"
|
||||||
printf "%-16s %-12s %-16s %-36s %-31s %-12s %s\n" "DATE-TIME" "QUEUE_ID" "EXT_IP" "SENDER" "RECIPIENT" "STATUS" "REASON"
|
printf "%-16s %-12s %-16s %-36s %-31s %-12s %s\n" "DATE-TIME" "QUEUE_ID" "EXT_IP" "SENDER" "RECIPIENT" "STATUS" "REASON"
|
||||||
[[ "$USE_COLOR" -eq 1 ]] && printf "\033[0m"
|
[[ "$USE_COLOR" -eq 1 ]] && printf "\033[0m"
|
||||||
@@ -208,3 +235,4 @@ else
|
|||||||
printf "%-16s %s%-12s%s %s%-16s%s %-36s %-31s %s%-12s%s %s[%s]%s\n", tm, (id=="NOQUEUE"?rd:b), id, r, gr, sprintf("%.15s", ip), r, d_cut(from, 35), d_cut(to, 30), scol, st, r, (st=="sent"?c:scol), re, r;
|
printf "%-16s %s%-12s%s %s%-16s%s %-36s %-31s %s%-12s%s %s[%s]%s\n", tm, (id=="NOQUEUE"?rd:b), id, r, gr, sprintf("%.15s", ip), r, d_cut(from, 35), d_cut(to, 30), scol, st, r, (st=="sent"?c:scol), re, r;
|
||||||
}'
|
}'
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
|||||||
Reference in New Issue
Block a user