Fix garbled ticket text from HTML emails (Outlook VML/entities)

extractBody() used a naive tag-stripping regex as a fallback, which
strips <style> tags but leaves their CSS content behind as visible
text, and never decodes HTML entities. Outlook's VML style block
(v\:* {behavior:url(#default#VML);} ...) and &nbsp;/&laquo; entities
were leaking straight into ticket bodies as a result.

Now uses html-to-text (already a transitive dep via mailparser,
promoted to direct) to convert the HTML part whenever present, which
correctly drops <head>/<style> content and decodes entities. Also
stopped trusting parsed.text unconditionally — some senders' plain-text
alternative isn't actually clean, so HTML is now the primary source
whenever available.
This commit is contained in:
ogrechko committed 2026-08-10 13:53:19 +00:00
1 parent 8844323cad
commit 87cf045715
4 files changed
+20 -2

No files matched your search

+1
View File
@@ -12,6 +12,7 @@
"better-sqlite3": "^12.11.1", "better-sqlite3": "^12.11.1",
"drizzle-orm": "^0.45.2", "drizzle-orm": "^0.45.2",
"framer-motion": "^12.4.7", "framer-motion": "^12.4.7",
"html-to-text": "^10.0.0",
"imapflow": "^1.5.0", "imapflow": "^1.5.0",
"ldapjs": "^3.0.7", "ldapjs": "^3.0.7",
"lucide-react": "^0.545.0", "lucide-react": "^0.545.0",
+1
View File
@@ -18,6 +18,7 @@
"better-sqlite3": "^12.11.1", "better-sqlite3": "^12.11.1",
"drizzle-orm": "^0.45.2", "drizzle-orm": "^0.45.2",
"framer-motion": "^12.4.7", "framer-motion": "^12.4.7",
"html-to-text": "^10.0.0",
"imapflow": "^1.5.0", "imapflow": "^1.5.0",
"ldapjs": "^3.0.7", "ldapjs": "^3.0.7",
"lucide-react": "^0.545.0", "lucide-react": "^0.545.0",
+8 -2
View File
@@ -1,5 +1,6 @@
import { ImapFlow } from "imapflow"; import { ImapFlow } from "imapflow";
import { simpleParser, type ParsedMail } from "mailparser"; import { simpleParser, type ParsedMail } from "mailparser";
import { htmlToText } from "html-to-text";
import { getMailboxSettings, saveMailboxSettings, disableMailboxConfig, type MailboxSettings } from "@/lib/mail/config"; import { getMailboxSettings, saveMailboxSettings, disableMailboxConfig, type MailboxSettings } from "@/lib/mail/config";
import { recordEmailInboundMessage } from "@/lib/tickets/service"; import { recordEmailInboundMessage } from "@/lib/tickets/service";
import { saveAttachment } from "@/lib/attachments/storage"; import { saveAttachment } from "@/lib/attachments/storage";
@@ -28,11 +29,16 @@ function createClient(settings: MailboxSettings): ImapFlow {
}); });
} }
// Prefer converting the HTML part ourselves rather than trusting parsed.text
// blindly — some senders (Outlook in particular) produce a text/plain
// alternative that isn't actually clean, while html-to-text reliably drops
// <head>/<style> content (e.g. Outlook's VML behavior: url(...) rules) and
// decodes entities, which a naive tag-stripping regex cannot do.
function extractBody(parsed: ParsedMail): string { function extractBody(parsed: ParsedMail): string {
if (parsed.text) return parsed.text;
if (typeof parsed.html === "string") { if (typeof parsed.html === "string") {
return parsed.html.replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim(); return htmlToText(parsed.html, { baseElements: { selectors: ["body"] }, wordwrap: false }).trim();
} }
if (parsed.text) return parsed.text;
return "(пустое письмо)"; return "(пустое письмо)";
} }
+10
View File
@@ -0,0 +1,10 @@
declare module "html-to-text" {
interface HtmlToTextOptions {
wordwrap?: number | false;
baseElements?: { selectors?: string[] };
[key: string]: unknown;
}
export function htmlToText(html: string, options?: HtmlToTextOptions): string;
export function convert(html: string, options?: HtmlToTextOptions): string;
}