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

+8 -2
View File
@@ -1,5 +1,6 @@
import { ImapFlow } from "imapflow";
import { simpleParser, type ParsedMail } from "mailparser";
import { htmlToText } from "html-to-text";
import { getMailboxSettings, saveMailboxSettings, disableMailboxConfig, type MailboxSettings } from "@/lib/mail/config";
import { recordEmailInboundMessage } from "@/lib/tickets/service";
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 {
if (parsed.text) return parsed.text;
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 "(пустое письмо)";
}
+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;
}