Update mail-status.sh
This commit is contained in:
1 parent
b5937cc39b
commit
dde33eec1a
1 file changed
+71
-62
+71
-62
@@ -1,11 +1,16 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# ==============================================================================
|
# ==============================================================================
|
||||||
# mail-stats - Postfix Mail Summary Tool (v5.0.2)
|
# mail-stats - Postfix Mail Summary Tool (v5.0.3)
|
||||||
#
|
#
|
||||||
# Requires: bash >= 4.2, GNU coreutils/findutils, util-linux (flock),
|
# Requires: bash >= 4.2, GNU coreutils/findutils, util-linux (flock),
|
||||||
# gawk (recommended) or mawk 1.3.4 (verified by a self-test at start)
|
# gawk (recommended) or mawk 1.3.4 (verified by a self-test at start)
|
||||||
# Optional: gzip/xz/bzip2/zstd (rotated logs), journalctl, less
|
# Optional: gzip/xz/bzip2/zstd (rotated logs), journalctl, less
|
||||||
#
|
#
|
||||||
|
# 5.0.3: log injection fixes (attacker-controlled Subject/addresses/remote
|
||||||
|
# replies could forge "sent"/"reject" records or hide deliveries):
|
||||||
|
# - parser anchors on the program/queue-id field positions
|
||||||
|
# - delivery lines validated against the fixed Postfix field structure
|
||||||
|
# - amavis log lines no longer parsed (linking only via Postfix itself)
|
||||||
# 5.0.2: security fixes
|
# 5.0.2: security fixes
|
||||||
# - arithmetic injection via MAIL_STATS_* (command execution) fixed
|
# - arithmetic injection via MAIL_STATS_* (command execution) fixed
|
||||||
# - env hardening (TMPDIR/XDG_RUNTIME_DIR ignored as root, LESS*/GZIP/XZ/
|
# - env hardening (TMPDIR/XDG_RUNTIME_DIR ignored as root, LESS*/GZIP/XZ/
|
||||||
@@ -50,7 +55,7 @@ if (( EUID == 0 )); then
|
|||||||
unset -v TMPDIR XDG_RUNTIME_DIR
|
unset -v TMPDIR XDG_RUNTIME_DIR
|
||||||
fi
|
fi
|
||||||
|
|
||||||
readonly VERSION="5.0.2"
|
readonly VERSION="5.0.3"
|
||||||
|
|
||||||
die() { local m="$*"; printf 'Error: %s\n' "${m//[[:cntrl:]]/?}" >&2; exit 1; }
|
die() { local m="$*"; printf 'Error: %s\n' "${m//[[:cntrl:]]/?}" >&2; exit 1; }
|
||||||
warn() { local m="$*"; printf 'Warning: %s\n' "${m//[[:cntrl:]]/?}" >&2; }
|
warn() { local m="$*"; printf 'Warning: %s\n' "${m//[[:cntrl:]]/?}" >&2; }
|
||||||
@@ -431,11 +436,13 @@ function parse_ts() {
|
|||||||
if (NF < 6 || $2 !~ /^[0-9]+$/ || $3 !~ /^[0-9][0-9]:[0-9][0-9]:[0-9][0-9]$/) return 0
|
if (NF < 6 || $2 !~ /^[0-9]+$/ || $3 !~ /^[0-9][0-9]:[0-9][0-9]:[0-9][0-9]$/) return 0
|
||||||
l_mm = mon[$1]; l_dd = sprintf("%02d", $2); l_time = $3
|
l_mm = mon[$1]; l_dd = sprintf("%02d", $2); l_time = $3
|
||||||
l_y = (l_mm + 0 > cur_m) ? cur_y - 1 : cur_y
|
l_y = (l_mm + 0 > cur_m) ? cur_y - 1 : cur_y
|
||||||
return 1
|
pf = 5
|
||||||
|
return ($5 ~ /^[^ ]+\[[0-9]+\]:$/)
|
||||||
}
|
}
|
||||||
if ($1 ~ /^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]T[0-9][0-9]:[0-9][0-9]:[0-9][0-9]/) {
|
if ($1 ~ /^[0-9][0-9][0-9][0-9]-[0-9][0-9]-[0-9][0-9]T[0-9][0-9]:[0-9][0-9]:[0-9][0-9]/) {
|
||||||
l_y = substr($1, 1, 4); l_mm = substr($1, 6, 2); l_dd = substr($1, 9, 2); l_time = substr($1, 12, 8)
|
l_y = substr($1, 1, 4); l_mm = substr($1, 6, 2); l_dd = substr($1, 9, 2); l_time = substr($1, 12, 8)
|
||||||
return 1
|
pf = 3
|
||||||
|
return (NF >= 4 && $3 ~ /^[^ ]+\[[0-9]+\]:$/)
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
@@ -527,21 +534,23 @@ function emit(q, to, relay, st, dsn, reason, rec) {
|
|||||||
out(rec)
|
out(rec)
|
||||||
}
|
}
|
||||||
|
|
||||||
function handle_reject(q, p, r, f, t, ip, ss, re, dsn, st, rec) {
|
function handle_reject(q, rest, p, r, f, t, ip, ss, re, dsn, st, rec) {
|
||||||
p = index($0, "reject: ")
|
# rest: "reject: RCPT from host[ip]: 554 5.7.1 ...; from=<s> to=<r> proto=... helo=<...>"
|
||||||
r = substr($0, p + 8)
|
r = rest; sub(/^(milter-)?reject: /, "", r)
|
||||||
ip = (q != "NOQUEUE" && (q in c_ip)) ? c_ip[q] : "unknown"
|
ip = (q != "NOQUEUE" && (q in c_ip)) ? c_ip[q] : "unknown"
|
||||||
if (match(r, /^[A-Z-]+ from [^ ]*\[[^]]*\]: /)) {
|
if (match(r, /^[A-Z-]+ from [^ \[]*\[[^] ]*\]: /)) {
|
||||||
ss = substr(r, 1, RLENGTH); r = substr(r, RLENGTH + 1)
|
ss = substr(r, 1, RLENGTH); r = substr(r, RLENGTH + 1)
|
||||||
sub(/\]: $/, "", ss); sub(/^.*\[/, "", ss); ip = clean(ss, 64)
|
sub(/\]: $/, "", ss); sub(/^[^\[]*\[/, "", ss); ip = clean(ss, 64)
|
||||||
|
}
|
||||||
|
f = "-"; t = "-"
|
||||||
|
# Postfix appends "; from=<..> to=<..> proto=" after the reply text
|
||||||
|
if (match(r, /; from=<[^>]*> to=<[^>]*> proto=/)) {
|
||||||
|
ss = substr(r, RSTART, RLENGTH); r = substr(r, 1, RSTART - 1)
|
||||||
|
match(ss, /from=<[^>]*>/); f = substr(ss, RSTART + 6, RLENGTH - 7); f = (f == "") ? "<>" : clean(f, 0)
|
||||||
|
match(ss, /to=<[^>]*>/); t = clean(substr(ss, RSTART + 4, RLENGTH - 5), 0)
|
||||||
}
|
}
|
||||||
f = "-"
|
|
||||||
if (match($0, /from=<[^>]*>/)) { f = substr($0, RSTART + 6, RLENGTH - 7); f = (f == "") ? "<>" : clean(f, 0) }
|
|
||||||
t = "-"
|
|
||||||
if (match($0, / to=<[^>]*>/)) t = clean(substr($0, RSTART + 5, RLENGTH - 6), 0)
|
|
||||||
p = index(r, "; from=<"); if (p > 0) r = substr(r, 1, p - 1)
|
|
||||||
re = clean(r, 512)
|
re = clean(r, 512)
|
||||||
dsn = "-"; if (match(re, /[245]\.[0-9]+\.[0-9]+/)) dsn = substr(re, RSTART, RLENGTH)
|
dsn = "-"; if (match(re, /^[245][0-9][0-9] [245]\.[0-9]+\.[0-9]+/)) { dsn = substr(re, RSTART + 4, RLENGTH - 4) }
|
||||||
st = (re ~ /[Gg]reylist/) ? "GREYLIST" : "REJECT"
|
st = (re ~ /[Gg]reylist/) ? "GREYLIST" : "REJECT"
|
||||||
rec = l_key OFS l_ts OFS ((q in c_time) ? c_time[q] : l_ts) OFS q OFS ((q in orig) ? orig[q] : "-")
|
rec = l_key OFS l_ts OFS ((q in c_time) ? c_time[q] : l_ts) OFS q OFS ((q in orig) ? orig[q] : "-")
|
||||||
rec = rec OFS ip OFS "-" OFS f OFS t OFS st OFS ((q in sz) ? sz[q] : 0)
|
rec = rec OFS ip OFS "-" OFS f OFS t OFS st OFS ((q in sz) ? sz[q] : 0)
|
||||||
@@ -549,79 +558,78 @@ function handle_reject(q, p, r, f, t, ip, ss, re, dsn, st, rec) {
|
|||||||
out(rec)
|
out(rec)
|
||||||
}
|
}
|
||||||
|
|
||||||
function amavis( q1, q2, ip) {
|
|
||||||
if (!match($0, /Queue-ID: [0-9A-Za-z]+/)) return
|
|
||||||
q1 = substr($0, RSTART + 10, RLENGTH - 10)
|
|
||||||
if (!match($0, /queued_as: [0-9A-Za-z]+/)) return
|
|
||||||
q2 = substr($0, RSTART + 11, RLENGTH - 11)
|
|
||||||
link(q1, q2)
|
|
||||||
if (match($0, /\[[0-9A-Fa-f:.]+\]/)) {
|
|
||||||
ip = substr($0, RSTART + 1, RLENGTH - 2)
|
|
||||||
if (!is_loop(ip) && (!(q2 in c_ip) || is_loop(c_ip[q2]))) {
|
|
||||||
c_ip[q2] = ip
|
|
||||||
if (q2 in pn) flush_pend(q2, 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
{
|
||||||
if (length($0) < 20 || !parse_ts()) next
|
if (length($0) < 20 || !parse_ts()) next
|
||||||
tt = l_time; gsub(/:/, "", tt)
|
tt = l_time; gsub(/:/, "", tt)
|
||||||
l_key = l_y l_mm l_dd tt
|
l_key = l_y l_mm l_dd tt
|
||||||
l_ts = l_y "-" l_mm "-" l_dd " " l_time
|
l_ts = l_y "-" l_mm "-" l_dd " " l_time
|
||||||
|
|
||||||
if (index($0, "NOQUEUE: ") && $0 ~ /NOQUEUE: (milter-)?reject: /) { handle_reject("NOQUEUE"); next }
|
# Everything after "<program>[pid]: <QUEUE_ID>: " is untrusted text only where
|
||||||
|
# Postfix embeds foreign data (addresses, Subject, remote replies). All
|
||||||
|
# matches below are anchored to the start of that remainder.
|
||||||
|
qf = $(pf + 1)
|
||||||
|
rest = substr($0, index($0, " " $pf " " qf " ") + length($pf) + length(qf) + 3)
|
||||||
|
|
||||||
if (index($0, "Queue-ID: ") && index($0, "queued_as: ") && $0 ~ / amavisd?\[[0-9]+\]: /) { amavis(); next }
|
if (qf == "NOQUEUE:") {
|
||||||
|
if (rest ~ /^(milter-)?reject: /) handle_reject("NOQUEUE", rest)
|
||||||
|
next
|
||||||
|
}
|
||||||
|
|
||||||
qid = ""
|
if (qf !~ QID_RE) next
|
||||||
for (i = 2; i <= 8 && i <= NF; i++) if ($i ~ QID_RE) { qid = substr($i, 1, length($i) - 1); break }
|
qid = substr(qf, 1, length(qf) - 1)
|
||||||
if (qid == "") next
|
|
||||||
track(qid)
|
track(qid)
|
||||||
|
|
||||||
if (index($0, qid ": client=")) {
|
if (rest ~ /^client=/) {
|
||||||
if (match($0, /client=[^ ,]*\[[^]]*\]/)) {
|
if (match(rest, /^client=[^ ,\[]*\[[^] ,]*\]/)) {
|
||||||
ip = substr($0, RSTART, RLENGTH); sub(/^[^[]*\[/, "", ip); sub(/\]$/, "", ip)
|
ip = substr(rest, 1, RLENGTH); sub(/^[^\[]*\[/, "", ip); sub(/\]$/, "", ip)
|
||||||
if (!(qid in c_ip) || is_loop(c_ip[qid])) c_ip[qid] = clean(ip, 64)
|
if (!(qid in c_ip) || is_loop(c_ip[qid])) c_ip[qid] = clean(ip, 64)
|
||||||
}
|
}
|
||||||
if (!(qid in c_time)) c_time[qid] = l_ts
|
if (!(qid in c_time)) c_time[qid] = l_ts
|
||||||
next
|
next
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index($0, qid ": removed")) { forget(qid); next }
|
if (rest == "removed") { forget(qid); next }
|
||||||
|
|
||||||
if (index($0, qid ": from=<")) {
|
if (rest ~ /^from=</) {
|
||||||
if (match($0, /from=<[^>]*>/)) { s = substr($0, RSTART + 6, RLENGTH - 7); snd[qid] = (s == "") ? "<>" : clean(s, 0) }
|
# qmgr: from=<sender>, size=N, nrcpt=N (queue active) - anchored at the end
|
||||||
if (match($0, /size=[0-9]+/)) sz[qid] = substr($0, RSTART + 5, RLENGTH - 5) + 0
|
if (match(rest, />, size=[0-9]+, nrcpt=[0-9]+ \(queue active\)$/)) {
|
||||||
|
s = substr(rest, 7, RSTART - 7); snd[qid] = (s == "") ? "<>" : clean(s, 0)
|
||||||
|
tail = substr(rest, RSTART); match(tail, /size=[0-9]+/)
|
||||||
|
sz[qid] = substr(tail, RSTART + 5, RLENGTH - 5) + 0
|
||||||
|
}
|
||||||
next
|
next
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index($0, " header Subject: ")) {
|
if (rest ~ /^(warning|info): header Subject: /) {
|
||||||
s = substr($0, index($0, " header Subject: ") + 17)
|
s = rest; sub(/^(warning|info): header Subject: /, "", s)
|
||||||
# "from host[ip]; from=<..." (smtpd) or "from local; from=<..." (pickup)
|
# "from host[ip]; from=<...>" (smtpd) or "from local; from=<...>" (pickup)
|
||||||
sub(/ from [^ ;]*; from=<.*$/, "", s)
|
sub(/ from [^ ;]*; from=<.*$/, "", s)
|
||||||
if (!(qid in subj)) subj[qid] = clean(s, 256)
|
if (!(qid in subj)) subj[qid] = clean(s, 256)
|
||||||
|
next
|
||||||
}
|
}
|
||||||
|
|
||||||
if (index($0, qid ": reject: ") || index($0, qid ": milter-reject: ")) { handle_reject(qid); next }
|
if (rest ~ /^(milter-)?reject: /) { handle_reject(qid, rest); next }
|
||||||
|
|
||||||
|
if (rest ~ /^to=</) {
|
||||||
|
# fixed Postfix structure after the (untrusted) recipient address; the
|
||||||
|
# first match wins so that text in the remote reply cannot spoof it
|
||||||
|
if (!match(rest, />, (orig_to=<[^>]*>, )?relay=[^ ,]+, (conn_use=[0-9]+, )?delay=[0-9.]+, (delays=[0-9.\/]+, )?dsn=[0-9.]+, status=[a-z]+/)) next
|
||||||
|
to = clean(substr(rest, 5, RSTART - 5), 0)
|
||||||
|
mid = substr(rest, RSTART, RLENGTH)
|
||||||
|
tail = substr(rest, RSTART + RLENGTH)
|
||||||
|
match(mid, /relay=[^ ,]+/); relay = substr(mid, RSTART + 6, RLENGTH - 6)
|
||||||
|
match(mid, /dsn=[0-9.]+/); dsn = substr(mid, RSTART + 4, RLENGTH - 4)
|
||||||
|
match(mid, /status=[a-z]+$/); st = substr(mid, RSTART + 7, RLENGTH - 7)
|
||||||
|
reason = "-"
|
||||||
|
if (tail ~ /^ \(.*\)$/) reason = clean(substr(tail, 3, length(tail) - 3), 512)
|
||||||
|
|
||||||
if (index($0, " to=<") && index($0, ", status=")) {
|
|
||||||
# content filter hop (amavis & co.) - link to re-injected queue id, do not report
|
# content filter hop (amavis & co.) - link to re-injected queue id, do not report
|
||||||
if ($0 ~ /relay=[^ ,]*\[(127\.0\.0\.1|(ipv6:)?::1)\]:10[0-9][0-9][0-9],/) {
|
if (relay ~ /^[^\[]*\[(127\.0\.0\.1|(ipv6:)?::1)\]:10[0-9][0-9][0-9]$/) {
|
||||||
if (match($0, /queued as [0-9A-Za-z]+/)) link(qid, substr($0, RSTART + 10, RLENGTH - 10))
|
if (st == "sent" && match(tail, /queued as [0-9A-Za-z]+\)$/)) link(qid, substr(tail, RSTART + 10, RLENGTH - 11))
|
||||||
next
|
next
|
||||||
}
|
}
|
||||||
to = "-"; if (match($0, / to=<[^>]*>/)) to = clean(substr($0, RSTART + 5, RLENGTH - 6), 0)
|
sub(/:[0-9]+$/, "", relay)
|
||||||
relay = "-"; if (match($0, /relay=[^ ,]+/)) { relay = substr($0, RSTART + 6, RLENGTH - 6); sub(/:[0-9]+$/, "", relay); relay = clean(relay, 255) }
|
emit(qid, to, clean(relay, 255), st, dsn, reason)
|
||||||
st = "-"; if (match($0, /status=[a-z]+/)) st = substr($0, RSTART + 7, RLENGTH - 7)
|
|
||||||
dsn = "-"; if (match($0, /dsn=[0-9.]+/)) dsn = substr($0, RSTART + 4, RLENGTH - 4)
|
|
||||||
reason = "-"
|
|
||||||
if (match($0, /status=[a-z]+ \(.*\)/)) {
|
|
||||||
reason = substr($0, RSTART, RLENGTH)
|
|
||||||
sub(/^status=[a-z]+ \(/, "", reason); sub(/\)$/, "", reason)
|
|
||||||
reason = clean(reason, 512)
|
|
||||||
}
|
|
||||||
emit(qid, to, relay, st, dsn, reason)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -666,9 +674,10 @@ NF >= 14 {
|
|||||||
selftest_out=$(printf '%s\n' \
|
selftest_out=$(printf '%s\n' \
|
||||||
'Sep 1 10:00:00 mx postfix/smtpd[1]: 4T3bV22xLnz1t: client=a.example[192.0.2.1]' \
|
'Sep 1 10:00:00 mx postfix/smtpd[1]: 4T3bV22xLnz1t: client=a.example[192.0.2.1]' \
|
||||||
'Sep 1 10:00:01 mx postfix/qmgr[2]: 4T3bV22xLnz1t: from=<a@example.org>, size=1234, nrcpt=1 (queue active)' \
|
'Sep 1 10:00:01 mx postfix/qmgr[2]: 4T3bV22xLnz1t: from=<a@example.org>, size=1234, nrcpt=1 (queue active)' \
|
||||||
'Sep 1 10:00:02 mx postfix/smtp[3]: 4T3bV22xLnz1t: to=<b@example.net>, relay=mx.example.net[192.0.2.2]:25, delay=1, dsn=2.0.0, status=sent (250 2.0.0 Ok)' \
|
'Sep 1 10:00:01 mx postfix/cleanup[4]: 4T3bV22xLnz1t: warning: header Subject: x, status=sent (1) to=<fake@example.com> from a.example[192.0.2.1]; from=<a@example.org> to=<b@example.net> proto=ESMTP helo=<a>' \
|
||||||
|
'Sep 1 10:00:02 mx postfix/smtp[3]: 4T3bV22xLnz1t: to=<b@example.net>, relay=mx.example.net[192.0.2.2]:25, delay=1, delays=0/0/0.5/0.5, dsn=2.0.0, status=sent (250 2.0.0 Ok)' \
|
||||||
| MS_CUR_Y=2026 MS_CUR_M=9 "$AWK" "$AWK_PARSE" 2>&1) || true
|
| MS_CUR_Y=2026 MS_CUR_M=9 "$AWK" "$AWK_PARSE" 2>&1) || true
|
||||||
if [[ $selftest_out != *$'\t4T3bV22xLnz1t\t-\t192.0.2.1\tmx.example.net[192.0.2.2]\ta@example.org\tb@example.net\tsent\t1234\t2.0.0\t-\t250 2.0.0 Ok' ]]; then
|
if [[ $selftest_out != *$'\t4T3bV22xLnz1t\t-\t192.0.2.1\tmx.example.net[192.0.2.2]\ta@example.org\tb@example.net\tsent\t1234\t2.0.0\tx, status=sent (1) to=<fake@example.com>\t250 2.0.0 Ok' || $selftest_out == *fake@example.com$'\t'* ]]; then
|
||||||
die "Parser self-test failed with '$AWK' (incompatible awk implementation?). Install gawk."
|
die "Parser self-test failed with '$AWK' (incompatible awk implementation?). Install gawk."
|
||||||
fi
|
fi
|
||||||
unset selftest_out
|
unset selftest_out
|
||||||
|
|||||||
Reference in new issue
Block a user