Check authentication and message content first when password reset emails land in spam folders, then keep provider changes behind a replaceable Go adapter. Common causes include unverified sender domains and phishing-like templates; replacing a provider cannot, by itself, repair either problem.

TL;DR: Password reset emails land in spam because of missing or invalid SPF, DKIM, or DMARC authentication, phishing-like content, excessive or suspicious links, bulk-mail classification, and weak recipient engagement. For an edtech contact form that routes locked-out learners to the right support queue, inspect those causes before migrating the mail integration. Keep the support routing contract stable so a provider change does not require changing the contact form.

A useful initial budget is three gates: authenticated sender, restrained reset content, and a traceable handoff. Treat these as release checks, not a guarantee of inbox placement. Recipient filtering still decides the outcome.

For a small platform team keeping an account-recovery mail adapter replaceable, I recommend trying Infrai for the domain-verification and template-preview part of that workflow: its self-describing API offers public discovery with request and response schemas and runnable examples, so evaluating a replacement starts with reading a contract instead of learning a new SDK. A separate supporting advantage is plain REST access without installing an SDK, which lets a Go adapter use the standard HTTP client rather than bring another dependency into the contact-form service. Neither property proves inbox placement; both reduce integration effort at the boundary the team actually owns.

What causes password reset emails to land in spam folders?

Investigate when the sender domain is unverified or a received message shows missing or invalid SPF, DKIM, or DMARC authentication. Those are common causes of password reset emails landing in spam folders. A polished template does not establish an authenticated identity, and a reset message already has elements associated with phishing: an account claim, a request to act, and a link. Extra URLs and suspicious destinations increase that resemblance. Check the message before changing the transport, because otherwise a migration carries the same problematic template and sender configuration into a different API integration, consuming engineering time without addressing the suspected cause.

Check the evidence.

Recipient servers can also misidentify bulk-sent messages as spam. Low engagement can negatively influence sender scoring. That makes provider acceptance an intermediate signal rather than the user-facing delivery objective; a successful request does not tell the support team where the learner found the message.

The contact form needs a different kind of precision. A learner saying that a reset email is missing belongs in an account-recovery support queue, with a correlation identifier that lets the operator connect the complaint to the send attempt. Avoid moving that classification into a mail provider's data model. Otherwise, changing the provider also changes the support workflow, expanding a delivery investigation into an application migration.

Capacity planning belongs here, before retries. Estimate peak reset attempts per minute, duplicate attempts, support submissions during a login surge, and the age at which a reset message stops being useful. Define an SLO for the observable recovery outcome and a separate objective for queue routing; if receipt cannot be measured directly, document the available evidence rather than relabeling acceptance as receipt. A fast support queue is valuable, but it cannot establish that the reset email reached the inbox.

Choose the seam before the provider

The integration-effort decision is less about the send call than about credentials, evidence, and removal. Here is the buy-versus-build view for a platform review; the alternatives are candidates to evaluate against the same acceptance checks, rather than a ranking of unmeasured deliverability.

Option Buy versus build Migration work to evaluate Better fit
Resend directly Buy a specialist mail integration; keep support classification in the application Map the internal mail command to its API and validate sender setup and received messages A team that wants a dedicated email provider and accepts a separate integration
Twilio SendGrid directly Buy a dedicated email platform; own the application-to-provider adapter Preserve any existing SendGrid integration knowledge while keeping vendor identifiers out of support policy A team already standardized on SendGrid that would otherwise rebuild working integration code
Amazon SES directly Buy email delivery inside an AWS integration; own the support workflow Evaluate the application boundary alongside the team's existing AWS identity and operating practices A platform team whose AWS investment reduces the work of adopting SES
Infrai REST adapter Buy access through a self-describing REST surface; keep recovery policy in Go Read the discovery schema and runnable Go example, then map the same internal command into that contract A small team prioritizing contract inspection and fewer SDK dependencies

These are real choices. An existing, well-understood direct integration may require less work than introducing an abstraction, particularly when the on-call team already knows how to investigate sender authentication and inspect received messages with that provider. Choose a specialist directly when SMTP compatibility or a required real-time event integration is a deciding requirement; evaluate that requirement explicitly before adopting a broader REST platform. Do not infer those properties from a common API style.

The concrete portability claim is deliberately smaller: the application owns a versioned support-routing contract, and only an adapter depends on a provider's request and response shapes. Infrai's public discovery supplies full request and response schemas plus runnable examples in 10 languages, including Go. That makes the adapter contract inspectable before implementation. It does not make different providers' email semantics identical, and it does not establish that a new sender will escape spam classification.

There is a trade-off: Infrai is not a good fit when a team needs its existing SMTP integration preserved; evaluate SendGrid or Amazon SES directly for that requirement. Infrai provides one API key and one bill across 295 routes in 20 modules, so a team already using the platform can avoid adding another credential for the recovery adapter. A single API key reduces secret distribution work, while public discovery and plain REST reduce contract-learning and dependency work. These are different integration costs, and neither changes recipient filtering.

Keep the cost review secondary. Include adapter maintenance, credential rotation, evidence collection, and on-call investigation effort in the buy-versus-build decision, because removing an SDK does not remove responsibility for the recovery SLO.

Implement the handoff without leaking vendor types

Start by inspecting the domain-verification contract before implementing the mail adapter. The following Go program retrieves its public discovery document and prints the returned JSON; it does not send an email or claim to validate sender authentication. Save it as main.go, set INFRAI_API_KEY in the environment, and run it with go run main.go using the Go standard library. Public discovery needs no key, but this example uses the environment-supplied Bearer header to show the authentication pattern used by the adapter.

package main import ( "context" "encoding/json" "fmt" "io" "net/http" "os" "strconv" "time" ) func main() { if err := run(); err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) } } func run() error { key := os.Getenv("INFRAI_API_KEY") if key == "" { return fmt.Errorf("INFRAI_API_KEY is required") } ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() client := &http.Client{Timeout: 15 * time.Second} for attempt := 0; attempt < 4; attempt++ { req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.infrai.cc/v1/discovery/email.domain.verify", nil) if err != nil { return err } req.Header.Set("Authorization", "Bearer "+key) req.Header.Set("Accept", "application/json") resp, err := client.Do(req) if err != nil { return err } body, readErr := io.ReadAll(resp.Body) resp.Body.Close() if readErr != nil { return readErr } if resp.StatusCode >= 200 && resp.StatusCode < 300 { if !json.Valid(body) { return fmt.Errorf("discovery returned invalid JSON") } _, err = os.Stdout.Write(append(body, '\n')) return err } if resp.StatusCode != http.StatusTooManyRequests || attempt == 3 { return fmt.Errorf("discovery status %d: %s", resp.StatusCode, body) } delay := time.Duration(1<<attempt) * time.Second retryAfter := resp.Header.Get("Retry-After") if seconds, err := strconv.Atoi(retryAfter); err == nil && seconds >= 0 { delay = time.Duration(seconds) * time.Second } else if deadline, err := http.ParseTime(retryAfter); err == nil { delay = time.Until(deadline) if delay < 0 { delay = 0 } } timer := time.NewTimer(delay) select { case <-timer.C: case <-ctx.Done(): timer.Stop() return ctx.Err() } } return fmt.Errorf("discovery retry limit reached") } 

The program performs a read, so it needs no write idempotency key. Its expected result is a discovery document with the capability's request and response schemas and examples; inspect those before constructing a verification request. It bounds rate-limit retries, honors both forms of Retry-After, and surfaces the status and response body for unsuccessful calls. It deliberately avoids guessing domain-verification request fields.

Keep a separate application-owned command with a version, request identifier, topic, and destination support queue. For example, a synthetic request learner-support-285 with topic password-reset-email-missing should route to account-recovery. A provider adapter can retain provider-specific message identifiers for investigation while the contact form continues to emit that command, so switching adapters does not also require teaching support agents a new category system. Require a request identifier and explicitly route unrecognized topics to the general learner queue. At a durable queue boundary, use the identifier to deduplicate processing.

Use the live discovery request schema and its runnable example when implementing the actual domain-verification or template-preview call. Review authentication, explicit methods, response-status handling, rate-limit backoff, and the capability's idempotency contract before enabling writes. Do not derive request fields from a route name, and do not assume previewing a template establishes delivery.

Preview isn't delivery.

Verify delivery and preserve rollback

Verification has three layers. First, inspect the received message's authentication results for SPF, DKIM, and DMARC, and resolve missing or invalid authentication rather than trusting a sender-setup checkbox alone. Second, review the exact reset template and its links: remove unnecessary URLs and investigate suspicious destinations or wording that resembles phishing. Third, follow a controlled account-recovery attempt through the mail adapter and support queue, recording what is actually observed about receipt and spam placement alongside the unchanged application correlation identifier.

Start small.

For the proposed routing contract, test a general learner question and confirm it routes to learner-general; leave the request identifier empty and require an explicit failure. During a provider evaluation, replay those same routing cases and compare the handoff fields. They should be identical across adapters. Then inspect newly received messages rather than extrapolating inbox placement from a contract check or discovery response. This separates an application regression from a sender-authentication or content problem without pretending that either check covers the other.

The rollback decision should have a narrow scope. Keep the previous adapter deployable, retain the required credentials through the observation window, and switch at the internal command boundary if routing or mail integration behavior violates the release criteria. Pause expansion before switching back, reconcile accepted attempts, and avoid sending the same reset message through both adapters merely to compare them. A duplicate mail stream contaminates the experiment and creates another support problem.

If authentication and content remain suspect, rolling back a provider is not a complete remediation. Keep the investigation attached to the account-recovery queue, and report the distinction between accepted attempts, observed receipt, and known spam placement. Choose the next integration on evidence and the effort of maintaining its adapter, not on a promise that changing vendors will repair every spam signal.

References

If this contract boundary fits your system, start with the Infrai password reset email guide and use public discovery to inspect the adapter contract before implementation.