[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
This commit is contained in:
Julian 2024-06-22 23:36:30 +02:00 committed by GitHub
parent 15e0bf6e5a
commit c2738474d5
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 33 additions and 3 deletions

View file

@ -26,7 +26,9 @@ import (
"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)

View file

@ -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"])
}

View file

@ -31,6 +31,7 @@ import (
// 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
}

View file

@ -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
}