A dry-run-first toolkit for restoring Google Workspace calendar exports from Google Takeout into Microsoft 365 Exchange Online.
The project is designed for organizations that no longer have access to the live Google Workspace tenant and must recover calendars from local ZIP/ICS exports. It inventories Takeout archives without bulk extraction, separates authoritative calendars from subscribed copies, provisions Exchange resources, and performs guarded Microsoft Graph imports.
Status: early-stage migration tooling. Always run against pilot mailboxes and disposable resources before production. Complex recurrence exceptions are reported for review rather than silently flattened.
- Streams ICS and resource metadata directly from Takeout ZIP files.
- Generates archive checksums and ZIP-member inventories.
- Classifies primary user calendars, resources, legacy calendars, appointment schedules, and subscribed copies.
- Produces editable resource, principal, organizer, and permission mappings.
- Deduplicates event components by Google
UIDplusRECURRENCE-ID. - Separates quiet historical restoration from active meetings that may send invitations.
- Converts common daily, weekly, monthly, and yearly recurrence rules to Microsoft Graph.
- Routes cancellations, recurrence exceptions, exclusions, attachments, and unsupported patterns to manual review.
- Uses a deterministic transaction ID and local SQLite state for resumable, idempotent imports.
- Verifies that the migration app can access in-scope mailboxes and cannot access an out-of-scope mailbox.
- Automates Exchange room/equipment provisioning, Places metadata, calendar processing, folder permissions, and scoped application RBAC.
- Keeps backup, cleanup planning, destructive cleanup, import, and reconciliation as separate operations.
The following safeguards are intentional:
- Source Takeout data is treated as immutable.
- All generated reports and configuration are ignored by Git by default.
- Inventory and manifest commands are read-only.
- Import requires
--apply --confirm 'RESTORE CALENDARS'. - Active import requires an additional explicit notification acknowledgment.
- Cleanup requires a completed target backup, a reviewed deletion plan, and a separate destructive confirmation.
- Deleting organizer meetings with attendees requires an additional cancellation-risk acknowledgment.
- Application access should be granted through Exchange Online RBAC for Applications, scoped to a migration-only mail-enabled security group.
- The app should not receive tenant-wide Microsoft Graph application roles when Exchange Application RBAC is used.
See AGENTS.md for the repository's non-negotiable working rules.
- Python 3.11 or later.
- OpenSSL available on
PATHfor certificate-based client assertions. - Network access to Microsoft Entra ID and Microsoft Graph during tenant operations.
- PowerShell 7.4 or later on Windows, macOS, Linux, or Azure Cloud Shell.
ExchangeOnlineManagementPowerShell module.- An account with Exchange Administrator access and sufficient Exchange Organization Management authority to delegate Application RBAC roles.
- Permission to create an Entra app registration and upload a certificate.
Privileged Role Administrator or Global Administrator is only necessary if tenant-wide Microsoft Graph application consent is used. This project instead recommends Exchange Application RBAC without tenant-wide Graph application roles.
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip setuptools
python -m pip install -e .
calendar-migrate --versionThe command is generated from the [project.scripts] entry in pyproject.toml and is available whenever the virtual environment is activated.
Keep at least one untouched backup. Add the actual Takeout directory to .gitignore; never commit calendar data, event bodies, attendee addresses, exported metadata, or source-generated reports.
Example layout:
workspace2exchange-calendar-migration/
├── takeout-export/ # ignored local data
├── config/ # ignored reviewed mappings
├── artifacts/ # ignored reports and manifests
├── scripts/
├── src/
└── tests/
The domain is required and is never inferred from project source code:
calendar-migrate inventory \
--source ./takeout-export \
--domain example.org \
--timezone America/New_York \
--graph-timezone 'Eastern Standard Time' \
--output artifacts/inventory--timezone is the IANA fallback for source events without usable timezone metadata. --graph-timezone is the corresponding Microsoft/Windows timezone used by Graph. Supplying both prevents the migration from silently assuming the original author's locale.
Important outputs include:
archive_checksums.csvzip_members.csvcalendars.csvresources.proposed.csvprincipals.proposed.csvorganizers.proposed.csvpermissions.proposed.csvmigration.proposed.jsoninventory_summary.json
calendar-migrate initialize-config \
--inventory artifacts/inventory \
--config-dir configReview every row in:
config/resources.csvconfig/principals.csvconfig/organizers.csvconfig/permissions.csvconfig/migration.json
Nothing is approved automatically. For each resource, choose a target SMTP address, resource type, disposition, and approval status. Classify principals that have no primary calendar as users, groups, aliases, or ignored records. Explicitly map any legacy Google group-calendar organizers.
Validate the reviewed configuration:
calendar-migrate validate --config config/migration.jsoncalendar-migrate manifest \
--config config/migration.json \
--output artifacts/dry-runReview:
manifest_summary.jsonevent_manifest.csvevent_manifest.jsonlduplicates.csvmanual_review.csvnotification_impact.csv
The JSONL file contains the exact Microsoft Graph payloads. Do not hand-edit it; correct the mappings or transformation logic and regenerate it.
Creating a Microsoft Graph event with attendees sends meeting invitations. The manifest therefore uses two phases:
- Restored quietly without active attendee relationships.
- Preserves calendar occupancy without replaying old invitations.
- Written to each approved historical target where the item belongs.
- Created only in the selected organizer mailbox.
- Includes mapped internal attendees and Exchange resources.
- May send invitations and room-processing messages.
- Requires the notification-impact report and an extra command-line confirmation.
Recurring series with future occurrences are active. RECURRENCE-ID, EXDATE, cancelled components, and unsupported recurrence patterns are placed in manual review.
Create a dedicated single-tenant Entra app registration with a short-lived certificate. Do not add tenant-wide Microsoft Graph application permissions when using Exchange Application RBAC.
Generate a certificate outside the repository:
mkdir -p ../migration-secrets
openssl req -x509 -newkey rsa:3072 -sha256 -nodes -days 90 \
-subj '/CN=Workspace to Exchange Calendar Migration' \
-keyout ../migration-secrets/migration-private-key.pem \
-out ../migration-secrets/migration-certificate.pemUpload only the public certificate to the app registration. Record:
- Tenant ID.
- Application/client ID.
- Enterprise application service-principal object ID.
- External private-key and public-certificate paths.
Put those values in the ignored config/migration.json.
Install the module and test the administrator account:
Install-Module ExchangeOnlineManagement -Scope CurrentUser
./scripts/Test-AdminPrerequisites.ps1 -AdminUserPrincipalName admin@example.orgAll mutating PowerShell scripts default to dry run.
./scripts/Provision-Resources.ps1 `
-MappingPath ./config/resources.csv `
-AdminUserPrincipalName admin@example.orgApply only after review:
-Apply -Confirmation 'CREATE EXCHANGE RESOURCES'./scripts/Configure-AppRbac.ps1 `
-ResourceMappingPath ./config/resources.csv `
-PrincipalMappingPath ./config/principals.csv `
-AdminUserPrincipalName admin@example.org `
-AppId '<application-client-id>' `
-ServicePrincipalObjectId '<enterprise-application-object-id>'Apply only after review:
-Apply -Confirmation 'CONFIGURE SCOPED APP ACCESS'Application RBAC changes can take time to propagate. Verify both allowed and denied access:
calendar-migrate verify-graph \
--config config/migration.json \
--in-scope pilot-user@example.org \
--out-of-scope excluded-user@example.org./scripts/Set-CalendarPermissions.ps1 `
-ResourceMappingPath ./config/resources.csv `
-PermissionMappingPath ./config/permissions.csv `
-AdminUserPrincipalName admin@example.orgApply only after review:
-Apply -Confirmation 'GRANT CALENDAR PERMISSIONS'Calendar Editor/Delegate access is different from Set-CalendarProcessing -ResourceDelegates, which controls approval of booking requests.
Back up existing target calendar data before planning any deletion:
calendar-migrate backup --config config/migration.json \
--mailbox pilot-user@example.org \
--output artifacts/target-backup
calendar-migrate plan-clear --config config/migration.json \
--mailbox pilot-user@example.org \
--output artifacts/clear-planplan-clear never deletes anything. Review the plan before using execute-clear. Organizer events with attendees can send cancellations and require a separate acknowledgment.
Pilot one quiet historical action:
calendar-migrate import \
--config config/migration.json \
--manifest artifacts/dry-run/event_manifest.jsonl \
--phase history --limit 1 \
--apply --confirm 'RESTORE CALENDARS'Pilot one active meeting only with approved test recipients:
calendar-migrate import \
--config config/migration.json \
--manifest artifacts/dry-run/event_manifest.jsonl \
--phase active --limit 1 \
--apply --confirm 'RESTORE CALENDARS' \
--allow-notifications \
--notification-confirm 'SEND MIGRATION INVITATIONS'Rerunning an already-successful action with the same payload hash is skipped using the ignored SQLite state database.
calendar-migrate reconcile \
--config config/migration.json \
--output artifacts/reconcileAfter migration, revoke the Exchange application assignment:
./scripts/Remove-AppRbac.ps1 `
-AdminUserPrincipalName admin@example.org `
-AppId '<application-client-id>'Review the dry run, then use:
-Apply -Confirmation 'REVOKE MIGRATION APP ACCESS'Remove the Entra certificate credential or delete the app registration, securely delete the private key, and retain non-secret audit evidence.
python -m unittest discover -s tests -vSee docs/PILOT_RUNBOOK.md for a staged tenant pilot.
This project is licensed under the GNU General Public License v3.0 or later. You may use, modify, and redistribute it under the GPL's terms. It is provided without warranty; see the license for details.
- The importer does not silently recreate complex recurrence exceptions. They require review or future exception-handling support.
- Google appointment-schedule products are inventoried separately and are not treated as ordinary calendars.
- External attendees are excluded by default to prevent accidental messages.
- ICS attachments require review; inaccessible Google Drive attachments cannot be recovered from ICS alone.
- Calendar organizer identity cannot be changed in-place in Outlook; changing organizer requires creating a new meeting.
- This project is not affiliated with or supported by Google or Microsoft.