INTEGRITY Документация

Email-агент

Agents могут отправлять и получать электронную почту с помощью Cloudflare Служба электронной почты. В этом руководстве показано, как отправлять исходящую почту через binding Workers, направлять входящую почту в Agents и безопасно обрабатывать ответные письма.

Предварительные требования

Прежде чем использовать email с Agents, вам понадобится:

  1. Домен, подключённый к Cloudflare Email Service.
  2. A send_email привязку в wrangler.jsonc для исходящей почты.
  3. Правило маршрутизации Email Service, которое отправляет входящую почту в ваш Worker.
  4. Необязательно: EMAIL_SECRET secret, если нужна безопасная маршрутизация ответов.

Настройка домена

  1. Войдите в Cloudflare Dashboard ↗.
  2. Перейдите в Compute & AI > Служба электронной почты.
  3. Выберите Подключить домен и выберите свой домен.
  4. Добавьте записи DNS (SPF и DKIM), чтобы авторизовать отправку.

Изменения DNS для доменов, использующих Cloudflare DNS, обычно вступают в силу в течение 5-15 минут, но их глобальное распространение может занять до 24 часов.

Конфигурация Wrangler

Добавьте привязку email в свой Worker:

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

remote = true опция позволяет вызывать реальный API Email Service во время локальной разработки с помощью wrangler dev.

быстрый старт

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>;

Отправка исходящих писем

Использование sendEmail()

sendEmail() отправляет исходящую почту через send_email привязку, которую вы передаёте явно. Она автоматически добавляет заголовки маршрутизации агента (X-Agent-Name, X-Agent-ID) в каждое сообщение и при необходимости подписывает их с помощью HMAC-SHA256, чтобы ответы можно было маршрутизировать обратно к тому же экземпляру агента.

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;
	}
}

Когда secret задан, агент подписывает заголовки маршрутизации, чтобы ответы, проверенные createSecureReplyEmailResolver route возвращается к тому же экземпляру агента.

Задайте replyTo в почтовый ящик, который направляет обратно на ваш Worker, если вы хотите, чтобы получатели продолжали разговор с тем же агентом.

Маршрутизация входящей почты

Резолверы определяют, какой экземпляр Agent получит входящее письмо. Выберите резолвер, который подходит вашему сценарию использования.

Для базовой отправки и получения через Email Service createAddressBasedEmailResolver() достаточно. Указанный ниже резолвер безопасного ответа необязателен и относится к подписанию ответов Agents SDK, а не является требованием самого Email Service.

createAddressBasedEmailResolver

Рекомендуется для входящей почты. Маршрутизирует письма по адресу получателя.

import { createAddressBasedEmailResolver } from "agents/email";

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

const resolver = createAddressBasedEmailResolver("EmailAgent");

Логика маршрутизации:

Адрес получателя Имя агента ID агента
[email protected] EmailAgent (по умолчанию) support
[email protected] EmailAgent (по умолчанию) sales
[email protected] NotificationAgent user123

Формат sub-адреса (agent+id@domain) позволяет направлять из одного почтового домена в разные пространства имён и экземпляры агентов.

createSecureReplyEmailResolver

Для сценариев ответа с проверкой подписи. Подтверждает, что входящие письма являются подлинными ответами на ваши исходящие письма, что не позволяет злоумышленникам направлять письма на произвольные экземпляры агента.

import { createSecureReplyEmailResolver } from "agents/email";

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

const resolver = createSecureReplyEmailResolver(env.EMAIL_SECRET);

Когда агент отправляет письмо с replyToEmail() или sendEmail() и secret, он подписывает заголовки маршрутизации временной меткой. Когда приходит ответ, этот resolver проверяет подпись и убеждается, что срок её действия не истёк, прежде чем маршрутизировать его.

Варианты:

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"
	},
});

Когда использовать: Если ваш агент инициирует переписку по электронной почте и вам нужно, чтобы ответы безопасно маршрутизировались обратно к тому же экземпляру агента.

createCatchAllEmailResolver

Для маршрутизации на один экземпляр. Направляет все письма на конкретный экземпляр агента независимо от адреса получателя.

import { createCatchAllEmailResolver } from "agents/email";

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

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

Когда использовать: Когда у вас есть единственный экземпляр агента, который обрабатывает все письма (например, общий почтовый ящик).

Сочетание резолверов

Вы можете комбинировать несколько resolver для обработки разных сценариев:

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>;

Обработка писем в своём Agent

AgentEmail интерфейс

Если у агента onEmail метод вызывается, он получает AgentEmail объект:

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
};

Разбор содержимого письма

Используйте библиотеку, например postal-mime ↗ чтобы разобрать исходное письмо:

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);
	}
}

Распознавание писем с автоматическим ответом

Используйте isAutoReplyEmail() чтобы обнаруживать автоответы и избегать почтовых петель:

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...
	}
}

Это проверяет наличие стандартных заголовков RFC 3834 (Auto-Submitted, X-Auto-Response-Suppress, Precedence) которые указывают на то, что письмо является автоответом.

Ответы на письма

Используйте this.replyToEmail() чтобы отправить ответ через канал ответа входящего письма:

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
		});
	}
}

Отложенные ответы

replyToEmail() требует активного AgentEmail объект, поэтому он работает только внутри onEmail(). Если вам нужно ответить позже: из запланированной задачи, вызываемого метода или после подтверждения human-in-the-loop, сохраните информацию об отправителе в состоянии и используйте 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 поле задаёт In-Reply-To заголовок, чтобы почтовые клиенты корректно группировали ответ в цепочку. secret подписывает заголовки маршрутизации агента, чтобы последующие ответы маршрутизировались обратно к этому экземпляру агента через createSecureReplyEmailResolver.

Пересылка писем

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]");
	}
}

Отклонение писем

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...
	}
}

Обработка ошибок

При отправке писем через sendEmail() или replyToEmail(), обрабатывайте следующие распространённые ошибки:

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);
			}
		}
	}
}

Типичные коды ошибок

Код ошибки Описание Решение
E_SENDER_NOT_VERIFIED Домен или адрес отправителя не проверен Проверьте в Cloudflare dashboard
E_RATE_LIMIT_EXCEEDED Достигнут лимит скорости отправки Реализация экспоненциальной задержки
E_DAILY_LIMIT_EXCEEDED Превышена дневная квота Дождитесь сброса квоты или перейдите на более высокий тарифный план
E_CONTENT_TOO_LARGE Письмо превышает ограничение по размеру Уменьшите объём вложений или содержимого
E_RECIPIENT_NOT_ALLOWED Получатель не входит в список разрешённых Проверка разрешённых адресов назначения
E_RECIPIENT_SUPPRESSED Получатель находится в списке подавления Удалить из списка подавления
E_VALIDATION_ERROR Неверный формат email Проверка адресов электронной почты
E_TOO_MANY_RECIPIENTS Более 50 получателей Разбиение на несколько отправок

Защищённая маршрутизация ответов

Если агент отправляет письма и ожидает ответы, используйте безопасную маршрутизацию ответов, чтобы злоумышленники не могли подделать заголовки и перенаправить письма на произвольные экземпляры агента.

Как это работает

  1. Исходящие: Когда вы вызываете replyToEmail() или sendEmail() с secret, агент подписывает заголовки маршрутизации (X-Agent-Name, X-Agent-ID) с использованием HMAC-SHA256.
  2. Входящие: createSecureReplyEmailResolver проверяет подпись перед маршрутизацией.
  3. Принудительное применение: Если письмо было маршрутизировано через защищённый resolver, replyToEmail() требует секрета (или явного null чтобы отказаться).

Настройка

  1. Сохраните подписывающий ключ как секрет Wrangler. Не помещайте его в vars или зафиксировать её в системе контроля версий:

    npx wrangler secret put EMAIL_SECRET
  2. Используйте комбинированный шаблон резолвера:

    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. Подписывайте исходящие письма:

    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
      });
     }
    }

Поведение принудительного применения

Когда письмо маршрутизируется через createSecureReplyEmailResolver, replyToEmail() метод требует обязательного подписания:

secret значение Поведение
"my-secret" Подписывает заголовки (безопасно)
undefined (опущено) Выбрасывает ошибку - необходимо указать secret или явно отказаться
null Разрешено, но не рекомендуется: явный отказ от подписания

Полный пример

Ниже приведён полноценный агент Email Service, который отправляет исходящую почту и обрабатывает защищённые ответы:

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>;

Справочник по 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>;

Отправьте исходящее письмо через привязку Email Service. Автоматически добавляет X-Agent-Name и X-Agent-ID заголовки. Когда secret задан, подписывает заголовки с помощью HMAC-SHA256 для безопасной маршрутизации ответов.

Опция Описание
binding send_email привязку (например, this.env.EMAIL). Обязательно.
to Адрес получателя, массив адресов или EmailAddress объект(ы)
from Адрес отправителя или EmailAddress объект
subject Тема письма
text Тело в виде обычного текста (не менее одного из text/html обязательно)
html HTML-тело (хотя бы одно из text/html обязательно)
replyTo Адрес для ответа или EmailAddress объект
cc адрес получателя CC, массив адресов или EmailAddress объект(ы)
bcc Адрес получателя BCC, массив адресов или EmailAddress объект(ы)
inReplyTo Message-ID для тредов (задаёт In-Reply-To заголовок)
headers Дополнительные пользовательские заголовки (при конфликте приоритет имеют заголовки агента)
secret Секретный ключ для подписи заголовков маршрутизации агента с помощью HMAC

routeAgentEmail

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

Маршрутизирует входящее письмо к подходящему Agent на основе решения резолвера.

Опция Описание
resolver Функция, которая определяет, какому агенту направить письмо
onNoRoute Необязательный колбэк, вызываемый, если информация о маршрутизации не найдена. Используйте его, чтобы отклонить письмо или выполнить собственную обработку. Если колбэк не указан, записывается предупреждение, и письмо отбрасывается.

createSecureReplyEmailResolver

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

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

Создаёт резолвер для маршрутизации ответов по электронной почте с проверкой подписи.

Опция Описание
secret Секретный ключ для проверки HMAC (должен совпадать с ключом, использованным для подписи)
maxAge Максимальный возраст подписи в секундах (по умолчанию: 30 дней / 2592000 секунд)
onInvalidSignature Необязательный колбэк для логирования при сбое проверки подписи

signAgentHeaders

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

Вручную подписать заголовки маршрутизации агента. Возвращает объект с X-Agent-Name, X-Agent-ID, X-Agent-Sig, а также X-Agent-Sig-Ts заголовки.

Полезно при отправке писем через внешние сервисы с сохранением безопасной маршрутизации ответов. Подпись включает временную метку и по умолчанию действительна в течение 30 дней.

Следующие шаги

HTTP и SSE

Обрабатывайте HTTP-запросы в своём Agent.

Webhooks

Получайте события от внешних сервисов.

Agents API

Полный справочник API для Agents SDK.