Add smtp-test.sh

This commit is contained in:
2026-02-21 16:13:31 +00:00
commit dd60107020

252
smtp-test.sh Normal file
View File

@@ -0,0 +1,252 @@
#!/bin/bash
# ============================================================
# Outbound SMTP/25 test + interface sweep + logging + rDNS/HELO
#
# Outputs: ./results.txt (appends)
#
# Usage:
# ./smtp25_sweep.sh
# ./smtp25_sweep.sh --domains "gmail.com outlook.com yahoo.com icloud.com proton.me"
# ./smtp25_sweep.sh --targets "gmail-smtp-in.l.google.com:25 outlook-com.olc.protection.outlook.com:25"
#
# Notes:
# - Checks TCP/25 connectivity from ALL global IPv4 addresses on the host
# - Resolves MX for domains (takes top MX by preference) unless explicit targets given
# - Validates rDNS (PTR exists) and forward-confirmed PTR (A of PTR contains the IP)
# - Sends EHLO and records server response (best-effort HELO sanity)
# ============================================================
set -u
TIMEOUT=6
LOGFILE="./results.txt"
DEFAULT_DOMAINS=("gmail.com" "outlook.com" "yahoo.com" "icloud.com" "proton.me") # +2 included (icloud, proton)
DOMAINS=("${DEFAULT_DOMAINS[@]}")
TARGETS=() # host:port entries; if provided, skips MX resolution
HELO_NAME=""
if command -v postconf >/dev/null 2>&1; then
HELO_NAME="$(postconf -h myhostname 2>/dev/null || true)"
fi
if [[ -z "${HELO_NAME}" ]]; then
HELO_NAME="$(hostname -f 2>/dev/null || hostname)"
fi
ts() { date +"%Y-%m-%d %H:%M:%S %z"; }
log() {
echo "[$(ts)] $*" | tee -a "$LOGFILE" >/dev/null
}
die() {
log "FATAL: $*"
exit 1
}
need_cmd() {
command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1"
}
need_cmd ip
need_cmd nc
need_cmd dig
need_cmd awk
need_cmd sort
need_cmd sed
need_cmd grep
# --------------------- args ---------------------
while [[ $# -gt 0 ]]; do
case "$1" in
--domains)
shift
IFS=' ' read -r -a DOMAINS <<< "${1:-}"
;;
--targets)
shift
IFS=' ' read -r -a TARGETS <<< "${1:-}"
;;
--timeout)
shift
TIMEOUT="${1:-6}"
;;
--log)
shift
LOGFILE="${1:-./results.txt}"
;;
*)
echo "Unknown arg: $1"
exit 2
;;
esac
shift
done
# --------------------- helpers ---------------------
get_source_ips() {
# global IPv4 addresses
ip -o -4 addr show scope global 2>/dev/null | awk '{print $4}' | sed 's#/.*##' | sort -u
}
mx_for_domain() {
local d="$1"
# Return MX hosts sorted by preference (lowest first), without trailing dot
dig +short MX "$d" 2>/dev/null | sort -n | awk '{print $2}' | sed 's/\.$//' | grep -v '^$' || true
}
resolve_targets_from_domains() {
local t=()
for d in "${DOMAINS[@]}"; do
local mxlist
mxlist="$(mx_for_domain "$d")"
if [[ -z "$mxlist" ]]; then
log "WARN: No MX found for domain: $d"
continue
fi
# Take top 2 MX for each domain to reduce noise; adjust if you want more.
local count=0
while IFS= read -r mx; do
t+=("${mx}:25")
count=$((count+1))
[[ $count -ge 2 ]] && break
done <<< "$mxlist"
done
# de-dup
printf "%s\n" "${t[@]}" | sort -u
}
rdns_check_ip() {
local ipaddr="$1"
local ptr
ptr="$(dig +short -x "$ipaddr" 2>/dev/null | head -n 1 | sed 's/\.$//')"
if [[ -z "$ptr" ]]; then
log "rDNS: $ipaddr -> PTR: (none) [FAIL]"
return
fi
# Forward-confirmed reverse DNS (FCrDNS): A/AAAA of PTR contains original IP
local a_records
a_records="$(dig +short A "$ptr" 2>/dev/null | tr '\n' ' ')"
if echo "$a_records" | grep -q -w "$ipaddr"; then
log "rDNS: $ipaddr -> PTR: $ptr ; A(PTR): $a_records [OK FCrDNS]"
else
log "rDNS: $ipaddr -> PTR: $ptr ; A(PTR): $a_records [WARN mismatch]"
fi
}
tcp25_test() {
local srcip="$1"
local host="$2"
local port="$3"
log "TEST: src=$srcip -> $host:$port (tcp/$port) timeout=${TIMEOUT}s"
# Connection + banner (best effort)
local out rc
out="$(nc -v -w "$TIMEOUT" -s "$srcip" "$host" "$port" < /dev/null 2>&1)"
rc=$?
if [[ $rc -eq 0 ]]; then
log "RESULT: CONNECTED"
# capture banner line if present
local banner
banner="$(echo "$out" | grep -E '^[0-9]{3} ' | head -n 1 || true)"
[[ -n "$banner" ]] && log "BANNER: $banner"
else
# common error strings: timed out, refused, no route, network unreachable
log "RESULT: FAILED rc=$rc"
log "DETAIL: $(echo "$out" | tr '\n' ' ' | sed 's/ */ /g' | cut -c1-350)"
fi
}
ehlo_probe() {
local srcip="$1"
local host="$2"
local port="$3"
log "EHLO: src=$srcip -> $host:$port using HELO='$HELO_NAME'"
# Speak minimal SMTP: EHLO + QUIT
# Some servers may tarp it; keep timeout short.
local resp
resp="$(
{
printf "EHLO %s\r\n" "$HELO_NAME"
printf "QUIT\r\n"
} | nc -w "$TIMEOUT" -s "$srcip" "$host" "$port" 2>/dev/null | head -n 20
)"
if [[ -z "$resp" ]]; then
log "EHLO: (no response captured) [WARN]"
return
fi
# Log first lines (avoid huge output)
while IFS= read -r line; do
line="$(echo "$line" | tr -d '\r')"
log "EHLO-RESP: $line"
done <<< "$resp"
}
# --------------------- main ---------------------
log "============================================================"
log "START smtp25 sweep on host=$(hostname) helo=$HELO_NAME"
log "LOGFILE=$LOGFILE"
SOURCE_IPS=()
while IFS= read -r ipaddr; do
[[ -n "$ipaddr" ]] && SOURCE_IPS+=("$ipaddr")
done < <(get_source_ips)
if [[ ${#SOURCE_IPS[@]} -eq 0 ]]; then
die "No global IPv4 addresses found to test from."
fi
log "SOURCE IPs: ${SOURCE_IPS[*]}"
# rDNS checks (per source IP)
log "---------------- rDNS validation ----------------"
for sip in "${SOURCE_IPS[@]}"; do
rdns_check_ip "$sip"
done
# targets
log "---------------- targets ----------------"
if [[ ${#TARGETS[@]} -gt 0 ]]; then
log "Using explicit targets: ${TARGETS[*]}"
FINAL_TARGETS=("${TARGETS[@]}")
else
mapfile -t FINAL_TARGETS < <(resolve_targets_from_domains)
if [[ ${#FINAL_TARGETS[@]} -eq 0 ]]; then
die "No targets resolved from domains: ${DOMAINS[*]}"
fi
log "Resolved targets (top MXs): ${FINAL_TARGETS[*]}"
fi
# connectivity tests
log "---------------- TCP/25 connectivity ----------------"
for sip in "${SOURCE_IPS[@]}"; do
for hp in "${FINAL_TARGETS[@]}"; do
host="${hp%:*}"
port="${hp##*:}"
tcp25_test "$sip" "$host" "$port"
done
done
# EHLO probes (optional but requested)
log "---------------- EHLO probes (best-effort) ----------------"
for sip in "${SOURCE_IPS[@]}"; do
for hp in "${FINAL_TARGETS[@]}"; do
host="${hp%:*}"
port="${hp##*:}"
ehlo_probe "$sip" "$host" "$port"
done
done
log "END smtp25 sweep"
log "============================================================"