Interactive, cross-platform tool to inspect, migrate and verify IMAP
mailboxes. Works on Windows, macOS and Linux with plain Python 3.8+;
its single third-party dependency (rich, for the colorized UI) is
installed automatically on first run.
Double-click the launcher for your system, it installs Python if needed and starts the tool:
| Operating system | Script |
|---|---|
| Windows | start-windows.bat (installs Python via winget or the official installer) |
| macOS | start-macos.command (installs Homebrew + Python if missing) |
| Linux | start-linux.sh (installs Python via apt/dnf/pacman/zypper) |
Or run it directly if you already have Python 3:
python3 imapsynchronizer.py
On the first run the tool installs its only dependency (rich) by
itself; everything after that works the same on Windows, macOS and
Linux.
No configuration files needed:
- Start the tool (double-click the launcher, or run
python3 imapsynchronizer.py). - Choose option 2, Copy messages from source to destination.
- Type the source account details: server, port (press Enter for 993), username and password. Then the same for the destination.
- Pick which folders to copy, exclusions accept list numbers and/or
full folder names (press Enter for all), choose the destination
layout (sandbox under
_IMAP-Synchronizer/, or merge into the same folders) and, optionally, a date range. - Review the migration plan on screen and confirm. Progress is shown folder by folder; at the end every message is re-verified and a summary table is displayed (exportable to TXT).
- Find the copied mail in the destination account under
_IMAP-Synchronizer/<user>_<host>/…. Existing mail in the destination is never touched.
Interrupted, or not sure it finished? Run it again, nothing is ever copied twice; only what is still missing is transferred. To double-check a migration later, use option 3 (Verify source against destination). To see what an account contains before doing anything, use option 1, it only reads.
The main menu offers:
-
List account folders and message counts, connects to an account, shows the full folder tree with per-folder message counts, and can export the listing to
reports/<account>_<ddmmyy>_<hhmm>.txt. -
Copy messages from source to destination, after testing both connections it analyzes the source, shows a migration plan and asks for confirmation. Two destination layouts are offered:
-
Sandbox (default): messages are copied into
_IMAP-Synchronizer/<user>_<host>/<original folder structure>, so nothing outside that subtree is ever touched. -
Merge: messages are added into the destination's own folders (INBOX→INBOX, Sent→Sent, …), mixing with its existing mail, for recovering an old account inside a live one. Standard folders are paired by their special-use role when both servers declare it (RFC 6154), so
Sentmerges intoSent Itemsor[Gmail]/Enviados; other folders keep their relative path, andINBOX.-style namespaces are translated automatically. Existing destination mail is never modified and does not affect verification, only the presence of every source message is checked. -
Folders are created as needed. Names with spaces or non-ASCII characters (ñ, accents, …) are fully supported, on the wire IMAP requires folder names in modified UTF-7 (RFC 3501), and the tool encodes/decodes them transparently, so you always see the real name.
-
Resumable and duplicate-safe: messages already present in the destination (identified by
Message-ID, or a header hash when a message has none) are skipped, so re-running an interrupted migration only copies what is still missing. -
Optional date filter: copy only messages within a from/to date range, entered as ISO
yyyy-mm-dd(dd/mm/yyyyis also accepted). Dates are matched against the server arrival date (IMAPINTERNALDATE); on the wire the protocol-mandatedd-Mon-yyyyformat with English month names is used, independent of the system locale. -
If the account is on Gmail, the virtual "All Mail" folder is detected (in any language, via its
\Allflag) and the tool offers to exclude it, copying it would duplicate the whole mailbox. -
Flags (read/answered/flagged) and original message dates are preserved; the source mailbox is opened read-only and never modified.
-
After copying, every folder is re-checked message-by-message and a verification table is shown; the summary can be exported to TXT.
-
An audit log per migration is written to
logs/with per-folder summaries (total/copied/already present/failed), failures and the verification result. -
The final report is also stored as an e-mail inside the destination account, in the
_IMAP-Synchronizerfolder (in both layouts, merge mode creates that folder just for the record), signed with the program name, version, author and repository: a permanent trace of the migration kept next to the mail itself. -
Built to survive failures: dropped connections are re-established and the operation retried automatically; a folder or message that cannot be processed is reported and skipped without aborting the rest; if the destination rejects a message's custom keyword flags, it is stored with standard flags instead (logged). Only a dead connection (10 consecutive failures) stops the run, and re-running resumes where it left off.
-
-
Verify source against destination, full comparison of two accounts (folder structure and message sets), as a mirror (same paths), against the
_IMAP-Synchronizer/…subtree of a previous migration, or as a merge check (folders mapped like merge mode; verifies everything from the source exists in the destination while ignoring the destination's own extra mail). Reports missing/extra messages per folder and can export a TXT report. Note: the comparison always covers everything, so after a migration restricted by a date filter it will legitimately report the messages outside that range as missing. -
Batch copy multiple accounts from a CSV file, one migration per CSV row, run sequentially without prompts, with a final summary table and report. The menu option creates a template CSV to fill in. Details and examples in [Unattended use](#unattended-use-scheduled-migrations).
-
Create an unattended job file, writes a JSON job description (accounts, exclusions, date range) for scheduled runs; passwords can be taken from environment variables instead of being stored. Details and examples in [Unattended use](#unattended-use-scheduled-migrations).
-
Manage saved connection profiles, server/port/username can be saved to
imapsynchronizer_profiles.jsonnext to the script to avoid retyping them. Passwords are never stored; they are asked for on every run (hidden input).
Menu options 4 and 5 cover migrations that run without any prompts:
they connect, copy, verify, save their TXT report automatically and
return an exit code usable from schedulers (cron, launchd, Windows
Task Scheduler):
python3 imapsynchronizer.py --job migration_job.json # one migration
python3 imapsynchronizer.py --batch batch_migrations.csv # many migrations
Exit codes: 0 = everything verified OK · 1 = error · 2 = differences found.
Both are resumable exactly like the interactive copy: re-running only copies what is still missing, so a nightly schedule keeps source and destination in sync. The Gmail-style "all mail" virtual folder is always excluded automatically in these modes.
Menu option 5 asks a few questions and writes the JSON file for you.
Alternatively, copy the shipped template
[migration_job.example.json](migration_job.example.json) and edit it.
Job files may contain comment lines starting with # (the template uses
them to document every field):
{
"source": { "host": "imap.old-server.com", "port": 993,
"user": "user@old-server.com", "password_env": "SRC_IMAP_PASS" },
"destination": { "host": "imap.new-server.com", "port": 993,
"user": "user@new-server.com", "password_env": "DST_IMAP_PASS" },
"exclude_folders": ["Trash", "Junk"],
"date_from": "",
"date_to": "",
"allow_insecure_ssl": false
}| Field | Required | Meaning |
|---|---|---|
host, user |
yes | IMAP server and account name |
port |
no | defaults to 993 (implicit TLS) |
password_env |
one of the two | name of an environment variable holding the password (recommended, the file stays credential-free) |
password |
one of the two | password in plain text (protect the file!) |
exclude_folders |
no | full folder names to skip, case-insensitive (e.g. ["Trash", "[Gmail]/Spam"]); use [] or omit the field to exclude nothing |
date_from, date_to |
no | copy only messages that arrived in this inclusive range, yyyy-mm-dd; empty = no limit |
merge |
no | true = copy into the destination's own folders (merge mode) instead of _IMAP-Synchronizer/ |
allow_insecure_ssl |
no | defaults to true (accept invalid/self-signed TLS certificates); set false to require a valid certificate |
Run it, providing the passwords through the environment:
# Linux / macOS
export SRC_IMAP_PASS='old-secret' DST_IMAP_PASS='new-secret'
python3 imapsynchronizer.py --job migration_job.json:: Windows *(cmd)*
set SRC_IMAP_PASS=old-secret
set DST_IMAP_PASS=new-secret
python imapsynchronizer.py --job migration_job.jsonSchedule it, every night at 03:00:
# Linux / macOS: crontab -e
0 3 * * * SRC_IMAP_PASS=... DST_IMAP_PASS=... /usr/bin/python3 /path/to/imapsynchronizer.py --job /path/to/migration_job.json:: Windows Task Scheduler (JSON with passwords)
schtasks /Create /SC DAILY /ST 03:00 /TN "IMAP sync" ^
/TR "cmd /c python C:\path\imapsynchronizer.py --job C:\path\migration_job.json":: Windows Task Scheduler (JSON without passwords)
schtasks /Create /SC DAILY /ST 03:00 /TN "IMAP sync" ^
/TR "cmd /c set SRC_IMAP_PASS=... && set DST_IMAP_PASS=... && python C:\path\imapsynchronizer.py --job C:\path\migration_job.json"One migration per row, run sequentially, with a final summary table and
a batch report. Menu option 4 offers to create a template CSV at the
path you give it; the same content ships as
[batch_migrations.example.csv](batch_migrations.example.csv):
src_host,src_port,src_user,src_password,dst_host,dst_port,dst_user,dst_password,date_from,date_to,exclude,insecure
imap.old-server.com,993,user1@old.com,"pass,word1",imap.new-server.com,993,user1@new.com,"secret2",,,Trash|Junk|Example_exclude,
imap.old-server.com,993,user2@old.com,secret3,imap.new-server.com,993,user2@new.com,secret4,2020-01-01,2025-12-31,,| Column | Required | Meaning |
|---|---|---|
src_host, src_user, src_password |
yes | source server, account, password |
dst_host, dst_user, dst_password |
yes | destination server, account, password |
src_port, dst_port |
no | default 993 |
date_from, date_to |
no | inclusive arrival-date range, yyyy-mm-dd |
exclude |
no | folder names to skip, separated by | |
insecure |
no | defaults to yes (accept invalid TLS certificates); no = require a valid certificate for that row |
merge |
no | yes = copy into the destination's own folders (merge mode) |
Notes:
- Passwords containing commas must be quoted CSV-style:
"pass,word". - Semicolon-delimited CSVs (as saved by some Excel locales) are detected automatically.
- The CSV contains passwords in plain text, restrict who can read it.
- Run from the menu (option 4, asks for confirmation) or scheduled:
python3 imapsynchronizer.py --batch batch_migrations.csv.
Passwords may contain any characters (ñ, commas, semicolons, quotes,
symbols, spaces). Everything you type at the password prompt is taken
literally, nothing is split or interpreted. You may optionally wrap the
password in quotes ("like this"); one matching surrounding pair is
removed, which protects passwords with leading or trailing spaces.
Credentials containing non-ASCII characters are transmitted with
AUTHENTICATE PLAIN (UTF-8 over base64), since the classic IMAP
LOGIN command is ASCII-only.
- Server addresses are cleaned automatically: pasting
imap://mail.example.com,mail.example.com:993or a trailing slash works, the host (and port, when present) is extracted for you. - Port 993 uses implicit TLS; other ports use STARTTLS when the server offers it (a warning is logged for unencrypted connections).
- Insecure SSL is the default: certificate verification is skipped
unless you answer No to the per-connection question in the menus
(or set
allow_insecure_ssl/insecureto false in job/batch files). The connection is still encrypted, but the server's identity is not verified, answer No when working over untrusted networks. - All exported TXT reports are written to a
reports/folder next to the script (created automatically), so they never mix with the program files. - Every start also writes a session error log to
logs/(one file per run, plus one audit log per migration): connection failures, failed messages or folders, unexpected errors, and transient errors that were retried and resolved (e.g. a timeout followed by a successful reconnect), everything an admin needs to debug afterwards.
python3 imapsynchronizer.py --selftest runs the built-in unit checks
for the UTF-7 codec, LIST/FETCH response parsing, duplicate detection,
password quote handling, date criteria, batch CSV parsing and tree
rendering.