Verified finding(s) from the 2026-07-09 whole-repo multi-agent review (independent finder, then adversarial verifier; verdict CONFIRMED).
Triage: above the cut — fix dispatched immediately; this issue tracks the fix to Done.
app/src/main/kotlin/org/libremail/ui/compose/ComposeViewModel.kt:283 — high
swapHtmlSignature treats a null bodyHtml as 'cleanly strippable' and builds the new HTML body from the signature block alone, dropping the entire plaintext body from the HTML representation; the editor then re-seeds from that signature-only HTML and the user's typed text visibly vanishes (and is overwritten in state/draft on the next keystroke or autosave).
Failure scenario: User types a message in plaintext (bodyHtml == null, e.g. account A has a plain or disabled signature), then switches From to account B whose signature was authored with formatting (e.g. 'Best, Bob'). cleanlyStrippable is true because currentHtml == null, so combined = "" + block.html; normalizedHtml keeps it (the span flips hasFormatting). state.body = typed text + Bob's sig but state.bodyHtml contains only the signature. RichTextBodyField re-seeds from bodyHtml (seedContent prefers HTML) so the typed message disappears from the editor; sending right away emits multipart mail whose text/html part is missing the whole message, and any subsequent keystroke/autosave permanently overwrites the draft with signature-only text. Same path corrupts a mailto:-prefilled body on first open when the default account has a rich signature (init's applySignature runs with currentHtml == null and non-empty body). The existing tests only cover the currentHtml != null branches.
Verifier justification (CONFIRMED): ComposeViewModel.swapHtmlSignature (line 281) treats currentHtml == null as cleanlyStrippable, so line 283 yields combined = "" + block.html: the HTML body becomes the new signature block alone. When the signature carries formatting (SignatureBlock.of concatenates signature.html, so a span makes RichTextHtml.fromHtml(combined).hasFormatting() true), normalizedHtml keeps this sig-only HTML instead of returning null. RichTextEditor.kt lines 120-125 detect the external (body, bodyHtml) change and re-seed via seedContent (line 656-657), which prefers bodyHtml over the plain body — the user's typed text disappears from the editor, and the next emit/autosave overwrites state.body and the persisted draft with signature-only content. Concrete trigger: type a plaintext message (bodyHtml stays null per emit(), line 130) then selectFrom an account whose signature has formatting; the init/mailto path (ComposeViewModel init lines 162-176, applySignature with null bodyHtml and non-empty prefilled body) hits the same defect. Existing tests only cover empty-body-with-rich-sig, plain-to-plain swap, and the currentHtml != null rebuild branch — none cover null currentHtml with a non-empty body and a rich signature.
Fix hint: In swapHtmlSignature, when currentHtml == null, don't start from "": prepend the HTML rendering of the stripped plaintext base body (e.g. RichTextHtml.toHtml(RichTextContent(newBody.removeSuffix(block.plain))) + block.html, or reuse the existing rebuild branch built from newBody) so the typed text survives in the HTML representation; add unit tests for null-bodyHtml + non-empty body + rich signature on both the selectFrom and init/mailto paths.
Verified finding(s) from the 2026-07-09 whole-repo multi-agent review (independent finder, then adversarial verifier; verdict **CONFIRMED**).
**Triage: above the cut — fix dispatched immediately; this issue tracks the fix to Done.**
## `app/src/main/kotlin/org/libremail/ui/compose/ComposeViewModel.kt:283` — high
swapHtmlSignature treats a null bodyHtml as 'cleanly strippable' and builds the new HTML body from the signature block alone, dropping the entire plaintext body from the HTML representation; the editor then re-seeds from that signature-only HTML and the user's typed text visibly vanishes (and is overwritten in state/draft on the next keystroke or autosave).
**Failure scenario:** User types a message in plaintext (bodyHtml == null, e.g. account A has a plain or disabled signature), then switches From to account B whose signature was authored with formatting (e.g. 'Best, <b>Bob</b>'). cleanlyStrippable is true because currentHtml == null, so combined = "" + block.html; normalizedHtml keeps it (the <b> span flips hasFormatting). state.body = typed text + Bob's sig but state.bodyHtml contains only the signature. RichTextBodyField re-seeds from bodyHtml (seedContent prefers HTML) so the typed message disappears from the editor; sending right away emits multipart mail whose text/html part is missing the whole message, and any subsequent keystroke/autosave permanently overwrites the draft with signature-only text. Same path corrupts a mailto:-prefilled body on first open when the default account has a rich signature (init's applySignature runs with currentHtml == null and non-empty body). The existing tests only cover the currentHtml != null branches.
**Verifier justification (CONFIRMED):** ComposeViewModel.swapHtmlSignature (line 281) treats currentHtml == null as cleanlyStrippable, so line 283 yields combined = "" + block.html: the HTML body becomes the new signature block alone. When the signature carries formatting (SignatureBlock.of concatenates signature.html, so a <b> span makes RichTextHtml.fromHtml(combined).hasFormatting() true), normalizedHtml keeps this sig-only HTML instead of returning null. RichTextEditor.kt lines 120-125 detect the external (body, bodyHtml) change and re-seed via seedContent (line 656-657), which prefers bodyHtml over the plain body — the user's typed text disappears from the editor, and the next emit/autosave overwrites state.body and the persisted draft with signature-only content. Concrete trigger: type a plaintext message (bodyHtml stays null per emit(), line 130) then selectFrom an account whose signature has formatting; the init/mailto path (ComposeViewModel init lines 162-176, applySignature with null bodyHtml and non-empty prefilled body) hits the same defect. Existing tests only cover empty-body-with-rich-sig, plain-to-plain swap, and the currentHtml != null rebuild branch — none cover null currentHtml with a non-empty body and a rich signature.
**Defective line:** `val cleanlyStrippable = old.isEmpty() || currentHtml == null || currentHtml.endsWith(old)
val combined = if (cleanlyStrippable) {
(currentHtml?.removeSuffix(old) ?: "") + block.html`
**Fix hint:** In swapHtmlSignature, when currentHtml == null, don't start from "": prepend the HTML rendering of the stripped plaintext base body (e.g. RichTextHtml.toHtml(RichTextContent(newBody.removeSuffix(block.plain))) + block.html, or reuse the existing rebuild branch built from newBody) so the typed text survives in the HTML representation; add unit tests for null-bodyHtml + non-empty body + rich signature on both the selectFrom and init/mailto paths.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Verified finding(s) from the 2026-07-09 whole-repo multi-agent review (independent finder, then adversarial verifier; verdict CONFIRMED).
Triage: above the cut — fix dispatched immediately; this issue tracks the fix to Done.
app/src/main/kotlin/org/libremail/ui/compose/ComposeViewModel.kt:283— highswapHtmlSignature treats a null bodyHtml as 'cleanly strippable' and builds the new HTML body from the signature block alone, dropping the entire plaintext body from the HTML representation; the editor then re-seeds from that signature-only HTML and the user's typed text visibly vanishes (and is overwritten in state/draft on the next keystroke or autosave).
Failure scenario: User types a message in plaintext (bodyHtml == null, e.g. account A has a plain or disabled signature), then switches From to account B whose signature was authored with formatting (e.g. 'Best, Bob'). cleanlyStrippable is true because currentHtml == null, so combined = "" + block.html; normalizedHtml keeps it (the span flips hasFormatting). state.body = typed text + Bob's sig but state.bodyHtml contains only the signature. RichTextBodyField re-seeds from bodyHtml (seedContent prefers HTML) so the typed message disappears from the editor; sending right away emits multipart mail whose text/html part is missing the whole message, and any subsequent keystroke/autosave permanently overwrites the draft with signature-only text. Same path corrupts a mailto:-prefilled body on first open when the default account has a rich signature (init's applySignature runs with currentHtml == null and non-empty body). The existing tests only cover the currentHtml != null branches.
Verifier justification (CONFIRMED): ComposeViewModel.swapHtmlSignature (line 281) treats currentHtml == null as cleanlyStrippable, so line 283 yields combined = "" + block.html: the HTML body becomes the new signature block alone. When the signature carries formatting (SignatureBlock.of concatenates signature.html, so a span makes RichTextHtml.fromHtml(combined).hasFormatting() true), normalizedHtml keeps this sig-only HTML instead of returning null. RichTextEditor.kt lines 120-125 detect the external (body, bodyHtml) change and re-seed via seedContent (line 656-657), which prefers bodyHtml over the plain body — the user's typed text disappears from the editor, and the next emit/autosave overwrites state.body and the persisted draft with signature-only content. Concrete trigger: type a plaintext message (bodyHtml stays null per emit(), line 130) then selectFrom an account whose signature has formatting; the init/mailto path (ComposeViewModel init lines 162-176, applySignature with null bodyHtml and non-empty prefilled body) hits the same defect. Existing tests only cover empty-body-with-rich-sig, plain-to-plain swap, and the currentHtml != null rebuild branch — none cover null currentHtml with a non-empty body and a rich signature.
Defective line:
val cleanlyStrippable = old.isEmpty() || currentHtml == null || currentHtml.endsWith(old) val combined = if (cleanlyStrippable) { (currentHtml?.removeSuffix(old) ?: "") + block.htmlFix hint: In swapHtmlSignature, when currentHtml == null, don't start from "": prepend the HTML rendering of the stripped plaintext base body (e.g. RichTextHtml.toHtml(RichTextContent(newBody.removeSuffix(block.plain))) + block.html, or reuse the existing rebuild branch built from newBody) so the typed text survives in the HTML representation; add unit tests for null-bodyHtml + non-empty body + rich signature on both the selectFrom and init/mailto paths.