fix: keep anchor hrefs when stripping HTML and accept bare text/html fragments
Forwarded mails (e.g. Yandex webmail) arrive as a single text/html part with no <html>/<body> wrapper and no text/plain alternative. Two bugs made their codes unextractable: - stripHtmlTags dropped href attributes, so button-only mails (Netflix 'Get Code') reached the LLM with no URL at all → codeExist=0. Anchors now render as 'label [url]' with & decoded. - extractMailBodies only recognised single-part HTML wrapped in <html>/<body>, so htmlBody was null and the Rendered preview was empty. A bare fragment with Content-Type text/html now counts as the html body. Verified against the real forwarded Netflix mail from production raw_mails: content now carries the travel/verify link and classifies 5/5 as login_code.
This commit is contained in:
1 parent
5508c14663
commit
b6a1ad7d9d
2 files changed
+55
-7
No files matched your search
+26
-7
@@ -100,12 +100,25 @@ export function decodeMimeHeader(value: string | null): string | null {
|
||||
}
|
||||
|
||||
export function stripHtmlTags(text: string): string {
|
||||
return text
|
||||
.replace(/<style[\s\S]*?<\/style>/gi, "")
|
||||
.replace(/<script[\s\S]*?<\/script>/gi, "")
|
||||
.replace(/<[^>]+>/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
return (
|
||||
text
|
||||
.replace(/<style[\s\S]*?<\/style>/gi, "")
|
||||
.replace(/<script[\s\S]*?<\/script>/gi, "")
|
||||
// 保留链接: <a href="url">text</a> → "text [url]"。
|
||||
// 验证码邮件常常只有一个按钮链接 (如 Netflix 的 Get Code), 剥掉 href 会让
|
||||
// LLM 无码可提取。仅 html-only 邮件走到这里, 多部分邮件优先用 text/plain。
|
||||
.replace(
|
||||
/<a\b[^>]*?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(/<html[\s\S]*<\/html>/i) ?? decodedBody.match(/<body[\s\S]*<\/body>/i);
|
||||
const htmlBody = htmlMatch ? htmlMatch[0].trim() : null;
|
||||
// Content-Type 明确是 text/html 时整个正文就是 HTML, 即使没有 <html>/<body>
|
||||
// 包裹 (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;
|
||||
|
||||
@@ -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 = '<p>Hi Thomas</p><a class="h5" href="https://www.netflix.com/verify?a=1&b=2" style="x">Get Code</a>';
|
||||
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('<a href="mailto:x@y.com">contact us</a>')).toBe("contact us");
|
||||
});
|
||||
});
|
||||
|
||||
describe("extractMailBodies single-part html", () => {
|
||||
it("treats a bare text/html fragment (no <html> wrapper) as the html body", () => {
|
||||
const rawEmail = [
|
||||
"Subject: code",
|
||||
"Content-Type: text/html; charset=utf-8",
|
||||
"Content-Transfer-Encoding: base64",
|
||||
"",
|
||||
Buffer.from('<div>Your code: <a href="https://x.com/v?t=1">Get Code</a></div>').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]");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user