[PORT] Use FullName in Emails to address the recipient if possible (gitea#31527) (#4516)

Before we had just the plain mail address as recipient. But now we provide additional Information for the Mail clients.

---
Porting information:

- Two behavior changes are noted with this patch, the display name is now always quoted although in some scenarios unnecessary it's a safety precaution of Go. B encoding is used when certain characters are present as they aren't 'legal' to be used as a display name and Q encoding would still show them and B encoding needs to be used, this is now done by Go's `address.String()`.
- Update and add new unit tests.

Co-authored-by: 6543 <6543@obermui.de>
Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/4516
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
Co-authored-by: Gusted <postmaster@gusted.xyz>
Co-committed-by: Gusted <postmaster@gusted.xyz>
This commit is contained in:
Gusted 2024-07-17 05:13:59 +00:00 committed by Earl Warren
parent 3c8cd43fec
commit 8a1924b51a
5 changed files with 67 additions and 13 deletions

View file

@ -9,6 +9,7 @@ import (
"context" "context"
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"net/mail"
"net/url" "net/url"
"path/filepath" "path/filepath"
"regexp" "regexp"
@ -439,6 +440,33 @@ func (u *User) DisplayName() string {
return u.Name return u.Name
} }
var emailToReplacer = strings.NewReplacer(
"\n", "",
"\r", "",
"<", "",
">", "",
",", "",
":", "",
";", "",
)
// EmailTo returns a string suitable to be put into a e-mail `To:` header.
func (u *User) EmailTo() string {
sanitizedDisplayName := emailToReplacer.Replace(u.DisplayName())
// should be an edge case but nice to have
if sanitizedDisplayName == u.Email {
return u.Email
}
address, err := mail.ParseAddress(fmt.Sprintf("%s <%s>", sanitizedDisplayName, u.Email))
if err != nil {
return u.Email
}
return address.String()
}
// GetDisplayName returns full name if it's not empty and DEFAULT_SHOW_FULL_NAME is set, // GetDisplayName returns full name if it's not empty and DEFAULT_SHOW_FULL_NAME is set,
// returns username otherwise. // returns username otherwise.
func (u *User) GetDisplayName() string { func (u *User) GetDisplayName() string {

View file

@ -601,6 +601,32 @@ func Test_NormalizeUserFromEmail(t *testing.T) {
} }
} }
func TestEmailTo(t *testing.T) {
testCases := []struct {
fullName string
mail string
result string
}{
{"Awareness Hub", "awareness@hub.net", `"Awareness Hub" <awareness@hub.net>`},
{"name@example.com", "name@example.com", "name@example.com"},
{"Hi Its <Mee>", "ee@mail.box", `"Hi Its Mee" <ee@mail.box>`},
{"Sinéad.O'Connor", "sinead.oconnor@gmail.com", "=?utf-8?b?U2luw6lhZC5PJ0Nvbm5vcg==?= <sinead.oconnor@gmail.com>"},
{"Æsir", "aesir@gmx.de", "=?utf-8?q?=C3=86sir?= <aesir@gmx.de>"},
{"new😀user", "new.user@alo.com", "=?utf-8?q?new=F0=9F=98=80user?= <new.user@alo.com>"},
{`"quoted"`, "quoted@test.com", `"quoted" <quoted@test.com>`},
{`gusted`, "gusted@test.com", `"gusted" <gusted@test.com>`},
{`Joe Q. Public`, "john.q.public@example.com", `"Joe Q. Public" <john.q.public@example.com>`},
{`Who?`, "one@y.test", `"Who?" <one@y.test>`},
}
for _, testCase := range testCases {
t.Run(testCase.result, func(t *testing.T) {
testUser := &user_model.User{FullName: testCase.fullName, Email: testCase.mail}
assert.EqualValues(t, testCase.result, testUser.EmailTo())
})
}
}
func TestDisabledUserFeatures(t *testing.T) { func TestDisabledUserFeatures(t *testing.T) {
assert.NoError(t, unittest.PrepareTestDatabase()) assert.NoError(t, unittest.PrepareTestDatabase())

View file

@ -82,7 +82,7 @@ func sendUserMail(language string, u *user_model.User, tpl base.TplName, code, s
return return
} }
msg := NewMessage(u.Email, subject, content.String()) msg := NewMessage(u.EmailTo(), subject, content.String())
msg.Info = fmt.Sprintf("UID: %d, %s", u.ID, info) msg.Info = fmt.Sprintf("UID: %d, %s", u.ID, info)
SendAsync(msg) SendAsync(msg)
@ -158,7 +158,7 @@ func SendRegisterNotifyMail(u *user_model.User) {
return return
} }
msg := NewMessage(u.Email, locale.TrString("mail.register_notify", setting.AppName), content.String()) msg := NewMessage(u.EmailTo(), locale.TrString("mail.register_notify", setting.AppName), content.String())
msg.Info = fmt.Sprintf("UID: %d, registration notify", u.ID) msg.Info = fmt.Sprintf("UID: %d, registration notify", u.ID)
SendAsync(msg) SendAsync(msg)
@ -189,7 +189,7 @@ func SendCollaboratorMail(u, doer *user_model.User, repo *repo_model.Repository)
return return
} }
msg := NewMessage(u.Email, subject, content.String()) msg := NewMessage(u.EmailTo(), subject, content.String())
msg.Info = fmt.Sprintf("UID: %d, add collaborator", u.ID) msg.Info = fmt.Sprintf("UID: %d, add collaborator", u.ID)
SendAsync(msg) SendAsync(msg)

View file

@ -40,10 +40,10 @@ func MailNewRelease(ctx context.Context, rel *repo_model.Release) {
return return
} }
langMap := make(map[string][]string) langMap := make(map[string][]*user_model.User)
for _, user := range recipients { for _, user := range recipients {
if user.ID != rel.PublisherID { if user.ID != rel.PublisherID {
langMap[user.Language] = append(langMap[user.Language], user.Email) langMap[user.Language] = append(langMap[user.Language], user)
} }
} }
@ -52,7 +52,7 @@ func MailNewRelease(ctx context.Context, rel *repo_model.Release) {
} }
} }
func mailNewRelease(ctx context.Context, lang string, tos []string, rel *repo_model.Release) { func mailNewRelease(ctx context.Context, lang string, tos []*user_model.User, rel *repo_model.Release) {
locale := translation.NewLocale(lang) locale := translation.NewLocale(lang)
var err error var err error
@ -88,7 +88,7 @@ func mailNewRelease(ctx context.Context, lang string, tos []string, rel *repo_mo
publisherName := rel.Publisher.DisplayName() publisherName := rel.Publisher.DisplayName()
msgID := createMessageIDForRelease(rel) msgID := createMessageIDForRelease(rel)
for _, to := range tos { for _, to := range tos {
msg := NewMessageFrom(to, publisherName, setting.MailService.FromEmail, subject, mailBody.String()) msg := NewMessageFrom(to.EmailTo(), publisherName, setting.MailService.FromEmail, subject, mailBody.String())
msg.Info = subject msg.Info = subject
msg.SetHeader("Message-ID", msgID) msg.SetHeader("Message-ID", msgID)
msgs = append(msgs, msg) msgs = append(msgs, msg)

View file

@ -28,13 +28,13 @@ func SendRepoTransferNotifyMail(ctx context.Context, doer, newOwner *user_model.
return err return err
} }
langMap := make(map[string][]string) langMap := make(map[string][]*user_model.User)
for _, user := range users { for _, user := range users {
if !user.IsActive { if !user.IsActive {
// don't send emails to inactive users // don't send emails to inactive users
continue continue
} }
langMap[user.Language] = append(langMap[user.Language], user.Email) langMap[user.Language] = append(langMap[user.Language], user)
} }
for lang, tos := range langMap { for lang, tos := range langMap {
@ -46,11 +46,11 @@ func SendRepoTransferNotifyMail(ctx context.Context, doer, newOwner *user_model.
return nil return nil
} }
return sendRepoTransferNotifyMailPerLang(newOwner.Language, newOwner, doer, []string{newOwner.Email}, repo) return sendRepoTransferNotifyMailPerLang(newOwner.Language, newOwner, doer, []*user_model.User{newOwner}, repo)
} }
// sendRepoTransferNotifyMail triggers a notification e-mail when a pending repository transfer was created for each language // sendRepoTransferNotifyMail triggers a notification e-mail when a pending repository transfer was created for each language
func sendRepoTransferNotifyMailPerLang(lang string, newOwner, doer *user_model.User, emails []string, repo *repo_model.Repository) error { func sendRepoTransferNotifyMailPerLang(lang string, newOwner, doer *user_model.User, emailTos []*user_model.User, repo *repo_model.Repository) error {
var ( var (
locale = translation.NewLocale(lang) locale = translation.NewLocale(lang)
content bytes.Buffer content bytes.Buffer
@ -78,8 +78,8 @@ func sendRepoTransferNotifyMailPerLang(lang string, newOwner, doer *user_model.U
return err return err
} }
for _, to := range emails { for _, to := range emailTos {
msg := NewMessage(to, subject, content.String()) msg := NewMessage(to.EmailTo(), subject, content.String())
msg.Info = fmt.Sprintf("UID: %d, repository pending transfer notification", newOwner.ID) msg.Info = fmt.Sprintf("UID: %d, repository pending transfer notification", newOwner.ID)
SendAsync(msg) SendAsync(msg)