Skip to content
Open
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
4 changes: 2 additions & 2 deletions apps/admin-x-framework/src/api/automated-emails.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,8 @@ export const useEditAutomatedEmail = createMutation<AutomatedEmailsResponseType,
}
});

export const useSendTestWelcomeEmail = createMutation<unknown, {id: string; email: string}>({
export const useSendTestWelcomeEmail = createMutation<unknown, {id: string; email: string; subject: string; lexical: string}>({
method: 'POST',
path: ({id}) => `/automated_emails/${id}/test/`,
body: ({email}) => ({email})
body: ({email, subject, lexical}) => ({email, subject, lexical})
});
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ const WelcomeEmailModal = NiceModal.create<WelcomeEmailModalProps>(({emailType =
const {settings} = useGlobalData();
const [siteTitle, defaultEmailAddress] = getSettingValues<string>(settings, ['title', 'default_email_address']);

const {formState, saveState, updateForm, handleSave, okProps, errors} = useForm({
const {formState, saveState, updateForm, handleSave, okProps, errors, validate} = useForm({
initialState: {
subject: automatedEmail?.subject || 'Welcome',
lexical: automatedEmail?.lexical || ''
Expand Down Expand Up @@ -136,14 +136,24 @@ const WelcomeEmailModal = NiceModal.create<WelcomeEmailModalProps>(({emailType =
return;
}

// check that subject and lexical are valid
if (!validate()) {
setTestEmailError('Please complete the required fields');
return;
}

setSendState('sending');

try {
await handleSave({fakeWhenUnchanged: true});
await sendTestEmail({id: automatedEmail.id, email: testEmail});
await sendTestEmail({
id: automatedEmail.id,
email: testEmail,
subject: formState.subject,
lexical: formState.lexical
});
setSendState('sent');
clearTimeout(sendStateTimeoutRef.current!);
sendStateTimeoutRef.current = setTimeout(() => setSendState('idle'), 2500);
sendStateTimeoutRef.current = setTimeout(() => setSendState('idle'), 2000);
} catch (error) {
setSendState('idle');
let message;
Expand Down Expand Up @@ -188,8 +198,6 @@ const WelcomeEmailModal = NiceModal.create<WelcomeEmailModalProps>(({emailType =
<label className='mb-2 block text-sm font-semibold'>Send test email</label>
<TextField
className='!h-[36px]'
error={Boolean(testEmailError)}
hint={testEmailError}
placeholder='you@yoursite.com'
value={testEmail}
onChange={(e) => {
Expand All @@ -204,6 +212,7 @@ const WelcomeEmailModal = NiceModal.create<WelcomeEmailModalProps>(({emailType =
label={sendState === 'sent' ? 'Sent' : sendState === 'sending' ? 'Sending...' : 'Send'}
onClick={handleSendTestEmail}
/>
{testEmailError && <Hint className='mt-2' color='red'>{testEmailError}</Hint>}
</div>
)}
</div>
Expand Down
6 changes: 5 additions & 1 deletion ghost/core/core/server/api/endpoints/automated-emails.js
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,9 @@ const controller = {
'id'
],
data: [
'email'
'email',
'subject',
'lexical'
],
validation: {
options: {
Expand All @@ -138,6 +140,8 @@ const controller = {
memberWelcomeEmailService.init();
await memberWelcomeEmailService.api.sendTestEmail({
email: frame.data.email,
subject: frame.data.subject,
lexical: frame.data.lexical,
automatedEmailId: frame.options.id
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,9 @@ const messages = {
invalidLexical: 'Lexical must be a valid JSON string',
invalidSlug: `Slug must be one of: ${ALLOWED_SLUGS.join(', ')}`,
invalidName: `Name must be one of: ${ALLOWED_NAMES.join(', ')}`,
invalidEmailReceived: 'The server did not receive a valid email'
invalidEmailReceived: 'The server did not receive a valid email',
subjectRequired: 'Subject is required',
lexicalRequired: 'Email content is required'
};

const validateAutomatedEmail = async function (frame) {
Expand Down Expand Up @@ -68,11 +70,25 @@ module.exports = {
},
sendTestEmail(apiConfig, frame) {
const email = frame.data.email;
const subject = frame.data.subject;
const lexical = frame.data.lexical;

if (typeof email !== 'string' || !validator.isEmail(email)) {
throw new BadRequestError({
message: tpl(messages.invalidEmailReceived)
});
}

if (typeof subject !== 'string' || !subject.trim()) {
throw new BadRequestError({
message: tpl(messages.subjectRequired)
});
}

if (typeof lexical !== 'string' || !lexical.trim()) {
throw new BadRequestError({
message: tpl(messages.lexicalRequired)
});
}
}
};
Original file line number Diff line number Diff line change
Expand Up @@ -102,9 +102,10 @@ class MemberWelcomeEmailService {
return Boolean(row && row.get('lexical') && row.get('status') === 'active');
}

async sendTestEmail({email, automatedEmailId}) {
async sendTestEmail({email, subject, lexical, automatedEmailId}) {
logging.info(`${MEMBER_WELCOME_EMAIL_LOG_KEY} Sending test welcome email to ${email}`);

// Still validate the automated email exists (for permission purposes)
const automatedEmail = await AutomatedEmail.findOne({id: automatedEmailId});

if (!automatedEmail) {
Expand All @@ -113,9 +114,6 @@ class MemberWelcomeEmailService {
});
}

const lexical = automatedEmail.get('lexical');
const subject = automatedEmail.get('subject');

const siteSettings = {
title: settingsCache.get('title') || 'Ghost',
url: urlUtils.urlFor('home', true),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -700,3 +700,65 @@ Object {
"x-powered-by": "Express",
}
`;

exports[`Automated Emails API SendTestEmail Cannot send test email without lexical 1: [body] 1`] = `
Object {
"errors": Array [
Object {
"code": null,
"context": null,
"details": null,
"ghostErrorCode": null,
"help": null,
"id": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
"message": "Email content is required",
"property": null,
"type": "BadRequestError",
},
],
}
`;

exports[`Automated Emails API SendTestEmail Cannot send test email without lexical 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "213",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Accept-Version, Origin, Accept-Encoding",
"x-powered-by": "Express",
}
`;

exports[`Automated Emails API SendTestEmail Cannot send test email without subject 1: [body] 1`] = `
Object {
"errors": Array [
Object {
"code": null,
"context": null,
"details": null,
"ghostErrorCode": null,
"help": null,
"id": StringMatching /\\[a-f0-9\\]\\{8\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{4\\}-\\[a-f0-9\\]\\{12\\}/,
"message": "Subject is required",
"property": null,
"type": "BadRequestError",
},
],
}
`;

exports[`Automated Emails API SendTestEmail Cannot send test email without subject 2: [headers] 1`] = `
Object {
"access-control-allow-origin": "http://127.0.0.1:2369",
"cache-control": "no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0",
"content-length": "207",
"content-type": "application/json; charset=utf-8",
"content-version": StringMatching /v\\\\d\\+\\\\\\.\\\\d\\+/,
"etag": StringMatching /\\(\\?:W\\\\/\\)\\?"\\(\\?:\\[ !#-\\\\x7E\\\\x80-\\\\xFF\\]\\*\\|\\\\r\\\\n\\[\\\\t \\]\\|\\\\\\\\\\.\\)\\*"/,
"vary": "Accept-Version, Origin, Accept-Encoding",
"x-powered-by": "Express",
}
`;
57 changes: 54 additions & 3 deletions ghost/core/test/e2e-api/admin/automated-emails.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ describe('Automated Emails API', function () {
});

beforeEach(async function () {
await dbUtils.truncate('brute');
await dbUtils.truncate('automated_emails');
});

Expand Down Expand Up @@ -362,7 +363,11 @@ describe('Automated Emails API', function () {
it('Can send test email', async function () {
await agent
.post(`automated_emails/${automatedEmailId}/test/`)
.body({email: 'test@ghost.org'})
.body({
email: 'test@ghost.org',
subject: 'Test Subject',
lexical: validLexical
})
.expectStatus(204)
.expectEmptyBody()
.matchHeaderSnapshot({
Expand All @@ -374,7 +379,11 @@ describe('Automated Emails API', function () {
it('Cannot send test email for non-existent automated email', async function () {
await agent
.post('automated_emails/abcd1234abcd1234abcd1234/test/')
.body({email: 'test@ghost.org'})
.body({
email: 'test@ghost.org',
subject: 'Test Subject',
lexical: validLexical
})
.expectStatus(404)
.matchBodySnapshot({
errors: [{
Expand Down Expand Up @@ -406,7 +415,49 @@ describe('Automated Emails API', function () {
it('Cannot send test email with invalid email format', async function () {
await agent
.post(`automated_emails/${automatedEmailId}/test/`)
.body({email: 'not-a-valid-email'})
.body({
email: 'not-a-valid-email',
subject: 'Test Subject',
lexical: validLexical
})
.expectStatus(400)
.matchBodySnapshot({
errors: [{
id: anyErrorId
}]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
});

it('Cannot send test email without subject', async function () {
await agent
.post(`automated_emails/${automatedEmailId}/test/`)
.body({
email: 'test@ghost.org',
lexical: validLexical
})
.expectStatus(400)
.matchBodySnapshot({
errors: [{
id: anyErrorId
}]
})
.matchHeaderSnapshot({
'content-version': anyContentVersion,
etag: anyEtag
});
});

it('Cannot send test email without lexical', async function () {
await agent
.post(`automated_emails/${automatedEmailId}/test/`)
.body({
email: 'test@ghost.org',
subject: 'Test Subject'
})
.expectStatus(400)
.matchBodySnapshot({
errors: [{
Expand Down
Loading