diff options
author | 2023-03-19 13:11:46 +0100 | |
---|---|---|
committer | 2023-03-19 13:11:46 +0100 | |
commit | 7db81cde444f6bc95e79527af0997de1788d48c7 (patch) | |
tree | f6c077ec298a4f018d0870798bc46bd64ba70069 /internal/email | |
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/email')
-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 |
10 files changed, 361 insertions, 240 deletions
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{}) -} |