feat(mailer): brand account access emails
giteabot / giteabot (push) Canceled after 0s
giteabot backport / giteabot (push) Canceled after 0s
release-nightly-snapcraft / build-and-publish (push) Canceled after 0s
release-nightly / nightly-binary (push) Canceled after 0s
release-nightly / nightly-container (push) Canceled after 0s

Assisted-by: Codex:GPT-5
This commit is contained in:
2026-08-22 15:51:48 +08:00
parent d10a24cb04
commit bc551009df
4 changed files with 142 additions and 19 deletions
+15 -1
View File
@@ -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)
+52 -1
View File
@@ -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, "用户名:<strong>user2</strong>")
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)
}