diff --git a/src/core/client/admin/routes/Configure/sections/Email/EmailConfigContainer.tsx b/src/core/client/admin/routes/Configure/sections/Email/EmailConfigContainer.tsx index 4930f0b70..3e2bea837 100644 --- a/src/core/client/admin/routes/Configure/sections/Email/EmailConfigContainer.tsx +++ b/src/core/client/admin/routes/Configure/sections/Email/EmailConfigContainer.tsx @@ -1,25 +1,31 @@ import { Localized } from "@fluent/react/compat"; -import React, { useMemo } from "react"; +import React, { useCallback, useMemo, useState } from "react"; import { useForm } from "react-final-form"; import { graphql } from "react-relay"; +import { useNotification } from "coral-admin/App/GlobalNotification"; import { DeepNullable } from "coral-common/types"; import { purgeMetadata, + useMutation, withFragmentContainer, } from "coral-framework/lib/relay"; import { GQLSettings } from "coral-framework/schema"; +import { AppNotification, Button, CallOut, Flex } from "coral-ui/components/v2"; import { EmailConfigContainer_email } from "coral-admin/__generated__/EmailConfigContainer_email.graphql"; +import { EmailConfigContainer_viewer } from "coral-admin/__generated__/EmailConfigContainer_viewer.graphql"; import Header from "../../Header"; import ConfigBoxWithToggleField from "../Auth/ConfigBoxWithToggleField"; import From from "./From"; import SMTP from "./SMTP"; +import TestSMTPMutation from "./TestSMTPMutation"; interface Props { submitting: boolean; email: EmailConfigContainer_email; + viewer: EmailConfigContainer_viewer | null; } export type FormProps = DeepNullable>; @@ -27,8 +33,34 @@ export type FormProps = DeepNullable>; const EmailConfigContainer: React.FunctionComponent = ({ email, submitting, + viewer, }) => { const form = useForm(); + const [loading, setLoading] = useState(false); + const [submitError, setSubmitError] = useState(null); + const { setMessage, clearMessage } = useNotification(); + const sendTest = useMutation(TestSMTPMutation); + const sendTestEmail = useCallback(async () => { + if (!viewer) { + return; + } + setLoading(true); + setSubmitError(null); + try { + await sendTest(); + setLoading(false); + setMessage( + + + Test email has been sent to {viewer.email} + + , + 3000 + ); + } catch (error) { + setSubmitError(error.message); + } + }, []); useMemo(() => { let values: any = { email }; if (email && email.smtp && email.smtp.authentication === null) { @@ -61,6 +93,21 @@ const EmailConfigContainer: React.FunctionComponent = ({ > {disabledInside => ( <> + + + + + + {submitError && ( + + {submitError} + + )} @@ -70,6 +117,11 @@ const EmailConfigContainer: React.FunctionComponent = ({ }; const enhanced = withFragmentContainer({ + viewer: graphql` + fragment EmailConfigContainer_viewer on User { + email + } + `, email: graphql` fragment EmailConfigContainer_email on EmailConfiguration { enabled diff --git a/src/core/client/admin/routes/Configure/sections/Email/EmailConfigRoute.tsx b/src/core/client/admin/routes/Configure/sections/Email/EmailConfigRoute.tsx index 0f87d05bb..e9ed47d54 100644 --- a/src/core/client/admin/routes/Configure/sections/Email/EmailConfigRoute.tsx +++ b/src/core/client/admin/routes/Configure/sections/Email/EmailConfigRoute.tsx @@ -26,6 +26,7 @@ class EmailConfigRoute extends React.Component { ); } @@ -34,6 +35,9 @@ class EmailConfigRoute extends React.Component { const enhanced = withRouteConfig({ query: graphql` query EmailConfigRouteQuery { + viewer { + ...EmailConfigContainer_viewer + } settings { email { ...EmailConfigContainer_email diff --git a/src/core/client/admin/routes/Configure/sections/Email/TestSMTPMutation.ts b/src/core/client/admin/routes/Configure/sections/Email/TestSMTPMutation.ts new file mode 100644 index 000000000..13d272d14 --- /dev/null +++ b/src/core/client/admin/routes/Configure/sections/Email/TestSMTPMutation.ts @@ -0,0 +1,34 @@ +import { graphql } from "react-relay"; +import { Environment } from "relay-runtime"; + +import { + commitMutationPromiseNormalized, + createMutation, + MutationInput, +} from "coral-framework/lib/relay"; + +import { TestSMTPMutation as MutationTypes } from "coral-admin/__generated__/TestSMTPMutation.graphql"; + +const clientMutationId = 0; + +const TestSMTPMutation = createMutation( + "testSMTP", + (environment: Environment, input: MutationInput) => { + return commitMutationPromiseNormalized(environment, { + mutation: graphql` + mutation TestSMTPMutation($input: TestSMTPInput!) { + testSMTP(input: $input) { + clientMutationId + } + } + `, + variables: { + input: { + clientMutationId: clientMutationId.toString(), + }, + }, + }); + } +); + +export default TestSMTPMutation; diff --git a/src/core/server/graph/mutators/Settings.ts b/src/core/server/graph/mutators/Settings.ts index cda89b2b1..7fe3baac0 100644 --- a/src/core/server/graph/mutators/Settings.ts +++ b/src/core/server/graph/mutators/Settings.ts @@ -14,6 +14,7 @@ import { regenerateSSOKey, rotateSSOKey, rotateWebhookEndpointSecret, + sendSMTPTest, update, updateWebhookEndpoint, } from "coral-server/services/tenant"; @@ -42,6 +43,8 @@ export const Settings = ({ tenant, config, now, + mailerQueue, + user, }: GraphContext) => ({ update: ( input: WithoutMutationID @@ -101,4 +104,5 @@ export const Settings = ({ input.inactiveIn, now ), + testSMTP: () => sendSMTPTest(tenant, user!, mailerQueue), }); diff --git a/src/core/server/graph/resolvers/Mutation.ts b/src/core/server/graph/resolvers/Mutation.ts index 141a515d4..a7ee0ff6a 100644 --- a/src/core/server/graph/resolvers/Mutation.ts +++ b/src/core/server/graph/resolvers/Mutation.ts @@ -326,4 +326,10 @@ export const Mutation: Required> = { endpoint: await ctx.mutators.Settings.rotateWebhookEndpointSecret(input), clientMutationId, }), + testSMTP: async (source, { input: { clientMutationId } }, ctx) => { + await ctx.mutators.Settings.testSMTP(); + return { + clientMutationId, + }; + }, }; diff --git a/src/core/server/graph/schema/schema.graphql b/src/core/server/graph/schema/schema.graphql index bfd29561b..a88986aad 100644 --- a/src/core/server/graph/schema/schema.graphql +++ b/src/core/server/graph/schema/schema.graphql @@ -6244,6 +6244,14 @@ type UpdateSitePayload { site: Site! } +input TestSMTPInput { + clientMutationId: String! +} + +type TestSMTPPayload { + clientMutationId: String! +} + ################## ## Mutation ################## @@ -6713,6 +6721,12 @@ type Mutation { """ removeStoryExpert(input: RemoveStoryExpertInput!): RemoveStoryExpertPayload! @auth(roles: [ADMIN, MODERATOR]) + + """ + testSMTP sends a test email. + """ + testSMTP(input: TestSMTPInput!): TestSMTPPayload! + @auth(roles: [ADMIN, MODERATOR]) } ################## diff --git a/src/core/server/locales/en-US/email.ftl b/src/core/server/locales/en-US/email.ftl index 854b161d3..79009c02d 100644 --- a/src/core/server/locales/en-US/email.ftl +++ b/src/core/server/locales/en-US/email.ftl @@ -121,3 +121,6 @@ email-template-notificationOnCommentRejected = # Notification Digest email-subject-notificationDigest = Your latest comment activity at { $organizationName } + +email-subject-testSmtpTest = Test email from Coral +email-template-testSmtpTest = This is a test email sent to { $email } diff --git a/src/core/server/queue/tasks/mailer/templates/index.ts b/src/core/server/queue/tasks/mailer/templates/index.ts index 1227c9f0b..8c49dfe7c 100644 --- a/src/core/server/queue/tasks/mailer/templates/index.ts +++ b/src/core/server/queue/tasks/mailer/templates/index.ts @@ -3,6 +3,15 @@ interface EmailTemplate { context: U; } +type TestContext = EmailTemplate; + +export type SMTPTestTemplate = TestContext< + "test/smtp-test", + { + email: string; + } +>; + /** * NotificationContext */ @@ -203,6 +212,7 @@ type Templates = | OnFeaturedTemplate | DigestTemplate | OnCommentRejectedTemplate + | SMTPTestTemplate | OnCommentApprovedTemplate; export { Templates as EmailTemplate }; diff --git a/src/core/server/queue/tasks/mailer/templates/test/smtp-test.html b/src/core/server/queue/tasks/mailer/templates/test/smtp-test.html new file mode 100644 index 000000000..a496c9336 --- /dev/null +++ b/src/core/server/queue/tasks/mailer/templates/test/smtp-test.html @@ -0,0 +1,8 @@ +{% extends "layouts/base.html" %} {% block content %} +
+ This is a test email sent to {{ context.email }} +
+{% endblock %} diff --git a/src/core/server/services/tenant/tenant.ts b/src/core/server/services/tenant/tenant.ts index 1d806540a..b37f92197 100644 --- a/src/core/server/services/tenant/tenant.ts +++ b/src/core/server/services/tenant/tenant.ts @@ -26,6 +26,8 @@ import { updateTenantWebhookEndpoint, UpdateTenantWebhookEndpointInput, } from "coral-server/models/tenant"; +import { User } from "coral-server/models/user"; +import { MailerQueue } from "coral-server/queue/tasks/mailer"; import { I18n } from "coral-server/services/i18n"; import { @@ -498,3 +500,28 @@ export async function deleteAnnouncement( await cache.update(redis, updated); return updated; } + +export async function sendSMTPTest( + tenant: Tenant, + user: User, + mailer: MailerQueue +) { + if (user.email) { + if (!tenant.email.enabled) { + throw new Error("Email not enabled"); + } + await mailer.add({ + tenantID: tenant.id, + message: { + to: user.email, + }, + template: { + name: "test/smtp-test", + context: { + email: user.email, + }, + }, + }); + } + return tenant; +} diff --git a/src/locales/en-US/admin.ftl b/src/locales/en-US/admin.ftl index c5a20936c..382ea3728 100644 --- a/src/locales/en-US/admin.ftl +++ b/src/locales/en-US/admin.ftl @@ -384,6 +384,7 @@ configure-email-smtpAuthenticationLabel = SMTP authentication configure-email-smtpCredentialsHeader = Email credentials configure-email-smtpUsernameLabel = Username configure-email-smtpPasswordLabel = Password +configure-email-send-test = Send test email ### Authentication