{"id":50519138,"url":"https://github.com/stalwartlabs/vandelay","last_synced_at":"2026-06-03T02:05:44.965Z","repository":{"id":361336509,"uuid":"1236882923","full_name":"stalwartlabs/vandelay","owner":"stalwartlabs","description":"Vandelay: the JMAP importer-exporter (and backup tool)","archived":false,"fork":false,"pushed_at":"2026-05-30T06:38:49.000Z","size":2762,"stargazers_count":4,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2026-05-30T08:08:10.971Z","etag":null,"topics":["backup","export","import","jmap","migration"],"latest_commit_sha":null,"homepage":"","language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/stalwartlabs.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSES/Apache-2.0.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null},"funding":{"open_collective":"stalwart","github":"stalwartlabs","ko_fi":null,"patreon":null,"tidelift":null,"community_bridge":null,"liberapay":null,"issuehunt":null,"otechie":null,"lfx_crowdfunding":null,"custom":null}},"created_at":"2026-05-12T17:00:54.000Z","updated_at":"2026-05-30T07:56:59.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/stalwartlabs/vandelay","commit_stats":null,"previous_names":["stalwartlabs/vandelay"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/stalwartlabs/vandelay","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stalwartlabs%2Fvandelay","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stalwartlabs%2Fvandelay/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stalwartlabs%2Fvandelay/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stalwartlabs%2Fvandelay/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/stalwartlabs","download_url":"https://codeload.github.com/stalwartlabs/vandelay/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stalwartlabs%2Fvandelay/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33844761,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-03T02:00:06.370Z","response_time":59,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["backup","export","import","jmap","migration"],"created_at":"2026-06-03T02:05:44.228Z","updated_at":"2026-06-03T02:05:44.960Z","avatar_url":"https://github.com/stalwartlabs.png","language":"Rust","funding_links":["https://opencollective.com/stalwart","https://github.com/sponsors/stalwartlabs"],"categories":[],"sub_categories":[],"readme":"\u003ch1 align=\"center\"\u003eVandelay\u003c/h1\u003e\n\n\u003ch3 align=\"center\"\u003e\n  The JMAP importer-exporter\n  \u003cbr/\u003e\n  \u003csub\u003eOne-shot account migration and backup across JMAP, IMAP, CalDAV, CardDAV, WebDAV, ManageSieve, Maildir, Google Takeout and Microsoft Exchange.\u003c/sub\u003e\n\u003c/h3\u003e\n\n\u003cbr\u003e\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://github.com/stalwartlabs/vandelay/actions/workflows/release.yml\"\u003e\u003cimg src=\"https://img.shields.io/github/actions/workflow/status/stalwartlabs/vandelay/release.yml?style=flat-square\" alt=\"build\"\u003e\u003c/a\u003e\n  \u0026nbsp;\n  \u003ca href=\"https://github.com/stalwartlabs/vandelay/releases/latest\"\u003e\u003cimg src=\"https://img.shields.io/github/v/release/stalwartlabs/vandelay?style=flat-square\" alt=\"release\"\u003e\u003c/a\u003e\n  \u0026nbsp;\n  \u003ca href=\"https://www.npmjs.com/package/@stalwartlabs/vandelay\"\u003e\u003cimg src=\"https://img.shields.io/npm/v/@stalwartlabs/vandelay?style=flat-square\u0026logo=npm\" alt=\"npm\"\u003e\u003c/a\u003e\n  \u0026nbsp;\n  \u003ca href=\"https://github.com/stalwartlabs/vandelay/releases\"\u003e\u003cimg src=\"https://img.shields.io/github/downloads/stalwartlabs/vandelay/total?style=flat-square\" alt=\"downloads\"\u003e\u003c/a\u003e\n  \u0026nbsp;\n  \u003cimg src=\"https://img.shields.io/badge/platforms-macOS%20%7C%20Linux%20%7C%20Windows-blue?style=flat-square\" alt=\"platforms\"\u003e\n  \u0026nbsp;\n  \u003ca href=\"#license\"\u003e\u003cimg src=\"https://img.shields.io/badge/license-MIT%2FApache--2.0-blue?style=flat-square\" alt=\"license\"\u003e\u003c/a\u003e\n\u003c/p\u003e\n\n\u003cbr\u003e\n\n\u003cimg align=\"right\" src=\"assets/importer-exporter.jpg\" alt=\"He's an importer-exporter.\" hspace=\"20\" vspace=\"6\"\u003e\n\n- Jerry: Well, what does *he* do?\n- George: He's an importer.\n- Jerry: Just imports, no exports?\n- George: He's an importer/exporter, okay?\n\n\u003cbr clear=\"all\"\u003e\n\u0026nbsp;\n\n## About\n\nVandelay is a one-shot account-migration utility for JMAP, the JMAP analogue of `imapsync` generalised to every JMAP data type (mail, contacts, calendars, identities, sieve scripts, file storage). It imports an account from a wide range of source protocols into a local SQLite \"archive\", then exports that archive into a target JMAP server. Import and export never talk to each other, only to the SQLite archive. One archive holds exactly one account.\n\nBecause the archive is a self-contained SQLite file that fully describes one account, vandelay doubles as a per-account backup tool: run an import on a schedule to capture a fresh snapshot, keep the resulting SQLite file as your backup, and restore it later by running an export against a JMAP target.\n\n## Features\n\n- **Many source protocols:**\n    - JMAP\n    - IMAP\n    - CalDAV\n    - CardDAV\n    - WebDAV\n    - ManageSieve\n    - Maildir++\n    - Google Takeout\n    - Microsoft Exchange via EWS (*experimental*)\n    - Microsoft Exchange Online via Graph (*experimental*)\n- **One target protocol:** JMAP, with type-by-type stateless re-matching on every run.\n- **Convergent:** Re-running an interrupted import or export picks up where it left off without bookkeeping flags.\n- **Multi-threaded, no async runtime:** Blocking HTTP with per-server concurrency caps respected automatically.\n- **Content-addressed blobs:** Emails, sieve scripts and file-storage payloads are stored once by BLAKE3 hash and deduplicated across the archive.\n- **Dry-run everywhere:** Every command supports `--dry-run` to compute the full plan without writing.\n- **Source-change protection:** An archive remembers which account it was filled from; pointing it at a different one fails unless explicitly permitted.\n- **Read-only inspection:** A built-in `inspect` command dumps any object type from an archive for verification.\n\n## Install\n\n```sh\n# macOS / Linux\ncurl --proto '=https' --tlsv1.2 -LsSf \\\n  https://github.com/stalwartlabs/vandelay/releases/latest/download/vandelay-installer.sh | sh\n\n# Homebrew\nbrew install stalwartlabs/tap/vandelay\n\n# Windows\npowershell -ExecutionPolicy Bypass -c \"irm https://github.com/stalwartlabs/vandelay/releases/latest/download/vandelay-installer.ps1 | iex\"\n\n# npm\nnpm install -g @stalwartlabs/vandelay\n\n# From source\ncargo install --path .\n```\n\nA signed `.msi` is also published with each release.\n\n## Quick start\n\nA typical run is two commands, one to capture a source account into a local SQLite archive and one to push that archive into a JMAP target.\n\n```sh\n# 1. Import an IMAP mailbox into a fresh archive.\nexport VANDELAY_PASSWORD='source-app-password'\nvandelay import imap \\\n  --url imaps://imap.example.com \\\n  --auth-basic alice@example.com \\\n  alice.sqlite\n\n# 2. Peek at what landed.\nvandelay inspect alice.sqlite                 # per-type summary\nvandelay inspect alice.sqlite mailbox         # mailbox tree\nvandelay inspect alice.sqlite email --limit 5\n\n# 3. Push the archive into a target JMAP server.\nexport VANDELAY_PASSWORD='target-password'\nvandelay export \\\n  --url https://jmap.example.org \\\n  --auth-basic alice@example.org \\\n  --account-name alice@example.org \\\n  alice.sqlite\n```\n\nBoth commands are convergent: rerun either to resume an interrupted run, or rerun `import` later to pick up new mail since the last snapshot. Use `--dry-run` on either side to compute the full plan without writing.\n\n## CLI quick reference\n\n```\nvandelay \u003cimport|export|inspect\u003e [args...]\n```\n\nAll actions accept a set of global flags (verbosity, worker pool size, retry policy, TLS handling) in addition to action-specific ones. The most useful are:\n\n| Flag | Purpose |\n| --- | --- |\n| `-j, --threads \u003cN\u003e` | Worker pool size (default: logical CPUs). |\n| `--dry-run` | Compute the full plan; perform no writes. |\n| `-v`, `-vv`, `-vvv` | Increase log verbosity. |\n| `-q, --quiet` | Warnings and errors only. |\n| `--max-retries \u003cN\u003e` | Max retries per request on transient failures (default 5). |\n| `--allow-invalid-certs` | Accept self-signed / invalid TLS certs. |\n\nCredentials should be supplied via the `VANDELAY_PASSWORD` / `VANDELAY_TOKEN` / `VANDELAY_EWS_CLIENT_SECRET` / `VANDELAY_GRAPH_TOKEN` environment variables, or via an interactive prompt; passing them on the command line is supported but not recommended.\n\n### Import\n\n```\nvandelay import \u003csource\u003e [source-args...] \u003cARCHIVE\u003e\n```\n\nReads a source account into the local SQLite `ARCHIVE` (created if absent). Every importer accepts `--allow-source-change` to override the archive's source-identity guard.\n\n#### JMAP\n\n```\nvandelay import jmap \\\n  --url \u003cURL\u003e \\\n  (--auth-basic \u003cUSER\u003e [--auth-password \u003cPASS\u003e] | --auth-bearer [TOKEN]) \\\n  (--account-id \u003cID\u003e | --account-name \u003cNAME\u003e) \\\n  [--objects \u003clist\u003e] \\\n  \u003cARCHIVE\u003e\n```\n\nImports a single JMAP account. `--objects` accepts a comma-separated list of object tokens (`mailbox,email,calendar,calendarevent,addressbook,contactcard,identity,sievescript,participantidentity,filenode`); default is everything the server advertises.\n\n#### IMAP\n\n```\nvandelay import imap \\\n  --url imap(s)://host[:port] \\\n  (--auth-basic \u003cUSER\u003e [--auth-password \u003cPASS\u003e] | --auth-bearer [TOKEN] --auth-user \u003cUSER\u003e) \\\n  [--include \u003cREGEX\u003e...] [--exclude \u003cREGEX\u003e...] [--exclude-special \u003cROLE\u003e...] \\\n  [--folder \u003cNAME\u003e...] [--subscribed-only] [--noautomap] \\\n  [--include-deleted] [--allow-cleartext] [--compress] \\\n  [--fetch-batch \u003cN\u003e] [--imap-connections \u003c1..8\u003e] \\\n  \u003cARCHIVE\u003e\n```\n\nImports mail (and only mail) from any IMAP server. Folder selection is via `--include`/`--exclude` regexes (mutually exclusive with the exact-match `--folder`); `--exclude-special` drops by SPECIAL-USE role.\n\n#### CalDAV\n\n```\nvandelay import caldav \\\n  --url \u003chttp(s)://host[/path]\u003e \\\n  (--auth-basic \u003cUSER\u003e [--auth-password \u003cPASS\u003e] | --auth-bearer [TOKEN]) \\\n  [--allow-cleartext] [--dav-connections \u003c1..8\u003e] [--multiget-batch \u003cN\u003e] \\\n  \u003cARCHIVE\u003e\n```\n\nDiscovers the user's CalDAV principal (or accepts a URL pointing straight at a calendar-home or calendar), then imports calendars and events.\n\n#### CardDAV\n\n```\nvandelay import carddav \\\n  --url \u003chttp(s)://host[/path]\u003e \\\n  (--auth-basic \u003cUSER\u003e [--auth-password \u003cPASS\u003e] | --auth-bearer [TOKEN]) \\\n  [--allow-cleartext] [--dav-connections \u003c1..8\u003e] [--multiget-batch \u003cN\u003e] \\\n  \u003cARCHIVE\u003e\n```\n\nSame shape as `caldav`, but for address books and contacts.\n\n#### WebDAV\n\n```\nvandelay import webdav \\\n  --url \u003chttp(s)://host[/path]\u003e \\\n  (--auth-basic \u003cUSER\u003e [--auth-password \u003cPASS\u003e] | --auth-bearer [TOKEN]) \\\n  [--allow-cleartext] [--dav-connections \u003c1..8\u003e] [--multiget-batch \u003cN\u003e] \\\n  \u003cARCHIVE\u003e\n```\n\nImports a plain WebDAV file collection as a JMAP `FileNode` tree.\n\n#### ManageSieve\n\n```\nvandelay import managesieve \\\n  --url sieve(s)://host[:port] \\\n  (--auth-basic \u003cUSER\u003e [--auth-password \u003cPASS\u003e] | --auth-bearer [TOKEN] --auth-user \u003cUSER\u003e) \\\n  [--allow-cleartext] \\\n  \u003cARCHIVE\u003e\n```\n\nImports sieve scripts only. Each script is content-addressed in the blob table; the active script is recorded.\n\n#### Maildir\n\n```\nvandelay import maildir \u003cMAILDIR\u003e \u003cARCHIVE\u003e \\\n  [--include \u003cREGEX\u003e...] [--exclude \u003cREGEX\u003e...] [--folder \u003cNAME\u003e...] \\\n  [--noautomap] [--include-deleted]\n```\n\nReads a local Maildir++ tree (a directory with `cur/`, `new/`, `tmp/`). No network. Folder selection mirrors the IMAP importer.\n\n#### Google Takeout\n\n```\nvandelay import takeout \u003cPATH\u003e \u003cARCHIVE\u003e [--noautomap]\n```\n\nScans a directory tree recursively for `.mbox`, `.ics` and `.vcf` files and imports them. Tailored to Google Takeout layouts but works on any such tree; system-label role assignment can be disabled with `--noautomap`.\n\n#### Microsoft Exchange (EWS)\n\n```\nvandelay import exchange-ews \\\n  [--url \u003cEWS-ENDPOINT\u003e] [--mailbox \u003cSMTP\u003e] \\\n  [--mailbox-kind primary|archive|public-folders] \\\n  (--auth-basic \u003cUSER\u003e [--auth-password \u003cPASS\u003e] \\\n   | --auth-bearer [TOKEN] [--ews-tenant \u003cT\u003e --ews-client-id \u003cID\u003e \\\n                            (--ews-device-code | --ews-client-secret \u003cSECRET\u003e)]) \\\n  [--ews-connections \u003c1..8\u003e] [--ews-getitem-batch \u003cN\u003e] [--ews-attachment-batch \u003cN\u003e] \\\n  [--ews-no-syncfolderitems] \\\n  \u003cARCHIVE\u003e\n```\n\nImports a mailbox via EWS, against either on-prem Exchange Server or Exchange Online. Autodiscover is used when `--url` is omitted (a `--mailbox` SMTP address is then required). Supports Basic, pre-acquired bearer, interactive device-code OAuth, and app-only client-credentials OAuth.\n\n#### Microsoft Exchange (Graph)\n\n```\nvandelay import exchange-graph \\\n  (--client-id \u003cUUID\u003e [--tenant \u003cID\u003e] | --access-token [TOKEN]) \\\n  [--user \u003cUPN|UUID\u003e] \\\n  [--mailbox-kind primary|archive] \\\n  [--objects mail,calendar,contacts] \\\n  [--event-body-format text|html] \\\n  [--graph-connections \u003c1..16\u003e] [--top \u003c1..1000\u003e] \\\n  \u003cARCHIVE\u003e\n```\n\nImports a mailbox from Exchange Online via Microsoft Graph. Without `--access-token`, the interactive device-code flow is used. `public-folders` is rejected here (use `exchange-ews` instead).\n\n### Export\n\n```\nvandelay export \\\n  --url \u003cURL\u003e \\\n  (--auth-basic \u003cUSER\u003e [--auth-password \u003cPASS\u003e] | --auth-bearer [TOKEN]) \\\n  (--account-id \u003cID\u003e | --account-name \u003cNAME\u003e) \\\n  [--objects \u003clist\u003e] [--prune [--yes]] \\\n  \u003cARCHIVE\u003e\n```\n\nStateless re-export of `ARCHIVE` into a target JMAP server account. The default behaviour is upsert-only: matched items are updated, unmatched local items are created, but pre-existing target items not covered by the archive are left alone.\n\n`--prune` enables destructive reconciliation: target objects that do not match anything in the archive are deleted. The confirmation prompt can be skipped with `--yes` for automation. Export speaks JMAP only; no other target protocols are currently supported.\n\n### Inspect\n\n```\nvandelay inspect \u003cARCHIVE\u003e [TYPE] [--limit \u003cN\u003e] [--offset \u003cN\u003e]\n```\n\nRead-only dump of a local archive. This command never opens a network connection and never writes to the archive.\n\n- Omit `TYPE` for a per-type summary (counts of every object kind plus blob storage stats).\n- Pass an object type to dump it: `mailbox`, `email`, `identity`, `sievescript`, `addressbook`, `contactcard`, `calendar`, `calendarevent`, `participantidentity`, `filenode`.\n- `mailbox` and `filenode` render as a tree (`--limit`/`--offset` are ignored); all other types use a paginated list and respect `--limit` and `--offset`.\n\n## Testing\n\nThe default suite is hermetic (unit tests plus `mockito`-scripted JMAP/DAV/EWS/Graph behaviours) and needs no network or Docker:\n\n```sh\ncargo build\ncargo clippy --all-targets\ncargo test\n```\n\n### Live and integration tests (Docker required)\n\nLive integration tests against a Stalwart server and container-based  tests against third-party servers (Dovecot, Cyrus, Radicale, Baikal, Apache `mod_dav`) are gated behind `--ignored`. **They require a running Docker daemon**: each test binary boots its own throwaway container via `testcontainers` (images are pulled automatically on first run), so Docker must be installed and `docker info` must succeed before invoking them.\n\nRun them per binary, and always with `--test-threads=1`:\n\n```sh\ncargo test --test sync_jmap          -- --ignored --test-threads=1   # live JMAP import/export/convergence/prune\ncargo test --test sync_imap          -- --ignored --test-threads=1\ncargo test --test sync_managesieve   -- --ignored --test-threads=1\ncargo test --test sync_maildir       -- --ignored --test-threads=1\ncargo test --test sync_caldav        -- --ignored --test-threads=1\ncargo test --test sync_carddav       -- --ignored --test-threads=1\ncargo test --test sync_webdav        -- --ignored --test-threads=1\ncargo test --test live_stalwart      -- --ignored --test-threads=1\ncargo test --test seed_smoke         -- --ignored --test-threads=1\ncargo test --test seed_only          -- --ignored --test-threads=1\n\n# Third-party-server tests (one container each):\ncargo test --test integration_radicale -- --ignored --test-threads=1\ncargo test --test integration_baikal   -- --ignored --test-threads=1\ncargo test --test integration_webdav   -- --ignored --test-threads=1\ncargo test --test integration_dovecot  -- --ignored --test-threads=1\ncargo test --test integration_cyrus    -- --ignored --test-threads=1\n\n# Slow tests\ncargo test --test mock_jmap -- --ignored\n```\n\n`--test-threads=1` is mandatory, not just advisory: within a binary every test shares a single per-binary container, and each test provisions then tears down the same disposable `vandelay.org` domain (and opens the archive with SQLite `EXCLUSIVE` locking). Separate binaries are isolated (each boots its own container on dynamic host ports), so plain `cargo test --test \u003cname\u003e` invocations are safe to run one after another.\n\n## License\n\nLicensed under either of\n\n * Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)\n * MIT license ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)\n\nat your option.\n\n## Copyright\n\nCopyright (C) 2020, Stalwart Labs LLC\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstalwartlabs%2Fvandelay","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstalwartlabs%2Fvandelay","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstalwartlabs%2Fvandelay/lists"}