
A Bash Script to Test Mail Server Deliverability End to End
Before you run this
This script runs a series of read-only and send checks against a mail domain you control: it looks up MX, SPF, DKIM, and DMARC records with dig, tests the STARTTLS handshake on the submission port with openssl, and then sends one real test message through your server with swaks. Its purpose is to confirm, in one pass, that mail for your domain is configured to leave and arrive correctly.
It does not need root. dig, openssl, and swaks all run fine as an unprivileged user; you only need sudo once, to install swaks if it is missing. The script changes nothing on your server — the only side effect is that it sends a live email, so the recipient mailbox gets a message you will want to delete afterward.
Still, read it before you run it, and run it first against a test recipient you own (a spare mailbox, a +tag address), not a customer or a mailing list. Sending repeatedly, or to addresses you don't control, can trip your server's rate limits or hurt your sending reputation. Nothing here is destructive or irreversible; the "undo" is simply deleting the test message that lands in the inbox.
There is no firewall or production config being rewritten here, so no maintenance window is strictly required — but if your submission port or relay rules are locked down, keep an SSH session open to the mail host so you can read its logs while you test.
Assumptions: Debian 12 / Ubuntu 22.04, bash, and a mail server you administer that accepts authenticated submission on port 587 with STARTTLS. On RHEL/Alma the only difference is the install command (dnf install swaks); swaks is packaged in EPEL there.
Install the tools
# swaks is the SMTP test client; dig and openssl are usually already present
sudo apt update
sudo apt install -y swaks dnsutils openssl
dnsutils provides dig. On RHEL-family systems that package is bind-utils and swaks comes from EPEL.
The script
Save this as mailtest.sh, edit the five variables at the top, then chmod +x mailtest.sh.
#!/usr/bin/env bash
set -u # error on unset variables; we handle command failures ourselves
# ---- edit these ----------------------------------------------------------
DOMAIN="example.com" # the domain you send mail for
SMTP_HOST="mail.example.com" # your submission host
SMTP_PORT="587" # 587 = submission with STARTTLS
DKIM_SELECTOR="default" # the selector your server signs with
FROM="postmaster@example.com" # envelope + header From
TO="you+mailtest@example.com" # a mailbox YOU control and can read
# AUTH is read from the environment so it never lives in the script:
# export SMTP_USER='postmaster@example.com'
# export SMTP_PASS='the-password'
# -------------------------------------------------------------------------
pass() { printf ' \033[32mPASS\033[0m %s\n' "$1"; }
warn() { printf ' \033[33mWARN\033[0m %s\n' "$1"; }
fail() { printf ' \033[31mFAIL\033[0m %s\n' "$1"; }
echo "== DNS records for $DOMAIN =="
# MX
MX=$(dig +short MX "$DOMAIN")
if [ -n "$MX" ]; then pass "MX:"; echo "$MX" | sed 's/^/ /'
else fail "no MX record found"; fi
# SPF (a TXT record starting v=spf1)
SPF=$(dig +short TXT "$DOMAIN" | grep -i 'v=spf1')
if [ -n "$SPF" ]; then pass "SPF: $SPF"
else warn "no SPF (v=spf1) TXT record found"; fi
# DKIM (public key at <selector>._domainkey.<domain>)
DKIM=$(dig +short TXT "${DKIM_SELECTOR}._domainkey.${DOMAIN}")
if [ -n "$DKIM" ]; then pass "DKIM selector '$DKIM_SELECTOR' present"
else warn "no DKIM TXT at ${DKIM_SELECTOR}._domainkey.${DOMAIN}"; fi
# DMARC
DMARC=$(dig +short TXT "_dmarc.${DOMAIN}" | grep -i 'v=DMARC1')
if [ -n "$DMARC" ]; then pass "DMARC: $DMARC"
else warn "no DMARC (v=DMARC1) TXT record found"; fi
echo
echo "== STARTTLS handshake on ${SMTP_HOST}:${SMTP_PORT} =="
# openssl exits non-zero if the handshake fails; we just want to see it complete
if echo "QUIT" | openssl s_client -starttls smtp \
-connect "${SMTP_HOST}:${SMTP_PORT}" -quiet 2>/dev/null \
| grep -qi "verify"; then
pass "STARTTLS negotiated (see full cert below)"
else
warn "could not confirm STARTTLS — inspect manually (command shown below)"
fi
echo
echo "== Sending test message =="
if [ -z "${SMTP_USER:-}" ] || [ -z "${SMTP_PASS:-}" ]; then
fail "SMTP_USER / SMTP_PASS not set in environment — export them first"
exit 1
fi
# swaks does the SMTP conversation and returns non-zero on any protocol error
swaks \
--to "$TO" \
--from "$FROM" \
--server "$SMTP_HOST" \
--port "$SMTP_PORT" \
--tls \
--auth LOGIN \
--auth-user "$SMTP_USER" \
--auth-password "$SMTP_PASS" \
--header "Subject: mailtest $(date -u +%Y-%m-%dT%H:%M:%SZ)" \
--body "End-to-end deliverability test from mailtest.sh"
RC=$?
echo
if [ "$RC" -eq 0 ]; then
pass "server accepted the message — now check the $TO mailbox"
else
fail "swaks exited $RC — message was NOT accepted (see output above)"
fi
exit "$RC"
A few notes on the non-obvious choices:
--tlstellsswaksto require STARTTLS and abort if the server won't upgrade the connection. That's what you want on port 587 — a submission that silently falls back to cleartext is a finding, not a convenience.--auth LOGINforces the LOGIN mechanism. If your server prefers PLAIN or CRAM-MD5, drop the mechanism name and just use--auth, which letsswaksnegotiate whatever the server advertises.- Credentials are pulled from
SMTP_USER/SMTP_PASSin the environment so the password never sits in the script file or your shell history. Export them right before running, in a subshell if you like.
Run it:
export SMTP_USER='postmaster@example.com'
export SMTP_PASS='your-app-password'
./mailtest.sh
Reading the output
The DNS block distinguishes PASS (present) from WARN (missing but not fatal to a basic send). A missing SPF, DKIM, or DMARC record won't stop a message leaving your server, but it will very likely get it filtered at the far end — so treat those warnings as work to do, not noise.
The send step is the real end-to-end check. swaks prints the full SMTP dialogue: the 250 responses, the AUTH exchange (password redacted), and the final 250 OK / queued message ID. A non-zero exit means the server rejected something — the last 4xx/5xx line tells you what.
Verify the message actually arrived
The script confirms your server accepted the mail; it can't see the recipient's inbox. To close the loop:
- Open the
$TOmailbox and confirm themailtest <timestamp>message is there — in the inbox, not spam. - View the raw headers of that message. Look for
Received-SPF: pass,dkim=pass, anddmarc=passin theAuthentication-Resultsheader the receiving server stamps on. That is your proof that SPF, DKIM alignment, and DMARC all evaluated correctly on a real hop. - If you sent to an external provider (Gmail, Outlook), their "show original" view shows the same authentication results plainly.
To inspect the TLS certificate and cipher in full, run the handshake by hand:
openssl s_client -starttls smtp -connect mail.example.com:587
Read the Verify return code, the certificate chain, and the negotiated protocol at the top of the output.
Undo
There's nothing to roll back on the server — the script only reads DNS and opens SMTP sessions. The one artefact is the test email itself: delete it from the recipient mailbox when you're done, and unset SMTP_PASS (or close the shell) so the credential doesn't linger in your environment.
For the exact option list, the swaks man page is authoritative (man swaks), and the official documentation lives on the swaks project site maintained by John Jetmore. For the meaning of the SPF/DKIM/DMARC records themselves, refer to the RFCs (7208 for SPF, 6376 for DKIM, 7489 for DMARC) rather than any single vendor's summary.
Runs enterprise networks and security for a living, and writes Shore Up to turn two decades of hands-on Linux, Windows and mail-server work into guides you can actually use.
More about the author →Was this article helpful?
Tap a star — no sign-in needed.
Be the first to rate this article.
