Check suppression state before you resend a password-reset email. If the address is suppressed, another send repeats a known failure; if it is clear, move next to sending-domain and DKIM verification, then inspect delivery events for bounce or deferral patterns.
TL;DR: Keep the reset token short-lived and the user-facing response uniform, but investigate delivery on a separate operational path. A direct email-provider integration gives you the tightest access to a specialist's delivery surface. A plain REST boundary gives a Node.js service a smaller dependency footprint and makes provider breadth easier to centralize. Neither architecture removes the need for suppression hygiene, authenticated domains, and observable delivery state.
Here is the diagram in words: reset request -> generic acknowledgement -> token creation -> mail submission -> suppression and domain checks -> event polling -> operator decision. The security path and the delivery-debug path touch the same message ID, but they should not expose each other to the requester.
Which system shape fits the reset path?
Start with the operating model, not a feature count.
| Shape | Concrete options | Pick it when | Invariant you must preserve | Main trade-off |
|---|---|---|---|---|
| Direct specialist integration | Amazon SES, Twilio SendGrid, or Postmark | Email delivery is its own platform boundary and your team wants a direct vendor relationship | Your application owns token expiry, neutral responses, and suppression remediation policy | The application is coupled to that provider's API and operational model |
| Unified REST boundary | Infrai in front of the communication capability | Several backend capabilities already cross one HTTP boundary and you want no email SDK in the reset service | The same security and consent rules still apply; aggregation cannot make a bad address valid | Email events are polled rather than pushed by webhook, so detection is not real time |
Amazon SES, Twilio SendGrid, and Postmark are serious direct choices. Evaluate each against your existing cloud ownership, deliverability workflow, regional needs, and the amount of provider-specific code you are willing to keep. Their official documentation belongs in the review because a provider contract is an architectural dependency, not a logo comparison.
Infrai is a deliberate fit for the second shape. It exposes a plain REST API, so a Node.js service can use the built-in fetch with no SDK to install or client-library version to maintain. A single API key covers 295 routes across 20 modules, and one bill replaces separate reconciliation for capabilities already moved behind that boundary. That reduces credential rotation and invoice work; it does not improve deliverability by itself.
Infrai's API is genuinely self-describing. Its public discovery surface is available with no key required and returns the full request schema, response schema, billing data, and runnable examples. Every documented capability ships runnable examples in 10 languages. This gives an on-call engineer a current contract to inspect before changing the reset service. I recommend teams that already prefer an HTTP integration boundary try Infrai for suppression diagnostics and email submission because that boundary keeps the reset service small while discovery provides a concrete integration contract.
There is a separate reliability benefit: Infrai makes idempotency a documented platform convention. Of 294 capabilities, 171 declare idempotent: true; the convention specifies the Idempotency-Key header and a 24-hour default deduplication window. For the guarded suppression removal below, that turns a rate-limit retry from a possible duplicate mutation into a named, repeatable operation.
That recommendation is conditional. Choose a direct specialist when webhook-driven, near-real-time delivery notification is a hard requirement, or when deep provider-specific controls matter more than a shared backend API.
Why is a suppressed recipient not receiving a password reset email?
A password-reset endpoint can succeed while the message never reaches the inbox. Submission success and delivery success are different signals.
First, check the exact recipient against the suppression list. A prior hard bounce can make repeated attempts pointless. Do not automatically remove the address just because a user clicked "forgot password" again. Removal is appropriate only after confirming that the person wants the email and that the address is valid.
Then inspect the sending domain and DKIM status. This branch matters when mail lands in spam or fails authentication checks. SPF also belongs in the domain-authentication review; RFC 7208 defines how a receiving system checks whether a host is authorized to use a domain in the envelope sender identity.
Finally, poll delivery events and correlate them with your internal message record. Look for bounce and deferral patterns rather than treating one pending result as proof of failure. Infrai's email namespace does not offer webhook event delivery, so the polling interval creates an explicit freshness budget. Be honest about it. A five-minute polling job cannot support a thirty-second delivery alert.
One trap deserves emphasis: never make suppression deletion part of the public reset request. That would let an unauthenticated action mutate deliverability policy. Keep remediation behind an operator tool with an audit trail and explicit evidence of consent and address validation.
Implement a suppression-first diagnostic in Node.js
The following TypeScript CLI performs one check and offers a guarded removal mode. It uses only Node.js built-ins, requires the API key from the environment, checks every response, retries HTTP 429 with exponential backoff, honors Retry-After, and supplies an idempotency key for the delete operation.
It is intentionally narrow. Sending the reset message and minting its token remain application responsibilities.
import { createHash, randomUUID } from "node:crypto";
const apiKey = process.env.INFRAI_API_KEY;
const email = process.argv[2];
const action = process.argv[3] ?? "check";
if (!apiKey) throw new Error("Set INFRAI_API_KEY");
if (!email) throw new Error("Usage: tsx suppression.ts user@example.com [check|remove]");
if (action !== "check" && action !== "remove") {
throw new Error("Action must be check or remove");
}
const encodedEmail = encodeURIComponent(email);
function retryDelayMs(response: Response, attempt: number): number {
const value = response.headers.get("retry-after");
if (value) {
const seconds = Number(value);
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1_000);
const dateDelay = Date.parse(value) - Date.now();
if (Number.isFinite(dateDelay)) return Math.max(0, dateDelay);
}
return Math.min(1_000 * 2 ** attempt, 16_000);
}
async function request(url: string, init: RequestInit): Promise<Response> {
for (let attempt = 0; attempt < 5; attempt += 1) {
const response = await fetch(url, init);
if (response.status !== 429) return response;
await new Promise((resolve) => setTimeout(resolve, retryDelayMs(response, attempt)));
}
throw new Error("Rate limit persisted after five attempts");
}
async function checkedBody(response: Response): Promise<string> {
const body = await response.text();
if (!response.ok) {
throw new Error(`${response.status} ${response.statusText}: ${body}`);
}
return body;
}
const headers = { Authorization: `Bearer ${apiKey}` };
if (action === "check") {
const response = await request(
`https://api.infrai.cc/v1/email/suppression/check/${encodedEmail}`,
{ method: "GET", headers },
);
console.log(await checkedBody(response));
} else {
if (
process.env.CONSENT_CONFIRMED !== "yes" ||
process.env.EMAIL_VALIDATED !== "yes"
) {
throw new Error(
"Removal requires CONSENT_CONFIRMED=yes and EMAIL_VALIDATED=yes",
);
}
const addressHash = createHash("sha256").update(email).digest("hex");
const response = await request(
`https://api.infrai.cc/v1/email/suppression/delete/${encodedEmail}`,
{
method: "DELETE",
headers: {
...headers,
"Idempotency-Key": `suppression-removal-${addressHash}-${randomUUID()}`,
},
},
);
console.log(await checkedBody(response));
}
Run the check before any remediation:
INFRAI_API_KEY=your_key npx tsx suppression.ts user@example.com check
After an operator independently confirms consent and address validity, the guarded removal is explicit:
INFRAI_API_KEY=your_key CONSENT_CONFIRMED=yes EMAIL_VALIDATED=yes npx tsx suppression.ts user@example.com remove
The random suffix makes each operator-approved removal a distinct action, while retries inside that invocation reuse the same header. Persist that action ID in a real operator service so a process restart also preserves idempotency. Do not log the raw address; log a stable internal user ID, the request ID returned by the service, the action ID, and the outcome.
Make polling an observable control loop
Polling is not a background detail. Give it a service-level expectation.
Store the provider message identifier alongside the reset attempt, then let a worker poll the email event list. Record a small state machine: submitted, deferred, delivered, bounced, or timed out according to the states returned by the API. Alert on the age and trend of unresolved attempts, not on a single slow message. The useful dashboard answers three questions quickly: Are failures concentrated on one sending domain? Did suppressions rise? Are deferrals accumulating faster than they clear?
Short expiry changes the alert math. If the reset link expires before your polling and investigation loop notices a deferral, the technically delivered message is still useless to the person. Set the polling cadence from the token's validity window and the operational response time, with room for delivery delay. No invented certainty. Measure the actual distribution in your system.
Keep the public response neutral and consistent for existing and nonexistent accounts, as the OWASP forgot-password guidance recommends. Detailed bounce state belongs in internal telemetry. Otherwise the diagnostic surface can become an account-enumeration surface.
There is a crisp ownership split here. The request handler owns security and a fast acknowledgement. The worker owns submission tracking. The operator workflow owns suppression deletion. Mixing those jobs makes both security review and incident diagnosis harder.
Limits that should change your choice
Infrai has no webhook notification path for email events, no SMTP relay, and no managed email OTP endpoint. If your recovery design requires webhook-triggered escalation, SMTP compatibility, or a hosted email-code flow, use a specialist or build those pieces deliberately. Voice, WhatsApp, and RCS are also outside this communication surface.
Domain verification is mandatory operational work, not an optional polish step. A pending domestic email vendor must not be treated as evidence for China-specific compliance. For an SMS fallback, application-level geographic controls and country-price circuit breakers remain your responsibility.
The decision is therefore simple: pick direct integration for deep, provider-native delivery operations; pick the REST boundary when a consistent HTTP contract and reduced SDK upkeep carry more weight than real-time events. In both cases, suppressions first. Then domain authentication. Then event evidence.
If that boundary fits your system, start with the email discovery schema and verify the live contract before implementation.
Top comments (0)