Gated mail inventory and dry-run
Duckbill™ can inventory folders and produce a dry-run plan. Bulk historical migration is not an MVP execute path.
Rules
InventorySourceandAnalyzeFoldersare read-only.- A successful Microsoft, Gmail, or iCloud connect writes one folder scan for that
account_id. - Reopening Accounts or Mail reads that scan. The read does not call the provider.
- A scan older than 24 hours is labeled
Folder scan is stale. - A failed rescan keeps the previous folders, labels the scan stale, and shows the error on Accounts.
- A first-scan failure stores a failed snapshot with no folders and shows the error on Accounts. Connect still succeeds.
- iCloud stays read-write. The scan only lists folders and counts.
CreateMigrationPlanpersists a plan withdry_run_required=trueandsource_delete_allowed=false./inbox/migrationcreates that plan from the stored Gmail and iCloud folder scans and names Microsoft as the target. The create does not call a provider.RunDryRunreportsselected = succeeded + skipped + duplicate + conflict + failedand keeps the adapter write counter at zero.POST /api/migrations/startreturnsPOLICY_DENIEDwhileENABLE_ICLOUD_MIGRATIONis off. Pause and resume stay denied with it.- Setting
ENABLE_ICLOUD_MIGRATION=truestill does not execute bulk migration in this release. /inbox/migrationshows the last dry-run for one source: selected, skipped, duplicate, conflict, and failed. The page says mail was not moved. Start, pause, and resume stay denied if a user clicks them.- A dry-run reads stored messages for that one source when they exist. Otherwise it uses recorded inventory. It does not call provider writes. iCloud stays read-write.
HTTP
| Method | Path | Behavior |
|---|---|---|
GET | /api/inventory/source | Folder/message inventory against a recorded or mock adapter |
GET | /api/inventory/source?accountId= | Stored scan for that account. Does not call the provider |
GET | /api/inventory/scans | Last AnalyzeFolders snapshot for every account in the session |
POST | /api/inventory/folders/analyze | Per-folder counts |
GET | /api/migrations/plans | Microsoft-target plan and connected Gmail or iCloud sources |
POST | /api/migrations/plans | Persist a dry-run-required plan from stored folder scans |
GET | /api/migrations/dry-run | Last dry-run counts for the signed-in user |
POST | /api/migrations/dry-run | Non-mutating estimate for one source |
POST | /api/migrations/start | Always POLICY_DENIED in this milestone |
POST | /api/migrations/pause | POLICY_DENIED |
POST | /api/migrations/resume | POLICY_DENIED |
Code
| Module | Role |
|---|---|
packages/application/src/inventory.ts | Inventory and analyze |
packages/application/src/folder-scan.ts | Connect-time scan, stale label, stored read |
packages/persistence/src/folder-scans.ts | Per-account scan rows |
packages/application/src/migration.ts | Plan, dry-run, gated start |
packages/application/src/migration-plan-page.ts | Microsoft-target plan from stored scans |
packages/application/src/migration-dry-run-view.ts | One-source dry-run counts and denial copy |
packages/persistence/src/migration-plans.ts | Plan persistence |
packages/persistence/src/migration-dry-runs.ts | Last dry-run row for a user |
apps/web/src/app/inbox/migration/page.tsx | Migration page with plan and dry-run counts |
database/migrations/0019_followon_providers.sql | Plan and dry-run tables |
database/migrations/0024_folder_scans.sql | Folder scan table |
See ADR 0027.
