Skip to content

fix(chatter): let callers post real HTML via message_post(body_is_html=) - #62

Open
glazperle wants to merge 1 commit into
erpipe-org:mainfrom
glazperle:upstream-pr/chatter-body-is-html
Open

fix(chatter): let callers post real HTML via message_post(body_is_html=)#62
glazperle wants to merge 1 commit into
erpipe-org:mainfrom
glazperle:upstream-pr/chatter-body-is-html

Conversation

@glazperle

Copy link
Copy Markdown

Summary

Any markup passed to chatter_post arrives in the chatter as literal tags. Odoo escapes every str body handed to message_post — only a markupsafe.Markup survives, and RPC cannot carry one, so no caller of this tool can currently post a formatted message.

Odoo 17 added message_post(body_is_html=...) for exactly this case ("to be used only for RPC calls", mail/models/mail_thread.py). chatter_post now exposes a body_is_html parameter, forwards it on Odoo 17+, and omits it on 16, which stores bodies verbatim anyway.

User-facing behavior change: a new optional body_is_html parameter (default false). With it unset, behavior is byte-for-byte unchanged; the only addition is an advisory warnings entry when a body looks like markup but the flag is off.

Why opt-in rather than auto-detected

Detection cannot separate markup from prose. Both of these are valid tag syntax:

  • Please forward to <a.schmidt@example.com>
  • if a<b and b>c

Treating either as HTML silently deletes the bracketed text when Odoo renders the field — a lossy failure worse than the escaped-tags one it would fix. So nothing is ever converted: looks_like_html exists only to raise the warning, and its accepted false positives are pinned by a test.

body_is_html is part of the canonical payload, so an approval previewed as plain text cannot be executed as markup.

Coverage

Seven unit tests in tests/test_server.py (forwarding on 17, omission on 16, unchanged plain-text path, warning path, preview warning, token binding, gated round-trip) and a looks_like_html group in tests/test_tool_helpers.py including the deliberately accepted false positives. They use the existing _ChatterClient pattern and need no live Odoo.

Verification

  • Odoo versions: 17.0 end-to-end against a live instance — the stored mail.message.body comes back byte-for-byte as sent, unescaped. 16 is covered by unit test only, since the change there is the absence of the kwarg.
  • Odoo transport: XML-RPC. MCP transport: Streamable HTTP.
  • Exact commands:
uv run python -m ruff check .                  # All checks passed!
uv run python -m mypy src                      # Success: no issues found in 34 source files
uv run python -m pytest                        # 925 passed, 1 deselected
uv run python -m pytest tests/test_server.py   # 219 passed

The deselected test is test_from_path_rejects_symlink_escape_within_upload_root, which needs the Windows symlink privilege and fails on this machine both before and after the change.

Checklist

  • Updated documentation (README.md, docs/troubleshooting.md)
  • Updated CHANGELOG.md under ## Unreleased
  • No credentials or production data in the change
  • Regression test that the default plain-text path gains no new kwarg and rewrites nothing

Relation to #61

No conflict expected: the tools_write.py regions are disjoint (chatter_post here, _execute_approved_write_gated there). Only tests/test_server.py and CHANGELOG.md are shared, both append-only.

Any markup passed to chatter_post arrived in the chatter as literal tags.
Odoo escapes every `str` body handed to message_post — only a
markupsafe.Markup survives, and RPC cannot carry one.

Odoo 17 added `message_post(body_is_html=...)` for exactly this case
("to be used only for RPC calls", mail/models/mail_thread.py). chatter_post
now exposes it, forwards it on 17+, and omits it on 16, which stores bodies
verbatim anyway. Verified end-to-end against Odoo 17: the body comes back
byte-for-byte as sent.

The parameter is opt-in rather than auto-detected. Detection cannot separate
markup from prose — "Please forward to <a.schmidt@example.com>" and
"if a<b and b>c" are both valid tag syntax — and treating them as markup
silently deletes the text when Odoo renders the field. A body that looks like
markup without the flag therefore gets an advisory warning and is posted
unchanged; looks_like_html exists only to raise that warning, and its
accepted false positives are pinned by a test.

body_is_html is part of the canonical payload, so an approval previewed as
plain text cannot be executed as markup.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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.

1 participant