Start from the mismatch, because every bug in this integration comes out of it.
Email hands you a MIME tree, an SMTP envelope, and a chain that defines the conversation.
Slack hands you a channel, a message of at most 50 blocks, and a that defines the conversation.
The whole job is mapping one onto the other without dropping information — the reply chain, the authentication verdicts, the attachments — on the floor.
Here's the whole inbound half, as a Cloudflare Worker.
It runs as pasted with one KV namespace bound as and one dependency (): The receiving side is MailKite, which we build — the event, the header, and are ours.
The Slack half is plain Web API and transfers to any inbound provider that gives you parsed JSON.
Three ways to get email into Slack, and when each is right Slack has had a native email-to-channel feature for years, and for a lot of teams it's the correct answer — don't write code you don't need: Slack email-to-channel No-code (Zapier / n8n) Your own handler Cost Paid Slack plan only Task/execution quota Compute you already have Setup Minutes, in the Slack UI ~15 minutes ~50 lines Route by recipient No — one address per channel Yes, with a filter step Yes, in code or a route Threading No No Yes () Reply to the customer No Extra action + step cost Yes, same handler Strip signatures/quoted text No Awkward Yes Attachments Rendered by Slack Passed as URLs Your choice The native feature is free of code but not free of money: channel email addresses are a paid-plan feature and unavailable on Slack's free plan, per Slack's own help docs.
It also gives you exactly one behaviour — this address dumps into that channel — which is fine for and wrong for the moment you want tickets grouped by conversation.
Write the handler when you need routing ( → , → ), threading, or a reply path.
Otherwise stop here and use the built-in.
Block Kit's limits are a contract, and long subjects break it Slack's block limits are hard validation, not truncation.
Exceed one and the API returns with — your message silently never appears, which is a miserable thing to debug at 2am.
The ones inbound email actually hits, from the Block Kit reference: Field Limit What overruns it block 150 chars Forwarded subjects: chains blow past this routinely block 3,000 chars Any real email body block 10 items, 2,000 chars each Long with a display name Blocks per message 50 Only if you build a block per paragraph ~1 message/sec/channel A mailing list burst, an autoresponder loop That's why every string in goes through .
The 150-character header is the one that bites first and hardest, because a subject line has no length limit in SMTP and forwarding prepends to it forever.
The rate limit is the second one. sits in Slack's special tier — roughly one message per second per channel, short bursts tolerated — and returns with a header when you exceed it.
Don't build a retry loop for that: return a non-2xx from your handler and let the webhook delivery retry, so the backpressure lives in the queue that was designed for it instead of in a Worker holding a request open.
Retries will double-post unless you dedupe Any webhook worth using retries, and a retry that reaches Slack twice puts the same customer email in the channel twice.
The fix is an idempotency key, and it has to be one that's stable across attempts — not a timestamp, not a hash of the body with a re-serialized field order.
MailKite re-sends the identical body on an automatic retry or a manual replay, and is stable across all of them, so is the key.
Two lines in the Worker above: Write the key after Slack accepts, never before.
If you mark it seen first and the Slack call fails, the retry is deduped away and the email is lost silently — the failure mode nobody notices until a customer asks why they were ignored.
The display name in that Slack message is a claim, not a fact This one is specific to piping email into a chat tool, and it's the part I'd push back on in review.
A Slack message rendered as Ada