diff --git a/src/services/mime.ts b/src/services/mime.ts index 8ef3437..8499903 100644 --- a/src/services/mime.ts +++ b/src/services/mime.ts @@ -100,12 +100,25 @@ export function decodeMimeHeader(value: string | null): string | null { } export function stripHtmlTags(text: string): string { - return text - .replace(//gi, "") - .replace(//gi, "") - .replace(/<[^>]+>/g, " ") - .replace(/\s+/g, " ") - .trim(); + return ( + text + .replace(//gi, "") + .replace(//gi, "") + // 保留链接: text → "text [url]"。 + // 验证码邮件常常只有一个按钮链接 (如 Netflix 的 Get Code), 剥掉 href 会让 + // LLM 无码可提取。仅 html-only 邮件走到这里, 多部分邮件优先用 text/plain。 + .replace( + /]*?href\s*=\s*(?:"([^"]*)"|'([^']*)')[^>]*>([\s\S]*?)<\/a>/gi, + (_match, doubleQuoted, singleQuoted, inner) => { + const url = (doubleQuoted ?? singleQuoted ?? "").replace(/&/gi, "&").trim(); + const label = inner.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim(); + return /^https?:\/\//i.test(url) ? ` ${label} [${url}] ` : ` ${label} `; + }, + ) + .replace(/<[^>]+>/g, " ") + .replace(/\s+/g, " ") + .trim() + ); } // 主题看起来像验证码/事务性邮件 (verification, OTP, sign-in, reset, 验证码…)。 @@ -199,7 +212,13 @@ export function extractMailBodies(rawEmail: string): { const decodedBody = decodeBodyPart(topBody, encoding, getCharset(topContentType)); const topMimeType = topContentType?.split(";")[0]?.trim().toLowerCase() ?? ""; const htmlMatch = decodedBody.match(//i) ?? decodedBody.match(//i); - const htmlBody = htmlMatch ? htmlMatch[0].trim() : null; + // Content-Type 明确是 text/html 时整个正文就是 HTML, 即使没有 / + // 包裹 (Yandex 等客户端转发出的就是裸片段), 否则 Rendered 预览会拿不到内容。 + const htmlBody = htmlMatch + ? htmlMatch[0].trim() + : topMimeType.includes("text/html") && decodedBody.trim() + ? decodedBody.trim() + : null; const decodedText = topMimeType.includes("text/html") ? stripHtmlTags(decodedBody) : decodedBody.trim(); const textBody = decodedText ? decodedText : htmlBody ? stripHtmlTags(htmlBody) : null; diff --git a/test/mime.test.ts b/test/mime.test.ts index 53d1a91..93214e6 100644 --- a/test/mime.test.ts +++ b/test/mime.test.ts @@ -4,6 +4,7 @@ import { decodeQuotedPrintable, extractMailBodies, isPromotionalEmail, + stripHtmlTags, } from "../src/services/mime"; describe("mime decoding", () => { @@ -92,3 +93,31 @@ describe("isPromotionalEmail", () => { expect(isPromotionalEmail(headers, "Subject: Hello\r\n\r\nbody")).toBe(false); }); }); + +describe("stripHtmlTags", () => { + it("preserves anchor hrefs so button-only mails keep their link", () => { + const html = '

Hi Thomas

Get Code'; + expect(stripHtmlTags(html)).toContain("Get Code [https://www.netflix.com/verify?a=1&b=2]"); + }); + + it("drops non-http hrefs but keeps the label", () => { + expect(stripHtmlTags('contact us')).toBe("contact us"); + }); +}); + +describe("extractMailBodies single-part html", () => { + it("treats a bare text/html fragment (no wrapper) as the html body", () => { + const rawEmail = [ + "Subject: code", + "Content-Type: text/html; charset=utf-8", + "Content-Transfer-Encoding: base64", + "", + Buffer.from('
Your code: Get Code
').toString("base64"), + "", + ].join("\r\n"); + + const bodies = extractMailBodies(rawEmail); + expect(bodies.htmlBody).toContain("https://x.com/v?t=1"); + expect(bodies.textBody).toContain("Get Code [https://x.com/v?t=1]"); + }); +});