From 0460771d5db37a1162901bcfc1df6374d2533141 Mon Sep 17 00:00:00 2001 From: garrettmills Date: Sat, 4 Jan 2025 04:43:39 -0500 Subject: [PATCH] Implement first-draft of HTML message/reply parsing + HTML sanitization --- bun.lockb | Bin 15431 -> 30971 bytes package.json | 8 +++-- src/mail/read.ts | 2 ++ src/mail/replies.ts | 2 +- src/mail/sanitize.ts | 72 +++++++++++++++++++++++++++++++++++++++++ src/threads/refresh.ts | 7 ++-- src/types.ts | 2 ++ 7 files changed, 88 insertions(+), 5 deletions(-) create mode 100644 src/mail/sanitize.ts diff --git a/bun.lockb b/bun.lockb index 960e3dbb02259c574bf153fab280d8fb96c6a9e9..a0d36874afc9a060d2c693d036822ae50d23d30b 100755 GIT binary patch literal 30971 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 w2hrO3@zh=wNhMXMX-B> { // There are 2 possibilities for where mail might end up. @@ -112,6 +113,7 @@ export class MailboxIterable extends Iterable { subject: message.envelope.subject, mailbox: this.mailbox, modseq: message.modseq, + html: htmlReplyPipeline.apply(source), recipients, content, thread, diff --git a/src/mail/replies.ts b/src/mail/replies.ts index c40b648..ecc2a44 100644 --- a/src/mail/replies.ts +++ b/src/mail/replies.ts @@ -50,7 +50,7 @@ export class Email { this.foundVisible = false; let fragment: Fragment | null = null; - const lines = modifiedText.split("\n"); + const lines = modifiedText.split(/\n|>\/?\s*[rR][bB]"); diff --git a/src/mail/sanitize.ts b/src/mail/sanitize.ts new file mode 100644 index 0000000..982aae1 --- /dev/null +++ b/src/mail/sanitize.ts @@ -0,0 +1,72 @@ +import {Pipeline} from "../bones/Pipe.ts"; +import {extract} from "letterparser"; +import {ReplyParser} from "./replies.ts"; +import DOMPurify from 'isomorphic-dompurify' +import { JSDOM } from 'jsdom' + + +export const allowedTags = [ + 'li', 'ol', 'ul', 'ul', 'b', 'br', 'code', 'em', + 'i', 'small', 'strong', 'sub', 'sup', 'u', +] + +export const lineBreakingTags = ['p', 'div'] + +export const sanitizeHtml = (html: string): string => + DOMPurify((new JSDOM('')).window).sanitize(html, { ALLOWED_TAGS: allowedTags }) + +/** + * Transforms an HTML email content string by sanitizing the HTML + * and stripping out quotes and signatures as best as possible. + * This is imperfect and currently results in too many line breaks + * in some cases, but it works okay enough. + */ +export const htmlReplyPipeline = Pipeline.id() + // extract the HTML from the email source + .tap(source => extract(source).html || '') + + // here it gets weird -- the reply parser expects to work in terms of newlines and
tags + // however a lot of email uses

. Do a pass to insert an artificial
at the beginning + // of each

tag + .tap(html => { + const window = (new JSDOM('')).window + const dp = DOMPurify(window) + + dp.addHook('uponSanitizeElement', node => { + if ( node.nodeType !== window.Node.ELEMENT_NODE ) { + // Skip text/document nodes, e.g. + return + } + + if ( node.textContent === '' && node.nodeName.toLowerCase() !== 'br' ) { + // Drop empty nodes as long as they're not self-closing + node.parentNode?.removeChild(node) + return + } + + if ( lineBreakingTags.includes(node.nodeName.toLowerCase()) ) { + // If a wrapping tag would cause a line break, explicitly add in that break + const br = window.document.createElement('br') + const child = node.firstChild + child ? node.insertBefore(br, child) : node.appendChild(br) + return + } + }) + + const result = dp.sanitize(html, { + ALLOWED_TAGS: [...allowedTags, ...lineBreakingTags], + }) + + dp.removeHook('uponSanitizeElement') + + return result + }) + + // then, do a second pass which filters out the

tags, leaving the
in their place + .tap(html => sanitizeHtml(html)) + + // run that through the reply parser to strip out quotes and signatures + .tap(html => ReplyParser.parseReply(html)) + + // finally, clean it up and add the
's back in since the reply parser replaced them with \n + .tap(reply => reply.trim().replace(/\n+/g, '
')) diff --git a/src/threads/refresh.ts b/src/threads/refresh.ts index 3d7613e..86b1d97 100644 --- a/src/threads/refresh.ts +++ b/src/threads/refresh.ts @@ -4,6 +4,8 @@ import type {Message, ThreadData} from "../types.ts"; import {AsyncCollection} from "../bones/collection/AsyncCollection.ts"; import {sha256} from "../bones/crypto.ts"; import {config} from "../config.ts"; +import { marked } from "marked"; +import {sanitizeHtml} from "../mail/sanitize.ts"; export async function refreshThreadsEntirely(): Promise { await withClient(async client => { @@ -51,12 +53,13 @@ export async function refreshThreadsEntirely(): Promise { threadData.comments.push({ user: { name: message.from.name || '(anonymous)', - mailId: sha256(message.from.address!), - domainId: sha256(message.from.address!.split('@').reverse()[0]), + mailId: sha256(message.from.address!.toLowerCase()), + domainId: sha256(message.from.address!.toLowerCase().split('@').reverse()[0]), }, date: message.date, subject: message.subject, text: message.content, + rendered: message.html || sanitizeHtml(await marked(message.content)), }) } diff --git a/src/types.ts b/src/types.ts index a5058d9..2b1e1f6 100644 --- a/src/types.ts +++ b/src/types.ts @@ -38,6 +38,7 @@ export type Message = { }, subject: string, content: string, + html?: string, mailbox: string, modseq: BigInt, thread?: string, @@ -54,6 +55,7 @@ export type ThreadComment = { date: Date, subject: string, text: string, + rendered: string, } export type ThreadData = {