Shore Up
An envelope travelling along a route past a series of gates, each gate stamped with a tick as the letter passes, ending in a mailbox with a green flag up.
MailLinux

A Bash Script to Test Mail Server Deliverability End to End

Ketan Aagja10 min read
No ratings yet

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:

  • --tls tells swaks to 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 LOGIN forces the LOGIN mechanism. If your server prefers PLAIN or CRAM-MD5, drop the mechanism name and just use --auth, which lets swaks negotiate whatever the server advertises.
  • Credentials are pulled from SMTP_USER / SMTP_PASS in 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:

  1. Open the $TO mailbox and confirm the mailtest <timestamp> message is there — in the inbox, not spam.
  2. View the raw headers of that message. Look for Received-SPF: pass, dkim=pass, and dmarc=pass in the Authentication-Results header the receiving server stamps on. That is your proof that SPF, DKIM alignment, and DMARC all evaluated correctly on a real hop.
  3. 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.

Written by
Ketan Aagja

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.