branch:
secure-email-agent.ts
6556 bytesRaw
import { callable } from "agents";
import { PlaygroundAgent as Agent } from "../../shared/playground-agent";
import { type AgentEmail, isAutoReplyEmail } from "agents/email";
import PostalMime from "postal-mime";
// ─────────────────────────────────────────────────────────────────────────────
// Types
// ─────────────────────────────────────────────────────────────────────────────
export interface ParsedEmail {
id: string;
from: string;
to: string;
subject: string;
text?: string;
html?: string;
timestamp: string;
messageId?: string;
isSecureReply?: boolean;
}
export interface SentReply {
id: string;
to: string;
subject: string;
body: string;
timestamp: string;
signed: boolean;
inReplyTo: string;
}
export interface SecureEmailState {
inbox: ParsedEmail[];
outbox: SentReply[];
totalReceived: number;
totalReplies: number;
autoReplyEnabled: boolean;
lastReceivedAt?: string;
}
// ─────────────────────────────────────────────────────────────────────────────
// Agent
// ─────────────────────────────────────────────────────────────────────────────
/**
* SecureEmailAgent - Demonstrates secure email replies with signed headers
*
* This agent receives emails and sends signed replies using replyToEmail().
* The signed headers (X-Agent-Name, X-Agent-ID, X-Agent-Sig, X-Agent-Sig-Ts)
* allow secure routing of replies back to this agent instance.
*
* To use:
* 1. Deploy to Cloudflare
* 2. Set EMAIL_SECRET: `wrangler secret put EMAIL_SECRET`
* 3. Configure Email Routing in Cloudflare dashboard
* 4. Route emails to: secure+{instanceId}@yourdomain.com
*/
export class SecureEmailAgent extends Agent<Env, SecureEmailState> {
initialState: SecureEmailState = {
inbox: [],
outbox: [],
totalReceived: 0,
totalReplies: 0,
autoReplyEnabled: true
};
async onEmail(email: AgentEmail): Promise<void> {
console.log("🔐 SecureEmailAgent: Email from", email.from, "to", email.to);
console.log("🔐 Secure routed:", email._secureRouted ? "Yes" : "No");
try {
// Parse the email
const raw = await email.getRaw();
const parsed = await PostalMime.parse(raw);
// Check if this is a reply (has our signed headers)
const isSecureReply = email._secureRouted === true;
// Create parsed email record
const parsedEmail: ParsedEmail = {
id: crypto.randomUUID(),
from: parsed.from?.address || email.from,
to: email.to,
subject: parsed.subject || "(No Subject)",
text: parsed.text,
html: parsed.html,
timestamp: new Date().toISOString(),
messageId: parsed.messageId,
isSecureReply
};
// Add to inbox
this.setState({
...this.state,
inbox: [...this.state.inbox.slice(-49), parsedEmail],
totalReceived: this.state.totalReceived + 1,
lastReceivedAt: parsedEmail.timestamp
});
// Broadcast to connected clients
this.broadcast(
JSON.stringify({
type: "email_received",
email: parsedEmail,
isSecureReply
})
);
// Send auto-reply if enabled and not an auto-reply itself
// Use the SDK's isAutoReplyEmail utility to detect auto-replies
if (this.state.autoReplyEnabled && !isAutoReplyEmail(parsed.headers)) {
await this.sendSignedReply(email, parsedEmail);
}
console.log("🔐 Email processed:", parsedEmail.subject);
} catch (error) {
console.error("❌ Error processing email:", error);
throw error;
}
}
private async sendSignedReply(
email: AgentEmail,
parsedEmail: ParsedEmail
): Promise<void> {
const replyBody = `Thank you for your email!
I received your message with subject: "${parsedEmail.subject}"
This is an automated response from the Secure Email Agent.
Your reply will be securely routed back to this agent instance.
---
Instance ID: ${this.name}
Total emails processed: ${this.state.totalReceived + 1}
`;
// Use the SDK's replyToEmail with signed headers
await this.replyToEmail(email, {
fromName: "Secure Email Agent",
body: replyBody,
secret: this.env.EMAIL_SECRET // Sign the reply for secure routing
});
// Record the sent reply
const sentReply: SentReply = {
id: crypto.randomUUID(),
to: parsedEmail.from,
subject: `Re: ${parsedEmail.subject}`,
body: replyBody,
timestamp: new Date().toISOString(),
signed: true,
inReplyTo: parsedEmail.id
};
this.setState({
...this.state,
outbox: [...this.state.outbox.slice(-49), sentReply],
totalReplies: this.state.totalReplies + 1
});
this.broadcast(
JSON.stringify({
type: "reply_sent",
reply: sentReply
})
);
console.log("🔐 Signed reply sent to:", parsedEmail.from);
}
@callable({ description: "Toggle auto-reply on/off" })
toggleAutoReply(): boolean {
const newValue = !this.state.autoReplyEnabled;
this.setState({
...this.state,
autoReplyEnabled: newValue
});
this.broadcast(
JSON.stringify({
type: "auto_reply_toggled",
enabled: newValue
})
);
return newValue;
}
@callable({ description: "Clear all emails" })
clearEmails(): void {
this.setState({
...this.state,
inbox: [],
outbox: []
});
this.broadcast(JSON.stringify({ type: "emails_cleared" }));
}
@callable({ description: "Get email stats" })
getStats(): {
inboxCount: number;
outboxCount: number;
totalReceived: number;
totalReplies: number;
autoReplyEnabled: boolean;
} {
return {
inboxCount: this.state.inbox.length,
outboxCount: this.state.outbox.length,
totalReceived: this.state.totalReceived,
totalReplies: this.state.totalReplies,
autoReplyEnabled: this.state.autoReplyEnabled
};
}
}