fix: 遵循用户发件人原样配置不篡改发信地址并升级版本号至v1.52.3
Some checks failed
Build and Release / Build Release Package (tar.gz, amd64, linux) (push) Has been cancelled
Build and Release / Build Release Package (tar.gz, arm64, linux) (push) Has been cancelled
Build and Release / Build Release Package (zip, amd64, windows) (push) Has been cancelled

This commit is contained in:
xuans 2026-10-18 22:08:54 +08:00
parent 206c90451c
commit 7d83fbc093
10 changed files with 57 additions and 38 deletions

View File

@ -143,11 +143,11 @@ go build -trimpath -o ../bin/scert-admin ./cmd/scert-admin
项目配备统一的打包脚本 `deploy/package.mjs`,产物严格遵循 **`项目名+版本+架构`** 的命名规范:
```bash
# 打包 Linux amd64 归档包 (生成 Scert_v1.52.2_linux_amd64.tar.gz)
node deploy/package.mjs --target=linux_amd64 --version=v1.52.2
# 打包 Linux amd64 归档包 (生成 Scert_v1.52.3_linux_amd64.tar.gz)
node deploy/package.mjs --target=linux_amd64 --version=v1.52.3
# 打包 Windows amd64 压缩包 (生成 Scert_v1.52.2_windows_amd64.zip)
node deploy/package.mjs --target=windows_amd64 --version=v1.52.2
# 打包 Windows amd64 压缩包 (生成 Scert_v1.52.3_windows_amd64.zip)
node deploy/package.mjs --target=windows_amd64 --version=v1.52.3
```
发布包内包含:
@ -157,7 +157,7 @@ node deploy/package.mjs --target=windows_amd64 --version=v1.52.2
- `systemd/` 与 `nginx/` 配置示例模板
- `SHA256SUMS` 完整性校验文件
推送版本标签(例如 `git tag v1.52.2 && git push Scert v1.52.2`)后,GitHub 与 Gitea 会自动触发 Actions 矩阵编译,生成各架构的发布包并自动挂载至 Release 资产清单中。
推送版本标签(例如 `git tag v1.52.3 && git push Scert v1.52.3`)后,GitHub 与 Gitea 会自动触发 Actions 矩阵编译,生成各架构的发布包并自动挂载至 Release 资产清单中。
---
@ -167,8 +167,8 @@ node deploy/package.mjs --target=windows_amd64 --version=v1.52.2
1. 从 Release 页面下载对应架构的发布包并解压:
```bash
tar -zxvf Scert_v1.52.2_linux_amd64.tar.gz
cd Scert_v1.52.2_linux_amd64
tar -zxvf Scert_v1.52.3_linux_amd64.tar.gz
cd Scert_v1.52.3_linux_amd64
```
2. 执行安装脚本:
@ -188,7 +188,7 @@ node deploy/package.mjs --target=windows_amd64 --version=v1.52.2
### Windows
1. 解压 `Scert_v1.52.2_windows_amd64.zip`。
1. 解压 `Scert_v1.52.3_windows_amd64.zip`。
2. 以管理员权限启动 PowerShell 并执行:
```powershell
powershell -ExecutionPolicy Bypass -File .\install-windows.ps1 -SourceDirectory .

View File

@ -59,7 +59,7 @@ func TestCompleterStoresNotificationCredentialsWithoutPlaintext(t *testing.T) {
t.Fatalf("通知选项未完整保存:%+v", stored.Notification)
}
httpCredentials, err := fixture.store.Get(t.Context(), stored.Notification.HTTPCredentialRef)
if stored.Notification.SenderEmail != "notification+system@example.test" || stored.Notification.SenderName != "系统通知" {
if stored.Notification.SenderEmail != "smtp-owner@example.test" || stored.Notification.SenderName != "Scert" {
t.Fatal("初始化没有保存 Smail 系统通知发件人")
}
if err != nil || httpCredentials["password"] != "private-http-password" {

View File

@ -1,3 +1,3 @@
package buildinfo
const Version = "1.52.2"
const Version = "1.52.3"

View File

@ -11,13 +11,26 @@ func (c NotificationConfig) UsesSmailSystemAccount() bool {
return strings.HasPrefix(strings.ToLower(strings.TrimSpace(c.SMTPUsername)), "notify:")
}
// SenderIdentity 根据系统通知账号确定发件人,不能将管理员邮箱当作 SMTP 发件人。
// SenderIdentity 根据用户配置或通知账号确定发件人。
// 遵循“用户怎么填就用什么”的原则,原样使用用户填写的发件人或发信账号,不擅自拼接或篡改发信地址。
func (c NotificationConfig) SenderIdentity() (string, string, error) {
name := strings.TrimSpace(c.SenderName)
email := strings.TrimSpace(c.SenderEmail)
if !c.SMTPEnabled {
return name, email, nil
}
// 如果用户明确填写了发件人邮箱,则原样优先使用
if email != "" {
parsed, err := mail.ParseAddress(email)
if err != nil || parsed.Address != email {
return "", "", errors.New("发件人邮箱地址格式无效")
}
if name == "" {
name = "Scert"
}
return name, email, nil
}
username := strings.ToLower(strings.TrimSpace(c.SMTPUsername))
if c.UsesSmailSystemAccount() {
account := strings.TrimPrefix(username, "notify:")
@ -27,15 +40,19 @@ func (c NotificationConfig) SenderIdentity() (string, string, error) {
strings.ContainsAny(account, "\r\n<>()") {
return "", "", errors.New("Smail 系统通知用户名必须使用 notify:账号@邮件域名 格式")
}
// Smail 保留该发件地址;不能使用登录账号、管理员邮箱或自定义名称替代。
return "系统通知", "notification+system@" + parts[1], nil
}
if email == "" {
parsed, err := mail.ParseAddress(username)
if err != nil || parsed.Address != username || parsed.Name != "" {
return "", "", errors.New("请填写 Smail 系统通知用户名;兼容 SMTP 账号必须是完整邮箱地址")
// 用户未单独指定发件人邮箱时,原样使用 notify: 后的账号邮箱地址,发件人名称优先使用用户配置的 SenderName
if name == "" {
name = "系统通知"
}
email = username
return name, account, nil
}
return name, email, nil
parsed, err := mail.ParseAddress(username)
if err != nil || parsed.Address != username || parsed.Name != "" {
return "", "", errors.New("请填写 Smail 系统通知用户名;兼容 SMTP 账号必须是完整邮箱地址")
}
if name == "" {
name = "Scert"
}
return name, username, nil
}

View File

@ -4,16 +4,17 @@ import "testing"
func TestNotificationSenderIdentity(t *testing.T) {
for _, item := range []struct {
name, username, sender, wantName, wantEmail string
name, username, senderName, senderEmail, wantName, wantEmail string
}{
{"system", "notify:scert@example.test", "", "系统通知", "notification+system@example.test"},
{"stale_admin", "notify:scert@example.test", "admin@other.test", "系统通知", "notification+system@example.test"},
{"normalized", " NOTIFY:SCERT@EXAMPLE.TEST ", "", "系统通知", "notification+system@example.test"},
{"smtp_compatibility", "sender@example.test", "", "Scert", "sender@example.test"},
{"explicit_legacy", "smtp-login", "sender@example.test", "Scert", "sender@example.test"},
{"system_default_name", "notify:scert@example.test", "", "", "系统通知", "scert@example.test"},
{"system_custom_name", "notify:scert@example.test", "Scert通知", "", "Scert通知", "scert@example.test"},
{"system_explicit_sender", "notify:scert@example.test", "Scert", "custom@example.test", "Scert", "custom@example.test"},
{"normalized", " NOTIFY:SCERT@EXAMPLE.TEST ", "", "", "系统通知", "scert@example.test"},
{"smtp_compatibility", "sender@example.test", "Scert", "", "Scert", "sender@example.test"},
{"explicit_legacy", "smtp-login", "Scert", "sender@example.test", "Scert", "sender@example.test"},
} {
t.Run(item.name, func(t *testing.T) {
cfg := NotificationConfig{SMTPEnabled: true, SMTPUsername: item.username, SenderName: "Scert", SenderEmail: item.sender}
cfg := NotificationConfig{SMTPEnabled: true, SMTPUsername: item.username, SenderName: item.senderName, SenderEmail: item.senderEmail}
name, email, err := cfg.SenderIdentity()
if err != nil || name != item.wantName || email != item.wantEmail {
t.Fatalf("发件身份不匹配:name=%q email=%q err=%v", name, email, err)

View File

@ -286,7 +286,8 @@ func TestSmailSystemSenderUsesSameEnvelopeAndHeader(t *testing.T) {
host, port := server.address()
cfg := smtpTestConfig(host, port, "starttls")
cfg.SMTPUsername = "notify:scert@example.test"
cfg.SenderEmail = "admin@other.test"
cfg.SenderName = "系统通知"
cfg.SenderEmail = ""
service := New(cfg, credentialReaderStub{values: map[string]string{"password": "test-only-notification-credential"}})
service.smtpTLSConfig = server.clientTLSConfig
var err error
@ -301,15 +302,15 @@ func TestSmailSystemSenderUsesSameEnvelopeAndHeader(t *testing.T) {
server.mu.Lock()
envelope := server.mailFrom
server.mu.Unlock()
if !strings.HasPrefix(envelope, "MAIL FROM:<notification+system@example.test>") {
if !strings.HasPrefix(envelope, "MAIL FROM:<scert@example.test>") {
t.Fatal("SMTP 信封没有使用系统通知地址")
}
message := string(server.message())
if !strings.Contains(message, formatSMTPAddress(address{Name: "系统通知", Email: "notification+system@example.test"})) || strings.Contains(message, "admin@other.test") {
if !strings.Contains(message, formatSMTPAddress(address{Name: "系统通知", Email: "scert@example.test"})) {
t.Fatal("邮件头没有使用固定系统通知身份,或泄漏旧管理员发件地址")
}
if security && strings.Contains(message, "receiver@example.com") {
t.Fatal("验è¯<EFBFBD>ç <EFBFBD>ä¸<EFBFBD>å¾—å<EFBFBD>‘é€<EFBFBD>给普通通知æ”ä»äº?)
t.Fatal("验证码不得发送给普通通知收件人")
}
})
}
@ -340,7 +341,7 @@ func TestSmailSystemNotificationUsesSMTPOnly(t *testing.T) {
server.mu.Lock()
defer server.mu.Unlock()
if len(server.messages) != 1 {
t.Fatalf("SMTP 系统通知数é‡<EFBFBD>错误ï¼?d", len(server.messages))
t.Fatalf("SMTP 系统通知数量错误:%d", len(server.messages))
}
}
@ -449,7 +450,7 @@ func TestBuildSMTPMessageIncludesStyledHTMLAlternative(t *testing.T) {
"<p style=\"color:red\">HTML 通知</p>",
} {
if !strings.Contains(message, value) {
t.Fatalf("SMTP multipart 邮件缺少 %q�s", value, message)
t.Fatalf("SMTP multipart 邮件缺少 %q:%s", value, message)
}
}
}

View File

@ -1,12 +1,12 @@
{
"name": "scert-frontend",
"version": "1.52.2",
"version": "1.52.3",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "scert-frontend",
"version": "1.52.2",
"version": "1.52.3",
"dependencies": {
"vue": "3.5.41"
},

View File

@ -1,7 +1,7 @@
{
"name": "scert-frontend",
"private": true,
"version": "1.52.2",
"version": "1.52.3",
"type": "module",
"packageManager": "npm@11.17.0",
"engines": {

View File

@ -85,7 +85,7 @@ defineEmits(["select", "settings"]);
<div v-if="currentUser" class="navigation-account-card" aria-label="当前登录账号">
<div class="navigation-account-header">
<span class="navigation-account-label">当前账号</span>
<span class="navigation-version-label">版本 {{ version ? (version.startsWith("v") || version.startsWith("V") ? version : "V" + version) : "V1.52.2" }}</span>
<span class="navigation-version-label">版本 {{ version ? (version.startsWith("v") || version.startsWith("V") ? version : "V" + version) : "V1.52.3" }}</span>
</div>
<div class="navigation-account-row">
<strong class="navigation-account-name" :title="currentUser.account">{{ currentUser.account || "未命名账号" }}</strong>

View File

@ -60,7 +60,7 @@ test("系统设置导航仅对当前已确认的超级管理员显示", async ()
const html = await renderComponent("../Dashboard/DashboardNavigation.vue", {
activeSection: "overview", sections: [],
currentUser: role ? { account: "测试", role } : null,
version: "1.52.2",
version: "1.52.3",
roleLabels: workspaceRoleLabels,
loggingOut: false, error: "", logout() {},
});