mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 08:13:50 +00:00
121 lines
6.2 KiB
TypeScript
121 lines
6.2 KiB
TypeScript
/**
|
|
* The organisation's changes to outgoing mail, as send finalisers
|
|
* (lib/outgoing/send-pipeline.ts): they run for the composer and quick
|
|
* reply, on sends only, never on drafts. Registered when this module is
|
|
* first imported (the composer and lib/outgoing/quick-reply.ts import it);
|
|
* each reads the effective policy at send time.
|
|
*
|
|
* - org-signature: a locked (or hidden) organisation signature the user
|
|
* deleted or changed in the editor is appended again once, at the end of
|
|
* the message (below the quote, where a reply's signature goes by
|
|
* default). Each part is checked and fixed on its own.
|
|
* - org-disclaimer (stage `last`): the admin's disclaimer below
|
|
* everything, quote and plugin footers included, before a plugin signs or
|
|
* encrypts: `<hr>` and the HTML in the HTML part, a blank line and the
|
|
* text in the text part (text from the HTML when the admin wrote none).
|
|
* A part that already ends with it keeps the one it has.
|
|
*/
|
|
|
|
import type { Identity } from '@/lib/jmap/types';
|
|
import { registerOutgoingFinalizer, type OutgoingBodies, type OutgoingContext } from '@/lib/outgoing/send-pipeline';
|
|
import { isSignatureEnforced, type FilledSignature } from '@/lib/org-signature';
|
|
import { currentOrgSignatureSetup } from '@/lib/org/signature-state';
|
|
import {
|
|
DISCLAIMER_MARKER,
|
|
htmlCarriesDisclaimer,
|
|
htmlCarriesSignature,
|
|
textCarriesDisclaimer,
|
|
textCarriesSignature,
|
|
} from '@/lib/org/body-match';
|
|
import { appendHtmlSignature, appendPlainTextSignature, getPlainTextSignature, signatureAsHtml } from '@/lib/signature-utils';
|
|
import { stripCrossAccountIdentityPrefix } from '@/hooks/use-pro-multi-account-identities';
|
|
import { useIdentityStore } from '@/stores/identity-store';
|
|
import { useSettingsStore } from '@/stores/settings-store';
|
|
|
|
export const ORG_SIGNATURE_FINALIZER = 'org-signature';
|
|
export const ORG_DISCLAIMER_FINALIZER = 'org-disclaimer';
|
|
const SIGNATURE_ORDER = 10;
|
|
/** The disclaimer goes last, after anything else a finaliser adds. */
|
|
const DISCLAIMER_ORDER = 1000;
|
|
|
|
/**
|
|
* The identity the message goes out from, for the placeholders, and the
|
|
* signed-in account it belongs to, for the policy: as the sender says, or
|
|
* looked up among the active account's identities. An identity of another
|
|
* account ("<account>::<id>") is never looked up there.
|
|
*/
|
|
function senderOf(context: OutgoingContext): { identity: Pick<Identity, 'name' | 'email'> | null; accountId: string | null } {
|
|
if (context.sender) return { identity: context.sender, accountId: context.sender.accountId ?? null };
|
|
const { localAccountId } = context.identityId
|
|
? stripCrossAccountIdentityPrefix(context.identityId)
|
|
: { localAccountId: null };
|
|
const fallback = context.fromEmail ? { name: '', email: context.fromEmail } : null;
|
|
if (localAccountId) return { identity: fallback, accountId: localAccountId };
|
|
const identities = useIdentityStore.getState().identities;
|
|
const byId = context.identityId ? identities.find((identity) => identity.id === context.identityId) : undefined;
|
|
const email = context.fromEmail?.toLowerCase();
|
|
const byEmail = email ? identities.find((identity) => identity.email.toLowerCase() === email) : undefined;
|
|
return { identity: byId ?? byEmail ?? fallback, accountId: null };
|
|
}
|
|
|
|
function setupFor(context: OutgoingContext) {
|
|
const { identity, accountId } = senderOf(context);
|
|
return currentOrgSignatureSetup(identity, accountId);
|
|
}
|
|
|
|
const asSignature = (filled: FilledSignature) => ({ htmlSignature: filled.html, textSignature: filled.text });
|
|
|
|
/**
|
|
* Puts a locked organisation signature back when the user deleted or
|
|
* changed it. The composer and quick reply insert it, so this does nothing
|
|
* unless it is missing, and then adds it once to the part that lacks it.
|
|
*/
|
|
export function orgSignatureFinalizer(bodies: OutgoingBodies, context: OutgoingContext): OutgoingBodies {
|
|
const setup = setupFor(context);
|
|
if (!setup.signature || !isSignatureEnforced(setup.restriction)) return bodies;
|
|
const signature = setup.signature;
|
|
const separator = { separator: useSettingsStore.getState().signatureSeparatorEnabled };
|
|
const htmlMissing = bodies.html !== undefined && !htmlCarriesSignature(bodies.html, signature, context.forwardedSeparator);
|
|
// The text part of an HTML message is made from the HTML: what the HTML
|
|
// lacks, it lacks too.
|
|
const textMissing = htmlMissing || !textCarriesSignature(bodies.text, signature, context.forwardedSeparator);
|
|
if (!htmlMissing && !textMissing) return bodies;
|
|
return {
|
|
html: htmlMissing ? appendHtmlSignature(bodies.html!, asSignature(signature), separator) : bodies.html,
|
|
text: textMissing ? appendPlainTextSignature(bodies.text, asSignature(signature), separator) : bodies.text,
|
|
};
|
|
}
|
|
|
|
/** The disclaimer as it goes into the HTML part. */
|
|
export function disclaimerHtml(disclaimer: FilledSignature): string {
|
|
return `<hr><div ${DISCLAIMER_MARKER}="">${signatureAsHtml(asSignature(disclaimer))}</div>`;
|
|
}
|
|
|
|
/**
|
|
* Adds the disclaimer below everything, quote included. A part that already
|
|
* ends with it (a message sent again after Undo, where the first send's copy
|
|
* came back into the draft) keeps the one it has; a quoted copy doesn't count.
|
|
*/
|
|
export function orgDisclaimerFinalizer(bodies: OutgoingBodies, context: OutgoingContext): OutgoingBodies {
|
|
const disclaimer = setupFor(context).disclaimer;
|
|
if (!disclaimer) return bodies;
|
|
const text = getPlainTextSignature(asSignature(disclaimer));
|
|
if (!text) return bodies;
|
|
const htmlMissing = bodies.html !== undefined && !htmlCarriesDisclaimer(bodies.html, disclaimer);
|
|
const textMissing = htmlMissing || !textCarriesDisclaimer(bodies.text, disclaimer);
|
|
if (!htmlMissing && !textMissing) return bodies;
|
|
return {
|
|
html: htmlMissing ? `${bodies.html}${disclaimerHtml(disclaimer)}` : bodies.html,
|
|
text: textMissing ? `${bodies.text.replace(/\s+$/, '')}\n\n${text}` : bodies.text,
|
|
};
|
|
}
|
|
|
|
/** Registers both finalisers; registering again replaces them. */
|
|
export function registerOrgOutgoing(): void {
|
|
registerOutgoingFinalizer(ORG_SIGNATURE_FINALIZER, SIGNATURE_ORDER, orgSignatureFinalizer);
|
|
// After the plugins' own footers, before a plugin signs or encrypts.
|
|
registerOutgoingFinalizer(ORG_DISCLAIMER_FINALIZER, DISCLAIMER_ORDER, orgDisclaimerFinalizer, 'last');
|
|
}
|
|
|
|
registerOrgOutgoing();
|