fix: refine Telegram links and mailbox selector
Docker Release / Check web and api (push) Waiting to run
Docker Release / Resolve release tag (push) Blocked by required conditions
Docker Release / Build and publish all-in-one (push) Blocked by required conditions
Docker Release / Build and publish api (push) Blocked by required conditions
Docker Release / Build and publish web (push) Blocked by required conditions
Docker Release / Build and publish dovecot (push) Blocked by required conditions
Docker Release / Build and publish postfix (push) Blocked by required conditions
Docker Release / Build and publish rspamd (push) Blocked by required conditions
Docker Release / Create GitHub release (push) Blocked by required conditions
Docker Release / Check web and api (push) Waiting to run
Docker Release / Resolve release tag (push) Blocked by required conditions
Docker Release / Build and publish all-in-one (push) Blocked by required conditions
Docker Release / Build and publish api (push) Blocked by required conditions
Docker Release / Build and publish web (push) Blocked by required conditions
Docker Release / Build and publish dovecot (push) Blocked by required conditions
Docker Release / Build and publish postfix (push) Blocked by required conditions
Docker Release / Build and publish rspamd (push) Blocked by required conditions
Docker Release / Create GitHub release (push) Blocked by required conditions
This commit is contained in:
@@ -0,0 +1,4 @@
|
|||||||
|
- 修复邮箱选择列表超过侧栏边框的问题,展开列表现在与上方选择框保持相同宽度。
|
||||||
|
- 修复含日期年份的邮件可能漏识别验证码的问题,Gate 等验证码邮件可正常显示一键复制按钮。
|
||||||
|
- 邮件通知中的网址改为可点击链接,超长追踪地址使用简短文字显示,阅读更清晰。
|
||||||
|
- 版本频道通知移除底部按钮,改为正文中的“查看本次更新”文字链接。
|
||||||
@@ -298,7 +298,9 @@ jobs:
|
|||||||
|
|
||||||
tag = os.environ["RELEASE_TAG"]
|
tag = os.environ["RELEASE_TAG"]
|
||||||
prefix = f"<b>NewSzxcn Email {html.escape(tag)}</b>\n新版本现已发布\n\n<b>本次更新</b>\n\n"
|
prefix = f"<b>NewSzxcn Email {html.escape(tag)}</b>\n新版本现已发布\n\n<b>本次更新</b>\n\n"
|
||||||
available = max(0, 3600 - len(prefix))
|
release_url = html.escape(os.environ["RELEASE_URL"], quote=True)
|
||||||
|
footer = f'\n\n🔗 <a href="{release_url}">查看本次更新</a>'
|
||||||
|
available = max(0, 3600 - len(prefix) - len(footer))
|
||||||
visible_sections = []
|
visible_sections = []
|
||||||
used = 0
|
used = 0
|
||||||
for section in sections:
|
for section in sections:
|
||||||
@@ -309,20 +311,18 @@ jobs:
|
|||||||
used += added
|
used += added
|
||||||
body = "\n\n".join(visible_sections)
|
body = "\n\n".join(visible_sections)
|
||||||
if len(visible_sections) < len(sections):
|
if len(visible_sections) < len(sections):
|
||||||
body += "\n\n更新内容较长,请点击下方按钮查看完整内容。"
|
body += "\n\n更新内容较长,请打开下方链接查看完整内容。"
|
||||||
open("telegram-release-message.txt", "w", encoding="utf-8").write(prefix + body)
|
open("telegram-release-message.txt", "w", encoding="utf-8").write(prefix + body + footer)
|
||||||
PY
|
PY
|
||||||
|
|
||||||
jq -n \
|
jq -n \
|
||||||
--arg chat_id "${TELEGRAM_CHAT_ID}" \
|
--arg chat_id "${TELEGRAM_CHAT_ID}" \
|
||||||
--arg release_url "${RELEASE_URL}" \
|
|
||||||
--rawfile text telegram-release-message.txt \
|
--rawfile text telegram-release-message.txt \
|
||||||
'{
|
'{
|
||||||
chat_id:$chat_id,
|
chat_id:$chat_id,
|
||||||
text:$text,
|
text:$text,
|
||||||
parse_mode:"HTML",
|
parse_mode:"HTML",
|
||||||
disable_web_page_preview:true,
|
disable_web_page_preview:true
|
||||||
reply_markup:{inline_keyboard:[[{text:"查看本次更新 ↗",url:$release_url}]]}
|
|
||||||
}' > telegram-release-payload.json
|
}' > telegram-release-payload.json
|
||||||
|
|
||||||
http_code="$(curl -sS --retry 2 --retry-all-errors --connect-timeout 10 --max-time 30 \
|
http_code="$(curl -sS --retry 2 --retry-all-errors --connect-timeout 10 --max-time 30 \
|
||||||
|
|||||||
@@ -473,6 +473,7 @@ func sanitizeTelegramAttachmentName(value string) string {
|
|||||||
var (
|
var (
|
||||||
telegramOTPKeywordRe = regexp.MustCompile(`(?i)(验证码|校验码|动态码|登录码|安全码|一次性密码|otp|verification[ -]?code|security[ -]?code|login[ -]?code|passcode|one[ -]?time[ -]?(?:password|code))`)
|
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}`)
|
telegramOTPCandidateRe = regexp.MustCompile(`(?i)[a-z0-9]{4,10}`)
|
||||||
|
telegramURLRe = regexp.MustCompile(`(?i)https?://[^\s<>"']+`)
|
||||||
)
|
)
|
||||||
|
|
||||||
func detectTelegramOTP(subject, body string) string {
|
func detectTelegramOTP(subject, body string) string {
|
||||||
@@ -506,6 +507,9 @@ func detectTelegramOTP(subject, body string) string {
|
|||||||
if !hasDigit || telegramOTPKeywordRe.MatchString(value) {
|
if !hasDigit || telegramOTPKeywordRe.MatchString(value) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if isTelegramOTPNonCode(value) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
best := 0
|
best := 0
|
||||||
for _, keyword := range keywords {
|
for _, keyword := range keywords {
|
||||||
distance := match[0] - keyword[1]
|
distance := match[0] - keyword[1]
|
||||||
@@ -557,6 +561,20 @@ func detectTelegramOTP(subject, body string) string {
|
|||||||
return items[0].value
|
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 {
|
func isTelegramOTPAlphaNumeric(r rune) bool {
|
||||||
return r <= unicode.MaxASCII && (unicode.IsLetter(r) || unicode.IsDigit(r))
|
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<b>" + label + "</b>\n<blockquote>"
|
prefix := strings.Join(lines, "\n") + "\n\n<b>" + label + "</b>\n<blockquote>"
|
||||||
suffix := "</blockquote>"
|
suffix := "</blockquote>"
|
||||||
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}
|
lines = []string{prefix + body + suffix}
|
||||||
}
|
}
|
||||||
htmlText := strings.Join(lines, "\n")
|
htmlText := strings.Join(lines, "\n")
|
||||||
@@ -731,6 +749,73 @@ func formatTelegramMailMessage(payload telegramMailPayload) telegramFormattedMes
|
|||||||
return telegramFormattedMessage{HTML: htmlText, PlainText: plain, OTP: payload.OTP}
|
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 := `<a href="` + html.EscapeString(rawURL) + `">` + html.EscapeString(display) + `</a>`
|
||||||
|
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 {
|
func (a *App) sendTelegramMessage(ctx context.Context, token, chatID, text string) error {
|
||||||
_, err := a.sendTelegramPayload(ctx, token, map[string]any{
|
_, err := a.sendTelegramPayload(ctx, token, map[string]any{
|
||||||
"chat_id": chatID,
|
"chat_id": chatID,
|
||||||
|
|||||||
@@ -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
|
||||||
|
如非本人操作,请点击此处禁用账户 <https://data.gate.com/track/click?token=abcdefghijklmnopqrstuvwxyz0123456789abcdefghijklmnopqrstuvwxyz0123456789>`
|
||||||
|
if otp := detectTelegramOTP("Fwd: 登录验证码 (https://www.gate.com)", body); otp != "311665" {
|
||||||
|
t.Fatalf("forwarded Gate OTP not detected: %q", otp)
|
||||||
|
}
|
||||||
|
if otp := detectTelegramOTP("登录验证码", "日期 2026-08-06,验证码将在稍后发送"); otp != "" {
|
||||||
|
t.Fatalf("year was incorrectly detected as OTP: %q", otp)
|
||||||
|
}
|
||||||
|
message := formatTelegramMailMessage(telegramMailPayload{
|
||||||
|
From: "no-reply@alert.gate.com", Recipient: "admin@example.com", Subject: "登录验证码",
|
||||||
|
ReceivedAt: time.Now().UTC().Format(time.RFC3339Nano), Body: body, BodyMode: "full", OTP: "311665",
|
||||||
|
})
|
||||||
|
if !strings.Contains(message.HTML, `<a href="https://www.gate.com">https://www.gate.com</a>`) {
|
||||||
|
t.Fatalf("normal URL was not linkified: %s", message.HTML)
|
||||||
|
}
|
||||||
|
if !strings.Contains(message.HTML, `>🔗 data.gate.com 链接</a>`) {
|
||||||
|
t.Fatalf("long tracking URL was not shortened: %s", message.HTML)
|
||||||
|
}
|
||||||
|
if strings.Contains(message.HTML, "<a href=") || utf8.RuneCountInString(message.HTML) > telegramMessageBudget {
|
||||||
|
t.Fatalf("generated Telegram HTML is invalid or too long: %s", message.HTML)
|
||||||
|
}
|
||||||
|
markup := telegramCopyMarkup(message.OTP)
|
||||||
|
buttons, ok := markup["inline_keyboard"].([][]map[string]any)
|
||||||
|
if !ok || len(buttons) != 1 || len(buttons[0]) != 1 || buttons[0][0]["text"] != "复制验证码" {
|
||||||
|
t.Fatalf("copy OTP button missing: %#v", markup)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestTelegramPseudoHTMLAndBodyCharset(t *testing.T) {
|
func TestTelegramPseudoHTMLAndBodyCharset(t *testing.T) {
|
||||||
pseudo := `<html><head><style>.hidden{display:none}</style></head><body><p>验证码:778899</p><div>欢迎登录</div></body></html>`
|
pseudo := `<html><head><style>.hidden{display:none}</style></head><body><p>验证码:778899</p><div>欢迎登录</div></body></html>`
|
||||||
text := telegramMessageBody(storedMessage{BodyText: pseudo})
|
text := telegramMessageBody(storedMessage{BodyText: pseudo})
|
||||||
|
|||||||
@@ -3292,7 +3292,7 @@ function MailboxSwitcher({ collapsed, mailboxes, loading, selectedMailboxId, sel
|
|||||||
align="start"
|
align="start"
|
||||||
className={cn(
|
className={cn(
|
||||||
"max-w-[calc(100vw-32px)] p-1",
|
"max-w-[calc(100vw-32px)] p-1",
|
||||||
collapsed ? "w-[204px]" : "w-[21rem] min-w-[var(--radix-dropdown-menu-trigger-width)]"
|
collapsed ? "w-[204px]" : "w-[var(--radix-dropdown-menu-trigger-width)] min-w-0"
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{mailboxes.length > 0 && (
|
{mailboxes.length > 0 && (
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
| NSX-20260806-004 | 2026-08-06 | 已完成 | 前端/UI/响应式布局 | “全部邮箱”选择器右侧存在复制按钮空白占位 | S3 | v1.2.15 | 随 v1.2.15 发布 |
|
| NSX-20260806-004 | 2026-08-06 | 已完成 | 前端/UI/响应式布局 | “全部邮箱”选择器右侧存在复制按钮空白占位 | S3 | v1.2.15 | 随 v1.2.15 发布 |
|
||||||
| NSX-20260806-005 | 2026-08-06 | 已完成 | 后端/通知;前端/设置;部署运维/CI | Telegram 私聊邮件通知与 Release 频道通知 | S3 | v1.2.16 | 随 v1.2.16 发布 |
|
| NSX-20260806-005 | 2026-08-06 | 已完成 | 后端/通知;前端/设置;部署运维/CI | Telegram 私聊邮件通知与 Release 频道通知 | S3 | v1.2.16 | 随 v1.2.16 发布 |
|
||||||
| NSX-20260806-006 | 2026-08-06 | 已完成 | 后端/通知;邮件核心;前端/设置;质量复核 | Telegram 邮件通知安全、验证码复制和可靠性复核 | S2 | v1.2.17 | 随 v1.2.17 发布 |
|
| NSX-20260806-006 | 2026-08-06 | 已完成 | 后端/通知;邮件核心;前端/设置;质量复核 | Telegram 邮件通知安全、验证码复制和可靠性复核 | S2 | v1.2.17 | 随 v1.2.17 发布 |
|
||||||
|
| NSX-20260806-007 | 2026-08-06 | 待验收 | 前端/UI;后端/通知;部署运维/CI | 邮箱下拉层越界、验证码漏识别、邮件与版本通知链接样式 | S3 | v1.2.18 | 待发布 |
|
||||||
|
|
||||||
## NSX-20260804-001
|
## NSX-20260804-001
|
||||||
|
|
||||||
@@ -252,3 +253,25 @@
|
|||||||
| 2026-08-06 | 用户确认继续修改,并明确保留现有机器人 Token。 |
|
| 2026-08-06 | 用户确认继续修改,并明确保留现有机器人 Token。 |
|
||||||
| 2026-08-06 | 完成实现和自动化回归,状态流转为待验收。 |
|
| 2026-08-06 | 完成实现和自动化回归,状态流转为待验收。 |
|
||||||
| 2026-08-06 | 完成桌面端与移动端页面验收及最终回归,状态流转为已完成。 |
|
| 2026-08-06 | 完成桌面端与移动端页面验收及最终回归,状态流转为已完成。 |
|
||||||
|
|
||||||
|
## NSX-20260806-007
|
||||||
|
|
||||||
|
| 字段 | 内容 |
|
||||||
|
| --- | --- |
|
||||||
|
| 编号 | NSX-20260806-007 |
|
||||||
|
| 日期 | 2026-08-06 |
|
||||||
|
| 状态 | 待验收 |
|
||||||
|
| 模块 | 前端/UI;后端/通知;部署运维/CI;质量复核 |
|
||||||
|
| 现象 | 邮箱选择器展开层超过侧栏边框;含日期年份的验证码邮件未显示复制按钮;邮件长链接难以阅读;Release 通知底部按钮需改为正文文字链接。 |
|
||||||
|
| 根因 | 展开层固定为 21rem,未跟随触发按钮;年份与真实验证码同时进入评分后触发歧义保护;正文仅转义未生成显式链接;Release 工作流使用 inline keyboard。 |
|
||||||
|
| 实现 | 展开层宽度跟随触发按钮;排除年份和紧凑日期候选;正文 URL 安全转义并生成链接,长追踪地址缩短显示;Release 移除按钮并在正文末尾加入“查看本次更新”链接。 |
|
||||||
|
| 目标版本 | v1.2.18 |
|
||||||
|
| 测试结果 | Gate 转发邮件验证码与链接专项测试、Telegram 全部竞态测试、Go 全量测试和 vet、前端 check/build、工作流 YAML、差异格式检查均通过;桌面端“全部邮箱”和具体邮箱状态下触发按钮与下拉层均为 263px,左右边界一致且无控制台错误。 |
|
||||||
|
| 发布状态 | 待发布。 |
|
||||||
|
|
||||||
|
### 历史
|
||||||
|
|
||||||
|
| 时间 | 记录 |
|
||||||
|
| --- | --- |
|
||||||
|
| 2026-08-06 | 用户提供三张截图并确认本批修改范围,问题进入处理中。 |
|
||||||
|
| 2026-08-06 | 完成实现、自动化回归和桌面端视觉验收,状态流转为待验收。 |
|
||||||
|
|||||||
Reference in New Issue
Block a user