<!-- GENERATED by scripts/build-llms.mjs from llms/viewers.md — do not edit this file. -->

# `lr-email-viewer`

- **Import** `import '@aceshooting/lyra-ui/components/lr-email-viewer.js';` (stable tag alias; registers the tag)
- **Class** `LyraEmailViewer`, also available unregistered from `@aceshooting/lyra-ui/components/viewers/email-viewer/email-viewer.class.js`
- **Family** `components/viewers/` — see `llms/index.md` for its siblings
- **Status** `stable` since `4.0.0` — see the maturity and deprecation policy in `llms/shared.md`
- **Release history** [CHANGELOG.md](../../CHANGELOG.md)
- **Deprecations** none
- **Optional peers** `dompurify`, `postal-mime` — see `llms/peers.md`
- **Themeable via** 25 parts, 1 custom property — see this component's own `@csspart`/`@cssprop` list below
- **Library-wide behavior** (events, form association, `locale`/`strings`, tokens, TS types): `llms/shared.md`

---

## `lr-email-viewer`

Fetches and parses `.eml` messages with the optional `postal-mime` peer. HTML message bodies are
sanitized through the existing optional `dompurify` peer before rendering; plain-text messages
remain available without DOMPurify. Attachments are listed as filename and size only (the parsed
`mimeType` never reaches the DOM) and their content is never rendered by this component. Each
attachment row is a real `<button>` that emits `lr-attachment-open` with an immutable Blob snapshot
of the decoded bytes; opening, downloading, or object-URL'ing them is the host's job (e.g.
`URL.createObjectURL(content)` → `<lr-document-viewer>` → revoke on
`lr-close`).
An attachment with no filename uses the localized `emailViewerUnnamedAttachment` fallback
(`"Unnamed attachment"` in English) for its visible name, open-button accessible name, and
`lr-attachment-open` detail. The fallback resolves while rendering, so changing the locale or
per-instance `strings` after the message loads updates it without reparsing the message.

When `foldQuotes` is enabled, `search(query)` reveals matching supported plain-text and HTML quoted
replies before navigating. Reveal uses the shared text index's Unicode normalization, whitespace
collapse, soft-hyphen removal, locale matching, and query/work bounds, so padded or normalized
queries reveal the same quote as ordinary matching text. Folding remains opt-in.

Parser output is admitted through a bounded direct-data projection: malformed optional fields,
recipients, and attachment entries are omitted without discarding later valid siblings. Accepted
string, `Uint8Array`, and `ArrayBuffer` attachment bytes are copied before use, and every
`lr-attachment-open` creates a fresh Blob, so later source mutation cannot change event bytes.

Sanitized HTML uses the passive-document profile: anchors, form controls, and custom elements are
unwrapped to ordinary text/children where safe, remote navigation/resource attributes are removed,
and an `<a>` itself never remains. Images render only inline base64 GIF, JPEG, PNG, or WebP data;
same-document SVG fragment references may remain.

Remote resources are capped at 25 MB; exceeding it surfaces the localized
`documentPreviewResourceTooLarge` message instead of the message.

**Properties:** `src: string = ''`, `name: string = ''`, and `maxHeight: string = ''` (attribute
`max-height`); invalid CSS
`max-height` values, declaration breaks, and `url()` are ignored. `foldQuotes: boolean = false`
(attribute `fold-quotes`) — collapses trailing quoted-reply text/HTML behind a
localized show/hide toggle. `false` (the default) preserves the full body rendering. A nonempty
host `aria-label` makes the host the sole named semantic owner; an explicitly empty host label
keeps the shadow `region` with an empty name, and an absent host label falls back to `name` or the
localized label. `highlights`, `activeHighlightId`, `anchor`, and
`anchorKinds` (`['text-quote', 'fragment']`) provide the shared text-viewer contract. A fragment is
an exact DOM `id` lookup: Lyra generates no ids for message headers or a plain-text body, while an
HTML message can resolve only an id retained from its sanitized body. Without such an id the jump
reports `found: false`; text-quote anchors work across all rendered message text.

**Methods:** `search(query)`, `searchNext()`, `searchPrevious()`, `clearSearch()`, and
`scrollToAnchor()` operate on rendered message text and emit the shared search/anchor events.

**Events:**

- `lr-render-error` with `detail.error` when fetching or parsing fails.
- `lr-attachment-open` — recursively frozen `detail: { attachment: { filename, mimeType,
  content?: Blob } }`; call `content.arrayBuffer()` to read the immutable copied bytes. This
  replaces the mutable `Uint8Array` event field.
- `lr-search-change` — `detail: { query: string; matchCount: number; matchCountExact: boolean; activeIndex: number }` — fired
  whenever rendered-message search state changes.
- `lr-anchor-result` — `detail: { found: boolean }` — fired after an `anchor` assignment or
  `scrollToAnchor()` call is applied.
- `lr-text-select` — `detail: TextSelectDetail` (`{ text: string; anchor: LyraAnchor | null; rects:
DOMRect[] }`) — fired after a selection ends inside the rendered message.

`lr-highlight-activate` is not part of this viewer's event contract: painted text highlights are
passive and cannot be activated.

The three shared text-viewer events bubble and compose and are non-cancelable.

**CSS parts:** `base`, `headers`, `from-label`, `from`, `to-label`, `to`, `subject-label`, `subject`,
`date-label`, `date`, `body`, `body-html`, `body-text`, `attachments`, `attachments-label`,
`attachment-list`, `attachment-item`, `attachment-button` (an attachment's open button, inside its
`attachment-item`), `attachment-name` (an attachment's filename, inside `attachment-button`),
`attachment-size` (an attachment's formatted file size, inside `attachment-button`), `quoted` (a
folded quoted-text block, hidden until expanded, only while `foldQuotes`), `quote-toggle` (the
show/hide-quoted-text toggle button, only while `foldQuotes`), `spinner`, `error`, and
`anchor-live-region` (an aria-hidden, non-live shadow mirror of the latest anchor-jump message; the
spoken copy is appended to the shared document-level polite sink only while the viewer and its
composed ancestors are exposed to the accessibility tree).

**Themeable custom properties:** `--lr-email-viewer-max-height` (default `none`) — maximum block size
of `[part="body"]`; also settable via the `max-height` property, which writes this token inline.

**Optional peer dependencies:** install `postal-mime` and `dompurify` with
`pnpm add postal-mime dompurify`. The component registers `message/rfc822` and falls back to
matching `.eml` filenames in `<lr-document-viewer>`. Fail-closed behavior is explicit: an absent
`postal-mime` renders `[part="error"]` with the localized
`emailViewerMissingParser` message (nothing is parseable without it), and an HTML-only message
(no `text/plain` alternative) with `dompurify` absent renders the localized
`documentViewerMissingSanitizer` message rather than silently showing an empty body.
