From a4cc91c4fbf63ab16f7afbb71fe425b572818192 Mon Sep 17 00:00:00 2001
From: Linus Rath <139418639+rathlinus@users.noreply.github.com>
Date: Sun, 11 Oct 2026 19:41:48 +0200
Subject: [PATCH] fix: show one signature separator and keep its trailing space
---
.../email/__tests__/compose-format.test.ts | 6 +++++
components/email/compose-format.ts | 3 ++-
components/email/email-composer.tsx | 11 ++++----
lib/__tests__/html-to-text.test.ts | 6 +++++
lib/__tests__/signature-utils.test.ts | 19 +++++++++++++
lib/html-to-text.ts | 6 ++++-
lib/signature-delimiter.ts | 27 +++++++++++++++++++
lib/signature-utils.ts | 11 +++++---
8 files changed, 79 insertions(+), 10 deletions(-)
create mode 100644 lib/signature-delimiter.ts
diff --git a/components/email/__tests__/compose-format.test.ts b/components/email/__tests__/compose-format.test.ts
index 5837505f8..b53a558c4 100644
--- a/components/email/__tests__/compose-format.test.ts
+++ b/components/email/__tests__/compose-format.test.ts
@@ -83,4 +83,10 @@ describe('compose-format (#1022): HTML <-> plain text switch', () => {
const back = plainComposeBodyToHtml(text, identity, { separator: true });
expect(back).toBe(start);
});
+
+ it('adds no separator paragraph in front of a signature that has its own delimiter', () => {
+ const html = buildEmbeddedSignatureHtml({ textSignature: '-- \nUser A' }, { embed: true, separator: true });
+ expect(html).not.toContain(`${SIGNATURE_RANGE_MARKER}="separator"`);
+ expect(count(html, '-- ')).toBe(1);
+ });
});
diff --git a/components/email/compose-format.ts b/components/email/compose-format.ts
index 4d4e918a6..0cc0a9f98 100644
--- a/components/email/compose-format.ts
+++ b/components/email/compose-format.ts
@@ -6,6 +6,7 @@ import {
plainTextBodyHasSignature,
plainTextBodyWithoutSignature,
} from "@/lib/signature-utils";
+import { wantsSignatureSeparator } from "@/lib/signature-delimiter";
import { buildSignatureBlock, SIGNATURE_RANGE_MARKER } from "@/components/email/signature-block";
/**
@@ -36,7 +37,7 @@ export function buildEmbeddedSignatureHtml(
options: { embed: boolean; separator: boolean }
): string {
if (!options.embed) return '';
- const startMarker = options.separator
+ const startMarker = wantsSignatureSeparator(options.separator, identity)
? `
--
`
: ``;
const endMarker = ``;
diff --git a/components/email/email-composer.tsx b/components/email/email-composer.tsx
index 1d6a3d68c..509b6e5bc 100644
--- a/components/email/email-composer.tsx
+++ b/components/email/email-composer.tsx
@@ -47,6 +47,7 @@ import { TemplatePicker } from "@/components/templates/template-picker";
import { TemplateForm } from "@/components/templates/template-form";
import type { EmailTemplate } from "@/lib/template-types";
import { appendPlainTextSignature, getPlainTextSignature, plainTextBodyHasSignature, plainTextBodyWithoutSignature } from "@/lib/signature-utils";
+import { wantsSignatureSeparator } from "@/lib/signature-delimiter";
import { findComposeIdentityId, findDraftIdentityId, findReplyIdentityId, resolveReplyFrom } from "@/lib/reply-identity";
import { buildReplyRecipients, isSelfSent } from "@/lib/reply-recipients";
import { computeReplyThreadingHeaders, type ReplyThreadingHeaders } from "@/lib/email-threading";
@@ -438,7 +439,7 @@ export function EmailComposer({
// editable. Fixes #329 (A,B).
if (mode === 'compose') {
if (!shouldEmbedSignatureInNewMail) return prefix;
- const sep = signatureSeparatorEnabled ? '\n\n-- \n' : '\n\n';
+ const sep = wantsSignatureSeparator(signatureSeparatorEnabled, initialSignatureIdentity) ? '\n\n-- \n' : '\n\n';
return `${prefix}${sep}${getPlainTextSignature(initialSignatureIdentity)}`;
}
if (!replyTo?.body && !replyTo?.htmlBody) return prefix;
@@ -460,7 +461,7 @@ export function EmailComposer({
// drafting area and the quoted content so it reads naturally as a
// closing for the reply body. Send-time append is skipped - see
// shouldEmbedSignatureAboveQuote.
- const plainSep = signatureSeparatorEnabled ? '\n\n-- \n' : '\n\n';
+ const plainSep = wantsSignatureSeparator(signatureSeparatorEnabled, initialSignatureIdentity) ? '\n\n-- \n' : '\n\n';
const signatureBlock = shouldEmbedSignatureAboveQuote
? `${plainSep}${getPlainTextSignature(initialSignatureIdentity)}`
: '';
@@ -2481,7 +2482,7 @@ export function EmailComposer({
// Build HTML signature block (used only in rich text mode)
const buildSignatureHtml = (): string => {
if (signatureAlreadyInBody) return '';
- const sep = signatureSeparatorEnabled ? `
--
` : `
`;
+ const sep = wantsSignatureSeparator(signatureSeparatorEnabled, signatureIdentity) ? `
--
` : `
`;
if (signatureIdentity?.htmlSignature) {
return `${sep}${sanitizeSignatureHtml(signatureIdentity.htmlSignature)}`;
}
@@ -3342,13 +3343,13 @@ export function EmailComposer({
: plainTextMode ? (
getPlainTextSignature(signatureIdentity) ? (
- {signatureSeparatorEnabled ? '-- \n' : ''}{getPlainTextSignature(signatureIdentity)}
+ {wantsSignatureSeparator(signatureSeparatorEnabled, signatureIdentity) ? '-- \n' : ''}{getPlainTextSignature(signatureIdentity)}
) : null
) : composerSignatureHtml ? (
--
' : ''}${composerSignatureHtml}` }}
+ dangerouslySetInnerHTML={{ __html: `${wantsSignatureSeparator(signatureSeparatorEnabled, signatureIdentity) ? '--
' : ''}${composerSignatureHtml}` }}
/>
) : null}
diff --git a/lib/__tests__/html-to-text.test.ts b/lib/__tests__/html-to-text.test.ts
index 4f3bcee27..8473fcdaf 100644
--- a/lib/__tests__/html-to-text.test.ts
+++ b/lib/__tests__/html-to-text.test.ts
@@ -54,4 +54,10 @@ describe('htmlToPlainText', () => {
it('handles nested lists as separate lines', () => {
expect(htmlToPlainText('')).toBe('One\nTwo');
});
+
+ it('keeps the trailing space of a "-- " signature delimiter', () => {
+ // Without it the text part's delimiter was "--", which clients do not
+ // recognise as the start of a signature.
+ expect(htmlToPlainText('Hi
--
User A
')).toBe('Hi\n-- \nUser A');
+ });
});
diff --git a/lib/__tests__/signature-utils.test.ts b/lib/__tests__/signature-utils.test.ts
index 795e57419..d8e717456 100644
--- a/lib/__tests__/signature-utils.test.ts
+++ b/lib/__tests__/signature-utils.test.ts
@@ -116,4 +116,23 @@ describe('signature-utils', () => {
expect(hasMeaningfulHtmlBody('Hello world
')).toBe(false);
});
});
+
+ describe('signature delimiter', () => {
+ // Identities often carry their own "-- " line; the composer's separator on
+ // top of it showed two delimiter lines.
+ const own = { textSignature: '-- \nUser A\nstw-test' };
+
+ it('keeps the space of the "-- " delimiter', () => {
+ expect(getPlainTextSignature(own)).toBe('-- \nUser A\nstw-test');
+ });
+
+ it('adds no second delimiter when the signature has its own', () => {
+ expect(appendPlainTextSignature('Hello', own, { separator: true })).toBe('Hello\n\n-- \nUser A\nstw-test');
+ expect(appendHtmlSignature('Hello
', own, { separator: true })).toBe('Hello
--
User A
stw-test');
+ });
+
+ it('still adds the delimiter for a signature without one', () => {
+ expect(appendPlainTextSignature('Hello', { textSignature: 'User A' }, { separator: true })).toBe('Hello\n\n-- \nUser A');
+ });
+ });
});
\ No newline at end of file
diff --git a/lib/html-to-text.ts b/lib/html-to-text.ts
index 941bbbcb0..64dfb9a3a 100644
--- a/lib/html-to-text.ts
+++ b/lib/html-to-text.ts
@@ -61,7 +61,11 @@ function normalizeLineBreaks(value: string): string {
return value
.replace(/\r\n?/g, '\n')
.replace(new RegExp(String.fromCharCode(160), 'g'), ' ')
- .replace(/[ \t]+\n/g, '\n')
+ // Trims trailing spaces, but keeps the one that makes "-- " the RFC 3676
+ // signature delimiter, so the text part still marks the signature.
+ .split('\n')
+ .map((line) => (/^--[ \t]*$/.test(line) ? '-- ' : line.replace(/[ \t]+$/, '')))
+ .join('\n')
.replace(/\n{3,}/g, '\n\n')
.trim();
}
diff --git a/lib/signature-delimiter.ts b/lib/signature-delimiter.ts
new file mode 100644
index 000000000..76a513852
--- /dev/null
+++ b/lib/signature-delimiter.ts
@@ -0,0 +1,27 @@
+import { sanitizeSignatureHtml } from '@/lib/email-sanitization';
+import { htmlToPlainText } from '@/lib/html-to-text';
+
+type SignatureSource = {
+ textSignature?: string;
+ htmlSignature?: string;
+};
+
+/**
+ * Whether the signature starts with its own "-- " delimiter line. Many do
+ * (imported from other clients, or set on the server), and the composer's own
+ * separator on top of it showed two delimiter lines.
+ */
+export function signatureHasOwnDelimiter(signature?: SignatureSource | null): boolean {
+ const text = signature?.textSignature?.trim()
+ ? signature.textSignature
+ : signature?.htmlSignature?.trim()
+ ? htmlToPlainText(sanitizeSignatureHtml(signature.htmlSignature))
+ : '';
+ const firstLine = text.replace(/\r\n?/g, '\n').replace(/^\s*\n/, '').split('\n')[0];
+ return /^--[ \t\u00a0]*$/.test(firstLine);
+}
+
+/** Whether to add the "-- " separator line in front of this signature. */
+export function wantsSignatureSeparator(enabled: boolean | undefined, signature?: SignatureSource | null): boolean {
+ return enabled !== false && !signatureHasOwnDelimiter(signature);
+}
diff --git a/lib/signature-utils.ts b/lib/signature-utils.ts
index 180ae7854..d130c0d11 100644
--- a/lib/signature-utils.ts
+++ b/lib/signature-utils.ts
@@ -1,5 +1,6 @@
import { parseHtmlSafely, sanitizeSignatureHtml } from '@/lib/email-sanitization';
import { htmlToPlainText } from '@/lib/html-to-text';
+import { wantsSignatureSeparator } from '@/lib/signature-delimiter';
type SignatureSource = {
textSignature?: string;
@@ -10,7 +11,11 @@ function normalizeSignatureLineBreaks(value: string): string {
return value
.replace(/\r\n?/g, '\n')
.replace(/\u00a0/g, ' ')
- .replace(/[ \t]+\n/g, '\n')
+ // Trims trailing spaces, but keeps the one that makes "-- " the RFC 3676
+ // signature delimiter.
+ .split('\n')
+ .map((line) => (/^--[ \t]*$/.test(line) ? '-- ' : line.replace(/[ \t]+$/, '')))
+ .join('\n')
.replace(/\n{3,}/g, '\n\n')
.trim();
}
@@ -37,7 +42,7 @@ export function appendPlainTextSignature(
return body;
}
- const sep = options.separator === false ? '\n\n' : '\n\n-- \n';
+ const sep = wantsSignatureSeparator(options.separator, signature) ? '\n\n-- \n' : '\n\n';
return `${body}${sep}${plainTextSignature}`;
}
@@ -95,7 +100,7 @@ export function appendHtmlSignature(
signature?: SignatureSource | null,
options: { separator?: boolean } = {},
): string {
- const sep = options.separator === false ? '
' : '
--
';
+ const sep = wantsSignatureSeparator(options.separator, signature) ? '
--
' : '
';
if (signature?.htmlSignature?.trim()) {
return `${htmlBody}${sep}${sanitizeSignatureHtml(signature.htmlSignature)}`;