mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-12-27 09:18:52 +01:00
[feature] Allow admins to send test emails (#1620)
* [feature] Allow admins to send test emails * implement unwrap on new error type * add + use gtserror types * GoToSocial Email Test -> GoToSocial Test Email * add + use getInstance db call * removed unused "unknown" error type
This commit is contained in:
parent
d5529d6c9f
commit
196cd88b1c
@ -3695,6 +3695,46 @@ paths:
|
||||
summary: View domain block with the given ID.
|
||||
tags:
|
||||
- admin
|
||||
/api/v1/admin/email/test:
|
||||
post:
|
||||
consumes:
|
||||
- multipart/form-data
|
||||
description: |-
|
||||
This can be used to validate an instance's SMTP configuration, and to debug any potential issues.
|
||||
|
||||
If an error is returned by the SMTP connection, this handler will return code 422 to indicate that
|
||||
the request could not be processed, and the SMTP error will be returned to the caller.
|
||||
operationId: testEmailSend
|
||||
parameters:
|
||||
- description: The email address that the test email should be sent to.
|
||||
in: formData
|
||||
name: email
|
||||
type: string
|
||||
produces:
|
||||
- application/json
|
||||
responses:
|
||||
"202":
|
||||
description: Test email was sent.
|
||||
"400":
|
||||
description: bad request
|
||||
"401":
|
||||
description: unauthorized
|
||||
"403":
|
||||
description: forbidden
|
||||
"404":
|
||||
description: not found
|
||||
"406":
|
||||
description: not acceptable
|
||||
"422":
|
||||
description: An smtp occurred while the email attempt was in progress. Check the returned json for more information. The smtp error will be included, to help you debug communication with the smtp server.
|
||||
"500":
|
||||
description: internal server error
|
||||
security:
|
||||
- OAuth2 Bearer:
|
||||
- admin
|
||||
summary: Send a generic test email to a specified email address.
|
||||
tags:
|
||||
- admin
|
||||
/api/v1/admin/media_cleanup:
|
||||
post:
|
||||
consumes:
|
||||
|
@ -25,60 +25,37 @@
|
||||
)
|
||||
|
||||
const (
|
||||
// BasePath is the base API path for this module, excluding the api prefix
|
||||
BasePath = "/v1/admin"
|
||||
// EmojiPath is used for posting/deleting custom emojis.
|
||||
EmojiPath = BasePath + "/custom_emojis"
|
||||
// EmojiPathWithID is used for interacting with a single emoji.
|
||||
EmojiPathWithID = EmojiPath + "/:" + IDKey
|
||||
// EmojiCategoriesPath is used for interacting with emoji categories.
|
||||
EmojiCategoriesPath = EmojiPath + "/categories"
|
||||
// DomainBlocksPath is used for posting domain blocks.
|
||||
DomainBlocksPath = BasePath + "/domain_blocks"
|
||||
// DomainBlocksPathWithID is used for interacting with a single domain block.
|
||||
BasePath = "/v1/admin"
|
||||
EmojiPath = BasePath + "/custom_emojis"
|
||||
EmojiPathWithID = EmojiPath + "/:" + IDKey
|
||||
EmojiCategoriesPath = EmojiPath + "/categories"
|
||||
DomainBlocksPath = BasePath + "/domain_blocks"
|
||||
DomainBlocksPathWithID = DomainBlocksPath + "/:" + IDKey
|
||||
// AccountsPath is used for listing + acting on accounts.
|
||||
AccountsPath = BasePath + "/accounts"
|
||||
// AccountsPathWithID is used for interacting with a single account.
|
||||
AccountsPathWithID = AccountsPath + "/:" + IDKey
|
||||
// AccountsActionPath is used for taking action on a single account.
|
||||
AccountsActionPath = AccountsPathWithID + "/action"
|
||||
MediaCleanupPath = BasePath + "/media_cleanup"
|
||||
MediaRefetchPath = BasePath + "/media_refetch"
|
||||
// ReportsPath is for serving admin view of user reports.
|
||||
ReportsPath = BasePath + "/reports"
|
||||
// ReportsPathWithID is for viewing/acting on one report.
|
||||
ReportsPathWithID = ReportsPath + "/:" + IDKey
|
||||
// ReportsResolvePath is for marking one report as resolved.
|
||||
ReportsResolvePath = ReportsPathWithID + "/resolve"
|
||||
AccountsPath = BasePath + "/accounts"
|
||||
AccountsPathWithID = AccountsPath + "/:" + IDKey
|
||||
AccountsActionPath = AccountsPathWithID + "/action"
|
||||
MediaCleanupPath = BasePath + "/media_cleanup"
|
||||
MediaRefetchPath = BasePath + "/media_refetch"
|
||||
ReportsPath = BasePath + "/reports"
|
||||
ReportsPathWithID = ReportsPath + "/:" + IDKey
|
||||
ReportsResolvePath = ReportsPathWithID + "/resolve"
|
||||
EmailPath = BasePath + "/email"
|
||||
EmailTestPath = EmailPath + "/test"
|
||||
|
||||
// ExportQueryKey is for requesting a public export of some data.
|
||||
ExportQueryKey = "export"
|
||||
// ImportQueryKey is for submitting an import of some data.
|
||||
ImportQueryKey = "import"
|
||||
// IDKey specifies the ID of a single item being interacted with.
|
||||
IDKey = "id"
|
||||
// FilterKey is for applying filters to admin views of accounts, emojis, etc.
|
||||
FilterQueryKey = "filter"
|
||||
// MaxShortcodeDomainKey is the url query for returning emoji results lower (alphabetically)
|
||||
// than the given `[shortcode]@[domain]` parameter.
|
||||
ExportQueryKey = "export"
|
||||
ImportQueryKey = "import"
|
||||
IDKey = "id"
|
||||
FilterQueryKey = "filter"
|
||||
MaxShortcodeDomainKey = "max_shortcode_domain"
|
||||
// MaxShortcodeDomainKey is the url query for returning emoji results higher (alphabetically)
|
||||
// than the given `[shortcode]@[domain]` parameter.
|
||||
MinShortcodeDomainKey = "min_shortcode_domain"
|
||||
// LimitKey is for specifying maximum number of results to return.
|
||||
LimitKey = "limit"
|
||||
// DomainQueryKey is for specifying a domain during admin actions.
|
||||
DomainQueryKey = "domain"
|
||||
// ResolvedKey is for filtering reports by their resolved status
|
||||
ResolvedKey = "resolved"
|
||||
// AccountIDKey is for selecting account in API paths.
|
||||
AccountIDKey = "account_id"
|
||||
// TargetAccountIDKey is for selecting target account in API paths.
|
||||
TargetAccountIDKey = "target_account_id"
|
||||
MaxIDKey = "max_id"
|
||||
SinceIDKey = "since_id"
|
||||
MinIDKey = "min_id"
|
||||
LimitKey = "limit"
|
||||
DomainQueryKey = "domain"
|
||||
ResolvedKey = "resolved"
|
||||
AccountIDKey = "account_id"
|
||||
TargetAccountIDKey = "target_account_id"
|
||||
MaxIDKey = "max_id"
|
||||
SinceIDKey = "since_id"
|
||||
MinIDKey = "min_id"
|
||||
)
|
||||
|
||||
type Module struct {
|
||||
@ -117,4 +94,7 @@ func (m *Module) Route(attachHandler func(method string, path string, f ...gin.H
|
||||
attachHandler(http.MethodGet, ReportsPath, m.ReportsGETHandler)
|
||||
attachHandler(http.MethodGet, ReportsPathWithID, m.ReportGETHandler)
|
||||
attachHandler(http.MethodPost, ReportsResolvePath, m.ReportResolvePOSTHandler)
|
||||
|
||||
// email stuff
|
||||
attachHandler(http.MethodPost, EmailTestPath, m.EmailTestPOSTHandler)
|
||||
}
|
||||
|
120
internal/api/client/admin/emailtest.go
Normal file
120
internal/api/client/admin/emailtest.go
Normal file
@ -0,0 +1,120 @@
|
||||
// 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/>.
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/mail"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
|
||||
apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/oauth"
|
||||
)
|
||||
|
||||
// EmailTestPostHandler swagger:operation POST /api/v1/admin/email/test testEmailSend
|
||||
//
|
||||
// Send a generic test email to a specified email address.
|
||||
//
|
||||
// This can be used to validate an instance's SMTP configuration, and to debug any potential issues.
|
||||
//
|
||||
// If an error is returned by the SMTP connection, this handler will return code 422 to indicate that
|
||||
// the request could not be processed, and the SMTP error will be returned to the caller.
|
||||
//
|
||||
// ---
|
||||
// tags:
|
||||
// - admin
|
||||
//
|
||||
// consumes:
|
||||
// - multipart/form-data
|
||||
//
|
||||
// produces:
|
||||
// - application/json
|
||||
//
|
||||
// parameters:
|
||||
// -
|
||||
// name: email
|
||||
// in: formData
|
||||
// description: The email address that the test email should be sent to.
|
||||
// type: string
|
||||
//
|
||||
// security:
|
||||
// - OAuth2 Bearer:
|
||||
// - admin
|
||||
//
|
||||
// responses:
|
||||
// '202':
|
||||
// description: Test email was sent.
|
||||
// '400':
|
||||
// description: bad request
|
||||
// '401':
|
||||
// description: unauthorized
|
||||
// '403':
|
||||
// description: forbidden
|
||||
// '404':
|
||||
// description: not found
|
||||
// '406':
|
||||
// description: not acceptable
|
||||
// '422':
|
||||
// description: >-
|
||||
// An smtp occurred while the email attempt was in progress.
|
||||
// Check the returned json for more information. The smtp error
|
||||
// will be included, to help you debug communication with the
|
||||
// smtp server.
|
||||
// '500':
|
||||
// description: internal server error
|
||||
func (m *Module) EmailTestPOSTHandler(c *gin.Context) {
|
||||
authed, err := oauth.Authed(c, true, true, true, true)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorUnauthorized(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if !*authed.User.Admin {
|
||||
err := fmt.Errorf("user %s not an admin", authed.User.ID)
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorForbidden(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err := apiutil.NegotiateAccept(c, apiutil.JSONAcceptHeaders...); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
form := &apimodel.AdminSendTestEmailRequest{}
|
||||
if err := c.ShouldBind(form); err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
email, err := mail.ParseAddress(form.Email)
|
||||
if err != nil {
|
||||
apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
errWithCode := m.processor.Admin().EmailTest(c.Request.Context(), authed.Account, email.Address)
|
||||
if errWithCode != nil {
|
||||
apiutil.ErrorHandler(c, errWithCode, m.processor.InstanceGetV1)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusAccepted, gin.H{"status": "test email sent"})
|
||||
}
|
@ -183,3 +183,9 @@ type MediaCleanupRequest struct {
|
||||
// If value is not specified, the value of media-remote-cache-days in the server config will be used.
|
||||
RemoteCacheDays *int `form:"remote_cache_days" json:"remote_cache_days" xml:"remote_cache_days"`
|
||||
}
|
||||
|
||||
// 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"`
|
||||
}
|
||||
|
@ -97,6 +97,20 @@ func (i *instanceDB) CountInstanceDomains(ctx context.Context, domain string) (i
|
||||
return count, nil
|
||||
}
|
||||
|
||||
func (i *instanceDB) GetInstance(ctx context.Context, domain string) (*gtsmodel.Instance, db.Error) {
|
||||
instance := >smodel.Instance{}
|
||||
|
||||
if err := i.conn.
|
||||
NewSelect().
|
||||
Model(instance).
|
||||
Where("? = ?", bun.Ident("instance.domain"), domain).
|
||||
Scan(ctx); err != nil {
|
||||
return nil, i.conn.ProcessError(err)
|
||||
}
|
||||
|
||||
return instance, nil
|
||||
}
|
||||
|
||||
func (i *instanceDB) GetInstancePeers(ctx context.Context, includeSuspended bool) ([]*gtsmodel.Instance, db.Error) {
|
||||
instances := []*gtsmodel.Instance{}
|
||||
|
||||
|
@ -23,6 +23,7 @@
|
||||
|
||||
"github.com/stretchr/testify/suite"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||
)
|
||||
|
||||
type InstanceTestSuite struct {
|
||||
@ -59,6 +60,18 @@ func (suite *InstanceTestSuite) TestCountInstanceDomains() {
|
||||
suite.Equal(2, count)
|
||||
}
|
||||
|
||||
func (suite *InstanceTestSuite) TestGetInstanceOK() {
|
||||
instance, err := suite.db.GetInstance(context.Background(), "localhost:8080")
|
||||
suite.NoError(err)
|
||||
suite.NotNil(instance)
|
||||
}
|
||||
|
||||
func (suite *InstanceTestSuite) TestGetInstanceNonexistent() {
|
||||
instance, err := suite.db.GetInstance(context.Background(), "doesnt.exist.com")
|
||||
suite.ErrorIs(err, db.ErrNoEntries)
|
||||
suite.Nil(instance)
|
||||
}
|
||||
|
||||
func (suite *InstanceTestSuite) TestGetInstancePeers() {
|
||||
peers, err := suite.db.GetInstancePeers(context.Background(), false)
|
||||
suite.NoError(err)
|
||||
|
@ -34,6 +34,9 @@ type Instance interface {
|
||||
// CountInstanceDomains returns the number of known instances known that the given domain federates with.
|
||||
CountInstanceDomains(ctx context.Context, domain string) (int, Error)
|
||||
|
||||
// GetInstance returns the instance entry for the given domain, if it exists.
|
||||
GetInstance(ctx context.Context, domain string) (*gtsmodel.Instance, Error)
|
||||
|
||||
// GetInstanceAccounts returns a slice of accounts from the given instance, arranged by ID.
|
||||
GetInstanceAccounts(ctx context.Context, domain string, maxID string, limit int) ([]*gtsmodel.Account, Error)
|
||||
|
||||
|
@ -21,8 +21,7 @@
|
||||
"bytes"
|
||||
"net/smtp"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/log"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -30,21 +29,6 @@
|
||||
confirmSubject = "GoToSocial Email Confirmation"
|
||||
)
|
||||
|
||||
func (s *sender) SendConfirmEmail(toAddress string, data ConfirmData) error {
|
||||
buf := &bytes.Buffer{}
|
||||
if err := s.template.ExecuteTemplate(buf, confirmTemplate, data); err != nil {
|
||||
return err
|
||||
}
|
||||
confirmBody := buf.String()
|
||||
|
||||
msg, err := assembleMessage(confirmSubject, confirmBody, toAddress, s.from)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Trace(nil, s.hostAddress+"\n"+config.GetSMTPUsername()+":password"+"\n"+s.from+"\n"+toAddress+"\n\n"+string(msg)+"\n")
|
||||
return smtp.SendMail(s.hostAddress, s.auth, s.from, []string{toAddress}, msg)
|
||||
}
|
||||
|
||||
// ConfirmData represents data passed into the confirm email address template.
|
||||
type ConfirmData struct {
|
||||
// Username to be addressed.
|
||||
@ -57,3 +41,22 @@ type ConfirmData struct {
|
||||
// Should be a full link with protocol eg., https://example.org/confirm_email?token=some-long-token
|
||||
ConfirmLink string
|
||||
}
|
||||
|
||||
func (s *sender) SendConfirmEmail(toAddress string, data ConfirmData) error {
|
||||
buf := &bytes.Buffer{}
|
||||
if err := s.template.ExecuteTemplate(buf, confirmTemplate, data); err != nil {
|
||||
return err
|
||||
}
|
||||
confirmBody := buf.String()
|
||||
|
||||
msg, err := assembleMessage(confirmSubject, confirmBody, toAddress, s.from)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := smtp.SendMail(s.hostAddress, s.auth, s.from, []string{toAddress}, msg); err != nil {
|
||||
return gtserror.SetType(err, gtserror.TypeSMTP)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -88,3 +88,24 @@ func (s *noopSender) SendResetEmail(toAddress string, data ResetData) error {
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *noopSender) SendTestEmail(toAddress string, data TestData) error {
|
||||
buf := &bytes.Buffer{}
|
||||
if err := s.template.ExecuteTemplate(buf, testTemplate, data); err != nil {
|
||||
return err
|
||||
}
|
||||
testBody := buf.String()
|
||||
|
||||
msg, err := assembleMessage(testSubject, testBody, toAddress, "test@example.org")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
log.Tracef(nil, "NOT SENDING test email to %s with contents: %s", toAddress, msg)
|
||||
|
||||
if s.sendCallback != nil {
|
||||
s.sendCallback(toAddress, string(msg))
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -20,6 +20,8 @@
|
||||
import (
|
||||
"bytes"
|
||||
"net/smtp"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -27,20 +29,6 @@
|
||||
resetSubject = "GoToSocial Password Reset"
|
||||
)
|
||||
|
||||
func (s *sender) SendResetEmail(toAddress string, data ResetData) error {
|
||||
buf := &bytes.Buffer{}
|
||||
if err := s.template.ExecuteTemplate(buf, resetTemplate, data); err != nil {
|
||||
return err
|
||||
}
|
||||
resetBody := buf.String()
|
||||
|
||||
msg, err := assembleMessage(resetSubject, resetBody, toAddress, s.from)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return smtp.SendMail(s.hostAddress, s.auth, s.from, []string{toAddress}, msg)
|
||||
}
|
||||
|
||||
// ResetData represents data passed into the reset email address template.
|
||||
type ResetData struct {
|
||||
// Username to be addressed.
|
||||
@ -53,3 +41,22 @@ type ResetData struct {
|
||||
// Should be a full link with protocol eg., https://example.org/reset_password?token=some-reset-password-token
|
||||
ResetLink string
|
||||
}
|
||||
|
||||
func (s *sender) SendResetEmail(toAddress string, data ResetData) error {
|
||||
buf := &bytes.Buffer{}
|
||||
if err := s.template.ExecuteTemplate(buf, resetTemplate, data); err != nil {
|
||||
return err
|
||||
}
|
||||
resetBody := buf.String()
|
||||
|
||||
msg, err := assembleMessage(resetSubject, resetBody, toAddress, s.from)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := smtp.SendMail(s.hostAddress, s.auth, s.from, []string{toAddress}, msg); err != nil {
|
||||
return gtserror.SetType(err, gtserror.TypeSMTP)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
@ -32,6 +32,9 @@ type Sender interface {
|
||||
|
||||
// SendResetEmail sends a 'reset your password' style email to the given toAddress, with the given data.
|
||||
SendResetEmail(toAddress string, data ResetData) error
|
||||
|
||||
// SendTestEmail sends a 'testing email sending' style email to the given toAddress, with the given data.
|
||||
SendTestEmail(toAddress string, data TestData) error
|
||||
}
|
||||
|
||||
// NewSender returns a new email Sender interface with the given configuration, or an error if something goes wrong.
|
||||
|
58
internal/email/test.go
Normal file
58
internal/email/test.go
Normal file
@ -0,0 +1,58 @@
|
||||
// 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/>.
|
||||
|
||||
package email
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"net/smtp"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
)
|
||||
|
||||
const (
|
||||
testTemplate = "email_test_text.tmpl"
|
||||
testSubject = "GoToSocial Test Email"
|
||||
)
|
||||
|
||||
type TestData struct {
|
||||
// Username of admin user who sent the test.
|
||||
SendingUsername string
|
||||
// URL of the instance to present to the receiver.
|
||||
InstanceURL string
|
||||
// Name of the instance to present to the receiver.
|
||||
InstanceName string
|
||||
}
|
||||
|
||||
func (s *sender) SendTestEmail(toAddress string, data TestData) error {
|
||||
buf := &bytes.Buffer{}
|
||||
if err := s.template.ExecuteTemplate(buf, testTemplate, data); err != nil {
|
||||
return err
|
||||
}
|
||||
testBody := buf.String()
|
||||
|
||||
msg, err := assembleMessage(testSubject, testBody, toAddress, s.from)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := smtp.SendMail(s.hostAddress, s.auth, s.from, []string{toAddress}, msg); err != nil {
|
||||
return gtserror.SetType(err, gtserror.TypeSMTP)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -24,11 +24,18 @@
|
||||
// package private error key type.
|
||||
type errkey int
|
||||
|
||||
// ErrorType denotes the type of an error, if set.
|
||||
type ErrorType string
|
||||
|
||||
const (
|
||||
// error value keys.
|
||||
_ errkey = iota
|
||||
statusCodeKey
|
||||
notFoundKey
|
||||
errorTypeKey
|
||||
|
||||
// error types
|
||||
TypeSMTP ErrorType = "smtp" // smtp (mail) error
|
||||
)
|
||||
|
||||
// StatusCode checks error for a stored status code value. For example
|
||||
@ -57,3 +64,17 @@ func NotFound(err error) bool {
|
||||
func SetNotFound(err error) error {
|
||||
return errors.WithValue(err, notFoundKey, struct{}{})
|
||||
}
|
||||
|
||||
// Type checks error for a stored "type" value. For example
|
||||
// an error from sending an email may set a value of "smtp"
|
||||
// to indicate this was an SMTP error.
|
||||
func Type(err error) ErrorType {
|
||||
s, _ := errors.Value(err, errorTypeKey).(ErrorType)
|
||||
return s
|
||||
}
|
||||
|
||||
// SetType will wrap the given error to store a "type" value,
|
||||
// returning wrapped error. See Type() for example use-cases.
|
||||
func SetType(err error, errType ErrorType) error {
|
||||
return errors.WithValue(err, errorTypeKey, errType)
|
||||
}
|
||||
|
@ -18,6 +18,7 @@
|
||||
package admin
|
||||
|
||||
import (
|
||||
"github.com/superseriousbusiness/gotosocial/internal/email"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/media"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/state"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/transport"
|
||||
@ -29,14 +30,16 @@ type Processor struct {
|
||||
tc typeutils.TypeConverter
|
||||
mediaManager media.Manager
|
||||
transportController transport.Controller
|
||||
emailSender email.Sender
|
||||
}
|
||||
|
||||
// New returns a new admin processor.
|
||||
func New(state *state.State, tc typeutils.TypeConverter, mediaManager media.Manager, transportController transport.Controller) Processor {
|
||||
func New(state *state.State, tc typeutils.TypeConverter, mediaManager media.Manager, transportController transport.Controller, emailSender email.Sender) Processor {
|
||||
return Processor{
|
||||
state: state,
|
||||
tc: tc,
|
||||
mediaManager: mediaManager,
|
||||
transportController: transportController,
|
||||
emailSender: emailSender,
|
||||
}
|
||||
}
|
||||
|
61
internal/processing/admin/email.go
Normal file
61
internal/processing/admin/email.go
Normal file
@ -0,0 +1,61 @@
|
||||
// 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/>.
|
||||
|
||||
package admin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/email"
|
||||
"github.com/superseriousbusiness/gotosocial/internal/gtserror"
|
||||
"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 {
|
||||
// Pull our instance entry from the database,
|
||||
// so we can greet the email recipient nicely.
|
||||
instance, err := p.state.DB.GetInstance(ctx, config.GetHost())
|
||||
if err != nil {
|
||||
err = fmt.Errorf("SendConfirmEmail: error getting instance: %s", err)
|
||||
return gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
testData := email.TestData{
|
||||
SendingUsername: account.Username,
|
||||
InstanceURL: instance.URI,
|
||||
InstanceName: instance.Title,
|
||||
}
|
||||
|
||||
if err := p.emailSender.SendTestEmail(toAddress, testData); err != nil {
|
||||
if errorType := gtserror.Type(err); errorType == gtserror.TypeSMTP {
|
||||
// An error occurred during the SMTP part.
|
||||
// We should indicate this to the caller, as
|
||||
// it will likely help them debug the issue.
|
||||
return gtserror.NewErrorUnprocessableEntity(err, err.Error())
|
||||
}
|
||||
// An actual error has occurred.
|
||||
return gtserror.NewErrorInternalError(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -125,7 +125,7 @@ func NewProcessor(
|
||||
|
||||
// sub processors
|
||||
processor.account = account.New(state, tc, mediaManager, oauthServer, federator, parseMentionFunc)
|
||||
processor.admin = admin.New(state, tc, mediaManager, federator.TransportController())
|
||||
processor.admin = admin.New(state, tc, mediaManager, federator.TransportController(), emailSender)
|
||||
processor.fedi = fedi.New(state, tc, federator)
|
||||
processor.media = media.New(state, tc, mediaManager, federator.TransportController())
|
||||
processor.report = report.New(state, tc)
|
||||
|
24
web/template/email_test_text.tmpl
Normal file
24
web/template/email_test_text.tmpl
Normal file
@ -0,0 +1,24 @@
|
||||
{{- /*
|
||||
// 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/>.
|
||||
*/ -}}
|
||||
|
||||
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}}.
|
Loading…
Reference in New Issue
Block a user