feat: pluggable reply/forward quote header #295
This commit is contained in:
@@ -222,6 +222,12 @@ export const emailHooks = {
|
||||
onBeforeReply: new HookBus(),
|
||||
onBeforeReplyAll: new HookBus(),
|
||||
onBeforeForward: new HookBus(),
|
||||
// Transform hook - lets plugins replace the quote header block ("On X,
|
||||
// Y wrote:" / "---------- Forwarded message ----------") used when opening
|
||||
// a reply or forward. Initial value: QuoteHeader (host default), second
|
||||
// argument: QuoteHeaderContext. Handlers return a QuoteHeader (or
|
||||
// undefined to pass through). Fires once per composer open.
|
||||
onBuildQuoteHeader: new HookBus(),
|
||||
// Intercept hook fired before a file is added to the composer as an
|
||||
// attachment. Handler receives AttachmentInfo (size/type/name only - the
|
||||
// raw file is not exposed). Return false to refuse the upload.
|
||||
|
||||
@@ -645,6 +645,52 @@ export interface ReplyContext {
|
||||
mode: 'reply' | 'reply-all' | 'forward';
|
||||
}
|
||||
|
||||
/**
|
||||
* Second argument to onBuildQuoteHeader transform handlers. Describes the
|
||||
* original message and how the host plans to render the quote header so
|
||||
* plugins can produce a replacement block (e.g. an Outlook-style
|
||||
* From/Sent/To/Cc/Subject section).
|
||||
*/
|
||||
export interface QuoteHeaderContext {
|
||||
mode: 'reply' | 'replyAll' | 'forward';
|
||||
/** Recipients of the new outgoing message (already resolved by the host). */
|
||||
newTo: string[];
|
||||
newCc: string[];
|
||||
/** Original message metadata. */
|
||||
from: { name?: string; email: string } | null;
|
||||
to: { name?: string; email: string }[];
|
||||
cc: { name?: string; email: string }[];
|
||||
subject: string;
|
||||
/** Pre-formatted date string the host already produced (locale-aware). */
|
||||
date: string;
|
||||
/** Raw ISO datetime, in case the plugin wants to reformat. */
|
||||
receivedAt?: string;
|
||||
/** Active UI locale (BCP-47), useful for Intl.DateTimeFormat in plugins. */
|
||||
locale: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initial value for the onBuildQuoteHeader transform hook. Plugins return a
|
||||
* replacement; returning undefined falls through to the next handler or the
|
||||
* default. The composer splices `html` into HTML drafts and `text` into
|
||||
* plain-text drafts.
|
||||
*
|
||||
* For HTML, returning a header that includes its own surrounding wrapper
|
||||
* (`<div>...</div>`) is fine; the composer does not add extra wrappers.
|
||||
* For text, the host appends the quoted body after the header.
|
||||
*/
|
||||
export interface QuoteHeader {
|
||||
html: string;
|
||||
text: string;
|
||||
/**
|
||||
* When false, the composer skips its default blockquote wrapping around the
|
||||
* quoted body (HTML mode only). Use this for the Outlook style where the
|
||||
* quoted message is intended to follow the header without indentation.
|
||||
* Defaults to true (preserve the existing blockquote wrapping).
|
||||
*/
|
||||
wrapInBlockquote?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Describes an attachment crossing an attachment hook (upload, download, preview).
|
||||
*/
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
// Builds the default reply/forward quote header and runs it through the
|
||||
// emailHooks.onBuildQuoteHeader transform so plugins can replace it (e.g.
|
||||
// with an Outlook-style From/Sent/To/Cc/Subject block).
|
||||
//
|
||||
// This module is the single source of truth for the default header strings -
|
||||
// the composer keeps the same defaults inline as a fallback, but production
|
||||
// flow goes through here.
|
||||
|
||||
import { formatDateTime } from "@/lib/utils";
|
||||
import { emailHooks } from "@/lib/plugin-hooks";
|
||||
import type { QuoteHeader, QuoteHeaderContext } from "@/lib/plugin-types";
|
||||
|
||||
interface BuildArgs {
|
||||
mode: "reply" | "replyAll" | "forward";
|
||||
email: {
|
||||
from?: { email?: string; name?: string }[];
|
||||
to?: { email?: string; name?: string }[];
|
||||
cc?: { email?: string; name?: string }[];
|
||||
subject?: string;
|
||||
receivedAt?: string;
|
||||
};
|
||||
newTo: string[];
|
||||
newCc: string[];
|
||||
locale: string;
|
||||
timeFormat: "12h" | "24h";
|
||||
unknownLabel: string;
|
||||
}
|
||||
|
||||
function defaultHeader(args: BuildArgs): QuoteHeader {
|
||||
const { mode, email, timeFormat, unknownLabel } = args;
|
||||
const date = email.receivedAt
|
||||
? formatDateTime(email.receivedAt, timeFormat, {
|
||||
weekday: "short",
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
})
|
||||
: "";
|
||||
const from = email.from?.[0];
|
||||
const fromStr = from ? `${from.name || from.email}` : unknownLabel;
|
||||
const subject = email.subject || "";
|
||||
|
||||
if (mode === "forward") {
|
||||
const text = `---------- Forwarded message ----------\nFrom: ${fromStr}\nDate: ${date}\nSubject: ${subject}\n`;
|
||||
const html = `<div>---------- Forwarded message ----------<br>From: ${fromStr}<br>Date: ${date}<br>Subject: ${subject}<br><br></div>`;
|
||||
return { html, text, wrapInBlockquote: false };
|
||||
}
|
||||
|
||||
const text = `On ${date}, ${fromStr} wrote:\n`;
|
||||
const html = `<div>On ${date}, ${fromStr} wrote:<br></div>`;
|
||||
return { html, text, wrapInBlockquote: true };
|
||||
}
|
||||
|
||||
export async function buildQuoteHeader(args: BuildArgs): Promise<QuoteHeader> {
|
||||
const def = defaultHeader(args);
|
||||
const ctx: QuoteHeaderContext = {
|
||||
mode: args.mode,
|
||||
newTo: args.newTo,
|
||||
newCc: args.newCc,
|
||||
from: args.email.from?.[0]?.email
|
||||
? { name: args.email.from[0].name, email: args.email.from[0].email }
|
||||
: null,
|
||||
to: (args.email.to ?? [])
|
||||
.filter((r): r is { email: string; name?: string } => !!r.email)
|
||||
.map((r) => ({ name: r.name, email: r.email })),
|
||||
cc: (args.email.cc ?? [])
|
||||
.filter((r): r is { email: string; name?: string } => !!r.email)
|
||||
.map((r) => ({ name: r.name, email: r.email })),
|
||||
subject: args.email.subject ?? "",
|
||||
date: args.email.receivedAt
|
||||
? formatDateTime(args.email.receivedAt, args.timeFormat, {
|
||||
weekday: "short",
|
||||
year: "numeric",
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
})
|
||||
: "",
|
||||
receivedAt: args.email.receivedAt,
|
||||
locale: args.locale,
|
||||
};
|
||||
return emailHooks.onBuildQuoteHeader.transform<QuoteHeader>(def, ctx);
|
||||
}
|
||||
Reference in New Issue
Block a user