跳转到内容
搜索文档

处理硬退信邮件

在 Email Service 中检测并处理硬退信通知,以维护发件人声誉。

最后更新 查看 MarkdownAgent 设置

处理硬退信(hard bounce)通知,以自动从邮件列表中移除无效电子邮件地址,并维护良好的发件人声誉。

什么是硬退信?

硬退信是指由于永久性原因导致电子邮件无法投递的情况:

  • 无效电子邮件地址:该电子邮件地址不存在
  • 域名不存在:域名无效或已过期
  • 邮箱已满:收件人邮箱超出存储限制
  • 邮件被阻止:收件人服务器永久拒绝邮件

配置

配置你的 Worker 以处理退信通知:

{
	"name": "bounce-handler",
	// Set this to today's date
	"compatibility_date": "2026-08-17",
	"send_email": [{ "name": "EMAIL" }],
	"kv_namespaces": [
		{
			"binding": "SUPPRESSION_LIST",
			"id": "your-kv-namespace-id",
		},
	],
}
name = "bounce-handler"
# Set this to today's date
compatibility_date = "2026-08-17"

[[send_email]]
name = "EMAIL"

[[kv_namespaces]]
binding = "SUPPRESSION_LIST"
id = "your-kv-namespace-id"

硬退信检测

import * as PostalMime from "postal-mime";

export default {
	async email(message, env, ctx) {
		// Parse the raw email message
		const parser = new PostalMime.default();
		const rawEmail = new Response(message.raw);
		const email = await parser.parse(await rawEmail.arrayBuffer());

		// Check if this is a bounce notification
		if (isBounceNotification(email)) {
			const bounceInfo = await parseBounceInfo(email);

			if (bounceInfo.type === "hard") {
				await handleHardBounce(bounceInfo, env);
				console.log(
					`Hard bounce processed for: ${bounceInfo.originalRecipient}`,
				);
				return;
			}
		}

		// Forward non-bounce emails normally
		await message.forward("admin@yourdomain.com");
	},
};

function isBounceNotification(email) {
	// Check common bounce indicators
	const subject = email.subject?.toLowerCase() || "";
	const fromAddress = email.from?.address?.toLowerCase() || "";

	// Common bounce indicators
	const bounceSubjects = [
		"mail delivery failed",
		"undelivered mail returned to sender",
		"delivery status notification",
		"returned mail",
		"mail system error",
	];

	const bounceFromPatterns = [
		"mailer-daemon",
		"mail-daemon",
		"postmaster",
		"noreply",
		"bounce",
	];

	return (
		bounceSubjects.some((phrase) => subject.includes(phrase)) ||
		bounceFromPatterns.some((pattern) => fromAddress.includes(pattern))
	);
}

async function parseBounceInfo(email) {
	const text = email.text || "";
	const html = email.html || "";
	const content = text + " " + html;

	// Extract original recipient email
	const recipientMatch =
		content.match(/(?:to|for|recipient):\s*([^\s<]+@[^\s>]+)/i) ||
		content.match(/([^\s<]+@[^\s>]+)/);

	const originalRecipient = recipientMatch ? recipientMatch[1] : null;

	// Determine bounce type based on content
	const hardBounceIndicators = [
		"user unknown",
		"no such user",
		"invalid recipient",
		"recipient address rejected",
		"mailbox unavailable",
		"domain not found",
		"5.1.1", // SMTP error code for bad destination mailbox
		"5.1.2", // SMTP error code for bad destination system
		"5.4.1", // SMTP error code for no answer from host
	];

	const isHardBounce = hardBounceIndicators.some((indicator) =>
		content.toLowerCase().includes(indicator.toLowerCase()),
	);

	return {
		type: isHardBounce ? "hard" : "soft",
		originalRecipient,
		reason: extractBounceReason(content),
		timestamp: new Date().toISOString(),
	};
}

function extractBounceReason(content) {
	// Extract the specific error message
	const reasonPatterns = [
		/diagnostic[- ]code:\s*(.+)/i,
		/reason:\s*(.+)/i,
		/error:\s*(.+)/i,
		/(5\.\d+\.\d+[^.\n]*)/i,
	];

	for (const pattern of reasonPatterns) {
		const match = content.match(pattern);
		if (match) {
			return match[1].trim().split("\n")[0]; // Take first line only
		}
	}

	return "Unknown bounce reason";
}

async function handleHardBounce(bounceInfo, env) {
	if (!bounceInfo.originalRecipient) {
		console.log("Could not extract original recipient from bounce");
		return;
	}

	// Add to suppression list in KV
	await env.SUPPRESSION_LIST.put(
		bounceInfo.originalRecipient,
		JSON.stringify({
			type: "hard_bounce",
			reason: bounceInfo.reason,
			timestamp: bounceInfo.timestamp,
			status: "suppressed",
		}),
		{
			metadata: {
				bounceType: "hard",
				addedDate: bounceInfo.timestamp,
			},
		},
	);

	console.log(
		`Added ${bounceInfo.originalRecipient} to suppression list: ${bounceInfo.reason}`,
	);
}

测试硬退信处理

创建测试用的退信通知:

curl --request POST 'http://localhost:8787/cdn-cgi/handler/email' \
  --url-query 'from=mailer-daemon@example.com' \
  --url-query 'to=bounce-handler@yourdomain.com' \
  --header 'Content-Type: application/json' \
  --data-raw 'From: Mail Delivery Subsystem <mailer-daemon@example.com>
To: bounce-handler@yourdomain.com
Subject: Mail delivery failed: returning message to sender
Date: Wed, 28 Aug 2024 10:30:00 +0000
Message-ID: <bounce123@example.com>

This message was created automatically by mail delivery software.

A message that you sent could not be delivered to one or more of its
recipients. This is a permanent error. The following address(es) failed:

  nonexistent@example.com
    SMTP error from remote mail server after RCPT TO:<nonexistent@example.com>:
    host mx.example.com [192.168.1.1]: 550 5.1.1 User unknown

------ This is a copy of the message, including all the headers. ------

Return-path: <sender@yourdomain.com>
From: sender@yourdomain.com
To: nonexistent@example.com
Subject: Welcome to our service
Message-ID: <original123@yourdomain.com>

Welcome! Thanks for signing up.'

检查抑制列表

添加一个实用函数,在发送前检查电子邮件是否已被抑制:

async function isEmailSuppressed(email, env) {
	const suppressionEntry = await env.SUPPRESSION_LIST.get(email);

	if (suppressionEntry) {
		const data = JSON.parse(suppressionEntry);
		console.log(`Email ${email} is suppressed: ${data.reason}`);
		return true;
	}

	return false;
}

// Use before sending emails
export async function sendEmail(recipient, subject, content, env) {
	if (await isEmailSuppressed(recipient, env)) {
		console.log(`Skipping email to suppressed address: ${recipient}`);
		return { success: false, reason: "suppressed" };
	}

	// Proceed with email sending
	// ... your email sending logic
}

最佳实践

  1. 监控退信率:跟踪退信率以维护良好的发件人声誉
  2. 自动清理:定期审查并清理抑制列表
  3. 双重确认订阅:使用双重确认(double opt-in)以减少无效地址
  4. 重试逻辑:为软退信实现适当的重试逻辑
  5. 日志记录:记录所有退信处理,便于调试和分析

后续步骤

这篇文档对您有帮助吗?