branch:
ReceiveDemo.tsx
12404 bytesRaw
import { useAgent } from "agents/react";
import { useState } from "react";
import {
EnvelopeIcon,
TrayIcon,
ClockIcon,
HashIcon
} from "@phosphor-icons/react";
import { Button, Surface, Empty, Text } from "@cloudflare/kumo";
import { DemoWrapper } from "../../layout";
import {
LogPanel,
ConnectionStatus,
LocalDevBanner,
CodeExplanation,
type CodeSection
} from "../../components";
import { useLogs, useUserId } from "../../hooks";
import type {
ReceiveEmailAgent,
ReceiveEmailState,
ParsedEmail
} from "./receive-email-agent";
const codeSections: CodeSection[] = [
{
title: "Handle incoming emails",
description:
"Override the onEmail method to process incoming messages. The agent receives a parsed AgentEmail object with from, to, and a getRaw() method for full MIME parsing with postal-mime.",
code: `import { Agent } from "agents";
import type { AgentEmail } from "agents/email";
import PostalMime from "postal-mime";
class ReceiveEmailAgent extends Agent<Env> {
async onEmail(email: AgentEmail) {
const raw = await email.getRaw();
const parsed = await PostalMime.parse(raw);
this.setState({
...this.state,
emails: [...this.state.emails, {
id: crypto.randomUUID(),
from: parsed.from?.address || email.from,
to: email.to,
subject: parsed.subject || "(No Subject)",
text: parsed.text,
timestamp: new Date().toISOString(),
}],
});
this.broadcast(JSON.stringify({ type: "new_email" }));
}
}`
},
{
title: "Route emails with routeAgentEmail",
description:
"Use routeAgentEmail in your Worker's email handler with createAddressBasedEmailResolver. Plus-addressing (user+id@domain) automatically maps to the right agent and instance — no manual parsing needed.",
code: `import { routeAgentEmail } from "agents";
import { createAddressBasedEmailResolver } from "agents/email";
export default {
async email(message: ForwardableEmailMessage, env: Env) {
// "receive+demo@example.com" routes to
// ReceiveEmailAgent with agentId "demo"
const resolver = createAddressBasedEmailResolver(
"ReceiveEmailAgent"
);
await routeAgentEmail(message, env, {
resolver,
onNoRoute: async (email) => {
console.warn("No route for:", email.to);
},
});
},
};`
}
];
export function ReceiveDemo() {
const userId = useUserId();
const { logs, addLog, clearLogs } = useLogs();
const [selectedEmail, setSelectedEmail] = useState<ParsedEmail | null>(null);
const [state, setState] = useState<ReceiveEmailState>({
emails: [],
totalReceived: 0
});
const agent = useAgent<ReceiveEmailAgent, ReceiveEmailState>({
agent: "receive-email-agent",
name: `email-receive-${userId}`,
onStateUpdate: (newState) => {
if (newState) {
setState(newState);
addLog("in", "state_update", {
emails: newState.emails.length,
total: newState.totalReceived
});
}
},
onOpen: () => addLog("info", "connected"),
onClose: () => addLog("info", "disconnected"),
onError: () => addLog("error", "error", "Connection error"),
onMessage: (message) => {
try {
const data = JSON.parse(message.data as string);
if (data.type) {
addLog("in", data.type, data);
}
} catch {
// ignore
}
}
});
return (
<DemoWrapper
title="Receive Emails"
description={
<>
Agents can receive real emails via Cloudflare Email Routing. Override
the{" "}
<code className="text-xs bg-kumo-fill px-1 py-0.5 rounded">
onEmail
</code>{" "}
method to process incoming messages — parse them, store them in state,
and notify connected clients. Use plus-addressing (e.g.{" "}
<code className="text-xs bg-kumo-fill px-1 py-0.5 rounded">
receive+id@domain
</code>
) to route emails to specific agent instances.
</>
}
statusIndicator={
<ConnectionStatus
status={
agent.readyState === WebSocket.OPEN ? "connected" : "connecting"
}
/>
}
>
<LocalDevBanner />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6 mt-4">
{/* Left Panel - Info & Stats */}
<div className="space-y-6">
<Surface className="p-4 rounded-lg ring ring-kumo-line">
<div className="text-xs text-kumo-subtle">
Instance:{" "}
<code className="bg-kumo-control px-1 rounded text-kumo-default">
demo
</code>
</div>
</Surface>
<Surface className="p-4 rounded-lg ring ring-kumo-line">
<div className="mb-4">
<Text variant="heading3">Stats</Text>
</div>
<div className="grid grid-cols-2 gap-3">
<div className="p-3 bg-kumo-elevated rounded">
<div className="flex items-center gap-2 text-kumo-subtle text-xs mb-1">
<TrayIcon size={12} />
Inbox
</div>
<div className="text-2xl font-semibold text-kumo-default">
{state.emails.length}
</div>
</div>
<div className="p-3 bg-kumo-elevated rounded">
<div className="flex items-center gap-2 text-kumo-subtle text-xs mb-1">
<HashIcon size={12} />
Total
</div>
<div className="text-2xl font-semibold text-kumo-default">
{state.totalReceived}
</div>
</div>
</div>
{state.lastReceivedAt && (
<div className="mt-3 text-xs text-kumo-subtle flex items-center gap-1">
<ClockIcon size={12} />
Last: {new Date(state.lastReceivedAt).toLocaleString()}
</div>
)}
</Surface>
<Surface className="p-4 rounded-lg bg-kumo-elevated">
<div className="mb-3">
<Text variant="heading3">Setup Instructions</Text>
</div>
<ol className="text-sm text-kumo-subtle space-y-2">
<li>
<strong className="text-kumo-default">1.</strong> Deploy this
playground to Cloudflare
</li>
<li>
<strong className="text-kumo-default">2.</strong> Go to
Cloudflare Dashboard → Email → Email Routing
</li>
<li>
<strong className="text-kumo-default">3.</strong> Add a
catch-all or specific rule routing to this Worker
</li>
<li>
<strong className="text-kumo-default">4.</strong> Send email to:{" "}
<code className="bg-kumo-control px-1 rounded text-xs text-kumo-default">
receive+demo@yourdomain.com
</code>
</li>
</ol>
</Surface>
<Surface className="p-4 rounded-lg ring ring-kumo-line">
<div className="mb-2">
<Text bold size="sm">
Address Format
</Text>
</div>
<div className="text-xs text-kumo-subtle space-y-1">
<div>
<code className="bg-kumo-control px-1 rounded text-kumo-default">
receive+id@domain
</code>
</div>
<div>Routes to ReceiveEmailAgent with instance "id"</div>
</div>
</Surface>
</div>
{/* Center Panel - Inbox */}
<div className="space-y-6">
<Surface className="overflow-hidden rounded-lg ring ring-kumo-line">
<div className="px-4 py-3 border-b border-kumo-line flex items-center gap-2">
<EnvelopeIcon size={16} />
<Text variant="heading3">Inbox</Text>
<span className="text-xs text-kumo-subtle">
({state.emails.length})
</span>
</div>
<div className="max-h-80 overflow-y-auto">
{state.emails.length > 0 ? (
[...state.emails].reverse().map((email) => (
<button
key={email.id}
type="button"
onClick={() => setSelectedEmail(email)}
className={`w-full text-left p-3 border-b border-kumo-fill last:border-0 hover:bg-kumo-tint transition-colors ${
selectedEmail?.id === email.id ? "bg-kumo-control" : ""
}`}
>
<div className="flex items-center justify-between mb-1">
<span className="text-sm font-medium truncate text-kumo-default">
{email.from}
</span>
<span className="text-xs text-kumo-inactive">
{new Date(email.timestamp).toLocaleTimeString()}
</span>
</div>
<p className="text-sm text-kumo-subtle truncate">
{email.subject}
</p>
</button>
))
) : (
<div className="py-8">
<Empty title="No emails received yet" size="sm" />
<p className="text-xs text-kumo-inactive text-center mt-1">
Send an email to see it appear here
</p>
</div>
)}
</div>
</Surface>
{/* Email Detail */}
{selectedEmail && (
<Surface className="p-4 rounded-lg ring ring-kumo-line">
<div className="mb-3">
<div className="flex items-center justify-between">
<Text variant="heading3">{selectedEmail.subject}</Text>
<Button
variant="ghost"
shape="square"
size="xs"
aria-label="Close email"
onClick={() => setSelectedEmail(null)}
>
×
</Button>
</div>
<div className="text-xs text-kumo-subtle mt-1 space-y-0.5">
<div>From: {selectedEmail.from}</div>
<div>To: {selectedEmail.to}</div>
<div>
Date: {new Date(selectedEmail.timestamp).toLocaleString()}
</div>
{selectedEmail.messageId && (
<div className="truncate">
ID: {selectedEmail.messageId}
</div>
)}
</div>
</div>
<div className="bg-kumo-recessed rounded p-3 text-sm whitespace-pre-wrap max-h-48 overflow-y-auto text-kumo-default">
{selectedEmail.text || selectedEmail.html || "(No content)"}
</div>
{selectedEmail.headers &&
Object.keys(selectedEmail.headers).length > 0 && (
<details className="mt-3">
<summary className="text-xs text-kumo-subtle cursor-pointer">
Headers ({Object.keys(selectedEmail.headers).length})
</summary>
<div className="mt-2 text-xs font-mono bg-kumo-recessed rounded p-2 max-h-32 overflow-y-auto text-kumo-default">
{Object.entries(selectedEmail.headers).map(
([key, value]) => (
<div key={key} className="truncate">
<span className="text-kumo-subtle">{key}:</span>{" "}
{value}
</div>
)
)}
</div>
</details>
)}
</Surface>
)}
</div>
{/* Right Panel - Logs */}
<div className="space-y-6">
<LogPanel logs={logs} onClear={clearLogs} maxHeight="500px" />
</div>
</div>
<CodeExplanation sections={codeSections} />
</DemoWrapper>
);
}