This commit is contained in:
parent
781e2c1559
commit
66d2c957d9
@ -4,6 +4,7 @@ import (
|
||||
"bytes"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
|
||||
"git.pcux.cn/xuans/Smail/dto/parsemail"
|
||||
"golang.org/x/net/html"
|
||||
@ -23,17 +24,27 @@ func extractWeChatVerificationCode(email *parsemail.Email) (string, bool) {
|
||||
return "", false
|
||||
}
|
||||
// 分别检查标题和各正文,避免跨字段拼接出不存在的验证码。
|
||||
if code, ok := findWeChatVerificationCode(truncateWeChatText(email.Subject, maxWeChatVerificationScanBytes)); ok {
|
||||
if code, ok := findWeChatVerificationCode(boundedWeChatVerificationText(email.Subject)); ok {
|
||||
return code, true
|
||||
}
|
||||
text := email.Text[:min(len(email.Text), maxWeChatVerificationScanBytes)]
|
||||
if code, ok := findWeChatVerificationCode(string(text)); ok {
|
||||
plain := string(text)
|
||||
if len(email.Text) > len(text) {
|
||||
plain += "_"
|
||||
}
|
||||
if code, ok := findWeChatVerificationCode(plain); ok {
|
||||
return code, true
|
||||
}
|
||||
return findWeChatVerificationCode(weChatHTMLNotificationText(email.HTML))
|
||||
}
|
||||
|
||||
func findWeChatVerificationCode(content string) (string, bool) {
|
||||
content = strings.Map(func(char rune) rune {
|
||||
if unicode.IsSpace(char) {
|
||||
return ' '
|
||||
}
|
||||
return char
|
||||
}, content)
|
||||
for _, pattern := range weChatVerificationPatterns {
|
||||
for _, match := range pattern.FindAllStringSubmatch(content, -1) {
|
||||
code := match[1]
|
||||
@ -46,8 +57,17 @@ func findWeChatVerificationCode(content string) (string, bool) {
|
||||
return "", false
|
||||
}
|
||||
|
||||
func boundedWeChatVerificationText(content string) string {
|
||||
if len(content) > maxWeChatVerificationScanBytes {
|
||||
// 用标识符边界阻止截断的长串被误识别为完整验证码,不进入通知正文。
|
||||
return truncateWeChatText(content, maxWeChatVerificationScanBytes) + "_"
|
||||
}
|
||||
return content
|
||||
}
|
||||
|
||||
func weChatHTMLNotificationText(content []byte) string {
|
||||
// 限制原始输入,避免大邮件在通知提取阶段产生无界扫描和额外分配。
|
||||
truncated := len(content) > maxWeChatVerificationScanBytes
|
||||
content = content[:min(len(content), maxWeChatVerificationScanBytes)]
|
||||
tokenizer := html.NewTokenizer(bytes.NewReader(content))
|
||||
var text strings.Builder
|
||||
@ -55,8 +75,11 @@ func weChatHTMLNotificationText(content []byte) string {
|
||||
for {
|
||||
switch tokenizer.Next() {
|
||||
case html.ErrorToken:
|
||||
if truncated {
|
||||
text.WriteByte('_')
|
||||
}
|
||||
return text.String()
|
||||
case html.StartTagToken:
|
||||
case html.StartTagToken, html.SelfClosingTagToken:
|
||||
name, _ := tokenizer.TagName()
|
||||
if weChatHiddenHTMLTag(string(name)) {
|
||||
hiddenDepth++
|
||||
@ -69,8 +92,8 @@ func weChatHTMLNotificationText(content []byte) string {
|
||||
case html.TextToken:
|
||||
if hiddenDepth == 0 {
|
||||
// 分词器负责解码实体;不读取属性、注释、脚本及样式中的内容。
|
||||
text.Write(tokenizer.Text())
|
||||
text.WriteByte(' ')
|
||||
text.Write(tokenizer.Text())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,6 +113,115 @@ func TestWeChatRuntimeConfigRejectsTrailingJSON(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFindWeChatVerificationCode(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
content string
|
||||
want string
|
||||
}{
|
||||
{name: "chinese", content: "您的验证码为:123456,请勿泄露", want: "123456"},
|
||||
{name: "before keyword", content: "847291 是您的验证码", want: "847291"},
|
||||
{name: "english", content: "Your verification code is A7B9C2", want: "A7B9C2"},
|
||||
{name: "leading zero", content: "验证码:001234", want: "001234"},
|
||||
{name: "four characters", content: "校验码:a1B2", want: "a1B2"},
|
||||
{name: "eight characters", content: "动态码:a1B2c3D4", want: "a1B2c3D4"},
|
||||
{name: "one time password", content: "一次性密码:123456", want: "123456"},
|
||||
{name: "english security", content: "SECURITY CODE: a1B2c3", want: "a1B2c3"},
|
||||
{name: "english before", content: "123456 is your verification code", want: "123456"},
|
||||
{name: "otp", content: "OTP: 123456", want: "123456"},
|
||||
{name: "nonbreaking space", content: "verification code:\u00a0123456", want: "123456"},
|
||||
{name: "three characters", content: "验证码:123"},
|
||||
{name: "nine characters", content: "验证码:123456789"},
|
||||
{name: "letters only", content: "verification code: secure"},
|
||||
{name: "no keyword", content: "订单号 123456"},
|
||||
{name: "year", content: "2026 年验证码安全规则更新"},
|
||||
{name: "identifier suffix", content: "验证码:123456_value"},
|
||||
{name: "identifier prefix", content: "验证码:value_123456"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, ok := findWeChatVerificationCode(tt.content)
|
||||
if got != tt.want || ok != (tt.want != "") {
|
||||
t.Fatalf("验证码 = %q,匹配 = %v,期望 %q", got, ok, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildWeChatMessageVerificationContent(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
email *parsemail.Email
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "html entities",
|
||||
email: &parsemail.Email{HTML: []byte("<p>验证码: <b>654321</b></p>")},
|
||||
want: "验证码:654321",
|
||||
},
|
||||
{
|
||||
name: "subject priority",
|
||||
email: &parsemail.Email{Subject: "验证码:123456", Text: []byte("验证码:654321")},
|
||||
want: "验证码:123456",
|
||||
},
|
||||
{
|
||||
name: "text priority",
|
||||
email: &parsemail.Email{Text: []byte("验证码:123456"), HTML: []byte("验证码:654321")},
|
||||
want: "验证码:123456",
|
||||
},
|
||||
{
|
||||
name: "ordinary email unchanged",
|
||||
email: &parsemail.Email{Subject: "项目进度 2026", Text: []byte("今天已完成开发")},
|
||||
want: "<<项目进度 2026>> 今天已完成开发",
|
||||
},
|
||||
{
|
||||
name: "do not combine parts",
|
||||
email: &parsemail.Email{Subject: "验证码", Text: []byte("123456")},
|
||||
want: "<<验证码>> 123456",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := buildWeChatMessage(tt.email); got != tt.want {
|
||||
t.Fatalf("通知内容 = %q,期望 %q", got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractWeChatVerificationCodeBoundsInput(t *testing.T) {
|
||||
inputs := map[string]string{
|
||||
"past limit": strings.Repeat(" ", maxWeChatVerificationScanBytes) + "验证码:123456",
|
||||
"partial code": strings.Repeat(" ", maxWeChatVerificationScanBytes-len("验证码:123456")) +
|
||||
"验证码:123456789",
|
||||
}
|
||||
for name, content := range inputs {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
parts := map[string]*parsemail.Email{
|
||||
"subject": {Subject: content},
|
||||
"text": {Text: []byte(content)},
|
||||
"html": {HTML: []byte(content)},
|
||||
}
|
||||
for part, email := range parts {
|
||||
if code, ok := extractWeChatVerificationCode(email); ok || code != "" {
|
||||
t.Fatalf("%s 不应提取扫描范围外或被截断的验证码:%q", part, code)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWeChatHTMLNotificationTextIgnoresAttributes(t *testing.T) {
|
||||
email := &parsemail.Email{
|
||||
Subject: "通知",
|
||||
Text: []byte("查看邮件"),
|
||||
HTML: []byte(`<img alt="验证码:123456"><p>普通消息</p>`),
|
||||
}
|
||||
if got := buildWeChatMessage(email); got != "<<通知>> 查看邮件" {
|
||||
t.Fatalf("HTML 属性不应作为验证码来源:%q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildWeChatMessageLimitsContent(t *testing.T) {
|
||||
message := buildWeChatMessage(&parsemail.Email{
|
||||
Subject: strings.Repeat("界", maxWeChatMessageBytes),
|
||||
|
||||
Loading…
Reference in New Issue
Block a user