Files
webmail/lib/store/outbox/items.ts
T
Linus Rath 60eef9c1ef chore: merge the reviewed mail branch, pop-outs and desktop core into the offline drafts branch
The flush keeps one `outbox.dropOrphans` caller, the mail branch's daily
one with its 7 days, and drafts' own query says what's needed, so
`addHeldFileSource` and its test are gone. `unsentCount` counts local
drafts, and a draft a waiting send names counts once. The scope check in
`recordBlob` and the permanent-status rule moved to
`lib/store/mail/attachments.ts`, with tests.
2026-11-03 10:40:52 +01:00

108 lines
3.4 KiB
TypeScript

/**
* What a finished send looks like when it waits. An outbox entry is a finished
* message: everything a send does up to the transport ran when the user clicked
* Send (the organisation's checks, the label, signatures, plugin send checks,
* S/MIME or PGP with the user present). What is kept is the arguments of the
* final send, or the finished raw message of a plugin's send. Sending it later
* can't skip any of that.
*/
/** The arguments of useEmailStore.sendEmail after the client (positional, as the client takes them). */
export interface EmailSendItem {
kind: 'email';
args: unknown[];
}
export interface RawSendItem {
kind: 'raw';
/** The finished message, kept as an upload until it is sent. */
blobId: string;
identityId: string;
delayedUntil?: string;
envelopeRecipients?: string[];
/** submitRaw: no copy in Sent. */
submitOnly?: boolean;
}
export type SendItem = EmailSendItem | RawSendItem;
export interface OutboxSummary {
subject: string;
to: string[];
}
/**
* - waiting: goes out by itself when the server answers;
* - sending: on its way (a tab took it);
* - failed: the server refused it; the error says why;
* - uncertain: the connection broke after it may have reached the server.
* Looked up when the server is back; stays this way only when the lookup
* can't tell, and then it's the user's to send again or delete;
* - held: the organisation's rules got stricter while it waited.
*/
export type OutboxStatus = 'waiting' | 'sending' | 'failed' | 'uncertain' | 'held';
/** Why an entry failed: one of ours (a code the UI translates) or the server's own words. */
export interface OutboxError {
code?: 'file_missing' | 'no_sent_mailbox' | 'no_drafts_mailbox';
text?: string;
}
/** What the organisation required when the message was finished. */
export interface PolicyFingerprint {
labelRequired: boolean;
hasLabel: boolean;
guards: string[];
}
export interface StoredOutboxEntry {
id: number;
seq: number;
createdAt: number;
summary: OutboxSummary;
status: OutboxStatus;
sendingSince?: number;
/** When the first try that may have reached the server began. */
firstAttemptAt?: number;
lastAttemptAt?: number;
error?: OutboxError;
item: SendItem;
fingerprint?: PolicyFingerprint;
messageId?: string;
copyId?: string;
}
/** Stand-in blob ids of files kept on this device start with this. */
export const LOCAL_BLOB_PREFIX = 'bulwark-local-';
export function isLocalBlobId(blobId: unknown): blobId is string {
return typeof blobId === 'string' && blobId.startsWith(LOCAL_BLOB_PREFIX);
}
/** The stand-in blob ids an outbox entry needs uploaded first. */
export function heldBlobIds(item: SendItem): string[] {
if (item.kind === 'raw') return isLocalBlobId(item.blobId) ? [item.blobId] : [];
const attachments = item.args[10];
if (!Array.isArray(attachments)) return [];
return attachments.map((a) => (a as { blobId?: unknown }).blobId).filter(isLocalBlobId);
}
export type ConflictReason = 'changed' | 'gone' | 'refused';
export interface ConflictSummary {
subject: string;
/** `draft`: a draft saved here met a version changed elsewhere. Both are kept. */
op: 'update' | 'destroy' | 'draft';
reason: ConflictReason;
/** The server's word when it refused. */
detail?: string;
}
export interface StoredConflict {
id: number;
jmap: string;
recordId: string;
createdAt: number;
summary: ConflictSummary;
}