Build1 distinct publisher3 min readUpdated
A Cloudflare Worker walkthrough puts the whole bridge in one file, and the load-bearing part is four key-value writes, not the API calls.
The Engineer · Build desk

Compiled by The EngineerSomething wrong?How this is made
A walkthrough on dev.to, written by the team that builds the inbound-email service it uses, ships a complete email-to-Slack handler as a single Cloudflare Worker and spends its first paragraph on the thing most integration posts skip [1][9]. Email defines a conversation with a MIME tree, an SMTP envelope and a Message-ID chain; Slack defines one with a channel, a message of at most 50 blocks, and a `ts` [1]. Neither API is hard. The mapping between those two definitions is where the duplicates live.
The handler's actual logic is four writes to a single KV namespace bound as `SEEN` [2]. On a successful post it stores the inbound message id with a 48-hour TTL, then, if this is the first email in a conversation, writes `thread:{threadId}` to the Slack `ts` and the reverse `slack:{ts}` back to the mail thread, plus the sender address and subject, all at 30 days [3][4][5][6]. The next email in that conversation reads the stored `ts` and passes it as `thread_ts`, so it lands in the existing Slack thread instead of starting a new one [7].
Note the asymmetry: the thread map outlives the dedupe key by a factor of 15 [12]. That is the right way round. The dedupe key only needs to survive a provider's retry window, while the conversation map needs to survive a customer going quiet for three weeks. It also means a reply arriving after the 30-day expiry posts as a fresh top-level message, because the lookup returns null and `thread_ts` goes undefined [13].
The retry contract is the part worth copying. The handler verifies an HMAC signature with a constant-time compare and a plus-or-minus five-minute replay window before it parses the body, and returns 401 on failure [8][14]. Slack answers a rejected post with HTTP 200 and `{ok: false, error}`, so the handler inspects the body and returns 502 upstream, which makes the email provider retry delivery [10]. Because the `SEEN.put` happens only after `ok: true`, a failed post does not burn the idempotency key, and the retry, which re-sends an identical body, is either absorbed or re-attempted rather than double-posted [3][11].
Two details that will bite anyone building this from scratch. Slack's block limits are hard validation, not truncation: exceed one and you get 200 OK with `ok: false`, which is why the sample clamps the header to 150 characters, section fields to 2,000 and the body text to 3,000 [15][16]. And the display name is gated on authentication, not on presence: the sender's name is only rendered when DMARC passes, otherwise the raw address goes up with a warning marker [17].
The honest caveat is in the post itself. Slack has had native email-to-channel for years, and per Slack's own help docs channel email addresses are a paid-plan feature unavailable on the free plan [18][19]. Native gives exactly one behaviour, one address into one channel, which is correct for `bugs@` and wrong for `support@` the moment you want grouping by conversation [20]. Write the handler when you need routing, threading or a reply path [21].
Watch the reverse maps. The post writes `slack:{ts}`, `from:{ts}` and `subj:{ts}` but the published code is the inbound half only [5][6][2]. The outbound direction is where a mis-keyed thread stops being a cosmetic duplicate and starts sending a customer someone else's reply.
Follow any of these and your For You feed starts watching them — no settings page required.
Ranked by verification strength, evidence, and original report placement.
The handler verifies the x-mailkite-signature header via MailKite.verifyWebhook, described as HMAC recompute, constant-time compare and a plus-or-minus five-minute replay window, returning 401 'bad signature' on failure, and parses the JSON body only after verifying.
Slack answers HTTP 200 with { ok: false, error } on rejection; the handler checks posted.ok and returns status 502 with the Slack error, and a non-2xx response makes MailKite retry the delivery.
Channel email addresses are a paid-plan feature and unavailable on Slack's free plan, per Slack's own help docs as cited by the post.
Email hands you a MIME tree, an SMTP envelope, and a Message-ID chain that defines the conversation; Slack hands you a channel, a message of at most 50 blocks, and a ts that defines the conversation. Every bug in the integration comes out of that mismatch.
The published inbound handler is a single Cloudflare Worker that runs as pasted with one KV namespace bound as SEEN and one dependency (npm install mailkite).
The handler treats the inbound message id as the idempotency key: if env.SEEN.get(email.id) returns a value it returns replyOk() and does nothing, and it only writes env.SEEN.put(email.id, "1") after Slack returns ok: true. A retry re-sends the identical body.
Evidence-backed comparisons of source perspectives and observed adoption signals. Read the methodology
Which Builder, Operator, and Investor concerns the observed source mix emphasized—not a truth score.
Evidence, demonstrated adoption, hype gap, incentives, and confidence are assessed independently, each on its own current evidence. How these are measured.
Detailed primary code, no external corroboration
The mechanism is unusually well evidenced for a single-source item: the full Worker is printed, so the idempotency ordering, the four KV writes, both TTL constants, the thread_ts lookup and the clamp thresholds are directly inspectable and internally consistent. Evidence weakens outside the code — MailKite's HMAC recompute, constant-time compare and ±5-minute replay window are asserted by the library's own authors, and Slack's block validation, rate-limit tier and paid-plan gating are relayed from Slack docs that are not themselves in the cluster.
No adoption signal in cluster
The cluster contains one tutorial and no release, deployment, benchmark, usage-disclosure or pricing-change observation. Nothing indicates whether the pattern or MailKite is in production anywhere, at what volume, or by whom, so adoption cannot be measured without inventing facts.
Slightly overstated reliability, honest about scope
Framing is mostly restrained: the post recommends Slack's built-in feature for many teams and scopes custom code to routing, threading and reply paths, which is the opposite of overclaiming. The small positive gap comes from durability language around correctness that the artifact does not fully earn — dedupe depends on Workers KV behaviour under concurrent retries that is never discussed, threading silently degrades after the 30-day map expires, and the security guarantees of the verification call are the vendor's own assertion with no production evidence behind any of it.
Disclosed vendor-authored walkthrough
The inbound half of the pipeline is the authors' own product: the email.received event, the x-mailkite-signature header and replyOk() are MailKite's, and installing mailkite is the sample's only dependency. That is a direct commercial interest in the reader adopting the pattern. It is mitigated by explicit disclosure in the post, by the statement that the Slack half transfers to any provider returning parsed JSON, and by the recommendation to use Slack's native feature where it suffices — the last of which cuts against, rather than serves, the vendor's interest.
High detail, single vendor-authored source
Confidence in the implementation specifics is high because the code is printed and self-consistent, and the derived items (TTL ratio, retry-after-failure behaviour, threading decay) follow mechanically from it. Confidence in the surrounding platform and security claims is limited: one publisher, one item, vendor authorship, no corroborating Slack documentation in the cluster, and no adoption evidence at all.
build
Judge transactional email on retries and DKIM alignment, not open rates1 distinct publisher
build
Buy transactional email on recovery controls, not send price1 distinct publisher
build
1,400 npm maintainer domains, 18 flags, and one word doing too much work1 distinct publisher
build
A build step instead of a backend: 1,025 records, 8 locales, no runtime API1 distinct publisher
Distinct publishers with included, body-backed reporting in this cluster.
dev.to
1 article · August 15, 2026