Skip to content

One body, one format: body_format replaces body_html on the compose tools - #22

Merged
rutgerhofste merged 2 commits into
mainfrom
claude/optimistic-gauss-1gz443
Sep 16, 2026
Merged

rutgerhofste merged 2 commits into
mainfrom
claude/optimistic-gauss-1gz443

Conversation

@rutgerhofste

Copy link
Copy Markdown
Member

What

mail_send, mail_create_draft and mail_edit_draft now take one body plus
body_format = text (the default) | html, in place of the old body +
body_html pair.

mail_send(to=..., subject=..., body="<p>Hoi Iris,</p>", body_format="html")

Why

Choosing HTML meant writing the same message twice, and the second copy is the
one that rots: an edit to the HTML leaves yesterday's sentence in the
plain-text half, where nobody proofreading the mail will see it. Two arguments
for one decision.

How

  • tools/_common.as_bodies(body, body_format) splits the one body into the
    (text, html) pair MailProvider still takes. The wire is unchanged --
    multipart/alternative, text first. The provider-level body_html is
    untouched, so the admin package's Graph backend needs no change.
  • html_text.html_to_text derives the plain-text half: blocks to line breaks,
    list items to dashes, entities to characters. That is the trade being made,
    and it is named in CLAUDE.md -- worse than an alternative written by hand,
    better than one that disagrees with the HTML. A caller who wants to write it
    themselves still can, one level down in the provider.
  • The same function now serves the read path. It used to be a private copy
    in imap.py that collapsed every message to a single line and left &amp;
    sitting in the text.
  • An unrecognised format is refused, not guessed: markdown would otherwise
    leave as literal asterisks.
  • Inline images now read body_format="html"; the multipart/related
    placement in mime._add_parts is unchanged.

Breaking

body_html is gone from the MCP tool surface (it remains on MailProvider).
The admin package registers this same tool layer, so the two ship together via
SQUIRREL_MCP_REF.

Tests

tests/test_body_format.py pins the split, the flattening and the refusal;
tests/test_attachments_inline.py updated to the new argument. Lint, types and
the 295-test unit suite are green locally; the GreenMail e2e only touches the
provider-level API, which is unchanged.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Q8d2HEM2snepz9KrWXTF77


Generated by Claude Code

…ools

Choosing HTML meant passing body AND body_html -- the same message written
twice, where the second copy is the one that rots: an edit to the HTML leaves
yesterday's sentence in the plain-text half, and nobody proofreading the mail
sees it.

The compose tools (mail_send, mail_create_draft, mail_edit_draft) now take one
`body` plus `body_format` = text (the default) | html. tools/_common.as_bodies
splits it into the (text, html) pair MailProvider still takes, so the wire is
unchanged: multipart/alternative, text first. The provider-level body_html is
untouched, which is where a caller wanting a hand-written plain-text
alternative still has one -- the case this deliberately drops one level up.

The derived half is html_text.html_to_text: blocks to line breaks, list items
to dashes, entities to characters. That same function now serves the read path
too. It used to be a private copy in imap.py that collapsed every message to a
single line and left `&amp;` sitting in the text.

An unrecognised format is refused, not guessed -- "markdown" would otherwise
leave as literal asterisks.

tests/test_body_format.py pins the split, the flattening and the refusal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Q8d2HEM2snepz9KrWXTF77
@rutgerhofste
rutgerhofste marked this pull request as ready for review September 16, 2026 09:20
@rutgerhofste
rutgerhofste merged commit 8c62494 into main Sep 16, 2026
3 checks passed
@rutgerhofste
rutgerhofste deleted the claude/optimistic-gauss-1gz443 branch September 16, 2026 09:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants