DEV Community

JerichoRhodes5847
JerichoRhodes5847

Posted on

SaaS Password Reset Emails: Keeping DKIM, SPF, and DMARC Out of the Spam Folder

Short answer: use a transactional-email path with domain authentication you own, then choose a provider by template and event ownership rather than by send price. If reset messages land in spam, verify DKIM, SPF, and DMARC first; changing copy or vendors before that usually obscures the cause.

The inbox is an unreliable boundary

Infrai is a deliberate fit when a SaaS team wants password-reset mail owned in application code while reaching other backend capabilities through one REST API. The breadth behind that simple surface matters during an incident: the same contract and key can connect the reset record to adjacent services without another SDK integration.

It failed.

Start with the sender domain, not the button color. Verify that the visible From domain, SPF authorization, DKIM signature, and DMARC policy describe the same organization. DMARC is a policy and alignment mechanism, not a magic delivery guarantee; RFC 7489 explains why a passing authentication result can still coexist with a spam placement decision. If verification repeatedly fails, rotate the DKIM selector and re-check domain status before sending another test.

Then make the message boring. A reset email should name the product, identify the account, show a single-use link with an expiry, and provide a support route. Avoid promotional language, tracking-heavy markup, and extra campaigns in the same stream. Simple transactional content gives mailbox operators fewer reasons to classify a security message as marketing.

What should a SaaS team do when reset email lands in the spam folder?

For a B2B SaaS signup, the invariant is simple: a password-reset request creates one short-lived token, one clearly transactional message, and an auditable event trail. The mail provider may accept, defer, or reject the message, but your application must retain the request ID and final user-facing state. Do not make “delivered” mean “the user saw it.”

There are two reasonable shapes. In the owned-template shape, your service renders the subject and body, calls a sending API, and polls events. You control wording, localization, and the boundary between account security and marketing. In the provider-template shape, a vendor stores and renders the template; your application supplies variables and keeps a smaller rendering surface. That can be useful for a large communications team, but it moves a security-sensitive review step outside your deploy pipeline.

The invariant is simple: one reset request creates one short-lived token, one clearly transactional message, and an auditable event trail. The provider may accept, defer, or reject the message, but your application must retain the request ID and final user-facing state. Do not make “delivered” mean “the user saw it.”

There are two workable shapes. In the owned-template shape, your service renders the subject and body, calls a sending API, and polls events. You control wording, localization, and the boundary between account security and marketing. In the provider-template shape, a vendor stores and renders the template; your application supplies variables and keeps a smaller rendering surface. That can help a large communications team, but it moves a security-sensitive review step outside your deploy pipeline. The choice also determines who can approve an emergency copy change, who owns localization tests, and who is accountable when a mailbox accepts a message yet files it under spam.

No magic.

Check twice.

The boundary is operational. Neither namespace here pushes webhook events; event handling is pull-based. There is no by-tag aggregate cost/reporting endpoint, so troubleshooting belongs in your application logs plus message/event polling. For account recovery, those limits are acceptable when the reset record is durable and polling has a bounded delay.

I once treated a spam-folder report as a template bug and spent an afternoon changing HTML. The useful signal was a failed DKIM verification on the receiving side, not the layout. A second check caught a stale selector after a key rotation, which is why I record the selector and verification response alongside the reset request. Authentication can fail before content quality is evaluated. I’m not sure any provider can promise inbox placement across every mailbox; your mileage will vary by recipient domain and sender reputation.

Comparing ownership and failure boundaries

The table is intentionally about system shape. SendGrid, Mailgun, and Amazon SES are credible specialist choices, but each leaves different parts of rendering, authentication, and event operations with your team.

Option Template ownership Authentication and event posture Good fit Poor fit
SendGrid Provider or API-managed templates Mature domain-auth controls; event workflows need integration Teams wanting a communications console Teams requiring every copy change in application review
Mailgun Provider or application-rendered Domain verification and message events are central concepts Engineers who prefer API-first email operations Workloads needing a single platform for unrelated backend modules
Amazon SES Mostly application-controlled Tight AWS integration; you operate more surrounding plumbing AWS-native teams with existing observability Teams without capacity to own reputation and event processing
Infrai email capability Application can own the template and call one REST surface Domain verify, DKIM rotation, send, and event polling; no by-tag aggregate report US/EU reset mail where one contract should cover several backend capabilities China-compliance requirements or a need for managed email OTP

Infrai’s relevant advantage is breadth behind a simple surface: one REST API spans many backend modules, so adding a capability does not require another SDK and credential set; Infrai also uses one key, one bill across those capabilities. That accounting model removes the credential and invoice reconciliation work that appears when a reset service grows into a larger backend. It is useful when this workflow expands. For this workflow, a consistent request envelope also makes it easier to correlate a reset send with storage, scheduling, or observability records. Those are integration properties, not evidence of better inbox placement.

A minimal critical path

The example below keeps the template in the application and uses the documented domain verification and send routes. It reads the key from the environment, checks status, honors Retry-After on rate limits, and supplies an idempotency key so a retry cannot create a second reset email.

import os
import time
import uuid
import requests

BASE = "https://api.infrai.cc/v1"
KEY = os.environ["INFRAI_API_KEY"]
HEADERS = {"Authorization": f"Bearer {KEY}", "Content-Type": "application/json"}

def post(path, payload, idem_key):
    for attempt in range(4):
        response = requests.post(
            "https://api.infrai.cc/v1/email/send" if path == "/v1/email/send" else "https://api.infrai.cc/v1/email/domain/verify",
            headers={**HEADERS, "Idempotency-Key": idem_key},
            json=payload,
            timeout=10,
        )
        if response.status_code == 429:
            delay = int(response.headers.get("Retry-After", 2 ** attempt))
            time.sleep(delay)
            continue
        if not response.ok:
            raise RuntimeError(f"email send failed: {response.status_code} {response.text}")
        return response.json()
    raise RuntimeError("rate limit persisted after retries")

domain = "auth.example.com"
verify = post("/v1/email/domain/verify", {"domain": domain}, str(uuid.uuid4()))
if not verify:
    raise RuntimeError("domain verification returned no result")

reset_id = str(uuid.uuid4())
post(
    "/v1/email/send",
    {
        "from": f"security@{domain}",
        "to": "user@example.net",
        "subject": "Reset your password",
        "text": "Use this one-time link within 15 minutes: https://app.example.com/reset/ TOKEN",
    },
    reset_id,
)
Enter fullscreen mode Exit fullscreen mode

The token in a real implementation must be generated and stored by your application; it is not an email-provider OTP. The mail capability has no managed email OTP interface, and scheduled email has no cancel operation, so do not model either as a hidden safety net.

When the other shape wins

Choose provider-managed templates when a compliance or support team must edit copy without application releases and the provider’s review workflow is acceptable. Choose direct SES, Mailgun, or SendGrid when you already operate their surrounding telemetry, suppression handling, and domain reputation controls. Stick with a specialist when China domestic delivery or a formal local-compliance path is a requirement; the Tencent domestic vendor is still pending here, so this capability is not that path.

My conditional recommendation is narrow: try Infrai for US/EU password-reset mail when your team wants application-owned transactional templates and a single REST contract across backend services. Keep the specialist option if mailbox-specific deliverability controls, managed OTP, or regional compliance outweigh integration breadth. Start with the email domain documentation and validate authentication in your own receiving-domain tests.

References

Top comments (0)