From a276b1ca06ce3ebfc201b9aaf3aa8c37c98fe584 Mon Sep 17 00:00:00 2001 From: tobi <31960611+tsmethurst@users.noreply.github.com> Date: Mon, 27 May 2024 19:03:54 +0200 Subject: [PATCH] [feature/frontend] Let admins send test email to validate SMTP config (#2934) * [feature/frontend] Let admins send test email to validate SMTP config * wee --- docs/admin/settings.md | 4 + docs/api/swagger.yaml | 5 ++ docs/configuration/smtp.md | 2 + internal/api/client/admin/emailtest.go | 13 +++- internal/api/model/admin.go | 4 +- internal/email/test.go | 2 + internal/processing/admin/email.go | 19 +++-- .../settings/lib/query/admin/actions/index.ts | 73 ++++++++++++++++++ web/source/settings/lib/query/admin/index.ts | 22 ------ .../views/admin/actions/email/index.tsx | 29 +++++++ .../views/admin/actions/email/test.tsx | 77 +++++++++++++++++++ .../views/admin/actions/keys/expireremote.tsx | 25 +++--- .../views/admin/actions/keys/index.tsx | 5 +- .../views/admin/actions/media/cleanup.tsx | 22 ++++-- .../views/admin/actions/media/index.tsx | 5 +- web/source/settings/views/admin/menu.tsx | 8 +- web/source/settings/views/admin/router.tsx | 6 +- web/template/email_test.tmpl | 7 ++ 18 files changed, 276 insertions(+), 52 deletions(-) create mode 100644 web/source/settings/lib/query/admin/actions/index.ts create mode 100644 web/source/settings/views/admin/actions/email/index.tsx create mode 100644 web/source/settings/views/admin/actions/email/test.tsx diff --git a/docs/admin/settings.md b/docs/admin/settings.md index 2429b5ffe..137ad257f 100644 --- a/docs/admin/settings.md +++ b/docs/admin/settings.md @@ -66,6 +66,10 @@ Instance administration settings. Run one-off administrative actions. +#### Email + +You can use this section to send a test email to the given email address, with an optional test message. + #### Media You can use this section run a media action to clean up the remote media cache using the specified number of days. Media older than the given number of days will be removed from storage (s3 or local). Media removed in this way will be refetched again later if the media is required again. This action is functionally identical to the media cleanup that runs automatically. diff --git a/docs/api/swagger.yaml b/docs/api/swagger.yaml index 8bd43ae8e..b069eaf55 100644 --- a/docs/api/swagger.yaml +++ b/docs/api/swagger.yaml @@ -4894,6 +4894,11 @@ paths: - description: The email address that the test email should be sent to. in: formData name: email + required: true + type: string + - description: Optional message to include in the email. + in: formData + name: message type: string produces: - application/json diff --git a/docs/configuration/smtp.md b/docs/configuration/smtp.md index d707fb291..ab6e9135d 100644 --- a/docs/configuration/smtp.md +++ b/docs/configuration/smtp.md @@ -6,6 +6,8 @@ Configuring GoToSocial to send emails is **not required** in order to have a pro In order to make GoToSocial email sending work, you need an smtp-compatible mail service running somewhere, either as a server on the same machine that GoToSocial is running on, or via an external service like [Mailgun](https://mailgun.com). It may also be possible to use a free personal email address for sending emails, if your email provider supports smtp (check with them--most do), but you might run into trouble sending lots of emails. +To validate your configuration, you can use the "Administration -> Actions -> Email" section of the settings panel to send a test email. + ## Settings The configuration options for smtp are as follows: diff --git a/internal/api/client/admin/emailtest.go b/internal/api/client/admin/emailtest.go index 42b405ce7..9b214a926 100644 --- a/internal/api/client/admin/emailtest.go +++ b/internal/api/client/admin/emailtest.go @@ -54,6 +54,12 @@ import ( // in: formData // description: The email address that the test email should be sent to. // type: string +// required: true +// - +// name: message +// in: formData +// description: Optional message to include in the email. +// type: string // // security: // - OAuth2 Bearer: @@ -115,7 +121,12 @@ func (m *Module) EmailTestPOSTHandler(c *gin.Context) { return } - errWithCode := m.processor.Admin().EmailTest(c.Request.Context(), authed.Account, email.Address) + errWithCode := m.processor.Admin().EmailTest( + c.Request.Context(), + authed.Account, + email.Address, + form.Message, + ) if errWithCode != nil { apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1) return diff --git a/internal/api/model/admin.go b/internal/api/model/admin.go index 637ab0ed7..4623a720f 100644 --- a/internal/api/model/admin.go +++ b/internal/api/model/admin.go @@ -201,7 +201,9 @@ type MediaCleanupRequest struct { // AdminSendTestEmailRequest models a test email send request (woah). type AdminSendTestEmailRequest struct { // Email address to send the test email to. - Email string `form:"email" json:"email" xml:"email"` + Email string `form:"email" json:"email"` + // Optional message to include in the test email. + Message string `form:"message" json:"message"` } type AdminInstanceRule struct { diff --git a/internal/email/test.go b/internal/email/test.go index 7d6ac2b3b..762711b76 100644 --- a/internal/email/test.go +++ b/internal/email/test.go @@ -25,6 +25,8 @@ const ( type TestData struct { // Username of admin user who sent the test. SendingUsername string + // (Optional) message to include in the email. + Message string // URL of the instance to present to the receiver. InstanceURL string // Name of the instance to present to the receiver. diff --git a/internal/processing/admin/email.go b/internal/processing/admin/email.go index fb78f1fcc..fda60754c 100644 --- a/internal/processing/admin/email.go +++ b/internal/processing/admin/email.go @@ -27,11 +27,19 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" ) -// EmailTest sends a generic test email to the given toAddress (which -// should be a valid email address). To help callers differentiate between -// proper errors and the smtp errors they're likely fishing for, will return -// 422 + help text on an SMTP error, or error 500 otherwise. -func (p *Processor) EmailTest(ctx context.Context, account *gtsmodel.Account, toAddress string) gtserror.WithCode { +// EmailTest sends a generic test email to the given +// toAddress (which should be a valid email address). +// Message is optional and can be an empty string. +// +// To help callers differentiate between proper errors +// and the smtp errors they're likely fishing for, will +// return 422 + help text on an SMTP error, or 500 otherwise. +func (p *Processor) EmailTest( + ctx context.Context, + account *gtsmodel.Account, + toAddress string, + message string, +) gtserror.WithCode { // Pull our instance entry from the database, // so we can greet the email recipient nicely. instance, err := p.state.DB.GetInstance(ctx, config.GetHost()) @@ -42,6 +50,7 @@ func (p *Processor) EmailTest(ctx context.Context, account *gtsmodel.Account, to testData := email.TestData{ SendingUsername: account.Username, + Message: message, InstanceURL: instance.URI, InstanceName: instance.Title, } diff --git a/web/source/settings/lib/query/admin/actions/index.ts b/web/source/settings/lib/query/admin/actions/index.ts new file mode 100644 index 000000000..7bf34c9ba --- /dev/null +++ b/web/source/settings/lib/query/admin/actions/index.ts @@ -0,0 +1,73 @@ +/* + GoToSocial + Copyright (C) GoToSocial Authors admin@gotosocial.org + SPDX-License-Identifier: AGPL-3.0-or-later + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +import { gtsApi } from "../../gts-api"; + +const extended = gtsApi.injectEndpoints({ + endpoints: (build) => ({ + mediaCleanup: build.mutation({ + query: (days) => ({ + method: "POST", + url: `/api/v1/admin/media_cleanup`, + params: { + remote_cache_days: days + } + }) + }), + + instanceKeysExpire: build.mutation({ + query: (domain) => ({ + method: "POST", + url: `/api/v1/admin/domain_keys_expire`, + params: { + domain: domain + } + }) + }), + + sendTestEmail: build.mutation({ + query: (params) => ({ + method: "POST", + url: `/api/v1/admin/email/test`, + params: params, + }) + }), + }), +}); + +/** + * POST to /api/v1/admin/media_cleanup to trigger manual cleanup. + */ +const useMediaCleanupMutation = extended.useMediaCleanupMutation; + +/** + * POST to /api/v1/admin/domain_keys_expire to expire domain keys for the given domain. + */ +const useInstanceKeysExpireMutation = extended.useInstanceKeysExpireMutation; + +/** + * POST to /api/v1/admin/email/test to send a test email to the given address. + */ +const useSendTestEmailMutation = extended.useSendTestEmailMutation; + +export { + useMediaCleanupMutation, + useInstanceKeysExpireMutation, + useSendTestEmailMutation, +}; diff --git a/web/source/settings/lib/query/admin/index.ts b/web/source/settings/lib/query/admin/index.ts index 3a095a8a1..fba028853 100644 --- a/web/source/settings/lib/query/admin/index.ts +++ b/web/source/settings/lib/query/admin/index.ts @@ -37,26 +37,6 @@ const extended = gtsApi.injectEndpoints({ ...replaceCacheOnMutation("instanceV1"), }), - mediaCleanup: build.mutation({ - query: (days) => ({ - method: "POST", - url: `/api/v1/admin/media_cleanup`, - params: { - remote_cache_days: days - } - }) - }), - - instanceKeysExpire: build.mutation({ - query: (domain) => ({ - method: "POST", - url: `/api/v1/admin/domain_keys_expire`, - params: { - domain: domain - } - }) - }), - getAccount: build.query({ query: (id) => ({ url: `/api/v1/admin/accounts/${id}` @@ -214,8 +194,6 @@ const extended = gtsApi.injectEndpoints({ export const { useUpdateInstanceMutation, - useMediaCleanupMutation, - useInstanceKeysExpireMutation, useGetAccountQuery, useLazyGetAccountQuery, useActionAccountMutation, diff --git a/web/source/settings/views/admin/actions/email/index.tsx b/web/source/settings/views/admin/actions/email/index.tsx new file mode 100644 index 000000000..e1b2fc759 --- /dev/null +++ b/web/source/settings/views/admin/actions/email/index.tsx @@ -0,0 +1,29 @@ +/* + GoToSocial + Copyright (C) GoToSocial Authors admin@gotosocial.org + SPDX-License-Identifier: AGPL-3.0-or-later + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +import React from "react"; +import Test from "./test"; + +export default function Email() { + return ( +
+ +
+ ); +} diff --git a/web/source/settings/views/admin/actions/email/test.tsx b/web/source/settings/views/admin/actions/email/test.tsx new file mode 100644 index 000000000..09b7ed909 --- /dev/null +++ b/web/source/settings/views/admin/actions/email/test.tsx @@ -0,0 +1,77 @@ +/* + GoToSocial + Copyright (C) GoToSocial Authors admin@gotosocial.org + SPDX-License-Identifier: AGPL-3.0-or-later + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +import React from "react"; +import { TextInput } from "../../../../components/form/inputs"; +import MutationButton from "../../../../components/form/mutation-button"; +import { useTextInput } from "../../../../lib/form"; +import { useSendTestEmailMutation } from "../../../../lib/query/admin/actions"; +import { useInstanceV1Query } from "../../../../lib/query/gts-api"; +import useFormSubmit from "../../../../lib/form/submit"; + +export default function Test({}) { + const { data: instance } = useInstanceV1Query(); + + const form = { + email: useTextInput("email", { defaultValue: instance?.email }), + message: useTextInput("message") + }; + + const [submit, result] = useFormSubmit(form, useSendTestEmailMutation(), { changedOnly: false }); + + return ( +
+
+

Send test email

+

+ To check whether your instance email configuration is correct, you can + try sending a test email to the given address, with an optional message. +
+ If you do not have SMTP configured for your instance, this will do nothing. +

+ + Learn more about SMTP configuration (opens in a new tab) + +
+ + + + + ); +} diff --git a/web/source/settings/views/admin/actions/keys/expireremote.tsx b/web/source/settings/views/admin/actions/keys/expireremote.tsx index 82045942c..d695ec0c8 100644 --- a/web/source/settings/views/admin/actions/keys/expireremote.tsx +++ b/web/source/settings/views/admin/actions/keys/expireremote.tsx @@ -21,7 +21,7 @@ import React from "react"; import { TextInput } from "../../../../components/form/inputs"; import MutationButton from "../../../../components/form/mutation-button"; import { useTextInput } from "../../../../lib/form"; -import { useInstanceKeysExpireMutation } from "../../../../lib/query/admin"; +import { useInstanceKeysExpireMutation } from "../../../../lib/query/admin/actions"; export default function ExpireRemote({}) { const domainField = useTextInput("domain"); @@ -35,15 +35,20 @@ export default function ExpireRemote({}) { return (
-

Expire remote instance keys

-

- Mark all public keys from the given remote instance as expired.

- This is useful in cases where the remote domain has had to rotate their keys for whatever - reason (security issue, data leak, routine safety procedure, etc), and your instance can no - longer communicate with theirs properly using cached keys. A key marked as expired in this way - will be lazily refetched next time a request is made to your instance signed by the owner of that - key. -

+
+

Expire remote instance keys

+

+ Mark all public keys from the given remote instance as expired. +
+ This is useful in cases where the remote domain has had to rotate + their keys for whatever reason (security issue, data leak, routine + safety procedure, etc), and your instance can no longer communicate + with theirs properly using cached keys. +
+ A key marked as expired in this way will be lazily refetched next time + a request is made to your instance signed by the owner of that key. +

+
-

Key Actions

+
- +
); } diff --git a/web/source/settings/views/admin/actions/media/cleanup.tsx b/web/source/settings/views/admin/actions/media/cleanup.tsx index c1df511e1..46f00b548 100644 --- a/web/source/settings/views/admin/actions/media/cleanup.tsx +++ b/web/source/settings/views/admin/actions/media/cleanup.tsx @@ -22,10 +22,10 @@ import React from "react"; import { useTextInput } from "../../../../lib/form"; import { TextInput } from "../../../../components/form/inputs"; import MutationButton from "../../../../components/form/mutation-button"; -import { useMediaCleanupMutation } from "../../../../lib/query/admin"; +import { useMediaCleanupMutation } from "../../../../lib/query/admin/actions"; export default function Cleanup({}) { - const daysField = useTextInput("days", { defaultValue: "30" }); + const daysField = useTextInput("days", { defaultValue: "7" }); const [mediaCleanup, mediaCleanupResult] = useMediaCleanupMutation(); @@ -36,12 +36,24 @@ export default function Cleanup({}) { return ( -

Cleanup

-

+

+

Cleanup

+

Clean up remote media older than the specified number of days. +
If the remote instance is still online they will be refetched when needed. +
Also cleans up unused headers and avatars from the media cache. -

+

+ + Learn more about media caching + cleanup (opens in a new tab) + +
-

Media Actions

+
- +
); } diff --git a/web/source/settings/views/admin/menu.tsx b/web/source/settings/views/admin/menu.tsx index 481f51a4d..3b88f6be3 100644 --- a/web/source/settings/views/admin/menu.tsx +++ b/web/source/settings/views/admin/menu.tsx @@ -34,6 +34,7 @@ import { useHasPermission } from "../../lib/navigation/util"; * - /settings/admin/emojis/local/:emojiId * - /settings/admin/emojis/remote * - /settings/admin/actions + * - /settings/admin/actions/email * - /settings/admin/actions/media * - /settings/admin/actions/keys * - /settings/admin/http-header-permissions/blocks @@ -94,9 +95,14 @@ function AdminActionsMenu() { + + - + diff --git a/web/template/email_test.tmpl b/web/template/email_test.tmpl index d7af4d161..92e075e46 100644 --- a/web/template/email_test.tmpl +++ b/web/template/email_test.tmpl @@ -22,3 +22,10 @@ This is a test email from {{.InstanceName}} ({{.InstanceURL}}). If you're seeing this email, that means the SMTP configuration is correct! This email was sent by the admin user @{{.SendingUsername}}. +{{- if .Message }} + +The following message was included by the admin user: + +{{ .Message }} +{{- else }} +{{- end }} \ No newline at end of file