mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-02 16:15:39 +00:00
Settings > Backup & reset gets "Keep mail on this device", off until the user turns it on in this browser. While it is on: - the JMAP client hands the server's answers to an encrypted cache in IndexedDB (AES-GCM, non-extractable key) and answers from it when the network is gone: the folder list, the last 30 days of the Inbox and of the folders the user opened, bodies and attachments, at most 250 MB, oldest out first; a background sync keeps it current - read, flag, tags and moves made offline are queued and sent in order when the server answers again; deleting for good and emptying a folder are refused offline - the composer, quick reply and the S/MIME and PGP plugins' sends that complete the composer's go to an outbox once every step up to the transport ran (checks, label, signature, plugin send checks); only the finished send waits, with files attached offline - the service worker keeps this build's app shell, so the app also starts offline from the kept session and the last policy - a full sign-out, the idle sign-out and signing one account out delete it, a sign-out the user starts asks first when mail is unsent, and the login page names what an automatic sign-out deleted; a policy fetch that forbids it (devices.noOffline) wipes that account Not in Lite.
104 lines
3.5 KiB
TypeScript
104 lines
3.5 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 {
|
|
// Waiting in the offline outbox (#523): nothing left yet.
|
|
if (result.queued) return;
|
|
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();
|
|
}
|