mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-11-27 19:01:01 +00:00
[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:
parent
1e7b32490d
commit
a276b1ca06
18 changed files with 276 additions and 52 deletions
|
@ -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.
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
73
web/source/settings/lib/query/admin/actions/index.ts
Normal file
73
web/source/settings/lib/query/admin/actions/index.ts
Normal 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,
|
||||
};
|
|
@ -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,
|
||||
|
|
29
web/source/settings/views/admin/actions/email/index.tsx
Normal file
29
web/source/settings/views/admin/actions/email/index.tsx
Normal 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>
|
||||
);
|
||||
}
|
77
web/source/settings/views/admin/actions/email/test.tsx
Normal file
77
web/source/settings/views/admin/actions/email/test.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -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}>
|
||||
<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.
|
||||
</p>
|
||||
<div className="form-section-docs">
|
||||
<h2>Expire remote instance keys</h2>
|
||||
<p>
|
||||
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"
|
||||
|
|
|
@ -22,9 +22,8 @@ import ExpireRemote from "./expireremote";
|
|||
|
||||
export default function Keys() {
|
||||
return (
|
||||
<>
|
||||
<h1>Key Actions</h1>
|
||||
<div className="admin-actions-keys">
|
||||
<ExpireRemote />
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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}>
|
||||
<h2>Cleanup</h2>
|
||||
<p>
|
||||
<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>
|
||||
</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"
|
||||
|
|
|
@ -22,9 +22,8 @@ import Cleanup from "./cleanup";
|
|||
|
||||
export default function Media() {
|
||||
return (
|
||||
<>
|
||||
<h1>Media Actions</h1>
|
||||
<div className="admin-actions-media">
|
||||
<Cleanup />
|
||||
</>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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 }}
|
Loading…
Reference in a new issue