Files
webmail/lib/mail-import/estimate.ts
T
Linus Rath 084f93a786 feat: import Thunderbird and Outlook data from this computer in the desktop app
The import dialog in the desktop app can now find Thunderbird profiles and
Outlook PST files, show what it found and import what the user confirms.
Browsers behave as before.

- The bridge's `localImport` gives opaque handles and range URLs. The shell
  asks natively before it looks, before it reads and before it goes on after
  a restart.
- `lib/mail-import` gets a Maildir source and a per-file `folder`, so
  Thunderbird's `.sbd` tree maps onto the account's folders. The PST worker
  reads through a synchronous range XHR when there is no Blob.
- What was imported is remembered by Message-ID and size per folder, because
  compacting a folder changes its checkpoint key and Email/import doesn't
  dedupe. It runs from the dialog and on a resume.
- The job's record lives in IndexedDB, since Chromium writes localStorage too
  late to survive a kill. Windows wait for the job's lock, so one takes over
  when another goes.
- The desktop dialog says how long an import takes at the upload limit, warns
  when Thunderbird is running, and turns down .ost files with the reason.

The engine used to be able to lose an import while offline: uploadBlob kept
the file on this device under a stand-in id and reported success. It now
passes `noHold`, Email/import is network only, an offline spell pauses without
using up a message's tries, and a device-only client stops at once. The same
fix applies to imports in the browser.
2026-11-03 17:57:54 +01:00

28 lines
1.1 KiB
TypeScript

/**
* How long an import takes at a server's upload limit. Stalwart's default is 1000
* uploads or 50 MB per user per hour, and each message is an upload. Without a count of
* the messages this goes by bytes, so it is the least the import can take.
*/
export const DEFAULT_UPLOAD_LIMIT = { files: 1000, bytes: 50_000_000 };
export function importHours(
amount: { bytes: number; messages?: number },
limit: { files?: number; bytes?: number } = DEFAULT_UPLOAD_LIMIT,
): number {
const byBytes = amount.bytes / (limit.bytes || DEFAULT_UPLOAD_LIMIT.bytes);
const byCount = (amount.messages ?? 0) / (limit.files || DEFAULT_UPLOAD_LIMIT.files);
return Math.max(byBytes, byCount);
}
export type DurationWords = { unit: "hours" | "days"; count: number };
/** Below this the limit doesn't matter enough to mention: the dialog's general note covers it. */
export const WORTH_SAYING_HOURS = 1;
/** Whole hours up to two days, whole days after that, rounded up. */
export function durationWords(hours: number): DurationWords {
if (hours < 48) return { unit: "hours", count: Math.ceil(hours) };
return { unit: "days", count: Math.ceil(hours / 24) };
}