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
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@ const errors = require('@tryghost/errors');
const logging = require('@tryghost/logging');
const tpl = require('@tryghost/tpl');
const models = require('../../models');
const emailAddressService = require('../../services/email-address');
const {DEFAULT_EMAIL_DESIGN_SETTING_SLUG} = require('../../services/member-welcome-emails/constants');
const {validateEmailSenderFields} = require('./utils/validate-email-sender-fields');

const messages = {
defaultDesignNotFound: 'Default automated email design setting not found.'
Expand Down Expand Up @@ -69,6 +71,8 @@ const controller = {
message: 'The slug field cannot be modified.'
});
}
emailAddressService.init();
validateEmailSenderFields(emailAddressService.service, data);

const defaultDesign = await resolveDefaultDesign(frame.options);

Expand Down
40 changes: 31 additions & 9 deletions ghost/core/core/server/api/endpoints/automated-emails.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ const tpl = require('@tryghost/tpl');
const errors = require('@tryghost/errors');
const models = require('../../models');
const memberWelcomeEmailService = require('../../services/member-welcome-emails/service');
const emailAddressService = require('../../services/email-address');
const {DEFAULT_EMAIL_DESIGN_SETTING_SLUG} = require('../../services/member-welcome-emails/constants');
const {validateEmailSenderFields} = require('./utils/validate-email-sender-fields');

const messages = {
automatedEmailNotFound: 'Automated email not found.'
Expand Down Expand Up @@ -36,8 +38,8 @@ function flattenAutomation(automation, email = automation.related('welcomeEmailA
return result;
}

async function getDefaultEmailDesignSettings() {
const designSettings = await models.EmailDesignSetting.findOne({slug: DEFAULT_EMAIL_DESIGN_SETTING_SLUG});
async function getDefaultEmailDesignSettings(options = {}) {
const designSettings = await models.EmailDesignSetting.findOne({slug: DEFAULT_EMAIL_DESIGN_SETTING_SLUG}, options);

if (!designSettings?.id) {
throw new errors.NotFoundError({
Expand Down Expand Up @@ -70,6 +72,10 @@ async function updateEmailDesignSenderFields(email, senderData, options) {
return models.EmailDesignSetting.findOne({id}, options);
}

function getChangedSenderData(senderData, designSettings) {
return _.pickBy(senderData, (value, field) => value !== designSettings?.get(field));
}

/** @type {import('@tryghost/api-framework').Controller} */
const controller = {
docName: 'automated_emails',
Expand Down Expand Up @@ -139,6 +145,8 @@ const controller = {
const emailData = _.pick(data, EMAIL_FIELDS);
const senderData = _.pick(data, SENDER_FIELDS);
const automationData = _.pick(data, AUTOMATION_FIELDS);
emailAddressService.init();
validateEmailSenderFields(emailAddressService.service, senderData);

return models.Base.transaction(async (transacting) => {
const automation = await models.Automation.add(automationData, {...frame.options, transacting});
Expand Down Expand Up @@ -190,20 +198,30 @@ const controller = {
});
}
let email = automation.related('welcomeEmailAutomatedEmail');
const hasEmailContent = Boolean(email.id);
const designSettings = hasEmailContent ? email.related('emailDesignSetting') : null;
const changedSenderData = hasEmailContent ? getChangedSenderData(senderData, designSettings) : {};

emailAddressService.init();
validateEmailSenderFields(emailAddressService.service, changedSenderData);

if (Object.keys(emailData).length > 0) {
if (hasEmailContent && Object.keys(emailData).length > 0) {
email = await models.WelcomeEmailAutomatedEmail.edit(emailData, {
...frame.options,
transacting,
id: email.id
});
}

const designSettings = await updateEmailDesignSenderFields(
email,
senderData,
{...frame.options, transacting}
);
let updatedDesignSettings = designSettings;

if (hasEmailContent) {
updatedDesignSettings = await updateEmailDesignSenderFields(
email,
changedSenderData,
{...frame.options, transacting}
);
}

if (Object.keys(automationData).length > 0) {
automation = await models.Automation.edit(automationData, {
Expand All @@ -212,7 +230,11 @@ const controller = {
});
}

return flattenAutomation(automation, email, designSettings);
if (!hasEmailContent) {
updatedDesignSettings = await getDefaultEmailDesignSettings({...frame.options, transacting});
}

return flattenAutomation(automation, email, updatedDesignSettings);
});
}
},
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import errors from '@tryghost/errors';
import type {EmailAddressService} from '../../../services/email-address/email-address-service';
import type {ReadonlyDeep} from 'type-fest';

const EMAIL_VALIDATIONS = [
{
field: 'sender_email',
addressType: 'from'
},
{
field: 'sender_reply_to',
addressType: 'replyTo'
}
] as const;

export function validateEmailSenderFields(
emailAddressService: Pick<EmailAddressService, 'validate'>,
data: ReadonlyDeep<{
sender_email?: string;
sender_reply_to?: string;
}>
): void {
for (const {field, addressType} of EMAIL_VALIDATIONS) {
const value = data[field];
if (!value) {
continue;
}

const validated = emailAddressService.validate(value, addressType);
if (!validated.allowed) {
throw new errors.ValidationError({
message: `You cannot set ${field} to ${value}`
});
}

if (validated.verificationEmailRequired) {
Comment thread
EvanHahn marked this conversation as resolved.
throw new errors.ValidationError({
message: `You cannot set ${field} to ${value} without verification`
});
}
}
}
45 changes: 45 additions & 0 deletions ghost/core/test/e2e-api/admin/automated-email-design.test.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
const assert = require('node:assert/strict');
const {agentProvider, fixtureManager, matchers} = require('../../utils/e2e-framework');
const {anyContentVersion, anyObjectId, anyISODateTime, anyErrorId, anyEtag} = matchers;
const sinon = require('sinon');
const emailAddressService = require('../../../core/server/services/email-address');
const models = require('../../../core/server/models');

const matchEmailDesignSetting = {
id: anyObjectId,
Expand Down Expand Up @@ -107,6 +110,48 @@ describe('Automated Email Design API', function () {
etag: anyEtag
});
});

it('Rejects disallowed sender email', async function () {
await models.Base.knex('email_design_settings')
.where('slug', 'default-automated-email')
.update({
background_color: 'light',
sender_name: 'Existing Sender',
sender_email: 'existing@example.com',
sender_reply_to: 'existing-reply@example.com'
});

emailAddressService.init();
const validateStub = sinon.stub(emailAddressService.service, 'validate')
.returns({allowed: false, verificationEmailRequired: false});

try {
await agent
.put('automated_emails/design')
.body({automated_email_design: [{
background_color: 'dark',
sender_name: 'Custom Sender',
sender_email: 'sender@example.com',
sender_reply_to: 'reply@example.com'
}]})
.expectStatus(422);

sinon.assert.calledOnceWithExactly(validateStub, 'sender@example.com', 'from');

const designSettings = await models.Base.knex('email_design_settings')
.where('slug', 'default-automated-email')
.first('background_color', 'sender_name', 'sender_email', 'sender_reply_to');

assert.deepEqual(designSettings, {
background_color: 'light',
sender_name: 'Existing Sender',
sender_email: 'existing@example.com',
sender_reply_to: 'existing-reply@example.com'
});
} finally {
validateStub.restore();
}
});
});

describe('Permissions', function () {
Expand Down
Loading
Loading