[CORL-943] Send test emails (#2882)

* add smtp test email

* disable button if email not enabled

* add error handling

* internationalize strings

* sort imports
This commit is contained in:
Tessa Thornton
2020-03-27 11:46:53 -04:00
committed by GitHub
parent 7fd524cd6a
commit 4645ed4cd7
11 changed files with 164 additions and 1 deletions
@@ -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<Pick<GQLSettings, "email">>;
@@ -27,8 +33,34 @@ export type FormProps = DeepNullable<Pick<GQLSettings, "email">>;
const EmailConfigContainer: React.FunctionComponent<Props> = ({
email,
submitting,
viewer,
}) => {
const form = useForm();
const [loading, setLoading] = useState(false);
const [submitError, setSubmitError] = useState<null | string>(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(
<Localized id="configure-smtp-test-success" $email={viewer.email}>
<AppNotification icon="check_circle_outline" onClose={clearMessage}>
Test email has been sent to {viewer.email}
</AppNotification>
</Localized>,
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<Props> = ({
>
{disabledInside => (
<>
<Flex justifyContent="flex-end">
<Localized id="configure-email-send-test">
<Button
disabled={disabledInside || loading || !email.enabled}
onClick={sendTestEmail}
>
Send test email
</Button>
</Localized>
</Flex>
{submitError && (
<CallOut fullWidth color="error">
{submitError}
</CallOut>
)}
<From disabled={disabledInside} />
<SMTP disabled={disabledInside} />
</>
@@ -70,6 +117,11 @@ const EmailConfigContainer: React.FunctionComponent<Props> = ({
};
const enhanced = withFragmentContainer<Props>({
viewer: graphql`
fragment EmailConfigContainer_viewer on User {
email
}
`,
email: graphql`
fragment EmailConfigContainer_email on EmailConfiguration {
enabled
@@ -26,6 +26,7 @@ class EmailConfigRoute extends React.Component<Props> {
<EmailConfigContainer
email={this.props.data.settings.email}
submitting={this.props.submitting}
viewer={this.props.data.viewer}
/>
);
}
@@ -34,6 +35,9 @@ class EmailConfigRoute extends React.Component<Props> {
const enhanced = withRouteConfig<Props>({
query: graphql`
query EmailConfigRouteQuery {
viewer {
...EmailConfigContainer_viewer
}
settings {
email {
...EmailConfigContainer_email
@@ -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<MutationTypes>) => {
return commitMutationPromiseNormalized<MutationTypes>(environment, {
mutation: graphql`
mutation TestSMTPMutation($input: TestSMTPInput!) {
testSMTP(input: $input) {
clientMutationId
}
}
`,
variables: {
input: {
clientMutationId: clientMutationId.toString(),
},
},
});
}
);
export default TestSMTPMutation;
@@ -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<GQLUpdateSettingsInput>
@@ -101,4 +104,5 @@ export const Settings = ({
input.inactiveIn,
now
),
testSMTP: () => sendSMTPTest(tenant, user!, mailerQueue),
});
@@ -326,4 +326,10 @@ export const Mutation: Required<GQLMutationTypeResolver<void>> = {
endpoint: await ctx.mutators.Settings.rotateWebhookEndpointSecret(input),
clientMutationId,
}),
testSMTP: async (source, { input: { clientMutationId } }, ctx) => {
await ctx.mutators.Settings.testSMTP();
return {
clientMutationId,
};
},
};
@@ -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])
}
##################
+3
View File
@@ -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 }
@@ -3,6 +3,15 @@ interface EmailTemplate<T extends string, U extends {}> {
context: U;
}
type TestContext<T extends string, U extends {}> = EmailTemplate<T, U>;
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 };
@@ -0,0 +1,8 @@
{% extends "layouts/base.html" %} {% block content %}
<div
data-l10n-id="email-template-testSmtpTest"
data-l10n-args="{{ context | dump }}"
>
This is a test email sent to {{ context.email }}
</div>
{% endblock %}
+27
View File
@@ -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;
}
+1
View File
@@ -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