INTEGRITY Dokumentace

E-mailový agent

Agents mohou odesílat a přijímat e-maily pomocí Cloudflare Email Service. Tento návod ukazuje, jak pomocí vazby Workers odesílat odchozí e-maily, směrovat příchozí poštu do Agents a bezpečně zpracovávat navazující odpovědi.

Předpoklady

Než začnete s Agents používat e-mail, potřebujete:

  1. Doména zapojená do Cloudflare Email Service.
  2. A send_email vazba v wrangler.jsonc pro odchozí e-maily.
  3. Směrovací pravidlo Email Service, které odesílá příchozí poštu do vašeho Workeru.
  4. Volitelné: EMAIL_SECRET tajný klíč, pokud chcete zabezpečené směrování odpovědí.

Nastavení domény

  1. Přihlaste se do Cloudflare Dashboard ↗.
  2. Přejděte na Compute & AI > Email Service.
  3. Vyberte Onboard Domain a vyberte doménu.
  4. Přidejte záznamy DNS (SPF a DKIM) pro autorizaci odesílání.

Změny DNS se u domén využívajících Cloudflare DNS obvykle projeví do 5-15 minut, ale globální rozšíření může trvat až 24 hodin.

Konfigurace Wrangleru

Přidejte e-mailovou vazbu do svého Workeru:

{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "send_email": [
    {
      "name": "EMAIL",
      "remote": true
    }
  ]
}
[[send_email]]
name = "EMAIL"
remote = true

remote = true možnost umožňuje volat skutečné Email Service API během lokálního vývoje s wrangler dev.

Rychlý start

import { Agent, callable, routeAgentEmail } from "agents";
import { createAddressBasedEmailResolver } from "agents/email";
import PostalMime from "postal-mime";

export class EmailAgent extends Agent {
	@callable()
	async sendWelcomeEmail(to) {
		await this.sendEmail({
			binding: this.env.EMAIL,
			to,
			from: "[email protected]",
			replyTo: "[email protected]",
			subject: "Welcome to our service",
			text: "Thanks for signing up. Reply to this email if you need help.",
		});
	}

	async onEmail(email) {
		const raw = await email.getRaw();
		const parsed = await PostalMime.parse(raw);

		console.log("Received email from:", email.from);
		console.log("Subject:", parsed.subject);

		await this.replyToEmail(email, {
			fromName: "Support Agent",
			body: "Thanks for your email! We received it.",
		});
	}
}

export default {
	async email(message, env) {
		await routeAgentEmail(message, env, {
			resolver: createAddressBasedEmailResolver("EmailAgent"),
		});
	},
};
import { Agent, callable, routeAgentEmail } from "agents";
import { createAddressBasedEmailResolver, type AgentEmail } from "agents/email";
import PostalMime from "postal-mime";

export class EmailAgent extends Agent {
	@callable()
	async sendWelcomeEmail(to: string) {
		await this.sendEmail({
			binding: this.env.EMAIL,
			to,
			from: "[email protected]",
			replyTo: "[email protected]",
			subject: "Welcome to our service",
			text: "Thanks for signing up. Reply to this email if you need help.",
		});
	}

	async onEmail(email: AgentEmail) {
		const raw = await email.getRaw();
		const parsed = await PostalMime.parse(raw);

		console.log("Received email from:", email.from);
		console.log("Subject:", parsed.subject);

		await this.replyToEmail(email, {
			fromName: "Support Agent",
			body: "Thanks for your email! We received it.",
		});
	}
}

export default {
	async email(message, env) {
		await routeAgentEmail(message, env, {
			resolver: createAddressBasedEmailResolver("EmailAgent"),
		});
	},
} satisfies ExportedHandler<Env>;

Odesílání odchozích e-mailů

Použití sendEmail()

sendEmail() odesílá odchozí e-maily prostřednictvím send_email vazbu, kterou předáte explicitně. Automaticky vloží hlavičky pro směrování agenta (X-Agent-Name, X-Agent-ID) do každé zprávy a volitelně je podepíše pomocí HMAC-SHA256, aby bylo možné odpovědi směrovat zpět ke stejné instanci agenta.

class MyAgent extends Agent {
	@callable()
	async sendReceipt(to, orderId) {
		const result = await this.sendEmail({
			binding: this.env.EMAIL,
			to,
			from: { email: "[email protected]", name: "Billing Bot" },
			replyTo: "[email protected]",
			subject: `Receipt for order ${orderId}`,
			text: `Your receipt for order ${orderId} is ready.`,
			secret: this.env.EMAIL_SECRET,
		});

		return result.messageId;
	}
}
class MyAgent extends Agent {
	@callable()
	async sendReceipt(to: string, orderId: string) {
		const result = await this.sendEmail({
			binding: this.env.EMAIL,
			to,
			from: { email: "[email protected]", name: "Billing Bot" },
			replyTo: "[email protected]",
			subject: `Receipt for order ${orderId}`,
			text: `Your receipt for order ${orderId} is ready.`,
			secret: this.env.EMAIL_SECRET,
		});

		return result.messageId;
	}
}

Když secret je zadán, agent podepíše směrovací hlavičky, aby odpovědi ověřené createSecureReplyEmailResolver route směruje zpět ke stejné instanci agenta.

Nastavte replyTo do schránky, která směruje zpět na váš Worker, pokud chcete, aby příjemci pokračovali v konverzaci se stejným agentem.

Směrování příchozí pošty

Resolvery určují, která instance Agenta přijme příchozí e-mail. Vyberte resolver, který odpovídá vašemu případu užití.

Pro základní odesílání a přijímání přes Email Service createAddressBasedEmailResolver() stačí. Níže uvedený resolver zabezpečené odpovědi je volitelný a specifický pro podepisování odpovědí v Agents SDK, není to požadavek samotné služby Email Service.

createAddressBasedEmailResolver

Doporučeno pro příchozí poštu. Směruje e-maily podle adresy příjemce.

import { createAddressBasedEmailResolver } from "agents/email";

const resolver = createAddressBasedEmailResolver("EmailAgent");
import { createAddressBasedEmailResolver } from "agents/email";

const resolver = createAddressBasedEmailResolver("EmailAgent");

Logika směrování:

Adresa příjemce Agent Name Agent ID
[email protected] EmailAgent (výchozí) support
[email protected] EmailAgent (výchozí) sales
[email protected] NotificationAgent user123

Formát sub-adresy (agent+id@domain) umožňuje směrovat z jedné e-mailové domény na různé namespace a instance agentů.

createSecureReplyEmailResolver

Pro toky odpovědí s ověřením podpisu. Ověřuje, že příchozí e-maily jsou skutečné odpovědi na vaše odchozí e-maily, což útočníkům brání směrovat e-maily na libovolné instance agentů.

import { createSecureReplyEmailResolver } from "agents/email";

const resolver = createSecureReplyEmailResolver(env.EMAIL_SECRET);
import { createSecureReplyEmailResolver } from "agents/email";

const resolver = createSecureReplyEmailResolver(env.EMAIL_SECRET);

Když váš agent odešle e-mail s replyToEmail() nebo sendEmail() a secret, podepíše směrovací hlavičky časovým razítkem. Když přijde odpověď, tento resolver před směrováním ověří podpis a zkontroluje, že nevypršela platnost.

Možnosti:

const resolver = createSecureReplyEmailResolver(env.EMAIL_SECRET, {
	// Maximum age of signature in seconds (default: 30 days)
	maxAge: 7 * 24 * 60 * 60, // 7 days

	// Callback for logging/debugging signature failures
	onInvalidSignature: (email, reason) => {
		console.warn(`Invalid signature from ${email.from}: ${reason}`);
		// reason can be: "missing_headers", "expired", "invalid", "malformed_timestamp"
	},
});
const resolver = createSecureReplyEmailResolver(env.EMAIL_SECRET, {
	// Maximum age of signature in seconds (default: 30 days)
	maxAge: 7 * 24 * 60 * 60, // 7 days

	// Callback for logging/debugging signature failures
	onInvalidSignature: (email, reason) => {
		console.warn(`Invalid signature from ${email.from}: ${reason}`);
		// reason can be: "missing_headers", "expired", "invalid", "malformed_timestamp"
	},
});

Kdy použít: Pokud váš agent zahajuje e-mailové konverzace a potřebujete odpovědi bezpečně směrovat zpět ke stejné instanci agenta.

createCatchAllEmailResolver

Pro směrování na jednu instanci. Směruje všechny e-maily na konkrétní instanci agenta bez ohledu na adresu příjemce.

import { createCatchAllEmailResolver } from "agents/email";

const resolver = createCatchAllEmailResolver("EmailAgent", "default");
import { createCatchAllEmailResolver } from "agents/email";

const resolver = createCatchAllEmailResolver("EmailAgent", "default");

Kdy použít: Když máte jedinou instanci agenta, která zpracovává všechny e-maily (například sdílenou schránku).

Kombinace resolverů

Resolvery můžete kombinovat pro zpracování různých scénářů:

export default {
	async email(message, env) {
		const secureReplyResolver = createSecureReplyEmailResolver(
			env.EMAIL_SECRET,
		);
		const addressResolver = createAddressBasedEmailResolver("EmailAgent");

		await routeAgentEmail(message, env, {
			resolver: async (email, env) => {
				// First, check if this is a signed reply
				const replyRouting = await secureReplyResolver(email, env);
				if (replyRouting) return replyRouting;

				// Otherwise, route based on recipient address
				return addressResolver(email, env);
			},

			// Handle emails that do not match any routing rule
			onNoRoute: (email) => {
				console.warn(`No route found for email from ${email.from}`);
				email.setReject("Unknown recipient");
			},
		});
	},
};
export default {
	async email(message, env) {
		const secureReplyResolver = createSecureReplyEmailResolver(
			env.EMAIL_SECRET,
		);
		const addressResolver = createAddressBasedEmailResolver("EmailAgent");

		await routeAgentEmail(message, env, {
			resolver: async (email, env) => {
				// First, check if this is a signed reply
				const replyRouting = await secureReplyResolver(email, env);
				if (replyRouting) return replyRouting;

				// Otherwise, route based on recipient address
				return addressResolver(email, env);
			},

			// Handle emails that do not match any routing rule
			onNoRoute: (email) => {
				console.warn(`No route found for email from ${email.from}`);
				email.setReject("Unknown recipient");
			},
		});
	},
} satisfies ExportedHandler<Env>;

Zpracování e-mailů ve vašem agentovi

AgentEmail rozhraní

Když u agenta onEmail metoda je zavolána, obdrží AgentEmail objekt:

type AgentEmail = {
	from: string; // Sender's email address
	to: string; // Recipient's email address
	headers: Headers; // Email headers (subject, message-id, etc.)
	rawSize: number; // Size of the raw email in bytes

	getRaw(): Promise<Uint8Array>; // Get the full raw email content
	reply(options): Promise<void>; // Send a reply
	forward(rcptTo, headers?): Promise<void>; // Forward the email
	setReject(reason): void; // Reject the email with a reason
};

Parsování obsahu e-mailu

Použijte knihovnu, jako je postal-mime ↗ k parsování surového e-mailu:

import PostalMime from "postal-mime";

class MyAgent extends Agent {
	async onEmail(email) {
		const raw = await email.getRaw();
		const parsed = await PostalMime.parse(raw);

		console.log("Subject:", parsed.subject);
		console.log("Text body:", parsed.text);
		console.log("HTML body:", parsed.html);
		console.log("Attachments:", parsed.attachments);
	}
}
import PostalMime from "postal-mime";

class MyAgent extends Agent {
	async onEmail(email: AgentEmail) {
		const raw = await email.getRaw();
		const parsed = await PostalMime.parse(raw);

		console.log("Subject:", parsed.subject);
		console.log("Text body:", parsed.text);
		console.log("HTML body:", parsed.html);
		console.log("Attachments:", parsed.attachments);
	}
}

Rozpoznávání e-mailů s automatickou odpovědí

Použijte isAutoReplyEmail() k detekci automatických odpovědí e-mailem a zamezení e-mailovým smyčkám:

import { isAutoReplyEmail } from "agents/email";
import PostalMime from "postal-mime";

class MyAgent extends Agent {
	async onEmail(email) {
		const raw = await email.getRaw();
		const parsed = await PostalMime.parse(raw);

		// Detect auto-reply emails to avoid sending duplicate responses
		if (isAutoReplyEmail(parsed.headers)) {
			console.log("Skipping auto-reply email");
			return;
		}

		// Process the email...
	}
}
import { isAutoReplyEmail } from "agents/email";
import PostalMime from "postal-mime";

class MyAgent extends Agent {
	async onEmail(email: AgentEmail) {
		const raw = await email.getRaw();
		const parsed = await PostalMime.parse(raw);

		// Detect auto-reply emails to avoid sending duplicate responses
		if (isAutoReplyEmail(parsed.headers)) {
			console.log("Skipping auto-reply email");
			return;
		}

		// Process the email...
	}
}

Tím se kontroluje přítomnost standardních hlaviček RFC 3834 (Auto-Submitted, X-Auto-Response-Suppress, Precedence) které signalizují, že e-mail je automatickou odpovědí.

Odpovídání na e-maily

Použijte this.replyToEmail() k odeslání odpovědi prostřednictvím kanálu odpovědí příchozího e-mailu:

class MyAgent extends Agent {
	async onEmail(email) {
		await this.replyToEmail(email, {
			fromName: "Support Bot", // Display name for the sender
			subject: "Re: Your inquiry", // Optional, defaults to "Re: "
			body: "Thanks for contacting us!", // Email body
			contentType: "text/plain", // Optional, defaults to "text/plain"
			headers: {
				// Optional custom headers
				"X-Custom-Header": "value",
			},
			secret: this.env.EMAIL_SECRET, // Optional, signs headers for secure reply routing
		});
	}
}
class MyAgent extends Agent {
	async onEmail(email: AgentEmail) {
		await this.replyToEmail(email, {
			fromName: "Support Bot", // Display name for the sender
			subject: "Re: Your inquiry", // Optional, defaults to "Re: "
			body: "Thanks for contacting us!", // Email body
			contentType: "text/plain", // Optional, defaults to "text/plain"
			headers: {
				// Optional custom headers
				"X-Custom-Header": "value",
			},
			secret: this.env.EMAIL_SECRET, // Optional, signs headers for secure reply routing
		});
	}
}

Odložené odpovědi

replyToEmail() vyžaduje živé AgentEmail objekt, takže funguje pouze uvnitř onEmail(). Pokud potřebujete odpovědět později, například z naplánované úlohy, z volatelné metody nebo po schválení s lidským zásahem, uložte údaje o odesílateli do stavu a použijte sendEmail():

class MyAgent extends Agent {
	async onEmail(email) {
		const raw = await email.getRaw();
		const parsed = await PostalMime.parse(raw);

		this.setState({
			...this.state,
			pendingReply: {
				to: email.from,
				messageId: parsed.messageId,
				subject: parsed.subject,
			},
		});
	}

	@callable()
	async sendDelayedReply(body) {
		const { pendingReply } = this.state;
		if (!pendingReply) return;

		await this.sendEmail({
			binding: this.env.EMAIL,
			to: pendingReply.to,
			from: "[email protected]",
			subject: `Re: ${pendingReply.subject}`,
			text: body,
			inReplyTo: pendingReply.messageId,
			secret: this.env.EMAIL_SECRET,
		});
	}
}
class MyAgent extends Agent {
	async onEmail(email: AgentEmail) {
		const raw = await email.getRaw();
		const parsed = await PostalMime.parse(raw);

		this.setState({
			...this.state,
			pendingReply: {
				to: email.from,
				messageId: parsed.messageId,
				subject: parsed.subject,
			},
		});
	}

	@callable()
	async sendDelayedReply(body: string) {
		const { pendingReply } = this.state;
		if (!pendingReply) return;

		await this.sendEmail({
			binding: this.env.EMAIL,
			to: pendingReply.to,
			from: "[email protected]",
			subject: `Re: ${pendingReply.subject}`,
			text: body,
			inReplyTo: pendingReply.messageId,
			secret: this.env.EMAIL_SECRET,
		});
	}
}

inReplyTo pole nastavuje In-Reply-To hlavička, aby e-mailoví klienti správně seřadili odpověď do vlákna. secret podepíše hlavičky směrování agenta, aby se navazující odpovědi směrovaly zpět k této instanci agenta prostřednictvím createSecureReplyEmailResolver.

Předávání e-mailů

class MyAgent extends Agent {
	async onEmail(email) {
		await email.forward("[email protected]");
	}
}
class MyAgent extends Agent {
	async onEmail(email: AgentEmail) {
		await email.forward("[email protected]");
	}
}

Odmítání e-mailů

class MyAgent extends Agent {
	async onEmail(email) {
		if (isSpam(email)) {
			email.setReject("Message rejected as spam");
			return;
		}
		// Process the email...
	}
}
class MyAgent extends Agent {
	async onEmail(email: AgentEmail) {
		if (isSpam(email)) {
			email.setReject("Message rejected as spam");
			return;
		}
		// Process the email...
	}
}

Zpracování chyb

Při odesílání e-mailů přes sendEmail() nebo replyToEmail(), ošetřete tyto běžné chyby:

class MyAgent extends Agent {
	async onEmail(email) {
		try {
			await this.replyToEmail(email, {
				fromName: "Support Bot",
				body: "Thanks for your email!",
			});
		} catch (error) {
			switch (error.code) {
				case "E_SENDER_NOT_VERIFIED":
					console.error("Sender domain not verified. Verify in dashboard.");
					break;
				case "E_RATE_LIMIT_EXCEEDED":
					console.error("Rate limit exceeded. Back off and retry.");
					break;
				case "E_DAILY_LIMIT_EXCEEDED":
					console.error("Daily sending quota reached.");
					break;
				case "E_CONTENT_TOO_LARGE":
					console.error("Email content exceeds size limit.");
					break;
				default:
					console.error("Email sending failed:", error.message);
			}
		}
	}
}
class MyAgent extends Agent {
	async onEmail(email: AgentEmail) {
		try {
			await this.replyToEmail(email, {
				fromName: "Support Bot",
				body: "Thanks for your email!",
			});
		} catch (error) {
			switch (error.code) {
				case "E_SENDER_NOT_VERIFIED":
					console.error("Sender domain not verified. Verify in dashboard.");
					break;
				case "E_RATE_LIMIT_EXCEEDED":
					console.error("Rate limit exceeded. Back off and retry.");
					break;
				case "E_DAILY_LIMIT_EXCEEDED":
					console.error("Daily sending quota reached.");
					break;
				case "E_CONTENT_TOO_LARGE":
					console.error("Email content exceeds size limit.");
					break;
				default:
					console.error("Email sending failed:", error.message);
			}
		}
	}
}

Běžné chybové kódy

Kód chyby Popis Řešení
E_SENDER_NOT_VERIFIED Doména nebo adresa odesílatele není ověřena Ověřte v Cloudflare dashboardu
E_RATE_LIMIT_EXCEEDED Dosažen limit rychlosti odesílání Implementace exponenciálního odkladu opakovaných pokusů
E_DAILY_LIMIT_EXCEEDED Překročena denní kvóta Počkejte na obnovení kvóty nebo přejděte na vyšší plán
E_CONTENT_TOO_LARGE E-mail překračuje limit velikosti Omezte přílohy nebo obsah
E_RECIPIENT_NOT_ALLOWED Příjemce není v seznamu povolených adres Kontrola povolených cílových adres
E_RECIPIENT_SUPPRESSED Příjemce je na seznamu potlačených adres Odebrat ze seznamu potlačených adres
E_VALIDATION_ERROR Neplatný formát e-mailu Kontrola e-mailových adres
E_TOO_MANY_RECIPIENTS Více než 50 příjemců Rozdělení do více odeslání

Zabezpečené směrování odpovědí

Pokud váš agent odesílá e-maily a očekává odpovědi, použijte zabezpečené směrování odpovědí, abyste útočníkům zabránili falšovat hlavičky a směrovat e-maily na libovolné instance agenta.

Jak to funguje

  1. Odchozí: Když zavoláte replyToEmail() nebo sendEmail() s secret, agent podepíše směrovací hlavičky (X-Agent-Name, X-Agent-ID) pomocí HMAC-SHA256.
  2. Příchozí: createSecureReplyEmailResolver před směrováním ověří podpis.
  3. Vynucování: Pokud byl e-mail směrován přes bezpečný resolver, replyToEmail() vyžaduje tajný klíč (nebo explicitní null k odhlášení).

Nastavení

  1. Uložte podpisový klíč jako tajný klíč Wrangler. Neumisťujte ho do vars nebo jej commitněte do systému správy verzí:

    npx wrangler secret put EMAIL_SECRET
  2. Použijte kombinovaný vzor resolveru:

    export default {
    	async email(message, env) {
    		const secureReplyResolver = createSecureReplyEmailResolver(
    			env.EMAIL_SECRET,
    		);
    		const addressResolver = createAddressBasedEmailResolver("EmailAgent");
    
    		await routeAgentEmail(message, env, {
    			resolver: async (email, env) => {
    				const replyRouting = await secureReplyResolver(email, env);
    				if (replyRouting) return replyRouting;
    				return addressResolver(email, env);
    			},
    		});
    	},
    };
    export default {
     async email(message, env) {
      const secureReplyResolver = createSecureReplyEmailResolver(
       env.EMAIL_SECRET,
      );
      const addressResolver = createAddressBasedEmailResolver("EmailAgent");
    
      await routeAgentEmail(message, env, {
       resolver: async (email, env) => {
        const replyRouting = await secureReplyResolver(email, env);
        if (replyRouting) return replyRouting;
        return addressResolver(email, env);
       },
      });
     },
    } satisfies ExportedHandler<Env>;
  3. Podepisujte odchozí e-maily:

    class MyAgent extends Agent {
    	async onEmail(email) {
    		await this.replyToEmail(email, {
    			fromName: "My Agent",
    			body: "Thanks for your email!",
    			secret: this.env.EMAIL_SECRET, // Signs the routing headers
    		});
    	}
    }
    class MyAgent extends Agent {
     async onEmail(email: AgentEmail) {
      await this.replyToEmail(email, {
       fromName: "My Agent",
       body: "Thanks for your email!",
       secret: this.env.EMAIL_SECRET, // Signs the routing headers
      });
     }
    }

Chování vynucování

Když je e-mail směrován přes createSecureReplyEmailResolver, replyToEmail() metoda vynucuje podepisování:

secret hodnota Chování
"my-secret" Podepisuje hlavičky (bezpečné)
undefined (vynecháno) Vyhazuje chybu - je nutné zadat tajný klíč nebo explicitně zvolit vyloučení
null Povoleno, ale nedoporučeno: explicitně vylučuje podepisování

Kompletní příklad

Zde je kompletní agent Email Service, který odesílá odchozí poštu a zpracovává zabezpečené odpovědi:

import { Agent, callable, routeAgentEmail } from "agents";
import {
	createAddressBasedEmailResolver,
	createSecureReplyEmailResolver,
} from "agents/email";
import PostalMime from "postal-mime";

export class EmailAgent extends Agent {
	@callable()
	async sendWelcome(to) {
		return this.sendEmail({
			binding: this.env.EMAIL,
			to,
			from: "[email protected]",
			subject: "Welcome!",
			text: "Thanks for signing up.",
			secret: this.env.EMAIL_SECRET,
		});
	}

	async onEmail(email) {
		const raw = await email.getRaw();
		const parsed = await PostalMime.parse(raw);

		console.log(`Email from ${email.from}: ${parsed.subject}`);

		const emails = this.state.emails || [];
		emails.push({
			from: email.from,
			subject: parsed.subject,
			receivedAt: new Date().toISOString(),
		});
		this.setState({ ...this.state, emails });

		await this.replyToEmail(email, {
			fromName: "Support Bot",
			body: `Thanks for your email! We received: "${parsed.subject}"`,
			secret: this.env.EMAIL_SECRET,
		});
	}
}

export default {
	async email(message, env) {
		const secureReplyResolver = createSecureReplyEmailResolver(
			env.EMAIL_SECRET,
			{
				maxAge: 7 * 24 * 60 * 60, // 7 days
				onInvalidSignature: (email, reason) => {
					console.warn(`Invalid signature from ${email.from}: ${reason}`);
				},
			},
		);
		const addressResolver = createAddressBasedEmailResolver("EmailAgent");

		await routeAgentEmail(message, env, {
			resolver: async (email, env) => {
				const replyRouting = await secureReplyResolver(email, env);
				if (replyRouting) return replyRouting;
				return addressResolver(email, env);
			},
			onNoRoute: (email) => {
				console.warn(`No route found for email from ${email.from}`);
				email.setReject("Unknown recipient");
			},
		});
	},
};
import { Agent, callable, routeAgentEmail } from "agents";
import {
	createAddressBasedEmailResolver,
	createSecureReplyEmailResolver,
	type AgentEmail,
} from "agents/email";
import PostalMime from "postal-mime";

interface Env {
	EmailAgent: DurableObjectNamespace<EmailAgent>;
	EMAIL: SendEmail;
	EMAIL_SECRET: string;
}

export class EmailAgent extends Agent<Env> {
	@callable()
	async sendWelcome(to: string) {
		return this.sendEmail({
			binding: this.env.EMAIL,
			to,
			from: "[email protected]",
			subject: "Welcome!",
			text: "Thanks for signing up.",
			secret: this.env.EMAIL_SECRET,
		});
	}

	async onEmail(email: AgentEmail) {
		const raw = await email.getRaw();
		const parsed = await PostalMime.parse(raw);

		console.log(`Email from ${email.from}: ${parsed.subject}`);

		const emails = this.state.emails || [];
		emails.push({
			from: email.from,
			subject: parsed.subject,
			receivedAt: new Date().toISOString(),
		});
		this.setState({ ...this.state, emails });

		await this.replyToEmail(email, {
			fromName: "Support Bot",
			body: `Thanks for your email! We received: "${parsed.subject}"`,
			secret: this.env.EMAIL_SECRET,
		});
	}
}

export default {
	async email(message, env: Env) {
		const secureReplyResolver = createSecureReplyEmailResolver(
			env.EMAIL_SECRET,
			{
				maxAge: 7 * 24 * 60 * 60, // 7 days
				onInvalidSignature: (email, reason) => {
					console.warn(`Invalid signature from ${email.from}: ${reason}`);
				},
			},
		);
		const addressResolver = createAddressBasedEmailResolver("EmailAgent");

		await routeAgentEmail(message, env, {
			resolver: async (email, env) => {
				const replyRouting = await secureReplyResolver(email, env);
				if (replyRouting) return replyRouting;
				return addressResolver(email, env);
			},
			onNoRoute: (email) => {
				console.warn(`No route found for email from ${email.from}`);
				email.setReject("Unknown recipient");
			},
		});
	},
} satisfies ExportedHandler<Env>;

Referenční dokumentace API

EmailAddress

interface EmailAddress {
	email: string;
	name?: string;
}

sendEmail

async sendEmail(options: {
	binding: EmailSendBinding;
	to: string | EmailAddress | (string | EmailAddress)[];
	from: string | EmailAddress;
	subject: string;
	text?: string;
	html?: string;
	replyTo?: string | EmailAddress;
	cc?: string | EmailAddress | (string | EmailAddress)[];
	bcc?: string | EmailAddress | (string | EmailAddress)[];
	inReplyTo?: string;
	headers?: Record<string, string>;
	secret?: string;
}): Promise<EmailSendResult>;

Odešlete odchozí e-mail prostřednictvím vazby Email Service. Automaticky vloží X-Agent-Name a X-Agent-ID hlavičky. Když secret je zadán, podepíše hlavičky pomocí HMAC-SHA256 pro bezpečné směrování odpovědí.

Možnost Popis
binding send_email vazba (například this.env.EMAIL). Povinné.
to Adresa příjemce, pole adres nebo EmailAddress objekt(y)
from Adresa odesílatele nebo EmailAddress objekt
subject Předmět e-mailu
text Tělo ve formátu prostého textu (alespoň jedno z text/html povinné)
html Tělo HTML (alespoň jedno z text/html povinné)
replyTo Adresa pro odpověď nebo EmailAddress objekt
cc Adresa příjemce v kopii CC, pole adres nebo EmailAddress objekt(y)
bcc Adresa příjemce v kopii BCC, pole adres nebo EmailAddress objekt(y)
inReplyTo Message-ID pro vlákna (nastaví In-Reply-To hlavička)
headers Doplňkové vlastní hlavičky (pokud dojde ke kolizi, mají přednost hlavičky agenta)
secret Tajný klíč pro podepisování hlaviček směrování agenta pomocí HMAC

routeAgentEmail

function routeAgentEmail<Env>(
	email: ForwardableEmailMessage,
	env: Env,
	options: {
		resolver: EmailResolver;
		onNoRoute?: (email: ForwardableEmailMessage) => void | Promise<void>;
	},
): Promise<void>;

Směruje příchozí e-mail k příslušnému Agentovi na základě rozhodnutí resolveru.

Možnost Popis
resolver Funkce, která určuje, kterému agentovi se má e-mail přesměrovat
onNoRoute Volitelný callback, který se zavolá, pokud se nenajdou žádné informace o směrování. Použijte jej k odmítnutí e-mailu nebo k vlastnímu zpracování. Pokud není zadán, zaloguje se varování a e-mail se zahodí.

createSecureReplyEmailResolver

function createSecureReplyEmailResolver(
	secret: string,
	options?: {
		maxAge?: number;
		onInvalidSignature?: (
			email: ForwardableEmailMessage,
			reason: SignatureFailureReason,
		) => void;
	},
): EmailResolver;

type SignatureFailureReason =
	| "missing_headers"
	| "expired"
	| "invalid"
	| "malformed_timestamp";

Vytvoří resolver pro směrování e-mailových odpovědí s ověřením podpisu.

Možnost Popis
secret Tajný klíč pro ověřování pomocí HMAC (musí odpovídat klíči použitému k podepsání)
maxAge Maximální stáří podpisu v sekundách (výchozí: 30 dní / 2592000 sekund)
onInvalidSignature Volitelný callback pro logování při selhání ověření podpisu

signAgentHeaders

function signAgentHeaders(
	secret: string,
	agentName: string,
	agentId: string,
): Promise<Record<string, string>>;

Ručně podepsat hlavičky směrování agenta. Vrátí objekt s X-Agent-Name, X-Agent-ID, X-Agent-Sig, a X-Agent-Sig-Ts hlaviček.

Užitečné při odesílání e-mailů prostřednictvím externích služeb při zachování zabezpečeného směrování odpovědí. Podpis obsahuje časové razítko a ve výchozím nastavení bude platný 30 dní.

Další kroky

HTTP a SSE

Zpracovávejte HTTP požadavky ve svém agentovi.

Webhooks

Přijímejte události z externích služeb.

Agents API

Kompletní referenční dokumentace API pro Agents SDK.