From 0f533241829d0d48aa16a91e7dc0614fe50bc317 Mon Sep 17 00:00:00 2001 From: 6543 <6543@obermui.de> Date: Sun, 14 Jul 2024 14:27:00 -0700 Subject: [PATCH] Add option to change mail from user display name (#31528) Make it posible to let mails show e.g.: `Max Musternam (via gitea.kithara.com) ` Docs: https://gitea.com/gitea/docs/pulls/23 --- *Sponsored by Kithara Software GmbH* --- custom/conf/app.example.ini | 4 +++ modules/setting/mailer.go | 15 +++++++++++ services/mailer/mail.go | 18 ++++++++++++- services/mailer/mail_release.go | 2 +- services/mailer/mail_repo.go | 2 +- services/mailer/mail_test.go | 48 +++++++++++++++++++++++++++++++++ 6 files changed, 86 insertions(+), 3 deletions(-) diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini index f522b9da28a..c29d2e5be43 100644 --- a/custom/conf/app.example.ini +++ b/custom/conf/app.example.ini @@ -1676,6 +1676,10 @@ LEVEL = Info ;; Sometimes it is helpful to use a different address on the envelope. Set this to use ENVELOPE_FROM as the from on the envelope. Set to `<>` to send an empty address. ;ENVELOPE_FROM = ;; +;; If gitea sends mails on behave of users, it will just use the name also displayed in the WebUI. If you want e.g. `Mister X (by CodeIt) `, +;; set it to `{{ .DisplayName }} (by {{ .AppName }})`. Available Variables: `.DisplayName`, `.AppName` and `.Domain`. +;FROM_DISPLAY_NAME_FORMAT = {{ .DisplayName }} +;; ;; Mailer user name and password, if required by provider. ;USER = ;; diff --git a/modules/setting/mailer.go b/modules/setting/mailer.go index 58bfd67bfb5..d4db55dc7bc 100644 --- a/modules/setting/mailer.go +++ b/modules/setting/mailer.go @@ -8,6 +8,7 @@ import ( "net" "net/mail" "strings" + "text/template" "time" "code.gitea.io/gitea/modules/log" @@ -46,6 +47,10 @@ type Mailer struct { SendmailArgs []string `ini:"-"` SendmailTimeout time.Duration `ini:"SENDMAIL_TIMEOUT"` SendmailConvertCRLF bool `ini:"SENDMAIL_CONVERT_CRLF"` + + // Customization + FromDisplayNameFormat string `ini:"FROM_DISPLAY_NAME_FORMAT"` + FromDisplayNameFormatTemplate *template.Template `ini:"-"` } // MailService the global mailer @@ -226,6 +231,16 @@ func loadMailerFrom(rootCfg ConfigProvider) { log.Error("no mailer.FROM provided, email system may not work.") } + MailService.FromDisplayNameFormatTemplate, _ = template.New("mailFrom").Parse("{{ .DisplayName }}") + if MailService.FromDisplayNameFormat != "" { + template, err := template.New("mailFrom").Parse(MailService.FromDisplayNameFormat) + if err != nil { + log.Error("mailer.FROM_DISPLAY_NAME_FORMAT is no valid template: %v", err) + } else { + MailService.FromDisplayNameFormatTemplate = template + } + } + switch MailService.EnvelopeFrom { case "": MailService.OverrideEnvelopeFrom = false diff --git a/services/mailer/mail.go b/services/mailer/mail.go index 1f5df6efe7e..23c91595b79 100644 --- a/services/mailer/mail.go +++ b/services/mailer/mail.go @@ -314,7 +314,7 @@ func composeIssueCommentMessages(ctx *mailCommentContext, lang string, recipient for _, recipient := range recipients { msg := NewMessageFrom( recipient.Email, - ctx.Doer.GetCompleteName(), + fromDisplayName(ctx.Doer), setting.MailService.FromEmail, subject, mailBody.String(), @@ -536,3 +536,19 @@ func actionToTemplate(issue *issues_model.Issue, actionType activities_model.Act } return typeName, name, template } + +func fromDisplayName(u *user_model.User) string { + if setting.MailService.FromDisplayNameFormatTemplate != nil { + var ctx bytes.Buffer + err := setting.MailService.FromDisplayNameFormatTemplate.Execute(&ctx, map[string]any{ + "DisplayName": u.DisplayName(), + "AppName": setting.AppName, + "Domain": setting.Domain, + }) + if err == nil { + return mime.QEncoding.Encode("utf-8", ctx.String()) + } + log.Error("fromDisplayName: %w", err) + } + return u.GetCompleteName() +} diff --git a/services/mailer/mail_release.go b/services/mailer/mail_release.go index f1f2e558a77..01a8929e2d0 100644 --- a/services/mailer/mail_release.go +++ b/services/mailer/mail_release.go @@ -86,7 +86,7 @@ func mailNewRelease(ctx context.Context, lang string, tos []*user_model.User, re } msgs := make([]*Message, 0, len(tos)) - publisherName := rel.Publisher.DisplayName() + publisherName := fromDisplayName(rel.Publisher) msgID := generateMessageIDForRelease(rel) for _, to := range tos { msg := NewMessageFrom(to.EmailTo(), publisherName, setting.MailService.FromEmail, subject, mailBody.String()) diff --git a/services/mailer/mail_repo.go b/services/mailer/mail_repo.go index 28b9cef8a75..7003584786a 100644 --- a/services/mailer/mail_repo.go +++ b/services/mailer/mail_repo.go @@ -79,7 +79,7 @@ func sendRepoTransferNotifyMailPerLang(lang string, newOwner, doer *user_model.U } for _, to := range emailTos { - msg := NewMessage(to.EmailTo(), subject, content.String()) + msg := NewMessageFrom(to.EmailTo(), fromDisplayName(doer), setting.MailService.FromEmail, subject, content.String()) msg.Info = fmt.Sprintf("UID: %d, repository pending transfer notification", newOwner.ID) SendAsync(msg) diff --git a/services/mailer/mail_test.go b/services/mailer/mail_test.go index 0739f4233fc..40fd21dea58 100644 --- a/services/mailer/mail_test.go +++ b/services/mailer/mail_test.go @@ -403,3 +403,51 @@ func TestGenerateMessageIDForRelease(t *testing.T) { }) assert.Equal(t, "", msgID) } + +func TestFromDisplayName(t *testing.T) { + template, err := texttmpl.New("mailFrom").Parse("{{ .DisplayName }}") + assert.NoError(t, err) + setting.MailService = &setting.Mailer{FromDisplayNameFormatTemplate: template} + defer func() { setting.MailService = nil }() + + tests := []struct { + userDisplayName string + fromDisplayName string + }{{ + userDisplayName: "test", + fromDisplayName: "test", + }, { + userDisplayName: "Hi Its ", + fromDisplayName: "Hi Its ", + }, { + userDisplayName: "Æsir", + fromDisplayName: "=?utf-8?q?=C3=86sir?=", + }, { + userDisplayName: "new😀user", + fromDisplayName: "=?utf-8?q?new=F0=9F=98=80user?=", + }} + + for _, tc := range tests { + t.Run(tc.userDisplayName, func(t *testing.T) { + user := &user_model.User{FullName: tc.userDisplayName, Name: "tmp"} + got := fromDisplayName(user) + assert.EqualValues(t, tc.fromDisplayName, got) + }) + } + + t.Run("template with all available vars", func(t *testing.T) { + template, err = texttmpl.New("mailFrom").Parse("{{ .DisplayName }} (by {{ .AppName }} on [{{ .Domain }}])") + assert.NoError(t, err) + setting.MailService = &setting.Mailer{FromDisplayNameFormatTemplate: template} + oldAppName := setting.AppName + setting.AppName = "Code IT" + oldDomain := setting.Domain + setting.Domain = "code.it" + defer func() { + setting.AppName = oldAppName + setting.Domain = oldDomain + }() + + assert.EqualValues(t, "Mister X (by Code IT on [code.it])", fromDisplayName(&user_model.User{FullName: "Mister X", Name: "tmp"})) + }) +}