send_email cannot send HTML, so a styled briefing has no route #29

Closed
opened 2026-08-29 14:13:01 +00:00 by jlxq0 · 0 comments
Owner

send_email builds a text-only email and its schema has no way to say otherwise, so an agent composing a styled briefing has no route. src/mcp.rs:1374:

"bodyValues": { "b": { "value": params.body_text, "isTruncated": false } },
"textBody": [ { "partId": "b", "type": "text/plain" } ]

SendEmailParams at src/mcp.rs:883 takes from, to, cc, bcc, subject, body_text, in_reply_to. The two variants beside it, _with_attachments and _with_url_attachment, are about files rather than markup.

JMAP carries htmlBody and multiple bodyValues natively, so this surfaces something the protocol already has rather than adding a mechanism.

What to build

An optional body_html on SendEmailParams, defaulting to None.

With it absent, the request must be byte-identical to today's. That is a regression test rather than a claim.

With it present, both bodies go out, textBody naming the plain part and htmlBody naming the HTML one, distinct partIds:

"bodyValues": { "t": {...}, "h": {...} },
"textBody": [ { "partId": "t", "type": "text/plain" } ],
"htmlBody": [ { "partId": "h", "type": "text/html" } ]

HTML alone is refused. body_html present with an empty body_text returns invalid_params. A briefing that arrives blank on a client that will not render HTML is worse than an unstyled one, and making that structural beats leaving it to whoever composes.

Do not add a new tool

Extend send_email; do not add send_html_email. A new tool name is denied by default, and restoring it costs an allowlist entry and a decision that is Julian's. #260 merged four hours ago after exactly that took three days and three wrong mounts. The existing grant covers send_email whatever its parameters are.

The read_only_hint = false annotation stays, so tool-scope.sh generates the same deny lines and no agent's scope moves.

Not in scope

No sanitiser on the outbound HTML. The body is composed by the calling agent and is not external content, which is the distinction read_email's wrapping already draws. Adding one here would be scope nobody asked for and would corrupt legitimate markup.

Acceptance

cargo fmt --check, cargo clippy -- -D warnings and cargo test on the toolchain pinned in ci.yml, plus three tests each shown to fail against the unfixed code:

  1. omitting body_html produces today's exact object
  2. supplying both produces two bodyValues, a textBody and an htmlBody
  3. HTML with empty text is refused

The real acceptance is a briefing Julian can read on his phone with the styling visible, which is downstream of a release and a deploy and is not this issue.

`send_email` builds a text-only email and its schema has no way to say otherwise, so an agent composing a styled briefing has no route. `src/mcp.rs:1374`: "bodyValues": { "b": { "value": params.body_text, "isTruncated": false } }, "textBody": [ { "partId": "b", "type": "text/plain" } ] `SendEmailParams` at `src/mcp.rs:883` takes `from`, `to`, `cc`, `bcc`, `subject`, `body_text`, `in_reply_to`. The two variants beside it, `_with_attachments` and `_with_url_attachment`, are about files rather than markup. JMAP carries `htmlBody` and multiple `bodyValues` natively, so this surfaces something the protocol already has rather than adding a mechanism. ## What to build An optional `body_html` on `SendEmailParams`, defaulting to `None`. **With it absent, the request must be byte-identical to today's.** That is a regression test rather than a claim. **With it present, both bodies go out**, `textBody` naming the plain part and `htmlBody` naming the HTML one, distinct `partId`s: "bodyValues": { "t": {...}, "h": {...} }, "textBody": [ { "partId": "t", "type": "text/plain" } ], "htmlBody": [ { "partId": "h", "type": "text/html" } ] **HTML alone is refused.** `body_html` present with an empty `body_text` returns `invalid_params`. A briefing that arrives blank on a client that will not render HTML is worse than an unstyled one, and making that structural beats leaving it to whoever composes. ## Do not add a new tool **Extend `send_email`; do not add `send_html_email`.** A new tool name is denied by default, and restoring it costs an allowlist entry and a decision that is Julian's. `#260` merged four hours ago after exactly that took three days and three wrong mounts. The existing grant covers `send_email` whatever its parameters are. The `read_only_hint = false` annotation stays, so `tool-scope.sh` generates the same deny lines and no agent's scope moves. ## Not in scope **No sanitiser on the outbound HTML.** The body is composed by the calling agent and is not external content, which is the distinction `read_email`'s wrapping already draws. Adding one here would be scope nobody asked for and would corrupt legitimate markup. ## Acceptance `cargo fmt --check`, `cargo clippy -- -D warnings` and `cargo test` on the toolchain pinned in `ci.yml`, plus three tests each shown to fail against the unfixed code: 1. omitting `body_html` produces today's exact object 2. supplying both produces two `bodyValues`, a `textBody` and an `htmlBody` 3. HTML with empty text is refused **The real acceptance is a briefing Julian can read on his phone with the styling visible**, which is downstream of a release and a deploy and is not this issue.
jlxq0 closed this issue 2026-08-29 14:27:15 +00:00
Sign in to join this conversation.
No labels
waiting-on-julian
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
jlxq0/jmap-mcp#29
No description provided.