mirror of
https://github.com/TryGhost/Ghost.git
synced 2025-02-17 23:44:39 -05:00
Updated support email address verification for DMARC changes (#19147)
fixes GRO-71 - Current flow: unchanged - New managed flow: verification required - New managed flow with custom sending domain: only verification required for different domains - Self hosters (feature flag): no verification required
This commit is contained in:
parent
be6916f066
commit
3687feca07
8 changed files with 3545 additions and 13 deletions
|
@ -335,11 +335,11 @@ class NewslettersService {
|
||||||
fromEmail = `no-reply@${toDomain}`;
|
fromEmail = `no-reply@${toDomain}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.emailAddressService.useNewEmailAddresses) {
|
if (this.emailAddressService.service.useNewEmailAddresses) {
|
||||||
// Gone with the old logic: always use the default email address here
|
// Gone with the old logic: always use the default email address here
|
||||||
// We don't need to validate the FROM address, only the to address
|
// We don't need to validate the FROM address, only the to address
|
||||||
// Also because we are not only validating FROM addresses, but also possible REPLY-TO addresses, which we won't send FROM
|
// Also because we are not only validating FROM addresses, but also possible REPLY-TO addresses, which we won't send FROM
|
||||||
fromEmail = this.emailAddressService.defaultFromAddress;
|
fromEmail = this.emailAddressService.service.defaultFromAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {ghostMailer} = this;
|
const {ghostMailer} = this;
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
const _ = require('lodash');
|
const _ = require('lodash');
|
||||||
const tpl = require('@tryghost/tpl');
|
const tpl = require('@tryghost/tpl');
|
||||||
const {NotFoundError, NoPermissionError, BadRequestError, IncorrectUsageError} = require('@tryghost/errors');
|
const {NotFoundError, NoPermissionError, BadRequestError, IncorrectUsageError, ValidationError} = require('@tryghost/errors');
|
||||||
const {obfuscatedSetting, isSecretSetting, hideValueIfSecret} = require('./settings-utils');
|
const {obfuscatedSetting, isSecretSetting, hideValueIfSecret} = require('./settings-utils');
|
||||||
const logging = require('@tryghost/logging');
|
const logging = require('@tryghost/logging');
|
||||||
const MagicLink = require('@tryghost/magic-link');
|
const MagicLink = require('@tryghost/magic-link');
|
||||||
|
@ -9,7 +9,8 @@ const verifyEmailTemplate = require('./emails/verify-email');
|
||||||
const EMAIL_KEYS = ['members_support_address'];
|
const EMAIL_KEYS = ['members_support_address'];
|
||||||
const messages = {
|
const messages = {
|
||||||
problemFindingSetting: 'Problem finding setting: {key}',
|
problemFindingSetting: 'Problem finding setting: {key}',
|
||||||
accessCoreSettingFromExtReq: 'Attempted to access core setting from external request'
|
accessCoreSettingFromExtReq: 'Attempted to access core setting from external request',
|
||||||
|
invalidEmail: 'Invalid email address'
|
||||||
};
|
};
|
||||||
|
|
||||||
class SettingsBREADService {
|
class SettingsBREADService {
|
||||||
|
@ -22,11 +23,13 @@ class SettingsBREADService {
|
||||||
* @param {Object} options.singleUseTokenProvider
|
* @param {Object} options.singleUseTokenProvider
|
||||||
* @param {Object} options.urlUtils
|
* @param {Object} options.urlUtils
|
||||||
* @param {Object} options.labsService - labs service instance
|
* @param {Object} options.labsService - labs service instance
|
||||||
|
* @param {{service: Object}} options.emailAddressService
|
||||||
*/
|
*/
|
||||||
constructor({SettingsModel, settingsCache, labsService, mail, singleUseTokenProvider, urlUtils}) {
|
constructor({SettingsModel, settingsCache, labsService, mail, singleUseTokenProvider, urlUtils, emailAddressService}) {
|
||||||
this.SettingsModel = SettingsModel;
|
this.SettingsModel = SettingsModel;
|
||||||
this.settingsCache = settingsCache;
|
this.settingsCache = settingsCache;
|
||||||
this.labs = labsService;
|
this.labs = labsService;
|
||||||
|
this.emailAddressService = emailAddressService;
|
||||||
|
|
||||||
/* email verification setup */
|
/* email verification setup */
|
||||||
|
|
||||||
|
@ -323,6 +326,14 @@ class SettingsBREADService {
|
||||||
const hasChanged = getSetting(setting).value !== email;
|
const hasChanged = getSetting(setting).value !== email;
|
||||||
|
|
||||||
if (await this.requiresEmailVerification({email, hasChanged})) {
|
if (await this.requiresEmailVerification({email, hasChanged})) {
|
||||||
|
const validated = this.emailAddressService.service.validate(email, 'replyTo');
|
||||||
|
if (!validated.allowed) {
|
||||||
|
throw new ValidationError({
|
||||||
|
message: messages.invalidEmail
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (validated.verificationEmailRequired) {
|
||||||
emailsToVerify.push({email, key});
|
emailsToVerify.push({email, key});
|
||||||
} else {
|
} else {
|
||||||
filteredSettings.push(setting);
|
filteredSettings.push(setting);
|
||||||
|
@ -330,6 +341,9 @@ class SettingsBREADService {
|
||||||
} else {
|
} else {
|
||||||
filteredSettings.push(setting);
|
filteredSettings.push(setting);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
filteredSettings.push(setting);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {filteredSettings, emailsToVerify};
|
return {filteredSettings, emailsToVerify};
|
||||||
|
@ -375,6 +389,13 @@ class SettingsBREADService {
|
||||||
fromEmail = `no-reply@${toDomain}`;
|
fromEmail = `no-reply@${toDomain}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.emailAddressService.service.useNewEmailAddresses) {
|
||||||
|
// Gone with the old logic: always use the default email address here
|
||||||
|
// We don't need to validate the FROM address, only the to address
|
||||||
|
// Also because we are not only validating FROM addresses, but also possible REPLY-TO addresses, which we won't send FROM
|
||||||
|
fromEmail = this.emailAddressService.service.defaultFromAddress;
|
||||||
|
}
|
||||||
|
|
||||||
const {ghostMailer} = this;
|
const {ghostMailer} = this;
|
||||||
|
|
||||||
this.magicLinkService.transporter = {
|
this.magicLinkService.transporter = {
|
||||||
|
|
|
@ -15,6 +15,7 @@ const urlUtils = require('../../../shared/url-utils');
|
||||||
|
|
||||||
const ObjectId = require('bson-objectid').default;
|
const ObjectId = require('bson-objectid').default;
|
||||||
const settingsHelpers = require('../settings-helpers');
|
const settingsHelpers = require('../settings-helpers');
|
||||||
|
const emailAddressService = require('../email-address');
|
||||||
|
|
||||||
const MAGIC_LINK_TOKEN_VALIDITY = 24 * 60 * 60 * 1000;
|
const MAGIC_LINK_TOKEN_VALIDITY = 24 * 60 * 60 * 1000;
|
||||||
const MAGIC_LINK_TOKEN_VALIDITY_AFTER_USAGE = 10 * 60 * 1000;
|
const MAGIC_LINK_TOKEN_VALIDITY_AFTER_USAGE = 10 * 60 * 1000;
|
||||||
|
@ -35,7 +36,8 @@ const getSettingsBREADServiceInstance = () => {
|
||||||
validityPeriodAfterUsage: MAGIC_LINK_TOKEN_VALIDITY_AFTER_USAGE,
|
validityPeriodAfterUsage: MAGIC_LINK_TOKEN_VALIDITY_AFTER_USAGE,
|
||||||
maxUsageCount: MAGIC_LINK_TOKEN_MAX_USAGE_COUNT
|
maxUsageCount: MAGIC_LINK_TOKEN_MAX_USAGE_COUNT
|
||||||
}),
|
}),
|
||||||
urlUtils
|
urlUtils,
|
||||||
|
emailAddressService: emailAddressService
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -3036,7 +3036,7 @@ Object {
|
||||||
"forceTextContent": true,
|
"forceTextContent": true,
|
||||||
"from": "\\"Ghost\\" <noreply@sendingdomain.com>",
|
"from": "\\"Ghost\\" <noreply@sendingdomain.com>",
|
||||||
"generateTextFromHTML": false,
|
"generateTextFromHTML": false,
|
||||||
"replyTo": "noreply@acme.com",
|
"replyTo": null,
|
||||||
"subject": "Verify email address",
|
"subject": "Verify email address",
|
||||||
"to": "hello@acme.com",
|
"to": "hello@acme.com",
|
||||||
}
|
}
|
||||||
|
@ -3913,7 +3913,7 @@ Object {
|
||||||
"forceTextContent": true,
|
"forceTextContent": true,
|
||||||
"from": "\\"Ghost\\" <default@email.com>",
|
"from": "\\"Ghost\\" <default@email.com>",
|
||||||
"generateTextFromHTML": false,
|
"generateTextFromHTML": false,
|
||||||
"replyTo": "noreply@acme.com",
|
"replyTo": null,
|
||||||
"subject": "Verify email address",
|
"subject": "Verify email address",
|
||||||
"to": "hello@acme.com",
|
"to": "hello@acme.com",
|
||||||
}
|
}
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -3,10 +3,10 @@ const sinon = require('sinon');
|
||||||
const logging = require('@tryghost/logging');
|
const logging = require('@tryghost/logging');
|
||||||
const SingleUseTokenProvider = require('../../../core/server/services/members/SingleUseTokenProvider');
|
const SingleUseTokenProvider = require('../../../core/server/services/members/SingleUseTokenProvider');
|
||||||
const settingsCache = require('../../../core/shared/settings-cache');
|
const settingsCache = require('../../../core/shared/settings-cache');
|
||||||
const {agentProvider, fixtureManager, mockManager, matchers} = require('../../utils/e2e-framework');
|
const {agentProvider, fixtureManager, mockManager, matchers, configUtils} = require('../../utils/e2e-framework');
|
||||||
const {stringMatching, anyEtag, anyUuid, anyContentLength, anyContentVersion} = matchers;
|
const {stringMatching, anyEtag, anyUuid, anyContentLength, anyContentVersion} = matchers;
|
||||||
const models = require('../../../core/server/models');
|
const models = require('../../../core/server/models');
|
||||||
const {mockLabsDisabled} = require('../../utils/e2e-framework-mock-manager');
|
const {mockLabsDisabled, mockLabsEnabled} = require('../../utils/e2e-framework-mock-manager');
|
||||||
const {anyErrorId} = matchers;
|
const {anyErrorId} = matchers;
|
||||||
|
|
||||||
const CURRENT_SETTINGS_COUNT = 86;
|
const CURRENT_SETTINGS_COUNT = 86;
|
||||||
|
@ -255,7 +255,7 @@ describe('Settings API', function () {
|
||||||
mockManager.assert.sentEmailCount(0);
|
mockManager.assert.sentEmailCount(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('editing members_support_address triggers email verification flow', async function () {
|
it('[LEGACY] editing members_support_address triggers email verification flow', async function () {
|
||||||
await agent.put('settings/')
|
await agent.put('settings/')
|
||||||
.body({
|
.body({
|
||||||
settings: [{key: 'members_support_address', value: 'support@example.com'}]
|
settings: [{key: 'members_support_address', value: 'support@example.com'}]
|
||||||
|
@ -498,4 +498,197 @@ describe('Settings API', function () {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Managed email without custom sending domain', function () {
|
||||||
|
this.beforeEach(function () {
|
||||||
|
configUtils.set('hostSettings:managedEmail:enabled', true);
|
||||||
|
configUtils.set('hostSettings:managedEmail:sendingDomain', null);
|
||||||
|
configUtils.set('mail:from', 'default@email.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('editing members_support_address triggers email verification flow', async function () {
|
||||||
|
const currentSetting = settingsCache.get('members_support_address');
|
||||||
|
assert(currentSetting !== 'othersupport@example.com', 'This test requires a changed email address');
|
||||||
|
|
||||||
|
await agent.put('settings/')
|
||||||
|
.body({
|
||||||
|
settings: [{key: 'members_support_address', value: 'othersupport@example.com'}]
|
||||||
|
})
|
||||||
|
.expectStatus(200)
|
||||||
|
.matchBodySnapshot({
|
||||||
|
settings: matchSettingsArray(CURRENT_SETTINGS_COUNT)
|
||||||
|
})
|
||||||
|
.matchHeaderSnapshot({
|
||||||
|
etag: anyEtag,
|
||||||
|
// Special rule for this test, as the labs setting changes a lot
|
||||||
|
'content-length': anyContentLength,
|
||||||
|
'content-version': anyContentVersion
|
||||||
|
})
|
||||||
|
.expect(({body}) => {
|
||||||
|
const membersSupportAddress = body.settings.find(setting => setting.key === 'members_support_address');
|
||||||
|
assert.equal(membersSupportAddress.value, currentSetting);
|
||||||
|
|
||||||
|
assert.deepEqual(body.meta, {
|
||||||
|
sent_email_verification: ['members_support_address']
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
mockManager.assert.sentEmailCount(1);
|
||||||
|
mockManager.assert.sentEmail({
|
||||||
|
subject: 'Verify email address',
|
||||||
|
to: 'othersupport@example.com'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('editing members_support_address equaling default does not trigger verification flow', async function () {
|
||||||
|
await agent.put('settings/')
|
||||||
|
.body({
|
||||||
|
settings: [{key: 'members_support_address', value: 'default@email.com'}]
|
||||||
|
})
|
||||||
|
.expectStatus(200)
|
||||||
|
.matchBodySnapshot({
|
||||||
|
settings: matchSettingsArray(CURRENT_SETTINGS_COUNT)
|
||||||
|
})
|
||||||
|
.matchHeaderSnapshot({
|
||||||
|
etag: anyEtag,
|
||||||
|
// Special rule for this test, as the labs setting changes a lot
|
||||||
|
'content-length': anyContentLength,
|
||||||
|
'content-version': anyContentVersion
|
||||||
|
});
|
||||||
|
|
||||||
|
mockManager.assert.sentEmailCount(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Managed email with custom sending domain', function () {
|
||||||
|
this.beforeEach(function () {
|
||||||
|
configUtils.set('hostSettings:managedEmail:enabled', true);
|
||||||
|
configUtils.set('hostSettings:managedEmail:sendingDomain', 'sendingdomain.com');
|
||||||
|
configUtils.set('mail:from', 'default@email.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('editing members_support_address without matching domain triggers email verification flow', async function () {
|
||||||
|
const currentSetting = settingsCache.get('members_support_address');
|
||||||
|
assert(currentSetting !== 'othersupport@example.com', 'This test requires a changed email address');
|
||||||
|
|
||||||
|
await agent.put('settings/')
|
||||||
|
.body({
|
||||||
|
settings: [{key: 'members_support_address', value: 'othersupport@example.com'}]
|
||||||
|
})
|
||||||
|
.expectStatus(200)
|
||||||
|
.matchBodySnapshot({
|
||||||
|
settings: matchSettingsArray(CURRENT_SETTINGS_COUNT)
|
||||||
|
})
|
||||||
|
.matchHeaderSnapshot({
|
||||||
|
etag: anyEtag,
|
||||||
|
// Special rule for this test, as the labs setting changes a lot
|
||||||
|
'content-length': anyContentLength,
|
||||||
|
'content-version': anyContentVersion
|
||||||
|
})
|
||||||
|
.expect(({body}) => {
|
||||||
|
const membersSupportAddress = body.settings.find(setting => setting.key === 'members_support_address');
|
||||||
|
assert.equal(membersSupportAddress.value, currentSetting);
|
||||||
|
|
||||||
|
assert.deepEqual(body.meta, {
|
||||||
|
sent_email_verification: ['members_support_address']
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
mockManager.assert.sentEmailCount(1);
|
||||||
|
mockManager.assert.sentEmail({
|
||||||
|
subject: 'Verify email address',
|
||||||
|
to: 'othersupport@example.com'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('editing members_support_address with matching domain does not trigger email verification flow', async function () {
|
||||||
|
const currentSetting = settingsCache.get('members_support_address');
|
||||||
|
assert(currentSetting !== 'support@sendingdomain.com', 'This test requires a changed email address');
|
||||||
|
|
||||||
|
await agent.put('settings/')
|
||||||
|
.body({
|
||||||
|
settings: [{key: 'members_support_address', value: 'support@sendingdomain.com'}]
|
||||||
|
})
|
||||||
|
.expectStatus(200)
|
||||||
|
.matchBodySnapshot({
|
||||||
|
settings: matchSettingsArray(CURRENT_SETTINGS_COUNT)
|
||||||
|
})
|
||||||
|
.matchHeaderSnapshot({
|
||||||
|
etag: anyEtag,
|
||||||
|
// Special rule for this test, as the labs setting changes a lot
|
||||||
|
'content-length': anyContentLength,
|
||||||
|
'content-version': anyContentVersion
|
||||||
|
});
|
||||||
|
|
||||||
|
mockManager.assert.sentEmailCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('editing members_support_address equaling default does not trigger verification flow', async function () {
|
||||||
|
await agent.put('settings/')
|
||||||
|
.body({
|
||||||
|
settings: [{key: 'members_support_address', value: 'default@email.com'}]
|
||||||
|
})
|
||||||
|
.expectStatus(200)
|
||||||
|
.matchBodySnapshot({
|
||||||
|
settings: matchSettingsArray(CURRENT_SETTINGS_COUNT)
|
||||||
|
})
|
||||||
|
.matchHeaderSnapshot({
|
||||||
|
etag: anyEtag,
|
||||||
|
// Special rule for this test, as the labs setting changes a lot
|
||||||
|
'content-length': anyContentLength,
|
||||||
|
'content-version': anyContentVersion
|
||||||
|
});
|
||||||
|
|
||||||
|
mockManager.assert.sentEmailCount(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Self hoster without managed email', function () {
|
||||||
|
this.beforeEach(function () {
|
||||||
|
configUtils.set('hostSettings:managedEmail:enabled', false);
|
||||||
|
configUtils.set('hostSettings:managedEmail:sendingDomain', '');
|
||||||
|
mockLabsEnabled('newEmailAddresses');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('editing members_support_address does not trigger email verification flow', async function () {
|
||||||
|
const currentSetting = settingsCache.get('members_support_address');
|
||||||
|
assert(currentSetting !== 'support@customdomain.com', 'This test requires a changed email address');
|
||||||
|
|
||||||
|
await agent.put('settings/')
|
||||||
|
.body({
|
||||||
|
settings: [{key: 'members_support_address', value: 'support@customdomain.com'}]
|
||||||
|
})
|
||||||
|
.expectStatus(200)
|
||||||
|
.matchBodySnapshot({
|
||||||
|
settings: matchSettingsArray(CURRENT_SETTINGS_COUNT)
|
||||||
|
})
|
||||||
|
.matchHeaderSnapshot({
|
||||||
|
etag: anyEtag,
|
||||||
|
// Special rule for this test, as the labs setting changes a lot
|
||||||
|
'content-length': anyContentLength,
|
||||||
|
'content-version': anyContentVersion
|
||||||
|
});
|
||||||
|
|
||||||
|
mockManager.assert.sentEmailCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('editing members_support_address equaling default does not trigger verification flow', async function () {
|
||||||
|
await agent.put('settings/')
|
||||||
|
.body({
|
||||||
|
settings: [{key: 'members_support_address', value: 'default@email.com'}]
|
||||||
|
})
|
||||||
|
.expectStatus(200)
|
||||||
|
.matchBodySnapshot({
|
||||||
|
settings: matchSettingsArray(CURRENT_SETTINGS_COUNT)
|
||||||
|
})
|
||||||
|
.matchHeaderSnapshot({
|
||||||
|
etag: anyEtag,
|
||||||
|
// Special rule for this test, as the labs setting changes a lot
|
||||||
|
'content-length': anyContentLength,
|
||||||
|
'content-version': anyContentVersion
|
||||||
|
});
|
||||||
|
|
||||||
|
mockManager.assert.sentEmailCount(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -187,6 +187,16 @@ describe('UNIT > Settings BREAD Service:', function () {
|
||||||
isSet() {
|
isSet() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
emailAddressService: {
|
||||||
|
service: {
|
||||||
|
validate() {
|
||||||
|
return {
|
||||||
|
allowed: true,
|
||||||
|
verificationEmailRequired: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -112,7 +112,7 @@ export class EmailAddressService {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Invalid configuration: don't allow to send from this sending domain
|
// Invalid configuration: don't allow to send from this sending domain
|
||||||
logging.error(`[EmailAddresses] Invalid configuration: cannot send emails from ${preferred.from} when sending domain is ${this.sendingDomain}`);
|
logging.error(`[EmailAddresses] Invalid configuration: cannot send emails from ${preferred.from.address} when sending domain is ${this.sendingDomain}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only allow to send from the configured from address
|
// Only allow to send from the configured from address
|
||||||
|
|
Loading…
Add table
Reference in a new issue