Never send a message twice after a lost reply #42

Merged
jcoffey-dev merged 1 commits from fix/send-no-duplicates into main 2026-10-05 19:04:00 +00:00
Owner

When a send's request went out and no answer came back, the composer said
"Send failed" and offered the draft back. If the server had in fact
accepted it and only the reply was lost, sending the draft again
delivered the message twice. Nothing identified the first attempt, so
nothing could check.

Each send now carries its own Message-ID (on the sending identity's
domain, RFC 5322 §3.6.4), kept on the draft if it comes back. A failure
that may have happened after the server acted (no answer, a timeout, a
5xx from the proxy) is followed by asking the server what it did:

  • a message with that Message-ID was submitted (EmailSubmission/query by
    emailIds, RFC 8621 §7.3): it was sent, and the composer says so;
  • one exists with no submission: it was created and never sent; it is
    removed from Sent and the failure stands, so resending is safe;
  • none exists: the failure stands;
  • the server can't be asked either: the composer says it couldn't confirm
    and to check Sent, instead of a plain "Send failed".

A refusal (4xx) means the server did not run the request, so it is not
checked. Sending a draft again that came back from a failure asks first,
and sends nothing if a message with its Message-ID already exists;
submissions are expunged after the server's hold period, so later on any
surviving copy counts as sent (every failure path removes the copy it
made).

New strings: 3, in all ten catalogs (1699 -> 1702, no new fallbacks).

Tests: 11 new (send-no-duplicates.test.ts) cover each failure mode against a scripted server: lost reply, never arrived, orphan, gateway error, refusal, unreachable after, and resends. Full suite 1503 passed; typecheck clean; i18n literals clean, catalog check unchanged from main.

When a send's request went out and no answer came back, the composer said "Send failed" and offered the draft back. If the server had in fact accepted it and only the reply was lost, sending the draft again delivered the message twice. Nothing identified the first attempt, so nothing could check. Each send now carries its own Message-ID (on the sending identity's domain, RFC 5322 §3.6.4), kept on the draft if it comes back. A failure that may have happened after the server acted (no answer, a timeout, a 5xx from the proxy) is followed by asking the server what it did: - a message with that Message-ID was submitted (EmailSubmission/query by emailIds, RFC 8621 §7.3): it was sent, and the composer says so; - one exists with no submission: it was created and never sent; it is removed from Sent and the failure stands, so resending is safe; - none exists: the failure stands; - the server can't be asked either: the composer says it couldn't confirm and to check Sent, instead of a plain "Send failed". A refusal (4xx) means the server did not run the request, so it is not checked. Sending a draft again that came back from a failure asks first, and sends nothing if a message with its Message-ID already exists; submissions are expunged after the server's hold period, so later on any surviving copy counts as sent (every failure path removes the copy it made). New strings: 3, in all ten catalogs (1699 -> 1702, no new fallbacks). Tests: 11 new (send-no-duplicates.test.ts) cover each failure mode against a scripted server: lost reply, never arrived, orphan, gateway error, refusal, unreachable after, and resends. Full suite 1503 passed; typecheck clean; i18n literals clean, catalog check unchanged from main.
jcoffey-dev added 1 commit 2026-10-05 18:44:07 +00:00
Never send a message twice after a lost reply
ci / node (pull_request) Skipped
ci / version (pull_request) Skipped
ci / docker-build (pull_request) Skipped
ci / publish (pull_request) Skipped
github/ci (branch) GitHub Actions
ci / github (pull_request) Successful in 1m44s
ci / announce (pull_request) Skipped
152311b035
When a send's request went out and no answer came back, the composer said
"Send failed" and offered the draft back. If the server had in fact
accepted it and only the reply was lost, sending the draft again
delivered the message twice. Nothing identified the first attempt, so
nothing could check.

Each send now carries its own Message-ID (on the sending identity's
domain, RFC 5322 §3.6.4), kept on the draft if it comes back. A failure
that may have happened after the server acted (no answer, a timeout, a
5xx from the proxy) is followed by asking the server what it did:

- a message with that Message-ID was submitted (EmailSubmission/query by
  emailIds, RFC 8621 §7.3): it was sent, and the composer says so;
- one exists with no submission: it was created and never sent; it is
  removed from Sent and the failure stands, so resending is safe;
- none exists: the failure stands;
- the server can't be asked either: the composer says it couldn't confirm
  and to check Sent, instead of a plain "Send failed".

A refusal (4xx) means the server did not run the request, so it is not
checked. Sending a draft again that came back from a failure asks first,
and sends nothing if a message with its Message-ID already exists;
submissions are expunged after the server's hold period, so later on any
surviving copy counts as sent (every failure path removes the copy it
made).

New strings: 3, in all ten catalogs (1699 -> 1702, no new fallbacks).
jcoffey-dev merged commit 38dcbd9e0c into main 2026-10-05 19:04:00 +00:00
jcoffey-dev deleted branch fix/send-no-duplicates 2026-10-05 19:04:00 +00:00
Sign in to join this conversation.