Add smtp-test.sh
This commit is contained in:
252
smtp-test.sh
Normal file
252
smtp-test.sh
Normal 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 "============================================================"
|
||||
Reference in New Issue
Block a user