From 5f3d2d3e4a16a740e77516d7f13ebdaf66288f7c Mon Sep 17 00:00:00 2001
From: Linus Rath <139418639+rathlinus@users.noreply.github.com>
Date: Mon, 11 May 2026 17:05:59 +0200
Subject: [PATCH] feat: signature above quoted text option #266
---
components/email/email-composer.tsx | 71 +++++++++++++++++++---
components/settings/composing-settings.tsx | 12 ++++
locales/en/common.json | 6 ++
stores/settings-store.ts | 4 ++
4 files changed, 83 insertions(+), 10 deletions(-)
diff --git a/components/email/email-composer.tsx b/components/email/email-composer.tsx
index 076cfd1d..75012ce8 100644
--- a/components/email/email-composer.tsx
+++ b/components/email/email-composer.tsx
@@ -135,6 +135,24 @@ export function EmailComposer({
const autoSelectReplyIdentity = useSettingsStore((state) => state.autoSelectReplyIdentity);
const attachmentReminderEnabled = useSettingsStore((state) => state.attachmentReminderEnabled);
const attachmentReminderKeywords = useSettingsStore((state) => state.attachmentReminderKeywords);
+ const signaturePosition = useSettingsStore((state) => state.signaturePosition);
+ const identities = useIdentityStore((s) => s.identities);
+ const primaryIdentity = identities[0] ?? null;
+
+ // The signature identity used when embedding the signature into the initial
+ // body for "above quote" mode. Mirrors the signatureIdentity derivation
+ // below, but uses initialData (or primary) since selectedIdentityId state
+ // does not exist yet at this point.
+ const initialCurrentIdentityForSig = initialData?.selectedIdentityId
+ ? identities.find((i) => i.id === initialData.selectedIdentityId) || primaryIdentity
+ : primaryIdentity;
+ const initialSignatureIdentity = (initialCurrentIdentityForSig?.htmlSignature || initialCurrentIdentityForSig?.textSignature)
+ ? initialCurrentIdentityForSig
+ : primaryIdentity;
+ const shouldEmbedSignatureAboveQuote =
+ (mode === 'reply' || mode === 'replyAll' || mode === 'forward') &&
+ signaturePosition === 'above_quote' &&
+ !!(initialSignatureIdentity?.htmlSignature || initialSignatureIdentity?.textSignature);
// Initialize with reply/forward data if provided
const getInitialTo = () => {
@@ -184,10 +202,18 @@ export function EmailComposer({
const originalText = replyTo.body || (replyTo.htmlBody ? htmlToPlainText(replyTo.htmlBody) : '');
const quotedText = originalText.split('\n').map(line => `> ${line}`).join('\n');
+ // When "above quote" is configured, splice signature between the user's
+ // 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 signatureBlock = shouldEmbedSignatureAboveQuote
+ ? `\n\n-- \n${getPlainTextSignature(initialSignatureIdentity)}`
+ : '';
+
if (mode === 'forward') {
- return `${prefix}\n\n---------- Forwarded message ----------\nFrom: ${fromStr}\nDate: ${date}\nSubject: ${replyTo.subject || ''}\n\n${originalText}`;
+ return `${prefix}${signatureBlock}\n\n---------- Forwarded message ----------\nFrom: ${fromStr}\nDate: ${date}\nSubject: ${replyTo.subject || ''}\n\n${originalText}`;
} else if (mode === 'reply' || mode === 'replyAll') {
- return `${prefix}\n\nOn ${date}, ${fromStr} wrote:\n${quotedText}`;
+ return `${prefix}${signatureBlock}\n\nOn ${date}, ${fromStr} wrote:\n${quotedText}`;
}
return prefix;
}
@@ -199,20 +225,36 @@ export function EmailComposer({
const from = replyTo.from?.[0];
const fromStr = from ? `${from.name || from.email}` : tCommon('unknown');
+ // When "above quote" is configured, splice signature between the user's
+ // 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 buildEmbeddedSignatureHtml = (): string => {
+ if (!shouldEmbedSignatureAboveQuote) return '';
+ if (initialSignatureIdentity?.htmlSignature) {
+ return `
--
${sanitizeEmailHtml(initialSignatureIdentity.htmlSignature)}`;
+ }
+ if (initialSignatureIdentity?.textSignature) {
+ return `
--
${initialSignatureIdentity.textSignature.replace(/&/g, '&').replace(//g, '>').replace(/\n/g, '
')}`;
+ }
+ return '';
+ };
+ const signatureBlock = buildEmbeddedSignatureHtml();
+
// Build quoted content as HTML
if (replyTo.htmlBody && (mode === 'reply' || mode === 'replyAll' || mode === 'forward')) {
const quoteHeader = mode === 'forward'
? `---------- Forwarded message ----------
From: ${fromStr}
Date: ${date}
Subject: ${replyTo.subject || ''}
`
: `On ${date}, ${fromStr} wrote:
`;
- return `${prefix}
${replyTo.htmlBody}`; + return `${prefix}${signatureBlock}
${replyTo.htmlBody}`; } if (replyTo.body) { const escapedOriginal = replyTo.body.replace(/&/g, '&').replace(//g, '>').replace(/\n/g, '
${escapedOriginal}`; + return `${prefix}${signatureBlock}
${escapedOriginal}`; } } return prefix; @@ -284,8 +326,6 @@ export function EmailComposer({ }); const { client } = useAuthStore(); - const identities = useIdentityStore((s) => s.identities); - const primaryIdentity = identities[0] ?? null; const currentIdentity = selectedIdentityId ? identities.find((identity) => identity.id === selectedIdentityId) || primaryIdentity : primaryIdentity; @@ -988,8 +1028,16 @@ export function EmailComposer({ const envelopeMailFrom = overrideActive ? identityFromEmail : undefined; // Body is already HTML from the rich text editor (or plain text in plain text mode). + // When "above quote" mode is configured for replies/forwards, the signature + // was embedded into the body during init (see getInitialBody) so the + // trailing append must be skipped to avoid duplicating it. + const signatureAlreadyInBody = + (mode === 'reply' || mode === 'replyAll' || mode === 'forward') && + signaturePosition === 'above_quote'; + // Build HTML signature block (used only in rich text mode) const buildSignatureHtml = (): string => { + if (signatureAlreadyInBody) return ''; if (signatureIdentity?.htmlSignature) { return `