Tick the parity items the fix pass finished (server-side recurrence, filters and vacation for shared accounts, in-app file preview, the trusted-senders toggle, UnifiedPush, sender taps in the viewer), and note on the already ticked ones what this pass changed, such as the tag view and search reaching shared accounts or contact photos as avatars. Items that only moved forward say what is left. Fix hints and "verified" lines that named removed functions (`setEmailKeywords`, `getEmailDetail`, `fetchEmailsChunked`, `buildRecurrenceOverridePatch`) or the old whole-map keyword writes now point at what replaced them. PARITY_CHECKLIST.md counts done and open items per area: 379 of 415 are done, 36 open.
47 KiB
Composer, drafts, sending, identities, signatures, templates, scheduled send
Summary
RN has a working "compose / reply / forward / send now / send later" happy path (WebView rich editor with send-time readback, attachments + inline images, contact suggestions, identity sheet, HOLDFOR scheduling with a cancel list) but is far from parity with the 3,900-line web composer. The biggest gaps are structural: RN has no drafts at all (no autosave, no save-on-close, no opening/editing an existing draft), no signatures (identities carry textSignature/htmlSignature but the composer never uses them), no Bcc, no template insertion in the composer, no Reply-To handling, and broken threading headers (In-Reply-To is set to the JMAP email id instead of the RFC Message-ID). The send path also files the message into Sent before submission, so a failed EmailSubmission/set leaves a never-sent copy in Sent. Several web bugfixes (reply-all self-dedupe / #703, #570 attachment reminder on quoted text, #702 request timeout, unclosed angle brackets, subject-prefix dedupe) are not present in RN.
Legend for refs: WEB paths are relative to the webmail repo, RN paths to the native repo.
Findings
Drafts
-
No draft autosave / save-on-close — closing a compose loses everything — fixed in
5b72c4d—P1—missing— offline or without a Drafts folder the close dialog still dropped the text, and the iOS swipe skipped the dialog, until92b42e4(audit B16)- What WEB does: debounced 2 s autosave to the server Drafts folder with
$draft+$seenkeywords (components/email/email-composer.tsx:1648-1815saveDraftOnce,:1842-1883timer;lib/jmap/client.ts:2998-3060createDraft), serialized against send (#303,:1821-1837),beforeunloadbest-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
onCloseinsrc/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 (nobeforeRemovelistener anywhere inComposeScreen.tsxorsrc/navigation/*), so the dirty guard is bypassed entirely by the OS back gesture. - Fix hint: add a
createDraft(...)-style call insrc/api/email.ts(Email/set create into thedrafts-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 registernavigation.addListener('beforeRemove', …)to route back gestures through the same guard.
- What WEB does: debounced 2 s autosave to the server Drafts folder with
-
Cannot open / edit an existing draft — fixed in
5b72c4d—P1—missing- What WEB does:
handleEditDraftincomponents/mail/mail-app.tsx:1812re-opens a draft with recipients, subject, HTML body, identity restored fromFrom(lib/reply-identity.ts:140-167findDraftIdentityId), 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 nodraftId/initial-body params; tapping a message in the Drafts folder opens the read-onlyEmailThreadScreen(norole === 'drafts'handling anywhere insrc/screens/EmailThreadScreen.tsxorsrc/screens/EmailListScreen.tsx). - Fix hint: add a
draftroute param (id, to/cc/bcc, subject, htmlBody, attachments, from), an "Edit draft" action in the thread screen when the mailbox role isdraftsor the email has$draft, and passdraftIdintosendEmailso the old draft is destroyed after a successful submission. Depends on the previous item.
- What WEB does:
-
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:Referenceson the draft Email/set and read them back viainReplyTo/referencesproperties when reopening.
- What WEB does: drafts store the reply context in
Sending / EmailSubmission
-
Message is filed into Sent before submission — a failed send leaves a fake "sent" copy — fixed in
ccbe67c—P1—bug- What WEB does: creates the email in Drafts with
$draftand moves it to Sent viaonSuccessUpdateEmailon theEmailSubmission/set(lib/jmap/client.ts:3195-3196,:3234-3239, #188); anotCreatedon the submission throws and the draft simply stays in Drafts (:3289-3312). - What RN does:
src/api/email.ts:794-796setsmailboxIds: { [sentMailboxId]: true }, keywords: { $seen: true }on theEmail/setcreate, and theEmailSubmission/setat:848-855has noonSuccessUpdateEmail. If the submission is rejected (notCreatedhandled at:870-872) the user gets a "Send failed" alert but the message is already sitting in Sent looking sent; a retry duplicates it. - Fix hint: create into the Drafts mailbox with
$draft, addonSuccessUpdateEmail: { '#draft': { 'mailboxIds/<sent>': true, 'mailboxIds/<drafts>': null, 'keywords/$draft': null } }to the submission call; RN already hasownMailboxes(mailboxes)to find the Drafts role.
- What WEB does: creates the email in Drafts with
-
In-Reply-To / References carry the JMAP email id instead of the Message-ID — fixed in
7f956d6—P1—rn-only-bug- What WEB does:
computeReplyThreadingHeaders(lib/email-threading.ts:30-51) buildsinReplyTo = [parent.messageId],references = parent.references + parent.messageId, brackets stripped; the client sends them as the JMAPinReplyTo/referencesproperties (lib/jmap/client.ts:3177-3194) and fetchesmessageId/inReplyTo/referenceson the email (:1787-1788). - What RN does:
src/screens/EmailThreadScreen.tsx:379passesinReplyTo: email.id(the JMAP id, e.g.a1b2c3), neverreferences;src/api/email.ts:823-826writes that value verbatim intoheader:In-Reply-To:asTextandheader:References:asText.EMAIL_FULL_PROPERTIES(src/api/email.ts:11-15) does not even requestmessageId/references, and the RNEmailtype (src/api/types.ts:65-87) has no such fields. Every RN reply therefore breaks threading in recipients' clients and emits a syntactically invalidIn-Reply-To. - Fix hint: add
'messageId', 'inReplyTo', 'references'toEMAIL_FULL_PROPERTIESand theEmailtype, portcomputeReplyThreadingHeaders, and sendinReplyTo/referencesas JMAP array properties (bare msg-ids) instead of rawheader:*:asTextstrings. Only replies should continue the chain; forwards should not (WEBemail-composer.tsx:2142-2145).
- What WEB does:
-
No request timeout — a stalled send hangs the composer forever — fixed in
ccbe67c—P2—bugfix-parity(changelog 1.7.x "Time out stalled JMAP requests so a send can't hang forever (#702)")- What WEB does:
JMAPClient.REQUEST_TIMEOUT_MS = 30_000(lib/jmap/client.ts:629,:811,:830-843) raisingRequestTimeoutError; the composer shows a dedicated "may already have gone out, check Sent" message instead of "send failed" so users do not re-send duplicates (email-composer.tsx:2315-2323). - What RN does:
jmapClient.request(src/api/jmap-client.ts:405-445) usessecureFetchwith noAbortSignal/deadline;performSend(ComposeScreen.tsx:906-982) leavessending=trueuntil the promise settles, so the Send button spins indefinitely on a stalled connection. - Fix hint: wrap
secureFetchin anAbortController+setTimeout(30 s) injmapClient.request(or atimeoutMsoption used bysendEmail), throw a distinguishableRequestTimeoutError, and show a "may already be sent - check Sent" alert inperformSend's catch.
- What WEB does:
-
Undo-send delay has no undo toast / "send now" — fixed in
5b72c4d—P2—partial- What WEB does: after a delayed send
pendingUndoSendis 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:4381cancelUndoSend). - What RN does:
onSend(ComposeScreen.tsx:825-828) appliessendDelaySecondsas HOLDFOR andperformSenddeliberately stays silent for the undo window (:961-963comment: "invisible unless the user cancels from the Scheduled view"). There is anUndoSnackbarcomponent (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
sendEmailreturns{ scheduled: true, emailSubmissionId }showUndoSnackbarforsendDelaySecondswith Undo →cancelScheduledSend(id)and "Send now" → EmailSubmission/set update of the envelope without HOLDFOR (or cancel + resend), mirroringrescheduleEmailSubmissioninlib/jmap/client.ts:7985.
- What WEB does: after a delayed send
-
Double-submit guard relies on async state — fixed in
7f956d6—P3—bugfix-parity(changelog 1.6.x "Guard Send against double-submit")- What WEB does: synchronous
isSendingRefre-entry guard (email-composer.tsx:1980-1986,:2007,:2078-2079). - What RN does:
performSendcheckscanSendwhich includes!sending(ComposeScreen.tsx:508-516,:883), butsetSending(true)only happens after two awaits (getHtmlat:892, attachment reminder at:905), so two quick taps both pass the guard and submit twice. - Fix hint: add a
sendingRef = useRef(false)set synchronously at the top ofperformSend, reset infinally.
- What WEB does: synchronous
-
Reply / forward does not mark the original
$answered/$forwarded— fixed in7f956d6—P2—missing- What WEB does: after a successful reply/forward the original gets
$answeredor$forwarded(components/mail/mail-app.tsx:1663-1673), routed to the owning account. - What RN does: nothing —
grep -F '$answered'oversrc/returns no hits;performSend(ComposeScreen.tsx:941-974) has no access to the original email id (replyToparams insrc/navigation/types.ts:10-19carry noemailId). - Fix hint: add
originalEmailId/jmapAccountIdto thereplyToroute param and call the existing keyword primitive (setEmailKeywordsviaapplyOrQueueinsrc/stores/outbox-store.ts) after the send resolves. (setEmailKeywordsis gone since 5041897; the keyword primitive is nowpatchKeywordsForEmails, and the outbox queueskeywords/<name>patches.)
- What WEB does: after a successful reply/forward the original gets
-
Identity
replyTo/bccare never applied to outgoing mail — fixed in7f956d6—P2—missing- What WEB does:
sendEmailcopies the identity'sreplyToonto the message (lib/jmap/client.ts:3167-3172,:3184); identity form lets users edit Reply-To and Bcc (components/identity/identity-form.tsx:39-41,:66-70,:132-133). - What RN does: RN
Identitytype hasreplyTo/bcc(src/api/types.ts:136-145) butsendEmail(src/api/email.ts:788-796) never setsreplyTo, and no composer Reply-To field exists;IdentitySettings.tsx:209-215edits only name/email/textSignature. - Fix hint: in
sendEmailsetreplyTo: identity.replyToand mergeidentity.bccintobcc; pass the fullIdentity(not onlyprimaryIdentity.id) fromperformSend.
- What WEB does:
-
Read-receipt (MDN) request missing — fixed in
5b72c4d—P3—missing- What WEB does: composer toggle (
email-composer.tsx:3053-3066),requestReadReceiptDefaultsetting (stores/settings-store.ts:351), headerDisposition-Notification-Toset on create (lib/jmap/client.ts:3201-3204). - What RN does: no toggle, no setting, header never set.
- Fix hint: add a toolbar toggle +
requestReadReceiptoption tosendEmailthat writesheader:Disposition-Notification-To:asText.
- What WEB does: composer toggle (
-
Client-side Message-ID not generated — fixed in
ccbe67c—P3—partial- What WEB does:
generateMessageId(fromEmail)with the sender's domain, guarded for insecure origins (lib/jmap/client.ts:544-575,:3192; changelog 1.9.0 fix). - What RN does:
sendEmailsets nomessageId, relying on the server; RN'ssrc/lib/uuid.ts:6-20already 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}]insendEmailso the id is known before the response (useful once drafts/threading exist).
- What WEB does:
-
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-3134targetAccountId) and preselects the shared folder's identity (lib/reply-identity.ts:114-128). - What RN does:
ComposeScreen.tsx:267-272explicitly 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/getfor the sharedaccountId, submit with thataccountId, and file into that account's Sent.
- What WEB does: routes the submission to the identity's owning account (#461,
-
Dead duplicate send implementation — fixed in
42ab246—P3—rn-only-bug- What RN does:
src/api/submission.ts:5-64is an oldersendEmail(no HOLDFOR, no result) that nothing imports (grep "api/submission"→ no hits);ComposeScreen.tsx:38importssendEmailfromsrc/api/email.ts. - Fix hint: delete
src/api/submission.ts(and its test if any) to avoid someone wiring the stale path.
- What RN does:
Recipients
-
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-349state,:1058-1149UI);sendEmailacceptsbcc(src/api/email.ts:772) but the screen never passes it. - Fix hint: clone the Cc block (
ccRecipients/ccInput/ccVisible) as Bcc, include it inalreadySelected,commitTyped,isDirty, and thesendEmailcall.
- What WEB does: Bcc chip input toggled next to To (
-
Reply-To header ignored when replying — fixed in
7f956d6—P2—bugfix-parity(changelog 1.9.0 "Honour an external Reply-To even on a self-sent message"; 1.7.x #703)- What WEB does:
buildReplyRecipients(lib/reply-recipients.ts:74-111) replies toreplyToAddresseswhen present (RFC 5322), falls back to From, and handles self-sent messages (isSelfSent,:52-54) by addressing the original To/Cc instead of yourself. - What RN does:
initialTo(ComposeScreen.tsx:291-312) always usesreplyTo.from;EmailThreadScreen.tsx:370-381does not passemail.replyToalthough it is fetched (EMAIL_FULL_PROPERTIESincludesreplyTo). Replying to a mailing-list/ticket mail with Reply-To goes to the wrong address; replying to your own sent message mails yourself. - Fix hint: pass
replyToAddresses: email.replyToin the route param and portbuildReplyRecipients(pure TS, no DOM) intosrc/lib/.
- What WEB does:
-
Reply-all does not drop the user's own identities / duplicates — fixed in
7f956d6—P2—bugfix-parity- What WEB does: reply-all filters every own identity address (exact and
+tag-stripped) out of To/Cc (lib/reply-recipients.ts:39-45,:104-110) andexpandRecipientsdedupes case-insensitively across the whole list (lib/email-composer-utils.ts:425-437). - What RN does:
initialTo(ComposeScreen.tsx:304-310) adds every originaltoexcept the From;initialCc(:314-319) copiesccunfiltered — the user's own address lands in To/Cc of every reply-all, and the same address can appear in both To and Cc (alreadySelectedat:371-374only 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 incommitTyped.
- What WEB does: reply-all filters every own identity address (exact and
-
Cc addresses are not validated;
Name <addrrecovery and quoted names missing — fixed in5b72c4d—P3—bugfix-parity(changelog 1.9.0 "Split recipient lists whose angle brackets never close", 1.7.x #672)- What WEB does:
splitRecipientsis quote/angle-aware and recovers from an unclosed<(lib/email-composer-utils.ts:275-321),splitPastedRecipientskeeps display names, unwraps<addr>tokens and returns leftovers to the input (:470-521),splitMailboxtolerates a missing>(lib/rfc5322-mailbox.ts:49-58),isValidEmailis 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"), andhasValidRecipients(:504) only checks To against a looseEMAIL_RE(:66) — an invalid Cc chip is sent to the server and fails there."Doe, John" <j@x>becomes two chips. - Fix hint: port
splitRecipients/parseRecipient/splitPastedRecipientsandisValidEmail(all DOM-free) intosrc/lib/recipients.ts; validate Cc/Bcc incanSendand mark invalid chips red.
- What WEB does:
-
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 viaGroupDetailScreen.tsx:83prefill. - Fix hint: include groups in
suggestionsand expand members on pick (useisGroup/members fromcontacts-store); server search is optional.
- What WEB does: groups appear in autocomplete as a single expandable chip (
-
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".
- What WEB does: avatars in suggestions, chip context menu (copy, add to contacts, move To/Cc/Bcc) (
Identity / From
-
Sub-addressing (
user+tag@) missing — fixed in5b72c4d—P3—missing- What WEB does:
SubAddressHelpernext 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-441is exact-address only. - Fix hint: port
lib/sub-addressing.ts(pure) and add a small "+tag" affordance in the From row.
- What WEB does:
-
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-2708UI,:2113-2121envelope). - What RN does: From is always an identity (
ComposeScreen.tsx:908). - Fix hint: port
resolveReplyFrom; allow an override name/email pair and setsubmissionCreate.envelope.mailFromto the identity (RN already builds envelopes for HOLDFOR atsrc/api/email.ts:828-844).
- What WEB does:
-
Identity preselection does not consider Bcc or
+tagand never uses the primary-identity setting — fixed inc659b54—P3—partial- What WEB does: exact → base (
+tagstripped) match over to/cc/bcc (lib/reply-identity.ts:38-68), self-sent keeps the sending identity (email-composer.tsx:812-822), syncedpreferredPrimaryIddefault identity (#507,stores/identity-store.ts:18,77;hooks/use-identity-sync.ts). - What RN does:
ComposeScreen.tsx:424-441matches exact addresses in to/cc only (no bcc, no base match), default = identity matching the active account email elseidentities[0]; no "default identity" setting exists inIdentitySettings.tsx. - Fix hint: port
findReplyIdentityId; add a "Use as default" action inIdentitySettingspersisted insettings-store.
- What WEB does: exact → base (
-
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:
sanitizeIdentityDisplayNamebefore every Email/set (lib/jmap/client.ts:3034,:3181) andsanitizeDisplayName(lib/rfc5322-mailbox.ts:27-30). - What RN does:
IdentitySettings.tsx:253-278sends the raw name;performSendusesprimaryIdentity.nameverbatim (ComposeScreen.tsx:908). A name likeJane <jane@x>or one containing a newline produces a malformed From. - Fix hint: port
sanitizeDisplayNameand apply it insaveDraft(IdentitySettings) and insendEmail'sfrom.
- What WEB does:
-
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.tsxedits 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-215DraftIdentity 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 portedisValidEmail).
- What WEB does:
-
Identity sync (visibility/interval refresh) missing —
P3—partial— deferred: composer refetches on every mount; acceptable per finding- What WEB does:
hooks/use-identity-sync.tsrefreshes identities on tab focus and every 30 min. - What RN does:
ComposeScreen.tsx:406-419refetches on every composer mount (adequate), butsettings-store.identities(src/stores/settings-store.ts:424-431) only refreshes when the Identities settings pane mounts. Acceptable; listed for completeness. - Fix hint: optional
AppStateforeground refresh.
- What WEB does:
Signatures
-
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-275buildEmbeddedSignatureHtml,:437-443), replies place it above or below the quote persignaturePosition(: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/htmlSignatureexist (src/api/types.ts:142-143) and are editable in settings, butbuildInitialHtml(src/lib/compose-html.ts:64-106) andperformSend(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
buildInitialHtmlappend<p>-- </p><div data-signature>…</div>(sanitizedhtmlSignature, else escapedtextSignature) after the empty paragraph for compose and above/below the quote for replies according to a newsignaturePositionsetting; re-splice on identity change viaeditorRef.setHtml. The WebView editor is a plain contenteditable, so no "unlock" mechanism is needed (mobile equivalent of 3d1bdf09 is simply "always editable").
- What WEB does: compose embeds the signature as an editable block (
-
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-20exposes auto-select identity, send delay, attachment reminder only;settings-store.ts:131-137has no signature keys. - Fix hint: add
signaturePosition/signatureSeparatorEnabledtosettings-storeand the Composing pane (do this together with the signature finding).
- What WEB does:
Editor / body
-
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:
getQuoteBodiespicks 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), rewritescid: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-65plainTextBodyreturns the text part orpreview, andnavigateComposepasses onlybody(:377);buildInitialHtmlsupportshtmlBody(compose-html.ts:76-80) but is never given one. Replies to HTML-only mail quote whatevertextBody[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 viastripDangerousTags/email-html.ts) from the thread screen, portgetQuoteBodies' MIME routing, and forcid:images fetch the blob (getDownloadUrlinsrc/api/blob.ts:119) as base64, insert withdata-cid, and add the part asdisposition: 'inline'on send (RN'srewriteInlineImagesatcompose-html.ts:143-161already handlesdata-cid).
- What WEB does:
-
Forward drops the original attachments — fixed in
7f956d6—P1—missing- What WEB does: forward seeds the composer with the original's attachments as blobId refs (
email-composer.tsx:598-617), skipping cid-embedded images, and sends them along (:2200-2213); "Forward as attachment" also exists (lib/forward-as-attachment.ts). - What RN does:
navigateCompose(EmailThreadScreen.tsx:366-382) passes no attachments andComposeparams have no attachment field;attachmentsstate starts empty (ComposeScreen.tsx:354). A forwarded invoice arrives without the invoice, silently. - Fix hint: add
attachments: email.attachments(blobId/name/type/size, filterdisposition==='inline'images referenced by the body) to the route param and seedAttachmentEntrys withblobIdset anduploading:false;OutgoingAttachmentalready accepts blobIds from another message in the same account.
- What WEB does: forward seeds the composer with the original's attachments as blobId refs (
-
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 andOn {date}, {from} wrote:(lib/quote-header.ts:54-91), sender rendered asName <email>. - What RN does:
compose-html.ts:84-105hardcodes "Forwarded message", "From/Date/Subject", "On …, X wrote:" andsenderName(:30-33) returns name or email, never both. Locale files exist underlocales/<lang>/. - Fix hint: pass translated labels via
QuoteHeaderOptionsand renderName <email>(escaped) like the web.
- What WEB does: localized
-
Subject prefix handling does not strip foreign / stacked prefixes and is not localized — fixed in
7f956d6—P3—bugfix-parity(changelog "Drop single-letter R:/I: tokens and deduplicate localized reply/forward prefixes", "full-width colon")- What WEB does:
lib/subject-prefix.ts:14-56token lists +buildReplySubject/buildForwardSubject(:109-121) stripRe: AW: WG: Fwd:chains,Re[2]:, full-width colons, then add the locale prefix fromemail_composer.prefix.*. - What RN does:
ComposeScreen.tsx:321-328only tests^re:/^fwd?:; "AW: foo" becomes "Re: AW: foo", "Fwd: foo" replied becomes "Re: Fwd: foo". - Fix hint: copy
lib/subject-prefix.tsverbatim (pure TS) and use the locale's prefix strings.
- What WEB does:
-
Attachment reminder scans the quoted original — fixed in
5b72c4d—P3—bugfix-parity(changelog #570)- What WEB does:
extractUserAuthoredTextstrips blockquotes/quoted island and everything after the forwarded separator before keyword matching (lib/email-composer-utils.ts:175-210, used atemail-composer.tsx:2060-2066). - What RN does:
passesAttachmentReminder(ComposeScreen.tsx:764-791) runshtmlToPlainTextover the whole editor HTML including the<blockquote>/forward block built bybuildInitialHtml, so replying to any mail that mentions "attached" fires the warning. - Fix hint: strip
<blockquote>…</blockquote>and everything from the forwarded separator before matching (regex is fine, no DOM needed).
- What WEB does:
-
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, settingemptySubjectWarningEnabled). - What RN does:
canSendrequiressubject.trim().length > 0(ComposeScreen.tsx:513) — the Send button is simply disabled with no explanation. - Fix hint: drop the subject condition from
canSendand add an Alert ("Send without subject?") gated by a newemptySubjectWarningEnabledsetting.
- What WEB does: confirm dialog with "Don't ask again" (
-
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 insrc/components/RichTextEditor.tsx:8-24). Nodir="auto"on the editor (src/lib/editor-html.ts:244). - Fix hint:
execCommand('foreColor')+ a small palette,insertHTMLfor a basic table; setdir="auto"on#editor.
- What WEB does: Tiptap with Underline, Link, TextAlign, TextStyle+Color, ResizableImage, Table (
-
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
<textarea>(email-composer.tsx:2875-2891) andhtmlToPlainTextrenders links astext <href>(lib/html-to-text.ts:134-146); the setting lives under Composing (#422). - What RN does:
plainTextModeonly suppresses the HTML part at send (ComposeScreen.tsx:951); the user still sees formatting buttons;htmlToPlainText(compose-html.ts:113-135) strips<a>tags entirely so pasted links disappear from the text/plain alternative (which is the only part in plain-text mode). The toggle sits inReadingSettings.tsx:262instead of Composing. - Fix hint: hide the format bar and use a
TextInput multilinewhenplainTextMode; inhtmlToPlainTextreplace<a href="X">T</a>withT <X>before stripping tags; move the toggle toComposingSettings.
- What WEB does: plain-text mode swaps to a
-
Inline image insert: no camera, no paste, no multi-select; not offered for file attachments either — fixed in
5b72c4d—P3—partial- What WEB does: drag/drop and paste of images embed as data URLs + cid (
email-composer.tsx:1519-1551, changelog #163). - What RN does:
insertInlineImage(ComposeScreen.tsx:693-732) library only, single image;pickFileAttachment(:646-673) picks one file at a time; nolaunchCameraAsync. - Fix hint: add a "Camera" option to the attach Alert (
ImagePicker.launchCameraAsync) and allow multiple inpickFileAsyncwhere supported.
- What WEB does: drag/drop and paste of images embed as data URLs + cid (
-
Send-time editor readback (RN #9/#11) — verified robust, one edge — fixed in
5b72c4d—P3—rn-only-bug- What RN does:
getHtmlrejects after 2 s when the page script is dead (src/components/RichTextEditor.tsx:143-152) andperformSendaborts loudly (ComposeScreen.tsx:890-902);editor-html.tsis escape-free and unit-tested (src/lib/__tests__/editor-html.test.ts). The Android release fix (c5e5092) has since been superseded by the inlinesource={{ html }}+originWhitelist={['about:blank']}approach (RichTextEditor.tsx:198-230);android/app/src/main/assets/editor.htmlfrom that commit is no longer referenced (grep editor.html→ only the TS module). Edge:removeAttachment(ComposeScreen.tsx:553-571) rewritesbodyHtmlfrom the lastchangemessage rather than a live readback, so if the bridge lags it cansetHtmla stale body and drop the last keystrokes. - Fix hint: use
await editorRef.current.getHtml()before stripping the<img data-cid>; delete the orphanededitor.htmlasset.
- What RN does:
Attachments / upload
-
No byte progress, cancel does not abort the transfer, no size-limit check — fixed in
5b72c4d—P2—partial(changelog 1.9.0 "Real byte progress…cancel aborts", 1652a0ec "respect server limits")- What WEB does: XHR upload with
onProgress+AbortSignal(lib/jmap/client.ts:3954-3990), chip progress bar (email-composer.tsx:2951-2965),abortController.abort()on remove (:1605-1609),getMaxSizeUpload()from the core capability (lib/jmap/client.ts:4180-4181; used for files atcomponents/files/files-app.tsx:311-336). - What RN does:
uploadBlob(src/api/blob.ts:5-62) reads the whole file into memory (new File(uri).bytes()) andfetches it with no progress or signal;AttachmentChipshows a spinner + "Uploading..." (ComposeScreen.tsx:174-192);removeAttachment(:553-571) removes the chip while the upload keeps running; nomaxSizeUpload/maxSizeAttachmentsPerEmailcheck — a 60 MB video from the picker fails only after the full upload with a rawUpload failed: 413. - Fix hint: check
session.capabilities['urn:ietf:params:jmap:core'].maxSizeUpload(and the mail capability'smaxSizeAttachmentsPerEmailfor the running total) before starting; useexpo-file-systemcreateUploadTask/uploadAsyncwith progress callback andcancelAsync(), keep the handle onAttachmentEntry.
- What WEB does: XHR upload with
-
Attachment chip has no preview / open — fixed in
5b72c4d—P3—missing- What WEB does: click a chip to preview via
FilePreviewModal(email-composer.tsx:1615-1646). - What RN does: chip is display-only (
ComposeScreen.tsx:164-200). - Fix hint: on press open the local
uriwithexpo-sharing/Linking.
- What WEB does: click a chip to preview via
Scheduled send
-
Scheduled screen: cancel only — no reschedule, no "send now", no edit — fixed in
c659b54—P2—partial— reschedule dropped Cc and Bcc untile67947a, and a refused half of it could drop or double-send the message until7e598c5(audit B19)- What WEB does: scheduled view with send-now button (changelog 1.6.x), reschedule (
stores/email-store.ts:4353rescheduleScheduledEmail,lib/jmap/client.ts:7985), cancel-for-edit that reopens the composer (email-store.ts:4326), listing routed to the owning account (#874). - What RN does:
src/screens/ScheduledScreen.tsx:51-78offers only "Cancel send";src/api/email.ts:902-988haslistScheduledEmails/cancelScheduledSendonly. - Fix hint: add
rescheduleScheduledSend(submissionId, emailId, identityId, holdFor)=EmailSubmission/set { update: {id: {undoStatus:'canceled'}}, create: {replacement: {emailId, identityId, envelope(HOLDFOR)}} }and an edit path once drafts exist (cancel → open composer prefilled from the Email).
- What WEB does: scheduled view with send-now button (changelog 1.6.x), reschedule (
-
Scheduled screen strings are hard-coded English — fixed in
c659b54—P3—rn-only-bug- What RN does:
ScheduledScreen.tsx:52-56,:69,:85(toLocaleString()without locale/12h setting),:118"Scheduled",:20/:88"(no recipient)"/"(no subject)" — not()calls at all, whereasComposeScreenis localized. - Fix hint: wrap in
useLocaleStore().tand format with the shared date helper (src/lib/date-format.ts).
- What RN does:
-
Scheduled preset times ignore the user's 12/24h format — fixed in
c659b54—P3—rn-only-bug- What RN does:
ComposeScreen.tsx:966-970and:1321-1326usetoLocaleString(undefined, …)/toLocaleString()rather than thetimeFormatsetting the quote header already respects (compose-html.ts:37-51). - Fix hint: reuse
formatQuoteDate-style formatting withtimeFormat.
- What RN does:
Templates
-
No way to insert a template in the RN composer — fixed in
5b72c4d—P2—missing- What WEB does: template picker + "save as template" buttons in the composer (
email-composer.tsx:3030-3049),tshortcut, insertion at the caret keeping typed text and the signature (#539/#540/#621,:1278-1373), placeholder auto-fill (lib/template-utils.ts:46-62,components/templates/placeholder-fill-modal.tsx), default recipients / identity per template (lib/template-types.ts:8-13). - What RN does: templates can be created/edited/imported/exported in
src/components/settings/TemplateSettings.tsxand stored insrc/stores/templates-store.ts, butComposeScreen.tsxnever importsuseTemplatesStore— the feature is unreachable from the composer. - Fix hint: add a toolbar button that opens a bottom sheet listing
templates; on pick set subject if empty andeditorRef.insertHtml(escaped body)(theinsertHtmlbridge exists ineditor-html.ts:368-373); substitute{{date}},{{day_of_week}},{{sender_name}}via a port ofgetAutoFilledPlaceholdersand prompt for the rest.
- What WEB does: template picker + "save as template" buttons in the composer (
-
Template model lacks isHTML, category UI, favourites, default recipients, identity; export format differs — fixed in
c659b54—P3—partial- What WEB does:
lib/template-types.ts:1-17, export envelope{version:1,type:'webmail-templates',…}(lib/template-utils.ts:148-156), import validation (:286-351), favourites/recent (stores/template-store.ts). - What RN does:
templates-store.ts:307-316has name/subject/body/category/isFavorite only;exportAll(:395-397) emits{version:1,templates}withouttype:'webmail-templates', so the webimportTemplatesrejects an RN export withinvalid_type(lib/template-utils.ts:302-304); RN import (:399-434) accepts web exports. Category/favourite are not editable inTemplateSettings.tsx:68-81. - Fix hint: add
type: 'webmail-templates'andexportedAttoexportAll; carryisHTML/defaultRecipients/identityIdthrough import so they survive a round trip.
- What WEB does:
-
Templates not in cross-device settings sync (#825) —
P3—missing(blocked) — deferred: blocked on native settings sync (issue #1)- What WEB does: templates ride in the per-account settings blob with tombstone merge (
stores/template-store.ts:208-218,lib/template-utils.ts:163-284). - What RN does: no settings sync exists at all (native issue #1 "Common settings / global settings" is open), so this is blocked on that feature.
- Fix hint: when settings sync lands, reuse
mergeSyncedTemplates(pure TS).
- What WEB does: templates ride in the per-account settings blob with tombstone merge (
mailto / share intents
- No
mailto:handling / share-to-app intent —P2—missing— composer side (prefill*, prefillAttachments upload) done in 5b72c4d; scheme, intent filters and links to Compose done in5802cae(area 08); shared files,SENDTOand+addresses work sincecd4ce9d,06e8942,ee652d3(audit B9–B11)- What WEB does: PWA
protocol_handlersfor mailto (app/manifest.ts:118),parseMailtoUrl(lib/validation.ts:133-167), in-app composer open through the unsaved-draft guard (components/mail/mail-app.tsx:1110, changelog 1.9.0). - What RN does:
app.config.js:29registers only the custombulwarkmobilescheme; nointentFilters(Androidmailto/SEND) or iOSLSApplicationQueriesSchemes/CFBundleURLTypesformailto; noLinking.getInitialURL/addEventListener('url')handling that routes toCompose(grep overApp.tsx/src/navigationshows none). Tapping a mailto link in another app cannot open Bulwark; sharing a file/text to the app is impossible. - Fix hint: add
scheme: ['bulwarkmobile', 'mailto']+ AndroidintentFiltersformailtoandandroid.intent.action.SEND, parse with a port ofparseMailtoUrl, and navigate toComposewithprefillTo/subject/body (add those params); for SEND intents seedattachmentsfrom the sharedcontent://URI (upload path already supports it,src/api/blob.ts:17-22).
- What WEB does: PWA
Security / crypto
- S/MIME sign/encrypt on send is a stub —
P3—missing— deferred: no raw-MIME send path in RN; certificates are stored only- What WEB does: crypto plugins hook
onComposeSend(email-composer.tsx:2180-2216) and the store hassendRawEmail(stores/email-store.ts:1724). - What RN does:
src/components/settings/SmimeSettings.tsxlists imported certificates ("Your Certificates", "Recipient Certificates") butComposeScreen.tsxhas no sign/encrypt control (grep -i smime|sign|encrypt→ none) andsendEmailhas no raw-MIME path. - Fix hint: out of scope for parity right now; document as a known gap in the settings pane ("certificates are stored but not yet used for sending").
- What WEB does: crypto plugins hook
Misc
-
Auto-add reply recipients to trusted senders — fixed in
5b72c4d—P3—missing(changelog 1.5.x)- What WEB does: after a reply, To/Cc are added to the trusted-senders list/address book (
email-composer.tsx:2283-2294). - What RN does:
settings-store.ts:105-106hastrustedSenders, butComposeScreen.tsxnever touches it (grep -i trusted→ none). - Fix hint: after a successful reply push
finalTo/finalCcemails intotrustedSenders(dedupe).
- What WEB does: after a reply, To/Cc are added to the trusted-senders list/address book (
-
Offline send is not queued —
P3—missing(RN-only opportunity) — deferred: needs a new outbox op kind carrying the whole Email/set + submission payload; drafts now autosave to the server instead- What RN does:
outbox-store.ts:40-43only knowskeywords|mailboxes|destroyops;performSendfails with a network error when offline (ComposeScreen.tsx:975-979) and the composed text is lost unless the user keeps the screen open. WEB has no offline outbox either, so this is not a parity item, but once drafts exist the natural mobile behaviour is "save draft locally, submit when online". - Fix hint: add a
sendop kind carrying the Email/set create + submission payload, flushed byoutbox-store.flush.
- What RN does:
-
Permission / picker error strings are not localized — fixed in
5b72c4d—P3—rn-only-bug- What RN does:
ComposeScreen.tsx:620-623,:696-699,:719hard-code "Photo library permission is required…", "Could not load image";identityErrorfallback text at:1052("identity unavailable", "Loading..."). - Fix hint: route through
t()with keys inlocales/<lang>/common.json.
- What RN does:
Verified at parity (brief list, so the fixer knows what NOT to redo)
- Scheduled send via
EmailSubmissionenvelopeHOLDFORwith explicitrcptTo(bare addresses, names stripped) andmaxDelayedSend/FUTURERELEASE capability checks:src/api/email.ts:828-844,src/api/jmap-client.ts:513-531,ComposeScreen.tsx:796-821— matcheslib/jmap/client.ts:592-612,:3245-3258. The capability check read only the session-level object, which Stalwart leaves empty, so scheduling and the undo delay were off on Stalwart untile28e8b4(#57, audit B8); holds are capped at Stalwart's 7-day limit since4e7ae2f. - Undo-send delay setting (0/5/10/20/30 s) applied only when the server supports delayed send:
ComposeScreen.tsx:825-828,ComposingSettings.tsx:42-48. - Send-time readback of the live editor DOM with timeout + loud abort (RN #9/#11):
RichTextEditor.tsx:143-152,ComposeScreen.tsx:890-902; escape-free page script with a parse test. Name <addr>recipients split into JMAPname/email(#672):ComposeScreen.tsx:69-84,:944-947.- Duplicate recipients within one field rejected on blur/submit:
ComposeScreen.tsx:466-499; suggestion list hides already-added addresses:371-392. - Quote header values HTML-escaped (#482):
src/lib/compose-html.ts:88-100. - Inline images: data-URL placeholder in the editor,
cid:rewrite +disposition:'inline'parts on send, orphaned cids not attached:compose-html.ts:143-161,ComposeScreen.tsx:910-927; removing the chip strips the<img>. - Attachment reminder with word-boundary keyword match and configurable keyword list:
ComposeScreen.tsx:764-791,ComposingSettings.tsx:95-136. - Send blocked while uploads are in flight or errored (web waits instead; equivalent outcome):
ComposeScreen.tsx:502-516. - Body/attachment presence required to send; dirty-check close guard on the header X:
ComposeScreen.tsx:506-541. - Auto-select reply identity from original To/Cc + setting:
ComposeScreen.tsx:431-437,settings-store.ts:131; preselect identity matching the active account (a971d5a1 / "pre-select primary sender identity")::426-429. - Identity picker bottom sheet (
src/components/IdentitySheet.tsx), identity CRUD in settings with email immutable on edit (IdentitySettings.tsx:263-277). - Guarded UUID generator (a92c852c equivalent):
src/lib/uuid.ts:6-20. - Pasted
<script>/<style>stripped before send:ComposeScreen.tsx:913viastripDangerousTags; link insertion rejects non-http/mailto/tel schemes:editor-html.ts:380-384. - Plain-text-only send (text/plain part only) when
plainTextMode:ComposeScreen.tsx:951,src/api/email.ts:798-806. - Blob upload tolerant of both Stalwart upload-response shapes:
src/api/blob.ts:38-61. - Scheduled list filters
undoStatus==='pending'and futuresendAt, cancel viaundoStatus:'canceled'keeping the Sent copy:src/api/email.ts:902-988. - Contact prefill from contact/group detail:
ContactDetailScreen.tsx:159,GroupDetailScreen.tsx:83.
N/A on mobile
- Ctrl/Cmd+Enter send, Ctrl+Shift+Enter schedule,
ttemplate shortcut, Escape close (email-composer.tsx:2428-2468). - Drag-and-drop of files onto the composer and drag-to-reorder / drag-between-fields recipient chips (#593), chip drag preview.
- Pro multi-account From dropdown namespacing (44cdd083), per-pane composer tabs, tab-title logic (
lib/subject-prefix.ts:129-144buildComposeTabTitle), "Keep the fresh-compose tab title clear". - Composer sidebar / toolbar plugin slots and all
emailHooks.*plugin transforms (no plugin runtime in RN). beforeunloadserver autosave (browser lifecycle) — replaced by thebeforeRemoverecommendation above.- Browser
registerProtocolHandler/PWA manifest mailto registration (replaced by OS intent filters above). - Signature "double-click to unlock" atom (Tiptap-specific); the RN contenteditable is always editable.
- Quoted-HTML shadow-island typing detection (#654) — Tiptap/shadow-DOM specific.
- Send through another logged-in account's client for DKIM (#461) — RN is single-active-account; identities always belong to the submitting account.
- Route scheduled sends / identity on reschedule to a shared account (#874, a15af722) — RN does not compose on behalf of shared accounts, but the Scheduled screen lists, cancels, reschedules and edits sends held in shared accounts since
72f48db.