Short answer: for marketplace password recovery, choose the delivery setup that can prove what happened to every message, then keep the suppression decision in your own system. Inbox placement matters, but an evidence trail is the decision axis. A custom sending domain with aligned DKIM and SPF, bounce events, and exportable history gives an auditor something better than a green dashboard.
The decision note: which delivery shape leaves evidence?
| Delivery shape | Evidence you can normally retain | Best fit | Trade-off |
|---|---|---|---|
| Managed transactional service | Webhooks, message ids, DNS guidance | Teams that need mailbox feedback quickly | Retention and event detail vary by contract |
| Cloud notification primitive | Basic accepted or failed status | Small systems with an existing mail pipeline | Bounce reason and suppression semantics may be thin |
| Self-hosted MTA | Full local logs and routing policy | Data-residency teams with on-call capacity | Reputation, feedback loops, and maintenance become yours |
My default is the first shape, with a local ledger beside it. The transport can change; the account-recovery policy should not. That split also makes a provider review concrete: ask for a sample event export, its retention period, and the fields that connect a bounce to a reset request.
The catch is operational capacity. A self-hosted stack is not suitable for a marketplace that cannot staff reputation incidents, while a managed service is a poor fit when its export cannot satisfy your retention or residency rules. Keep the cloud primitive for low-volume internal tools, not as an automatic answer for customer recovery.
What must a password reset email evidence trail capture?
Begin with the reset request. Store a hash of a random, single-use token, its expiry, the account identifier, and the request time. OWASP recommends a consistent response for existing and non-existing accounts, rate limiting, and invalidation after use; those controls prevent delivery telemetry from becoming an account-enumeration signal.
At enqueue time, create an internal message id. Put that id in provider metadata, and record the From domain, DKIM selector, SPF policy version, template revision, and queue timestamp. The message itself should contain one obvious action and no sensitive account detail.
Normalize incoming events into an append-only record. Keep the raw reason as well as your versioned classification; the latter is what drives suppression.
type MailEvent = {
messageId: string;
recipientHash: string;
kind: "accepted" | "delivered" | "temporary_bounce" | "permanent_bounce" | "complaint";
providerEventId: string;
occurredAt: string;
rawReason?: string;
};
function nextAction(event: MailEvent): "retry" | "suppress" | "none" {
if (event.kind === "permanent_bounce" || event.kind === "complaint") return "suppress";
if (event.kind === "temporary_bounce") return "retry";
return "none";
}
The code is the easy part. A temporary mailbox-full response gets a bounded retry with backoff; a rejection that says the recipient does not exist suppresses before the next attempt. Store both the source event and the derived action so a reviewer can replay the decision.
I once assumed the provider's top-level “bounce” label was enough. It wasn't. The subreason was the only clue that separated a transient refusal from a dead address. Your mileage may vary because taxonomies differ, so make the raw payload available under controlled access and test your classifier against real samples.
How should custom domain, DKIM, SPF, and bounce handling work together?
Publish SPF for the envelope sender and DKIM for the exact custom domain used by recovery mail. DMARC then evaluates alignment with the visible From domain. Test staging and production separately; a passing marketing subdomain says nothing about the account-recovery stream.
Authentication does not decide suppression. A correctly signed message still causes harm if a permanent rejection is retried. Process webhook events idempotently, reject duplicates by provider event id, and write the suppression row before another reset request can enqueue mail. Hash recipient addresses in routine logs, retaining a reversible mapping only for support and legal workflows.
Separate recovery traffic from promotional traffic when reputation or audit scope differs. CAN-SPAM governs commercial messages; even when a reset message is transactional, adjacent marketplace campaigns can create a confusing record if they share templates, domains, and queues. Document the purpose of each stream and the people allowed to remove a suppression.
No single setup wins every constraint. Managed delivery is usually the shortest path to mailbox feedback, but a contractual export gap can outweigh its convenience. Self-hosting offers deep logs, yet it demands operational maturity. A notification primitive may be enough for a private beta, then become an evidence liability as support volume grows.
Use a simple replay gate before launch: given one reset request id, can an engineer show the token lifecycle, authenticated domain, event ids, raw bounce reason, suppression action, and user-visible outcome? Replay three fixtures: delivered, temporary bounce, and permanent bounce. If one join is missing, fix the ledger before tuning copy or changing providers.
The replay is deliberately mundane. For a marketplace account created by a buyer, I would open the request row, follow its message id into the queue, verify the custom-domain headers, inspect the webhook's provider event id, and then compare the suppression row with the support view. A reviewer should be able to answer “why did this address stop receiving mail?” without asking an operator to remember a dashboard click or exposing the reset token. That chain also exposes retention mistakes: raw events may be gone while the suppression decision remains, or timestamps may be recorded in local time in one service and UTC in another. Fix those joins before changing templates.
Ship the proof, not just the email.
Top comments (0)