From bf92a00199ef11f803327c26023b7728e8b22e45 Mon Sep 17 00:00:00 2001 From: Linus Rath <139418639+rathlinus@users.noreply.github.com> Date: Sat, 29 Aug 2026 18:34:09 +0200 Subject: [PATCH] test: cover the draft route-param builder and tick the composer parity items --- docs/parity/04-composer-send.md | 80 ++++++++++++------------- src/lib/__tests__/draft-context.test.ts | 52 ++++++++++++++++ 2 files changed, 92 insertions(+), 40 deletions(-) create mode 100644 src/lib/__tests__/draft-context.test.ts diff --git a/docs/parity/04-composer-send.md b/docs/parity/04-composer-send.md index 85f17144..43625cbb 100644 --- a/docs/parity/04-composer-send.md +++ b/docs/parity/04-composer-send.md @@ -9,17 +9,17 @@ Legend for refs: WEB paths are relative to `the webmail repo`, RN paths to `the ### Drafts -- [ ] **No draft autosave / save-on-close — closing a compose loses everything** — `P1` — `missing` +- [x] **No draft autosave / save-on-close — closing a compose loses everything** — fixed in 5b72c4d — `P1` — `missing` - What WEB does: debounced 2 s autosave to the server Drafts folder with `$draft`+`$seen` keywords (`components/email/email-composer.tsx:1648-1815` saveDraftOnce, `:1842-1883` timer; `lib/jmap/client.ts:2998-3060` createDraft), serialized against send (#303, `:1821-1837`), `beforeunload` best-effort save (`:1082-1091`), "Save / Discard / Cancel" close dialog (`:2347-2426`, `handleSaveDraftAndClose`, `handleDiscardAndClose`), toast when the save is refused so text is not dropped (`:2384-2401`, #702). - What RN does: the only close path is `onClose` in `src/screens/ComposeScreen.tsx:518-541`, an Alert with Cancel / **Discard** — no "Save draft" option and no server draft is ever created. The hardware back button / iOS swipe-back is not intercepted at all (no `beforeRemove` listener anywhere in `ComposeScreen.tsx` or `src/navigation/*`), so the dirty guard is bypassed entirely by the OS back gesture. - Fix hint: add a `createDraft(...)`-style call in `src/api/email.ts` (Email/set create into the `drafts`-role mailbox with `$draft`/`$seen`, destroy previous draft id only after the new create succeeds), call it from a debounced effect + from the close Alert ("Save draft" button), and register `navigation.addListener('beforeRemove', …)` to route back gestures through the same guard. -- [ ] **Cannot open / edit an existing draft** — `P1` — `missing` +- [x] **Cannot open / edit an existing draft** — fixed in 5b72c4d — `P1` — `missing` - What WEB does: `handleEditDraft` in `components/mail/mail-app.tsx:1812` re-opens a draft with recipients, subject, HTML body, identity restored from `From` (`lib/reply-identity.ts:140-167` findDraftIdentityId), server attachments carried as blobId refs (`email-composer.tsx:577-597`, #849) and the signature embedded (`:520-535`, #848); sending destroys the old draft only after success (`lib/jmap/client.ts:3342-3360`) and clears the viewer when the sent draft was displayed (`mail-app.tsx:1645-1648`). - What RN does: `RootStackParamList.Compose` (`src/navigation/types.ts:7-22`) has no `draftId`/initial-body params; tapping a message in the Drafts folder opens the read-only `EmailThreadScreen` (no `role === 'drafts'` handling anywhere in `src/screens/EmailThreadScreen.tsx` or `src/screens/EmailListScreen.tsx`). - Fix hint: add a `draft` route param (id, to/cc/bcc, subject, htmlBody, attachments, from), an "Edit draft" action in the thread screen when the mailbox role is `drafts` or the email has `$draft`, and pass `draftId` into `sendEmail` so the old draft is destroyed after a successful submission. Depends on the previous item. -- [ ] **Draft keeps In-Reply-To/References and reply-mode state** — `P2` — `missing` +- [x] **Draft keeps In-Reply-To/References and reply-mode state** — fixed in 5b72c4d — `P2` — `missing` - What WEB does: drafts store the reply context in `ComposerDraftData` (`email-composer.tsx:109-132`) and threading headers are computed at send from the parent (`lib/email-threading.ts:30-51`). - What RN does: N/A today because drafts do not exist; when implementing drafts make sure `inReplyTo`/`references` (see the threading finding below) survive the round trip. - Fix hint: persist `header:In-Reply-To`/`header:References` on the draft Email/set and read them back via `inReplyTo`/`references` properties when reopening. @@ -41,7 +41,7 @@ Legend for refs: WEB paths are relative to `the webmail repo`, RN paths to `the - What RN does: `jmapClient.request` (`src/api/jmap-client.ts:405-445`) uses `secureFetch` with no `AbortSignal`/deadline; `performSend` (`ComposeScreen.tsx:906-982`) leaves `sending=true` until the promise settles, so the Send button spins indefinitely on a stalled connection. - Fix hint: wrap `secureFetch` in an `AbortController` + `setTimeout` (30 s) in `jmapClient.request` (or a `timeoutMs` option used by `sendEmail`), throw a distinguishable `RequestTimeoutError`, and show a "may already be sent - check Sent" alert in `performSend`'s catch. -- [ ] **Undo-send delay has no undo toast / "send now"** — `P2` — `partial` +- [x] **Undo-send delay has no undo toast / "send now"** — fixed in 5b72c4d — `P2` — `partial` - What WEB does: after a delayed send `pendingUndoSend` is set (`stores/email-store.ts:1700-1712`) and a toast offers Undo / Send now (changelog 1.7.0 "'Send now' action on the send-delay toast"; `email-store.ts:4381` cancelUndoSend). - What RN does: `onSend` (`ComposeScreen.tsx:825-828`) applies `sendDelaySeconds` as HOLDFOR and `performSend` deliberately stays silent for the undo window (`:961-963` comment: "invisible unless the user cancels from the Scheduled view"). There is an `UndoSnackbar` component (`src/components/UndoSnackbar.tsx`) but it is not used for send. The user has to know to open the Scheduled screen within the 5-30 s window. - Fix hint: after `sendEmail` returns `{ scheduled: true, emailSubmissionId }` show `UndoSnackbar` for `sendDelaySeconds` with Undo → `cancelScheduledSend(id)` and "Send now" → EmailSubmission/set update of the envelope without HOLDFOR (or cancel + resend), mirroring `rescheduleEmailSubmission` in `lib/jmap/client.ts:7985`. @@ -61,7 +61,7 @@ Legend for refs: WEB paths are relative to `the webmail repo`, RN paths to `the - What RN does: RN `Identity` type has `replyTo`/`bcc` (`src/api/types.ts:136-145`) but `sendEmail` (`src/api/email.ts:788-796`) never sets `replyTo`, and no composer Reply-To field exists; `IdentitySettings.tsx:209-215` edits only name/email/textSignature. - Fix hint: in `sendEmail` set `replyTo: identity.replyTo` and merge `identity.bcc` into `bcc`; pass the full `Identity` (not only `primaryIdentity.id`) from `performSend`. -- [ ] **Read-receipt (MDN) request missing** — `P3` — `missing` +- [x] **Read-receipt (MDN) request missing** — fixed in 5b72c4d — `P3` — `missing` - What WEB does: composer toggle (`email-composer.tsx:3053-3066`), `requestReadReceiptDefault` setting (`stores/settings-store.ts:351`), header `Disposition-Notification-To` set on create (`lib/jmap/client.ts:3201-3204`). - What RN does: no toggle, no setting, header never set. - Fix hint: add a toolbar toggle + `requestReadReceipt` option to `sendEmail` that writes `header:Disposition-Notification-To:asText`. @@ -71,18 +71,18 @@ Legend for refs: WEB paths are relative to `the webmail repo`, RN paths to `the - What RN does: `sendEmail` sets no `messageId`, relying on the server; RN's `src/lib/uuid.ts:6-20` already has a guarded UUID generator (Hermes fallback) so the crash class of the web fix does not apply. - Fix hint: optional — set `messageId: [`${generateUUID()}@${domain}`]` in `sendEmail` so the id is known before the response (useful once drafts/threading exist). -- [ ] **Sending from shared/group accounts unsupported** — `P3` — `missing` (partly N/A) +- [ ] **Sending from shared/group accounts unsupported** — `P3` — `missing` (partly N/A) — deferred: RN is single-active-account; envelope/identity routing for shared accounts not implemented - What WEB does: routes the submission to the identity's owning account (#461, `email-composer.tsx:2258-2274`, `lib/jmap/client.ts:3131-3134` targetAccountId) and preselects the shared folder's identity (`lib/reply-identity.ts:114-128`). - What RN does: `ComposeScreen.tsx:267-272` explicitly always uses the user's own Sent ("composing on behalf of a shared account isn't supported"), `getIdentities` (`src/api/identity.ts:5-12`) only queries the primary account. Replying from a shared-folder message sends as the reaching login. - Fix hint: low priority; if implemented, query `Identity/get` for the shared `accountId`, submit with that `accountId`, and file into that account's Sent. -- [ ] **Dead duplicate send implementation** — `P3` — `rn-only-bug` +- [x] **Dead duplicate send implementation** — fixed in 42ab246 — `P3` — `rn-only-bug` - What RN does: `src/api/submission.ts:5-64` is an older `sendEmail` (no HOLDFOR, no result) that nothing imports (`grep "api/submission"` → no hits); `ComposeScreen.tsx:38` imports `sendEmail` from `src/api/email.ts`. - Fix hint: delete `src/api/submission.ts` (and its test if any) to avoid someone wiring the stale path. ### Recipients -- [ ] **No Bcc field** — `P2` — `missing` +- [x] **No Bcc field** — fixed in 5b72c4d — `P2` — `missing` - What WEB does: Bcc chip input toggled next to To (`email-composer.tsx:2768-2792`, `:2825-2851`), included in send/draft/dirty-check. - What RN does: only To and Cc (`ComposeScreen.tsx:346-349` state, `:1058-1149` UI); `sendEmail` accepts `bcc` (`src/api/email.ts:772`) but the screen never passes it. - Fix hint: clone the Cc block (`ccRecipients`/`ccInput`/`ccVisible`) as Bcc, include it in `alreadySelected`, `commitTyped`, `isDirty`, and the `sendEmail` call. @@ -97,68 +97,68 @@ Legend for refs: WEB paths are relative to `the webmail repo`, RN paths to `the - What RN does: `initialTo` (`ComposeScreen.tsx:304-310`) adds every original `to` except the From; `initialCc` (`:314-319`) copies `cc` unfiltered — the user's own address lands in To/Cc of every reply-all, and the same address can appear in both To and Cc (`alreadySelected` at `:371-374` only guards suggestions). - Fix hint: filter against the identities list (already loaded at `:406-419`, but note it loads asynchronously — compute recipients after identities arrive or filter at send time) and dedupe across To/Cc/Bcc in `commitTyped`. -- [ ] **Cc addresses are not validated; `Name ` tokens and returns leftovers to the input (`:470-521`), `splitMailbox` tolerates a missing `>` (`lib/rfc5322-mailbox.ts:49-58`), `isValidEmail` is RFC 5322 with injection guards (`lib/validation.ts:4-28`). - What RN does: `parseRecipients` (`ComposeScreen.tsx:69-84`) splits on `[,;\n]` regardless of quotes, requires a closed `>` (else the whole string becomes the "email"), and `hasValidRecipients` (`:504`) only checks **To** against a loose `EMAIL_RE` (`:66`) — an invalid Cc chip is sent to the server and fails there. `"Doe, John" ` becomes two chips. - Fix hint: port `splitRecipients`/`parseRecipient`/`splitPastedRecipients` and `isValidEmail` (all DOM-free) into `src/lib/recipients.ts`; validate Cc/Bcc in `canSend` and mark invalid chips red. -- [ ] **Contact groups not offered as recipients; no server (Sent-folder) search** — `P3` — `missing` +- [x] **Contact groups not offered as recipients; no server (Sent-folder) search** — fixed in 5b72c4d — `P3` — `missing` - What WEB does: groups appear in autocomplete as a single expandable chip (`email-composer.tsx:1214-1245`, changelog 1.7.0), plus an on-demand "search the server" row backed by Sent (`:1189-1212`, changelog 1.6.x). - What RN does: suggestions come only from `individuals` (`ComposeScreen.tsx:367-392`, groups filtered out); groups are reachable only via `GroupDetailScreen.tsx:83` prefill. - Fix hint: include groups in `suggestions` and expand members on pick (use `isGroup`/members from `contacts-store`); server search is optional. -- [ ] **Recipient chips show no avatar / no "Name " tooltip; no chip editing** — `P3` — `partial` (N/A for drag-reorder) +- [x] **Recipient chips show no avatar / no "Name " tooltip; no chip editing** — fixed in 5b72c4d — `P3` — `partial` (N/A for drag-reorder) - What WEB does: avatars in suggestions, chip context menu (copy, add to contacts, move To/Cc/Bcc) (`email-composer.tsx:3860-3895`). - What RN does: `RecipientChip` (`ComposeScreen.tsx:111-124`) shows name-or-email with an X only. - Fix hint: long-press menu with "Move to Cc/Bcc", "Copy address", "Add to contacts". ### Identity / From -- [ ] **Sub-addressing (`user+tag@`) missing** — `P3` — `missing` +- [x] **Sub-addressing (`user+tag@`) missing** — fixed in 5b72c4d — `P3` — `missing` - What WEB does: `SubAddressHelper` next to From with tag suggestions (`components/identity/sub-address-helper.tsx`, `lib/sub-addressing.ts`), configurable delimiter (`stores/settings-store.ts:347`), identity matching strips `+tag` (`lib/reply-identity.ts:18-31`). - What RN does: none; identity matching in `ComposeScreen.tsx:424-441` is exact-address only. - Fix hint: port `lib/sub-addressing.ts` (pure) and add a small "+tag" affordance in the From row. -- [ ] **From override / catch-all alias on an owned domain missing** — `P3` — `missing` (changelog 1.5.x #246) +- [x] **From override / catch-all alias on an owned domain missing** — fixed in 5b72c4d — `P3` — `missing` (changelog 1.5.x #246) - What WEB does: `resolveReplyFrom` (`lib/reply-identity.ts:197-256`) picks an identity or, when the message arrived at an unknown address on a domain the user owns, pre-fills a From override sent through the identity's envelope (`email-composer.tsx:2598-2708` UI, `:2113-2121` envelope). - What RN does: From is always an identity (`ComposeScreen.tsx:908`). - Fix hint: port `resolveReplyFrom`; allow an override name/email pair and set `submissionCreate.envelope.mailFrom` to the identity (RN already builds envelopes for HOLDFOR at `src/api/email.ts:828-844`). -- [ ] **Identity preselection does not consider Bcc or `+tag` and never uses the primary-identity setting** — `P3` — `partial` +- [x] **Identity preselection does not consider Bcc or `+tag` and never uses the primary-identity setting** — fixed in c659b54 — `P3` — `partial` - What WEB does: exact → base (`+tag` stripped) match over to/cc/bcc (`lib/reply-identity.ts:38-68`), self-sent keeps the sending identity (`email-composer.tsx:812-822`), synced `preferredPrimaryId` default identity (#507, `stores/identity-store.ts:18,77`; `hooks/use-identity-sync.ts`). - What RN does: `ComposeScreen.tsx:424-441` matches exact addresses in to/cc only (no bcc, no base match), default = identity matching the active account email else `identities[0]`; no "default identity" setting exists in `IdentitySettings.tsx`. - Fix hint: port `findReplyIdentityId`; add a "Use as default" action in `IdentitySettings` persisted in `settings-store`. -- [ ] **Identity display name not sanitized on create/update** — `P3` — `bugfix-parity` (changelog 1.5.x "Sanitize identity display name to prevent invalid From headers") +- [x] **Identity display name not sanitized on create/update** — fixed in ccbe67c — `P3` — `bugfix-parity` (changelog 1.5.x "Sanitize identity display name to prevent invalid From headers") - What WEB does: `sanitizeIdentityDisplayName` before every Email/set (`lib/jmap/client.ts:3034`, `:3181`) and `sanitizeDisplayName` (`lib/rfc5322-mailbox.ts:27-30`). - What RN does: `IdentitySettings.tsx:253-278` sends the raw name; `performSend` uses `primaryIdentity.name` verbatim (`ComposeScreen.tsx:908`). A name like `Jane ` or one containing a newline produces a malformed From. - Fix hint: port `sanitizeDisplayName` and apply it in `saveDraft` (IdentitySettings) and in `sendEmail`'s `from`. -- [ ] **Identity editor lacks HTML signature, Reply-To, Bcc fields and size caps** — `P3` — `partial` +- [x] **Identity editor lacks HTML signature, Reply-To, Bcc fields and size caps** — fixed in c659b54 — `P3` — `partial` — note: the HTML signature is edited as raw HTML in a monospace field (no rich preview); links/images are sanitized on save - What WEB does: `components/identity/identity-form.tsx` edits name, Reply-To list, Bcc list, text + HTML signature with a 2047-byte cap hint (`:12`, `:39-41`, `:66-70`, `:126-133`), sanitizes HTML. - What RN does: `IdentitySettings.tsx:209-215` DraftIdentity has name/email/textSignature only. - Fix hint: add multiline `htmlSignature` (or keep text-only but at least Reply-To/Bcc comma lists validated with the ported `isValidEmail`). -- [ ] **Identity sync (visibility/interval refresh) missing** — `P3` — `partial` +- [ ] **Identity sync (visibility/interval refresh) missing** — `P3` — `partial` — deferred: composer refetches on every mount; acceptable per finding - What WEB does: `hooks/use-identity-sync.ts` refreshes identities on tab focus and every 30 min. - What RN does: `ComposeScreen.tsx:406-419` refetches on every composer mount (adequate), but `settings-store.identities` (`src/stores/settings-store.ts:424-431`) only refreshes when the Identities settings pane mounts. Acceptable; listed for completeness. - Fix hint: optional `AppState` foreground refresh. ### Signatures -- [ ] **Signatures are never inserted or sent** — `P2` — `missing` +- [x] **Signatures are never inserted or sent** — fixed in 5b72c4d — `P2` — `missing` - What WEB does: compose embeds the signature as an editable block (`email-composer.tsx:254-275` buildEmbeddedSignatureHtml, `:437-443`), replies place it above or below the quote per `signaturePosition` (`:345-353`, `stores/settings-store.ts:349-350`), `-- ` separator toggle, plain-text fallback (`lib/signature-utils.ts:18-51`), alias without signature falls back to the primary identity's (`email-composer.tsx:688-693`), identity switch swaps the block (`:702-775`), double-click unlocks the atom for editing (`components/email/signature-block.ts:112-120`, `:240`), drafts keep it (#848). - What RN does: `Identity.textSignature`/`htmlSignature` exist (`src/api/types.ts:142-143`) and are editable in settings, but `buildInitialHtml` (`src/lib/compose-html.ts:64-106`) and `performSend` (`ComposeScreen.tsx:906-960`) never reference them. Users who set a signature in RN or on the web get none on mobile. - Fix hint: in `buildInitialHtml` append `

--

…
` (sanitized `htmlSignature`, else escaped `textSignature`) after the empty paragraph for compose and above/below the quote for replies according to a new `signaturePosition` setting; re-splice on identity change via `editorRef.setHtml`. The WebView editor is a plain contenteditable, so no "unlock" mechanism is needed (mobile equivalent of 3d1bdf09 is simply "always editable"). -- [ ] **Signature settings (position, separator) absent** — `P3` — `missing` +- [x] **Signature settings (position, separator) absent** — fixed in c659b54 — `P3` — `missing` - What WEB does: `components/settings/composing-settings.tsx:84-96` (position above/below quote, `-- ` separator). - What RN does: `ComposingSettings.tsx:13-20` exposes auto-select identity, send delay, attachment reminder only; `settings-store.ts:131-137` has no signature keys. - Fix hint: add `signaturePosition` / `signatureSeparatorEnabled` to `settings-store` and the Composing pane (do this together with the signature finding). ### Editor / body -- [ ] **Reply / forward quotes plain text only — HTML layout and inline images are lost** — `P2` — `missing` (changelog #163, #543, "Editable, layout-preserving quote island") +- [x] **Reply / forward quotes plain text only — HTML layout and inline images are lost** — fixed in 5b72c4d — `P2` — `missing` (changelog #163, #543, "Editable, layout-preserving quote island") - What WEB does: `getQuoteBodies` picks the right part by MIME (`lib/email-composer-utils.ts:36-55`, #649), quotes the sanitized HTML as an editable island (`email-composer.tsx:482-496`), rewrites `cid:` images and hydrates them as data URLs so they render and are re-attached on send (`:873-957`, `:1938-1978`). - What RN does: `EmailThreadScreen.tsx:59-65` `plainTextBody` returns the text part or `preview`, and `navigateCompose` passes only `body` (`:377`); `buildInitialHtml` supports `htmlBody` (`compose-html.ts:76-80`) but is never given one. Replies to HTML-only mail quote whatever `textBody[0]` holds (for HTML-only messages RFC 8621 exposes the HTML source there — raw tags in the quote, the #649 bug) and inline images vanish. - Fix hint: pass `htmlBody` (sanitized via `stripDangerousTags`/`email-html.ts`) from the thread screen, port `getQuoteBodies`' MIME routing, and for `cid:` images fetch the blob (`getDownloadUrl` in `src/api/blob.ts:119`) as base64, insert with `data-cid`, and add the part as `disposition: 'inline'` on send (RN's `rewriteInlineImages` at `compose-html.ts:143-161` already handles `data-cid`). @@ -168,7 +168,7 @@ Legend for refs: WEB paths are relative to `the webmail repo`, RN paths to `the - What RN does: `navigateCompose` (`EmailThreadScreen.tsx:366-382`) passes no attachments and `Compose` params have no attachment field; `attachments` state starts empty (`ComposeScreen.tsx:354`). A forwarded invoice arrives without the invoice, silently. - Fix hint: add `attachments: email.attachments` (blobId/name/type/size, filter `disposition==='inline'` images referenced by the body) to the route param and seed `AttachmentEntry`s with `blobId` set and `uploading:false`; `OutgoingAttachment` already accepts blobIds from another message in the same account. -- [ ] **Quote header is English-only and shows the sender name without address** — `P3` — `partial` (changelog #482, i18n quote header) +- [x] **Quote header is English-only and shows the sender name without address** — fixed in 5b72c4d — `P3` — `partial` (changelog #482, i18n quote header) - What WEB does: localized `quote_header.*` labels and `On {date}, {from} wrote:` (`lib/quote-header.ts:54-91`), sender rendered as `Name `. - What RN does: `compose-html.ts:84-105` hardcodes "Forwarded message", "From/Date/Subject", "On …, X wrote:" and `senderName` (`:30-33`) returns name **or** email, never both. Locale files exist under `locales//`. - Fix hint: pass translated labels via `QuoteHeaderOptions` and render `Name ` (escaped) like the web. @@ -178,105 +178,105 @@ Legend for refs: WEB paths are relative to `the webmail repo`, RN paths to `the - What RN does: `ComposeScreen.tsx:321-328` only tests `^re:`/`^fwd?:`; "AW: foo" becomes "Re: AW: foo", "Fwd: foo" replied becomes "Re: Fwd: foo". - Fix hint: copy `lib/subject-prefix.ts` verbatim (pure TS) and use the locale's prefix strings. -- [ ] **Attachment reminder scans the quoted original** — `P3` — `bugfix-parity` (changelog #570) +- [x] **Attachment reminder scans the quoted original** — fixed in 5b72c4d — `P3` — `bugfix-parity` (changelog #570) - What WEB does: `extractUserAuthoredText` strips blockquotes/quoted island and everything after the forwarded separator before keyword matching (`lib/email-composer-utils.ts:175-210`, used at `email-composer.tsx:2060-2066`). - What RN does: `passesAttachmentReminder` (`ComposeScreen.tsx:764-791`) runs `htmlToPlainText` over the **whole** editor HTML including the `
`/forward block built by `buildInitialHtml`, so replying to any mail that mentions "attached" fires the warning. - Fix hint: strip `
…
` and everything from the forwarded separator before matching (regex is fine, no DOM needed). -- [ ] **Empty subject blocks Send instead of confirming** — `P3` — `bugfix-parity` (changelog #684) +- [x] **Empty subject blocks Send instead of confirming** — fixed in 5b72c4d — `P3` — `bugfix-parity` (changelog #684) - What WEB does: confirm dialog with "Don't ask again" (`email-composer.tsx:2046-2051`, `:3228-3264`, setting `emptySubjectWarningEnabled`). - What RN does: `canSend` requires `subject.trim().length > 0` (`ComposeScreen.tsx:513`) — the Send button is simply disabled with no explanation. - Fix hint: drop the subject condition from `canSend` and add an Alert ("Send without subject?") gated by a new `emptySubjectWarningEnabled` setting. -- [ ] **Toolbar coverage: no text color, tables, font, horizontal rule, text-direction** — `P3` — `partial` +- [x] **Toolbar coverage: no text color, tables, font, horizontal rule, text-direction** — fixed in 5b72c4d — `P3` — `partial` - What WEB does: Tiptap with Underline, Link, TextAlign, TextStyle+Color, ResizableImage, Table (`components/email/rich-text-editor.tsx:191-225`), auto text direction (changelog 1.7.0), tables (#236). - What RN does: bold/italic/underline/strike, H1/H2, lists, blockquote, align, link, image, clear, undo/redo (`ComposeScreen.tsx:1190-1240`; commands in `src/components/RichTextEditor.tsx:8-24`). No `dir="auto"` on the editor (`src/lib/editor-html.ts:244`). - Fix hint: `execCommand('foreColor')` + a small palette, `insertHTML` for a basic table; set `dir="auto"` on `#editor`. -- [ ] **Plain-text mode still renders the rich editor and derives text from HTML (links lose their href)** — `P3` — `partial` +- [x] **Plain-text mode still renders the rich editor and derives text from HTML (links lose their href)** — fixed in c659b54 — `P3` — `partial` — note: the toggle now also lives under Composing; the duplicate in ReadingSettings (area 08 file, uncommitted edits there) was left in place - What WEB does: plain-text mode swaps to a `