From bc551009df0aac9ff93725e5b6c5c7d7eca5b50e Mon Sep 17 00:00:00 2001 From: chermack <505360893@qq.com> Date: Sat, 22 Aug 2026 15:51:48 +0800 Subject: [PATCH] feat(mailer): brand account access emails Assisted-by: Codex:GPT-5 --- services/mailer/mail_user.go | 16 +++++- services/mailer/mail_user_test.go | 53 +++++++++++++++++- templates/mail/user/auth/register_notify.tmpl | 54 +++++++++++++++---- templates/mail/user/auth/reset_passwd.tmpl | 38 ++++++++++--- 4 files changed, 142 insertions(+), 19 deletions(-) diff --git a/services/mailer/mail_user.go b/services/mailer/mail_user.go index 3292d90..a4dd809 100644 --- a/services/mailer/mail_user.go +++ b/services/mailer/mail_user.go @@ -34,6 +34,7 @@ func sendUserMail(language string, u *user_model.User, tpl templates.TplName, co "Code": code, "Language": locale.Language(), } + subject = renderUserMailSubject(tpl, data, subject) var content bytes.Buffer @@ -48,6 +49,18 @@ func sendUserMail(language string, u *user_model.User, tpl templates.TplName, co SendAsync(msg) } +func renderUserMailSubject(tpl templates.TplName, data any, fallback string) string { + var subject bytes.Buffer + if err := LoadedTemplates().SubjectTemplates.ExecuteTemplate(&subject, string(tpl), data); err != nil { + log.Error("ExecuteTemplate [%s]: %v", tpl+"/subject", err) + return fallback + } + if rendered := sanitizeSubject(subject.String()); rendered != "" { + return rendered + } + return fallback +} + // SendActivateAccountMail sends an activation mail to the user (new user registration) func SendActivateAccountMail(locale translation.Locale, u *user_model.User) { if setting.MailService == nil { @@ -119,6 +132,7 @@ func SendRegisterNotifyMail(u *user_model.User) { opts := &user_model.TimeLimitCodeOptions{Purpose: user_model.TimeLimitCodeResetPassword} data["Code"] = user_model.GenerateUserTimeLimitCode(opts, u) } + subject := renderUserMailSubject(mailAuthRegisterNotify, data, locale.TrString("mail.register_notify", setting.AppName)) var content bytes.Buffer @@ -127,7 +141,7 @@ func SendRegisterNotifyMail(u *user_model.User) { return } - msg := sender_service.NewMessage(u.EmailTo(), locale.TrString("mail.register_notify", setting.AppName), content.String()) + msg := sender_service.NewMessage(u.EmailTo(), subject, content.String()) msg.Info = fmt.Sprintf("UID: %d, registration notify", u.ID) SendAsync(msg) diff --git a/services/mailer/mail_user_test.go b/services/mailer/mail_user_test.go index 0b42d80..645751e 100644 --- a/services/mailer/mail_user_test.go +++ b/services/mailer/mail_user_test.go @@ -4,6 +4,7 @@ package mailer import ( + "mime" "net/url" "regexp" "testing" @@ -19,11 +20,20 @@ import ( "github.com/stretchr/testify/require" ) +func assertMailSubject(t *testing.T, expected, actual string) { + t.Helper() + decoded, err := (&mime.WordDecoder{}).DecodeHeader(actual) + require.NoError(t, err) + assert.Equal(t, expected, decoded) +} + func TestSendRegisterNotifyMailPasswordLink(t *testing.T) { require.NoError(t, unittest.PrepareTestDatabase()) defer test.MockVariableValue(&setting.MailService, &setting.Mailer{})() defer test.MockVariableValue(&setting.AppURL, "https://example.com/")() + defer test.MockVariableValue(&setting.Service.ActiveCodeLives, 180)() + defer test.MockVariableValue(&setting.Service.ResetPwdCodeLives, 180)() var sent []*sender_service.Message defer test.MockVariableValue(&SendAsync, func(msgs ...*sender_service.Message) { @@ -34,6 +44,9 @@ func TestSendRegisterNotifyMailPasswordLink(t *testing.T) { SendRegisterNotifyMail(u) require.Len(t, sent, 1) + assertMailSubject(t, "海南荣刻科技有限公司|账号信息", sent[0].Subject) + assert.Contains(t, sent[0].Body, "海南荣刻科技有限公司已为您开通代码协作平台账号") + assert.Contains(t, sent[0].Body, "用户名:user2") assert.NotContains(t, sent[0].Body, "/user/forgot_password") codePattern := regexp.MustCompile(`user/recover_account\?code=([^"]+)`) @@ -47,7 +60,7 @@ func TestSendRegisterNotifyMailPasswordLink(t *testing.T) { verifiedUser := user_model.VerifyUserTimeLimitCode(t.Context(), &user_model.TimeLimitCodeOptions{ Purpose: user_model.TimeLimitCodeResetPassword, }, code) - require.NotNil(t, verifiedUser) + require.NotNil(t, verifiedUser, "rendered code: %q", match[1]) assert.Equal(t, u.ID, verifiedUser.ID) } @@ -56,6 +69,8 @@ func TestSendRegisterNotifyMailExternalAccountUsesForgotPassword(t *testing.T) { defer test.MockVariableValue(&setting.MailService, &setting.Mailer{})() defer test.MockVariableValue(&setting.AppURL, "https://example.com/")() + defer test.MockVariableValue(&setting.Service.ActiveCodeLives, 180)() + defer test.MockVariableValue(&setting.Service.ResetPwdCodeLives, 180)() var sent []*sender_service.Message defer test.MockVariableValue(&SendAsync, func(msgs ...*sender_service.Message) { @@ -67,6 +82,42 @@ func TestSendRegisterNotifyMailExternalAccountUsesForgotPassword(t *testing.T) { SendRegisterNotifyMail(u) require.Len(t, sent, 1) + assertMailSubject(t, "海南荣刻科技有限公司|账号信息", sent[0].Subject) assert.Contains(t, sent[0].Body, "https://example.com/user/forgot_password") assert.NotContains(t, sent[0].Body, "/user/recover_account?code=") } + +func TestSendResetPasswordMailBrandingAndLink(t *testing.T) { + require.NoError(t, unittest.PrepareTestDatabase()) + + defer test.MockVariableValue(&setting.MailService, &setting.Mailer{})() + defer test.MockVariableValue(&setting.AppURL, "https://example.com/")() + defer test.MockVariableValue(&setting.Service.ActiveCodeLives, 180)() + defer test.MockVariableValue(&setting.Service.ResetPwdCodeLives, 180)() + + var sent []*sender_service.Message + defer test.MockVariableValue(&SendAsync, func(msgs ...*sender_service.Message) { + sent = append(sent, msgs...) + })() + + u := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: 2}) + SendResetPasswordMail(u) + + require.Len(t, sent, 1) + assertMailSubject(t, "海南荣刻科技有限公司|账号访问链接", sent[0].Subject) + assert.Contains(t, sent[0].Body, "我们收到了一次账号访问设置请求") + + codePattern := regexp.MustCompile(`user/recover_account\?code=([^"]+)`) + match := codePattern.FindStringSubmatch(sent[0].Body) + if !assert.Len(t, match, 2) { + return + } + code, err := url.QueryUnescape(match[1]) + require.NoError(t, err) + + verifiedUser := user_model.VerifyUserTimeLimitCode(t.Context(), &user_model.TimeLimitCodeOptions{ + Purpose: user_model.TimeLimitCodeResetPassword, + }, code) + require.NotNil(t, verifiedUser, "rendered code: %q", match[1]) + assert.Equal(t, u.ID, verifiedUser.ID) +} diff --git a/templates/mail/user/auth/register_notify.tmpl b/templates/mail/user/auth/register_notify.tmpl index 42482e5..7083771 100644 --- a/templates/mail/user/auth/register_notify.tmpl +++ b/templates/mail/user/auth/register_notify.tmpl @@ -1,19 +1,55 @@ +海南荣刻科技有限公司|账号信息 +--- - +
- + -{{.locale.Tr "mail.hi_user_x" (.DisplayName|DotEscape)}}
{{.locale.Tr "mail.register_notify.text_1" AppName}}
{{.locale.Tr "mail.register_notify.text_2" .Username}}
{{.locale.Tr "mail.register_notify.text_3" $set_pwd_url}}
{{.DisplayName | DotEscape}},您好:
+ +海南荣刻科技有限公司已为您开通代码协作平台账号。
+ ++ 用户名:{{.Username}} +
+ + {{if .Code}} +请在 {{.ResetPwdCodeLives}} 内点击下面的链接设置登录密码:
+ {{else}} +请点击下面的链接设置登录密码:
+ {{end}} + ++ 完成账号设置 +
+ +{{$set_pwd_url}}
+ +完成账号设置后,可通过以下地址访问平台:
+ + + +
+ 如果您并未预期收到此邮件,请联系管理员确认。
+ 本邮件由系统自动发送,请勿直接回复。
+
+ 海南荣刻科技有限公司
+ 代码协作平台
+ {{AppUrl}}
+
{{.locale.Tr "mail.hi_user_x" (.DisplayName|DotEscape)}}
{{.locale.Tr "mail.reset_password.text" .ResetPwdCodeLives}}
{{.locale.Tr "mail.link_not_working_do_paste"}}
+ +{{.DisplayName | DotEscape}},您好:
+ +我们收到了一次账号访问设置请求。
+ +请在 {{.ResetPwdCodeLives}} 内点击下面的链接完成设置:
+ ++ 继续操作 +
+ +{{$recover_url}}
+ ++ 如果您没有发起本次请求,可以忽略此邮件。为保障账号安全,请勿将此链接转发给他人。 +
+ +
+ 海南荣刻科技有限公司
+ 代码协作平台
+ {{AppUrl}}
+