malipetek

← Knowledgebase

Solved

Email Sending rewrites the envelope sender, so a loopback test proves nothing

cloudflare email email-routing email-sending testing

This worked, and here is why.

The problem

A Worker sends mail through its own Email Sending binding to an address the same Worker routes, to prove the inbound path end to end. The send reports success, the routing rule exists, the handler is deployed — and the expected row never appears. Nothing errors. A rejected message and a message that never arrived look identical, because the handler's early returns are silent by design (auto-replies are accepted and dropped).

The fix

Add one log line at the top of the inbound handler and watch wrangler tail. The message had arrived perfectly:

{"to":"new@malipetek.dev","from":"bounces@cf-bounce.malipetek.dev","subject":"Your sign-in link"}

message.from is the envelope sender (MAIL FROM), not the header From. Cloudflare Email Sending rewrites it to its own bounce address on the cf-bounce return-path subdomain. The handler authorises by envelope sender — deliberately, because a header From is spoofable — so it saw an unknown client and rejected the message.

Two consequences worth knowing before you debug the wrong layer:

  • A loopback through your own sending binding cannot test client-facing inbound authorisation, because the sender is always the bounce address. Use a real external address, or temporarily register the bounce address to exercise everything after the lookup.
  • A client whose provider sets a bounce envelope sender will be rejected even though their header From looks correct. That is the price of trusting the envelope; trusting the header instead trades it for spoofability.

Also learned on the same trip: not every credential reaches every endpoint. Email Sending subdomains are zone-scoped (/zones/{zone_id}/email/sending/subdomains) — account-scoped guesses return 404, which reads as "no such feature" rather than "wrong scope" — and a scoped API token needs Zone → Email Sending → Edit, while the wrangler OAuth session already carries email_sending:write.

The general lesson: when a pipeline has silent drop paths, instrument it before testing it. One log line turned an afternoon of guessing into a single tail read.