[feature/frontend] Let admins send test email to validate SMTP config (#2934)

* [feature/frontend] Let admins send test email to validate SMTP config

* wee
This commit is contained in:
tobi 2024-05-27 19:03:54 +02:00 committed by GitHub
parent 1e7b32490d
commit a276b1ca06
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 276 additions and 52 deletions

View file

@ -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.

View file

@ -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

View file

@ -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:

View file

@ -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

View file

@ -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 {

View file

@ -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.

View file

@ -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,
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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<any, { email: string, message?: string }>({
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,
};

View file

@ -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<AdminAccount, string>({
query: (id) => ({
url: `/api/v1/admin/accounts/${id}`
@ -214,8 +194,6 @@ const extended = gtsApi.injectEndpoints({
export const {
useUpdateInstanceMutation,
useMediaCleanupMutation,
useInstanceKeysExpireMutation,
useGetAccountQuery,
useLazyGetAccountQuery,
useActionAccountMutation,

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
import React from "react";
import Test from "./test";
export default function Email() {
return (
<div className="admin-actions-email">
<Test />
</div>
);
}

View file

@ -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 <http://www.gnu.org/licenses/>.
*/
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 (
<form onSubmit={submit}>
<div className="form-section-docs">
<h2>Send test email</h2>
<p>
To check whether your instance email configuration is correct, you can
try sending a test email to the given address, with an optional message.
<br/>
If you do not have SMTP configured for your instance, this will do nothing.
</p>
<a
href="https://docs.gotosocial.org/en/latest/configuration/smtp/"
target="_blank"
className="docslink"
rel="noreferrer"
>
Learn more about SMTP configuration (opens in a new tab)
</a>
</div>
<TextInput
field={form.email}
label="Email"
placeholder="someone@example.org"
// Get email validation for free.
type="email"
required={true}
/>
<TextInput
field={form.message}
label="Message (optional)"
placeholder="Please disregard this test email, thanks!"
/>
<MutationButton
disabled={!form.email.value}
label="Send"
result={result}
/>
</form>
);
}

View file

@ -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 (
<form onSubmit={submitExpire}>
<div className="form-section-docs">
<h2>Expire remote instance keys</h2>
<p>
Mark all public keys from the given remote instance as expired.<br/><br/>
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.
Mark all public keys from the given remote instance as expired.
<br/>
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.
<br/>
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.
</p>
</div>
<TextInput
field={domainField}
label="Domain"

View file

@ -22,9 +22,8 @@ import ExpireRemote from "./expireremote";
export default function Keys() {
return (
<>
<h1>Key Actions</h1>
<div className="admin-actions-keys">
<ExpireRemote />
</>
</div>
);
}

View file

@ -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 (
<form onSubmit={submitCleanup}>
<div className="form-section-docs">
<h2>Cleanup</h2>
<p>
Clean up remote media older than the specified number of days.
<br/>
If the remote instance is still online they will be refetched when needed.
<br/>
Also cleans up unused headers and avatars from the media cache.
</p>
<a
href="https://docs.gotosocial.org/en/latest/admin/media_caching/"
target="_blank"
className="docslink"
rel="noreferrer"
>
Learn more about media caching + cleanup (opens in a new tab)
</a>
</div>
<TextInput
field={daysField}
label="Days"

View file

@ -22,9 +22,8 @@ import Cleanup from "./cleanup";
export default function Media() {
return (
<>
<h1>Media Actions</h1>
<div className="admin-actions-media">
<Cleanup />
</>
</div>
);
}

View file

@ -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() {
<MenuItem
name="Actions"
itemUrl="actions"
defaultChild="media"
defaultChild="email"
icon="fa-bolt"
>
<MenuItem
name="Email"
itemUrl="email"
icon="fa-email-bulk"
/>
<MenuItem
name="Media"
itemUrl="media"

View file

@ -31,6 +31,7 @@ import EmojiDetail from "./emoji/local/detail";
import RemoteEmoji from "./emoji/remote";
import HeaderPermsOverview from "./http-header-permissions/overview";
import HeaderPermDetail from "./http-header-permissions/detail";
import Email from "./actions/email";
/*
EXPORTED COMPONENTS
@ -47,6 +48,7 @@ import HeaderPermDetail from "./http-header-permissions/detail";
* - /settings/admin/actions
* - /settings/admin/actions/media
* - /settings/admin/actions/keys
* - /settings/admin/actions/email
* - /settings/admin/http-header-permissions/allows
* - /settings/admin/http-header-permissions/allows/:allowId
* - /settings/admin/http-header-permissions/blocks
@ -108,6 +110,7 @@ function AdminEmojisRouter() {
/**
* - /settings/admin/actions
* - /settings/admin/actions/email
* - /settings/admin/actions/media
* - /settings/admin/actions/keys
*/
@ -121,9 +124,10 @@ function AdminActionsRouter() {
<Router base={thisBase}>
<ErrorBoundary>
<Switch>
<Route path="/email" component={Email} />
<Route path="/media" component={Media} />
<Route path="/keys" component={Keys} />
<Route><Redirect to="/media" /></Route>
<Route><Redirect to="/email" /></Route>
</Switch>
</ErrorBoundary>
</Router>

View file

@ -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 }}