Files
tutabridge/ui/package.json
T
Anthony MandGitHub d27c278cab Complete mailbox backup to .eml files (#6)
* Add complete mailbox backup to .eml files (CLI)

`tutabridge backup <dir>` exports every mail of every folder to a
plaintext `.eml` tree, mirroring the IMAP folder hierarchy.

A backup must be *complete*: it enumerates all mails per folder from
the server (`limit == 0`), not just the `sync_limit`-capped subset the
bridge keeps cached. Dumping only the synced subset would silently drop
mail — live-tested here against an INBOX with 6288 server-side mails vs
1050 cached, all 6288 exported. The encrypted local cache
(`.eml.enc`) is used as a fast path; only never-synced mails trigger a
rate-limited (150ms) server fetch.

Format: one `.eml` per mail in `<output>/<folder path>/<YYYYMMDD-HHMMSS>_<id>.eml`.
EML is the most portable target — native to Thunderbird/Apple Mail/
Outlook, no Maildir `:2,S` colons that break on Windows, and a single
corrupt file never takes down the whole archive. Folder path segments
are sanitised for cross-platform filesystems (Windows-illegal chars +
trailing dot/space stripped); the date prefix makes a directory listing
sort chronologically.

`backup::export_eml` is surface-agnostic (takes a progress callback) so
a GUI button can wrap the same engine later. Per-mail failures are
collected in `BackupStats::errors` rather than aborting the run. The CLI
shares the keychain/password login flow with the bridge via the new
`login_session` helper, and opens the cache without the bridge's
reset-on-key-mismatch (a backup must never destroy the cache).

8 backup unit/integration tests: filename + folder sanitisation,
date stamp, and an end-to-end export over a mock backend asserting the
cache-vs-server split, file tree layout, and verbatim cached bodies.

GUI button is a follow-up (needs the Tauri dialog plugin).

* Make backup resumable / incremental

Skip a mail when its `.eml` is already on disk, before any cache read
or server fetch. The filename is deterministic (stable receivedDate +
element id) and mail content is immutable, so an existing file is never
stale. This turns an interrupted backup into a resume (re-run continues
where it stopped) and a periodic re-backup into an incremental one
(only new mail is fetched — the expensive part). New
`BackupStats::skipped` counter, surfaced in the CLI summary.

Two tests: a re-run skips every already-exported mail (zero server
loads), and an incremental run fetches only the newly-arrived mail.

* Add Backup tab to the GUI

A "Backup" tab wraps the same `backup::export_eml` engine as the CLI:
a native folder picker (tauri-plugin-dialog), a live per-folder
progress bar driven by `bridge://backup-progress` events, and a result
summary (mails written, folders, MB, cache vs server vs skipped).

`BridgeHandle` now keeps the logged-in backend + local cache after
`start` and exposes them via `backend_and_store()`, so the
`export_mails` command reuses the live session instead of opening a
second one — and drops the handle lock before the (minutes-long)
export so status/stats stay responsive. The button is disabled unless
the bridge is running.

`BackupStats` is now `Serialize` so it can cross the Tauri boundary.

* Keep backup state across tab switches

The Backup tab is conditionally rendered, so switching away unmounted
`BackupPanel` mid-export — dropping its progress + result state and the
`bridge://backup-progress` listener while the Rust task kept running.
Coming back showed an idle panel even though the backup was still going.

Lift all backup state (busy / progress / result / error), the
`startBackup` action, and the progress listener into the always-mounted
`useBridge` hook. The listener is now active regardless of which tab is
shown, and `BackupPanel` is purely presentational — switch tabs freely
mid-backup and the progress is intact on return. `startBackup` guards
against a double launch while one is in flight.
2026-05-29 14:43:47 +02:00

34 lines
823 B
JSON

{
"name": "ui",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"build": "tsc -b && vite build",
"lint": "eslint .",
"preview": "vite preview"
},
"dependencies": {
"@tauri-apps/api": "^2.11.0",
"@tauri-apps/plugin-dialog": "^2.4.0",
"@tauri-apps/plugin-shell": "^2.3.5",
"react": "^19.2.6",
"react-dom": "^19.2.6"
},
"devDependencies": {
"@eslint/js": "^10.0.1",
"@types/node": "^24.12.3",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@vitejs/plugin-react": "^6.0.1",
"eslint": "^10.3.0",
"eslint-plugin-react-hooks": "^7.1.1",
"eslint-plugin-react-refresh": "^0.5.2",
"globals": "^17.6.0",
"typescript": "~6.0.2",
"typescript-eslint": "^8.59.2",
"vite": "^8.0.12"
}
}