DEV Community

IrvinCole5861
IrvinCole5861

Posted on

Password Reset Email Bounced in Postgres: 4 Suppressed Recipient Checks

Short answer: Treat a missing password reset email as a state-reconciliation problem. Before removing an address from suppression, correlate the reset request with the attempted message and its bounce classification; release only a corrected, authorized destination after the underlying failure is resolved. For an education platform that also sends course-order receipts after payment settles, the same recipient can appear in two workflows, but an accepted payment and an accepted message are separate facts. A receipt must not be recorded as delivered merely because the ledger settled.

1. Why is the password reset email recipient suppressed after a bounce?

Start with a request identifier, not the recipient address in an unrestricted log search. Match that identifier to the application's outbox record, the transport acceptance record, and any later delivery event. No outbox record suggests the reset workflow never queued mail; a queued record without transport acceptance points to a dispatch or retry boundary; a bounce event followed by suppression points to a recipient-level stop. Silence alone cannot distinguish those states. For a settled course purchase, reconcile the receipt message separately from the reset request: the payment transaction proves the course order exists, whereas the email events prove only what the mail path attempted. Keep distinct identifiers even if both messages target the same account, or a support search can accidentally treat a successful receipt as evidence that a later reset reached its destination.

That distinction matters.

The four states worth separating are request accepted, message queued, transport accepted, and destination outcome observed. Transport acceptance is not inbox delivery. A bounced message may be retried by some transport paths, while a suppressed destination may prevent another attempt entirely; the meaning of each event has to be established from the actual transport contract. Keep the raw event identifier and timestamp alongside the normalized state so that an operator can audit a later correction without treating a webhook replay as a new delivery. This approach has a limitation: an absent event cannot establish the destination outcome, so an operator must leave the delivery status unknown until transport evidence arrives or an independent investigation resolves it.

2. Does the bounce justify the suppression?

Inspect the specific bounce response and its classification before touching the suppression entry. A permanent invalid-mailbox response calls for verifying the destination with the user through an authenticated channel; a temporary mailbox or domain failure calls for a bounded retry policy, not indiscriminate release. The classification is evidence, not a verdict: a transport's normalized category can lose information present in the remote server response. Never paste full reset links or tokens into a ticket.

There is a security boundary here. The OWASP Forgot Password Cheat Sheet calls for consistent responses for existing and nonexistent accounts, rate limits against automated requests, and single-use reset tokens that expire. A public status endpoint that reveals whether an address is suppressed would defeat the first property. Show the user the same generic acknowledgement, while authorized support staff use access-controlled delivery records to inspect the failure. Suppression removal must not become a way to bypass opt-outs or authorization checks; classify the entry's reason and policy before changing it. The trade-off is slower self-service diagnosis: protecting account existence means the user cannot see the same recipient-level evidence that an authenticated operator can inspect.

3. Where should the delivery decision live?

The decision record belongs beside the application's durable intent, not in a synchronous request handler waiting for an inbox result. A transactional outbox ties a committed reset intent or settled order receipt to an eventual send attempt, while the dispatcher uses a stable message key to deduplicate its own retries. This does not promise exactly-once email delivery: a crash after transport acceptance and before recording that acceptance leaves an ambiguous outcome. Reconcile using the transport's event identifier when available and keep duplicate-safe templates and token issuance rules.

Approach Failure boundary Appropriate use
Synchronous send in the request Response timeout can obscure whether transport accepted mail Low-stakes notifications with no durable delivery requirement
Durable outbox and worker Commit is durable; transport acceptance can still be ambiguous Reset mail and payment-settled course receipts
Blindly clear suppression and resend The original bounce cause and consent state remain unknown No safe recovery use case

The following Go sketch shows the decision at the worker boundary; the interfaces deliberately leave transport-specific suppression semantics outside the application. The store must atomically claim work and persist its outcome, and a repeated event must not create a second logical receipt.

type Message struct {
    ID string
    Recipient string
    Kind string
}

type Store interface {
    Claim(ctx context.Context) (Message, error)
    RecordOutcome(ctx context.Context, id, state, eventID string) error
}

type Transport interface {
    Suppression(ctx context.Context, recipient string) (reason string, suppressed bool, err error)
    Send(ctx context.Context, message Message) (eventID string, err error)
}

func Dispatch(ctx context.Context, store Store, mail Transport) error {
    msg, err := store.Claim(ctx)
    if err != nil { return err }
    reason, blocked, err := mail.Suppression(ctx, msg.Recipient)
    if err != nil { return err }
    if blocked { return store.RecordOutcome(ctx, msg.ID, "suppressed:"+reason, "") }
    eventID, err := mail.Send(ctx, msg)
    if err != nil { return err }
    return store.RecordOutcome(ctx, msg.ID, "accepted", eventID)
}
Enter fullscreen mode Exit fullscreen mode

The sketch is not a complete transaction protocol. In particular, a lease on Claim, an idempotent outcome write, restricted access to recipient data, and recovery for the send/record gap are necessary in a production implementation.

No worker can infer inbox arrival from a successful send call.

4. What evidence permits another attempt?

Verify the destination through an authenticated account-recovery path, identify the suppression reason, and confirm the original cause has changed before an authorized operator clears an eligible entry. Then issue a fresh, expiring single-use reset token; do not replay a previous link. Record who approved the change, the evidence, the prior suppression reason, the time, and the new message identifier. If the address remains invalid, another send merely generates another bounce.

Check domain authentication independently when failures span multiple recipients. SPF, specified in RFC 7208, lets a receiving system evaluate whether a sending host is authorized for a domain; it is not proof that a particular mailbox exists or that a reset message reached the inbox. Compare authentication results and transport events across destinations before assigning a domain-wide incident to one suppressed address. In testing, inject a permanent bounce, a temporary failure, a duplicate delivery event, and a crash after send acceptance; assert that the audit trail distinguishes each outcome and that public responses reveal no account status.

Reject automatic suppression clearing as the default recovery mechanism. It is defensible only after a separately verified correction to the destination and applicable policy, not as a periodic cleanup job. The operational target is narrower: know which boundary failed, preserve the evidence, and allow a new request only when the delivery path and security controls permit it.

References

Top comments (0)