# BlockNote to Email-Safe HTML Exporter

## Overview

Messages uses [BlockNote](https://www.blocknotejs.org/) as its rich-text editor.
BlockNote's built-in `blocksToHTMLLossy` produces HTML that relies on CSS classes
and modern layout which email clients strip or ignore.

The **EmailExporter** converts BlockNote's block tree into HTML that is safe for
email rendering: every style is **inline**, images reference **cid:** URLs for
MIME embedding, and layout is achieved through `<table role="presentation">`
wrappers generated by [@react-email/components](https://react.email/).

```
src/frontend/src/features/blocknote/email-exporter/
├── index.tsx          # Exporter implementation
└── index.test.tsx     # Test suite
```

## Architecture

### Conversion pipeline

```
BlockNote blocks (JSON)
    │
    ▼
transformBlocks()          ← Groups consecutive list items, recurses children
    │
    ▼
React nodes                ← Using @react-email/components (Section, Text, Img…)
    │
    ▼
renderToStaticMarkup()     ← react-dom/server, no hydration markers
    │
    ▼
Email-safe HTML string
```

### Public API

```ts
import { EmailExporter } from '@/features/blocknote/email-exporter';

const exporter = new EmailExporter();

const html = exporter.exportBlocks(
  blocks,              // BlockNote document blocks
  editorDomElement,    // Editor DOM element (for image width fallback), nullable
);
```

### Integration points

| Caller | File | Notes |
|--------|------|-------|
| Message send | `message-composer/index.tsx` via `exportContent()` ref | Called at send time, not on every keystroke |
| Signature editor | `hooks/use-base64-composer.tsx` | Object URLs resolved to base64 after export |
| Template editor | `hooks/use-base64-composer.tsx` | Same hook, shared with signatures |

Export is intentionally **deferred to send/save time** to avoid the cost of
`blocksToMarkdownLossy()` (which creates real DOM elements) on every keystroke.

## Supported blocks

### Content blocks

| BlockNote type | HTML output | Styling |
|---|---|---|
| `paragraph` | `<p>` (via `<Text>`) | textAlignment, textColor, backgroundColor |
| `heading` | `<h1>`–`<h6>` (via `<Heading>`) | level, textAlignment, textColor, backgroundColor |
| `quote` | `<blockquote>` with left border | textAlignment, textColor, backgroundColor |
| `codeBlock` | `<pre><code>` | Grey background, rounded corners |
| `divider` | `<hr>` (via `<Hr>`) | — |

### List blocks

Consecutive list items of the same type are grouped into a single `<ul>` or
`<ol>`, as expected for valid HTML. Nested blocks (children) are rendered
recursively.

| BlockNote type | HTML output | Notes |
|---|---|---|
| `bulletListItem` | `<ul><li>` | — |
| `numberedListItem` | `<ol><li>` | — |
| `checkListItem` | `<ul><li>` with `<input type="checkbox" disabled>` | `checked` prop honoured |

### Media blocks

| BlockNote type | HTML output | Notes |
|---|---|---|
| `image` | `<Img>` (optionally in `<figure>` with `<figcaption>`) | See [Image handling](#image-handling) |

### Skipped blocks

| BlockNote type | Output | Reason |
|---|---|---|
| `signature` | Empty `<span>` | Rendered by the backend at MIME composition time |
| `quoted-message` | Empty `<span>` | Replaced by the original message content by the backend |
| `table` | Not rendered | Not supported in email export |
| Unknown types | `<div>` with inline content, or `null` | Graceful fallback |

## Inline content

Three types of inline content are supported inside blocks:

| Type | Rendering |
|---|---|
| **Styled text** | `<span style="…">` (or plain text when unstyled) |
| **Link** | `<a>` (via `<Link>`) with hardcoded blue color (`#0b6e99`) |
| **Template variable** | `<span data-inline-content-type="template-variable">{value}</span>` |

### Text styles

All styles are applied as inline CSS properties:

| Style | CSS |
|---|---|
| `bold` | `font-weight: bold` |
| `italic` | `font-style: italic` |
| `underline` | `text-decoration-line: underline` |
| `strike` | `text-decoration-line: line-through` |
| `code` | `font-family: monospace; background-color: #f0f0f0; padding: 2px 4px` |
| `textColor` | `color: <hex>` — Named colors mapped from BlockNote palette |
| `backgroundColor` | `background-color: <hex>` — Named colors mapped from BlockNote palette |

When multiple text-decoration styles are present (e.g. underline + strikethrough),
they are merged into a single `text-decoration-line` value
(`"underline line-through"`).

### Color palette

The exporter embeds a copy of BlockNote's default color palette (not exposed
via public API) to resolve named colors like `"red"`, `"blue"`, etc. to their
hex values. Custom hex values are passed through as-is. The special value
`"default"` produces no style.

## Image handling

Images go through several transformations from the editor to the recipient's
email client:

```
Editor (Object URL: blob:…)
    │
    ▼
EmailExporter.exportBlocks()
    │  MailHelper.replaceBlobUrlsWithCid(url)
    │  /api/v1/…/blobs/{blobId}/ → cid:{blobId}
    ▼
HTML with cid: references
    │
    ▼
Backend: prepare_outbound_message()
    │  extract_base64_images_from_html() for signature/template images
    │  Blob attachments added as MIME inline parts with matching Content-ID
    ▼
RFC 5322 MIME message
    │
    ▼
Email client renders <img src="cid:…"> from MIME parts
```

### Width resolution

Image width is resolved with a two-step fallback:

1. `props.previewWidth` — Set when the user resizes the image in the editor
2. `editorDomElement` DOM query — Reads `naturalWidth` from the rendered `<img>`

### Alignment

Image alignment is achieved through CSS margins (the `<Img>` component already
sets `display: block`):

- `center` → `margin-left: auto; margin-right: auto`
- `right` → `margin-left: auto`

### Captions

When a caption is present, the image is wrapped in a `<figure>` with a
`<figcaption>` element.

## Backend MIME composition

The backend (`core/mda/outbound.py`) receives `htmlBody` and `textBody` from
the frontend and builds the final RFC 5322 message:

1. **Signature injection** — Appends rendered signature HTML (may contain
   base64 images)
2. **Reply/forward embedding** — Wraps original message content for
   reply/forward threads
3. **Base64 image extraction** — `extract_base64_images_from_html()` finds
   `data:image/…;base64,…` URLs (from signatures/templates), replaces them
   with `cid:` references, and returns the extracted images as MIME attachments
4. **Deduplication** — Images with the same SHA-256 hash share a single CID,
   so the same image in HTML and text bodies is attached only once
5. **MIME assembly** — All blob attachments, extracted base64 images, and Drive
   attachment links are composed into the final multipart message

## Testing

The exporter has a comprehensive test suite in `src/features/blocknote/email-exporter/` covering:

- All block types (paragraph, heading, lists, images, code, quotes, dividers)
- Inline styles (bold, italic, underline, strike, code, colors)
- List grouping (consecutive items, mixed types, nested lists)
- Image handling (captions, alignment, width resolution, cid replacement)
- Template variables
- Edge cases (empty content, unknown blocks, combined styles)

Run with:

```bash
make front-test -- src/features/blocknote/email-exporter/index.test.tsx
```
