mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 08:13:50 +00:00
161 lines
4.6 KiB
TypeScript
161 lines
4.6 KiB
TypeScript
import { parseHtmlSafely, sanitizeSignatureHtml } from '@/lib/email-sanitization';
|
|
import { htmlToPlainText } from '@/lib/html-to-text';
|
|
import { wantsSignatureSeparator } from '@/lib/signature-delimiter';
|
|
|
|
type SignatureSource = {
|
|
textSignature?: string;
|
|
htmlSignature?: string;
|
|
};
|
|
|
|
function normalizeSignatureLineBreaks(value: string): string {
|
|
return value
|
|
.replace(/\r\n?/g, '\n')
|
|
.replace(/\u00a0/g, ' ')
|
|
// 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();
|
|
}
|
|
|
|
export function getPlainTextSignature(signature?: SignatureSource | null): string {
|
|
if (signature?.textSignature?.trim()) {
|
|
return normalizeSignatureLineBreaks(signature.textSignature);
|
|
}
|
|
|
|
if (signature?.htmlSignature?.trim()) {
|
|
return htmlToPlainText(sanitizeSignatureHtml(signature.htmlSignature));
|
|
}
|
|
|
|
return '';
|
|
}
|
|
|
|
export function appendPlainTextSignature(
|
|
body: string,
|
|
signature?: SignatureSource | null,
|
|
options: { separator?: boolean } = {},
|
|
): string {
|
|
const plainTextSignature = getPlainTextSignature(signature);
|
|
if (!plainTextSignature) {
|
|
return body;
|
|
}
|
|
|
|
const sep = wantsSignatureSeparator(options.separator, signature) ? '\n\n-- \n' : '\n\n';
|
|
return `${body}${sep}${plainTextSignature}`;
|
|
}
|
|
|
|
/**
|
|
* Whether a plain-text body already ends with the identity's signature.
|
|
*
|
|
* The HTML path detects an embedded signature by its markers
|
|
* (`containsEmbeddedSignature`); plain text has none, so the body is matched
|
|
* against the signature itself. Both sides go through the same line-break
|
|
* normalization so a body that round-tripped through a draft (CRLF, trailing
|
|
* spaces) still matches.
|
|
*/
|
|
export function plainTextBodyHasSignature(
|
|
body: string,
|
|
signature?: SignatureSource | null,
|
|
): boolean {
|
|
const plainTextSignature = getPlainTextSignature(signature);
|
|
if (!plainTextSignature) {
|
|
return false;
|
|
}
|
|
return normalizeSignatureLineBreaks(body).endsWith(plainTextSignature);
|
|
}
|
|
|
|
/**
|
|
* The plain-text body with a trailing signature - and the `-- ` separator
|
|
* line in front of it - removed. Returns the body unchanged when it does not
|
|
* end with the identity's signature. Used to tell an untouched compose body
|
|
* (signature only) from one the user has already written into (#540).
|
|
*/
|
|
export function plainTextBodyWithoutSignature(
|
|
body: string,
|
|
signature?: SignatureSource | null,
|
|
): string {
|
|
const plainTextSignature = getPlainTextSignature(signature);
|
|
if (!plainTextSignature) {
|
|
return body;
|
|
}
|
|
const normalized = normalizeSignatureLineBreaks(body);
|
|
if (!normalized.endsWith(plainTextSignature)) {
|
|
return body;
|
|
}
|
|
return normalized
|
|
.slice(0, normalized.length - plainTextSignature.length)
|
|
.replace(/\n*(?:-- ?)?\n*$/, '');
|
|
}
|
|
|
|
/**
|
|
* 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 html = signatureAsHtml(signature);
|
|
if (!html) return htmlBody;
|
|
const sep = wantsSignatureSeparator(options.separator, signature) ? '<br><br>-- <br>' : '<br><br>';
|
|
return `${htmlBody}${sep}${html}`;
|
|
}
|
|
|
|
/**
|
|
* A signature as it goes into an HTML body: its HTML, sanitised, or its
|
|
* text, escaped, with line breaks. Empty without either.
|
|
*/
|
|
export function signatureAsHtml(signature?: SignatureSource | null): string {
|
|
if (signature?.htmlSignature?.trim()) return sanitizeSignatureHtml(signature.htmlSignature);
|
|
if (signature?.textSignature?.trim()) {
|
|
return signature.textSignature
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/\n/g, '<br>');
|
|
}
|
|
return '';
|
|
}
|
|
|
|
export function hasMeaningfulHtmlBody(html: string): boolean {
|
|
if (!html.trim()) return false;
|
|
|
|
const document = parseHtmlSafely(html);
|
|
const richSelector = [
|
|
'table',
|
|
'img',
|
|
'style',
|
|
'b',
|
|
'strong',
|
|
'i',
|
|
'em',
|
|
'u',
|
|
'font',
|
|
'a[href]',
|
|
'div[style]',
|
|
'span[style]',
|
|
'p[style]',
|
|
'h1',
|
|
'h2',
|
|
'h3',
|
|
'h4',
|
|
'h5',
|
|
'h6',
|
|
'ul',
|
|
'ol',
|
|
'blockquote',
|
|
'br',
|
|
].join(', ');
|
|
|
|
if (document.querySelector(richSelector)) {
|
|
return true;
|
|
}
|
|
|
|
const blockElements = document.body.querySelectorAll('p, div, blockquote, li');
|
|
return blockElements.length > 1;
|
|
} |