mirror of
https://github.com/spartanz51/tutabridge.git
synced 2026-06-24 10:54:32 +02:00
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.
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
{"default":{"identifier":"default","description":"Default capabilities for the main window","local":true,"windows":["main"],"permissions":["core:default","shell:allow-open","core:event:default","core:event:allow-listen","core:event:allow-emit"]}}
|
||||
{"default":{"identifier":"default","description":"Default capabilities for the main window","local":true,"windows":["main"],"permissions":["core:default","shell:allow-open","dialog:allow-open","core:event:default","core:event:allow-listen","core:event:allow-emit"]}}
|
||||
@@ -2402,6 +2402,72 @@
|
||||
"const": "core:window:deny-unminimize",
|
||||
"markdownDescription": "Denies the unminimize command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`",
|
||||
"type": "string",
|
||||
"const": "dialog:default",
|
||||
"markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`"
|
||||
},
|
||||
{
|
||||
"description": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)",
|
||||
"type": "string",
|
||||
"const": "dialog:allow-ask",
|
||||
"markdownDescription": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)"
|
||||
},
|
||||
{
|
||||
"description": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)",
|
||||
"type": "string",
|
||||
"const": "dialog:allow-confirm",
|
||||
"markdownDescription": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)"
|
||||
},
|
||||
{
|
||||
"description": "Enables the message command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "dialog:allow-message",
|
||||
"markdownDescription": "Enables the message command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the open command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "dialog:allow-open",
|
||||
"markdownDescription": "Enables the open command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the save command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "dialog:allow-save",
|
||||
"markdownDescription": "Enables the save command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)",
|
||||
"type": "string",
|
||||
"const": "dialog:deny-ask",
|
||||
"markdownDescription": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)"
|
||||
},
|
||||
{
|
||||
"description": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)",
|
||||
"type": "string",
|
||||
"const": "dialog:deny-confirm",
|
||||
"markdownDescription": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)"
|
||||
},
|
||||
{
|
||||
"description": "Denies the message command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "dialog:deny-message",
|
||||
"markdownDescription": "Denies the message command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the open command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "dialog:deny-open",
|
||||
"markdownDescription": "Denies the open command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the save command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "dialog:deny-save",
|
||||
"markdownDescription": "Denies the save command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`",
|
||||
"type": "string",
|
||||
|
||||
@@ -2402,6 +2402,72 @@
|
||||
"const": "core:window:deny-unminimize",
|
||||
"markdownDescription": "Denies the unminimize command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`",
|
||||
"type": "string",
|
||||
"const": "dialog:default",
|
||||
"markdownDescription": "This permission set configures the types of dialogs\navailable from the dialog plugin.\n\n#### Granted Permissions\n\nAll dialog types are enabled.\n\n\n\n#### This default permission set includes:\n\n- `allow-message`\n- `allow-save`\n- `allow-open`"
|
||||
},
|
||||
{
|
||||
"description": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)",
|
||||
"type": "string",
|
||||
"const": "dialog:allow-ask",
|
||||
"markdownDescription": "Enables the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)"
|
||||
},
|
||||
{
|
||||
"description": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)",
|
||||
"type": "string",
|
||||
"const": "dialog:allow-confirm",
|
||||
"markdownDescription": "Enables the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `allow-message` and will be removed in v3)"
|
||||
},
|
||||
{
|
||||
"description": "Enables the message command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "dialog:allow-message",
|
||||
"markdownDescription": "Enables the message command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the open command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "dialog:allow-open",
|
||||
"markdownDescription": "Enables the open command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Enables the save command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "dialog:allow-save",
|
||||
"markdownDescription": "Enables the save command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)",
|
||||
"type": "string",
|
||||
"const": "dialog:deny-ask",
|
||||
"markdownDescription": "Denies the ask command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)"
|
||||
},
|
||||
{
|
||||
"description": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)",
|
||||
"type": "string",
|
||||
"const": "dialog:deny-confirm",
|
||||
"markdownDescription": "Denies the confirm command without any pre-configured scope. (**DEPRECATED**: This is now an alias to `deny-message` and will be removed in v3)"
|
||||
},
|
||||
{
|
||||
"description": "Denies the message command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "dialog:deny-message",
|
||||
"markdownDescription": "Denies the message command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the open command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "dialog:deny-open",
|
||||
"markdownDescription": "Denies the open command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "Denies the save command without any pre-configured scope.",
|
||||
"type": "string",
|
||||
"const": "dialog:deny-save",
|
||||
"markdownDescription": "Denies the save command without any pre-configured scope."
|
||||
},
|
||||
{
|
||||
"description": "This permission set configures which\nshell functionality is exposed by default.\n\n#### Granted Permissions\n\nIt allows to use the `open` functionality with a reasonable\nscope pre-configured. It will allow opening `http(s)://`,\n`tel:` and `mailto:` links.\n\n#### This default permission set includes:\n\n- `allow-open`",
|
||||
"type": "string",
|
||||
|
||||
Reference in New Issue
Block a user