diff options
author | 2023-03-19 13:11:46 +0100 | |
---|---|---|
committer | 2023-03-19 13:11:46 +0100 | |
commit | 7db81cde444f6bc95e79527af0997de1788d48c7 (patch) | |
tree | f6c077ec298a4f018d0870798bc46bd64ba70069 /internal | |
parent | [docs] Update docs on how to login (#1626) (diff) | |
download | gotosocial-7db81cde444f6bc95e79527af0997de1788d48c7.tar.xz |
[feature] Email notifications for new / closed moderation reports (#1628)
* start fiddling about with email sending to allow multiple recipients
* do some fiddling
* notifs working
* notify on closed report
* finishing up
* envparsing
* use strings.ContainsAny
Diffstat (limited to 'internal')
-rw-r--r-- | internal/config/config.go | 11 | ||||
-rw-r--r-- | internal/config/defaults.go | 11 | ||||
-rw-r--r-- | internal/config/flags.go | 1 | ||||
-rw-r--r-- | internal/config/helpers.gen.go | 25 | ||||
-rw-r--r-- | internal/db/bundb/instance.go | 31 | ||||
-rw-r--r-- | internal/db/bundb/instance_test.go | 38 | ||||
-rw-r--r-- | internal/db/instance.go | 4 | ||||
-rw-r--r-- | internal/email/common.go | 112 | ||||
-rw-r--r-- | internal/email/confirm.go | 26 | ||||
-rw-r--r-- | internal/email/email_test.go | 152 | ||||
-rw-r--r-- | internal/email/noopsender.go | 54 | ||||
-rw-r--r-- | internal/email/report.go | 64 | ||||
-rw-r--r-- | internal/email/reset.go | 26 | ||||
-rw-r--r-- | internal/email/sender.go | 11 | ||||
-rw-r--r-- | internal/email/test.go | 26 | ||||
-rw-r--r-- | internal/email/util.go | 71 | ||||
-rw-r--r-- | internal/email/util_test.go | 59 | ||||
-rw-r--r-- | internal/processing/admin/report.go | 16 | ||||
-rw-r--r-- | internal/processing/fromclientapi.go | 31 | ||||
-rw-r--r-- | internal/processing/fromcommon.go | 93 | ||||
-rw-r--r-- | internal/processing/fromfederator.go | 13 | ||||
-rw-r--r-- | internal/processing/processor.go | 6 |
22 files changed, 619 insertions, 262 deletions
diff --git a/internal/config/config.go b/internal/config/config.go index 21c1ca470..a1e00ea8d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -126,11 +126,12 @@ type Configuration struct { OIDCLinkExisting bool `name:"oidc-link-existing" usage:"link existing user accounts to OIDC logins based on the stored email value"` OIDCAdminGroups []string `name:"oidc-admin-groups" usage:"Membership of one of the listed groups makes someone a GtS admin"` - SMTPHost string `name:"smtp-host" usage:"Host of the smtp server. Eg., 'smtp.eu.mailgun.org'"` - SMTPPort int `name:"smtp-port" usage:"Port of the smtp server. Eg., 587"` - SMTPUsername string `name:"smtp-username" usage:"Username to authenticate with the smtp server as. Eg., 'postmaster@mail.example.org'"` - SMTPPassword string `name:"smtp-password" usage:"Password to pass to the smtp server."` - SMTPFrom string `name:"smtp-from" usage:"Address to use as the 'from' field of the email. Eg., 'gotosocial@example.org'"` + SMTPHost string `name:"smtp-host" usage:"Host of the smtp server. Eg., 'smtp.eu.mailgun.org'"` + SMTPPort int `name:"smtp-port" usage:"Port of the smtp server. Eg., 587"` + SMTPUsername string `name:"smtp-username" usage:"Username to authenticate with the smtp server as. Eg., 'postmaster@mail.example.org'"` + SMTPPassword string `name:"smtp-password" usage:"Password to pass to the smtp server."` + SMTPFrom string `name:"smtp-from" usage:"Address to use as the 'from' field of the email. Eg., 'gotosocial@example.org'"` + SMTPDiscloseRecipients bool `name:"smtp-disclose-recipients" usage:"If true, email notifications sent to multiple recipients will be To'd to every recipient at once. If false, recipients will not be disclosed"` SyslogEnabled bool `name:"syslog-enabled" usage:"Enable the syslog logging hook. Logs will be mirrored to the configured destination."` SyslogProtocol string `name:"syslog-protocol" usage:"Protocol to use when directing logs to syslog. Leave empty to connect to local syslog."` diff --git a/internal/config/defaults.go b/internal/config/defaults.go index b687b43f1..17cc71086 100644 --- a/internal/config/defaults.go +++ b/internal/config/defaults.go @@ -102,11 +102,12 @@ var Defaults = Configuration{ OIDCScopes: []string{oidc.ScopeOpenID, "profile", "email", "groups"}, OIDCLinkExisting: false, - SMTPHost: "", - SMTPPort: 0, - SMTPUsername: "", - SMTPPassword: "", - SMTPFrom: "GoToSocial", + SMTPHost: "", + SMTPPort: 0, + SMTPUsername: "", + SMTPPassword: "", + SMTPFrom: "GoToSocial", + SMTPDiscloseRecipients: false, SyslogEnabled: false, SyslogProtocol: "udp", diff --git a/internal/config/flags.go b/internal/config/flags.go index 09e927785..e9925ded0 100644 --- a/internal/config/flags.go +++ b/internal/config/flags.go @@ -132,6 +132,7 @@ func (s *ConfigState) AddServerFlags(cmd *cobra.Command) { cmd.Flags().String(SMTPUsernameFlag(), cfg.SMTPUsername, fieldtag("SMTPUsername", "usage")) cmd.Flags().String(SMTPPasswordFlag(), cfg.SMTPPassword, fieldtag("SMTPPassword", "usage")) cmd.Flags().String(SMTPFromFlag(), cfg.SMTPFrom, fieldtag("SMTPFrom", "usage")) + cmd.Flags().Bool(SMTPDiscloseRecipientsFlag(), cfg.SMTPDiscloseRecipients, fieldtag("SMTPDiscloseRecipients", "usage")) // Syslog cmd.Flags().Bool(SyslogEnabledFlag(), cfg.SyslogEnabled, fieldtag("SyslogEnabled", "usage")) diff --git a/internal/config/helpers.gen.go b/internal/config/helpers.gen.go index 53553d851..6fc195ad0 100644 --- a/internal/config/helpers.gen.go +++ b/internal/config/helpers.gen.go @@ -1924,6 +1924,31 @@ func GetSMTPFrom() string { return global.GetSMTPFrom() } // SetSMTPFrom safely sets the value for global configuration 'SMTPFrom' field func SetSMTPFrom(v string) { global.SetSMTPFrom(v) } +// GetSMTPDiscloseRecipients safely fetches the Configuration value for state's 'SMTPDiscloseRecipients' field +func (st *ConfigState) GetSMTPDiscloseRecipients() (v bool) { + st.mutex.Lock() + v = st.config.SMTPDiscloseRecipients + st.mutex.Unlock() + return +} + +// SetSMTPDiscloseRecipients safely sets the Configuration value for state's 'SMTPDiscloseRecipients' field +func (st *ConfigState) SetSMTPDiscloseRecipients(v bool) { + st.mutex.Lock() + defer st.mutex.Unlock() + st.config.SMTPDiscloseRecipients = v + st.reloadToViper() +} + +// SMTPDiscloseRecipientsFlag returns the flag name for the 'SMTPDiscloseRecipients' field +func SMTPDiscloseRecipientsFlag() string { return "smtp-disclose-recipients" } + +// GetSMTPDiscloseRecipients safely fetches the value for global configuration 'SMTPDiscloseRecipients' field +func GetSMTPDiscloseRecipients() bool { return global.GetSMTPDiscloseRecipients() } + +// SetSMTPDiscloseRecipients safely sets the value for global configuration 'SMTPDiscloseRecipients' field +func SetSMTPDiscloseRecipients(v bool) { global.SetSMTPDiscloseRecipients(v) } + // GetSyslogEnabled safely fetches the Configuration value for state's 'SyslogEnabled' field func (st *ConfigState) GetSyslogEnabled() (v bool) { st.mutex.Lock() diff --git a/internal/db/bundb/instance.go b/internal/db/bundb/instance.go index b4bdeb1d9..4fa898639 100644 --- a/internal/db/bundb/instance.go +++ b/internal/db/bundb/instance.go @@ -156,3 +156,34 @@ func (i *instanceDB) GetInstanceAccounts(ctx context.Context, domain string, max return accounts, nil } + +func (i *instanceDB) GetInstanceModeratorAddresses(ctx context.Context) ([]string, db.Error) { + addresses := []string{} + + // Select email addresses of approved, confirmed, + // and enabled moderators or admins. + + q := i.conn. + NewSelect(). + TableExpr("? AS ?", bun.Ident("users"), bun.Ident("user")). + Column("user.email"). + Where("? = ?", bun.Ident("user.approved"), true). + Where("? IS NOT NULL", bun.Ident("user.confirmed_at")). + Where("? = ?", bun.Ident("user.disabled"), false). + WhereGroup(" AND ", func(q *bun.SelectQuery) *bun.SelectQuery { + return q. + Where("? = ?", bun.Ident("user.moderator"), true). + WhereOr("? = ?", bun.Ident("user.admin"), true) + }). + OrderExpr("? ASC", bun.Ident("user.email")) + + if err := q.Scan(ctx, &addresses); err != nil { + return nil, i.conn.ProcessError(err) + } + + if len(addresses) == 0 { + return nil, db.ErrNoEntries + } + + return addresses, nil +} diff --git a/internal/db/bundb/instance_test.go b/internal/db/bundb/instance_test.go index 4269df5ca..580a7699b 100644 --- a/internal/db/bundb/instance_test.go +++ b/internal/db/bundb/instance_test.go @@ -24,6 +24,8 @@ import ( "github.com/stretchr/testify/suite" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/testrig" ) type InstanceTestSuite struct { @@ -90,6 +92,42 @@ func (suite *InstanceTestSuite) TestGetInstanceAccounts() { suite.Len(accounts, 1) } +func (suite *InstanceTestSuite) TestGetInstanceModeratorAddressesOK() { + // We have one admin user by default. + addresses, err := suite.db.GetInstanceModeratorAddresses(context.Background()) + suite.NoError(err) + suite.EqualValues([]string{"admin@example.org"}, addresses) +} + +func (suite *InstanceTestSuite) TestGetInstanceModeratorAddressesZorkAsModerator() { + // Promote zork to moderator role. + testUser := >smodel.User{} + *testUser = *suite.testUsers["local_account_1"] + testUser.Moderator = testrig.TrueBool() + if err := suite.db.UpdateUser(context.Background(), testUser, "moderator"); err != nil { + suite.FailNow(err.Error()) + } + + addresses, err := suite.db.GetInstanceModeratorAddresses(context.Background()) + suite.NoError(err) + suite.EqualValues([]string{"admin@example.org", "zork@example.org"}, addresses) +} + +func (suite *InstanceTestSuite) TestGetInstanceModeratorAddressesNoAdmin() { + // Demote admin from admin + moderator roles. + testUser := >smodel.User{} + *testUser = *suite.testUsers["admin_account"] + testUser.Admin = testrig.FalseBool() + testUser.Moderator = testrig.FalseBool() + if err := suite.db.UpdateUser(context.Background(), testUser, "admin", "moderator"); err != nil { + suite.FailNow(err.Error()) + } + + addresses, err := suite.db.GetInstanceModeratorAddresses(context.Background()) + suite.ErrorIs(err, db.ErrNoEntries) + suite.Empty(addresses) +} + func TestInstanceTestSuite(t *testing.T) { suite.Run(t, new(InstanceTestSuite)) } diff --git a/internal/db/instance.go b/internal/db/instance.go index dff471193..3166a0a18 100644 --- a/internal/db/instance.go +++ b/internal/db/instance.go @@ -42,4 +42,8 @@ type Instance interface { // GetInstancePeers returns a slice of instances that the host instance knows about. GetInstancePeers(ctx context.Context, includeSuspended bool) ([]*gtsmodel.Instance, Error) + + // GetInstanceModeratorAddresses returns a slice of email addresses belonging to active + // (as in, not suspended) moderators + admins on this instance. + GetInstanceModeratorAddresses(ctx context.Context) ([]string, Error) } diff --git a/internal/email/common.go b/internal/email/common.go new file mode 100644 index 000000000..ab4176895 --- /dev/null +++ b/internal/email/common.go @@ -0,0 +1,112 @@ +// 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" + "errors" + "fmt" + "net/smtp" + "os" + "path/filepath" + "strings" + "text/template" + + "github.com/superseriousbusiness/gotosocial/internal/config" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" +) + +func (s *sender) sendTemplate(template string, subject string, data any, toAddresses ...string) error { + buf := &bytes.Buffer{} + if err := s.template.ExecuteTemplate(buf, template, data); err != nil { + return err + } + + msg, err := assembleMessage(subject, buf.String(), s.from, toAddresses...) + if err != nil { + return err + } + + if err := smtp.SendMail(s.hostAddress, s.auth, s.from, toAddresses, msg); err != nil { + return gtserror.SetType(err, gtserror.TypeSMTP) + } + + return nil +} + +func loadTemplates(templateBaseDir string) (*template.Template, error) { + if !filepath.IsAbs(templateBaseDir) { + cwd, err := os.Getwd() + if err != nil { + return nil, fmt.Errorf("error getting current working directory: %s", err) + } + templateBaseDir = filepath.Join(cwd, templateBaseDir) + } + + // look for all templates that start with 'email_' + return template.ParseGlob(filepath.Join(templateBaseDir, "email_*")) +} + +// assembleMessage assembles a valid email message following: +// - https://datatracker.ietf.org/doc/html/rfc2822 +// - https://pkg.go.dev/net/smtp#SendMail +func assembleMessage(mailSubject string, mailBody string, mailFrom string, mailTo ...string) ([]byte, error) { + if strings.ContainsAny(mailSubject, "\r\n") { + return nil, errors.New("email subject must not contain newline characters") + } + + if strings.ContainsAny(mailFrom, "\r\n") { + return nil, errors.New("email from address must not contain newline characters") + } + + for _, to := range mailTo { + if strings.ContainsAny(to, "\r\n") { + return nil, errors.New("email to address must not contain newline characters") + } + } + + // Normalize the message body to use CRLF line endings + const CRLF = "\r\n" + mailBody = strings.ReplaceAll(mailBody, CRLF, "\n") + mailBody = strings.ReplaceAll(mailBody, "\n", CRLF) + + msg := bytes.Buffer{} + switch { + case len(mailTo) == 1: + // Address email directly to the one recipient. + msg.WriteString("To: " + mailTo[0] + CRLF) + case config.GetSMTPDiscloseRecipients(): + // Simply address To all recipients. + msg.WriteString("To: " + strings.Join(mailTo, ", ") + CRLF) + default: + // Address To anonymous group. + // + // Email will be sent to all recipients but we shouldn't include Bcc header. + // + // From the smtp.SendMail function: 'Sending "Bcc" messages is accomplished by + // including an email address in the to parameter but not including it in the + // msg headers.' + msg.WriteString("To: Undisclosed Recipients:;" + CRLF) + } + msg.WriteString("Subject: " + mailSubject + CRLF) + msg.WriteString(CRLF) + msg.WriteString(mailBody) + msg.WriteString(CRLF) + + return msg.Bytes(), nil +} diff --git a/internal/email/confirm.go b/internal/email/confirm.go index a6548e7d1..9f05a4f71 100644 --- a/internal/email/confirm.go +++ b/internal/email/confirm.go @@ -17,15 +17,8 @@ package email -import ( - "bytes" - "net/smtp" - - "github.com/superseriousbusiness/gotosocial/internal/gtserror" -) - const ( - confirmTemplate = "email_confirm_text.tmpl" + confirmTemplate = "email_confirm.tmpl" confirmSubject = "GoToSocial Email Confirmation" ) @@ -43,20 +36,5 @@ type ConfirmData struct { } 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 + return s.sendTemplate(confirmTemplate, confirmSubject, data, toAddress) } diff --git a/internal/email/email_test.go b/internal/email/email_test.go index 2fa52ec4a..91d128ef8 100644 --- a/internal/email/email_test.go +++ b/internal/email/email_test.go @@ -18,7 +18,10 @@ package email_test import ( + "testing" + "github.com/stretchr/testify/suite" + "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/email" "github.com/superseriousbusiness/gotosocial/testrig" ) @@ -36,3 +39,152 @@ func (suite *EmailTestSuite) SetupTest() { suite.sentEmails = make(map[string]string) suite.sender = testrig.NewEmailSender("../../web/template/", suite.sentEmails) } + +func (suite *EmailTestSuite) TestTemplateConfirm() { + confirmData := email.ConfirmData{ + Username: "test", + InstanceURL: "https://example.org", + InstanceName: "Test Instance", + ConfirmLink: "https://example.org/confirm_email?token=ee24f71d-e615-43f9-afae-385c0799b7fa", + } + + suite.sender.SendConfirmEmail("user@example.org", confirmData) + suite.Len(suite.sentEmails, 1) + suite.Equal("To: user@example.org\r\nSubject: GoToSocial Email Confirmation\r\n\r\nHello test!\r\n\r\nYou are receiving this mail because you've requested an account on https://example.org.\r\n\r\nWe just need to confirm that this is your email address. To confirm your email, paste the following in your browser's address bar:\r\n\r\nhttps://example.org/confirm_email?token=ee24f71d-e615-43f9-afae-385c0799b7fa\r\n\r\nIf you believe you've been sent this email in error, feel free to ignore it, or contact the administrator of https://example.org\r\n\r\n", suite.sentEmails["user@example.org"]) +} + +func (suite *EmailTestSuite) TestTemplateReset() { + resetData := email.ResetData{ + Username: "test", + InstanceURL: "https://example.org", + InstanceName: "Test Instance", + ResetLink: "https://example.org/reset_email?token=ee24f71d-e615-43f9-afae-385c0799b7fa", + } + + suite.sender.SendResetEmail("user@example.org", resetData) + suite.Len(suite.sentEmails, 1) + suite.Equal("To: user@example.org\r\nSubject: GoToSocial Password Reset\r\n\r\nHello test!\r\n\r\nYou are receiving this mail because a password reset has been requested for your account on https://example.org.\r\n\r\nTo reset your password, paste the following in your browser's address bar:\r\n\r\nhttps://example.org/reset_email?token=ee24f71d-e615-43f9-afae-385c0799b7fa\r\n\r\nIf you believe you've been sent this email in error, feel free to ignore it, or contact the administrator of https://example.org.\r\n\r\n", suite.sentEmails["user@example.org"]) +} + +func (suite *EmailTestSuite) TestTemplateReportRemoteToLocal() { + // Someone from a remote instance has reported one of our users. + reportData := email.NewReportData{ + InstanceURL: "https://example.org", + InstanceName: "Test Instance", + ReportURL: "https://example.org/settings/admin/reports/01GVJHN1RTYZCZTCXVPPPKBX6R", + ReportDomain: "fossbros-anonymous.io", + ReportTargetDomain: "", + } + + if err := suite.sender.SendNewReportEmail([]string{"user@example.org"}, reportData); err != nil { + suite.FailNow(err.Error()) + } + suite.Len(suite.sentEmails, 1) + suite.Equal("To: user@example.org\r\nSubject: GoToSocial New Report\r\n\r\nHello moderator of Test Instance (https://example.org)!\r\n\r\nSomeone from fossbros-anonymous.io has reported a user from your instance.\r\n\r\nTo view the report, paste the following link into your browser: https://example.org/settings/admin/reports/01GVJHN1RTYZCZTCXVPPPKBX6R\r\n\r\n", suite.sentEmails["user@example.org"]) +} + +func (suite *EmailTestSuite) TestTemplateReportLocalToRemote() { + // Someone from our instance has reported a remote user. + reportData := email.NewReportData{ + InstanceURL: "https://example.org", + InstanceName: "Test Instance", + ReportURL: "https://example.org/settings/admin/reports/01GVJHN1RTYZCZTCXVPPPKBX6R", + ReportDomain: "", + ReportTargetDomain: "fossbros-anonymous.io", + } + + if err := suite.sender.SendNewReportEmail([]string{"user@example.org"}, reportData); err != nil { + suite.FailNow(err.Error()) + } + suite.Len(suite.sentEmails, 1) + suite.Equal("To: user@example.org\r\nSubject: GoToSocial New Report\r\n\r\nHello moderator of Test Instance (https://example.org)!\r\n\r\nSomeone from your instance has reported a user from fossbros-anonymous.io.\r\n\r\nTo view the report, paste the following link into your browser: https://example.org/settings/admin/reports/01GVJHN1RTYZCZTCXVPPPKBX6R\r\n\r\n", suite.sentEmails["user@example.org"]) +} + +func (suite *EmailTestSuite) TestTemplateReportLocalToLocal() { + // Someone from our instance has reported another user on our instance. + reportData := email.NewReportData{ + InstanceURL: "https://example.org", + InstanceName: "Test Instance", + ReportURL: "https://example.org/settings/admin/reports/01GVJHN1RTYZCZTCXVPPPKBX6R", + ReportDomain: "", + ReportTargetDomain: "", + } + + if err := suite.sender.SendNewReportEmail([]string{"user@example.org"}, reportData); err != nil { + suite.FailNow(err.Error()) + } + suite.Len(suite.sentEmails, 1) + suite.Equal("To: user@example.org\r\nSubject: GoToSocial New Report\r\n\r\nHello moderator of Test Instance (https://example.org)!\r\n\r\nSomeone from your instance has reported another user from your instance.\r\n\r\nTo view the report, paste the following link into your browser: https://example.org/settings/admin/reports/01GVJHN1RTYZCZTCXVPPPKBX6R\r\n\r\n", suite.sentEmails["user@example.org"]) +} + +func (suite *EmailTestSuite) TestTemplateReportMoreThanOneModeratorAddress() { + reportData := email.NewReportData{ + InstanceURL: "https://example.org", + InstanceName: "Test Instance", + ReportURL: "https://example.org/settings/admin/reports/01GVJHN1RTYZCZTCXVPPPKBX6R", + ReportDomain: "fossbros-anonymous.io", + ReportTargetDomain: "", + } + + // Send the email to multiple addresses + if err := suite.sender.SendNewReportEmail([]string{"user@example.org", "admin@example.org"}, reportData); err != nil { + suite.FailNow(err.Error()) + } + suite.Len(suite.sentEmails, 1) + suite.Equal("To: Undisclosed Recipients:;\r\nSubject: GoToSocial New Report\r\n\r\nHello moderator of Test Instance (https://example.org)!\r\n\r\nSomeone from fossbros-anonymous.io has reported a user from your instance.\r\n\r\nTo view the report, paste the following link into your browser: https://example.org/settings/admin/reports/01GVJHN1RTYZCZTCXVPPPKBX6R\r\n\r\n", suite.sentEmails["user@example.org"]) +} + +func (suite *EmailTestSuite) TestTemplateReportMoreThanOneModeratorAddressDisclose() { + config.SetSMTPDiscloseRecipients(true) + + reportData := email.NewReportData{ + InstanceURL: "https://example.org", + InstanceName: "Test Instance", + ReportURL: "https://example.org/settings/admin/reports/01GVJHN1RTYZCZTCXVPPPKBX6R", + ReportDomain: "fossbros-anonymous.io", + ReportTargetDomain: "", + } + + // Send the email to multiple addresses + if err := suite.sender.SendNewReportEmail([]string{"user@example.org", "admin@example.org"}, reportData); err != nil { + suite.FailNow(err.Error()) + } + suite.Len(suite.sentEmails, 1) + suite.Equal("To: user@example.org, admin@example.org\r\nSubject: GoToSocial New Report\r\n\r\nHello moderator of Test Instance (https://example.org)!\r\n\r\nSomeone from fossbros-anonymous.io has reported a user from your instance.\r\n\r\nTo view the report, paste the following link into your browser: https://example.org/settings/admin/reports/01GVJHN1RTYZCZTCXVPPPKBX6R\r\n\r\n", suite.sentEmails["user@example.org"]) +} + +func (suite *EmailTestSuite) TestTemplateReportClosedOK() { + reportClosedData := email.ReportClosedData{ + InstanceURL: "https://example.org", + InstanceName: "Test Instance", + ReportTargetUsername: "foss_satan", + ReportTargetDomain: "fossbros-anonymous.io", + ActionTakenComment: "User was yeeted. Thank you for reporting!", + } + + if err := suite.sender.SendReportClosedEmail("user@example.org", reportClosedData); err != nil { + suite.FailNow(err.Error()) + } + suite.Len(suite.sentEmails, 1) + suite.Equal("To: user@example.org\r\nSubject: GoToSocial Report Closed\r\n\r\nHello !\r\n\r\nYou recently reported the account @foss_satan@fossbros-anonymous.io to the moderator(s) of Test Instance (https://example.org).\r\n\r\nThe report you submitted has now been closed.\r\n\r\nThe moderator who closed the report left the following comment: User was yeeted. Thank you for reporting!\r\n\r\n", suite.sentEmails["user@example.org"]) +} + +func (suite *EmailTestSuite) TestTemplateReportClosedLocalAccountNoComment() { + reportClosedData := email.ReportClosedData{ + InstanceURL: "https://example.org", + InstanceName: "Test Instance", + ReportTargetUsername: "1happyturtle", + ReportTargetDomain: "", + ActionTakenComment: "", + } + + if err := suite.sender.SendReportClosedEmail("user@example.org", reportClosedData); err != nil { + suite.FailNow(err.Error()) + } + suite.Len(suite.sentEmails, 1) + suite.Equal("To: user@example.org\r\nSubject: GoToSocial Report Closed\r\n\r\nHello !\r\n\r\nYou recently reported the account @1happyturtle to the moderator(s) of Test Instance (https://example.org).\r\n\r\nThe report you submitted has now been closed.\r\n\r\nThe moderator who closed the report did not leave a comment.\r\n\r\n", suite.sentEmails["user@example.org"]) +} + +func TestEmailTestSuite(t *testing.T) { + suite.Run(t, new(EmailTestSuite)) +} diff --git a/internal/email/noopsender.go b/internal/email/noopsender.go index 7164440f3..0ed7ff747 100644 --- a/internal/email/noopsender.go +++ b/internal/email/noopsender.go @@ -49,62 +49,40 @@ type noopSender struct { } func (s *noopSender) 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, "test@example.org") - if err != nil { - return err - } - - log.Tracef(nil, "NOT SENDING confirmation email to %s with contents: %s", toAddress, msg) - - if s.sendCallback != nil { - s.sendCallback(toAddress, string(msg)) - } - return nil + return s.sendTemplate(confirmTemplate, confirmSubject, data, toAddress) } func (s *noopSender) 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, "test@example.org") - if err != nil { - return err - } + return s.sendTemplate(resetTemplate, resetSubject, data, toAddress) +} - log.Tracef(nil, "NOT SENDING reset email to %s with contents: %s", toAddress, msg) +func (s *noopSender) SendTestEmail(toAddress string, data TestData) error { + return s.sendTemplate(testTemplate, testSubject, data, toAddress) +} - if s.sendCallback != nil { - s.sendCallback(toAddress, string(msg)) - } +func (s *noopSender) SendNewReportEmail(toAddresses []string, data NewReportData) error { + return s.sendTemplate(newReportTemplate, newReportSubject, data, toAddresses...) +} - return nil +func (s *noopSender) SendReportClosedEmail(toAddress string, data ReportClosedData) error { + return s.sendTemplate(reportClosedTemplate, reportClosedSubject, data, toAddress) } -func (s *noopSender) SendTestEmail(toAddress string, data TestData) error { +func (s *noopSender) sendTemplate(template string, subject string, data any, toAddresses ...string) error { buf := &bytes.Buffer{} - if err := s.template.ExecuteTemplate(buf, testTemplate, data); err != nil { + if err := s.template.ExecuteTemplate(buf, template, data); err != nil { return err } - testBody := buf.String() - msg, err := assembleMessage(testSubject, testBody, toAddress, "test@example.org") + msg, err := assembleMessage(subject, buf.String(), "test@example.org", toAddresses...) if err != nil { return err } - log.Tracef(nil, "NOT SENDING test email to %s with contents: %s", toAddress, msg) + log.Tracef(nil, "NOT SENDING email to %s with contents: %s", toAddresses, msg) if s.sendCallback != nil { - s.sendCallback(toAddress, string(msg)) + s.sendCallback(toAddresses[0], string(msg)) } return nil diff --git a/internal/email/report.go b/internal/email/report.go new file mode 100644 index 000000000..7c4c10c69 --- /dev/null +++ b/internal/email/report.go @@ -0,0 +1,64 @@ +// 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 + +const ( + newReportTemplate = "email_new_report.tmpl" + newReportSubject = "GoToSocial New Report" + reportClosedTemplate = "email_report_closed.tmpl" + reportClosedSubject = "GoToSocial Report Closed" +) + +type NewReportData struct { + // URL of the instance to present to the receiver. + InstanceURL string + // Name of the instance to present to the receiver. + InstanceName string + // URL to open the report in the settings panel. + ReportURL string + // Domain from which the report originated. + // Can be empty string for local reports. + ReportDomain string + // Domain targeted by the report. + // Can be empty string for local reports targeting local users. + ReportTargetDomain string +} + +func (s *sender) SendNewReportEmail(toAddresses []string, data NewReportData) error { + return s.sendTemplate(newReportTemplate, newReportSubject, data, toAddresses...) +} + +type ReportClosedData struct { + // Username to be addressed. + Username string + // URL of the instance to present to the receiver. + InstanceURL string + // Name of the instance to present to the receiver. + InstanceName string + // Username of the report target. + ReportTargetUsername string + // Domain of the report target. + // Can be empty string for local reports targeting local users. + ReportTargetDomain string + // Comment left by the admin who closed the report. + ActionTakenComment string +} + +func (s *sender) SendReportClosedEmail(toAddress string, data ReportClosedData) error { + return s.sendTemplate(reportClosedTemplate, reportClosedSubject, data, toAddress) +} diff --git a/internal/email/reset.go b/internal/email/reset.go index cb1da9fee..eb931a312 100644 --- a/internal/email/reset.go +++ b/internal/email/reset.go @@ -17,15 +17,8 @@ package email -import ( - "bytes" - "net/smtp" - - "github.com/superseriousbusiness/gotosocial/internal/gtserror" -) - const ( - resetTemplate = "email_reset_text.tmpl" + resetTemplate = "email_reset.tmpl" resetSubject = "GoToSocial Password Reset" ) @@ -43,20 +36,5 @@ type ResetData struct { } 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 + return s.sendTemplate(resetTemplate, resetSubject, data, toAddress) } diff --git a/internal/email/sender.go b/internal/email/sender.go index 13dd26531..b0d883d9d 100644 --- a/internal/email/sender.go +++ b/internal/email/sender.go @@ -35,6 +35,17 @@ type Sender interface { // SendTestEmail sends a 'testing email sending' style email to the given toAddress, with the given data. SendTestEmail(toAddress string, data TestData) error + + // SendNewReportEmail sends an email notification to the given addresses, letting them + // know that a new report has been created targeting a user on this instance. + // + // It is expected that the toAddresses have already been filtered to ensure that they + // all belong to admins + moderators. + SendNewReportEmail(toAddresses []string, data NewReportData) error + + // SendReportClosedEmail sends an email notification to the given address, letting them + // know that a report that they created has been closed / resolved by an admin. + SendReportClosedEmail(toAddress string, data ReportClosedData) error } // NewSender returns a new email Sender interface with the given configuration, or an error if something goes wrong. diff --git a/internal/email/test.go b/internal/email/test.go index 1e411f161..7d6ac2b3b 100644 --- a/internal/email/test.go +++ b/internal/email/test.go @@ -17,15 +17,8 @@ package email -import ( - "bytes" - "net/smtp" - - "github.com/superseriousbusiness/gotosocial/internal/gtserror" -) - const ( - testTemplate = "email_test_text.tmpl" + testTemplate = "email_test.tmpl" testSubject = "GoToSocial Test Email" ) @@ -39,20 +32,5 @@ type TestData struct { } 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 + return s.sendTemplate(testTemplate, testSubject, data, toAddress) } diff --git a/internal/email/util.go b/internal/email/util.go deleted file mode 100644 index bd024a3e0..000000000 --- a/internal/email/util.go +++ /dev/null @@ -1,71 +0,0 @@ -// 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 ( - "errors" - "fmt" - "os" - "path/filepath" - "strings" - "text/template" -) - -func loadTemplates(templateBaseDir string) (*template.Template, error) { - if !filepath.IsAbs(templateBaseDir) { - cwd, err := os.Getwd() - if err != nil { - return nil, fmt.Errorf("error getting current working directory: %s", err) - } - templateBaseDir = filepath.Join(cwd, templateBaseDir) - } - - // look for all templates that start with 'email_' - return template.ParseGlob(filepath.Join(templateBaseDir, "email_*")) -} - -// https://datatracker.ietf.org/doc/html/rfc2822 -// I did not read the RFC, I just copy and pasted from -// https://pkg.go.dev/net/smtp#SendMail -// and it did seem to work. -func assembleMessage(mailSubject string, mailBody string, mailTo string, mailFrom string) ([]byte, error) { - if strings.Contains(mailSubject, "\r") || strings.Contains(mailSubject, "\n") { - return nil, errors.New("email subject must not contain newline characters") - } - - if strings.Contains(mailFrom, "\r") || strings.Contains(mailFrom, "\n") { - return nil, errors.New("email from address must not contain newline characters") - } - - if strings.Contains(mailTo, "\r") || strings.Contains(mailTo, "\n") { - return nil, errors.New("email to address must not contain newline characters") - } - - // normalize the message body to use CRLF line endings - mailBody = strings.ReplaceAll(mailBody, "\r\n", "\n") - mailBody = strings.ReplaceAll(mailBody, "\n", "\r\n") - - msg := []byte( - "To: " + mailTo + "\r\n" + - "Subject: " + mailSubject + "\r\n" + - "\r\n" + - mailBody + "\r\n", - ) - - return msg, nil -} diff --git a/internal/email/util_test.go b/internal/email/util_test.go deleted file mode 100644 index 281d5630b..000000000 --- a/internal/email/util_test.go +++ /dev/null @@ -1,59 +0,0 @@ -// 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_test - -import ( - "testing" - - "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/email" -) - -type UtilTestSuite struct { - EmailTestSuite -} - -func (suite *UtilTestSuite) TestTemplateConfirm() { - confirmData := email.ConfirmData{ - Username: "test", - InstanceURL: "https://example.org", - InstanceName: "Test Instance", - ConfirmLink: "https://example.org/confirm_email?token=ee24f71d-e615-43f9-afae-385c0799b7fa", - } - - suite.sender.SendConfirmEmail("user@example.org", confirmData) - suite.Len(suite.sentEmails, 1) - suite.Equal("To: user@example.org\r\nSubject: GoToSocial Email Confirmation\r\n\r\nHello test!\r\n\r\nYou are receiving this mail because you've requested an account on https://example.org.\r\n\r\nWe just need to confirm that this is your email address. To confirm your email, paste the following in your browser's address bar:\r\n\r\nhttps://example.org/confirm_email?token=ee24f71d-e615-43f9-afae-385c0799b7fa\r\n\r\nIf you believe you've been sent this email in error, feel free to ignore it, or contact the administrator of https://example.org\r\n\r\n", suite.sentEmails["user@example.org"]) -} - -func (suite *UtilTestSuite) TestTemplateReset() { - resetData := email.ResetData{ - Username: "test", - InstanceURL: "https://example.org", - InstanceName: "Test Instance", - ResetLink: "https://example.org/reset_email?token=ee24f71d-e615-43f9-afae-385c0799b7fa", - } - - suite.sender.SendResetEmail("user@example.org", resetData) - suite.Len(suite.sentEmails, 1) - suite.Equal("To: user@example.org\r\nSubject: GoToSocial Password Reset\r\n\r\nHello test!\r\n\r\nYou are receiving this mail because a password reset has been requested for your account on https://example.org.\r\n\r\nTo reset your password, paste the following in your browser's address bar:\r\n\r\nhttps://example.org/reset_email?token=ee24f71d-e615-43f9-afae-385c0799b7fa\r\n\r\nIf you believe you've been sent this email in error, feel free to ignore it, or contact the administrator of https://example.org.\r\n\r\n", suite.sentEmails["user@example.org"]) -} - -func TestUtilTestSuite(t *testing.T) { - suite.Run(t, &UtilTestSuite{}) -} diff --git a/internal/processing/admin/report.go b/internal/processing/admin/report.go index 3a5435d9e..174bd3c24 100644 --- a/internal/processing/admin/report.go +++ b/internal/processing/admin/report.go @@ -23,10 +23,12 @@ import ( "strconv" "time" + "github.com/superseriousbusiness/gotosocial/internal/ap" apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/messages" "github.com/superseriousbusiness/gotosocial/internal/util" ) @@ -110,7 +112,10 @@ func (p *Processor) ReportGet(ctx context.Context, account *gtsmodel.Account, id return apimodelReport, nil } -// ReportResolve marks a report with the given id as resolved, and stores the provided actionTakenComment (if not null). +// ReportResolve marks a report with the given id as resolved, +// and stores the provided actionTakenComment (if not null). +// If the report creator is from this instance, an email will +// be sent to them to let them know that the report is resolved. func (p *Processor) ReportResolve(ctx context.Context, account *gtsmodel.Account, id string, actionTakenComment *string) (*apimodel.AdminReport, gtserror.WithCode) { report, err := p.state.DB.GetReportByID(ctx, id) if err != nil { @@ -138,6 +143,15 @@ func (p *Processor) ReportResolve(ctx context.Context, account *gtsmodel.Account return nil, gtserror.NewErrorInternalError(err) } + // Process side effects of closing the report. + p.state.Workers.EnqueueClientAPI(ctx, messages.FromClientAPI{ + APObjectType: ap.ActivityFlag, + APActivityType: ap.ActivityUpdate, + GTSModel: report, + OriginAccount: account, + TargetAccount: report.Account, + }) + apimodelReport, err := p.tc.ReportToAdminAPIReport(ctx, updatedReport, account) if err != nil { return nil, gtserror.NewErrorInternalError(err) diff --git a/internal/processing/fromclientapi.go b/internal/processing/fromclientapi.go index 391239abc..a4d4521ce 100644 --- a/internal/processing/fromclientapi.go +++ b/internal/processing/fromclientapi.go @@ -81,6 +81,9 @@ func (p *Processor) ProcessFromClientAPI(ctx context.Context, clientMsg messages case ap.ObjectProfile, ap.ActorPerson: // UPDATE ACCOUNT/PROFILE return p.processUpdateAccountFromClientAPI(ctx, clientMsg) + case ap.ActivityFlag: + // UPDATE A FLAG/REPORT (mark as resolved/closed) + return p.processUpdateReportFromClientAPI(ctx, clientMsg) } case ap.ActivityAccept: // ACCEPT @@ -240,6 +243,21 @@ func (p *Processor) processUpdateAccountFromClientAPI(ctx context.Context, clien return p.federateAccountUpdate(ctx, account, clientMsg.OriginAccount) } +func (p *Processor) processUpdateReportFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { + report, ok := clientMsg.GTSModel.(*gtsmodel.Report) + if !ok { + return errors.New("report was not parseable as *gtsmodel.Report") + } + + if report.Account.IsRemote() { + // Report creator is a remote account, + // we shouldn't email or notify them. + return nil + } + + return p.notifyReportClosed(ctx, report) +} + func (p *Processor) processAcceptFollowFromClientAPI(ctx context.Context, clientMsg messages.FromClientAPI) error { follow, ok := clientMsg.GTSModel.(*gtsmodel.Follow) if !ok { @@ -349,14 +367,17 @@ func (p *Processor) processReportAccountFromClientAPI(ctx context.Context, clien return errors.New("report was not parseable as *gtsmodel.Report") } - // TODO: in a separate PR, also email admin(s) + if *report.Forwarded { + if err := p.federateReport(ctx, report); err != nil { + return fmt.Errorf("processReportAccountFromClientAPI: error federating report: %w", err) + } + } - if !*report.Forwarded { - // nothing to do, don't federate the report - return nil + if err := p.notifyReport(ctx, report); err != nil { + return fmt.Errorf("processReportAccountFromClientAPI: error notifying report: %w", err) } - return p.federateReport(ctx, report) + return nil } // TODO: move all the below functions into federation.Federator diff --git a/internal/processing/fromcommon.go b/internal/processing/fromcommon.go index ccdeca3c5..c29ada5ba 100644 --- a/internal/processing/fromcommon.go +++ b/internal/processing/fromcommon.go @@ -19,11 +19,14 @@ package processing import ( "context" + "errors" "fmt" "strings" "sync" + "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/email" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/id" "github.com/superseriousbusiness/gotosocial/internal/stream" @@ -308,6 +311,96 @@ func (p *Processor) notifyAnnounce(ctx context.Context, status *gtsmodel.Status) return nil } +func (p *Processor) notifyReport(ctx context.Context, report *gtsmodel.Report) error { + instance, err := p.state.DB.GetInstance(ctx, config.GetHost()) + if err != nil { + return fmt.Errorf("notifyReport: error getting instance: %w", err) + } + + toAddresses, err := p.state.DB.GetInstanceModeratorAddresses(ctx) + if err != nil { + if errors.Is(err, db.ErrNoEntries) { + // No registered moderator addresses. + return nil + } + return fmt.Errorf("notifyReport: error getting instance moderator addresses: %w", err) + } + + if report.Account == nil { + report.Account, err = p.state.DB.GetAccountByID(ctx, report.AccountID) + if err != nil { + return fmt.Errorf("notifyReport: error getting report account: %w", err) + } + } + + if report.TargetAccount == nil { + report.TargetAccount, err = p.state.DB.GetAccountByID(ctx, report.TargetAccountID) + if err != nil { + return fmt.Errorf("notifyReport: error getting report target account: %w", err) + } + } + + reportData := email.NewReportData{ + InstanceURL: instance.URI, + InstanceName: instance.Title, + ReportURL: instance.URI + "/settings/admin/reports/" + report.ID, + ReportDomain: report.Account.Domain, + ReportTargetDomain: report.TargetAccount.Domain, + } + + if err := p.emailSender.SendNewReportEmail(toAddresses, reportData); err != nil { + return fmt.Errorf("notifyReport: error emailing instance moderators: %w", err) + } + + return nil +} + +func (p *Processor) notifyReportClosed(ctx context.Context, report *gtsmodel.Report) error { + user, err := p.state.DB.GetUserByAccountID(ctx, report.Account.ID) + if err != nil { + return fmt.Errorf("notifyReportClosed: db error getting user: %w", err) + } + + if user.ConfirmedAt.IsZero() || !*user.Approved || *user.Disabled || user.Email == "" { + // Only email users who: + // - are confirmed + // - are approved + // - are not disabled + // - have an email address + return nil + } + + instance, err := p.state.DB.GetInstance(ctx, config.GetHost()) + if err != nil { + return fmt.Errorf("notifyReportClosed: db error getting instance: %w", err) + } + + if report.Account == nil { + report.Account, err = p.state.DB.GetAccountByID(ctx, report.AccountID) + if err != nil { + return fmt.Errorf("notifyReportClosed: error getting report account: %w", err) + } + } + + if report.TargetAccount == nil { + report.TargetAccount, err = p.state.DB.GetAccountByID(ctx, report.TargetAccountID) + if err != nil { + return fmt.Errorf("notifyReportClosed: error getting report target account: %w", err) + } + } + + reportClosedData := email.ReportClosedData{ + Username: report.Account.Username, + InstanceURL: instance.URI, + InstanceName: instance.Title, + ReportTargetUsername: report.TargetAccount.Username, + ReportTargetDomain: report.TargetAccount.Domain, + ActionTakenComment: report.ActionTaken, + } + + return p.emailSender.SendReportClosedEmail(user.Email, reportClosedData) +} + // timelineStatus processes the given new status and inserts it into // the HOME timelines of accounts that follow the status author. func (p *Processor) timelineStatus(ctx context.Context, status *gtsmodel.Status) error { diff --git a/internal/processing/fromfederator.go b/internal/processing/fromfederator.go index 014c4a324..32a970114 100644 --- a/internal/processing/fromfederator.go +++ b/internal/processing/fromfederator.go @@ -359,10 +359,15 @@ func (p *Processor) processCreateBlockFromFederator(ctx context.Context, federat } func (p *Processor) processCreateFlagFromFederator(ctx context.Context, federatorMsg messages.FromFederator) error { - // TODO: handle side effects of flag creation: - // - send email to admins - // - notify admins - return nil + incomingReport, ok := federatorMsg.GTSModel.(*gtsmodel.Report) + if !ok { + return errors.New("flag was not parseable as *gtsmodel.Report") + } + + // TODO: handle additional side effects of flag creation: + // - notify admins by dm / notification + + return p.notifyReport(ctx, incomingReport) } // processUpdateAccountFromFederator handles Activity Update and Object Profile diff --git a/internal/processing/processor.go b/internal/processing/processor.go index 98b417ba3..ad485b9ae 100644 --- a/internal/processing/processor.go +++ b/internal/processing/processor.go @@ -48,6 +48,7 @@ type Processor struct { statusTimelines timeline.Manager state *state.State filter visibility.Filter + emailSender email.Sender /* SUB-PROCESSORS @@ -119,8 +120,9 @@ func NewProcessor( StatusPrepareFunction(state.DB, tc), StatusSkipInsertFunction(), ), - state: state, - filter: filter, + state: state, + filter: filter, + emailSender: emailSender, } // sub processors |