526 lines
17 KiB
Markdown
526 lines
17 KiB
Markdown
# Email Sync OAuth2
|
|
|
|
Dockerized multi-account IMAP synchronization tool with **OAuth2 support** for Office 365, Hotmail, Outlook.com, and Gmail.
|
|
|
|
## Problem This Solves
|
|
|
|
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](https://imapsync.lamiral.info/)** — battle-tested IMAP migration tool
|
|
- **imapsync's OAuth2 helper** — token authorization and refresh for Microsoft and Google accounts
|
|
- **Docker** — portable, reproducible, easy to deploy
|
|
|
|
## Features
|
|
|
|
| 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 | ✅ |
|
|
|
|
## Authentication Requirements
|
|
|
|
| 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.
|
|
|
|
---
|
|
|
|
## Quick Start
|
|
|
|
### 1. Clone and configure
|
|
|
|
```bash
|
|
git clone https://github.com/yourusername/email-sync-oauth2.git
|
|
cd email-sync-oauth2
|
|
cp .env.example .env
|
|
nano .env # edit your settings
|
|
```
|
|
|
|
### 2. First-time OAuth authorization (requires browser)
|
|
|
|
Run the container interactively for each account:
|
|
|
|
```bash
|
|
# 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 account
|
|
```
|
|
|
|
### 3. Start syncing
|
|
|
|
```bash
|
|
docker compose up -d
|
|
```
|
|
|
|
### 4. Monitor
|
|
|
|
```bash
|
|
docker logs -f email-sync-oauth2
|
|
tail -f logs/account1.log
|
|
```
|
|
|
|
---
|
|
|
|
## Configuration
|
|
|
|
All configuration is done through environment variables. Edit `.env` or pass directly to `docker-compose.yml`.
|
|
|
|
### Global Variables
|
|
|
|
| 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 |
|
|
|
|
### Per-Account Variables (ACCOUNT_1_, ACCOUNT_2_, ...)
|
|
|
|
| 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) |
|
|
|
|
### Password authentication is still supported
|
|
|
|
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.
|
|
|
|
```env
|
|
ACCOUNT_3_SOURCE_HOST=imap.example.com
|
|
ACCOUNT_3_SOURCE_USER=[email protected]
|
|
ACCOUNT_3_SOURCE_AUTH=password
|
|
ACCOUNT_3_SOURCE_PASS=source_password
|
|
|
|
ACCOUNT_3_DEST_HOST=mail.example.net
|
|
ACCOUNT_3_DEST_USER=[email protected]
|
|
ACCOUNT_3_DEST_PASS=destination_password
|
|
```
|
|
|
|
### Scheduling
|
|
|
|
By 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:
|
|
|
|
```env
|
|
SYNC_INTERVAL=3600
|
|
SYNC_CRON=
|
|
```
|
|
|
|
For calendar-based scheduling, set `SYNC_CRON` to a standard five-field cron
|
|
expression. It overrides `SYNC_INTERVAL`.
|
|
|
|
```env
|
|
# 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.
|
|
|
|
### SMTP notifications
|
|
|
|
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:
|
|
|
|
```env
|
|
NOTIFY_MODE=batch_success
|
|
SMTP_HOST=smtp.example.com
|
|
SMTP_PORT=587
|
|
SMTP_USER=[email protected]
|
|
SMTP_PASS=your_smtp_password
|
|
SMTP_FROM=[email protected]
|
|
SMTP_TO=[email protected],[email protected]
|
|
SMTP_TLS=starttls
|
|
```
|
|
|
|
`SMTP_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.
|
|
|
|
---
|
|
|
|
## Complete Examples
|
|
|
|
### Example 1: Hotmail → Mailcow (OAuth2)
|
|
|
|
```env
|
|
NUM_ACCOUNTS=1
|
|
SYNC_INTERVAL=21600
|
|
|
|
ACCOUNT_1_SOURCE_HOST=outlook.office365.com
|
|
ACCOUNT_1_SOURCE_USER=[email protected]
|
|
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=[email protected]
|
|
ACCOUNT_1_DEST_PASS=your_mailcow_password
|
|
```
|
|
|
|
### Example 2: Gmail → Mailcow (OAuth2)
|
|
|
|
```env
|
|
ACCOUNT_1_SOURCE_HOST=imap.gmail.com
|
|
ACCOUNT_1_SOURCE_USER=[email protected]
|
|
ACCOUNT_1_SOURCE_AUTH=oauth2
|
|
ACCOUNT_1_SOURCE_PROVIDER=gmail
|
|
ACCOUNT_1_SOURCE_TOKEN=/tokens/you_gmail.token
|
|
```
|
|
|
|
### Example 3: Multiple accounts
|
|
|
|
```env
|
|
NUM_ACCOUNTS=3
|
|
|
|
# Account 1: Hotmail -> Mailcow
|
|
ACCOUNT_1_SOURCE_HOST=outlook.office365.com
|
|
ACCOUNT_1_SOURCE_USER=[email protected]
|
|
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=[email protected]
|
|
ACCOUNT_1_DEST_PASS=password1
|
|
|
|
# Account 2: Gmail -> Mailcow
|
|
ACCOUNT_2_SOURCE_HOST=imap.gmail.com
|
|
ACCOUNT_2_SOURCE_USER=[email protected]
|
|
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=[email protected]
|
|
ACCOUNT_2_DEST_PASS=password2
|
|
|
|
# Account 3: Legacy IMAP -> Mailcow (password auth)
|
|
ACCOUNT_3_SOURCE_HOST=imap.oldprovider.com
|
|
ACCOUNT_3_SOURCE_USER=[email protected]
|
|
ACCOUNT_3_SOURCE_AUTH=password
|
|
ACCOUNT_3_SOURCE_PASS=oldpassword
|
|
ACCOUNT_3_DEST_HOST=mail.hoelee.com
|
|
ACCOUNT_3_DEST_USER=[email protected]
|
|
ACCOUNT_3_DEST_PASS=password3
|
|
```
|
|
|
|
### Example 4: One-shot sync (cron-friendly)
|
|
|
|
```bash
|
|
# .env
|
|
ONCE=true
|
|
NUM_ACCOUNTS=1
|
|
|
|
# Add to crontab:
|
|
# 0 */6 * * * cd /opt/email-sync-oauth2 && docker compose up --abort-on-container-exit
|
|
```
|
|
|
|
---
|
|
|
|
## Makefile Commands (Optional)
|
|
|
|
A `Makefile` is included for convenience:
|
|
|
|
```bash
|
|
# 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 clean
|
|
```
|
|
|
|
---
|
|
|
|
## Manual Operations
|
|
|
|
### Force sync a specific account immediately
|
|
|
|
```bash
|
|
docker compose exec imapsync sync.sh sync 1
|
|
```
|
|
|
|
### Reauthorize OAuth tokens
|
|
|
|
```bash
|
|
docker compose run --rm imapsync authorize 1
|
|
```
|
|
|
|
### Enter container shell
|
|
|
|
```bash
|
|
docker compose exec imapsync sync.sh shell
|
|
```
|
|
|
|
### Run imapsync manually with custom flags
|
|
|
|
```bash
|
|
docker compose exec imapsync sync.sh imapsync \
|
|
--host1 outlook.office365.com --user1 [email protected] \
|
|
--oauthaccesstoken1 /tokens/you_hotmail.token \
|
|
--host2 mail.hoelee.com --user2 [email protected] \
|
|
--password2 yourpassword \
|
|
--ssl1 --ssl2 --dry
|
|
```
|
|
|
|
---
|
|
|
|
## Monitoring & Healthcheck
|
|
|
|
The 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:
|
|
|
|
```text
|
|
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:
|
|
|
|
```bash
|
|
docker compose exec imapsync sync.sh sync 1
|
|
```
|
|
|
|
```bash
|
|
# 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
|
|
```
|
|
|
|
### Log files
|
|
|
|
```
|
|
./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
|
|
└── ...
|
|
```
|
|
|
|
### Check sync status
|
|
|
|
```bash
|
|
# 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)"
|
|
```
|
|
|
|
### Reauthorize reminders
|
|
|
|
For personal Microsoft accounts (90-day token limit), set a calendar reminder or monitoring check:
|
|
|
|
```bash
|
|
# Check if reauthorization is needed (token file older than 80 days)
|
|
find ./tokens -name "*.token" -mtime +80 -ls
|
|
```
|
|
|
|
---
|
|
|
|
## Architecture
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────┐
|
|
│ 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 │
|
|
└──────────────┘ └──────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## FAQ
|
|
|
|
**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.
|
|
|
|
---
|
|
|
|
## Building from Source
|
|
|
|
```bash
|
|
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=[email protected] \
|
|
-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=[email protected] \
|
|
-e ACCOUNT_1_DEST_PASS=yourpassword \
|
|
-v $(pwd)/tokens:/tokens \
|
|
-v $(pwd)/logs:/var/log/imapsync \
|
|
email-sync-oauth2:latest
|
|
```
|
|
|
|
---
|
|
|
|
## License
|
|
|
|
MIT License. See [LICENSE](LICENSE) for details.
|
|
|
|
## Credits
|
|
|
|
- [imapsync](https://imapsync.lamiral.info/) by Gilles Lamiral — the core migration engine
|
|
- [imapsync OAuth2 helper](https://github.com/imapsync/imapsync/tree/master/oauth2/oauth2_imap) — OAuth2 authorization and refresh
|
|
|
|
---
|
|
|
|
## Contributing
|
|
|
|
Pull requests welcome! Please test with your own accounts before submitting.
|
|
|
|
1. Fork the repo
|
|
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
5. Open a Pull Request
|