From c763b612ecdb2e9f4df8dc04ccb186c6b9f952bd Mon Sep 17 00:00:00 2001 From: Arhan Jain Date: Tue, 25 Oct 2022 04:30:05 -0700 Subject: [PATCH] Making composer recipient name warnings optional (#2420) * recipient warnings and errors split into a separate step for drafts * checking and updating recipient warning blacklist through localstorage * add option to reset emails that ignore warning in preferences --- .../composer/lib/composer-view.tsx | 44 ++++++++-- .../lib/tabs/preferences-general.tsx | 8 ++ app/package-lock.json | 64 +++++++------- app/src/flux/stores/draft-editing-session.ts | 85 +++++++++++++------ 4 files changed, 136 insertions(+), 65 deletions(-) diff --git a/app/internal_packages/composer/lib/composer-view.tsx b/app/internal_packages/composer/lib/composer-view.tsx index f38840090..ff8ca1390 100644 --- a/app/internal_packages/composer/lib/composer-view.tsx +++ b/app/internal_packages/composer/lib/composer-view.tsx @@ -342,7 +342,7 @@ export default class ComposerView extends React.Component { + _isValidDraft = (options: { forceRecipientWarnings?: boolean, forceMiscWarnings?: boolean } = {}) => { // We need to check the `DraftStore` because the `DraftStore` is // immediately and synchronously updated as soon as this function // fires. Since `setState` is asynchronous, if we used that as our only @@ -353,31 +353,61 @@ export default class ComposerView extends React.Component 0) { + // Display errors + if (recipientErrors.length > 0) { dialog.showMessageBox({ type: 'warning', buttons: [localized('Edit Message'), localized('Cancel')], message: localized('Cannot send message'), - detail: errors[0], + detail: recipientErrors[0], + }); + return false; + } + if (miscErrors.length > 0) { + dialog.showMessageBox({ + type: 'warning', + buttons: [localized('Edit Message'), localized('Cancel')], + message: localized('Cannot send message'), + detail: miscErrors[0], }); return false; } - if (warnings.length > 0 && !options.force) { + // Display warnings + if (recipientWarnings.length > 0 && !options.forceRecipientWarnings) { + const response = dialog.showMessageBoxSync({ + type: 'warning', + buttons: [localized('Send Anyway'), localized('Send & Ignore Warnings For This Email'), localized('Cancel')], + message: localized('Are you sure?'), + detail: recipientWarnings.join('. '), + }); + if (response === 0) { + // response is button array index + return this._isValidDraft({ forceRecipientWarnings: true, forceMiscWarnings: options.forceMiscWarnings }); + } else if (response === 1) { + // Send & Ignore Future Warnings for Recipient Email + session.addRecipientsToWarningBlacklist() + return this._isValidDraft({ forceRecipientWarnings: true, forceMiscWarnings: options.forceMiscWarnings }); + } + return false; + } + if (miscWarnings.length > 0 && !options.forceMiscWarnings) { const response = dialog.showMessageBoxSync({ type: 'warning', buttons: [localized('Send Anyway'), localized('Cancel')], message: localized('Are you sure?'), - detail: warnings.join('. '), + detail: miscWarnings.join('. '), }); if (response === 0) { // response is button array index - return this._isValidDraft({ force: true }); + return this._isValidDraft({ forceRecipientWarnings: options.forceRecipientWarnings, forceMiscWarnings: true }); } return false; } + return true; }; diff --git a/app/internal_packages/preferences/lib/tabs/preferences-general.tsx b/app/internal_packages/preferences/lib/tabs/preferences-general.tsx index 37d185ff5..49ce24991 100644 --- a/app/internal_packages/preferences/lib/tabs/preferences-general.tsx +++ b/app/internal_packages/preferences/lib/tabs/preferences-general.tsx @@ -27,6 +27,10 @@ class PreferencesGeneral extends React.Component<{ app.quit(); }; + _onResetEmailsThatIgnoreWarnings = () => { + localStorage.removeItem("recipientWarningBlacklist"); + } + _onResetAccountsAndSettings = () => { const chosen = require('@electron/remote').dialog.showMessageBoxSync({ type: 'info', @@ -55,6 +59,7 @@ class PreferencesGeneral extends React.Component<{ ipc.send('command', 'application:reset-database', {}); }; + render() { return (
@@ -76,6 +81,9 @@ class PreferencesGeneral extends React.Component<{
+
+ {localized('Reset Emails that Ignore Warnings')} +
diff --git a/app/package-lock.json b/app/package-lock.json index 1203ad961..c000f4f6c 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -1,12 +1,12 @@ { "name": "mailspring", - "version": "1.10.4", + "version": "1.10.5", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "mailspring", - "version": "1.10.4", + "version": "1.10.5", "license": "GPL-3.0", "dependencies": { "@bengotow/slate-edit-list": "github:bengotow/slate-edit-list#b868e108", @@ -460,9 +460,9 @@ } }, "node_modules/array.prototype.flat/node_modules/object.assign": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.3.tgz", - "integrity": "sha512-ZFJnX3zltyjcYJL0RoCJuzb+11zWGyaDbjgxZbdV7rFEcHQuYxrZqhow67aA7xpes6LhojyFDaBKAFfogQrikA==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -1667,9 +1667,9 @@ } }, "node_modules/enzyme/node_modules/object.assign": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.3.tgz", - "integrity": "sha512-ZFJnX3zltyjcYJL0RoCJuzb+11zWGyaDbjgxZbdV7rFEcHQuYxrZqhow67aA7xpes6LhojyFDaBKAFfogQrikA==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -4821,9 +4821,9 @@ } }, "node_modules/string.prototype.trim/node_modules/object.assign": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.3.tgz", - "integrity": "sha512-ZFJnX3zltyjcYJL0RoCJuzb+11zWGyaDbjgxZbdV7rFEcHQuYxrZqhow67aA7xpes6LhojyFDaBKAFfogQrikA==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -4972,9 +4972,9 @@ } }, "node_modules/string.prototype.trimend/node_modules/object.assign": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.3.tgz", - "integrity": "sha512-ZFJnX3zltyjcYJL0RoCJuzb+11zWGyaDbjgxZbdV7rFEcHQuYxrZqhow67aA7xpes6LhojyFDaBKAFfogQrikA==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -5123,9 +5123,9 @@ } }, "node_modules/string.prototype.trimstart/node_modules/object.assign": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.3.tgz", - "integrity": "sha512-ZFJnX3zltyjcYJL0RoCJuzb+11zWGyaDbjgxZbdV7rFEcHQuYxrZqhow67aA7xpes6LhojyFDaBKAFfogQrikA==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", "dependencies": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -6001,9 +6001,9 @@ } }, "object.assign": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.3.tgz", - "integrity": "sha512-ZFJnX3zltyjcYJL0RoCJuzb+11zWGyaDbjgxZbdV7rFEcHQuYxrZqhow67aA7xpes6LhojyFDaBKAFfogQrikA==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", "requires": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -6918,9 +6918,9 @@ } }, "object.assign": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.3.tgz", - "integrity": "sha512-ZFJnX3zltyjcYJL0RoCJuzb+11zWGyaDbjgxZbdV7rFEcHQuYxrZqhow67aA7xpes6LhojyFDaBKAFfogQrikA==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", "requires": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -9554,9 +9554,9 @@ } }, "object.assign": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.3.tgz", - "integrity": "sha512-ZFJnX3zltyjcYJL0RoCJuzb+11zWGyaDbjgxZbdV7rFEcHQuYxrZqhow67aA7xpes6LhojyFDaBKAFfogQrikA==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", "requires": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -9656,9 +9656,9 @@ } }, "object.assign": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.3.tgz", - "integrity": "sha512-ZFJnX3zltyjcYJL0RoCJuzb+11zWGyaDbjgxZbdV7rFEcHQuYxrZqhow67aA7xpes6LhojyFDaBKAFfogQrikA==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", "requires": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", @@ -9758,9 +9758,9 @@ } }, "object.assign": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.3.tgz", - "integrity": "sha512-ZFJnX3zltyjcYJL0RoCJuzb+11zWGyaDbjgxZbdV7rFEcHQuYxrZqhow67aA7xpes6LhojyFDaBKAFfogQrikA==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.4.tgz", + "integrity": "sha512-1mxKf0e58bvyjSCtKYY4sRe9itRk3PJpquJOjeIkz885CczcI4IvJJDLPS72oowuSh+pBxUFROpX+TU++hxhZQ==", "requires": { "call-bind": "^1.0.2", "define-properties": "^1.1.4", diff --git a/app/src/flux/stores/draft-editing-session.ts b/app/src/flux/stores/draft-editing-session.ts index b5f0515a3..a9668ef1e 100644 --- a/app/src/flux/stores/draft-editing-session.ts +++ b/app/src/flux/stores/draft-editing-session.ts @@ -246,22 +246,55 @@ export class DraftEditingSession extends MailspringStore { } validateDraftForSending() { - const warnings = []; - const errors = []; - const allRecipients = [...this._draft.to, ...this._draft.cc, ...this._draft.bcc]; + const miscWarnings = []; + const miscErrors = []; const hasAttachment = this._draft.files && this._draft.files.length > 0; + if (this._draft.subject.length === 0) { + miscWarnings.push(localized('The subject field is blank.')); + } + + let cleaned = QuotedHTMLTransformer.removeQuotedHTML(this._draft.body.trim()); + const sigIndex = cleaned.search(RegExpUtils.mailspringSignatureRegex()); + cleaned = sigIndex > -1 ? cleaned.substr(0, sigIndex) : cleaned; + + const signatureIndex = cleaned.indexOf(''); + if (signatureIndex !== -1) { + cleaned = cleaned.substr(0, signatureIndex - 1); + } + + if (cleaned.toLowerCase().includes('attach') && !hasAttachment) { + miscWarnings.push(localized('The message mentions an attachment but none are attached.')); + } + + // Check third party warnings added via Composer extensions + for (const extension of ComposerExtensionRegistry.extensions()) { + if (!extension.warningsForSending) { + continue; + } + miscWarnings.push(...extension.warningsForSending({ draft: this._draft })); + } + + return { miscErrors, miscWarnings }; + } + + + validateDraftRecipients() { + const recipientWarnings = []; + const recipientErrors = []; + const allRecipients = [...this._draft.to, ...this._draft.cc, ...this._draft.bcc]; + const allNames = [...Utils.commonlyCapitalizedSalutations]; let unnamedRecipientPresent = false; for (const contact of allRecipients) { if (!ContactStore.isValidContact(contact)) { - errors.push( + recipientErrors.push( `${contact.email} is not a valid email address - please remove or edit it before sending.` ); } const name = contact.fullName(); - if (name && name.length && name !== contact.email) { + if (name && name.length && name !== contact.email && !this.checkRecipientInWarningBlacklist(contact.email)) { allNames.push(name.toLowerCase()); // ben gotow allNames.push(...name.toLowerCase().split(' ')); // ben, gotow allNames.push(...name.toLowerCase().split('-')); // anne-marie => anne, marie @@ -276,17 +309,13 @@ export class DraftEditingSession extends MailspringStore { } if (allRecipients.length === 0) { - errors.push( + recipientErrors.push( localized('You need to provide one or more recipients before sending the message.') ); } - if (errors.length > 0) { - return { errors, warnings }; - } - - if (this._draft.subject.length === 0) { - warnings.push(localized('The subject field is blank.')); + if (recipientErrors.length > 0) { + return { errors: recipientErrors, warnings: recipientWarnings }; } let cleaned = QuotedHTMLTransformer.removeQuotedHTML(this._draft.body.trim()); @@ -298,10 +327,6 @@ export class DraftEditingSession extends MailspringStore { cleaned = cleaned.substr(0, signatureIndex - 1); } - if (cleaned.toLowerCase().includes('attach') && !hasAttachment) { - warnings.push(localized('The message mentions an attachment but none are attached.')); - } - if (!unnamedRecipientPresent) { // https://www.regexpal.com/?fam=99334 // note: requires that the name is capitalized, to avoid catching "Hey guys" @@ -312,7 +337,7 @@ export class DraftEditingSession extends MailspringStore { if (salutation.endsWith('-')) salutation = salutation.substr(0, salutation.length - 1); if (!allNames.find(n => n === salutation || (n.length > 1 && salutation.includes(n)))) { - warnings.push( + recipientWarnings.push( localized( `The message is addressed to a name that doesn't appear to be a recipient ("%@")`, match[1] @@ -322,17 +347,25 @@ export class DraftEditingSession extends MailspringStore { } } - // Check third party warnings added via Composer extensions - for (const extension of ComposerExtensionRegistry.extensions()) { - if (!extension.warningsForSending) { - continue; - } - warnings.push(...extension.warningsForSending({ draft: this._draft })); - } - - return { errors, warnings }; + return { recipientErrors, recipientWarnings }; } + addRecipientsToWarningBlacklist() { + const allRecipients = [...this._draft.to, ...this._draft.cc, ...this._draft.bcc]; + const allRecipientEmails = allRecipients.map(contact => contact.email); + let blacklist = JSON.parse(localStorage.getItem("recipientWarningBlacklist")); + if (blacklist === null) blacklist = []; + blacklist.push(...allRecipientEmails); + localStorage.setItem("recipientWarningBlacklist", JSON.stringify(blacklist)); + } + + checkRecipientInWarningBlacklist(email) { + const blacklist = JSON.parse(localStorage.getItem("recipientWarningBlacklist")); + if (blacklist && blacklist.includes(email)) return true; + return false; + } + + // This function makes sure the draft is attached to a valid account, and changes // it's accountId if the from address does not match the account for the from // address.