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

Синхронизация записей получателей

Используйте Подписки на события Email Sending для обновления записей приложения после проблем с доставкой. В этом примере используется Cloudflare Queues и Workers KV для исключения получателей из транзакционных уведомлений.

Подготовьте ресурсы

Перед началом работы:

Сохраняйте адрес каждого подходящего получателя в качестве ключа в KV. В значении можно указать настройки уведомлений или другие связанные метаданные.

Изучите последовательность событий

  1. Email Sending публикует события возврата и жалоб.
  2. Очередь доставляет эти события в Worker.
  3. Worker удаляет из KV записи о получателях, которые больше не соответствуют критериям.

Выберите события удаления

Удалите записи для каждого message.complained событие. Такие события означают, что получатель пометил сообщение как спам.

Удаляйте записи об отказах доставки только если payload.bounce.type это "hard". Временные сбои приводят к message.deferred события, пока остаются попытки повтора. Исчерпанные временные попытки повтора могут приводить к message.bounced события с "soft" тип возврата.

Подробнее о теле запроса см. в Доступные события Email Sending.

Создайте очередь и подписку

Создайте очередь и подпишите её на домен отправки:

  1. На панели управления Cloudflare перейдите к разделу Queues страница. Создайте очередь с именем email-events.

    Перейдите в Queues ↗
  2. Выберите email-events, затем выберите Подписки > Подписка на события.

  3. Введите имя подписки и выберите Email Sending в качестве источника.

  4. Выберите домен отправки и message.bounced и message.complained события.

  5. Выберите Подписаться.

Настройка Worker

Привяжите пространство имен KV и зарегистрируйте Worker как потребителя очереди:

{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "name": "recipient-record-sync",
  "main": "src/index.ts",
  // Set this to today's date
  "compatibility_date": "2026-08-28",
  "kv_namespaces": [
    {
      "binding": "RECIPIENTS",
      "id": "<RECIPIENTS_KV_NAMESPACE_ID>"
    }
  ],
  "queues": {
    "consumers": [
      {
        "queue": "email-events",
        "max_batch_size": 10,
        "max_retries": 3,
        "dead_letter_queue": "email-events-dlq"
      }
    ]
  }
}
name = "recipient-record-sync"
main = "src/index.ts"
# Set this to today's date
compatibility_date = "2026-08-28"

[[kv_namespaces]]
binding = "RECIPIENTS"
id = "<RECIPIENTS_KV_NAMESPACE_ID>"

[[queues.consumers]]
queue = "email-events"
max_batch_size = 10
max_retries = 3
dead_letter_queue = "email-events-dlq"

Эта конфигурация создаёт email-events-dlq во время развёртывания. Queues перемещает туда события после трёх попыток.

Добавьте потребителя очереди

queue() обработчик обрабатывает каждое событие независимо. Он удаляет соответствующие записи получателей и повторяет неудачные операции KV.

src/index.js
export default {
	async queue(batch, env) {
		for (const message of batch.messages) {
			try {
				const event = message.body;

				if (shouldRemove(event)) {
					await removeRecipient(env, event);
				}

				message.ack();
			} catch (error) {
				console.error("Failed to process Email Sending event", {
					eventId: message.body.payload.eventId,
					error,
				});
				message.retry();
			}
		}
	},
};

function shouldRemove(event) {
	if (event.type === "cf.email.sending.message.complained") {
		return true;
	}

	return (
		event.type === "cf.email.sending.message.bounced" &&
		event.payload.bounce?.type === "hard"
	);
}

async function removeRecipient(env, event) {
	await env.RECIPIENTS.delete(event.payload.recipient);
	console.log("Removed recipient record", {
		eventId: event.payload.eventId,
		reason: event.type,
	});
}
src/index.ts
interface Env {
	RECIPIENTS: KVNamespace;
}

interface EmailSendingEvent {
	type:
		| "cf.email.sending.message.bounced"
		| "cf.email.sending.message.complained";
	payload: {
		eventId: string;
		recipient: string;
		bounce?: {
			type: "hard" | "soft";
		};
	};
}

export default {
	async queue(batch, env): Promise<void> {
		for (const message of batch.messages) {
			try {
				const event = message.body;

				if (shouldRemove(event)) {
					await removeRecipient(env, event);
				}

				message.ack();
			} catch (error) {
				console.error("Failed to process Email Sending event", {
					eventId: message.body.payload.eventId,
					error,
				});
				message.retry();
			}
		}
	},
} satisfies ExportedHandler<Env, EmailSendingEvent>;

function shouldRemove(event: EmailSendingEvent): boolean {
	if (event.type === "cf.email.sending.message.complained") {
		return true;
	}

	return (
		event.type === "cf.email.sending.message.bounced" &&
		event.payload.bounce?.type === "hard"
	);
}

async function removeRecipient(
	env: Env,
	event: EmailSendingEvent,
): Promise<void> {
	await env.RECIPIENTS.delete(event.payload.recipient);
	console.log("Removed recipient record", {
		eventId: event.payload.eventId,
		reason: event.type,
	});
}

Удаление отсутствующего ключа KV завершается успешно. Это обеспечивает безопасность повторной доставки событий.

Обработчик подтверждает получение каждого успешного сообщения. Операции с ошибкой перемещаются в очередь недоставленных сообщений после трёх попыток.

Разверните Worker

Разверните Worker и конфигурацию его потребителя очереди:

npx wrangler deploy

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