mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-10-31 16:13:15 +00:00
Message ids repeat across accounts, so a hook payload naming only an emailId could not tell a plugin which mailbox the message lies in when it is a shared or group account's. onBeforeEmailStarToggle and onEmailStarToggle now carry accountId when the starred message is not in the user's own account, as the message-row-actions click context does. Every star (the row's button, the reader's star, the key, the menu) goes through toggleStar, one message at a time, so one accountId names it; the account is resolved before the plugins are asked. An own message's payload is unchanged. onAfterEmailSend now carries accountId, the JMAP account the sent message lies in (a mail sent from a shared or group identity lies in that account), so api.email.get finds it. sendEmail returns that account as emailAccountId, the S/MIME send too. The hook also carries composerId, the id the sending composer's slots and actions gave plugins, so a plugin can act on what the user chose in that composer.
102 lines
3.4 KiB
TypeScript
102 lines
3.4 KiB
TypeScript
// onAfterEmailSend: told once the server accepted a message.
|
|
//
|
|
// - A plain send is reported straight away, scheduledFor null.
|
|
// - A send the user scheduled for later is reported straight away too, with
|
|
// scheduledFor: the server accepted it, even though it goes out later.
|
|
// - A send held back for undo ("Sending in 10 s") is reported only when the
|
|
// hold runs out or the user picks "Send now". Undo drops it: that message
|
|
// never leaves.
|
|
// A failed send never gets here; the callers only report a resolved send.
|
|
//
|
|
// `accountId` names the account the sent message lies in (ids repeat across
|
|
// accounts; a group identity's send lies in the group's), `composerId` the
|
|
// composer it came from, as its slots and actions know it, so a plugin can
|
|
// act on what the user chose there.
|
|
|
|
import type { SendEmailResult } from './jmap/types';
|
|
import { emailHooks } from './plugin-hooks';
|
|
|
|
export interface SentMessage {
|
|
to: string[];
|
|
cc: string[];
|
|
bcc: string[];
|
|
subject: string;
|
|
/** Set when the user scheduled the message; absent for an undo-send hold. */
|
|
delayedUntil?: string;
|
|
/** The id plugins know the sending composer by (composer-registry.ts). */
|
|
composerId?: string;
|
|
}
|
|
|
|
interface AfterSendPayload {
|
|
emailId: string | null;
|
|
/** The JMAP account `emailId` lies in. */
|
|
accountId: string | null;
|
|
submissionId: string | null;
|
|
to: string[];
|
|
cc: string[];
|
|
bcc: string[];
|
|
subject: string;
|
|
scheduledFor: string | null;
|
|
composerId: string | null;
|
|
}
|
|
|
|
const held = new Map<string, { timer: ReturnType<typeof setTimeout>; payload: AfterSendPayload }>();
|
|
|
|
function emit(payload: AfterSendPayload): void {
|
|
if (emailHooks.onAfterEmailSend.size === 0) return;
|
|
void emailHooks.onAfterEmailSend.emit(payload);
|
|
}
|
|
|
|
/** Report a send the server accepted (see the rules above). */
|
|
export function announceSent(result: SendEmailResult, message: SentMessage): void {
|
|
const payload: AfterSendPayload = {
|
|
emailId: result.emailId ?? null,
|
|
accountId: result.emailAccountId ?? null,
|
|
submissionId: result.emailSubmissionId ?? null,
|
|
to: [...message.to],
|
|
cc: [...message.cc],
|
|
bcc: [...message.bcc],
|
|
subject: message.subject,
|
|
scheduledFor: null,
|
|
composerId: message.composerId ?? null,
|
|
};
|
|
if (message.delayedUntil) {
|
|
emit({ ...payload, scheduledFor: result.sendAt ?? message.delayedUntil });
|
|
return;
|
|
}
|
|
const submissionId = result.emailSubmissionId;
|
|
if (result.scheduled && submissionId && result.sendAt) {
|
|
const wait = Math.max(0, Date.parse(result.sendAt) - Date.now());
|
|
const timer = setTimeout(() => {
|
|
held.delete(submissionId);
|
|
emit(payload);
|
|
}, Number.isFinite(wait) ? wait : 0);
|
|
held.set(submissionId, { timer, payload });
|
|
return;
|
|
}
|
|
emit(payload);
|
|
}
|
|
|
|
/** The user undid a held send: it is not reported. */
|
|
export function cancelAnnouncedSend(submissionId: string): void {
|
|
const entry = held.get(submissionId);
|
|
if (!entry) return;
|
|
clearTimeout(entry.timer);
|
|
held.delete(submissionId);
|
|
}
|
|
|
|
/** The user chose "Send now" for a held send: report it now. */
|
|
export function releaseAnnouncedSend(submissionId: string): void {
|
|
const entry = held.get(submissionId);
|
|
if (!entry) return;
|
|
clearTimeout(entry.timer);
|
|
held.delete(submissionId);
|
|
emit(entry.payload);
|
|
}
|
|
|
|
/** Drop every held send (tests). */
|
|
export function resetAnnouncedSends(): void {
|
|
for (const { timer } of held.values()) clearTimeout(timer);
|
|
held.clear();
|
|
}
|