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
This commit is contained in:
Arhan Jain 2022-10-25 04:30:05 -07:00 committed by GitHub
parent c02898ac66
commit c763b612ec
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 136 additions and 65 deletions

View file

@ -342,7 +342,7 @@ export default class ComposerView extends React.Component<ComposerViewProps, Com
});
};
_isValidDraft = (options: { force?: boolean } = {}) => {
_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<ComposerViewProps, Com
const dialog = require('@electron/remote').dialog;
const { session } = this.props;
const { errors, warnings } = session.validateDraftForSending();
const { recipientErrors, recipientWarnings } = session.validateDraftRecipients();
const { miscErrors, miscWarnings } = session.validateDraftForSending();
if (errors.length > 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;
};

View file

@ -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 (
<div className="container-general">
@ -76,6 +81,9 @@ class PreferencesGeneral extends React.Component<{
<div className="two-columns-flexbox" style={{ paddingTop: 30 }}>
<div style={{ flex: 1 }}>
<SendingSection config={this.props.config} configSchema={this.props.configSchema} />
<div className="btn" onClick={this._onResetEmailsThatIgnoreWarnings} style={{ marginLeft: 0, marginTop:5 }}>
{localized('Reset Emails that Ignore Warnings')}
</div>
</div>
<div style={{ width: 30 }} />
<div style={{ flex: 1 }}>

64
app/package-lock.json generated
View file

@ -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",

View file

@ -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('<signature>');
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.