From feebc0881faa2b85500759d50629e4ed82cfc558 Mon Sep 17 00:00:00 2001 From: Ioan Moldovan Date: Mon, 12 Jan 2026 16:14:23 -0800 Subject: [PATCH 1/2] fix: signature accidentally attaches to email --- .../compose-modules/compose-input-module.ts | 3 +- .../compose-modules/compose-quote-module.ts | 22 ++++++++--- .../strategies/send-message-strategy.ts | 23 +++++++++++ test/source/tests/compose.ts | 38 +++++++++++++++++++ 4 files changed, 80 insertions(+), 6 deletions(-) diff --git a/extension/chrome/elements/compose-modules/compose-input-module.ts b/extension/chrome/elements/compose-modules/compose-input-module.ts index 215b8adadf8..929e79e63f3 100644 --- a/extension/chrome/elements/compose-modules/compose-input-module.ts +++ b/extension/chrome/elements/compose-modules/compose-input-module.ts @@ -67,7 +67,8 @@ export class ComposeInputModule extends ViewModule { public extract = (type: 'text' | 'html', elSel: 'input_text' | 'input_intro', flag?: 'SKIP-ADDONS') => { let html = this.view.S.cached(elSel)[0].innerHTML; if (elSel === 'input_text' && flag !== 'SKIP-ADDONS') { - html += this.view.quoteModule.getTripleDotSanitizedFormattedHtmlContent(); + // skipFooter: true because footer is already rendered in input when user changes sender alias (issue #6135) + html += this.view.quoteModule.getTripleDotSanitizedFormattedHtmlContent(true); } if (type === 'html') { return Xss.htmlSanitizeKeepBasicTags(html, 'IMG-KEEP'); diff --git a/extension/chrome/elements/compose-modules/compose-quote-module.ts b/extension/chrome/elements/compose-modules/compose-quote-module.ts index 9b03f12725a..97c5446294e 100644 --- a/extension/chrome/elements/compose-modules/compose-quote-module.ts +++ b/extension/chrome/elements/compose-modules/compose-quote-module.ts @@ -23,12 +23,24 @@ export class ComposeQuoteModule extends ViewModule { public tripleDotSanitizedHtmlContent: { quote: string | undefined; footer: string | undefined } | undefined; public messageToReplyOrForward: MessageToReplyOrForward | undefined; - public getTripleDotSanitizedFormattedHtmlContent = (): string => { - // email content order: [myMsg, myFooter, theirQuote] - if (this.tripleDotSanitizedHtmlContent) { - return '
' + (this.tripleDotSanitizedHtmlContent.footer || '') + (this.tripleDotSanitizedHtmlContent.quote || ''); + /** + * Returns the formatted HTML content for the triple-dot expandable section. + * Email content order: [myMsg, myFooter, theirQuote] + * + * @param skipFooter - If true, skip including the footer when there's no quote. + * Used to prevent duplicate signatures when footer was already + * rendered in input (issue #6135). + */ + public getTripleDotSanitizedFormattedHtmlContent = (skipFooter = false): string => { + if (!this.tripleDotSanitizedHtmlContent) { + return ''; } - return ''; + const { footer, quote } = this.tripleDotSanitizedHtmlContent; + // When skipFooter is true and there's no quote, return empty to avoid duplicate footer + if (skipFooter && !quote) { + return ''; + } + return '
' + (footer || '') + (quote || ''); }; public addSignatureToInput = async () => { diff --git a/test/source/mock/google/strategies/send-message-strategy.ts b/test/source/mock/google/strategies/send-message-strategy.ts index ba42a0f678c..2c80c1d497d 100644 --- a/test/source/mock/google/strategies/send-message-strategy.ts +++ b/test/source/mock/google/strategies/send-message-strategy.ts @@ -318,11 +318,32 @@ class PlainTextMessageTestStrategy implements ITestMsgStrategy { }; } + class NoopTestStrategy implements ITestMsgStrategy { // eslint-disable-next-line @typescript-eslint/no-empty-function public test = async () => {}; } +class SignatureRemovalTestStrategy implements ITestMsgStrategy { + private readonly signature = 'Test alias signature'; + + public test = async (parseResult: ParseMsgResult) => { + const mimeMsg = parseResult.mimeMsg; + const kisWithPp = await Config.getKeyInfo(['flowcrypt.compatibility.1pp1', 'flowcrypt.compatibility.2pp1']); + const encryptedData = mimeMsg.text!; + const decrypted = await MsgUtil.decryptMessage({ kisWithPp, encryptedData, verificationPubs: [] }); + expect(decrypted.success).to.be.true; + // We expect the body to contain the message text but NOT the signature + const body = decrypted.content?.toUtfStr() || ''; + if (!body.includes('Message without signature')) { + throw new HttpClientErr(`Error: Msg Text doesn't contain expected body. Current: '${body}'`); + } + if (body.includes(this.signature)) { + throw new HttpClientErr(`Error: Msg Text contains signature that should have been removed. Current: '${body}'`); + } + }; +} + class IncludeQuotedPartTestStrategy implements ITestMsgStrategy { private readonly quotedContent: string = [ 'On 2019-06-14 at 23:24, flowcrypt.compatibility@gmail.com wrote:', @@ -484,6 +505,8 @@ export class TestBySubjectStrategyContext { this.strategy = new SaveMessageInStorageStrategy(); } else if (subject.includes('Test Sending Message With Attachment Which Contains Emoji in Filename')) { this.strategy = new SaveMessageInStorageStrategy(); + } else if (subject.includes('Message without signature')) { + this.strategy = new SignatureRemovalTestStrategy(); } else if (subject.includes('Re: FROM: flowcrypt.compatibility@gmail.com, TO: flowcrypt.compatibility@gmail.com + vladimir@flowcrypt.com')) { this.strategy = new NoopTestStrategy(); } else { diff --git a/test/source/tests/compose.ts b/test/source/tests/compose.ts index 20b9b1507fc..b772e664711 100644 --- a/test/source/tests/compose.ts +++ b/test/source/tests/compose.ts @@ -104,6 +104,44 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te }) ); + test.only( + 'compose - signature must not reappear after manual removal (issue #6135)', + testWithBrowser(async (t, browser) => { + const primarySignature = 'Test primary signature'; + const aliasSignature = 'Test alias signature'; + const acctAliases = [{ + ...flowcryptCompatibilityAliasList[0], + signature: aliasSignature, + }]; + await BrowserRecipe.setupCommonAcctWithAttester(t, browser, 'compatibility', { + google: { acctAliases, acctPrimarySignature: primarySignature }, + attester: { includeHumanKey: true }, + }); + + const composePage = await ComposePageRecipe.openStandalone(t, browser, 'compatibility'); + // Select alias email to trigger signature change + await ComposePageRecipe.selectFromOption(composePage, 'flowcryptcompatibility@gmail.com'); + let emailBody = await composePage.read('@input-body'); + expect(emailBody).to.contain(aliasSignature); + + // meaningful user change: delete the signature + // clearInput is not available, so we select all and delete + await composePage.waitAndClick('@input-body'); + await composePage.page.keyboard.down('Control'); + await composePage.page.keyboard.press('a'); + await composePage.page.keyboard.up('Control'); + await composePage.page.keyboard.press('Backspace'); + await ComposePageRecipe.fillMsg(composePage, { to: 'human@flowcrypt.com' }, 'Message without signature', 'Message without signature'); + + // Verify signature is gone before sending + emailBody = await composePage.read('@input-body'); + expect(emailBody).to.not.contain(aliasSignature); + + // Send + await ComposePageRecipe.sendAndClose(composePage); + }) + ); + test( 'compose - check for sender [flowcrypt.compatibility@gmail.com] from a password-protected email', testWithBrowser(async (t, browser) => { From cdfc3d135cc1ff06ad28b4bbecedae3207eaeffc Mon Sep 17 00:00:00 2001 From: Ioan Moldovan Date: Mon, 12 Jan 2026 18:12:49 -0800 Subject: [PATCH 2/2] fix: only --- test/source/tests/compose.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/source/tests/compose.ts b/test/source/tests/compose.ts index b772e664711..e927a18d8a6 100644 --- a/test/source/tests/compose.ts +++ b/test/source/tests/compose.ts @@ -104,7 +104,7 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te }) ); - test.only( + test( 'compose - signature must not reappear after manual removal (issue #6135)', testWithBrowser(async (t, browser) => { const primarySignature = 'Test primary signature';