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
@@ -22,7 +22,14 @@ import type {
|
||||
AttachmentDTO,
|
||||
} from "@/lib/tickets/types";
|
||||
|
||||
type AttachmentInput = { filename: string; mimeType: string; sizeBytes: number; storageKey: string };
|
||||
type AttachmentInput = {
|
||||
id?: string;
|
||||
filename: string;
|
||||
mimeType: string;
|
||||
sizeBytes: number;
|
||||
storageKey: string;
|
||||
contentId?: string | null;
|
||||
};
|
||||
|
||||
function toTicketDTO(ticket: typeof tickets.$inferSelect, customerName: string, tags: TagDTO[]): TicketDTO {
|
||||
return {
|
||||
@@ -48,6 +55,7 @@ function toMessageDTO(message: typeof messages.$inferSelect, attachments: Attach
|
||||
authorType: message.authorType,
|
||||
authorName: message.authorName,
|
||||
body: message.body,
|
||||
bodyHtml: message.bodyHtml,
|
||||
direction: message.direction,
|
||||
visibility: message.visibility,
|
||||
attachments,
|
||||
@@ -115,6 +123,7 @@ async function appendMessage(params: {
|
||||
authorId: string | null;
|
||||
authorName: string;
|
||||
body: string;
|
||||
bodyHtml?: string | null;
|
||||
direction: "in" | "out";
|
||||
visibility?: "public" | "internal";
|
||||
emailMessageId?: string;
|
||||
@@ -128,6 +137,7 @@ async function appendMessage(params: {
|
||||
authorId: params.authorId,
|
||||
authorName: params.authorName,
|
||||
body: params.body,
|
||||
bodyHtml: params.bodyHtml ?? null,
|
||||
direction: params.direction,
|
||||
visibility: params.visibility ?? "public",
|
||||
emailMessageId: params.emailMessageId,
|
||||
@@ -288,6 +298,7 @@ export async function recordEmailInboundMessage(params: {
|
||||
fromName: string;
|
||||
subject: string;
|
||||
body: string;
|
||||
bodyHtml?: string | null;
|
||||
messageId: string | null;
|
||||
referencedMessageIds: string[];
|
||||
attachments?: AttachmentInput[];
|
||||
@@ -321,6 +332,7 @@ export async function recordEmailInboundMessage(params: {
|
||||
authorId: customer.id,
|
||||
authorName: params.fromName,
|
||||
body: params.body,
|
||||
bodyHtml: params.bodyHtml,
|
||||
direction: "in",
|
||||
emailMessageId: params.messageId ?? undefined,
|
||||
attachments: params.attachments,
|
||||
|
||||
Reference in new issue
Block a user