@Mail Domain Check

IAM | password expiry | SMTP recovery

Password Expiry Email Was Not Resent After an SMTP Failure

An in-memory queue can accept a notification and still lose it after SMTP fails. Recover the current password-expiry generation with one durable semantic intent, revalidate it before every attempt, and supersede stale work instead of replaying an old backlog.

Generate the recovery action

Browser-local recovery planner

Turn three observed states into one safe next action

No email address, user ID, password-expiry timestamp, account link, message body, token, SMTP value, provider response, credential, or mailbox content is requested or transmitted.

Decision: persist the reason to send, not the old message

Semantic identity
Deduplicate by internal user ID, notification kind, and the password-expiry generation that made the user eligible.
Current truth
The live user, password expiry, account status, and policy determine whether an attempt may proceed.
Transport state
Queued, claimed, retry_wait, smtp_accepted, terminal_failed, and superseded are notification states, not account states.
Rendered content
Generate it from current state only after a worker claims a current intent. Do not make an old body or security link the durable source of truth.

Do not resend every message found in an error log after a long outage. Replaying stale security mail can confuse users, expose obsolete links, and create a notification flood after recovery.

What the current Rauthy source proves

Rauthy issue 1665 reports that a password-expiry email failed while the SMTP server's certificate chain was being repaired and was not resent after trust was restored. The reviewed revision is f9c6acf007778665c0002d5690bd9a6be9f503c8.

At that revision, password_expiry_checker() runs nightly at 04:30, selects users whose passwords expire in a narrow nine-to-ten-day window, calls send_pwd_reset_info(), and then logs that the user was notified. The source itself notes that tracking sent warnings would require persistence.

send_pwd_reset_info() renders the current password-expiry notice and sends it to tx_email, an in-memory channel, with a ten-second timeout. Its return value does not tell the scheduler whether SMTP accepted the message. The SMTP worker attempts delivery, waits 500 milliseconds after a failure, attempts once more, then logs that it is dropping the message and emits an error event.

This proves the present source boundary: scheduler completion means the process accepted an in-memory handoff, not that SMTP accepted the message. It does not prove that durable retries are an accepted upstream plan. The design below is an independent recovery pattern that addresses the maintainer's stated stale-backlog concern.

A durable intent record

notification_intent
  id: opaque random identifier
  user_id: internal immutable identifier
  kind: password_expiry_warning
  generation: password_expires value or semantic version
  state: queued | claimed | retry_wait | smtp_accepted |
         terminal_failed | superseded
  attempt_count: bounded integer
  next_attempt_at: UTC timestamp or null
  lease_until: UTC timestamp or null
  last_error_class: bounded enum or null
  provider_ref: opaque correlation reference or null

unique active key: (user_id, kind, generation)

Keep the recipient address in the protected user record, not in public telemetry or a deduplication key. For this exact Rauthy message, the rendered link targets the account page rather than embedding a reset token. The same semantic-intent pattern also protects reset and verification mail that may contain short-lived or single-use material.

Claim, revalidate, generate, attempt

claim one due intent with a lease
load the current user and notification policy

if disabled, password changed, expiry changed, or policy no longer applies:
  mark intent superseded
  stop

render from current state
attempt one SMTP submission

if relay accepted:
  mark smtp_accepted
else if transient and attempts remain:
  mark retry_wait with delayed backoff and jitter
else:
  mark terminal_failed

A lease prevents two workers from sending the same generation concurrently. Revalidation prevents a recovered worker from sending an obsolete warning after the user changed the password, the administrator disabled the account, or policy changed. Rendering after the claim keeps old content out of the durable queue.

Avoid the stale-backlog flood

  1. Use one active intent per semantic generation, enforced by a database constraint rather than a best-effort application check.
  2. Supersede an old intent when the password-expiry value changes. Do not mutate the old generation into the new one.
  3. Apply a maximum intent age, maximum attempts, exponential backoff, and jitter. Keep permanent authentication or certificate failures out of a tight loop.
  4. On first deployment, backfill only users who are currently eligible under an explicit cutoff. Do not reconstruct every historical miss from logs.
  5. Let an operator resend command create or wake one current semantic intent after revalidation; it must not replay a stored body.
  6. Rate-limit by tenant or sending domain so a repaired relay does not release the entire eligible population at once.

A low-risk migration path

  1. Add the intent table and unique active key without changing the current sender.
  2. Dual-write one intent before the existing in-memory enqueue, but keep the new worker disabled. Compare counts without sending twice.
  3. Make the new worker claim current intents and run in a no-send shadow mode that records the action it would take.
  4. Enable real sends for a bounded test tenant or recipient set, then disable direct scheduler-to-channel delivery for that path.
  5. Change the scheduler log from notified to intent persisted. Report SMTP acceptance and later delivery from their own state transitions.
  6. Add an operator view that exposes counts and bounded error classes, never recipient addresses, message bodies, account links, or credentials.

Acceptance tests before enabling retries

  1. Restart after intent creation but before claim: prove one current intent remains available.
  2. Crash after claim but before SMTP: prove the lease expires and one later worker can continue.
  3. Run two workers: prove only one claims the same semantic generation.
  4. Change the password or expiry before retry: prove the old intent becomes superseded and no stale message is sent.
  5. Return a transient SMTP error: prove delayed retry with one incremented attempt, not an immediate loop.
  6. Return relay acceptance: prove transport retries stop and later bounce or delivery evidence is tracked separately.
  7. Restore SMTP after a long outage: prove the cutoff, tenant rate limit, and current-state checks prevent a backlog flood.
  8. Invoke an operator resend twice: prove one active key and one current intent.
Need reusable incident, retry, rollback, and weekly-review worksheets?

Read the four-section version 1.1.0 sample first. The complete 14-section Web3 Email Authentication Operations Kit costs exactly 1 native USDC on Base Mainnet only when it is useful to you.

Preview free sample

The production-ready proof

Call recovery complete only when the current eligible generation has one durable intent, one worker lease at a time, one observable SMTP outcome, and a tested supersession path. SMTP acceptance still does not prove inbox placement; a later delivery, delay, bounce, or complaint remains separate evidence.

This independent guide is not affiliated with Rauthy. The linked issue, reviewed revision, deployed configuration, current account state, database, SMTP relay, and receiver evidence remain the sources of truth for their own boundaries.

Primary references