commit cff1a79a39e0dfbb679721fbad1f3aa20cb0ffd9 Author: Thomas Gauvin Date: Thu Apr 16 00:54:30 2026 -0400 Agentic Inbox: self-hosted email client with AI agent on Cloudflare Workers diff --git a/.dev.vars.example b/.dev.vars.example new file mode 100644 index 0000000..f0fd0e1 --- /dev/null +++ b/.dev.vars.example @@ -0,0 +1,3 @@ +# Cloudflare Access (required in production) +POLICY_AUD=your-access-policy-audience-tag +TEAM_DOMAIN=https://your-team.cloudflareaccess.com diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..978e05f --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +node_modules/ +build/ +.wrangler/ +.react-router/ +*.tsbuildinfo +*.tgz +.env +.env.* +!.env.example +.dev.vars +!.dev.vars.example +.vite/ +dist/ +snapshot-*.txt +.agents/ +opencode.jsonc +skills-lock.json +.DS_Store +worker-configuration.d.ts diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..387f6e0 --- /dev/null +++ b/LICENSE @@ -0,0 +1,191 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to the Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by the Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding any notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + Copyright (c) 2026 Cloudflare, Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..5f44771 --- /dev/null +++ b/README.md @@ -0,0 +1,92 @@ +
+

Agentic Inbox

+

A self-hosted email client with an AI agent, running entirely on Cloudflare Workers

+
+ +Agentic Inbox lets you send, receive, and manage emails through a modern web interface -- all powered by your own Cloudflare account. Incoming emails arrive via [Cloudflare Email Routing](https://developers.cloudflare.com/email-routing/), each mailbox is isolated in its own [Durable Object](https://developers.cloudflare.com/durable-objects/) with a SQLite database, and attachments are stored in [R2](https://developers.cloudflare.com/r2/). + +An **AI-powered Email Agent** can read your inbox, search conversations, and draft replies -- built with the [Cloudflare Agents SDK](https://developers.cloudflare.com/agents/) and [Workers AI](https://developers.cloudflare.com/workers-ai/). + +![Agentic Inbox screenshot](./demo_app.png) + + +Read the blog post to learn more about Cloudflare Email Service and how to use it with the Agents SDK, MCP, and from the Wrangler CLI: [Email for Agents](https://blog.cloudflare.com/email-for-agents/). + +## How to setup + +[![Deploy to Cloudflare](https://deploy.workers.cloudflare.com/button)](https://deploy.workers.cloudflare.com/?url=https://github.com/cloudflare/agentic-inbox) + +Click the button above to deploy to your Cloudflare account. The deploy flow will automatically provision R2, Durable Objects, and Workers AI. You'll be prompted for: + +- **DOMAINS** -- your domain with Email Routing enabled (e.g. `example.com`) +- **POLICY_AUD** -- your Cloudflare Access Application Audience tag (for auth) +- **TEAM_DOMAIN** -- your Access team domain (e.g. `https://your-team.cloudflareaccess.com`) + +### After deploying + +1. **Set up Email Routing** -- In the Cloudflare dashboard, go to your domain > Email Routing and create a catch-all rule that forwards to this Worker +2. **Enable Email Service** -- The worker needs the `send_email` binding to send outbound emails. See [Email Service docs](https://developers.cloudflare.com/email-routing/email-workers/send-email-workers/) +3. **Create a mailbox** -- Visit your deployed app and create a mailbox for any address on your domain (e.g. `hello@example.com`) +4. **Configure Cloudflare Access** -- Create an Access Application for your Worker URL under Zero Trust > Access > Applications. Copy the Audience tag to `POLICY_AUD` and your team domain to `TEAM_DOMAIN` + +## Features + +- **Full email client** — Send and receive emails via Cloudflare Email Routing with a rich text composer, reply/forward threading, folder organization, search, and attachments +- **Per-mailbox isolation** — Each mailbox runs in its own Durable Object with SQLite storage and R2 for attachments +- **Built-in AI agent** — Side panel with 9 email tools for reading, searching, drafting, and sending +- **Auto-draft on new email** — Agent automatically reads inbound emails and generates draft replies, always requiring explicit confirmation before sending +- **Configurable and persistent** — Custom system prompts per mailbox, persistent chat history, streaming markdown responses, and tool call visibility + +## Stack + +- **Frontend:** React 19, React Router v7, Tailwind CSS, Zustand, TipTap, `@cloudflare/kumo` +- **Backend:** Hono, Cloudflare Workers, Durable Objects (SQLite), R2, Email Routing +- **AI Agent:** Cloudflare Agents SDK (`AIChatAgent`), AI SDK v6, Workers AI (`@cf/moonshotai/kimi-k2.5`), `react-markdown` + `remark-gfm` +- **Auth:** Cloudflare Access JWT validation (required outside local development) + +## Getting Started + +```bash +npm install +npm run dev +``` + +### Configuration + +1. Set your domain in `wrangler.jsonc` +2. Create an R2 bucket named `agentic-inbox`: `wrangler r2 bucket create agentic-inbox` + +### Deploy + +```bash +npm run deploy +``` + +## Prerequisites + +- Cloudflare account with a domain +- [Email Routing](https://developers.cloudflare.com/email-routing/) enabled for receiving +- [Email Service](https://developers.cloudflare.com/email-service/) enabled for sending +- [Workers AI](https://developers.cloudflare.com/workers-ai/) enabled (for the agent) +- [Cloudflare Access](https://developers.cloudflare.com/cloudflare-one/policies/access/) configured for deployed/shared environments (required in production) + +Any user who passes the shared Cloudflare Access policy can access all mailboxes in this app by design. This includes the MCP server at `/mcp` -- external AI tools (Claude Code, Cursor, etc.) connected via MCP can operate on any mailbox by passing a `mailboxId` parameter. There is no per-mailbox authorization; the Cloudflare Access policy is the single trust boundary. + +## Architecture + +``` +┌──────────────┐ ┌──────────────────┐ ┌─────────────────┐ +│ Browser │────>│ Hono Worker │────>│ MailboxDO │ +│ React SPA │ │ (API + SSR) │ │ (SQLite + R2) │ +│ Agent Panel │ │ │ └─────────────────┘ +└──────┬───────┘ │ /agents/* ──────┼────>┌─────────────────┐ + │ │ │ │ EmailAgent DO │ + │ WebSocket │ │ │ (AIChatAgent) │ + └─────────────┤ │ │ 9 email tools │ + │ │────>│ Workers AI │ + └──────────────────┘ └─────────────────┘ +``` + +## License + +Apache 2.0 -- see [LICENSE](LICENSE). diff --git a/app/components/AgentPanel.tsx b/app/components/AgentPanel.tsx new file mode 100644 index 0000000..5039f05 --- /dev/null +++ b/app/components/AgentPanel.tsx @@ -0,0 +1,574 @@ +// Copyright (c) 2026 Cloudflare, Inc. +// Licensed under the Apache 2.0 license found in the LICENSE file or at: +// https://opensource.org/licenses/Apache-2.0 + +import { Badge, Button, Loader, Tooltip } from "@cloudflare/kumo"; +import { + ArrowUpIcon, + RobotIcon, + TrashIcon, + UserIcon, + EnvelopeSimpleIcon, + MagnifyingGlassIcon, + PaperPlaneTiltIcon, + EyeIcon, + ArrowBendUpLeftIcon, + WrenchIcon, + CheckCircleIcon, + StopIcon, + PencilSimpleIcon, +} from "@phosphor-icons/react"; +import { useEffect, useRef, useState } from "react"; +import { useParams } from "react-router"; +import Markdown from "react-markdown"; +import remarkGfm from "remark-gfm"; +import { useUIStore } from "~/hooks/useUIStore"; +import type { UIMessage } from "ai"; + +const TOOL_LABELS: Record = { + list_emails: { + label: "Fetching emails", + icon: , + }, + get_email: { + label: "Reading email", + icon: , + }, + get_thread: { + label: "Loading thread", + icon: , + }, + search_emails: { + label: "Searching", + icon: , + }, + draft_email: { + label: "Drafting email", + icon: , + }, + draft_reply: { + label: "Drafting reply", + icon: , + }, + discard_draft: { + label: "Discarding draft", + icon: , + }, + mark_email_read: { + label: "Updating status", + icon: , + }, + move_email: { + label: "Moving email", + icon: , + }, +}; + +function ToolCallBadge({ + toolName, + state, +}: { + toolName: string; + state: string; +}) { + const info = TOOL_LABELS[toolName] || { + label: toolName, + icon: , + }; + const isDone = + state === "output-available" || + state === "result" || + state === "output-error"; + + return ( +
+ {info.icon} + {info.label} + {isDone ? ( + + ) : ( + + )} +
+ ); +} + +function getToolNameFromPart(part: UIMessage["parts"][number]): string | null { + if (part.type === "dynamic-tool") return (part as any).toolName ?? null; + if (part.type.startsWith("tool-")) return part.type.replace("tool-", ""); + return null; +} + +function hasDraftReplyTool(message: UIMessage): boolean { + return message.parts.some((part) => { + const toolName = getToolNameFromPart(part); + return toolName === "draft_reply"; + }); +} + +function DraftActions({ + onEdit, + disabled, +}: { + onEdit: () => void; + disabled: boolean; +}) { + return ( +
+ +
+ ); +} + +function MessageBubble({ + message, + onAction, + isStreaming, +}: { + message: UIMessage; + onAction?: (action: string) => void; + isStreaming: boolean; +}) { + const isUser = message.role === "user"; + + return ( +
+
+ {isUser ? ( + + ) : ( + + )} +
+
+ {message.parts.map((part, i) => { + const key = `${message.id}-part-${i}`; + if (part.type === "text" && part.text.trim()) { + return ( +
+ {isUser ? ( + part.text + ) : ( + ( + + {children} + + ), + p: ({ children }) => ( +

+ {children} +

+ ), + strong: ({ children }) => ( + + {children} + + ), + ul: ({ children }) => ( +
    + {children} +
+ ), + ol: ({ children }) => ( +
    + {children} +
+ ), + li: ({ children }) => ( +
  • {children}
  • + ), + h1: ({ children }) => ( +

    + {children} +

    + ), + h2: ({ children }) => ( +

    + {children} +

    + ), + h3: ({ children }) => ( +
    + {children} +
    + ), + code: ({ children }) => ( + + {children} + + ), + table: ({ children }) => ( +
    + + {children} +
    +
    + ), + thead: ({ children }) => ( + + {children} + + ), + th: ({ children }) => ( + + {children} + + ), + td: ({ children }) => ( + + {children} + + ), + }} + > + {part.text} +
    + )} +
    + ); + } + const toolName = getToolNameFromPart(part); + if (toolName) { + return ( + + ); + } + return null; + })} + {/* Show action buttons for draft replies */} + {!isUser && hasDraftReplyTool(message) && onAction && ( + onAction("edit")} + disabled={isStreaming} + /> + )} +
    +
    + ); +} + +function AgentChatConnected({ + mailboxId, + useAgent, + useAgentChat, +}: { + mailboxId: string; + useAgent: typeof import("agents/react").useAgent; + useAgentChat: typeof import("@cloudflare/ai-chat/react").useAgentChat; +}) { + const scrollRef = useRef(null); + const inputRef = useRef(null); + const [inputValue, setInputValue] = useState(""); + const { startCompose } = useUIStore(); + + const agent = useAgent({ agent: "EmailAgent", name: mailboxId }); + const { messages, sendMessage, status, setMessages, stop } = + useAgentChat({ agent }); + const isStreaming = status === "streaming" || status === "submitted"; + + useEffect(() => { + const el = scrollRef.current; + if (el) el.scrollTop = el.scrollHeight; + }, [messages]); + + useEffect(() => { + inputRef.current?.focus(); + }, []); + + const handleSend = () => { + const text = inputValue.trim(); + if (!text || isStreaming) return; + setInputValue(""); + sendMessage({ text }); + if (inputRef.current) inputRef.current.style.height = "auto"; + }; + + const handleKeyDown = (e: React.KeyboardEvent) => { + if (e.key === "Enter" && !e.shiftKey) { + e.preventDefault(); + handleSend(); + } + }; + + const suggestedPrompts = [ + "Show me the latest inbox emails", + "Any unread emails?", + "Draft a response to the latest email", + ]; + + return ( +
    + {/* Header */} +
    +
    + AI + + Email Agent + +
    +
    + {isStreaming && } + {messages.length > 0 && ( + +
    +
    + + {/* Messages */} +
    + {messages.length === 0 ? ( +
    +
    + +
    +

    + I can read emails, search conversations, and draft + replies. +

    +
    + {suggestedPrompts.map((prompt) => ( + + ))} +
    +
    + ) : ( +
    + {messages.map((msg) => ( + { + if (action === "edit") { + // Extract draft data from the draft_reply tool result + let draftData: { + to?: string; + subject?: string; + body?: string; + id?: string; + } | null = null; + for (const part of msg.parts) { + if ( + (part as any).toolName === "draft_reply" && + (part as any).result + ) { + draftData = (part as any).result; + break; + } + } + if (draftData) { + const draftEmail = { + id: draftData.id || "", + subject: draftData.subject || "", + sender: mailboxId, + recipient: draftData.to || "", + date: new Date().toISOString(), + read: true, + starred: false, + body: draftData.body || "", + }; + startCompose({ + mode: "reply", + originalEmail: null, + draftEmail, + }); + } else { + sendMessage({ + text: "Let me edit this draft first. Show me what you have so I can modify it.", + }); + } + } + }} + /> + ))} + {isStreaming && ( +
    +
    + +
    +
    + + + Thinking... + +
    +
    + )} +
    + )} +
    + + {/* Input */} +
    + {isStreaming ? ( +
    + +
    + ) : ( +
    +