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
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:
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -1,19 +1,55 @@
|
||||
海南荣刻科技有限公司|账号信息
|
||||
---
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<meta charset="utf-8">
|
||||
<meta name="format-detection" content="telephone=no,date=no,address=no,email=no,url=no">
|
||||
<title>{{.locale.Tr "mail.register_notify.title" (.DisplayName|DotEscape) AppName}}</title>
|
||||
<title>账号信息</title>
|
||||
</head>
|
||||
|
||||
{{$set_pwd_url := printf "%suser/forgot_password" AppUrl}}
|
||||
{{if .Code}}{{$set_pwd_url = printf "%suser/recover_account?code=%s" AppUrl (QueryEscape .Code)}}{{end}}
|
||||
<body>
|
||||
<p>{{.locale.Tr "mail.hi_user_x" (.DisplayName|DotEscape)}}</p><br>
|
||||
<p>{{.locale.Tr "mail.register_notify.text_1" AppName}}</p><br>
|
||||
<p>{{.locale.Tr "mail.register_notify.text_2" .Username}}</p><p><a href="{{AppUrl}}user/login">{{AppUrl}}user/login</a></p><br>
|
||||
<p>{{.locale.Tr "mail.register_notify.text_3" $set_pwd_url}}</p><br>
|
||||
<body style="margin:0;padding:24px;background:#f5f7fa;color:#1f2937;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Microsoft YaHei',Arial,sans-serif;line-height:1.7;">
|
||||
<div style="max-width:620px;margin:0 auto;background:#ffffff;border:1px solid #e5e7eb;border-radius:10px;padding:32px;">
|
||||
<h2 style="margin:0 0 24px;color:#111827;font-size:22px;">账号信息</h2>
|
||||
|
||||
<p>© <a href="{{AppUrl}}">{{AppName}}</a></p>
|
||||
<p>{{.DisplayName | DotEscape}},您好:</p>
|
||||
|
||||
<p>海南荣刻科技有限公司已为您开通代码协作平台账号。</p>
|
||||
|
||||
<p>
|
||||
用户名:<strong>{{.Username}}</strong>
|
||||
</p>
|
||||
|
||||
{{if .Code}}
|
||||
<p>请在 {{.ResetPwdCodeLives}} 内点击下面的链接设置登录密码:</p>
|
||||
{{else}}
|
||||
<p>请点击下面的链接设置登录密码:</p>
|
||||
{{end}}
|
||||
|
||||
<p>
|
||||
<a href="{{$set_pwd_url}}" style="display:inline-block;padding:10px 18px;border-radius:6px;background:#2563eb;color:#ffffff;text-decoration:none;">完成账号设置</a>
|
||||
</p>
|
||||
|
||||
<p style="word-break:break-all;color:#4b5563;font-size:13px;">{{$set_pwd_url}}</p>
|
||||
|
||||
<p>完成账号设置后,可通过以下地址访问平台:</p>
|
||||
|
||||
<p>
|
||||
<a href="{{AppUrl}}user/login">{{AppUrl}}user/login</a>
|
||||
</p>
|
||||
|
||||
<p style="margin-top:28px;color:#6b7280;font-size:13px;">
|
||||
如果您并未预期收到此邮件,请联系管理员确认。<br>
|
||||
本邮件由系统自动发送,请勿直接回复。
|
||||
</p>
|
||||
|
||||
<p style="margin:24px 0 0;color:#374151;">
|
||||
海南荣刻科技有限公司<br>
|
||||
代码协作平台<br>
|
||||
{{AppUrl}}
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,17 +1,39 @@
|
||||
海南荣刻科技有限公司|账号访问链接
|
||||
---
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
<meta charset="utf-8">
|
||||
<meta name="format-detection" content="telephone=no,date=no,address=no,email=no,url=no">
|
||||
<title>{{.locale.Tr "mail.reset_password.title" (.DisplayName|DotEscape)}}</title>
|
||||
<title>账号访问链接</title>
|
||||
</head>
|
||||
|
||||
{{$recover_url := printf "%suser/recover_account?code=%s" AppUrl (QueryEscape .Code)}}
|
||||
<body>
|
||||
<p>{{.locale.Tr "mail.hi_user_x" (.DisplayName|DotEscape)}}</p><br>
|
||||
<p>{{.locale.Tr "mail.reset_password.text" .ResetPwdCodeLives}}</p><p><a href="{{$recover_url}}">{{$recover_url}}</a></p><br>
|
||||
<p>{{.locale.Tr "mail.link_not_working_do_paste"}}</p>
|
||||
<body style="margin:0;padding:24px;background:#f5f7fa;color:#1f2937;font-family:-apple-system,BlinkMacSystemFont,'Segoe UI','Microsoft YaHei',Arial,sans-serif;line-height:1.7;">
|
||||
<div style="max-width:620px;margin:0 auto;background:#ffffff;border:1px solid #e5e7eb;border-radius:10px;padding:32px;">
|
||||
<h2 style="margin:0 0 24px;color:#111827;font-size:22px;">账号访问链接</h2>
|
||||
|
||||
<p>© <a href="{{AppUrl}}">{{AppName}}</a></p>
|
||||
<p>{{.DisplayName | DotEscape}},您好:</p>
|
||||
|
||||
<p>我们收到了一次账号访问设置请求。</p>
|
||||
|
||||
<p>请在 {{.ResetPwdCodeLives}} 内点击下面的链接完成设置:</p>
|
||||
|
||||
<p>
|
||||
<a href="{{$recover_url}}" style="display:inline-block;padding:10px 18px;border-radius:6px;background:#2563eb;color:#ffffff;text-decoration:none;">继续操作</a>
|
||||
</p>
|
||||
|
||||
<p style="word-break:break-all;color:#4b5563;font-size:13px;">{{$recover_url}}</p>
|
||||
|
||||
<p style="margin-top:24px;color:#6b7280;font-size:13px;">
|
||||
如果您没有发起本次请求,可以忽略此邮件。为保障账号安全,请勿将此链接转发给他人。
|
||||
</p>
|
||||
|
||||
<p style="margin:24px 0 0;color:#374151;">
|
||||
海南荣刻科技有限公司<br>
|
||||
代码协作平台<br>
|
||||
{{AppUrl}}
|
||||
</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user