From 5ca081794e28c5a86223080d1adfb5698618ac7f Mon Sep 17 00:00:00 2001 From: Ioan Moldovan Date: Mon, 22 Dec 2025 02:37:42 -0500 Subject: [PATCH 1/6] fix: do not display no message dialog when rich text --- .../compose-modules/compose-err-module.ts | 52 +++++++++++++------ test/source/tests/compose.ts | 34 ++++++++++-- 2 files changed, 68 insertions(+), 18 deletions(-) diff --git a/extension/chrome/elements/compose-modules/compose-err-module.ts b/extension/chrome/elements/compose-modules/compose-err-module.ts index 2f7dfab8d1a..b3b95b0c1d9 100644 --- a/extension/chrome/elements/compose-modules/compose-err-module.ts +++ b/extension/chrome/elements/compose-modules/compose-err-module.ts @@ -19,9 +19,9 @@ import { Lang } from '../../../js/common/lang.js'; import linkifyHtml from 'linkifyHtml'; import { MsgUtil } from '../../../js/common/core/crypto/pgp/msg-util.js'; -export class ComposerUserError extends Error {} -class ComposerNotReadyError extends ComposerUserError {} -export class ComposerResetBtnTrigger extends Error {} +export class ComposerUserError extends Error { } +class ComposerNotReadyError extends ComposerUserError { } +export class ComposerResetBtnTrigger extends Error { } export const PUBKEY_LOOKUP_RESULT_FAIL = 'fail'; @@ -147,20 +147,42 @@ export class ComposeErrModule extends ViewModule { throw new ComposerNotReadyError('Still working, please wait.'); }; - public throwIfFormValsInvalid = async ({ subject, plaintext, from }: NewMsgData) => { + public throwIfFormValsInvalid = async ({ subject, plaintext, plainhtml, from }: NewMsgData) => { if (!subject && !(await Ui.modal.confirm('Send without a subject?'))) { throw new ComposerResetBtnTrigger(); } - let footer = await this.view.footerModule.getFooterFromStorage(from.email); - if (footer) { - // format footer the way it would be in outgoing plaintext - footer = Xss.htmlUnescape(Xss.htmlSanitizeAndStripAllTags(this.view.footerModule.createFooterHtml(footer), '\n')).trim(); - } - if ((!plaintext.trim() || (footer && plaintext.trim() === footer.trim())) && !(await Ui.modal.confirm('Send empty message?'))) { + const footer = await this.getFormattedFooter(from.email); + const hasContent = this.hasMessageContent(plaintext, plainhtml, footer); + if (!hasContent && !(await Ui.modal.confirm('Send empty message?'))) { throw new ComposerResetBtnTrigger(); } }; + private hasMessageContent = (plaintext: string, plainhtml: string, footer: string): boolean => { + const textWithoutFooter = plaintext.trim() === footer.trim() ? '' : plaintext.trim(); + if (textWithoutFooter) { + return true; // Has text content + } + // Check for file attachments + if (this.view.attachmentsModule.attachment.hasAttachment()) { + return true; + } + // Check for embedded images in rich text mode + if (this.view.inputModule.isRichText() && plainhtml.includes(' => { + const footer = await this.view.footerModule.getFooterFromStorage(email); + if (!footer) { + return ''; + } + // Format footer the way it would be in outgoing plaintext + return Xss.htmlUnescape(Xss.htmlSanitizeAndStripAllTags(this.view.footerModule.createFooterHtml(footer), '\n')).trim(); + }; + public throwIfEncryptionPasswordInvalidOrDisabled = async ({ subject, pwd }: { subject: string; pwd?: string }) => { const disallowedPasswordMessageTerms = this.view.clientConfiguration.getDisallowPasswordMessagesForTerms(); const disallowedPasswordMessageErrorText = this.view.clientConfiguration.getDisallowPasswordMessagesErrorText(); @@ -175,22 +197,22 @@ export class ComposeErrModule extends ViewModule { if (await this.view.storageModule.isPwdMatchingPassphrase(pwd)) { throw new ComposerUserError( 'Please do not use your private key passphrase as a password for this message.\n\n' + - 'You should come up with some other unique password that you can share with recipient.' + 'You should come up with some other unique password that you can share with recipient.' ); } if (subject.toLowerCase().includes(pwd.toLowerCase())) { throw new ComposerUserError( `Please do not include the password in the email subject. ` + - `Sharing password over email undermines password based encryption.\n\n` + - `You can ask the recipient to also install FlowCrypt, messages between FlowCrypt users don't need a password.` + `Sharing password over email undermines password based encryption.\n\n` + + `You can ask the recipient to also install FlowCrypt, messages between FlowCrypt users don't need a password.` ); } const intro = this.view.S.cached('input_intro').length ? this.view.inputModule.extract('text', 'input_intro') : ''; if (intro.toLowerCase().includes(pwd.toLowerCase())) { throw new ComposerUserError( 'Please do not include the password in the email intro. ' + - `Sharing password over email undermines password based encryption.\n\n` + - `You can ask the recipient to also install FlowCrypt, messages between FlowCrypt users don't need a password.` + `Sharing password over email undermines password based encryption.\n\n` + + `You can ask the recipient to also install FlowCrypt, messages between FlowCrypt users don't need a password.` ); } if (!this.view.pwdOrPubkeyContainerModule.isMessagePasswordStrong(pwd)) { diff --git a/test/source/tests/compose.ts b/test/source/tests/compose.ts index 20b9b1507fc..ae90ac186a3 100644 --- a/test/source/tests/compose.ts +++ b/test/source/tests/compose.ts @@ -1856,6 +1856,35 @@ export const defineComposeTests = (testVariant: TestVariant, testWithBrowser: Te }) ); + test( + 'compose - send only image in rich text mode without empty message dialog', + testWithBrowser(async (t, browser) => { + await BrowserRecipe.setupCommonAcctWithAttester(t, browser, 'compatibility', { + attester: { includeHumanKey: true }, + }); + const imgBase64 = + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAYAAABw4pVUAAAAnElEQVR42u3RAQ0AAAgDIE1u9FvDOahAVzLFGS1ECEKEIEQIQoQgRIgQIQgRghAhCBGCECEIQYgQhAhBiBCECEEIQoQgRAhChCBECEIQIgQhQhAiBCFCEIIQIQgRghAhCBGCEIQIQYgQhAhBiBCEIEQIQoQgRAhChCAEIUIQIgQhQhAiBCEIEYIQIQgRghAhCBEiRAhChCBECEK+W3uw+TnWoJc/AAAAAElFTkSuQmCC'; + const composePage = await ComposePageRecipe.openStandalone(t, browser, 'compatibility'); + // Enable rich text mode and set up recipient + subject + await ComposePageRecipe.fillMsg(composePage, { to: 'human@flowcrypt.com' }, 'Test sending only image', '', { + richtext: true, + }); + // Clear the body completely (including any footer) - issue #3204 is about sending ONLY an image + await composePage.page.evaluate(() => { + $('#input_text').html(''); + }); + // Insert image - this is the only content + await composePage.page.evaluate((src: string) => { + $('[data-test=action-insert-image]').val(src).trigger('click'); + }, imgBase64); + await Util.sleep(0.5); + // Send should work without "Send empty message?" dialog + await ComposePageRecipe.sendAndClose(composePage); + }) + ); + + + test( 'compose - check existing draft not saved without changes', testWithBrowser(async (t, browser) => { @@ -3443,9 +3472,8 @@ const sendTextAndVerifyPresentInSentMsg = async ( text: string, sendingOpt: { encrypt?: boolean; sign?: boolean; richtext?: boolean } = {} ) => { - const subject = `Test Sending ${sendingOpt.sign ? 'Signed' : ''} ${ - sendingOpt.encrypt ? 'Encrypted' : '' - } Message With Test Text ${text} ${Util.lousyRandom()}`; + const subject = `Test Sending ${sendingOpt.sign ? 'Signed' : ''} ${sendingOpt.encrypt ? 'Encrypted' : '' + } Message With Test Text ${text} ${Util.lousyRandom()}`; const composePage = await ComposePageRecipe.openStandalone(t, browser, 'compatibility'); await ComposePageRecipe.fillMsg(composePage, { to: 'human@flowcrypt.com' }, subject, text, sendingOpt); const acctEmail = 'flowcrypt.compatibility@gmail.com'; From fad9010e4924659a6d95781c87a403e4eb7c4ec8 Mon Sep 17 00:00:00 2001 From: Ioan Moldovan Date: Mon, 22 Dec 2025 07:40:19 -0500 Subject: [PATCH 2/6] fix: eslint --- .../compose-modules/compose-err-module.ts | 50 +++++++++---------- 1 file changed, 25 insertions(+), 25 deletions(-) diff --git a/extension/chrome/elements/compose-modules/compose-err-module.ts b/extension/chrome/elements/compose-modules/compose-err-module.ts index b3b95b0c1d9..f94c237d2c7 100644 --- a/extension/chrome/elements/compose-modules/compose-err-module.ts +++ b/extension/chrome/elements/compose-modules/compose-err-module.ts @@ -158,31 +158,6 @@ export class ComposeErrModule extends ViewModule { } }; - private hasMessageContent = (plaintext: string, plainhtml: string, footer: string): boolean => { - const textWithoutFooter = plaintext.trim() === footer.trim() ? '' : plaintext.trim(); - if (textWithoutFooter) { - return true; // Has text content - } - // Check for file attachments - if (this.view.attachmentsModule.attachment.hasAttachment()) { - return true; - } - // Check for embedded images in rich text mode - if (this.view.inputModule.isRichText() && plainhtml.includes(' => { - const footer = await this.view.footerModule.getFooterFromStorage(email); - if (!footer) { - return ''; - } - // Format footer the way it would be in outgoing plaintext - return Xss.htmlUnescape(Xss.htmlSanitizeAndStripAllTags(this.view.footerModule.createFooterHtml(footer), '\n')).trim(); - }; - public throwIfEncryptionPasswordInvalidOrDisabled = async ({ subject, pwd }: { subject: string; pwd?: string }) => { const disallowedPasswordMessageTerms = this.view.clientConfiguration.getDisallowPasswordMessagesForTerms(); const disallowedPasswordMessageErrorText = this.view.clientConfiguration.getDisallowPasswordMessagesErrorText(); @@ -224,4 +199,29 @@ export class ComposeErrModule extends ViewModule { throw new ComposerUserError("Some recipients don't have encryption set up. Please add a password."); } }; + + private hasMessageContent = (plaintext: string, plainhtml: string, footer: string): boolean => { + const textWithoutFooter = plaintext.trim() === footer.trim() ? '' : plaintext.trim(); + if (textWithoutFooter) { + return true; // Has text content + } + // Check for file attachments + if (this.view.attachmentsModule.attachment.hasAttachment()) { + return true; + } + // Check for embedded images in rich text mode + if (this.view.inputModule.isRichText() && plainhtml.includes(' => { + const footer = await this.view.footerModule.getFooterFromStorage(email); + if (!footer) { + return ''; + } + // Format footer the way it would be in outgoing plaintext + return Xss.htmlUnescape(Xss.htmlSanitizeAndStripAllTags(this.view.footerModule.createFooterHtml(footer), '\n')).trim(); + }; } From d73aee983b200186cac0f736e1df4d49cbd949e2 Mon Sep 17 00:00:00 2001 From: Ioan Moldovan Date: Mon, 22 Dec 2025 08:41:09 -0500 Subject: [PATCH 3/6] fix: increase timeout --- .../chrome/elements/pgp_block_modules/pgp-block-print-module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/chrome/elements/pgp_block_modules/pgp-block-print-module.ts b/extension/chrome/elements/pgp_block_modules/pgp-block-print-module.ts index bd1f60194aa..40dbe8af77d 100644 --- a/extension/chrome/elements/pgp_block_modules/pgp-block-print-module.ts +++ b/extension/chrome/elements/pgp_block_modules/pgp-block-print-module.ts @@ -13,7 +13,7 @@ export class PgpBlockViewPrintModule { public printPGPBlock = async () => { // If printMailInfoHtml is not yet prepared, wait briefly to handle race conditions if (!this.printMailInfoHtml) { - for (let i = 0; i < 6 && !this.printMailInfoHtml; i++) { + for (let i = 0; i < 15 && !this.printMailInfoHtml; i++) { await Time.sleep(200); } } From 87550b8b9f2b6f536bdc04955845d7ff94e83fd9 Mon Sep 17 00:00:00 2001 From: Ioan Moldovan Date: Mon, 22 Dec 2025 23:35:42 -0500 Subject: [PATCH 4/6] fix: flaky issue --- extension/chrome/elements/pgp_block.ts | 2 +- .../chrome/elements/pgp_block_modules/pgp-block-print-module.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/extension/chrome/elements/pgp_block.ts b/extension/chrome/elements/pgp_block.ts index e47474f762e..44bfc36b884 100644 --- a/extension/chrome/elements/pgp_block.ts +++ b/extension/chrome/elements/pgp_block.ts @@ -72,7 +72,7 @@ export class PgpBlockView extends View { // to ensure printMailInfo is delivered reliably let resendAttempts = 0; const resendInterval = Catch.setHandledInterval(() => { - if (this.printModule.printMailInfoHtml || resendAttempts >= 6) { + if (this.printModule.printMailInfoHtml || resendAttempts >= 20) { clearInterval(resendInterval); return; } diff --git a/extension/chrome/elements/pgp_block_modules/pgp-block-print-module.ts b/extension/chrome/elements/pgp_block_modules/pgp-block-print-module.ts index 40dbe8af77d..68ded813b7c 100644 --- a/extension/chrome/elements/pgp_block_modules/pgp-block-print-module.ts +++ b/extension/chrome/elements/pgp_block_modules/pgp-block-print-module.ts @@ -13,7 +13,7 @@ export class PgpBlockViewPrintModule { public printPGPBlock = async () => { // If printMailInfoHtml is not yet prepared, wait briefly to handle race conditions if (!this.printMailInfoHtml) { - for (let i = 0; i < 15 && !this.printMailInfoHtml; i++) { + for (let i = 0; i < 30 && !this.printMailInfoHtml; i++) { await Time.sleep(200); } } From a0e760ac110285fd53fbfaaedaf35034a8201416 Mon Sep 17 00:00:00 2001 From: Ioan Moldovan Date: Tue, 23 Dec 2025 02:58:50 -0500 Subject: [PATCH 5/6] fix: flaky issue --- extension/chrome/elements/pgp_block.ts | 7 +++++-- .../elements/pgp_block_modules/pgp-block-print-module.ts | 5 ++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/extension/chrome/elements/pgp_block.ts b/extension/chrome/elements/pgp_block.ts index 44bfc36b884..bf7a61b4d46 100644 --- a/extension/chrome/elements/pgp_block.ts +++ b/extension/chrome/elements/pgp_block.ts @@ -27,7 +27,7 @@ export class PgpBlockView extends View { public readonly quoteModule: PgpBlockViewQuoteModule; public readonly errorModule: PgpBlockViewErrorModule; public readonly renderModule: PgpBlockViewRenderModule; - public readonly printModule = new PgpBlockViewPrintModule(); + public readonly printModule: PgpBlockViewPrintModule; private readonly tabId = BrowserMsg.generateTabId(); private progressOperation?: { text: string; @@ -42,6 +42,9 @@ export class PgpBlockView extends View { this.parentTabId = Assert.urlParamRequire.string(uncheckedUrlParams, 'parentTabId'); this.frameId = Assert.urlParamRequire.string(uncheckedUrlParams, 'frameId'); this.debug = uncheckedUrlParams.debug === true; + this.printModule = new PgpBlockViewPrintModule(() => { + BrowserMsg.send.pgpBlockReady(this, { frameId: this.frameId, messageSender: this.getDest() }); + }); // modules this.attachmentsModule = new PgpBlockViewAttachmentsModule(this); this.quoteModule = new PgpBlockViewQuoteModule(this); @@ -72,7 +75,7 @@ export class PgpBlockView extends View { // to ensure printMailInfo is delivered reliably let resendAttempts = 0; const resendInterval = Catch.setHandledInterval(() => { - if (this.printModule.printMailInfoHtml || resendAttempts >= 20) { + if (this.printModule.printMailInfoHtml || resendAttempts >= 6) { clearInterval(resendInterval); return; } diff --git a/extension/chrome/elements/pgp_block_modules/pgp-block-print-module.ts b/extension/chrome/elements/pgp_block_modules/pgp-block-print-module.ts index 68ded813b7c..c3a8b9fb450 100644 --- a/extension/chrome/elements/pgp_block_modules/pgp-block-print-module.ts +++ b/extension/chrome/elements/pgp_block_modules/pgp-block-print-module.ts @@ -10,10 +10,13 @@ import { Xss } from '../../../js/common/platform/xss.js'; export class PgpBlockViewPrintModule { public printMailInfoHtml: string | undefined; + public constructor(private readonly requestPrintInfo: () => void) { } + public printPGPBlock = async () => { // If printMailInfoHtml is not yet prepared, wait briefly to handle race conditions if (!this.printMailInfoHtml) { - for (let i = 0; i < 30 && !this.printMailInfoHtml; i++) { + this.requestPrintInfo(); + for (let i = 0; i < 6 && !this.printMailInfoHtml; i++) { await Time.sleep(200); } } From d3c677853e9fb9f657d105b4477983f9720e1bb6 Mon Sep 17 00:00:00 2001 From: Ioan Moldovan Date: Tue, 23 Dec 2025 07:59:56 -0500 Subject: [PATCH 6/6] fix: flaky issue --- .../chrome/elements/pgp_block_modules/pgp-block-print-module.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/extension/chrome/elements/pgp_block_modules/pgp-block-print-module.ts b/extension/chrome/elements/pgp_block_modules/pgp-block-print-module.ts index c3a8b9fb450..500af71b716 100644 --- a/extension/chrome/elements/pgp_block_modules/pgp-block-print-module.ts +++ b/extension/chrome/elements/pgp_block_modules/pgp-block-print-module.ts @@ -16,7 +16,7 @@ export class PgpBlockViewPrintModule { // If printMailInfoHtml is not yet prepared, wait briefly to handle race conditions if (!this.printMailInfoHtml) { this.requestPrintInfo(); - for (let i = 0; i < 6 && !this.printMailInfoHtml; i++) { + for (let i = 0; i < 25 && !this.printMailInfoHtml; i++) { await Time.sleep(200); } }