mirror of
https://github.com/wassname/talk.git
synced 2026-07-18 12:40:13 +08:00
[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:
@@ -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])
|
||||
}
|
||||
|
||||
##################
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user