In early October I wrote two posts about the inbound mail path at CogniPrep: why forwarding re-sends every pitch from our own authenticated domain and destroys the receiving inbox's spam filter, and why every signal that said "unsolicited stranger" stopped working the day we started emailing strangers ourselves.
The first of those had an opinion in its title: the fix flags rather than drops. Mail the classifier called spam was still forwarded, just from a different address and with a prefix on the subject:
/**
* Mail the spam check flags is still forwarded, but from this address, so a
* single exact `from:` filter in the receiving inbox can file it away.
*/
const SPAM_FROM = `Spam <spam@${FORWARD_FROM_DOMAIN}>`;
const { error: sendError } = await sendRawEmail({
from: spam ? SPAM_FROM : `${category} <noreply@${FORWARD_FROM_DOMAIN}>`,
to: [FORWARD_TO],
replyTo: from,
subject: `${spam ? '[Spam] ' : ''}[${category}] ${subject || '(no subject)'}`,
...
That is all gone now. The classifier's verdict ends the request:
// Mail the spam check flags is discarded rather than forwarded.
const spam = await isLikelySpam({
fromAddress: parsed.from?.value[0]?.address ?? null,
subject: subject || '',
text: parsed.text || '',
});
if (spam) {
logWebhook('resend:email.received:discarded-spam', { category });
return;
}
Why the earlier argument lost
The case for flagging was that it is reversible. Nothing is destroyed, the inbox gets a filter, and a false positive is still sitting in a folder where you can find it.
What actually happened is that the folder was a second system to maintain, in a place the code has no say over. The design depended on a Gmail filter matching one exact from: address, written by hand, in a dashboard, with nothing in the repository to keep it in step. Rename the address or add a second domain and the filter silently stops matching, which fails in the worst direction: pitches back in the main inbox, now with a [Spam] prefix that nothing acts on.
And the honest version of "I still see it, just filed away" is that filed-away spam is a folder you end up checking, which is the attention the filter was supposed to save.
So the flag became a drop. That raises the stakes of a false positive from "in the wrong folder" to "gone", with no copy anywhere, which is only acceptable because of what runs in front of it.
The order is the whole safety argument
Inside the handler, in order:
- Is this an address we forward at all? The MX record points at the mail provider, so every address at the domain exists and every message to any of them arrives at one webhook. Anything not on a monitored address returns early.
- Is it a DMARC report? Those are machine reports no human reads, and they never leave the provider.
- Fetch and parse the raw message. Size checked twice, from a download host that has to match an allowlist.
-
Is it a reply on an existing support ticket? If
threadTicketReplyclaims it, the handler returns. This is the important one, and it is deliberately before the classifier. - Only now, the spam check. A positive verdict logs and returns.
- Attachments are decoded, and the message is forwarded.
Step 4 in front of step 5 is what makes dropping defensible. A customer replying to a support thread is the single most expensive thing to lose, and their reply never reaches the classifier at all: it is matched to a ticket by the thread it belongs to and filed against it. No model opinion is involved.
Moving the classifier above the attachment loop in step 6 was also deliberate, if less dramatic. Discarded mail no longer gets every attachment read into memory and base64 encoded first. The old code did that work and then threw the result at a message it had already labelled as a pitch.
The classifier only ever deletes on a positive answer
The module is explicit about its failure direction:
Fails open. A missing key, timeout, DB error or unparseable reply all count as "not spam", because a missed spam email costs a second and a missed customer costs a customer.
That sentence is doing more work now than it was when it was written. Before, failing open meant an unflagged pitch. Now it means an undeleted one. Every path that is not a confident {"spam": true} ends up forwarded: no API key, a timeout, a database error looking the sender up, a reply that will not parse, an unsure answer. The prompt itself says so: if unsure, answer not spam.
Two more things keep the blast radius small. Senders who are registered users are trusted outright and never reach the model, so a customer writing from the address they signed up with cannot be classified at all. And the thing the model is asked is not "does this look commercial", it is a direction question, because our own outreach means plenty of legitimate mail is commercial and arrives from strangers with phone numbers in it. The question is who is selling to whom.
What is observable after a delete
The one thing a drop must not do is become invisible. The log event was renamed with the behaviour:
resend:email.received:flagged-spam -> resend:email.received:discarded-spam
The message is gone, but the fact that a message was discarded, and which category of address it was sent to, is recorded. That is enough to answer "has this suddenly started dropping ten a day" without keeping the mail itself, which is the whole point of dropping it.
The other door into the same queue is still the web form on the help page, which asks how urgent the problem is and goes nowhere near a spam classifier, and the about page says who you are writing to. If the drop ever eats a real message, that form is the path that cannot be eaten.
Top comments (0)