mirror of
https://github.com/suitenumerique/messages.git
synced 2026-09-10 20:07:46 +02:00
This large PR modernizes the backend of the app: - Python Dependency Management: Poetry → uv - Object Storage for local dev: MinIO (now unmaintained) → RustFS - Makefile Target Standardization to align with other LaSuite repos - Internationalization Removal on backend: we only care about i18n on the frontend - Backend dependencies upgrade
217 lines
7.3 KiB
Markdown
217 lines
7.3 KiB
Markdown
# 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 test-front -- src/features/blocknote/email-exporter/index.test.tsx
|
||
```
|