← work / ai-email

An Inbox That Answers Itself

A fully serverless AI email responder built entirely on Cloudflare. It reads inbound mail, answers from one bounded subject, replies inside the original thread, and costs under $0.0035 per interaction to run.

LIVE Working system. Live on [email protected], answering real inbound email end to end.
Diagram of the Cloudflare Worker and AI email pipeline: a sender's message is routed through Email Routing into a Worker, scoped and sent to AI Search for a grounded answer, then composed into a threaded reply and an audit copy, all inside one Cloudflare account at under $0.0035 per email
Project type
Full-stack serverless AI application (inbound email automation)
Role
Solo developer and architect — product, MIME parsing, AI integration, delivery, cost model
Created
November 2026
Status
Working system; live on [email protected], answering real inbound email end to end
Trigger
Inbound email to a single Cloudflare Email Routing address
Codebase
~210 lines of JavaScript, one file, zero npm dependencies, zero build step

// 01 — The problem

Enterprises answer the same email a thousand times, and a server is the wrong place to do it.

A large share of inbound business email is repetitive: the same handful of questions, asked by different people, at unpredictable times of day. Hiring a person to answer them doesn't scale with volume. Standing up a traditional backend — a mail processor, a queue, a worker fleet — to automate it means paying for capacity around the clock for traffic that arrives in bursts, if it arrives at all.

And an LLM sitting behind a public-facing email address has its own failure mode: asked a question it was never scoped to answer, it will answer anyway, fluently and off-brand, unless something upstream of the model stops it.

Four constraints shaped the build:

ConstraintConsequence
Inbound email is unstructured, raw MIMESubject, body, encoding and threading headers all have to be parsed by hand before anything useful can happen
A real inbox expects a real replyA bare acknowledgement isn't enough — the reply has to land in the sender's existing thread, from a sendable address, inside normal reply latency
A mailbox can't justify a serverRunning a mail-answering service behind a VM or container means paying around the clock for traffic that arrives in bursts, if at all
An open-ended LLM will answer anythingAn assistant living at a personal or brand email address has to refuse to become a general chatbot the moment someone asks it something unrelated

So the goal wasn't "wire an LLM to an inbox." It was: answer real email, inside the real thread, from a single bounded subject, on infrastructure that costs nothing while nobody is emailing.

// 02 — My responsibilities

  • → Defined the product: a single email address that reads inbound mail, answers from a bounded topic, and replies inside the same thread.
  • → Chose Cloudflare's email stack end to end — Email Routing for the trigger, an Email Worker for compute, AI Search for retrieval and generation — over a self-hosted mail processor.
  • → Built a hand-rolled MIME parser from scratch: subject and Message-ID/References extraction, multipart boundary walking, quoted-printable and base64 decoding, RFC 2047 encoded-header decoding.
  • → Designed the email threading model — carrying In-Reply-To and References forward and minting a fresh Message-ID per outbound send — so multi-turn conversations stay in one thread instead of starting over each reply.
  • → Scoped the assistant at the prompt layer: every call frames the question as a lookup against one subject and instructs the model to decline anything else, rather than trusting the sender's message to stay on topic.
  • → Wired the chat completions call to a Cloudflare AI Search instance, treating it as the single source of grounded knowledge rather than the model's own pretraining.
  • → Designed the identity split between the sending address and the reply-to address, so the domain that sends mail and the address people actually reply to can be managed and reputation-scored independently.
  • → Implemented the dual-send delivery: the sender's reply and an internal BCC audit copy as two independent envelope sends, since Email Workers has no native BCC header behaviour.
  • → Implemented failure handling via message.setReject so a broken AI call or malformed MIME bounces with a reason instead of disappearing silently.
  • → Modelled the full cost per interaction across Workers compute, Email Routing, and the AI Search call, and verified it against Cloudflare's published pricing.
  • → Documented every binding, secret and permission needed to run the Worker from the dashboard alone — no wrangler, no CLI deploy step required.
An email client showing a multi-turn conversation with ai@email.kalob.io correctly nested as one thread, answering questions about contacting Kalob and the projects he's worked on
A multi-turn conversation, correctly threaded: every reply carries In-Reply-To and References back to the message it answers.

// 03 — The approach

Seven stages. None of them run on a server.

Every stage is either a Cloudflare primitive or plain JavaScript inside a single Worker. There is no intermediate service, no database, and no process that has to stay running between emails.

  1. 01

    Receive — Email Routing hands the message straight to compute.

    Mail addressed to [email protected] is routed by Cloudflare Email Routing directly into the Worker's email() export. There is no inbox to poll, no webhook relay, and no queue sitting between the SMTP transaction and the code that handles it.

  2. 02

    Parse — turn raw MIME into a subject, a thread, and a question.

    Read the message's raw stream to text, pull the Subject, Message-ID and References headers, walk the MIME boundaries to find the first text/plain part, and decode whatever transfer encoding it was sent with. The output is just a subject string and a body string, plus the two headers needed to keep the thread intact.

  3. 03

    Scope — frame every question as a lookup, not an open prompt.

    The system prompt and the user turn both hard-code the same constraint: find information about one named subject and answer only from that, declining politely otherwise. The sender's wording never gets to redefine what the assistant is allowed to talk about.

  4. 04

    Generate — one HTTP call to a managed retrieval-and-generation endpoint.

    The scoped prompt is sent to an AI Search chat completions endpoint — a Cloudflare-managed instance that retrieves relevant context and produces the reply in a single request. No vector database, no retrieval code, and no model hosting live in this project at all.

  5. 05

    Compose — build the reply as a real, threaded email.

    Assemble a raw outbound MIME message: a Re: subject, a From address distinct from the Reply-To address, a fresh Message-ID, and In-Reply-To / References headers pointing back at the message just received, so the reply nests into the existing conversation in Gmail, Outlook and Apple Mail.

  6. 06

    Deliver — two independent sends, not one message with extra headers.

    The composed reply is sent to the original sender through the Email Workers send-email binding. A second, differently-labelled copy is built and sent to an internal address for audit visibility — a real BCC header is not meaningful to a send binding, so the copy is its own delivery.

  7. 07

    Guard — reject loudly, scale by doing nothing extra.

    Any failure — a bad AI response, a malformed message, a send error — calls message.setReject() with the reason, so the inbound message bounces instead of vanishing. There is no fleet to patch and no capacity to provision; the next email just triggers another isolated invocation.

The same pipeline, as a flow
inbound SMTP to [email protected]
      │
      ▼
[1] Cloudflare Email Routing ──► triggers the Worker directly, no inbox
      │
      ▼
[2] email() handler ──► raw MIME → subject, Message-ID, References, text body
      │
      ▼
[3] scoped prompt ──► "find info about <subject> and nothing else"
      │
      ▼
[4] AI Search /chat/completions ──► retrieval + generation, one HTTP call
      │
      ▼
[5] compose reply ──► Re: subject, fresh Message-ID, In-Reply-To, References
      │
      ▼
[6] send_email binding ──► reply to sender  +  audit copy to internal address
      │
      ▼
[7] on any failure ──► message.setReject(reason) — bounce, don't vanish

What makes it reusable across mailboxes

Per-mailboxShared by every deployment
The system prompt's one allowed subjectMIME parsing, decoding and threading-header handling
The From / Reply-To address pairThe chat-completions call and its error handling
The BCC audit addressThe dual-send delivery pattern
The AI Search instance and model nameThe whole Cloudflare account — no new infrastructure to provision per deployment

Standing up a second address for a different subject means changing a prompt string and two addresses — not re-architecting the parsing, threading, or delivery layer underneath it.

// 04 — Technical insights

Parsing email is the unglamorous 80% of the work

A raw inbound message is a stream of bytes with no guarantees. It might be single-part plain text, or multipart/alternative with an HTML sibling, or quoted-printable, or base64, or carry a subject line encoded under RFC 2047 because it contains a non-ASCII character. None of that is visible until it's parsed.

None of it needed a library. Walking MIME boundaries, decoding quoted-printable and base64 bodies, and pulling four headers out of a text block is well within what a few dozen lines of plain JavaScript can do — which matters here, because Workers has no npm install step for a hand-pasted file.

Parsing decisionValueReasoning
Body extractionFirst text/plain MIME part foundMost clients send multipart/alternative with HTML as a courtesy copy; the plain-text sibling is what the model should read
Header decodingRFC 2047 UTF-8 base64Non-ASCII subject lines arrive encoded; reading them literally would corrupt both the reply subject and the thread
Thread anchorIn-Reply-To + ReferencesMail clients group conversations by these two headers, not by matching subject text
Identity splitFrom [email protected], Reply-To [email protected]Isolates the sending subdomain's deliverability reputation from the address a human actually replies to

Threading is three headers, not a feature

The first version of this Worker replied with a rewritten subject line and nothing else. Every reply landed in the sender's inbox as a brand-new conversation, because mail clients don't group by subject text — they group by Message-ID, In-Reply-To, and References.

The fix was to carry the incoming Message-ID forward as In-Reply-To, append it to whatever References chain already existed, and mint a fresh Message-ID for the outbound reply so the next reply in the thread has something correct to point back to.

ComponentFindsMisses
Email Worker (compute)Every inbound trigger, MIME parsing, threading, the reject pathAny notion of what the reply should say
AI Search (chat completions)A grounded, on-topic answer in natural languageDelivery, threading, retries, or an audit trail
worker.js — the heart of the send path
// Threading: reference the message we're replying to so mail clients
// nest this reply into the existing conversation instead of starting a new one.
const threadReferences = [references, messageId].filter(Boolean).join(" ");

const rawReply = buildRawEmail({
  from: `${FROM_NAME} <${FROM_ADDRESS}>`,
  replyTo: REPLY_TO_ADDRESS,
  to: message.from,
  subject: replySubject,
  body: aiReplyText,
  inReplyTo: messageId,
  references: threadReferences,
});

// A send binding delivers to exactly the recipients it's called with —
// there is no BCC header, so the audit copy is a second, independent send.
await env.SEND_EMAIL.send(new EmailMessage(FROM_ADDRESS, message.from, rawReply));
await env.SEND_EMAIL.send(new EmailMessage(FROM_ADDRESS, BCC_ADDRESS, rawBccCopy));

Keep it inside Cloudflare

There's no separate mail processing service, no message queue, and no hosted model. The trigger, the compute, and the AI call are three Cloudflare products behind three bindings in one account.

BenefitDetail
One account, one dashboardEmail Routing, the Worker, and AI Search are bindings inside one Cloudflare account, not three vendors to bill, monitor and secure separately
No always-on computeWorkers execute per request on V8 isolates and bill in CPU-milliseconds; there is no idle server charge between emails
No message brokerEmail Routing hands the trigger directly to the Worker's email() export — no queue, webhook relay, or polling inbox to maintain
No secret store to stand upThe AI Search bearer token lives in the Worker's own encrypted secret bindings, set from the dashboard
Nothing to patchNo OS, no container base image, no cron job keeping a process alive

Scope is a prompt problem, not a model problem

Keeping the assistant from answering outside its lane doesn't require a classifier or a second model call. It requires framing the question the same way, every time, before the model ever sees it: find information about one named subject relevant to this message, and reply to it — nothing else is on the table.

Input shapeWhat happensWould otherwise produce
Unrelated question ("what's the weather today")The system prompt instructs the model to decline anything outside the one allowed subjectA fluent, confident answer about something the assistant was never meant to speak to
Empty or whitespace-only bodyFalls back to a literal placeholder string before the AI call is madeA wasted AI call answering nothing in particular
HTML-only email, no text/plain partThe parser returns an empty body rather than guessing at markupAn answer generated from an empty or malformed question
A long forwarded thread with quoted historyOnly the newest message's raw body reaches the modelA reply that assumes context from earlier turns it never actually saw
Diagram showing inbound email routed to Email Routing, triggering the Worker, which calls AI Search for a grounded answer, then builds a reply carrying In-Reply-To and References back to the original Message-ID so it nests into the sender's existing thread, plus an independent audit copy sent to an internal address
The reply carries the original Message-ID forward as In-Reply-To and appends it to References — how mail clients nest a reply into the same thread instead of opening a new one.

// 05 — Results

A real inbox, a real thread, and a cost per email measured in fractions of a cent.

MeasureResult
Fabricated answers about unrelated topicsScoped out at the prompt level on every single call — the assistant is told what it may discuss before it sees the question
Thread fragmentationFixed — In-Reply-To and References are carried through on every reply, so multi-turn conversations stay in one thread
Servers to patch, scale, or pay for at idleZero — the entire path is Cloudflare Workers, Email Routing, and AI Search, all billed per use
Time to first replyA few seconds end to end: trigger → parse → AI call → compose → send
Deployment footprintOne JavaScript file, no build step, editable directly from the dashboard
Audit trailEvery outbound reply is mirrored to an internal address as an independent send
Lines of code~210, zero external dependencies
Cost per interactionUnder $0.0035, all in

What this looks like for an enterprise considering the same pattern:

  • Automated first-response coverage for a known category of inbound email, with no on-call rotation required to keep it running.
  • A measurable reduction in manual reply volume for the subset of email that's genuinely repetitive.
  • A cost structure that tracks actual usage rather than provisioned capacity — a quiet month costs nothing beyond the odd email that does arrive.
  • An audit copy of every automated reply, landing in an internal mailbox the moment it's sent, with no separate logging infrastructure.
  • A deployment that's one file: reviewable, pasteable, and auditable without a build pipeline standing between the source and what's running.

Cost breakdown per interaction

ComponentCost
Workers invocation (parse + compose + two outbound sends)~$0.0003 at Workers' paid-plan request and CPU-time pricing
Email Routing (inbound trigger, outbound sends)$0 — no per-message fee on Cloudflare's own routing
AI Search chat completion (retrieval + generation)~$0.002–0.003 per call, depending on context size and model
Total, worst caseUnder $0.0035 per end-to-end interaction
At 10,000 emails a monthUnder $35 total — no autoscaling group, no idle server, no on-call rotation

// 06 — What I learned

The AI call is the easy part. Email is the hard part.

The hard part was never the AI call.

Generating the reply is one fetch to a chat completions endpoint. Everything that took real engineering sat around it: parsing raw MIME by hand, decoding headers correctly, carrying threading state forward, and sending a second copy for audit. The AI is the narrowest step in the pipeline, not the widest.

Scope has to live in the prompt, not in hope.

Nothing about an inbound email guarantees the sender stays on topic. Framing every question as 'find information about this one subject, and nothing else' at the top of every call — not as a one-time instruction — is what keeps an email address from quietly becoming a general-purpose chatbot.

Email threading is a protocol, not a UI trick.

Mail clients don't group conversations by matching subject lines; they match Message-ID against In-Reply-To and References. Get those three headers right once and every client renders a coherent thread for free.

From and Reply-To are different jobs.

The address that sends mail and the address a human replies to don't have to be the same, and often shouldn't be: splitting them keeps a sending subdomain's deliverability reputation separate from the identity people actually see and respond to.

BCC isn't a header here, it's a second send.

A send-email binding delivers to exactly the envelope recipients it's called with. Getting a visible copy to an internal address meant building and sending a second, differently labelled message — not adding a header and hoping.

Serverless billing matches the shape of email traffic.

Inbound mail arrives in bursts, not a steady load. Billing per request and per CPU-millisecond, instead of per provisioned hour, means the cost curve already matches how the traffic actually behaves — there's no capacity to size for a peak that mostly doesn't happen.

Reject loudly, don't fail silently.

A broken AI call or malformed message calling message.setReject() bounces with a reason the sender and the mail logs can both see. Swallowing the error would have looked fine right up until someone asked why their email vanished.

A single file is itself a feature.

No bundler, no package.json, no CI step — the entire system is one ES module that can be pasted into the Cloudflare dashboard and run. For a project this size, that's not a limitation; it's the deployment story.

// Technologies used

LayerTechnologyWhy it was chosen
ComputeCloudflare Workers (V8 isolates, ES modules)Runs the email() handler with no server, container, or VM to provision or keep warm
Inbound triggerCloudflare Email RoutingRoutes mail addressed to [email protected] straight into the Worker's email() export — no inbox, no polling
Outbound deliveryEmail Workers send_email bindingDelivers the reply and the audit copy as two independent, explicitly-addressed sends
Knowledge & generationCloudflare AI Search — chat completions endpointA managed retrieval-and-generation instance; one HTTP call replaces a vector store, a retrieval layer, and a hosted model
MIME parsingHand-rolled JavaScript, no dependencySubject, Message-ID/References and body extraction, plus quoted-printable and base64 decoding, in well under 150 lines
SecretsWorker encrypted secret bindingsThe AI Search bearer token is stored outside source and injected at runtime
DeploymentSingle ES module file, no build stepEdited and shipped directly from the Cloudflare dashboard; nothing to compile, bundle, or npm install

// Appendix — system at a glance

MetricValue
Source fileworker.js, 1 file
Lines of code~210
External dependencies0
Cloudflare products used4 — Workers, Email Routing, Email Workers send binding, AI Search
Secrets configured1 — AI_API_TOKEN
Outbound sends per inbound email2 — reply to sender, audit copy via BCC-style second send
Median cost per interactionUnder $0.0035
Idle cost$0
GitHub ↗ Related: Arbington
Next case study → Arbington A peer-to-peer education marketplace — a startup retrospective on the product decisions and tradeoffs.