Skip to content

Send outbound replies with an HTML body (multipart/alternative) - #25

Open
APTSPC wants to merge 1 commit into
agentmail-to:mainfrom
APTSPC:feat/html-reply-body
Open

APTSPC wants to merge 1 commit into
agentmail-to:mainfrom
APTSPC:feat/html-reply-body

Conversation

@APTSPC

@APTSPC APTSPC commented Oct 28, 2026

Copy link
Copy Markdown

Send outbound replies with an HTML body (multipart/alternative)

What this changes

AgentMail replies are delivered as text/plain only. The provider's reply API already accepts an
html body — this PR uses it, passing a rendered version of the same Markdown the agent produced:

const text = (ctx.payload?.text ?? ctx.text)?.trim() || undefined;
const html = text ? renderEmailHtml(text) : undefined;
...
{
  ...(text ? { text } : {}),
  ...(html ? { html } : {}),
  ...
}

A new dependency-free renderer (src/channel/src/markdown-html.ts) covers what agent replies
actually contain: headings, bold/italic/strikethrough, inline code, fenced code, links (explicit and
bare URLs), bullet/numbered lists, pipe tables, blockquotes, horizontal rules and paragraphs.

Why

Agent replies are Markdown. In any HTML-capable client that means recipients currently see literal
**bold**, - item and | table | syntax — an email that looks broken rather than formatted.
Supplying both parts is the standard fix: HTML clients render the formatted version, text-only
clients and archives keep the readable plain-text alternative.

Safety / compatibility

  • text is still sent in every case, so this is additive — text-only clients, mail archives and
    providers that strip HTML are unaffected.
  • Attachment-only replies are unchanged (html is only built when there is text).
  • No new runtime dependency; the renderer is string handling only (no marked, no DOM).
  • All user/agent content is HTML-escaped before any markup is applied, and links are restricted to
    http(s) URLs. Raw input like <script> is escaped, not passed through.

Rendering notes

  • The document uses a role="presentation" table shell with text-align:left and no
    max-width + margin:0 auto wrapper: Gmail honours those two properties and centres the entire
    message body, which reads wrong for a business email. An optional maxWidth option anchors a
    column to the left edge instead.
  • Inline styles only (no <style> block), since several clients drop head CSS.
  • 720px-class layouts were deliberately avoided for the same reason.

Testing

  • src/channel/src/markdown-html.test.ts covers headings/emphasis/lists, escaping, autolink
    punctuation, explicit links, inline code, pipe tables, fenced code, paragraph line breaks, the
    left-aligned shell, and the optional max width.
  • npm test (vitest) — new suite only; no existing tests touched.
  • Manually verified end to end against a live AgentMail inbox: the reply arrives with both parts,
    Gmail renders the HTML and the plain-text alternative is intact.

Follow-ups (not in this PR)

  • Optionally expose linkColor / maxWidth through channel config.
  • If maintainers prefer opt-in behaviour, the renderer can be gated behind a htmlFormat
    config enum defaulting to "text"; happy to add that if it is the preferred shape.

AgentMail replies were delivered as text/plain only, so Markdown-authored agent replies reached recipients as literal **bold**, '- item' lists and pipe tables. The reply API already accepts an html body: render the same Markdown and send both parts.

Adds src/channel/src/markdown-html.ts, a dependency-free renderer covering headings, emphasis, inline/fenced code, links and bare URLs, lists, pipe tables, blockquotes, rules and paragraphs; all content is HTML-escaped before formatting and links are restricted to http(s).

The wrapper uses a presentation table with text-align:left and no max-width + auto margins, because Gmail honours those and centres the entire body. text is still sent in every case, so text-only clients and archives are unaffected.
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