Blue Team of One field notes · security

HomeEmailDelivery & mail flow

Deep dive · Email delivery

The bounce that lied: when "Delivered" isn't delivered

A user's mail to a client kept bouncing — while every trace on our side showed the message as Delivered. Here's the full anatomy of how email actually travels, and the one habit that cracks these cases in about a minute.

Most people picture email as one thing: type, click Send, it appears. In reality every message is a small parcel handed through a chain of at least six independent systems, each with its own job. If any link fails or refuses, the message never arrives — and the failure always happens at one specific stage. Learning to find the broken link fast is most of the skill.

This walkthrough is built around a real troubleshooting case (anonymized). The setup: a finance user was emailing a client's CFO. Some messages arrived normally; others bounced. And here's the twist that makes the case worth studying — both the Exchange Online message trace and the outbound gateway logs showed every message as Delivered. On paper, nothing was wrong. Yet the bounces kept coming.

01The journey every email takes

Six stages, six chances to fail.

1 Sender app Outlook 2 Mail server Exchange 3 Outbound gw scan + policy 4 DNS lookup MX + A 5 Inbound gw spam / auth 6 Mailbox inbox / spam The journey of one email
Six stages. Sender app → sender's mail server → outbound gateway → DNS lookup → recipient's inbound gateway → mailbox. Think of it as a letter through the postal system: your post office sorts it, customs scans it on the way out, the driver looks up the address, the destination customs checks it, the carrier drops it in the box.

Between stages 2 and 3, two mail servers actually talk using SMTP — a plain back-and-forth, like a polite phone call. It matters because the responses in that conversation are exactly what later show up in a bounce:

EHLO mx01.gatewaysender introduces itself
220 readyserver answers
MAIL FROM: <user@fabrikam.com>envelope sender (gets bounces)
250 OKsender accepted
RCPT TO: <cfo@contoso.com>envelope recipient
250 OKrecipient accepted
DATA … headers + body … .the message itself
250 accepted for deliveryhanded off

Two things trip everyone up here. First, the envelope (MAIL FROM / RCPT TO — what servers route on) is a different layer from the headers (From / To / Subject — what humans see); they can legitimately differ. Second, every SMTP response is a numbered code whose first digit tells you the whole story: 2xx success, 3xx intermediate, 4xx temporary (will retry), 5xx permanent (bounces). That single digit tells you instantly whether a problem is fatal or will clear on its own.

The key insight

"Delivered" in a message trace usually means "successfully handed off to the next stage," not "the recipient is reading it." This is the single most common source of confusion when troubleshooting — and it's the exact trap in this case.

02Reading the NDR — the most honest document you'll get

The bounce isn't bad news to delete. It's a machine-readable confession.

When a server gives up, it generates a Non-Delivery Report — a bounce, addressed back to the sender, stating exactly what failed. People delete these unread. That's backwards: the NDR is the best diagnostic you have. Here's the one that cracked this case:

550 5.1.2 <cfo@contoso.com>... Host unknown
 (Name server: contoso-com.mail.protection.outlook.com:
  host not found)
⚠ Undeliverable: Draft From: MAILER-DAEMON@mx0a.pphosted.com To: user@fabrikam.com 550 5.1.2 <cfo@contoso.com> ... Host unknown (Name server: contoso-com.mail.protection .outlook.com: host not found) 1 Who generated it? MAILER-DAEMON host = which server gave up. Here: our own outbound gw. 2 SMTP code family 550 → 5xx = permanent. It bounced, won't retry. 3 Enhanced status 5.1.2 = destination host can't be resolved. A DNS problem, not a mailbox. 4 Human-readable error "host not found" names the exact hostname to go investigate.
Four things to extract from every NDR. Who generated it, the code family, the enhanced status code, and the plain-English error. Train yourself to pull these four and you'll diagnose most email problems in under a minute.

The enhanced status codes (the dotted number) are worth memorizing — they point straight at the stage that failed:

5.1.1Recipient mailbox doesn't exist (bad username).
5.1.2Destination host can't be resolved — a DNS problem on the recipient side.
5.4.1No answer from the destination host (timeout / network).
5.7.1Policy rejection — the receiver refused (blocklist, compliance).
5.7.26Authentication failure (SPF/DKIM/DMARC) — a sender-side problem.

03The three locks: SPF, DKIM, DMARC

SMTP has no built-in way to verify who anyone is. These three layers, all riding on DNS, fix that.

SPF Sender Policy Framework The guest list Domain owner publishes which mail servers may send for it. Receiver checks the sending IP. Lives in: DNS TXT v=spf1 include:... -all DKIM DomainKeys Identified Mail The wax seal Sender signs the message with a private key; public key is in DNS. Proves it wasn't altered. Lives in: header + TXT d=fabrikam.com; s=selector1; b=... DMARC The rulebook What to do on fail Tells receivers: ignore, spam, or reject — and requires the From domain to align. Lives in: _dmarc TXT v=DMARC1; p=reject; rua=...
Nested checks. SPF verifies the source IP. DKIM verifies the message wasn't tampered with. DMARC ties both back to the visible From address and dictates the response. All three sit alongside SMTP, using DNS as the trust anchor.
fabrikam.com  TXT  "v=spf1 include:spf.protection.outlook.com -all"
_dmarc.fabrikam.com  TXT  "v=DMARC1; p=reject; rua=mailto:dmarc@fabrikam.com"

When a message you send is rejected with a code in the 5.7.x family (especially 5.7.26), that's an authentication problem on your side — check that SPF, DKIM, and DMARC all pass in the sent message's headers. In this case, they did. Our authentication was clean. The failure was somewhere else entirely.

04The case: a bounce that outran its trace

Clean handoffs on our side, bounces from our own gateway. Follow the chain.

Our outbound path was spotless: Outlook → Exchange Online → outbound gateway, logged successfully at every hop, SPF/DKIM/DMARC all passing. So why the bounce? The clue was who sent the NDR — it came from our own outbound gateway, not the recipient's server. That single fact means the message never reached the recipient's network. The gateway looked up where to deliver, and couldn't.

Outbound gw handoff OK DNS: MX record found DNS: A record for MX target Recipient never reached WHERE IT BROKE The recipient's MX record pointed to a hostname that had no A record. The address book said "deliver here" — but "here" had no IP address. DNS returned NXDOMAIN, our gateway had nowhere to connect, and it generated the bounce.
Where the chain broke. The MX lookup succeeded and handed back a hostname — but that hostname had no A record, so it couldn't be resolved to an IP. No mail server anywhere on the internet could complete delivery to that domain.

Three independent tools confirmed it, which matters enormously for what comes next — you never escalate to a third party on your word alone:

A public MX lookup returned the recipient's MX target with no A record. A global DNS propagation check, querying resolvers around the world, agreed. And a native Resolve-DnsName from a workstation confirmed the MX existed and its target did not. Three separate sources, one unambiguous finding: a recipient-side DNS misconfiguration.

05Why some mail still got through

The most counterintuitive part — and the fingerprint that named the cause.

If the recipient's DNS was broken, why did older messages deliver fine? The answer is DNS caching. At some earlier point the MX target did resolve, and our gateway cached that IP. While the cache was warm, deliveries succeeded — even after the underlying record broke. As cache entries aged out, the gateway re-queried authoritative DNS, got NXDOMAIN, and started bouncing. The pattern "older messages delivered, newer ones bounce" is a textbook cache-aging-out signature.

TTL is why the failure was intermittent

Every DNS record has a Time To Live — how long resolvers may cache it. A short TTL means changes (and breakages) propagate fast; a long TTL can hide a problem for a day. Intermittent, "sometimes it works" delivery failures are very often a caching or propagation artifact hiding a hard underlying fault.

06The workflow that catches most of these

Once you've seen one, you see the pattern everywhere.

1 · Check the sender side Trace + gateway logs. "Delivered" = handed off, not received. 2 · Read the NDR Who sent it, code family, enhanced status, plain error. 3 · Verify recipient DNS MX → A chain, from multiple independent resolvers. 4 · Check authentication SPF / DKIM / DMARC in the sent message headers. 5 · Decide ownership, communicate with evidence Sender fix, recipient fix, or transient — never escalate on a guess. SMTP CODES TO KNOW 5.1.1bad mailbox 5.1.2bad host / DNS 5.4.1no answer / timeout 5.7.1policy reject 5.7.26auth failure 4.x.xtemporary, retries 2.x.xsuccess VERIFY FROM OUTSIDE Public MX lookup · global DNS propagation · native DNS resolver
The five-step workflow. The order matters: confirm your own side is clean, let the NDR point you at the failed stage, verify from outside, rule out auth, then decide ownership. Most delivery problems fall out within a couple of steps.

The communication principle is the part people skip, and it's what separates a diagnosis from a hunch: never escalate to a third party without external, reproducible evidence. "Our trace shows Delivered" is not external — it's the very thing that misled us here. A public DNS lookup the other party can run themselves is external. Build the case from independently verifiable facts, then hand it off.

The one habit

When a trace says Delivered but bounces keep coming, don't trust the trace — read the NDR. Find the server that generated it, read the code, and verify the recipient's DNS from outside. The truth is in the bounce, not the trace.

Further reading

Comments

Questions or corrections welcome. Sign in with GitHub to join the thread.