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 8098multipart/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.
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.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
JMAP has an extension for read receipts — RFC 9007's
MDN/send— and Stalwart does not implement it:urn:ietf:params:jmap:mdnis absent from its capability list. So ihasmail assembles the RFC 8098multipart/reportitself and sends it the long way round — raw MIME uploaded as a blob,Email/import, thenEmailSubmission. 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
mdnDecisionrefuses:Auto-Submittedother thanno(RFC 3834 — otherwise two servers answer each other forever)Precedence: bulk/list/junkor aList-Id, where a receipt only confirms the address is liveA 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
$mdnsentkeyword 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
8bit, so nothing rests on 8BITMIME surviving every hop; the subject is RFC 2047 encoded.Testing
message/rfc822, receipt imports, submits, and the original reads back marked$mdnsent.🤖 Generated with Claude Code
Merged 2026-08-24 as coffey-labs/ihasmail@05c9bd46c2
Rebuilt from: git history, session transcript.