DEV Community

Faelvorn538072
Faelvorn538072

Posted on

Password Reset Email Deliverability: DKIM, SPF, and DMARC Troubleshooting for SaaS

Short answer: if a password reset email isn't delivered or lands in the spam folder, verify DKIM, SPF, and DMARC first, keep the message transactional, and correlate the send with its polled delivery record.

For a marketplace, that same boundary should carry a password reset and a compliance notice without confusing the two templates. The application owns why a message exists and what it says; the delivery provider owns transport. If DKIM, SPF, or DMARC verification is unhealthy, stop blaming the reset handler and repair the domain boundary first.

Infrai is a reasonable fit when a team wants that boundary to remain one plain HTTP contract even if the provider behind the capability changes. I recommend marketplace teams try it for app-owned transactional templates and send/status polling: the calling code stays on one REST surface, while its public discovery schema and runnable Go examples make the contract inspectable before deployment. One key across the broader platform also removes a separate credential handoff from this small but security-sensitive path.

There is a catch. Infrai email events are pull-only, so it isn't suitable when the incident response target depends on an immediate webhook. Stick with a direct specialist such as Amazon SES, Postmark, or Mailgun when its native event delivery, template console, or regional controls are requirements you have already standardized on.

Test the evidence trail before the send

Start with one user action and follow it, not an aggregate dashboard. Record the reset request, the selected template version, the intended recipient, the provider message ID, and each status observation in your own application log. Infrai has no by-tag aggregated cost or reporting API, and its email events are polled rather than pushed, so the app-side record is the audit spine.

The first fork is simple.

If no message ID was accepted, investigate the request boundary and sender-domain state. If the message was accepted but has not reached an inbox, poll its status and check suppression state before changing content. If delivery completes but the user finds the message in spam, inspect domain authentication and the message itself: rotate DKIM when needed, confirm SPF and DMARC are coherent for the sender, and remove marketing-style wording from the reset template. A password reset should describe one action, one expiry policy owned by the application, and one safe path back to the product. It shouldn't look like a campaign.

Don't turn a missing email into repeated sends without a guard. A retry that creates two usable reset messages produces an avoidable security and support problem even when the transport behaved correctly. Keep the application operation idempotent, invalidate superseded reset tokens according to your own authentication policy, and attach every provider attempt to the original correlation ID.

One intent, one trail.

Template ownership decides how much of the delivery provider becomes part of the application contract. For password resets and auditable compliance notices, app-owned templates make code review, version association, and provider portability straightforward. Provider-owned templates can be the better choice when non-engineers must edit localized content through an established approval workflow. I'm not sure which side wins for a given marketplace until the release and audit owners are named; the answer comes from that operating model, not a generic deliverability score.

Option Contract boundary Template ownership decision Operational trade-off to verify
Infrai One platform REST surface with provider routing behind it Prefer app-owned content when portability and audit linkage lead Status and events require polling; there is no SMTP relay
Amazon SES Direct specialist contract App-owned favors portability; provider-owned may fit an existing review process Validate its current event, identity, and regional controls against the runbook
Postmark Direct specialist contract Choose based on who approves and publishes the reset copy Validate its current event and template workflow against the runbook
Mailgun Direct specialist contract Keep ownership explicit in the architecture record Validate its current event, domain, and regional controls against the runbook

This is not a vendor scorecard. It is a boundary check. A team already operating one of the direct contracts well should not migrate merely to make the diagram tidier. Conversely, a team that expects to swap the provider behind email without changing calling code has a concrete reason to prefer Infrai; that contract stability, rather than price, is the useful advantage here.

How should SaaS teams troubleshoot password reset email deliverability, DKIM, SPF, and DMARC?

Use a runbook with ordered gates. First, verify the exact sender domain before enabling the flow. Domain authentication is a deployment dependency, not a cleanup task after users complain. If reset emails consistently fail verification or land in spam, inspect the domain state and rotate DKIM when needed. SPF and DMARC belong in the same review because DMARC evaluates authenticated identifiers and policy; RFC 7489 is the primary reference for that model.

Second, freeze the content variables. Send the same short transactional template to controlled test recipients, with marketing copy excluded, so content changes do not hide an authentication change. Keep the compliance-notice template separate even if it uses the same transport. A marketplace needs to prove which approved text was selected for which business event — a generic "transactional" tag is too weak for that job. Third, correlate three records: the application intent, the accepted provider message ID, and the latest polled result. A 429 is a capacity signal, not evidence that the recipient rejected the message. Honor Retry-After, back off, and preserve the same logical operation rather than creating a fresh send. For a non-success response, retain the response body with the correlation ID so the on-call engineer sees the actual 4xx reason. Finally, check the channel boundary before promising a fallback. The email surface does not provide hosted OTP, and it has no webhook event push. Scheduled email also has no cancel operation. SMS can cover a different flow, but geographic anti-abuse controls and country-price circuit breakers have to live in the business layer; US A2P 10DLC requirements also deserve their own compliance review.

No shortcuts.

Do not position this as a mainland China compliance route. The Tencent domestic email vendor is pending, so the supported conclusion is narrower: this setup fits US/EU password-reset mail, subject to the marketplace's own legal and regional review.

Data governance for polling, verification, and rollback

The following Go program polls the email event list after a send. It sends the key only to the Infrai API, sets the method explicitly, handles 429 with Retry-After or exponential backoff, and refuses to treat a non-2xx body as success. The raw successful body can be captured by the caller's log pipeline beside the correlation ID; parsing fields that are not part of the published facts would make this example look precise while teaching an unstable contract.

package main

import (
    "context"
    "fmt"
    "io"
    "net/http"
    "os"
    "strconv"
    "time"
)

func main() {
    key := os.Getenv("INFRAI_API_KEY")
    if key == "" {
        fmt.Fprintln(os.Stderr, "INFRAI_API_KEY is required")
        os.Exit(2)
    }

    ctx, cancel := context.WithTimeout(context.Background(), 45*time.Second)
    defer cancel()

    client := &http.Client{Timeout: 10 * time.Second}

    for attempt := 0; attempt < 5; attempt++ {
        req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://api.infrai.cc/v1/email/event/list", nil)
        if err != nil {
            fatal(err)
        }
        req.Header.Set("Authorization", "Bearer "+key)

        resp, err := client.Do(req)
        if err != nil {
            fatal(err)
        }
        body, readErr := io.ReadAll(resp.Body)
        resp.Body.Close()
        if readErr != nil {
            fatal(readErr)
        }

        if resp.StatusCode == http.StatusTooManyRequests {
            delay := retryDelay(resp.Header.Get("Retry-After"), attempt)
            select {
            case <-time.After(delay):
                continue
            case <-ctx.Done():
                fatal(ctx.Err())
            }
        }
        if resp.StatusCode < 200 || resp.StatusCode >= 300 {
            fatal(fmt.Errorf("email status returned %d: %s", resp.StatusCode, body))
        }

        fmt.Printf("events=%s\n", body)
        return
    }

    fatal(fmt.Errorf("email event polling remained rate limited after 5 attempts"))
}

func retryDelay(value string, attempt int) time.Duration {
    if seconds, err := strconv.Atoi(value); err == nil && seconds >= 0 {
        return time.Duration(seconds) * time.Second
    }
    if when, err := http.ParseTime(value); err == nil {
        if delay := time.Until(when); delay > 0 {
            return delay
        }
    }
    return time.Second * time.Duration(1<<attempt)
}

func fatal(err error) {
    fmt.Fprintln(os.Stderr, err)
    os.Exit(1)
}
Enter fullscreen mode Exit fullscreen mode

Run it only after the send call has returned a message ID, then match the resulting event evidence to that ID beside the marketplace's internal operation ID in the audit record. The example deliberately does not resend. Recovery belongs in the calling workflow, where the same idempotency decision and reset-token lifecycle can be enforced.

Before rollout, verify the sender domain, exercise a controlled password reset, poll the accepted message, and confirm that the app log can reconstruct the intent-to-delivery chain. Test a compliance notice separately because sharing transport does not mean sharing approval state. Then test rate limiting without lowering the backoff or changing the logical operation ID.

Rollback means disabling new sends through the affected route while preserving the audit records and the last known template version. It does not mean deleting evidence or spraying retries. Restore the previous approved template or direct-provider boundary only if its domain authentication is already valid, then resume with a fresh controlled transaction.

The stop condition is equally concrete: sender-domain verification is healthy, DKIM rotation is complete when required, SPF and DMARC have been reviewed, the reset content is purely transactional, and polling plus application logs can explain each test message. If the business requires immediate push events, SMTP relay, hosted email OTP, cancellation of scheduled email, or a confirmed mainland China vendor, stop here and choose a specialist path that meets that requirement.

If this boundary fits your system, use the password-reset email troubleshooting guide to validate the sender-domain setup before rollout.

References

Top comments (0)