feat(email): forward non-feed mail to FALLBACK_FORWARD_ADDRESS

Lets you point a domain's catch-all at the worker without losing personal
mail: inbound mail that isn't a feed (invalid_address / feed_not_found) is
forwarded to an optional verified destination instead of being dropped.
Expired feeds and blocked senders are still dropped so newsletters never
leak to the fallback inbox. Unset env keeps the original drop-and-log path.

Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
Julien HerrandClaude Opus 4.7 committed 2026-05-24 17:14:04 +02:00
1 parent 6cb036fe2c
commit 2c450817df
7 files changed
+192 -4

No files matched your search

+33 -1
View File
@@ -1,6 +1,10 @@
import PostalMime from "postal-mime";
import { Env } from "../types";
import { processEmail, RawAttachment } from "../application/email-processor";
import {
processEmail,
RawAttachment,
IngestRejectionReason,
} from "../application/email-processor";
import { normalizeCid } from "../infrastructure/html-processor";
import { logger } from "./logger";
@@ -51,8 +55,36 @@ export async function handleCloudflareEmail(
to: message.to,
reason: result.reason,
});
await maybeForwardFallback(message, env, result.reason);
}
} catch (error) {
console.error("Error processing Cloudflare email:", error);
}
}
// Reasons safe to forward to the catch-all fallback: the mail was never a feed's
// (wrong address shape, or no such feed). Expired feeds and blocked senders are
// dropped so a real newsletter never leaks into the fallback inbox.
const FORWARDABLE_REASONS = new Set<IngestRejectionReason>([
"invalid_address",
"feed_not_found",
]);
async function maybeForwardFallback(
message: ForwardableEmailMessage,
env: Env,
reason: IngestRejectionReason,
): Promise<void> {
const fallback = env.FALLBACK_FORWARD_ADDRESS;
if (!fallback || !FORWARDABLE_REASONS.has(reason)) return;
try {
await message.forward(fallback);
} catch (error) {
logger.warn("Fallback forward failed", {
to: message.to,
fallback,
error: error instanceof Error ? error.message : String(error),
});
}
}