Block a malformed password reset email request at dispatch; change a recipient's suppression state only when delivery evidence supports that decision. In a logistics portal, confusing those actions can turn a sender-domain deployment mistake into an account-recovery problem for every dispatcher who asks for help.
TL;DR: For malformed password reset email requests, verify the sending domain and DKIM configuration, validate the API JSON payload, and preview the template with the backend's actual reset-link variables. An invalid from domain or template render error calls for configuration or payload repair. A subsequent bounce calls for message-and-event inspection before suppressing an invalid recipient. Retrying everything is an operational policy, and a poor one.
The runbook below treats false suppression as the release hazard. A Node.js application remains the caller; a small Go program makes the decision boundary executable without pretending that local diagnostic fields are a provider's request schema. Its purpose is narrow: prevent the recovery path from treating its own mistakes as evidence against a recipient.
Should a malformed password reset email request trigger recipient suppression?
Start by recording where the attempt stopped. A JSON parsing failure occurs before a usable API payload exists. A schema mismatch can survive JSON parsing. A missing reset-link variable can survive both serialization and a superficial review of the payload, yet still leave the template unable to render the intended message. None of these observations establishes that a dispatcher's mailbox is invalid, even if the user-facing symptom is identical: the reset email never arrives.
Keep that distinction.
For a sender-domain problem, inspect domain verification and DKIM setup first. RFC 6376 specifies DKIM's domain-signing mechanism; it is not a recipient-existence test. Repairing a signing record while adding recipients to a suppression list mixes two unrelated state transitions, which makes later recovery harder because fixing the sender does not explain why those addresses were suppressed.
For a template problem, compare the maintained template's variable names with the exact data the Node.js backend serializes, then preview that combination before allowing dispatch. A local name such as reset_link is useful in a test fixture only if the selected template expects it; it must not become an assumed API field. Use synthetic reset data, and keep actual recovery tokens out of diagnostic records.
An accepted send belongs to a separate investigation.
Inspect its message record and delivery events, correlating them with the logical reset attempt before deciding whether the recipient is invalid. An event for a different attempt, or a delayed observation without enough context, is insufficient grounds for changing recipient state. The safe response to incomplete evidence is to continue bounded observation, not to manufacture another send. For example, when replaying a synthetic template-variable rename, preserve the address's previous state throughout the failed preview, the corrected preview, and any later observation; otherwise the replay can accidentally validate a suppression decision that was made before a message even existed. Don't let a green retry conceal that sequence.
This is the trap: suppression can look like successful containment while quietly blocking valid recovery requests.
Encode the decision before touching the send adapter
First inspect the sender record with this read-only diagnostic. Configure API_BASE_URL with the trusted HTTPS API base from the service documentation, including its version prefix, set SENDING_DOMAIN to the sending domain, and supply INFRAI_API_KEY through the environment. The program retrieves the domain record, checks the response status, and prints the JSON without guessing which response field proves verification. Review the record against the domain's verification requirements.
package main
import (
"encoding/json"
"fmt"
"io"
"net/http"
"net/url"
"os"
"strconv"
"strings"
"time"
)
func inspect() error {
base, err := url.Parse(os.Getenv("API_BASE_URL"))
key, domain := os.Getenv("INFRAI_API_KEY"), os.Getenv("SENDING_DOMAIN")
if err != nil || base.Scheme != "https" || base.Host == "" ||
base.User != nil || base.RawQuery != "" || base.Fragment != "" ||
key == "" || domain == "" {
return fmt.Errorf("configure a trusted HTTPS API base, key, and domain")
}
destination := strings.TrimRight(base.String(), "/") +
"/email/domain/get/" + url.PathEscape(domain)
client := &http.Client{
Timeout: 15 * time.Second,
CheckRedirect: func(_ *http.Request, _ []*http.Request) error {
return http.ErrUseLastResponse
},
}
for attempt := 0; attempt < 3; attempt++ {
req, err := http.NewRequest("GET", destination, nil)
if err != nil { return err }
req.Header.Set("Authorization", "Bearer " + key)
response, err := client.Do(req)
if err != nil { return err }
body, readErr := io.ReadAll(io.LimitReader(response.Body, (1<<20)+1))
response.Body.Close()
if readErr != nil { return readErr }
if len(body) > 1<<20 { return fmt.Errorf("response exceeds diagnostic limit") }
if response.StatusCode == 429 && attempt < 2 {
delay := time.Second * time.Duration(1<<attempt)
value := response.Header.Get("Retry-After")
if seconds, err := strconv.ParseInt(value, 10, 64); err == nil && seconds >= 0 {
if seconds > 30 { return fmt.Errorf("defer retry for %d seconds", seconds) }
if requested := time.Duration(seconds)*time.Second; requested > delay {
delay = requested
}
} else if deadline, err := http.ParseTime(value); err == nil {
if requested := time.Until(deadline); requested > delay { delay = requested }
}
if delay > 30*time.Second { return fmt.Errorf("defer retry for %s", delay) }
time.Sleep(delay)
continue
}
if response.StatusCode < 200 || response.StatusCode >= 300 {
return fmt.Errorf("HTTP %d: %s", response.StatusCode, body)
}
if !json.Valid(body) { return fmt.Errorf("response is not valid JSON") }
fmt.Println(string(body))
return nil
}
return fmt.Errorf("read retry budget exhausted")
}
func main() {
if err := inspect(); err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
}
Save this program separately from the policy example below. Its 3-attempt budget, 15-second request timeout (15,000 ms), and 30-second maximum inline wait are diagnostic choices, not provider limits. Longer waits return control to the operator rather than retrying early. Because this operation reads state, it creates no message to deduplicate; keep the response in a restricted diagnostic workflow.
Use an application-owned decision function between diagnostics and suppression writes. The example deliberately accepts an operator-reviewed conclusion about invalid-recipient evidence rather than inventing a provider bounce-code taxonomy. Production adapters must derive that conclusion from the selected service's documented event semantics and the matching message record; an arbitrary delivery failure is not equivalent to an invalid address.
The complete Go program below uses only the standard library. Save it as a Go source file and run it with a Go toolchain. It prints decisions for synthetic logistics recovery cases, makes no network requests, and performs no suppression writes.
package main
import "fmt"
type Evidence struct {
JSONValid bool
SchemaValid bool
DomainVerified bool
TemplatePreviewOK bool
Accepted bool
InvalidRecipientConfirmed bool
}
func decide(e Evidence) string {
if !e.JSONValid || !e.SchemaValid {
return "hold dispatch; repair payload; preserve recipient state"
}
if !e.DomainVerified {
return "hold dispatch; verify sender; preserve recipient state"
}
if !e.TemplatePreviewOK {
return "hold dispatch; repair template data; preserve recipient state"
}
if !e.Accepted {
return "inspect send result; preserve recipient state"
}
if e.InvalidRecipientConfirmed {
return "suppress invalid recipient; record supporting evidence"
}
return "observe delivery; preserve recipient state"
}
func main() {
cases := []struct {
name string
evidence Evidence
}{
{"dispatcher-malformed-json", Evidence{}},
{"dispatcher-unverified-sender", Evidence{
JSONValid: true, SchemaValid: true,
}},
{"dispatcher-template-mismatch", Evidence{
JSONValid: true, SchemaValid: true, DomainVerified: true,
}},
{"dispatcher-awaiting-events", Evidence{
JSONValid: true, SchemaValid: true, DomainVerified: true,
TemplatePreviewOK: true, Accepted: true,
}},
{"dispatcher-confirmed-invalid", Evidence{
JSONValid: true, SchemaValid: true, DomainVerified: true,
TemplatePreviewOK: true, Accepted: true,
InvalidRecipientConfirmed: true,
}},
}
for _, test := range cases {
fmt.Printf("%s: %s\n", test.name, decide(test.evidence))
}
}
The order is intentional. Payload and configuration failures preserve recipient state even if another input is accidentally marked as invalid-recipient evidence. This favors withholding an unsupported suppression decision over taking an irreversible operational shortcut; it also means the observer must keep investigating unresolved delivery outcomes instead of abandoning them.
This is a decision example, not an API JSON payload example. For the actual Node.js request, obtain the selected capability's request schema, serialize against that schema, and handle the returned status explicitly. Correct a malformed request before retrying it. For transient rate limiting, use bounded exponential backoff and honor Retry-After; for a retried write, retain the logical attempt identifier and use the capability's documented idempotency mechanism where supported. Do not confuse a fresh user recovery request with a transport retry of an existing send.
Budget the observer, then assign what to buy
Suppression depends on evidence arrival, so capacity planning must include the observer. With pull-based message and event inspection, the polling interval contributes to the time during which an invalid recipient can remain eligible for another attempt. Define an evidence-freshness SLO separately from request acceptance, and measure recovery success separately again. Acceptance is a useful milestone. It does not establish delivery.
For a hypothetical backlog of 900 pending messages and a 30-second per-message polling interval, the baseline is 30 status reads per second, before retries or event retrieval. These are planning inputs, not measured traffic or a provider quota. Bound observer concurrency, give rate-limited reads a retry budget, and consider event-list reconciliation where its documented semantics fit; shortening the interval without a capacity budget merely moves uncertainty into the observer's queue.
That ownership question should drive procurement. The following buy-versus-build table compares integration choices and acceptance work, rather than awarding an unsupported deliverability ranking.
| Choice | What to buy or build | Appropriate boundary and acceptance work |
|---|---|---|
| Amazon SES | Evaluate a direct email integration within an AWS operating model. | Choose it when AWS ownership is useful; require a demonstration of sender verification, bounce evidence, and suppression handling before committing. |
| Postmark | Evaluate a specialist transactional-email service. | Choose it when the team wants to focus its evaluation on transactional workflows; replay the same template-mismatch and invalid-recipient cases. |
| Twilio SendGrid | Evaluate a direct integration with a broader email service. | Choose it when that email scope fits the roadmap; keep password-reset evidence and suppression acceptance criteria explicit. |
| Unified backend API | Buy the capability contract and vendor integration layer. | Fits a platform team consolidating backend integrations; application code still owns the suppression decision. |
| Self-managed integration layer | Build the adapters, correlation store, observer, and suppression policy. | Fits a team prepared to own adapter changes and on-call diagnosis; retain the same evidence tests when replacing a managed contract. |
Infrai offers one API key, one bill, and a plain REST API with no SDK required, while switching the vendor behind a capability leaves the application's calling code unchanged. That shared credential covers 295 routes across 20 modules, reducing separate credential provisioning for email and adjacent backend workers. The Node.js sender and Go diagnostic can use their standard HTTP clients instead of maintaining separate vendor SDK dependencies. Public, keyless discovery also exposes request and response schemas so reviewers can check template payloads before dispatch. The trade-off is pull-based event observation: it is not suitable for a requirement that delivery events arrive through webhooks, and the polling budget belongs in the platform SLO.
The three direct services are real alternatives, not a measured ranking. Their documentation belongs in the procurement review because a service's reputation does not establish that its event semantics match this runbook. Compare the same synthetic cases and record which responsibilities remain with the platform team. A broader API saves integration work only if the resulting ownership boundary is acceptable.
Don't buy an SLO by implication.
For the unified-contract option, scope the purchase to email and SMS with API payload validation; SMTP relay and voice, WhatsApp, or RCS workflows belong outside this selection. Plan email verification-code generation in the application if recovery needs that branch. Keep reset sends immediate rather than making rollback depend on canceling scheduled email. Treat domestic compliance approval as a separate procurement requirement, and place geographic SMS abuse controls and country-based spend circuit breakers in the business layer. Tag-level cost allocation and SMS template inventory should also be explicit acceptance questions, rather than assumed reporting features.
Verify recipient state, then rehearse rollback
Before promotion, replay the five synthetic cases from the policy program through a staging integration, recording the decision and recipient state at each boundary. The malformed JSON, unverified sender, and template mismatch cases must leave suppression unchanged. The accepted-but-unresolved case must remain under observation. Only the confirmed invalid-recipient case should become eligible for a suppression write, with the matching evidence retained for review.
Then repeat the confirmed event twice. The second observation must not create a second logical state change. This checks the application's reconciliation behavior, which a successful send response cannot verify; separately, check that a transport retry reuses its write identity and a genuinely new recovery request receives a new one. Use controlled mailboxes and synthetic links throughout, and inspect the actual domain record and rendered template rather than treating local booleans as integration evidence.
Rollback needs a ledger.
Record the prior suppression state, the evidence reference, and the policy version for every change made during rollout. If verification reveals false suppression, pause the new suppression writer, restore the previous policy, and review only the entries attributable to that rollout before reversing unsupported changes; clearing the entire suppression list would also restore genuinely invalid recipients. Preserve observation while the writer is paused so the team can reconcile the backlog after repair. Fix the sender or template configuration before permitting another send, and never replay expired reset links as part of recovery.
The release gate is concrete: a configuration mistake must not mutate recipient eligibility, and a valid invalid-recipient observation must still produce a traceable suppression decision. Verify both directions. Otherwise the team has tested error containment while leaving account recovery untested.
References
- DKIM domain-signing specification, RFC 6376: https://datatracker.ietf.org/doc/html/rfc6376
- Amazon SES developer guide: https://docs.aws.amazon.com/ses/latest/dg/Welcome.html
- Postmark developer documentation: https://postmarkapp.com/developer
- Twilio SendGrid email documentation: https://www.twilio.com/docs/sendgrid
Top comments (0)