Email Migration Scripts: How to Move Messages, Filters and Contacts Off Google (with Examples)
how-toemailmigration

Email Migration Scripts: How to Move Messages, Filters and Contacts Off Google (with Examples)

ffrees
2026-03-09
10 min read
Advertisement

Concrete scripts and manifests to export mail, convert Gmail filters to Sieve, and import contacts — with imapsync, Python examples and 2026 migration guidance.

Stop paying in surprise — move your mailchain off Gmail with reproducible scripts

Hook: If new Google policies in 2026 have you re-evaluating where your email lives, this guide gives you the exact scripts, manifests and conversion recipes to export messages, contacts and filters — and import them into common alternatives — with a clear plan for verification and pitfalls to avoid.

Executive summary (most important first)

This guide shows three practical, production-ready migration paths and the utility scripts that make them repeatable and automatable for teams and power users:

  • Direct IMAP-to-IMAP transfer (recommended): imapsync examples with OAuth2 and Docker for large mailboxes.
  • Archive-first migration: export via Google Takeout or Gmail API to mbox/maildir, then sync to your target via mbsync or direct IMAP inject.
  • Filters & contacts migration: a small Python toolset to export Gmail filters via the Gmail API, convert to Sieve (best-effort), and export contacts to vCard/CSV using the People API.

You'll get YAML manifests, sample scripts, and mapping rules so you can automate and audit the whole transition.

Why move now (2026 context)

Late 2025 — early 2026 saw major email platform shifts: Google announced changes to Gmail personalization and address policies that accelerated migrations for privacy-conscious teams and companies. Meanwhile, demand for privacy-first or self-hosted mail (Fastmail, Proton, Dovecot) rose, and IMAP/Sieve-based toolchains remain the most portable choice for developers and admins.

In short: if you need to reduce vendor lock-in, avoid surprise product changes, or host mail on infrastructure you control, migrating now with repeatable scripts is the right move.

Before you start: checklist

  • Inventory accounts, labels, total message counts, aliases, and forwarding rules.
  • Credentials — obtain OAuth2 client credentials for the Gmail API or an app-password/OAuth token for IMAP access. For Workspace, consider a service account with domain-wide delegation.
  • Backups — always create a full MBOX export via Google Takeout or the Gmail API.
  • Test account — perform a complete dry-run using a representative mailbox before migrating production accounts.
  • Rate limits & throttling — plan for API/IMAP limits; split large mailboxes into time windows.

1) Direct IMAP migration (imapsync) — the fastest repeatable path

imapsync is the industry-standard tool to copy mail from one IMAP server to another while preserving flags and internal dates. Use it when you want minimal downtime and a streaming transfer.

Since 2022+, Google prefers OAuth2 for IMAP access. The command below shows a typical Docker-run imapsync invocation using credentials stored as environment variables (replace placeholders):

<!-- language: text -->
  docker run --rm -it --name imapsync \
    -e IMAPSYNC_OAUTH1_CLIENT_ID='YOUR_OAUTH_CLIENT_ID' \
    -e IMAPSYNC_OAUTH1_CLIENT_SECRET='YOUR_OAUTH_CLIENT_SECRET' \
    -v $PWD/logs:/var/log/imapsync \
    ghcr.io/imapsync/imapsync:latest \
    --host1 imap.gmail.com --user1 user@gmail.com --authmech1 XOAUTH2 --oauth2_token1 "$GMAIL_OAUTH_TOKEN" --ssl1 \
    --host2 imap.target.com --user2 user@yourdomain.com --password2 'TARGET_PASSWORD' --ssl2 \
    --syncinternaldates --skipheader 'X-GM-RAW' --addheader --subscribeall --folderrec "INBOX" --folderrec "Label*"
  

Key flags explained:

  • --authmech1 XOAUTH2: Use OAuth2 for Gmail IMAP.
  • --syncinternaldates: Preserve original message dates.
  • --addheader: Add a header that shows original UID if you want to audit duplicates.
  • --subscribeall: Ensure folders on the destination are subscribed.

For very large mailboxes: parallelize and checkpoint

Split by top-level labels or date ranges and run multiple imapsync jobs. Example wrapper to split by year:

<!-- language: bash -->
  for year in 2018 2019 2020 2021 2022 2023 2024 2025; do
    docker run --rm ghcr.io/imapsync/imapsync:latest \
      --host1 imap.gmail.com --user1 user@gmail.com --authmech1 XOAUTH2 --oauth2_token1 "$GMAIL_OAUTH_TOKEN" --ssl1 \
      --host2 imap.target.com --user2 user@yourdomain.com --password2 'TARGET_PASSWORD' --ssl2 \
      --search 'after ${year}-01-01 before ${year}-12-31' --syncinternaldates --addheader &
  done
  wait
  

Pitfalls (IMAP transfer)

  • Gmail labels map to IMAP folders. Messages with multiple labels will be copied into multiple folders — this is expected. You can dedupe by UID after transfer if necessary.
  • Forwarding and filters are not migrated by imapsync; handle them separately.
  • OAuth2 tokens expire — implement refresh flows or use long-lived service-account tokens for Workspace.

2) Archive-first: Google Takeout / Gmail API -> maildir -> mbsync

If you prefer a file-based canonical archive or need to keep a local copy, export to MBOX (Google Takeout) or fetch raw messages via the Gmail API and convert to Maildir for import.

Takeout route

  1. Request Mail export from Google Takeout (produces one or several MBOX files).
  2. Convert MBOX to Maildir: use mb2md or python's mailbox module.
  3. Push maildir to IMAP destination using mbsync/ isync with a short config.
<!-- example mbsync config -->
  IMAPAccount target
  Host imap.target.com
  User user@yourdomain.com
  PassCmd "gpg --quiet --for-your-eyes-only --no-tty -d ~/.mail/pass.gpg"
  
  IMAPStore target-remote
  Account target
  
  MaildirStore local-maildir
  Path ~/mail/user/\
  Inbox ~/mail/user/INBOX
  
  Channel user-sync
  Master :local-maildir:
  Slave :target-remote:
  Create Slave
  Sync Pull
  

Gmail API route (for automation & audit)

Use the Gmail API to list messages then fetch the raw RFC822 payload. The snippet below uses Python and google-auth to download message drafts to mbox or maildir.

<!-- language: python -->
  # gmail_export.py (abridged)
  from google.oauth2 import service_account
  from googleapiclient.discovery import build
  import base64, email, mailbox

  SCOPES = ['https://www.googleapis.com/auth/gmail.readonly']
  creds = service_account.Credentials.from_service_account_file('sa.json', scopes=SCOPES)
  delegated = creds.with_subject('user@domain.com')
  service = build('gmail','v1',credentials=delegated)

  mbox = mailbox.mbox('export.mbox')
  res = service.users().messages().list(userId='me', q=None).execute()
  for m in res.get('messages', []):
      msg = service.users().messages().get(userId='me', id=m['id'], format='raw').execute()
      raw = base64.urlsafe_b64decode(msg['raw'].encode('ASCII'))
      mbox.add(email.message_from_bytes(raw))
  mbox.flush()
  

Note: For consumer Gmail accounts you must use OAuth consent; for Workspace, service accounts with delegation simplify automation.

3) Filters: export, convert and import (Gmail -> Sieve)

Gmail filters are powerful and use Gmail-specific operators (has:attachment, category, query syntax). There is no perfect mapping to Sieve, but we can provide a best-effort converter that handles the common cases: header matches, from/to/subject, size, and attachments (best-effort).

Export Gmail filters

Two practical ways:

  • Gmail web UI: Settings → Filters and Blocked Addresses → Export — produces an XML file per filter.
  • Gmail API: users.settings.filters endpoint to list filters programmatically.

Example: Python converter from Gmail filters (API) to Sieve rules

<!-- language: python -->
  # gmail_filters_to_sieve.py (concept)
  import json
  
  def gmail_to_sieve(filter_obj):
      # filter_obj is the Gmail API representation
      sieves = []
      crit = filter_obj.get('criteria', {})
      act = filter_obj.get('action', {})

      tests = []
      if 'from' in crit:
          tests.append('header :is "From" "%s"' % crit['from'])
      if 'to' in crit:
          tests.append('header :contains "To" "%s"' % crit['to'])
      if 'subject' in crit:
          tests.append('header :contains "Subject" "%s"' % crit['subject'])
      if 'query' in crit:
          # best-effort: map simple "has:attachment" or "larger:"
          q = crit['query']
          if 'has:attachment' in q:
              tests.append('exists "Content-Disposition"')

      action_lines = []
      if act.get('removeLabelIds'):
          # map label removals to not moving into folder
          pass
      if act.get('addLabelIds'):
          # map Gmail label to fileinto folder of same name
          for lbl in act['addLabelIds']:
              action_lines.append('fileinto "%s";' % lbl)
      if act.get('forward'):
          action_lines.append('redirect "%s";' % act['forward'])

      sieve = 'if allof(%s) {\n  %s\n}\n' % (', '.join(tests), '\n  '.join(action_lines))
      return sieve
  
  # usage: iterate over filters JSON and write to a .sieve file
  

Mapping rules and limitations

  • Labels → folders: Gmail labels that are not exclusive will create duplicates when mapped to IMAP folders. Consider deduplication steps post-import.
  • has:attachment mapping to Sieve is heuristic: check Content-Disposition or Content-Type headers; false negatives are possible for inline attachments.
  • Complex queries (OR/NOT with nested operators) may not translate; log and review those filters manually.
  • Actions not supported: Gmail's “Mark as important” or category-based actions don't exist in Sieve; use flags or custom headers instead.

Contacts: export and import scripts

Google Contacts export via Takeout provides vCard/CSV; for automation use the People API to extract, normalize and emit a vCard/CSV file for target providers.

Python example: export contacts to vCard using People API

<!-- language: python -->
  # contacts_export.py (abridged)
  from google.oauth2 import service_account
  from googleapiclient.discovery import build
  import vobject

  SCOPES = ['https://www.googleapis.com/auth/contacts.readonly']
  creds = service_account.Credentials.from_service_account_file('sa.json', scopes=SCOPES).with_subject('user@domain.com')
  svc = build('people','v1',credentials=creds)

  results = svc.people().connections().list(resourceName='people/me', pageSize=1000, personFields='names,emailAddresses,phoneNumbers').execute()
  conns = results.get('connections', [])

  with open('contacts.vcf','w') as f:
      for p in conns:
          v = vobject.vCard()
          name = p.get('names',[{}])[0].get('displayName')
          if name: v.add('fn').value = name
          for e in p.get('emailAddresses',[]):
              v.add('email').value = e['value']
          f.write(v.serialize())
  

Import targets

  • Fastmail / most providers: import vCard/CSV directly via web UI or API.
  • Office365/Exchange: flatten to CSV matching their schema.
  • Self-hosted CardDAV: use vdirsyncer or cadaver to push contacts to the server.

Migration manifest: yaml example for automation and audit

Keep a manifest per account so migrations are repeatable and auditable. Example:

<!-- language: yaml -->
  account: user@domain.com
  source:
    type: gmail
    imap_host: imap.gmail.com
    auth: oauth2
    oauth_client_id: xxxxx
  target:
    type: imap
    imap_host: imap.target.com
    user: user@domain.com
  tasks:
    - export: takeout
      path: /archives/user-takeout/
    - transfer: imapsync
      options:
        syncinternaldates: true
        addheader: true
    - filters: export_api
      convert: sieve
      dest: /manifests/user.sieve
    - contacts: export_people
      output: /manifests/user.vcf
  verification:
    - check_counts: true
    - sample_messages: 50
  

Verification and cutover checklist

  1. Compare message counts per folder/label (source vs destination).
  2. Randomly verify message integrity (raw headers, dates, attachments).
  3. Test filters by sending sample messages that should trigger each rule.
  4. Verify contacts (searching for key contacts, email format, phone numbers).
  5. Set up a forwarding window: keep original account receiving mail and forward to new inbox for 14–30 days to catch missings.

Common pitfalls and how to avoid them

  • Missing messages: usually caused by search filters in imapsync or API list pagination — always iterate through pages and confirm totals.
  • Label explosion: mapping multi-labeled messages to multiple folders can bloat the destination. Option: pre-process to collapse rarely used labels into an "Archive" folder.
  • Forwarding verifications: destination providers often require confirmation for auto-forwarding; update filters to only tag and not forward until verified.
  • Privacy & scanning: some providers scan mail for AI features. If privacy is the goal, choose providers that explicitly avoid content scanning.

Tip: keep a read-only canonical MBOX archive before you start destructive operations. That gives you a forensic copy you can re-import or inspect if something goes wrong.

2026 trends to account for in your migration strategy:

  • AI-enabled scanning: Platforms are offering AI features that require content access; choose providers with clear data-use contracts if you want to avoid this.
  • Server-side filters (Sieve) are back: Many modern providers support Sieve and ManageSieve; building a Sieve repo from Gmail filters future-proofs server-side behavior.
  • Containerized migration pipelines: Use Docker + Compose to bundle imapsync, mbsync, and API scripts for repeatable runs across accounts.

Practical migration playbook (step-by-step)

  1. Inventory and manifest: create the YAML manifest for the account.
  2. Archive: perform Google Takeout (or run gmail_export.py) and store MBOX in immutable storage.
  3. Contacts: run contacts_export.py and import into target; verify key contacts.
  4. Filters: export via API or UI, convert to Sieve, review converted rules manually for complex queries.
  5. Transfer mail: run imapsync in Docker. Start small (INBOX + recent 12 months), validate, then run full migration.
  6. Verification: run the checks in the manifest. Sample and edge-case testing is mandatory.
  7. Cutover: update DNS/aliases or create forwarding and keep the old account forwarding for 14–30 days. Disable auto-forwarding in filters only after confirmation.

Appendix: quick reference commands

imapsync minimal

imapsync --host1 imap.gmail.com --user1 user@gmail.com --authmech1 XOAUTH2 --oauth2_token1 "$TOKEN" --ssl1 \
  --host2 imap.target.com --user2 user@domain.com --password2 'PWD' --ssl2 --syncinternaldates --addheader
  

mbox -> maildir (python)

python -c "import mailbox,maildir,sys; m=mailbox.mbox('export.mbox'); md=maildir.Maildir('Maildir',create=True); [md.add(m[i]) for i in range(len(m))]"
  

Final notes

Migrating email off Gmail in 2026 requires both technical steps and policy awareness. Use OAuth2 and service accounts for automation, keep immutable archives, and always verify filters and forwarding behavior after the move. The scripts here are practical starting points — customize for your provider and compliance needs.

Actionable takeaways (quick)

  • Use imapsync with OAuth2 for direct IMAP transfers and preserve metadata.
  • Export filters via API and convert to Sieve for server-side continuity.
  • Automate with manifests (YAML) so migrations are repeatable and auditable.

Call to action

If you want a migration manifest template or a tested Docker Compose pipeline that bundles imapsync, mbsync and the export scripts above for your team, download our free repo starter or contact us for a migration audit. Start with the manifest — it turns a risky manual process into a repeatable, reviewable workflow.

Advertisement

Related Topics

#how-to#email#migration
f

frees

Contributor

Senior editor and content strategist. Writing about technology, design, and the future of digital media. Follow along for deep dives into the industry's moving parts.

Advertisement
2026-04-20T00:36:05.789Z