Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,8 @@ export class ComposeInputModule extends ViewModule<ComposeView> {
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');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,24 @@ export class ComposeQuoteModule extends ViewModule<ComposeView> {
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 '<br />' + (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 '<br />' + (footer || '') + (quote || '');
};

public addSignatureToInput = async () => {
Expand Down
23 changes: 23 additions & 0 deletions test/source/mock/google/strategies/send-message-strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:',
Expand Down Expand Up @@ -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 {
Expand Down
38 changes: 38 additions & 0 deletions test/source/tests/compose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,44 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te
})
);

test(
'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) => {
Expand Down
Loading