Send the read receipt the sender asked for #21

Closed
opened 2026-08-25 05:53:19 +00:00 by jcoffey-dev · 0 comments
Owner

JMAP has an extension for read receipts — RFC 9007's MDN/send — and Stalwart does not implement it: urn:ietf:params:jmap:mdn is absent from its capability list. So ihasmail assembles the RFC 8098 multipart/report itself and sends it the long way round — raw MIME uploaded as a blob, Email/import, then EmailSubmission. That is also why the receipt lands in Sent, where it honestly belongs.

ihasmail already requested receipts and detected them, and said "not sent automatically". Only the sending was missing.

The refusals are the feature

A receipt tells whoever asked that the address is live and when the message was read, to an address the sender chose. So mdnDecision refuses:

  • anything marked Auto-Submitted other than no (RFC 3834 — otherwise two servers answer each other forever)
  • anything with Precedence: bulk/list/junk or a List-Id, where a receipt only confirms the address is live
  • anything already acknowledged, or that never arrived (a draft)

A receipt aimed somewhere other than the sender is still offered, but the banner says so first — that redirect is the forgery vector.

There is deliberately no "always send" setting, only ask or never.

Not remembered locally

Sending sets RFC 3503's $mdnsent keyword on the original, which Stalwart supports as a first-class keyword. A second look — or another client entirely — knows not to ask again.

Details worth a second pair of eyes

  • Non-ASCII parts go base64 rather than 8bit, so nothing rests on 8BITMIME surviving every hop; the subject is RFC 2047 encoded.
  • The receipt answers as whichever identity the message was addressed to, not a default persona.
  • A submission that fails destroys the imported copy, so an unsent receipt is not left sitting in Sent looking like it went.

Testing

  • 25 new unit tests over the policy, the MIME, the encoding and the round-trip (181 web + 88 server total, all passing); typecheck and build clean.
  • Exercised end to end over HTTP against the mock: blob uploads as message/rfc822, receipt imports, submits, and the original reads back marked $mdnsent.
  • Not yet exercised against the live server.

🤖 Generated with Claude Code

Merged 2026-08-24 as coffey-labs/ihasmail@05c9bd46c2

Rebuilt from: git history, session transcript.

JMAP has an extension for read receipts — [RFC 9007](https://www.rfc-editor.org/rfc/rfc9007.html)'s `MDN/send` — and **Stalwart does not implement it**: `urn:ietf:params:jmap:mdn` is absent from its capability list. So ihasmail assembles the [RFC 8098](https://www.rfc-editor.org/rfc/rfc8098.html) `multipart/report` itself and sends it the long way round — raw MIME uploaded as a blob, `Email/import`, then `EmailSubmission`. That is also why the receipt lands in Sent, where it honestly belongs. ihasmail already requested receipts and detected them, and said "not sent automatically". Only the sending was missing. ## The refusals are the feature A receipt tells whoever asked that the address is live and when the message was read, to an address **the sender chose**. So `mdnDecision` refuses: - anything marked `Auto-Submitted` other than `no` (RFC 3834 — otherwise two servers answer each other forever) - anything with `Precedence: bulk/list/junk` or a `List-Id`, where a receipt only confirms the address is live - anything already acknowledged, or that never arrived (a draft) A receipt aimed somewhere other than the sender is still offered, but the banner says so first — that redirect is the forgery vector. There is deliberately **no "always send"** setting, only *ask* or *never*. ## Not remembered locally Sending sets RFC 3503's `$mdnsent` keyword on the original, which Stalwart supports as a first-class keyword. A second look — or another client entirely — knows not to ask again. ## Details worth a second pair of eyes - Non-ASCII parts go **base64 rather than `8bit`**, so nothing rests on 8BITMIME surviving every hop; the subject is RFC 2047 encoded. - The receipt answers as whichever identity the message was addressed to, not a default persona. - A submission that fails destroys the imported copy, so an unsent receipt is not left sitting in Sent looking like it went. ## Testing - **25 new unit tests** over the policy, the MIME, the encoding and the round-trip (181 web + 88 server total, all passing); typecheck and build clean. - Exercised end to end over HTTP against the mock: blob uploads as `message/rfc822`, receipt imports, submits, and the original reads back marked `$mdnsent`. - **Not yet exercised against the live server.** 🤖 Generated with [Claude Code](https://claude.com/claude-code) **Merged** 2026-08-24 as coffey-labs/ihasmail@05c9bd46c234 <sub>Rebuilt from: git history, session transcript.</sub>
This repo is archived. You cannot comment on issues.