mirror of
https://github.com/bulwarkmail/webmail.git
synced 2026-11-02 16:15:39 +00:00
The review of offline mode found ways to send a message twice, to lose unsent mail without a word, and to send mail without its signing, encryption or check plugin. This fixes them and the smaller findings: - each outbox message is taken for sending in one database transaction under a Web Lock per account; only a failure before the submission request (SendNotStartedError from the client) puts it back, any other one marks it "may have gone out"; read-backs after the submission no longer fail a send; a send of a closed tab turns uncertain after a minute - every sign-out the user didn't confirm names the deleted messages on the sign-in page, or in a toast when another account stays signed in - nothing is queued while an enabled plugin isn't running (after a start without the network none can); the composer says why - the outbox waits for a fresh policy and holds messages the rules have since made stricter (a required label, a required send check) - another tab switching offline mode off sticks: only switching it on may create the database - only attachments of kept messages are kept, filed with their message, deleted with it and under the cap between syncs; file names and types are sealed; folders that went away leave the scope, and a folder that couldn't be asked keeps its mail without holding the others back - a group scope that forbids offline mode waits until the account's groups are known; one server down no longer holds another's mail - an offline start only when the server is really unreachable, and the real sign-in follows by itself once it answers - the service worker files pages and chunks under the build that served them, and the shell list of another build isn't kept under this one - a raw send counts as the composer's only from the plugin running its takeover, for that identity - errors and refusals are in all languages; the banner tells "offline" from "server can't be reached"; outbox buttons name their message; changes and messages go in the order they were made
102 lines
3.3 KiB
TypeScript
102 lines
3.3 KiB
TypeScript
/**
|
|
* What offline mode (#523) shows: which accounts keep mail on this device,
|
|
* whether the browser is offline, what waits in the outbox, how much the
|
|
* cache uses. The switch itself ("Keep mail on this device") is per account
|
|
* and per browser, in localStorage, never synced: it is off until the user
|
|
* turns it on here.
|
|
*/
|
|
|
|
import { create } from 'zustand';
|
|
import type { OutboxEntry, OutboxError } from './queue';
|
|
|
|
/** localStorage: the local account ids that keep mail on this device. */
|
|
export const OFFLINE_ACCOUNTS_KEY = 'bulwark-offline-accounts';
|
|
|
|
export interface OutboxRow {
|
|
id: number;
|
|
account: string;
|
|
createdAt: number;
|
|
subject: string;
|
|
to: string[];
|
|
status: OutboxEntry['status'];
|
|
error?: OutboxError;
|
|
}
|
|
|
|
interface OfflineState {
|
|
/** Accounts with "Keep mail on this device" on. */
|
|
enabled: string[];
|
|
/** The browser says it has no network. */
|
|
browserOffline: boolean;
|
|
/** Mail servers (their URLs) that couldn't be reached at the last request. */
|
|
downServers: string[];
|
|
/** The app started without the server, from the mail on this device. */
|
|
startedOffline: boolean;
|
|
outbox: OutboxRow[];
|
|
pendingChanges: number;
|
|
usageBytes: number;
|
|
/** Accounts whose mail is being brought onto the device right now. */
|
|
syncing: string[];
|
|
lastSyncedAt: Record<string, number>;
|
|
}
|
|
|
|
function readEnabled(): string[] {
|
|
try {
|
|
const raw = typeof localStorage === 'undefined' ? null : localStorage.getItem(OFFLINE_ACCOUNTS_KEY);
|
|
const parsed = raw ? JSON.parse(raw) : [];
|
|
return Array.isArray(parsed) ? parsed.filter((id): id is string => typeof id === 'string') : [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export const useOfflineStore = create<OfflineState>()(() => ({
|
|
enabled: readEnabled(),
|
|
browserOffline: typeof navigator !== 'undefined' && navigator.onLine === false,
|
|
downServers: [],
|
|
startedOffline: false,
|
|
outbox: [],
|
|
pendingChanges: 0,
|
|
usageBytes: 0,
|
|
syncing: [],
|
|
lastSyncedAt: {},
|
|
}));
|
|
|
|
export function writeEnabled(enabled: string[]): void {
|
|
useOfflineStore.setState({ enabled });
|
|
try {
|
|
if (enabled.length) localStorage.setItem(OFFLINE_ACCOUNTS_KEY, JSON.stringify(enabled));
|
|
else localStorage.removeItem(OFFLINE_ACCOUNTS_KEY);
|
|
} catch {
|
|
/* storage unavailable */
|
|
}
|
|
}
|
|
|
|
export function serverKey(url: string): string {
|
|
return url.replace(/\/+$/, '').toLowerCase();
|
|
}
|
|
|
|
/** The browser is offline, or this mail server (any, without one) couldn't be reached. */
|
|
export function isOfflineNow(serverUrl?: string): boolean {
|
|
const { browserOffline, downServers } = useOfflineStore.getState();
|
|
if (browserOffline) return true;
|
|
return serverUrl ? downServers.includes(serverKey(serverUrl)) : downServers.length > 0;
|
|
}
|
|
|
|
export function setServerDown(serverUrl: string, down: boolean): boolean {
|
|
const key = serverKey(serverUrl);
|
|
const { downServers } = useOfflineStore.getState();
|
|
const was = downServers.includes(key);
|
|
if (was !== down) useOfflineStore.setState({ downServers: down ? [...downServers, key] : downServers.filter((s) => s !== key) });
|
|
return was;
|
|
}
|
|
|
|
/** The switch as another tab left it (it changed in localStorage there). */
|
|
export function readEnabledFrom(raw: string | null): string[] {
|
|
try {
|
|
const parsed = raw ? JSON.parse(raw) : [];
|
|
return Array.isArray(parsed) ? parsed.filter((id): id is string => typeof id === 'string') : [];
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|