Preserve email formatting in ticket display; paste-to-attach screenshots in replies
Inbound HTML emails were being flattened through html-to-text for display, which lost all formatting and — for images embedded as data: URIs — leaked the raw base64 as visible "[data:image/png;base64,...]" link text (an Outlook/webmail screenshot-paste artifact). Customer messages now also store a sanitized HTML rendering (messages.body_html) that preserves the sender's fonts/colors/layout and shows inline images (data: URIs render natively; cid: references are rewritten to the matching attachment's serving URL via a new attachments.content_id column). Sanitization is a tag/style allowlist (lib/mail/sanitize-html.ts, covered by sanitize-html.test.ts) — no script/event handlers/javascript: hrefs, no layout-breaking CSS. The plain-text fallback (used for channels other than email) also drops image/data-URI link text via html-to-text selectors, for the same underlying bug on that path. Also: pasting a screenshot (Ctrl+V) into the agent reply box now attaches it directly, reusing the existing attachment-upload/email-delivery path — no more "save to disk, then click attach" round trip. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GteWhnWKTmnXcsd5jx6H7u
This commit is contained in:
1 parent
019f4a1b53
commit
955fdeee41
14 files changed
+1332
-8
No files matched your search
@@ -0,0 +1,58 @@
|
||||
import sanitizeHtml from "sanitize-html";
|
||||
|
||||
// Formatting-only tags — no <script>/<style>/<iframe>/<object>/<form>/svg/etc.
|
||||
// Unknown tags (Outlook's <o:p>, VML <v:*>) are unwrapped by sanitize-html
|
||||
// (tag dropped, text content kept), which is what we want for stray markup.
|
||||
const ALLOWED_TAGS = [
|
||||
"a", "p", "br", "div", "span", "b", "strong", "i", "em", "u", "s", "strike", "small",
|
||||
"ul", "ol", "li", "blockquote", "pre", "code", "hr",
|
||||
"h1", "h2", "h3", "h4", "h5", "h6",
|
||||
"table", "thead", "tbody", "tr", "td", "th",
|
||||
"img", "sub", "sup", "font",
|
||||
];
|
||||
|
||||
// Only visual/typographic properties — nothing that can reposition or
|
||||
// resize content out of the message bubble (position, width/height on
|
||||
// non-img elements, etc. are simply not in this allowlist and get dropped).
|
||||
const ALLOWED_STYLES = {
|
||||
"*": {
|
||||
color: [/^#[0-9a-fA-F]{3,8}$/, /^rgb\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*\)$/, /^[a-zA-Z]+$/],
|
||||
"background-color": [/^#[0-9a-fA-F]{3,8}$/, /^rgb\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*\)$/, /^[a-zA-Z]+$/],
|
||||
"font-family": [/^[\w\s,'"-]+$/],
|
||||
"font-size": [/^\d+(\.\d+)?(px|pt|em|%)$/],
|
||||
"font-weight": [/^(normal|bold|\d{3})$/],
|
||||
"font-style": [/^(normal|italic)$/],
|
||||
"text-align": [/^(left|right|center|justify)$/],
|
||||
"text-decoration": [/^[\w\s-]+$/],
|
||||
"line-height": [/^[\d.]+(px|em|%)?$/],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* Renders an inbound email's HTML part safely for display in the ticket
|
||||
* thread — preserves fonts/colors/layout/inline images (the whole point:
|
||||
* "как человек на почту отправил, ровно также оно и пришло"), while
|
||||
* stripping anything that could execute script or break the page layout.
|
||||
* `cid:` image references must already be rewritten to real attachment URLs
|
||||
* before calling this (see imap.ts) — `cid`/`javascript` are not in the
|
||||
* allowed URL schemes, so any that slip through as-is are dropped.
|
||||
*/
|
||||
export function sanitizeEmailHtml(html: string): string {
|
||||
return sanitizeHtml(html, {
|
||||
allowedTags: ALLOWED_TAGS,
|
||||
allowedAttributes: {
|
||||
a: ["href", "title", "target", "rel"],
|
||||
img: ["src", "alt", "width", "height", "style"],
|
||||
td: ["colspan", "rowspan", "style"],
|
||||
th: ["colspan", "rowspan", "style"],
|
||||
table: ["style", "border", "cellpadding", "cellspacing"],
|
||||
"*": ["style"],
|
||||
},
|
||||
allowedSchemes: ["http", "https", "mailto"],
|
||||
allowedSchemesByTag: { img: ["http", "https", "data"] },
|
||||
allowedStyles: ALLOWED_STYLES,
|
||||
transformTags: {
|
||||
a: sanitizeHtml.simpleTransform("a", { target: "_blank", rel: "noopener noreferrer" }),
|
||||
},
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user