Skip to content
Merged
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
52 changes: 37 additions & 15 deletions extension/chrome/elements/compose-modules/compose-err-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -147,16 +147,13 @@ export class ComposeErrModule extends ViewModule<ComposeView> {
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() || 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();
}
};
Expand All @@ -175,22 +172,22 @@ export class ComposeErrModule extends ViewModule<ComposeView> {
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)) {
Expand All @@ -202,4 +199,29 @@ export class ComposeErrModule extends ViewModule<ComposeView> {
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('<img')) {
return true;
}
return false; // No content at all
};

private getFormattedFooter = async (email: string): Promise<string> => {
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();
};
}
2 changes: 1 addition & 1 deletion extension/chrome/elements/pgp_block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,4 +158,4 @@ export class PgpBlockView extends View {
};
}

View.run(PgpBlockView);
View.run(PgpBlockView);
34 changes: 31 additions & 3 deletions test/source/tests/compose.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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';
Expand Down
Loading