From c2738474d5f4bcdd83bdc09c372f27fe677201ab Mon Sep 17 00:00:00 2001 From: Julian Date: Sat, 22 Jun 2024 23:36:30 +0200 Subject: [PATCH] [bugfix] add Date and Message-ID headers for email (#3031) * [bugfix] add Date and Message-ID headers for email This should make spam filters more happy, as most of them grant some negative score for not having those headers. Also the Date is convenient for the user receiving the mail. * make golangci-lint happy --- internal/email/common.go | 8 ++++++-- internal/email/email_test.go | 20 ++++++++++++++++++++ internal/email/noopsender.go | 5 ++++- internal/email/sender.go | 3 +++ 4 files changed, 33 insertions(+), 3 deletions(-) diff --git a/internal/email/common.go b/internal/email/common.go index 5864a82f7..25a469a39 100644 --- a/internal/email/common.go +++ b/internal/email/common.go @@ -26,7 +26,9 @@ "path/filepath" "strings" "text/template" + "time" + "github.com/google/uuid" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/gtserror" ) @@ -37,7 +39,7 @@ func (s *sender) sendTemplate(template string, subject string, data any, toAddre return err } - msg, err := assembleMessage(subject, buf.String(), s.from, toAddresses...) + msg, err := assembleMessage(subject, buf.String(), s.from, s.msgIDHost, toAddresses...) if err != nil { return err } @@ -65,7 +67,7 @@ func loadTemplates(templateBaseDir string) (*template.Template, error) { // 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) { +func assembleMessage(mailSubject string, mailBody string, mailFrom string, msgIDHost string, mailTo ...string) ([]byte, error) { if strings.ContainsAny(mailSubject, "\r\n") { return nil, errors.New("email subject must not contain newline characters") } @@ -103,7 +105,9 @@ func assembleMessage(mailSubject string, mailBody string, mailFrom string, mailT // msg headers.' msg.WriteString("To: Undisclosed Recipients:;" + CRLF) } + msg.WriteString("Date: " + time.Now().Format(time.RFC822Z) + CRLF) msg.WriteString("From: " + mailFrom + CRLF) + msg.WriteString("Message-ID: <" + uuid.New().String() + "@" + msgIDHost + ">" + CRLF) msg.WriteString("Subject: " + mailSubject + CRLF) msg.WriteString("MIME-Version: 1.0" + CRLF) msg.WriteString("Content-Transfer-Encoding: 8bit" + CRLF) diff --git a/internal/email/email_test.go b/internal/email/email_test.go index aacca1b3d..ce1ae177f 100644 --- a/internal/email/email_test.go +++ b/internal/email/email_test.go @@ -18,6 +18,7 @@ package email_test import ( + "regexp" "testing" "github.com/stretchr/testify/suite" @@ -40,6 +41,15 @@ func (suite *EmailTestSuite) SetupTest() { suite.sender = testrig.NewEmailSender("../../web/template/", suite.sentEmails) } +// strips non deteministic headers from mails +func (suite *EmailTestSuite) stripHeaders() { + re := regexp.MustCompile(`(?m)^(Date:|Message-ID:) .*$\n`) + for key, mail := range suite.sentEmails { + res := re.ReplaceAllString(mail, "") + suite.sentEmails[key] = res + } +} + func (suite *EmailTestSuite) TestTemplateConfirmNewSignup() { confirmData := email.ConfirmData{ Username: "test", @@ -50,6 +60,7 @@ func (suite *EmailTestSuite) TestTemplateConfirmNewSignup() { } suite.sender.SendConfirmEmail("user@example.org", confirmData) + suite.stripHeaders() suite.Len(suite.sentEmails, 1) suite.Equal("To: user@example.org\r\nFrom: test@example.org\r\nSubject: GoToSocial Email Confirmation\r\nMIME-Version: 1.0\r\nContent-Transfer-Encoding: 8bit\r\nContent-Type: text/plain; charset=\"UTF-8\"\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\nTo use your account, you must confirm that this is your email address.\r\n\r\nTo 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\n---\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"]) } @@ -64,6 +75,7 @@ func (suite *EmailTestSuite) TestTemplateConfirm() { } suite.sender.SendConfirmEmail("user@example.org", confirmData) + suite.stripHeaders() suite.Len(suite.sentEmails, 1) suite.Equal("To: user@example.org\r\nFrom: test@example.org\r\nSubject: GoToSocial Email Confirmation\r\nMIME-Version: 1.0\r\nContent-Transfer-Encoding: 8bit\r\nContent-Type: text/plain; charset=\"UTF-8\"\r\n\r\nHello test!\r\n\r\nYou are receiving this mail because you've requested an email address change on https://example.org.\r\n\r\nTo complete the change, you must confirm that this is your email address.\r\n\r\nTo 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\n---\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"]) } @@ -77,6 +89,7 @@ func (suite *EmailTestSuite) TestTemplateReset() { } suite.sender.SendResetEmail("user@example.org", resetData) + suite.stripHeaders() suite.Len(suite.sentEmails, 1) suite.Equal("To: user@example.org\r\nFrom: test@example.org\r\nSubject: GoToSocial Password Reset\r\nMIME-Version: 1.0\r\nContent-Transfer-Encoding: 8bit\r\nContent-Type: text/plain; charset=\"UTF-8\"\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\n---\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"]) } @@ -94,6 +107,7 @@ func (suite *EmailTestSuite) TestTemplateReportRemoteToLocal() { if err := suite.sender.SendNewReportEmail([]string{"user@example.org"}, reportData); err != nil { suite.FailNow(err.Error()) } + suite.stripHeaders() suite.Len(suite.sentEmails, 1) suite.Equal("To: user@example.org\r\nFrom: test@example.org\r\nSubject: GoToSocial New Report\r\nMIME-Version: 1.0\r\nContent-Transfer-Encoding: 8bit\r\nContent-Type: text/plain; charset=\"UTF-8\"\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"]) } @@ -111,6 +125,7 @@ func (suite *EmailTestSuite) TestTemplateReportLocalToRemote() { if err := suite.sender.SendNewReportEmail([]string{"user@example.org"}, reportData); err != nil { suite.FailNow(err.Error()) } + suite.stripHeaders() suite.Len(suite.sentEmails, 1) suite.Equal("To: user@example.org\r\nFrom: test@example.org\r\nSubject: GoToSocial New Report\r\nMIME-Version: 1.0\r\nContent-Transfer-Encoding: 8bit\r\nContent-Type: text/plain; charset=\"UTF-8\"\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"]) } @@ -128,6 +143,7 @@ func (suite *EmailTestSuite) TestTemplateReportLocalToLocal() { if err := suite.sender.SendNewReportEmail([]string{"user@example.org"}, reportData); err != nil { suite.FailNow(err.Error()) } + suite.stripHeaders() suite.Len(suite.sentEmails, 1) suite.Equal("To: user@example.org\r\nFrom: test@example.org\r\nSubject: GoToSocial New Report\r\nMIME-Version: 1.0\r\nContent-Transfer-Encoding: 8bit\r\nContent-Type: text/plain; charset=\"UTF-8\"\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"]) } @@ -145,6 +161,7 @@ func (suite *EmailTestSuite) TestTemplateReportMoreThanOneModeratorAddress() { if err := suite.sender.SendNewReportEmail([]string{"user@example.org", "admin@example.org"}, reportData); err != nil { suite.FailNow(err.Error()) } + suite.stripHeaders() suite.Len(suite.sentEmails, 1) suite.Equal("To: Undisclosed Recipients:;\r\nFrom: test@example.org\r\nSubject: GoToSocial New Report\r\nMIME-Version: 1.0\r\nContent-Transfer-Encoding: 8bit\r\nContent-Type: text/plain; charset=\"UTF-8\"\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"]) } @@ -164,6 +181,7 @@ func (suite *EmailTestSuite) TestTemplateReportMoreThanOneModeratorAddressDisclo if err := suite.sender.SendNewReportEmail([]string{"user@example.org", "admin@example.org"}, reportData); err != nil { suite.FailNow(err.Error()) } + suite.stripHeaders() suite.Len(suite.sentEmails, 1) suite.Equal("To: user@example.org, admin@example.org\r\nFrom: test@example.org\r\nSubject: GoToSocial New Report\r\nMIME-Version: 1.0\r\nContent-Transfer-Encoding: 8bit\r\nContent-Type: text/plain; charset=\"UTF-8\"\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"]) } @@ -180,6 +198,7 @@ func (suite *EmailTestSuite) TestTemplateReportClosedOK() { if err := suite.sender.SendReportClosedEmail("user@example.org", reportClosedData); err != nil { suite.FailNow(err.Error()) } + suite.stripHeaders() suite.Len(suite.sentEmails, 1) suite.Equal("To: user@example.org\r\nFrom: test@example.org\r\nSubject: GoToSocial Report Closed\r\nMIME-Version: 1.0\r\nContent-Transfer-Encoding: 8bit\r\nContent-Type: text/plain; charset=\"UTF-8\"\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---\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"]) } @@ -196,6 +215,7 @@ func (suite *EmailTestSuite) TestTemplateReportClosedLocalAccountNoComment() { if err := suite.sender.SendReportClosedEmail("user@example.org", reportClosedData); err != nil { suite.FailNow(err.Error()) } + suite.stripHeaders() suite.Len(suite.sentEmails, 1) suite.Equal("To: user@example.org\r\nFrom: test@example.org\r\nSubject: GoToSocial Report Closed\r\nMIME-Version: 1.0\r\nContent-Transfer-Encoding: 8bit\r\nContent-Type: text/plain; charset=\"UTF-8\"\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---\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"]) } diff --git a/internal/email/noopsender.go b/internal/email/noopsender.go index 20d7df2eb..bd9b1206e 100644 --- a/internal/email/noopsender.go +++ b/internal/email/noopsender.go @@ -31,6 +31,7 @@ // Passing a nil function is also acceptable, in which case the send functions will just return nil. func NewNoopSender(sendCallback func(toAddress string, message string)) (Sender, error) { templateBaseDir := config.GetWebTemplateBaseDir() + msgIDHost := config.GetHost() t, err := loadTemplates(templateBaseDir) if err != nil { @@ -39,12 +40,14 @@ func NewNoopSender(sendCallback func(toAddress string, message string)) (Sender, return &noopSender{ sendCallback: sendCallback, + msgIDHost: msgIDHost, template: t, }, nil } type noopSender struct { sendCallback func(toAddress string, message string) + msgIDHost string template *template.Template } @@ -86,7 +89,7 @@ func (s *noopSender) sendTemplate(template string, subject string, data any, toA return err } - msg, err := assembleMessage(subject, buf.String(), "test@example.org", toAddresses...) + msg, err := assembleMessage(subject, buf.String(), "test@example.org", s.msgIDHost, toAddresses...) if err != nil { return err } diff --git a/internal/email/sender.go b/internal/email/sender.go index a3efa6124..9db918f8a 100644 --- a/internal/email/sender.go +++ b/internal/email/sender.go @@ -76,11 +76,13 @@ func NewSender() (Sender, error) { host := config.GetSMTPHost() port := config.GetSMTPPort() from := config.GetSMTPFrom() + msgIDHost := config.GetHost() return &sender{ hostAddress: fmt.Sprintf("%s:%d", host, port), from: from, auth: smtp.PlainAuth("", username, password, host), + msgIDHost: msgIDHost, template: t, }, nil } @@ -89,5 +91,6 @@ type sender struct { hostAddress string from string auth smtp.Auth + msgIDHost string template *template.Template }