Skip to content

fix(outbox): don't retry permanent SMTP rejections - #37

Open
heraldb wants to merge 1 commit into
naust-mail:mainfrom
heraldb:fix/smtp-permanent-rejection-no-retry
Open

heraldb wants to merge 1 commit into
naust-mail:mainfrom
heraldb:fix/smtp-permanent-rejection-no-retry

Conversation

@heraldb

@heraldb heraldb commented Oct 29, 2026

Copy link
Copy Markdown

Problem

When the SMTP server permanently rejects a message (a 5xx reply), the outbox
worker treats it as a transient failure. The message stays in the Outbox for
5 attempts with exponential backoff (30 s … 4 min) before it is finally
marked failed, although every retry gets the same rejection.

Seen in practice with a Postfix submission policy rejecting the client:

permanent error (554): 5.7.1 <host[127.0.0.1]>: Client host rejected: Access denied

lettre already classifies this reply as permanent, but SmtpClient mapped it
to the generic SmtpError::SendFailed, which is_retryable treats as
retryable.

Change

  • SmtpError::Rejected(String): new variant for permanent (5xx) replies,
    detected with lettre's Error::is_permanent(). Authentication failures are
    still classified first as AuthenticationFailed.
  • is_retryable excludes Rejected, so the outbox entry is marked failed
    right away, with the server's reason as fail_reason.
  • Transient (4xx), timeout and connection errors are retried as before.

Tests

  • Unit tests for is_retryable: transient errors are retryable; permanent
    rejections and auth failures are not.
  • SmtpError display test extended with Rejected.
  • cargo test: all pass.

Note: CI clippy currently fails on main with Rust 1.99 for unrelated
reasons; that's fixed separately in fix/clippy-rust-1.99.

🤖 Generated with Claude Code

A 5xx reply (e.g. "554 5.7.1 Client host rejected") was mapped to the
generic SendFailed error, which the outbox worker treats as retryable, so
the message sat in the Outbox for five attempts with exponential backoff
before being marked failed.

Map permanent SMTP errors (lettre's is_permanent) to a new
SmtpError::Rejected and exclude it in is_retryable, so the entry is marked
failed immediately with the server's reason. Transient (4xx) and
connection errors are still retried.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant