diff --git a/.github/release-notes/v1.2.18.md b/.github/release-notes/v1.2.18.md new file mode 100644 index 0000000..90903a0 --- /dev/null +++ b/.github/release-notes/v1.2.18.md @@ -0,0 +1,4 @@ +- 修复邮箱选择列表超过侧栏边框的问题,展开列表现在与上方选择框保持相同宽度。 +- 修复含日期年份的邮件可能漏识别验证码的问题,Gate 等验证码邮件可正常显示一键复制按钮。 +- 邮件通知中的网址改为可点击链接,超长追踪地址使用简短文字显示,阅读更清晰。 +- 版本频道通知移除底部按钮,改为正文中的“查看本次更新”文字链接。 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index bf784d3..49ae4b6 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -298,7 +298,9 @@ jobs: tag = os.environ["RELEASE_TAG"] prefix = f"NewSzxcn Email {html.escape(tag)}\n新版本现已发布\n\n本次更新\n\n" - available = max(0, 3600 - len(prefix)) + release_url = html.escape(os.environ["RELEASE_URL"], quote=True) + footer = f'\n\n🔗 查看本次更新' + available = max(0, 3600 - len(prefix) - len(footer)) visible_sections = [] used = 0 for section in sections: @@ -309,20 +311,18 @@ jobs: used += added body = "\n\n".join(visible_sections) if len(visible_sections) < len(sections): - body += "\n\n更新内容较长,请点击下方按钮查看完整内容。" - open("telegram-release-message.txt", "w", encoding="utf-8").write(prefix + body) + body += "\n\n更新内容较长,请打开下方链接查看完整内容。" + open("telegram-release-message.txt", "w", encoding="utf-8").write(prefix + body + footer) PY jq -n \ --arg chat_id "${TELEGRAM_CHAT_ID}" \ - --arg release_url "${RELEASE_URL}" \ --rawfile text telegram-release-message.txt \ '{ chat_id:$chat_id, text:$text, parse_mode:"HTML", - disable_web_page_preview:true, - reply_markup:{inline_keyboard:[[{text:"查看本次更新 ↗",url:$release_url}]]} + disable_web_page_preview:true }' > telegram-release-payload.json http_code="$(curl -sS --retry 2 --retry-all-errors --connect-timeout 10 --max-time 30 \ diff --git a/apps/api/internal/app/telegram.go b/apps/api/internal/app/telegram.go index c5f0f64..fb55686 100644 --- a/apps/api/internal/app/telegram.go +++ b/apps/api/internal/app/telegram.go @@ -473,6 +473,7 @@ func sanitizeTelegramAttachmentName(value string) string { var ( telegramOTPKeywordRe = regexp.MustCompile(`(?i)(验证码|校验码|动态码|登录码|安全码|一次性密码|otp|verification[ -]?code|security[ -]?code|login[ -]?code|passcode|one[ -]?time[ -]?(?:password|code))`) telegramOTPCandidateRe = regexp.MustCompile(`(?i)[a-z0-9]{4,10}`) + telegramURLRe = regexp.MustCompile(`(?i)https?://[^\s<>"']+`) ) func detectTelegramOTP(subject, body string) string { @@ -506,6 +507,9 @@ func detectTelegramOTP(subject, body string) string { if !hasDigit || telegramOTPKeywordRe.MatchString(value) { continue } + if isTelegramOTPNonCode(value) { + continue + } best := 0 for _, keyword := range keywords { distance := match[0] - keyword[1] @@ -557,6 +561,20 @@ func detectTelegramOTP(subject, body string) string { return items[0].value } +func isTelegramOTPNonCode(value string) bool { + if len(value) == 4 { + if year, err := strconv.Atoi(value); err == nil && year >= 1900 && year <= 2099 { + return true + } + } + if len(value) == 8 { + if _, err := time.Parse("20060102", value); err == nil { + return true + } + } + return false +} + func isTelegramOTPAlphaNumeric(r rune) bool { return r <= unicode.MaxASCII && (unicode.IsLetter(r) || unicode.IsDigit(r)) } @@ -723,7 +741,7 @@ func formatTelegramMailMessage(payload telegramMailPayload) telegramFormattedMes } prefix := strings.Join(lines, "\n") + "\n\n" + label + "\n
" suffix := "" - body = escapeTelegramWithinBudget(body, telegramMessageBudget-utf8.RuneCountInString(prefix)-utf8.RuneCountInString(suffix)) + body = formatTelegramBodyHTML(body, telegramMessageBudget-utf8.RuneCountInString(prefix)-utf8.RuneCountInString(suffix)) lines = []string{prefix + body + suffix} } htmlText := strings.Join(lines, "\n") @@ -731,6 +749,73 @@ func formatTelegramMailMessage(payload telegramMailPayload) telegramFormattedMes return telegramFormattedMessage{HTML: htmlText, PlainText: plain, OTP: payload.OTP} } +func formatTelegramBodyHTML(value string, budget int) string { + if budget <= 3 { + return "" + } + var out strings.Builder + used := 0 + truncated := false + appendEscaped := func(text string) bool { + for _, r := range text { + escaped := html.EscapeString(string(r)) + length := utf8.RuneCountInString(escaped) + if used+length > budget-3 { + return false + } + out.WriteString(escaped) + used += length + } + return true + } + last := 0 + for _, match := range telegramURLRe.FindAllStringIndex(value, -1) { + if !appendEscaped(value[last:match[0]]) { + truncated = true + break + } + rawURL, trailing := trimTelegramURL(value[match[0]:match[1]]) + parsed, err := url.Parse(rawURL) + if err != nil || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") { + if !appendEscaped(value[match[0]:match[1]]) { + truncated = true + break + } + last = match[1] + continue + } + display := rawURL + if utf8.RuneCountInString(display) > 72 { + display = "🔗 " + parsed.Hostname() + " 链接" + } + anchor := `` + html.EscapeString(display) + `` + length := utf8.RuneCountInString(anchor) + if used+length > budget-3 { + truncated = true + break + } + out.WriteString(anchor) + used += length + if !appendEscaped(trailing) { + truncated = true + break + } + last = match[1] + } + if !truncated && last < len(value) && !appendEscaped(value[last:]) { + truncated = true + } + if truncated { + out.WriteString("...") + } + return out.String() +} + +func trimTelegramURL(value string) (string, string) { + trimmed := strings.TrimRight(value, ".,;:!?)]},。;:!?)》】") + return trimmed, value[len(trimmed):] +} + func (a *App) sendTelegramMessage(ctx context.Context, token, chatID, text string) error { _, err := a.sendTelegramPayload(ctx, token, map[string]any{ "chat_id": chatID, diff --git a/apps/api/internal/app/telegram_test.go b/apps/api/internal/app/telegram_test.go index d98dc25..328f3cf 100644 --- a/apps/api/internal/app/telegram_test.go +++ b/apps/api/internal/app/telegram_test.go @@ -223,6 +223,42 @@ func TestTelegramOTPDetectionAndMessageBudget(t *testing.T) { } } +func TestTelegramForwardedGateOTPAndLinks(t *testing.T) { + body := `---------- Forwarded message --------- +Date: 2026年8月6日周四 17:59 +Subject: 登录验证码 (https://www.gate.com) + +Gate 检测到您的账号正试图从此 IP 获得登录验证码: +IP: 87.83.105.229 +如为您本人登录,请输入如下验证码完成操作: +311665 +如非本人操作,请点击此处禁用账户
验证码:778899