diff --git a/app/(main)/[locale]/page.tsx b/app/(main)/[locale]/page.tsx index ae57e8ec..dfaa9a20 100644 --- a/app/(main)/[locale]/page.tsx +++ b/app/(main)/[locale]/page.tsx @@ -55,7 +55,7 @@ import { useProMultiAccountMailboxes } from "@/hooks/use-pro-multi-account-mailb import { Input } from "@/components/ui/input"; import { FilePreviewModal } from "@/components/files/file-preview-modal"; import { isFilePreviewable } from "@/lib/file-preview"; -import { appendPlainTextSignature } from "@/lib/signature-utils"; +import { appendHtmlSignature, appendPlainTextSignature } from "@/lib/signature-utils"; import { computeReplyThreadingHeaders } from "@/lib/email-threading"; import { EML_IMPORT_ACCEPT, expandImportableEmails } from "@/lib/eml-import"; import { resolveReplyFrom } from "@/lib/reply-identity"; @@ -2090,9 +2090,21 @@ export default function Home() { // Append signature from the sending identity (fall back to primary // when the reply-from lives on the same identity but a different alias). - const finalBody = appendPlainTextSignature(body, sendingIdentity, { - separator: useSettingsStore.getState().signatureSeparatorEnabled, - }); + const separator = useSettingsStore.getState().signatureSeparatorEnabled; + const finalBody = appendPlainTextSignature(body, sendingIdentity, { separator }); + + // When the identity has an HTML signature, send a matching HTML body so the + // signature keeps its formatting; appendPlainTextSignature would otherwise + // flatten it to plain text. Text-only identities keep the plain-text-only + // behavior (htmlBody stays undefined). + const escapedBody = body + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/\n/g, '
'); + const finalHtmlBody = sendingIdentity?.htmlSignature?.trim() + ? appendHtmlSignature(`
${escapedBody}
`, sendingIdentity, { separator }) + : undefined; const originalEmailId = selectedEmail.id; const sendDelaySeconds = useSettingsStore.getState().sendDelaySeconds; @@ -2124,7 +2136,7 @@ export default function Home() { headerFromEmail, undefined, headerFromName, - undefined, + finalHtmlBody, undefined, threading?.inReplyTo, threading?.references, diff --git a/lib/__tests__/signature-utils.test.ts b/lib/__tests__/signature-utils.test.ts index d2a95d61..27cb2d52 100644 --- a/lib/__tests__/signature-utils.test.ts +++ b/lib/__tests__/signature-utils.test.ts @@ -1,6 +1,7 @@ import { describe, expect, it } from 'vitest'; import { + appendHtmlSignature, appendPlainTextSignature, getPlainTextSignature, hasMeaningfulHtmlBody, @@ -27,6 +28,27 @@ describe('signature-utils', () => { }); }); + describe('appendHtmlSignature', () => { + it('appends a sanitized html signature, preserving formatting', () => { + expect(appendHtmlSignature('
Hello
', { htmlSignature: 'Alice' })) + .toBe('
Hello


--
Alice'); + }); + + it('escapes and appends a text signature when no html signature exists', () => { + expect(appendHtmlSignature('
Hello
', { textSignature: 'Alice\nEng' })) + .toBe('
Hello


--
Alice
Eng'); + }); + + it('omits the separator marker when disabled', () => { + expect(appendHtmlSignature('
Hi
', { htmlSignature: 'A' }, { separator: false })) + .toBe('
Hi


A'); + }); + + it('leaves the body untouched when no signature exists', () => { + expect(appendHtmlSignature('
Hi
', {})).toBe('
Hi
'); + }); + }); + describe('hasMeaningfulHtmlBody', () => { it('prefers html bodies that preserve signature formatting', () => { expect(hasMeaningfulHtmlBody('
Hello

Alice

')).toBe(true); diff --git a/lib/signature-utils.ts b/lib/signature-utils.ts index f348124e..678fa461 100644 --- a/lib/signature-utils.ts +++ b/lib/signature-utils.ts @@ -124,6 +124,35 @@ export function appendPlainTextSignature( return `${body}${sep}${plainTextSignature}`; } +/** + * Append a signature to an HTML body, preserving rich formatting. Used by the + * quick-reply path so an HTML signature keeps its markup instead of being + * flattened to plain text. Mirrors the composer's send-time signature block + * (`buildSignatureHtml` in email-composer.tsx). + */ +export function appendHtmlSignature( + htmlBody: string, + signature?: SignatureSource | null, + options: { separator?: boolean } = {}, +): string { + const sep = options.separator === false ? '

' : '

--
'; + + if (signature?.htmlSignature?.trim()) { + return `${htmlBody}${sep}${sanitizeSignatureHtml(signature.htmlSignature)}`; + } + + if (signature?.textSignature?.trim()) { + const escaped = signature.textSignature + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/\n/g, '
'); + return `${htmlBody}${sep}${escaped}`; + } + + return htmlBody; +} + export function hasMeaningfulHtmlBody(html: string): boolean { if (!html.trim()) return false;