Skip to content

Your email passes SPF, DKIM and DMARC, and still doesn't arrive

4 min readProTechBridge

We wired a contact form to Azure Communication Services. The API returned Succeeded on every send. Nothing arrived. Not in the inbox, not in junk, not in a quarantine digest.

It took four wrong diagnoses to find the cause, and the fix was not in DNS, not in Azure, and not in any code we controlled.

"Succeeded" does not mean delivered

The first mistake was trusting the send API. Azure's long-running send operation resolves with a status, and ours said Succeeded every time.

So we calibrated it. We sent a message to an address at a .invalid top-level domain: a TLD reserved by the IETF precisely so it can never resolve:

nobody@this-domain-cannot-possibly-exist.invalid  ->  Succeeded

That is the whole lesson in one line. Succeeded meant accepted for processing. It said nothing about whether any mail server ever took the message. We had been reading a receipt for handing the letter to the post office as proof it landed on the doormat.

Worse, our own handler compounded it. The poller resolves for a failed send as well as a successful one, and we returned ok: true on the strength of the promise settling. A visitor whose message vanished still saw "Thank you. Message received", which is the one outcome a contact form must never produce.

Turn on delivery logging before you need it

We enabled diagnostic logging to a Log Analytics workspace, sent three probes, and had the answer in under four minutes:

muhammad@ourdomain.com   Failed   smtp=550   enhanced=5.1.8
admin@ourdomain.com      Failed   smtp=550   enhanced=5.1.8

The enhanced status code is the part that mattered. Under RFC 3463:

  • 5.1.1 means the recipient mailbox does not exist
  • 5.1.8 means the sender's address was refused

We had been assuming a recipient problem: chasing whether an alias resolved, whether a mailbox was real. 5.1.8 ruled all of that out in one field. The recipient was fine. The sender was being rejected.

Logging should have gone in before the first test, not after the fourth failure. It is the difference between an afternoon of hypotheses and one query.

The authentication was already perfect

Everything the deliverability checklists tell you to do was done, and verified by the platform itself:

SPF v=spf1 include:spf.protection.outlook.com -all (verified)
DKIM both selectors published and Verified
DMARC published on the sending subdomain

This is worth sitting with. Correct authentication is necessary and it is not sufficient. A receiving server is entitled to refuse properly signed mail for policy reasons, and ours did.

The actual cause

Our MX records point at a hosted filtering service. It was rejecting the mail because the From address was our own domain arriving from outside our tenant: the textbook signature of a spoofing attempt.

That rule is correct and worth having. Domain impersonation is how a convincing invoice-fraud email reaches your finance team. The filter cannot tell the difference between an attacker forging your domain and a third-party service you legitimately hired to send as your domain. Both look identical on the wire.

Every third-party sender hits this: your CRM, your invoicing tool, your helpdesk, your marketing platform. If it sends as @yourcompany.com and it is not your mail tenant, it is a spoof until you say otherwise.

Allowlisting the sender was not enough

We added the sending subdomain to the mail filter's Allowed Senders list. Three more sends, three more 550 5.1.8.

Anti-spoofing is evaluated separately from the safe-sender list. A domain can sit on the allow list and still be refused by the impersonation rule, because the two controls answer different questions: one asks "is this sender known", the other asks "is this sender pretending to be us". Only an anti-spoofing exception answers the second.

This is the step most guides omit, and it is why "I allowlisted it and it still fails" is such a common dead end.

What we would do differently

  1. Enable delivery logging on day one. Not when something breaks. The API status is not a delivery signal and never was.
  2. Check the enhanced status code, not just the 550. 5.1.1 and 5.1.8 send you in opposite directions.
  3. Verify the receiving side, not only the sending side. We spent our effort on DNS because DNS is what we controlled. The rejection was never there.
  4. Send one test before you need the pipeline. Add a third-party sender, send one message to a real mailbox on your own domain, and confirm it arrives. Do it the day you configure it, not the day a customer says they never heard back.
  5. Never report success on an unconfirmed send. Check the operation status explicitly and fail loudly. Silent success is worse than a visible error, because nobody goes looking for it.

The honest limitation

We still cannot see inside the mail filter. We can prove the message left, we can read the code it was refused with, and we can infer the rule from that code. The confirmation lives in an administrative console on the receiving side.

If your mail is authenticated, your logs show 5.1.8, and the sender is your own domain, the fix is almost certainly an anti-spoofing exception in whatever filters your inbound mail, and no amount of DNS will substitute for it.

  • Email
  • Deliverability
  • Azure

Need a hand with this in practice?

Reading about it is one thing. If you want someone to actually do it, that is what we are for.