Dockerized multi-account IMAP synchronization tool with OAuth2 support for Office 365, Hotmail, Outlook.com, and Gmail.
Microsoft retired Basic Authentication for Exchange Online (IMAP/SMTP), and Google is pushing XOAUTH2 as the only authentication method. This breaks traditional IMAP sync tools that rely on username/password.
Mailcow's built-in sync job and many other tools don't support OAuth2, making it impossible to migrate emails from Hotmail/Outlook.com/Gmail into self-hosted mail servers.
This project solves that problem by combining:
- imapsync — battle-tested IMAP migration tool
- imapsync's OAuth2 helper — token authorization and refresh for Microsoft and Google accounts
- Docker — portable, reproducible, easy to deploy
| Feature | Status |
|---|---|
| Office 365 / Hotmail / Outlook.com sync | ✅ OAuth2 |
| Gmail / Google Workspace sync | ✅ OAuth2 |
| Any IMAP server (password auth) | ✅ |
| Multiple accounts in one container | ✅ Unlimited |
| Incremental sync (only new emails) | ✅ |
| Folder structure preservation | ✅ |
| Scheduled sync via interval | ✅ |
| Healthcheck endpoint | ✅ |
| One-shot sync mode | ✅ |
| Dry run mode | ✅ |
Delete propagation (--delete2) |
✅ |
| Custom imapsync flags per account | ✅ |
| Persistent token storage | ✅ |
| Automatic token refresh | ✅ |
| SMTP sync notifications | ✅ Optional |
| Interval or cron scheduling | ✅ |
| Provider | Auth Method | Token Lifetime | Auto-Refresh |
|---|---|---|---|
| Hotmail / Outlook.com / Personal Microsoft | OAuth2 | 90 days (hard limit) | ✅ until day 90 |
| Office 365 / Microsoft 365 (Work/School) | OAuth2 | 90 days (configurable by admin) | ✅ indefinitely |
| Gmail / Google Workspace | OAuth2 | 7 days (testing) / 90 days (verified) | ✅ |
| Other IMAP | Password | N/A | N/A |
⚠️ Personal Microsoft accounts (hotmail.com, outlook.com, live.com) have a hard 90-day refresh token limit. You must reauthorize every ~80 days. Work/school accounts can have indefinite tokens if configured by Azure AD admin.
git clone https://github.com/yourusername/email-sync-oauth2.git
cd email-sync-oauth2
cp .env.example .env
nano .env # edit your settingsRun the container interactively for each account:
# For Office 365 / Hotmail (Account 1)
docker compose run --rm imapsync authorize 1
# → Paste URL in browser, log in to Microsoft, click "Allow"
# → Token saved to ./tokens/user1_hotmail.token
# For Account 2
docker compose run --rm imapsync authorize 2
# → Same process for this accountdocker compose up -ddocker logs -f email-sync-oauth2
tail -f logs/account1.logAll configuration is done through environment variables. Edit .env or pass directly to docker-compose.yml.
| Variable | Default | Description |
|---|---|---|
NUM_ACCOUNTS |
1 |
Number of accounts to sync |
SYNC_INTERVAL |
21600 (6h) |
Seconds between sync cycles |
SYNC_CRON |
empty | Optional five-field cron expression; overrides SYNC_INTERVAL |
ONCE |
false |
Run once and exit (no daemon) |
TOKEN_DIR |
/tokens |
Directory inside container for token files |
LOG_DIR |
/var/log/imapsync |
Directory inside container for log files |
| Variable | Required | Description |
|---|---|---|
ACCOUNT_N_SOURCE_HOST |
✅ | Source IMAP server (e.g., outlook.office365.com) |
ACCOUNT_N_SOURCE_USER |
✅ | Source email address |
ACCOUNT_N_SOURCE_AUTH |
✅ | oauth2 or password |
ACCOUNT_N_SOURCE_PROVIDER |
If OAuth2 | office365, hotmail, outlook, gmail, google |
ACCOUNT_N_SOURCE_TOKEN |
If OAuth2 | Path to token file (e.g., /tokens/account1.token) |
ACCOUNT_N_SOURCE_PASS |
If password | Source password |
ACCOUNT_N_DEST_HOST |
✅ | Destination IMAP server (e.g., mail.hoelee.com) |
ACCOUNT_N_DEST_USER |
✅ | Destination email/username |
ACCOUNT_N_DEST_PASS |
✅ | Destination password |
ACCOUNT_N_DELETE |
No | true = delete emails on destination not on source |
ACCOUNT_N_DRY_RUN |
No | true = log only, don't actually sync |
ACCOUNT_N_EXTRA_OPTS |
No | Additional imapsync flags (optional) |
OAuth2 is required only for providers that require it. Any normal IMAP source
can still use a password by setting ACCOUNT_N_SOURCE_AUTH=password and
ACCOUNT_N_SOURCE_PASS. The destination currently uses password
authentication for every provider.
ACCOUNT_3_SOURCE_HOST=imap.example.com
ACCOUNT_3_SOURCE_USER=source@example.com
ACCOUNT_3_SOURCE_AUTH=password
ACCOUNT_3_SOURCE_PASS=source_password
ACCOUNT_3_DEST_HOST=mail.example.net
ACCOUNT_3_DEST_USER=destination@example.net
ACCOUNT_3_DEST_PASS=destination_passwordBy default, a batch runs immediately when the container starts and then every
six hours (SYNC_INTERVAL=21600). Set the interval in seconds to use a
different fixed frequency:
SYNC_INTERVAL=3600
SYNC_CRON=For calendar-based scheduling, set SYNC_CRON to a standard five-field cron
expression. It overrides SYNC_INTERVAL.
# Every six hours, on the hour
SYNC_CRON=0 */6 * * *
# Every day at 02:30
# SYNC_CRON=30 2 * * *All ACCOUNT_N_* variables in .env are passed to the container. To add an
account, copy an account block, use the next number, and increase
NUM_ACCOUNTS—there is no two-account Compose limit.
Notifications are off by default. Set NOTIFY_MODE to one of these values:
| Value | When an email is sent |
|---|---|
batch_success |
Once after every batch where every configured account succeeds |
account_success |
Once for each account that succeeds |
failure |
Once after a batch where one or more accounts fail |
Configure SMTP whenever notifications are enabled:
NOTIFY_MODE=batch_success
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=alerts@example.com
SMTP_PASS=your_smtp_password
SMTP_FROM=alerts@example.com
SMTP_TO=admin@example.com,ops@example.com
SMTP_TLS=starttlsSMTP_TLS accepts starttls (normally port 587), tls (implicit TLS,
normally port 465), or none. If a notification cannot be delivered, the
container writes the reason to logs/notification.log, logs the failure to the
container output, and becomes unhealthy until a notification sends
successfully.
NUM_ACCOUNTS=1
SYNC_INTERVAL=21600
ACCOUNT_1_SOURCE_HOST=outlook.office365.com
ACCOUNT_1_SOURCE_USER=you@hotmail.com
ACCOUNT_1_SOURCE_AUTH=oauth2
ACCOUNT_1_SOURCE_PROVIDER=office365
ACCOUNT_1_SOURCE_TOKEN=/tokens/you_hotmail.token
ACCOUNT_1_DEST_HOST=mail.hoelee.com
ACCOUNT_1_DEST_USER=you@mailcow.com
ACCOUNT_1_DEST_PASS=your_mailcow_passwordACCOUNT_1_SOURCE_HOST=imap.gmail.com
ACCOUNT_1_SOURCE_USER=you@gmail.com
ACCOUNT_1_SOURCE_AUTH=oauth2
ACCOUNT_1_SOURCE_PROVIDER=gmail
ACCOUNT_1_SOURCE_TOKEN=/tokens/you_gmail.tokenNUM_ACCOUNTS=3
# Account 1: Hotmail -> Mailcow
ACCOUNT_1_SOURCE_HOST=outlook.office365.com
ACCOUNT_1_SOURCE_USER=alice@hotmail.com
ACCOUNT_1_SOURCE_AUTH=oauth2
ACCOUNT_1_SOURCE_PROVIDER=office365
ACCOUNT_1_SOURCE_TOKEN=/tokens/alice_hotmail.token
ACCOUNT_1_DEST_HOST=mail.hoelee.com
ACCOUNT_1_DEST_USER=alice@mailcow.com
ACCOUNT_1_DEST_PASS=password1
# Account 2: Gmail -> Mailcow
ACCOUNT_2_SOURCE_HOST=imap.gmail.com
ACCOUNT_2_SOURCE_USER=bob@gmail.com
ACCOUNT_2_SOURCE_AUTH=oauth2
ACCOUNT_2_SOURCE_PROVIDER=gmail
ACCOUNT_2_SOURCE_TOKEN=/tokens/bob_gmail.token
ACCOUNT_2_DEST_HOST=mail.hoelee.com
ACCOUNT_2_DEST_USER=bob@mailcow.com
ACCOUNT_2_DEST_PASS=password2
# Account 3: Legacy IMAP -> Mailcow (password auth)
ACCOUNT_3_SOURCE_HOST=imap.oldprovider.com
ACCOUNT_3_SOURCE_USER=carol@oldprovider.com
ACCOUNT_3_SOURCE_AUTH=password
ACCOUNT_3_SOURCE_PASS=oldpassword
ACCOUNT_3_DEST_HOST=mail.hoelee.com
ACCOUNT_3_DEST_USER=carol@mailcow.com
ACCOUNT_3_DEST_PASS=password3# .env
ONCE=true
NUM_ACCOUNTS=1
# Add to crontab:
# 0 */6 * * * cd /opt/email-sync-oauth2 && docker compose up --abort-on-container-exitA Makefile is included for convenience:
# Build and start
make build
make up
# Check status and health
make status
make health
# Authorize an account (requires browser)
make authorize ACCOUNT=1
# Force sync now
make sync ACCOUNT=1
# Force a batch for every configured account
make sync-all
# Follow logs
make logs
# Enter container shell
make shell
# Run once and exit (one-shot mode)
make once
# Clean up
make cleandocker compose exec imapsync sync.sh sync 1docker compose run --rm imapsync authorize 1docker compose exec imapsync sync.sh shelldocker compose exec imapsync sync.sh imapsync \
--host1 outlook.office365.com --user1 you@hotmail.com \
--oauthaccesstoken1 /tokens/you_hotmail.token \
--host2 mail.hoelee.com --user2 you@mailcow.com \
--password2 yourpassword \
--ssl1 --ssl2 --dryThe container includes a Docker healthcheck based on a heartbeat mechanism:
- A background process writes a heartbeat file every 60 seconds
- Docker checks if the heartbeat is less than 5 minutes old
- If no heartbeat, the container is marked unhealthy and can be auto-restarted by orchestrators
It also becomes unhealthy if a configured SMTP notification fails to send. The
reason is logged in logs/notification.log and in the container output.
The heartbeat confirms that the sync daemon is running; it does not mean every mailbox was able to authenticate or sync. If an OAuth refresh token expires or is revoked, the container log includes a reauthorization command, for example:
Account 1: OAuth2 authorization is expired, revoked, or rejected by office365. Reauthorize with: docker compose run --rm imapsync authorize 1
After completing the browser authorization, start that account immediately with:
docker compose exec imapsync sync.sh sync 1# Check health status
docker inspect --format='{{.State.Health.Status}}' email-sync-oauth2
# View healthcheck logs (useful for debugging)
docker inspect --format='{{json .State.Health}}' email-sync-oauth2 | jq
# Watch live heartbeat
docker exec email-sync-oauth2 cat /var/log/imapsync/.heartbeat./logs/
├── .heartbeat # Healthcheck timestamp (updated every 60s)
├── account1.log # Full imapsync output for account 1
├── account1.last_success # Unix timestamp of last successful sync
├── account1.last_failure # Unix timestamp of last failed sync
├── account2.log
├── notification.log # SMTP notification delivery output
├── .notification_failed # Present only while notification delivery is failing
└── ...
# When did account 1 last succeed?
date -d @"$(cat logs/account1.last_success)"
# When did it last fail?
date -d @"$(cat logs/account1.last_failure)"For personal Microsoft accounts (90-day token limit), set a calendar reminder or monitoring check:
# Check if reauthorization is needed (token file older than 80 days)
find ./tokens -name "*.token" -mtime +80 -ls┌─────────────────────────────────────────────────────┐
│ Docker Container │
│ │
│ ┌──────────────┐ ┌──────────────────────────┐ │
│ │ sync.sh │────▶│ imapsync (per account) │ │
│ │ (daemon) │ └──────────┬───────────────┘ │
│ │ │ │ │
│ │ Interval: │ ┌──────────▼───────────────┐ │
│ │ 6h default │ │ OAuth2 token helpers │ │
│ │ │ │ oauth2_office365_with_ │ │
│ │ Healthcheck │ │ imap / oauth2_imap │ │
│ └──────────────┘ └──────────┬───────────────┘ │
│ │ │
│ ┌──────────▼───────────────┐ │
│ │ /tokens/ (mounted) │ │
│ │ account1.token │ │
│ │ account2.token │ │
│ └──────────────────────────┘ │
│ │
│ ┌────────────────────────────────────────────────┐ │
│ │ /var/log/imapsync/ (mounted) │ │
│ │ account1.log, account1.last_success, etc. │ │
│ └────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────┘
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ Source IMAP │ │ Dest IMAP │
│ (OAuth2) │ │ (password) │
│ outlook. │ │ mail.hoelee │
│ office365 │ │ .com │
└──────────────┘ └──────────────┘
Q: Do I need to reauthorize every 90 days? A: Only for personal Microsoft accounts (hotmail.com, outlook.com, live.com). Work/school Microsoft 365 accounts can have indefinite tokens. Gmail tokens last 7-90 days depending on app verification status.
Q: Can I use this for a one-time migration?
A: Yes! Set ONCE=true in .env and run docker compose up.
Q: Is the sync incremental? A: Yes. imapsync only transfers emails that don't already exist on the destination.
Q: Can I sync from any source to any destination? A: Any IMAP-compatible server. The source needs OAuth2 if it's Microsoft/Google. The destination always uses password auth.
Q: How many accounts can I sync?
A: Unlimited. Just increase NUM_ACCOUNTS and add ACCOUNT_N_* variables.
Q: Can I use this with Synology DSM?
A: Yes. Point Docker volume paths to /volume1/docker/email-sync-oauth2/.
Q: What if my source is Exchange with ActiveSync? A: ActiveSync doesn't work well with mailcow (Microsoft disabled basic auth for ActiveSync). Use IMAP with OAuth2 instead.
docker build -t email-sync-oauth2:latest .
docker run -d \
--name email-sync-oauth2 \
--restart unless-stopped \
-e NUM_ACCOUNTS=1 \
-e SYNC_INTERVAL=21600 \
-e ACCOUNT_1_SOURCE_HOST=outlook.office365.com \
-e ACCOUNT_1_SOURCE_USER=you@hotmail.com \
-e ACCOUNT_1_SOURCE_AUTH=oauth2 \
-e ACCOUNT_1_SOURCE_PROVIDER=office365 \
-e ACCOUNT_1_SOURCE_TOKEN=/tokens/you_hotmail.token \
-e ACCOUNT_1_DEST_HOST=mail.hoelee.com \
-e ACCOUNT_1_DEST_USER=you@mailcow.com \
-e ACCOUNT_1_DEST_PASS=yourpassword \
-v $(pwd)/tokens:/tokens \
-v $(pwd)/logs:/var/log/imapsync \
email-sync-oauth2:latestMIT License. See LICENSE for details.
- imapsync by Gilles Lamiral — the core migration engine
- imapsync OAuth2 helper — OAuth2 authorization and refresh
Pull requests welcome! Please test with your own accounts before submitting.
- Fork the repo
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request