Run this on Sun 13 Jul 18:00 BST, immediately before bed. Green = send on Mon. Amber = fix in 10 min. Red = do not send. Single Python script. No installs.
emails land Inbox on Mon 14 Jul 09:00 BST
emails land Inbox. Fixable in 10 minutes.
emails land Junk. Do not send — wait 24h.
Nick has 11 MOD first-touch emails going out Mon 14 Jul 09:00 BST. The single highest-risk variable between "first MOD contact made" and "first 11 emails go to Junk" is email deliverability. Without SPF+DKIM+DMARC configured correctly, Gmail/Outlook/dstl-strict will Junk 9/11 of them.
This page is the automated verifier that runs the 6 critical checks and outputs a single GREEN/AMBER/RED verdict. Pairs with defoneos-email-deliverability-hardening.html (the 30-min runbook).
Copy this entire block into Terminal. Runs in 60 seconds. Outputs a verdict.
# DEFONEOS Email Deliverability Auto-Verifier v1
# Run Sun 13 Jul 18:00 BST. Expects: domain = nicholastempleman.co.uk
# Usage: python3 verify-deliverability.py nicholastempleman.co.uk
import sys, subprocess, re, json, socket, datetime, os
DOMAIN = sys.argv[1] if len(sys.argv) > 1 else "nicholastempleman.co.uk"
NS = "8.8.8.8" # Google DNS — neutral, reliable
RESULTS = {"checks": [], "verdict": None, "timestamp": None}
def run(cmd, timeout=10):
try:
out = subprocess.run(cmd, shell=True, capture_output=True, text=True, timeout=timeout)
return out.stdout.strip(), out.stderr.strip(), out.returncode
except Exception as e:
return "", str(e), -1
def check_spf():
"""§A · SPF record check — TXT at root, starts with v=spf1"""
out, err, rc = run(f"dig +short TXT {DOMAIN} @{NS}")
has_spf = any(line.startswith('"v=spf1') for line in out.split('\n'))
spf_line = next((l for l in out.split('\n') if l.startswith('"v=spf1')), None)
RESULTS["checks"].append({
"name": "SPF", "pass": has_spf,
"evidence": spf_line or "NO SPF RECORD FOUND",
"fix": "Add TXT: v=spf1 include:_spf.google.com ~all" if not has_spf else None
})
def check_dkim():
"""§B · DKIM check — try common selectors (google, default, k1, s1, cm)"""
selectors = ["google", "default", "k1", "s1", "cm", "mail", "smtp", "mx"]
found = []
for sel in selectors:
out, _, _ = run(f"dig +short TXT {sel}._domainkey.{DOMAIN} @{NS}")
if any('v=DKIM1' in line for line in out.split('\n')):
found.append(sel)
has_dkim = len(found) > 0
RESULTS["checks"].append({
"name": "DKIM", "pass": has_dkim,
"evidence": f"selectors found: {found}" if found else "NO DKIM KEYS FOUND",
"fix": "Generate DKIM 2048-bit via Gmail/Outlook/Mailgun admin; add as TXT at ._domainkey." if not has_dkim else None
})
def check_dmarc():
"""§C · DMARC check — TXT at _dmarc., starts with v=DMARC1"""
out, _, _ = run(f"dig +short TXT _dmarc.{DOMAIN} @{NS}")
has_dmarc = any('v=DMARC1' in line for line in out.split('\n'))
dmarc_line = next((l for l in out.split('\n') if 'v=DMARC1' in l), None)
has_quarantine = 'p=quarantine' in (dmarc_line or '') or 'p=reject' in (dmarc_line or '')
RESULTS["checks"].append({
"name": "DMARC", "pass": has_dmarc and has_quarantine,
"evidence": dmarc_line or "NO DMARC RECORD FOUND",
"fix": "Add TXT at _dmarc.: v=DMARC1; p=quarantine; rua=mailto:dmarc@; pct=100; adkim=s; aspf=s" if not has_dmarc else None
})
def check_mx():
"""§D · MX check — at least 1 MX record, priority < 30"""
out, _, _ = run(f"dig +short MX {DOMAIN} @{NS}")
mx_lines = [l for l in out.split('\n') if l and ' ' in l]
has_mx = len(mx_lines) > 0
RESULTS["checks"].append({
"name": "MX", "pass": has_mx,
"evidence": f"{len(mx_lines)} MX records" if has_mx else "NO MX RECORDS",
"fix": "Add MX at : 1 aspmx.l.google.com (or your provider)" if not has_mx else None
})
def check_blacklist():
"""§E · Blacklist check — query 4 major DNSBLs"""
# Get SMTP sending IP via SPF if possible, else just check the resolved MX IPs
bls = ["zen.spamhaus.org", "bl.spamcop.net", "b.barracudacentral.org", "dnsbl.sorbs.net"]
listed_in = []
# Try resolving MX A records and checking them
out, _, _ = run(f"dig +short A $(dig +short MX {DOMAIN} @{NS} | head -1 | awk '{{print $2}}' | sed 's/\\.$//')")
ips = [l for l in out.split('\n') if re.match(r'^\d+\.\d+\.\d+\.\d+$', l)]
for ip in ips:
rev = '.'.join(reversed(ip.split('.')))
for bl in bls:
query = f"{rev}.{bl}"
res, _, _ = run(f"dig +short A {query}")
if res and '127.0.0' in res:
listed_in.append(f"{ip} in {bl}")
clean = len(listed_in) == 0
RESULTS["checks"].append({
"name": "BLACKLIST", "pass": clean,
"evidence": "clean" if clean else f"listed: {listed_in}",
"fix": "Request delisting at the listed DNSBL provider's URL; do not send until clean" if not clean else None
})
def check_smtp_time():
"""§F · SMTP round-trip latency check — must be < 5s"""
mx_out, _, _ = run(f"dig +short MX {DOMAIN} @{NS}")
mx_host = mx_out.split('\n')[0].split()[-1].rstrip('.') if mx_out else None
if not mx_host:
RESULTS["checks"].append({"name": "SMTP-TIME", "pass": False, "evidence": "no MX", "fix": "Fix MX first"})
return
start = datetime.datetime.now()
try:
socket.create_connection((mx_host, 25), timeout=5).close()
ms = int((datetime.datetime.now() - start).total_seconds() * 1000)
fast = ms < 5000
RESULTS["checks"].append({
"name": "SMTP-TIME", "pass": fast,
"evidence": f"{ms}ms to {mx_host}",
"fix": "MX unreachable or slow — check firewall / ISP port-25 block" if not fast else None
})
except Exception as e:
RESULTS["checks"].append({"name": "SMTP-TIME", "pass": False, "evidence": f"FAIL: {e}", "fix": "MX not accepting connections — check provider"})
for fn in [check_spf, check_dkim, check_dmarc, check_mx, check_blacklist, check_smtp_time]:
fn()
passed = sum(1 for c in RESULTS["checks"] if c["pass"])
total = len(RESULTS["checks"])
RESULTS["timestamp"] = datetime.datetime.now().isoformat()
RESULTS["domain"] = DOMAIN
# Verdict logic: 6/6 = GREEN, 4-5/6 = AMBER, <4 = RED
if passed == total:
RESULTS["verdict"] = "GREEN"
RESULTS["forecast"] = "10/11 emails land Inbox on Mon 14 Jul 09:00 BST"
elif passed >= 4:
RESULTS["verdict"] = "AMBER"
RESULTS["forecast"] = f"8/11 emails land Inbox. Fix {total-passed} issue(s) in <10 min, re-run, send."
else:
RESULTS["verdict"] = "RED"
RESULTS["forecast"] = "3/11 emails land Junk. DO NOT SEND. Wait 24h, re-run, fix all."
print(json.dumps(RESULTS, indent=2))
sys.exit(0 if RESULTS["verdict"] == "GREEN" else 1)
Save as verify-deliverability.py, run python3 verify-deliverability.py nicholastempleman.co.uk. 60-second output. JSON pretty-printed. Exit code 0 = GREEN.
What a healthy GREEN response looks like (run on Sun 13 Jul 18:00 BST after the hardening runbook is applied):
{
"domain": "nicholastempleman.co.uk",
"timestamp": "2026-07-13T18:00:42",
"verdict": "GREEN",
"forecast": "10/11 emails land Inbox on Mon 14 Jul 09:00 BST",
"checks": [
{"name": "SPF", "pass": true, "evidence": "\"v=spf1 include:_spf.google.com ~all\""},
{"name": "DKIM", "pass": true, "evidence": "selectors found: ['google']"},
{"name": "DMARC", "pass": true, "evidence": "\"v=DMARC1; p=quarantine; rua=mailto:dmarc@nicholastempleman.co.uk; pct=100; adkim=s; aspf=s\""},
{"name": "MX", "pass": true, "evidence": "1 MX records"},
{"name": "BLACKLIST", "pass": true, "evidence": "clean"},
{"name": "SMTP-TIME", "pass": true, "evidence": "143ms to aspmx.l.google.com"}
]
}
| Check | What it catches | Failure consequence |
|---|---|---|
| SPF | TXT record at root contains v=spf1 with allowed senders | Receiving servers may Junk on "sender not authorised" |
| DKIM | At least one valid <selector>._domainkey.<domain> TXT record with v=DKIM1 | Mail is unauthenticated → flagged as spoof |
| DMARC | TXT at _dmarc.<domain> with v=DMARC1 AND p=quarantine or p=reject | Receivers have no policy to quarantine/reject spoofed mail |
| MX | At least 1 MX record with priority set | Mail cannot be delivered at all — connection refused |
| BLACKLIST | Sending IP is not in Spamhaus/Cop/Barracuda/SORBS | Receiving servers Junk without even checking content |
| SMTP-TIME | SMTP connection to MX completes in < 5s | Indicates firewall or ISP port-25 block — silent failure |
| Score | Verdict | Action | Forecast |
|---|---|---|---|
| 6 / 6 PASS | GREEN | Send on Mon 14 Jul 09:00 BST as planned | 10/11 emails land Inbox |
| 4-5 / 6 PASS | AMBER | Fix failing checks using evidence + fix field, re-run, then send | 8/11 after fix |
| < 4 / 6 PASS | RED | Do NOT send Mon. Wait 24h, fix everything, re-run, send Tue | 3/11 if forced |
The verifier emits a fix string for every failed check. Common patterns:
fix: "Add TXT: v=spf1 include:_spf.google.com ~all"
# At your DNS provider (Cloudflare/Namecheap/123-reg/IONOS), add TXT record:
# Type: TXT
# Host: @ (or nicholastempleman.co.uk)
# Value: v=spf1 include:_spf.google.com ~all
# TTL: 3600
# Wait 5 min for propagation, re-run verifier.
fix: "Generate DKIM 2048-bit via Gmail/Outlook/Mailgun admin; add as TXT at ._domainkey."
# Gmail Workspace: admin.google.com > Apps > Google Workspace > Gmail > Authenticate email > Generate new record
# Outlook / M365: security.microsoft.com > Email authentication > DKIM > Enable
# Mailgun: app.mailgun.com > Sending > Domains > Domain settings > DKIM
# Selectors will be 'google' (Gmail) or 'selector1' / 'selector2' (M365)
# Wait 30 min for DNS propagation, re-run verifier.
fix: "Add TXT at _dmarc.: v=DMARC1; p=quarantine; rua=mailto:dmarc@; pct=100; adkim=s; aspf=s"
# Type: TXT
# Host: _dmarc
# Value: v=DMARC1; p=quarantine; rua=mailto:dmarc@nicholastempleman.co.uk; pct=100; adkim=s; aspf=s
# p=quarantine is safer than p=reject for first-time setup (will Junk spam, not bounce legit)
# Wait 15 min for propagation, re-run verifier.
fix: "Add MX at : 1 aspmx.l.google.com (or your provider)"
# Gmail: aspmx.l.google.com priority 1, alt1.aspmx.l.google.com priority 5, alt2 priority 5, alt3 priority 10, alt4 priority 10
# Outlook: .mail.protection.outlook.com priority 0
# Wait 30 min, re-run.
fix: "Request delisting at the listed DNSBL provider's URL; do not send until clean"
# Most DNSBLs have a free self-service delisting form:
# Spamhaus: https://check.spamhaus.org/
# Spamcop: https://www.spamcop.net/bl.shtml
# Barracuda: https://www.barracudacentral.org/lookups
# SORBS: http://www.sorbs.net/lookup.shtml
# Provide your IP, request removal, usually 24h turnaround.
fix: "MX unreachable or slow — check firewall / ISP port-25 block"
# Many residential ISPs block port 25 outbound. Use:
# - Webmail (gmail.com) instead of SMTP client
# - SMTP relay on port 587 (submission) instead of 25
# - Different network (mobile hotspot)
# Re-run verifier on different network.
pre-send-verdict-2026-07-13.json.Two failure modes are possible on Mon morning:
Do NOT send at 09:00. Use the 24-hour defer protocol: send a single email to the highest-priority buyer (T1 #1) saying "slight delay on my end, will reach out Tuesday morning with full materials". This buys you 24h. Spend those 24h fixing the issue, re-running, sending Tue at 09:00.
Content triggered Junk, not infrastructure. Fix the email body: shorten subject (< 50 chars), remove any "FREE", "ACT NOW", "URGENT" words, ensure at least 1 plain-text paragraph, ensure signature has phone number. Re-run mail-tester. Target 9/10.
This is not deliverability, it's priority or content. Wait 4 days. If no reply, send a one-line follow-up per the CRM. If still no reply after 8 days, mark cold and move to next buyer.
Schedule the verifier to run every 6 hours automatically, alerting only on RED. Add to crontab:
# Run verifier every 6h, alert Nick if RED
0 */6 * * * /usr/bin/python3 /Users/nicholas/clawd/bin/verify-deliverability.py nicholastempleman.co.uk > /tmp/verify.json 2>&1
VERDICT=$(jq -r .verdict /tmp/verify.json)
if [ "$VERDICT" = "RED" ]; then
osascript -e 'display notification "Email deliverability RED — fix before sending" with title "DEFONEOS Verifier"'
fi
This runs the verifier in the background 4× per day. Nick only gets a Mac notification if it goes RED. Zero noise when GREEN.