From d2cfad3cc4c2de6e8749ddd39bdcd31738ecc8c6 Mon Sep 17 00:00:00 2001 From: zxyszx <299979470+zxyszx@users.noreply.github.com> Date: Mon, 10 Aug 2026 20:24:02 +0800 Subject: [PATCH] feat: streamline forwarding email verification --- .github/release-notes/v1.2.23.md | 5 + VERSION | 2 +- apps/api/internal/app/app_test.go | 19 +++ apps/api/internal/app/forwarding_handlers.go | 12 +- apps/web/src/pages/profile.tsx | 137 ++++++++++++------- 5 files changed, 122 insertions(+), 53 deletions(-) create mode 100644 .github/release-notes/v1.2.23.md diff --git a/.github/release-notes/v1.2.23.md b/.github/release-notes/v1.2.23.md new file mode 100644 index 0000000..a52fbdb --- /dev/null +++ b/.github/release-notes/v1.2.23.md @@ -0,0 +1,5 @@ +- 优化转发验证完成页:移除“返回邮箱”入口,外部收件人确认 Netflix、ChatGPT 等验证码转发授权后不会进入邮箱登录页,只显示验证结果和关闭页面提示。 +- 合并验证邮箱搜索与添加入口:输入内容会实时筛选已添加地址,输入新邮箱时可直接发送验证邮件,已存在地址会明确显示为“已添加”。 +- 重整验证邮箱管理列表:待验证邮箱置顶展示,已验证邮箱按数字和字母排序并聚合为可折叠分组,邮箱数量较多时仍便于查找和管理。 +- 精简邮件转发主页面:不再平铺全部验证邮箱标签,改为显示已验证与待验证数量汇总,点击即可进入管理列表。 +- 补充验证完成页回归测试,确保页面不再出现邮箱首页或登录入口,并完成前端构建、组件规范、后端全量测试与静态检查。 diff --git a/VERSION b/VERSION index 9a83513..a93a6f7 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -1.2.22 +1.2.23 diff --git a/apps/api/internal/app/app_test.go b/apps/api/internal/app/app_test.go index 43dde3e..0e722ac 100644 --- a/apps/api/internal/app/app_test.go +++ b/apps/api/internal/app/app_test.go @@ -2613,6 +2613,25 @@ func TestMailSendQueuesSMTPFailureForRetry(t *testing.T) { } } +func TestForwardingVerificationPageDoesNotLinkToMailbox(t *testing.T) { + a := newTestApp(t) + recorder := httptest.NewRecorder() + + a.renderForwardingVerificationPage(recorder, http.StatusOK, true, "friend@example.test", "该邮箱已通过转发验证") + body := recorder.Body.String() + if recorder.Code != http.StatusOK { + t.Fatalf("status=%d", recorder.Code) + } + for _, forbidden := range []string{`href="/"`, "返回邮箱", "登录"} { + if strings.Contains(body, forbidden) { + t.Fatalf("verification page contains forbidden navigation %q: %s", forbidden, body) + } + } + if !strings.Contains(body, "可以关闭此页面") { + t.Fatalf("verification page is missing close guidance: %s", body) + } +} + func TestInboundForwardingSettingsAndDelivery(t *testing.T) { a := newTestApp(t) stopTestWorkers(a) diff --git a/apps/api/internal/app/forwarding_handlers.go b/apps/api/internal/app/forwarding_handlers.go index b8ea04b..62ab133 100644 --- a/apps/api/internal/app/forwarding_handlers.go +++ b/apps/api/internal/app/forwarding_handlers.go @@ -165,7 +165,7 @@ func (a *App) handleVerifyForwardingEmail(w http.ResponseWriter, r *http.Request a.renderForwardingVerificationPage(w, http.StatusInternalServerError, false, email, "验证失败,请稍后重试") return } - a.renderForwardingVerificationPage(w, http.StatusOK, true, email, "验证完成,可以回到设置页选择此转发目标") + a.renderForwardingVerificationPage(w, http.StatusOK, true, email, "该邮箱已通过转发验证") } func (a *App) handleDeleteForwardingVerifiedEmail(w http.ResponseWriter, r *http.Request) { @@ -439,14 +439,18 @@ func (a *App) renderForwardingVerificationPage(w http.ResponseWriter, status int title := "邮箱转发验证" heading := "验证失败" color := "#dc2626" + statusMark := "!" + closingMessage := "请联系验证发起人重新发送链接" if ok { heading = "验证完成" - color = "#2563eb" + color = "#16a34a" + statusMark = "✓" + closingMessage = "验证结果已记录,可以关闭此页面" } w.Header().Set("Content-Type", "text/html; charset=utf-8") w.WriteHeader(status) - _, _ = fmt.Fprintf(w, `%s

%s

%s

%s

返回邮箱
`, - title, heading, htmlEscape(message), htmlEscape(email), color) + _, _ = fmt.Fprintf(w, `%s

%s

%s

%s

%s

`, + title, color, statusMark, heading, htmlEscape(message), htmlEscape(email), htmlEscape(closingMessage)) } func (a *App) cleanForwardingVerificationEmail(w http.ResponseWriter, r *http.Request, userID, value string) (string, bool) { diff --git a/apps/web/src/pages/profile.tsx b/apps/web/src/pages/profile.tsx index ea88832..719974c 100644 --- a/apps/web/src/pages/profile.tsx +++ b/apps/web/src/pages/profile.tsx @@ -1130,10 +1130,21 @@ function MailboxManagement({ const [accountForwardTargets, setAccountForwardTargets] = React.useState([]) const [verifiedDialogOpen, setVerifiedDialogOpen] = React.useState(false) const [verifiedEmailDraft, setVerifiedEmailDraft] = React.useState("") + const [verifiedEmailsExpanded, setVerifiedEmailsExpanded] = React.useState(false) const [pendingExternalDelete, setPendingExternalDelete] = React.useState(null) const forwarding = useQuery({ queryKey: ["forwarding-settings"], queryFn: api.forwardingSettings, enabled: mailboxes.length > 0 }) const verifiedEmailItems = React.useMemo(() => [...(forwarding.data?.verifiedEmails || [])].sort((a, b) => forwardingTargetCollator.compare(a.email, b.email)), [forwarding.data?.verifiedEmails]) const verifiedEmails = React.useMemo(() => sortForwardingTargets(verifiedEmailItems.filter((item) => item.verified).map((item) => item.email)), [verifiedEmailItems]) + const pendingVerifiedEmailItems = React.useMemo(() => verifiedEmailItems.filter((item) => !item.verified), [verifiedEmailItems]) + const completedVerifiedEmailItems = React.useMemo(() => verifiedEmailItems.filter((item) => item.verified), [verifiedEmailItems]) + const normalizedVerifiedEmailDraft = verifiedEmailDraft.trim().toLowerCase() + const matchingPendingVerifiedEmailItems = React.useMemo(() => normalizedVerifiedEmailDraft + ? pendingVerifiedEmailItems.filter((item) => item.email.toLowerCase().includes(normalizedVerifiedEmailDraft)) + : pendingVerifiedEmailItems, [normalizedVerifiedEmailDraft, pendingVerifiedEmailItems]) + const matchingCompletedVerifiedEmailItems = React.useMemo(() => normalizedVerifiedEmailDraft + ? completedVerifiedEmailItems.filter((item) => item.email.toLowerCase().includes(normalizedVerifiedEmailDraft)) + : completedVerifiedEmailItems, [completedVerifiedEmailItems, normalizedVerifiedEmailDraft]) + const verifiedEmailDraftExists = verifiedEmailItems.some((item) => item.email.toLowerCase() === normalizedVerifiedEmailDraft) const hasPendingVerifiedEmails = verifiedEmailItems.some((item) => !item.verified) const mailboxForwards = React.useMemo>(() => { const next: Record = {} @@ -1363,17 +1374,14 @@ function MailboxManagement({ {verifiedEmailItems.length > 0 && ( -
- {verifiedEmailItems.map((item) => { - const tone = forwardingEmailTone(item) - return ( - - - {item.email} · {tone.shortLabel} - - ) - })} -
+ )} {verifiedEmails.length === 0 &&

暂未添加验证邮箱,请先点击「管理验证邮箱」添加。

}

提示:点击邮箱列表中的「转发」按钮,可在账号级目标之外追加该邮箱自己的转发目标。

@@ -1469,45 +1477,58 @@ function MailboxManagement({ - + { + setVerifiedDialogOpen(open) + if (!open) setVerifiedEmailDraft("") + }}> 验证邮箱管理 -
- 添加并验证外部邮箱地址后,才能用作转发目标。这里只展示投递状态摘要,不展示验证邮件内容。 +
+ 搜索已添加地址,或输入新的外部邮箱并发送验证邮件。
- setVerifiedEmailDraft(event.target.value)} className="h-12 text-base shadow-none" placeholder="输入邮箱地址" disabled={forwardingBusy} /> - +
+ + setVerifiedEmailDraft(event.target.value)} className="h-12 pl-10 text-base shadow-none" placeholder="搜索或输入新邮箱" disabled={forwardingBusy} aria-label="搜索或输入新邮箱" /> +
+
-
- {verifiedEmailItems.map((item) => ( -
-
-
{item.email}
-
{item.verified ? `已验证 - ${formatDateTime(item.verifiedAt || item.createdAt)}` : "待验证"}
- {!item.verified && ( -
- {forwardingEmailStatusText(item)} -
- )} -
-
- - {!item.verified && ( - - )} - -
+
+ {pendingVerifiedEmailItems.length > 0 && ( +
+
待验证 ({pendingVerifiedEmailItems.length})
+ {matchingPendingVerifiedEmailItems.map((item) => ( + + ))} + {matchingPendingVerifiedEmailItems.length === 0 &&
待验证邮箱中没有匹配地址
}
- ))} - {verifiedEmailItems.length === 0 &&
暂无验证邮箱
} + )} + {completedVerifiedEmailItems.length > 0 && ( +
+ + {(verifiedEmailsExpanded || !!normalizedVerifiedEmailDraft) && ( +
+ {matchingCompletedVerifiedEmailItems.map((item) => ( + + ))} + {matchingCompletedVerifiedEmailItems.length === 0 &&
已验证邮箱中没有匹配地址
} +
+ )} +
+ )} + {verifiedEmailItems.length === 0 &&
暂无验证邮箱
} + {normalizedVerifiedEmailDraft && !verifiedEmailDraftExists && matchingPendingVerifiedEmailItems.length === 0 && matchingCompletedVerifiedEmailItems.length === 0 && verifiedEmailItems.length > 0 && ( +
没有匹配地址,可点击「添加」发送验证邮件。
+ )}
- +
@@ -1852,35 +1873,55 @@ function ForwardingTargetPicker({ emails, selected, lockedSelected = [], lockedL ) } +function VerifiedEmailRow({ item, busy, onResend, onRemove }: { + item: ForwardingVerifiedEmail + busy: boolean + onResend: (item: ForwardingVerifiedEmail) => void + onRemove: (id: string, email: string) => void +}) { + const tone = forwardingEmailTone(item) + return ( +
+
+
+ + {item.email} +
+
+ {item.verified ? `验证于 ${formatDateTime(item.verifiedAt || item.createdAt)}` : forwardingEmailStatusText(item)} +
+
+
+ {!item.verified && ( + + )} + +
+
+ ) +} + function forwardingEmailTone(item: ForwardingVerifiedEmail) { if (item.verified) { return { - shortLabel: "已验证", dotClass: "bg-emerald-500", - chipClass: "bg-emerald-100 text-emerald-800", detailClass: "text-emerald-700", } } if (item.deliveryStatus === "failed") { return { - shortLabel: "发送失败", dotClass: "bg-destructive", - chipClass: "bg-destructive/10 text-destructive", detailClass: "text-destructive", } } if (item.deliveryStatus === "delivered") { return { - shortLabel: "待验证", dotClass: "bg-amber-500", - chipClass: "bg-amber-100 text-amber-800", detailClass: "text-amber-700", } } return { - shortLabel: "待验证", dotClass: "bg-muted-foreground", - chipClass: "bg-muted text-foreground", detailClass: "text-foreground", } }