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
- Enable delivery logging on day one. Not when something breaks. The API status is not a delivery signal and never was.
- Check the enhanced status code, not just the 550.
5.1.1and5.1.8send you in opposite directions. - 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.
- 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.
- 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.