How to migrate email with IMAPSync between servers without losing messages

Learn how to migrate email with IMAPSync between IMAP servers preserving folders, flags, and dates. Production-tested procedure.

Migrating mailboxes between IMAP servers is one of the most delicate procedures in email administration. Messages are heterogeneous (sizes ranging from 2 KB to 50 MB+), flags matter (read/unread, flagged, replied), and any MX interruption leaves in-flight messages floating between both sides. IMAPSync solves this with incremental sync — you can run it multiple times without duplicating messages and do the real cutover in a short window.

This tutorial is for sysadmins who need to move one or several IMAP accounts between providers without losing messages. It covers the full cycle: installation, dry-run, two-phase sync, DNS switch, and final validation. Typical persona: someone migrating from cPanel/Plesk/Zimbra to another provider, or consolidating accounts from an old Microsoft 365 to their own VPS infrastructure.

Estimated time: 30-45 minutes for setup and first sync of a 5-10 GB account. Larger accounts take hours and can be left running in the background. The final cutover (the second incremental run) typically takes 2-5 minutes per account.

Prerequisites

You need a Linux machine with internet access — it can be the destination VPS, an auxiliary VPS, or your own desktop. I recommend running IMAPSync on a VPS in the same datacenter as the destination server: low latency makes a real difference on large accounts.

Technical prerequisites

Ubuntu 22.04/24.04 LTS or Debian 12 with sudo access, Perl 5.30+ installed (default on the system), 2 GB free on disk for IMAPSync temporary cache, and port 993 (IMAPS) open for outbound. IMAP credentials for source and destination — with app password if 2FA is enabled.

IMAPS port 993
IMAP STARTTLS port 143
Local cache ~/.imapsync/
Per-account log LOG_imapsync/

Installing IMAPSync

The version in the Debian/Ubuntu repositories is old (usually 1.7xx). I recommend compiling from source — the 2.x version has important fixes for Microsoft 365 and Gmail and better handling of transient errors.

01

Install Perl dependencies and build tools:

sudo apt update
sudo apt install -y \
  git make cpanminus libio-socket-ssl-perl libnet-ssleay-perl \
  libcrypt-ssleay-perl libmail-imapclient-perl libdigest-md5-file-perl \
  libdist-checkconflicts-perl libfile-copy-recursive-perl \
  libio-tee-perl libjson-webtoken-perl libreadonly-perl \
  libregexp-common-perl libsys-meminfo-perl libterm-readkey-perl \
  libtest-fatal-perl libtest-mockobject-perl libtest-pod-perl \
  libtest-requires-perl libtest-deep-perl libunicode-string-perl \
  liburi-perl libencode-imaputf7-perl libfile-tail-perl \
  libdata-uniqid-perl libproc-processtable-perl libtime-hires-perl

The list looks long but each module covers a real case — SSL, IMAP UTF-7 encoding, unique ID parsing, live log tailing. Skipping any of them will result in a cryptic error in the middle of the migration.

02

Clone and install IMAPSync:

git clone https://github.com/imapsync/imapsync.git
cd imapsync
sudo make install
imapsync --version

The final command should return something like Imapsync 2.255 or higher. If you see “Can’t locate … in @INC”, a Perl module is missing — identify the name from the error and install it via sudo cpanm Module::Name.

Dry-run to validate credentials and folder mapping

Before copying any message, run in dry-run mode. This lists folders on both sides, validates login, and shows how many messages exist without transferring anything.

03

Create a migration.sh file with the account variables:

#!/bin/bash
imapsync \
  --host1 mail.source.com \
  --port1 993 \
  --ssl1 \
  --user1 [email protected] \
  --password1 'SOURCE_PASSWORD' \
  --host2 mail.destination.com \
  --port2 993 \
  --ssl2 \
  --user2 [email protected] \
  --password2 'DESTINATION_PASSWORD' \
  --dry \
  --justfolders

The --dry --justfolders combination lists folders on both sides without touching messages. It’s the quickest way to detect a mismatch (e.g., the source has “INBOX.Sent” and the destination expects “Sent Items”).

04

Run it and analyze the output:

chmod +x migration.sh
./migration.sh 2>&1 | tee dry-run.log

Look in the log for lines starting with Host1 folders: and Host2 folders:. If there’s a discrepancy (e.g., source uses dot hierarchy “INBOX.Work” and destination uses slash “INBOX/Work”), you’ll need --regextrans2 's,INBOX\.,INBOX/,g' on the real run.

Microsoft 365 and Gmail require app password

If the source or destination is Microsoft 365, Gmail, or iCloud with 2FA enabled, the normal password will NOT work over IMAP. Generate an “app password” in the provider’s panel before running. For Gmail, enable IMAP under Settings → Forwarding and POP/IMAP first.

First sync (pre-cutover)

The first run is the heavy one — it copies everything. It can take hours for large accounts. Run it inside screen or tmux so you don’t lose progress if the SSH session drops.

05

Start a tmux session to run in the background:

tmux new -s migration

If the SSH connection drops, reconnect and run tmux attach -t migration to keep watching.

06

Run the full sync by removing --dry --justfolders:

imapsync \
  --host1 mail.source.com --port1 993 --ssl1 \
  --user1 [email protected] --password1 'SOURCE_PASSWORD' \
  --host2 mail.destination.com --port2 993 --ssl2 \
  --user2 [email protected] --password2 'DESTINATION_PASSWORD' \
  --automap \
  --syncinternaldates \
  --useuid \
  --addheader \
  --logfile LOG_imapsync/user.log

The --automap flag automatically maps Sent/Sent Items/INBOX.Sent between different providers. --syncinternaldates preserves the original date. --useuid makes the sync idempotent — running again won’t duplicate anything. --addheader adds X-Imapsync to messages so you can audit later which ones were migrated.

Skip folders that don't matter

For accounts with huge Trash or Junk, add --exclude '^Trash$' --exclude '^Junk$' --exclude '^Spam$'. It saves hours and gigabytes on old accounts. Sent and Drafts are usually WORTH migrating.

DNS switch and cutover

With the first sync complete, it’s time to flip the MX. New messages will arrive at the destination server and the old one receives zero new traffic. The second IMAPSync run will pick up only the messages that arrived at the old one between the start of the first sync and when DNS propagated — typically 30-120 minutes of messages.

07

Lower the MX record TTL at the DNS provider to 300 seconds (5 min) BEFORE making any change — ideally 24h beforehand. This ensures fast propagation when you switch the destination. If the TTL was 3600 or 86400, everyone will keep seeing the old value until that time expires.

# Check current TTL before changing
dig +short mx domain.com
dig +nocmd +noall +answer mx domain.com
08

Switch the MX to the new server. In the DNS panel:

  • Type: MX
  • Priority: 10 (or whichever you use)
  • Value: mail.destination.com. (with trailing dot)
  • TTL: 300

Wait 5-10 minutes and confirm propagation:

dig +short mx domain.com @1.1.1.1
dig +short mx domain.com @8.8.8.8

Both should return the new MX. If one returns the old one, the resolver cache hasn’t expired yet — wait a few more minutes.

09

Run IMAPSync a second time — same command as Step 06. Because of --useuid, it will only copy messages that arrived at the source between the first and second run. Typically fast (2-10 minutes).

./migration.sh 2>&1 | tee LOG_imapsync/cutover.log

At the end of the log, look for Total bytes transferred. On a successful incremental sync, this value is small (a few MB at most).

Verification

The basic validation is counting messages on both sides. If they match, the migration is intact.

10

Compare counts on source and destination:

imapsync \
  --host1 mail.source.com --port1 993 --ssl1 \
  --user1 [email protected] --password1 'SOURCE_PASSWORD' \
  --host2 mail.destination.com --port2 993 --ssl2 \
  --user2 [email protected] --password2 'DESTINATION_PASSWORD' \
  --justconnect

Then run with --dry to see the final report of counted messages in each folder on both sides:

./migration.sh --dry

At the end, look for Folder Host1/Host2 messages — each pair should match. A difference of ≤ 5 messages in a 10k+ folder is normal (messages that arrived between the last sync and the verification).

Troubleshooting

”Too many connections” error during parallel migration

When you run IMAPSync for multiple accounts in parallel, some providers limit connections per IP. Reduce concurrency to 2-3 simultaneous processes and add --maxbytespersecond 5000000 (5 MB/s) to give the server some breathing room.

Duplicated messages after the second run

This indicates --useuid isn’t working — usually because the destination rewrites UIDs when copying messages. Solution: use --useheader Message-Id instead of --useuid. Slower, but deduplicates by header instead of native UID.

Gmail “Sent” folder appears duplicated

Gmail has a quirk: every message appears in “[Gmail]/All Mail” AND in “INBOX” or “Sent”. Add --exclude '^\[Gmail\]/All Mail$' or you’ll migrate everything twice.

Don't delete the source account yet

Keep the source mailbox active for at least 7-14 days after cutover. Messages can show up late (servers that cached the old MX for longer, retry systems from other providers). Running IMAPSync periodically during these days guarantees capture.

Next steps

With the migration done, consider automating checks to detect post-cutover issues:

  • Configure SPF, DKIM, and DMARC on the new MX before taking the old one offline
  • Monitor bounce rate in the first 48h to catch bad IP reputation on the new server
  • Document the generated app password and revoke it after 30 days
  • If you’re migrating dozens of accounts, build a script with GNU parallel to run in batch
  • Configure automatic backup of the new mailbox (rsnapshot, restic, or similar) for an independent restore point from the provider

If you’re consolidating email on your own infrastructure to control the MX and backups, a Hostini VPS with a dedicated IP and SMTP/IMAP ports open is the starting point — without port 25 blocking for outbound and with reverse DNS configurable from the panel.

Frequently asked questions

Does IMAPSync preserve the original message dates?

Yes. IMAPSync replicates each message's INTERNALDATE by default, so the receive date on the destination server matches the source. If the destination rejects INTERNALDATE (some providers do), use the --syncinternaldates 0 flag and accept that the date becomes the time of migration.

Can I run IMAPSync in parallel for multiple accounts?

You can, but with caution. Each process opens 2 simultaneous IMAP connections (one on each side), and many providers limit connections per IP. Start with 3-4 parallel processes and watch for 'Too many connections' errors in the log. GNU parallel is the simplest way to orchestrate in batch.

What happens to new messages that arrive during the migration?

While the MX still points to the old server, messages arrive there. You run IMAPSync once (full sync), switch the MX to the new server, and run it a second time incrementally. The second run only copies the delta — messages that arrived between the two runs. That's why the cutover always has 2 rounds.

How do I handle folder size limit differences between providers?

Some providers limit Trash, Junk, or Sent to a few GB. Use --exclude '^Trash$' --exclude '^Junk$' to skip folders that don't matter, or --maxsize 25000000 to ignore messages larger than 25 MB. The --justfolders flag on the first run lists every folder so you can decide what to migrate.

Do I need to disable two-factor authentication on the source account?

It depends on the provider. Gmail, Outlook 365, and iCloud require an app password when 2FA is on — plain IMAP doesn't pass through interactive 2FA. Generate the app password in the provider's panel, use it in --password1, and revoke it after migration. Providers with standard IMAP (cPanel, Plesk, Zimbra) accept the normal password.

Does IMAPSync work with Microsoft 365 and Gmail?

Yes. For Microsoft 365, use --host2 outlook.office365.com --port2 993 --ssl2 --authmech2 LOGIN. For Gmail, enable IMAP in account settings, generate an app password, and use --host2 imap.gmail.com --port2 993 --ssl2. Both have throughput limits — expect ~50 GB per day per account.

Topics:
Next steps Ryzen cloud with NVMe storage and always-on DDoS protection.Go live on a Hostini VPS →
Was this tutorial helpful?
Chat on WhatsApp