Files
webmail/app/api/offline/shell/route.ts
T
Linus Rath db5ac82f53 feat: keep recent mail on this device and send mail written offline #523
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.
2026-11-01 11:46:11 +01:00

38 lines
1.4 KiB
TypeScript

import { readdir } from 'node:fs/promises';
import { join } from 'node:path';
import { NextResponse } from 'next/server';
export const dynamic = 'force-dynamic';
const BASE_PATH = (process.env.NEXT_PUBLIC_BASE_PATH ?? '').replace(/\/+$/, '');
/** Files a page can load: scripts, styles, fonts and the images they name. */
const SHELL_FILE = /\.(js|css|woff2?|svg|png|webp)$/;
async function listFiles(dir: string, prefix: string, out: string[]): Promise<void> {
let entries;
try {
entries = await readdir(dir, { withFileTypes: true });
} catch {
return;
}
for (const entry of entries) {
if (entry.isDirectory()) await listFiles(join(dir, entry.name), `${prefix}${entry.name}/`, out);
else if (SHELL_FILE.test(entry.name)) out.push(`${prefix}${entry.name}`);
}
}
/**
* The files of this build's app shell (#523): everything under
* /_next/static/, which is the same for every user and content-hashed. A
* tab with offline mode on hands the list to the service worker, which keeps
* the files in this build's cache so the app also starts without a network.
*/
export async function GET() {
const files: string[] = [];
await listFiles(join(process.cwd(), '.next', 'static'), `${BASE_PATH}/_next/static/`, files);
return NextResponse.json(
{ buildId: process.env.NEXT_PUBLIC_BUILD_ID ?? '', files },
{ headers: { 'Cache-Control': 'no-store' } },
);
}