Uh oh!
There was an error while loading. Please reload this page.
Fix double interception on a queue worker's re-send - #670
Conversation
…seam Review findings (GLM-5.2 on this PR, doc added under dev_docs/pull_requests): - intercept_before_send/2 ran on every call, including the queue worker's re-send, so a provider whose interception is not idempotent would log every queued message twice. Interception is not required to be idempotent, so core no longer relies on it: the worker passes already_intercepted: true and the hook is skipped. skip_queue keeps its narrower meaning (do not offer this message to the queue) so a caller that wants a synchronous send still gets tracking. - A provider returning something outside the callback contract silently fell through to a live send; it still sends, but now says so in the log. - Added tests for the seam: a queuing provider double proves the offer short-circuits the send, that skip_queue sends without offering, and that already_intercepted skips interception while still closing out the after-send hook. Added tests for the new SMTP validation-error translations. The seam tests are :integration (they need the DataCase repo) and were not run here — this environment has no PostgreSQL for the core suite. The validator and transport tests were run: 29 and 28, no failures.
timujinne
commented
Jul 28, 2026
External code review — GLM-5.2 (effort: max, read-only)Run against this branch on 2026-07-29; the reviewer had the full PR diff and the working tree, no shell. Verdict and findings below are unedited. Every finding is fixed in the follow-up commit What changed in response:
I've completed my verification. Here is my review. VERDICT: APPROVEThe SMTP rewrite is faithful (verified option-by-option against the pre-PR logic), the fail-closed Major (latent) — queued messages are intercepted twice; |
ddon
commented
Jul 28, 2026
@timujinne there is a conflict! |
…-once # Conflicts: # lib/phoenix_kit/email/provider.ex
Impersonation (#672) documented "every attempt written to the activity feed before the session changes" and delivered neither half: the row was written only on success, only after the token had been minted, and alongside a second `session.account_added` row indistinguishable from a user adding an account of their own. Refusals — an Admin reaching for the Owner's account, an Admin reaching sideways, a non-staff user hitting the endpoint — left no trace at all, which is the entry a security review actually goes looking for. Refusals now write `session.impersonation_refused` with the deciding rule, carrying no target_uuid so they stay a feed entry rather than a message to the account they named; successes write one row, and it says impersonated. The controller resolved the uuid before checking authority, so its deliberately precise operator copy told every signed-in user which uuids were live accounts. `may_impersonate?/1` settles authority first and shares `staff?/1` with `authorize_impersonation/2` so the rules cannot drift. Both session actions reach the target's inbox via target_uuid but were claimed by no notification type and had no Render clause, so the customer's bell read "Session impersonated" and could not be switched off. Claimed by `security`, given recipient-facing copy. `already_intercepted` (#670) was independent of `skip_queue`, so a worker that set one and forgot the other handed its own dequeued job back to the queue. It can only mean "a worker is re-sending", so it now implies skip_queue. A generated username (#671) is put after `validate_username/2` has already run, so it reached the database with no uniqueness guard; two registrations racing on the same local part raised `Ecto.ConstraintError` out of `Repo.insert/1`. The constraint is re-attached to the generation branch. Reviews in dev_docs/pull_requests/2026/{670,671,672}-*/CLAUDE_REVIEW.md. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-up to #668, which merged before this review fix landed on the branch.
Double interception on a queue worker's re-send
intercept_and_offer_queue/2ranintercept_before_send/2on every call — including the call a queue worker makes to send what it dequeued. A provider whose interception is not idempotent (the obvious implementation: insert a log row) therefore logged every queued message twice, and only one of the two rows ever received the after-send update.Interception is not required to be idempotent, so core should not rely on it. The worker now says so explicitly:
skip_queuekeeps its narrower meaning — "do not offer this message to the queue" — so a caller that wants a synchronous send (a test-email button, for instance) still gets tracking.already_interceptedis what suppresses the second interception; the after-send hook still runs, which is what closes out the row the message already carries.Also here
maybe_enqueue/2outside the callback contract still sends the message — a provider bug must not eat mail — but is now logged instead of silently falling through.skip_queuesends without offering, and thatalready_interceptedskips interception while still running the after-send hook.:invalid_ca_cert,{:invalid_timeout, _}, and thesecurity/verify_cert/authguard clause) — thatcasesits outsideProbe.runand its rescue, so an unmatched reason would raiseCaseClauseErrorout of a LiveView callback.dev_docs/pull_requests/2026/668-smtp-tls-options-and-queue-hook/.Verification
mix test test/phoenix_kit/integrations/validators_test.exs— 29 tests, 0 failures.mix test test/phoenix_kit/mailer/smtp_transport_test.exs— 28 tests, 0 failures.The three new seam tests in
mailer_test.exsare:integration(they need theDataCaserepo) and were not executed here — this environment has no PostgreSQL for the core suite. They compile and load; please run them with the suite.Exercised end to end in a host application against the matching
phoenix_kit_emailsbranch (BeamLabEU/phoenix_kit_emails#22): a queued message produces exactly one log row and one Oban job, the job completes on the first attempt, and the same row ends atsent.