← Email Service / email-service / examples / email-routing
Handle hard bounce emails
Handle hard bounce notifications to automatically remove invalid email addresses from your mailing lists and maintain good sender reputation.
What are hard bounces?
Hard bounces occur when an email cannot be delivered due to permanent reasons:
- Invalid email address: The email address does not exist
- Domain does not exist: The domain name is invalid or expired
- Mailbox full: The recipient's mailbox has exceeded storage limits
- Email blocked: The recipient's server permanently rejects emails
Configuration
Configure your worker to handle bounce notifications:
{
"name": "bounce-handler",
// Set this to today's date
"compatibility_date": "2026-08-28",
"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-28"
[[send_email]]
name = "EMAIL"
[[kv_namespaces]]
binding = "SUPPRESSION_LIST"
id = "your-kv-namespace-id"Hard bounce detection
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("[email protected]");
},
};
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}`,
);
}Testing hard bounce handling
Create a test bounce notification:
curl --request POST 'http://localhost:8787/cdn-cgi/handler/email' \
--url-query '[email protected]' \
--url-query '[email protected]' \
--header 'Content-Type: application/json' \
--data-raw 'From: Mail Delivery Subsystem <[email protected]>
To: [email protected]
Subject: Mail delivery failed: returning message to sender
Date: Wed, 28 Aug 2024 10:30:00 +0000
Message-ID: <[email protected]>
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:
[email protected]
SMTP error from remote mail server after RCPT TO:<[email protected]>:
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: <[email protected]>
From: [email protected]
To: [email protected]
Subject: Welcome to our service
Message-ID: <[email protected]>
Welcome! Thanks for signing up.'Checking suppression list
Add a utility function to check if an email is suppressed before sending:
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
}Best practices
- Monitor bounce rates: Track bounce rates to maintain good sender reputation
- Automatic cleanup: Regularly review and clean suppression lists
- Double opt-in: Use double opt-in to reduce invalid addresses
- Retry logic: Implement appropriate retry logic for soft bounces
- Logging: Log all bounce handling for debugging and analytics
Next steps
- Learn about email authentication to improve deliverability
- Set up metrics and analytics to monitor bounce rates
- Implement spam filtering for incoming emails