fix: preserve HTML signature when sending a quick reply
The quick-reply box built its body with appendPlainTextSignature, which runs the identity's HTML signature through htmlToPlainText, and sent a text-only message (htmlBody was undefined). A formatted signature (e.g. <strong>…) was therefore flattened to plain text in the sent mail, even though it previewed correctly in the identity editor. The full composer already builds an HTML signature block; quick reply did not. Add an appendHtmlSignature helper (mirrors the composer's send-time block) and, when the sending identity has an HTML signature, send a matching HTML body from handleQuickReply so the markup is preserved. Text-only identities keep the plain-text-only behavior. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
committed by
Linus Rath
co-authored by
Claude Opus 4.8
parent
0879030dc8
commit
196e51e91b
@@ -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(/>/g, '>')
|
||||
.replace(/\n/g, '<br>');
|
||||
const finalHtmlBody = sendingIdentity?.htmlSignature?.trim()
|
||||
? appendHtmlSignature(`<div>${escapedBody}</div>`, 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,
|
||||
|
||||
@@ -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('<div>Hello</div>', { htmlSignature: '<strong>Alice</strong>' }))
|
||||
.toBe('<div>Hello</div><br><br>-- <br><strong>Alice</strong>');
|
||||
});
|
||||
|
||||
it('escapes and appends a text signature when no html signature exists', () => {
|
||||
expect(appendHtmlSignature('<div>Hello</div>', { textSignature: 'Alice\nEng' }))
|
||||
.toBe('<div>Hello</div><br><br>-- <br>Alice<br>Eng');
|
||||
});
|
||||
|
||||
it('omits the separator marker when disabled', () => {
|
||||
expect(appendHtmlSignature('<div>Hi</div>', { htmlSignature: '<strong>A</strong>' }, { separator: false }))
|
||||
.toBe('<div>Hi</div><br><br><strong>A</strong>');
|
||||
});
|
||||
|
||||
it('leaves the body untouched when no signature exists', () => {
|
||||
expect(appendHtmlSignature('<div>Hi</div>', {})).toBe('<div>Hi</div>');
|
||||
});
|
||||
});
|
||||
|
||||
describe('hasMeaningfulHtmlBody', () => {
|
||||
it('prefers html bodies that preserve signature formatting', () => {
|
||||
expect(hasMeaningfulHtmlBody('<div>Hello</div><br><p>Alice</p>')).toBe(true);
|
||||
|
||||
@@ -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 ? '<br><br>' : '<br><br>-- <br>';
|
||||
|
||||
if (signature?.htmlSignature?.trim()) {
|
||||
return `${htmlBody}${sep}${sanitizeSignatureHtml(signature.htmlSignature)}`;
|
||||
}
|
||||
|
||||
if (signature?.textSignature?.trim()) {
|
||||
const escaped = signature.textSignature
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/\n/g, '<br>');
|
||||
return `${htmlBody}${sep}${escaped}`;
|
||||
}
|
||||
|
||||
return htmlBody;
|
||||
}
|
||||
|
||||
export function hasMeaningfulHtmlBody(html: string): boolean {
|
||||
if (!html.trim()) return false;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user