Initial commit: Multi-account IMAP sync with OAuth2 support

This commit is contained in:
2026-08-10 05:54:52 +08:00
commit 482298371e
13 changed files with 1436 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
# Git
.git
.gitignore
# Docker
docker-compose.yml
Dockerfile
.dockerignore
# Documentation
README.md
LICENSE
# Tokens (sensitive)
tokens/
*.token
# Logs
logs/
*.log
# IDE
.vscode/
.idea/
# OS
.DS_Store
Thumbs.db
+93
View File
@@ -0,0 +1,93 @@
# ============================================================
# Email Sync OAuth2 - Docker Compose Configuration
# ============================================================
# Copy this file to .env and edit your settings.
# ============================================================
# ------------------------------------------------------------
# Global settings
# ------------------------------------------------------------
# Number of accounts to sync
NUM_ACCOUNTS=2
# Sync interval in seconds (default: 21600 = 6 hours).
# Used when SYNC_CRON is empty.
SYNC_INTERVAL=21600
# Optional standard five-field cron expression. When set, it overrides
# SYNC_INTERVAL. Example: 0 */6 * * * (every six hours)
SYNC_CRON=
# Run once and exit (true/false)
ONCE=false
# Token and log directories inside container
TOKEN_DIR=/tokens
LOG_DIR=/var/log/imapsync
# ------------------------------------------------------------
# Account 1: Hotmail / Office 365 -> Mailcow (OAuth2)
# ------------------------------------------------------------
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/user1_hotmail.token
ACCOUNT_1_DEST_HOST=mail.hoelee.com
ACCOUNT_1_DEST_USER=[email protected]
ACCOUNT_1_DEST_PASS=your_mailcow_password
# Delete emails on destination that don't exist on source (true/false)
ACCOUNT_1_DELETE=false
# Dry run - log what would happen without actually syncing (true/false)
ACCOUNT_1_DRY_RUN=false
# Extra imapsync options (optional)
ACCOUNT_1_EXTRA_OPTS=
# ------------------------------------------------------------
# Account 2: Any standard IMAP source (password authentication)
# ------------------------------------------------------------
ACCOUNT_2_SOURCE_HOST=imap.example.com
ACCOUNT_2_SOURCE_USER=[email protected]
ACCOUNT_2_SOURCE_AUTH=password
ACCOUNT_2_SOURCE_PASS=your_source_password
ACCOUNT_2_DEST_HOST=mail.hoelee.com
ACCOUNT_2_DEST_USER=[email protected]
ACCOUNT_2_DEST_PASS=your_mailcow_password
ACCOUNT_2_DELETE=false
ACCOUNT_2_DRY_RUN=false
ACCOUNT_2_EXTRA_OPTS=
# ------------------------------------------------------------
# Account 3+: copy an account block and increase NUM_ACCOUNTS.
# All ACCOUNT_N_* variables from .env are passed into the container.
# ------------------------------------------------------------
# ------------------------------------------------------------
# Optional SMTP notifications
# ------------------------------------------------------------
# none | batch_success | account_success | failure
# batch_success: one email after every fully successful sync batch
# account_success: one email for every successful account
# failure: one email when one or more accounts fail in a batch
NOTIFY_MODE=none
# Required only when NOTIFY_MODE is not none
SMTP_HOST=
SMTP_PORT=587
SMTP_USER=
SMTP_PASS=
SMTP_FROM=
# One or more comma-separated recipients
SMTP_TO=
# starttls (port 587), tls (implicit TLS, usually port 465), or none
SMTP_TLS=starttls
+35
View File
@@ -0,0 +1,35 @@
---
name: Bug report
about: Create a report to help us improve
title: "[BUG] "
labels: bug
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Which account (source/destination)?
2. Configuration used (redact passwords!)
3. Command run
**Expected behavior**
What you expected to happen.
**Logs**
```
Relevant log output from logs/accountN.log or container logs
```
**Environment:**
- Docker version:
- Host OS:
- Source email provider:
- Destination IMAP server:
- Number of accounts:
**Additional context**
Add any other context about the problem here.
+20
View File
@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: "[FEATURE] "
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.
+73
View File
@@ -0,0 +1,73 @@
name: Build & Push to Docker Hub
on:
push:
branches: [main]
tags: ["v*"]
pull_request:
branches: [main]
env:
REGISTRY: docker.io
IMAGE_NAME: ${{ vars.DOCKERHUB_USERNAME }}/email-sync-oauth2
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
if: github.event_name != "pull_request"
uses: docker/login-action@v3
with:
username: ${{ vars.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
push: ${{ github.event_name != "pull_request" }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Run healthcheck test
if: github.event_name == 'pull_request'
run: |
set -euo pipefail
trap 'docker compose down -v' EXIT
docker compose config --quiet
docker compose up -d
for _ in $(seq 1 18); do
status="$(docker inspect --format='{{.State.Health.Status}}' email-sync-oauth2)"
if [ "$status" = "healthy" ]; then
docker compose exec -T imapsync bash -n /sync.sh
exit 0
fi
sleep 10
done
docker compose logs
echo "Container did not become healthy" >&2
exit 1
+26
View File
@@ -0,0 +1,26 @@
# Logs
logs/
*.log
# Tokens (sensitive - never commit)
tokens/
*.token
# Environment variables (may contain passwords)
.env
# Docker
.docker/
# IDE
.vscode/
.idea/
# OS
.DS_Store
Thumbs.db
# Temp files
*.tmp
*.swp
*~
+69
View File
@@ -0,0 +1,69 @@
FROM alpine/git:2.47.2@sha256:062a01ad7a0eb17cff382bc5e26086b4d710e56dfdfdf001109a49b6d9bd378c AS imapsync-source
ARG IMAPSYNC_REV=93654c6025ff7814f983ab74dd300f9bed9282d9
RUN git clone https://github.com/imapsync/imapsync.git /src \
&& git -C /src checkout "$IMAPSYNC_REV" \
&& test "$(git -C /src rev-parse HEAD)" = "$IMAPSYNC_REV"
FROM perl:5.38-slim
LABEL maintainer="Hoelee <[email protected]>"
LABEL description="Multi-account IMAP sync with OAuth2 support for Office 365, Hotmail, Outlook.com, and Gmail"
# Install imapsync + dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
libauthen-ntlm-perl \
libdbd-sqlite3-perl \
libdist-checkconflicts-perl \
libfile-copy-recursive-perl \
libemail-address-perl \
libhttp-daemon-ssl-perl \
libio-compress-perl \
libio-socket-inet6-perl \
libio-socket-ssl-perl \
libio-tee-perl \
libjson-pp-perl \
libmail-imapclient-perl \
libmodule-runtime-perl \
libnet-dbus-perl \
libnet-dns-perl \
libpackage-stash-perl \
libpar-dist-perl \
libproc-processtable-perl \
libproc-simple-perl \
libregexp-common-perl \
libssl-dev \
libtest-fatal-perl \
libtest-mock-guard-perl \
libtest-nowarnings-perl \
libtest-pod-perl \
libtest-requires-perl \
libtest-simple-perl \
libunicode-string-perl \
liburi-perl \
libwww-perl \
cron \
msmtp \
netcat-openbsd \
procps \
&& rm -rf /var/lib/apt/lists/*
# Install one immutable, verified upstream revision of imapsync and its current
# OAuth helper. The old helper URLs no longer exist upstream.
COPY --from=imapsync-source /src/imapsync /usr/local/bin/imapsync
COPY --from=imapsync-source /src/oauth2/oauth2_imap/oauth2_imap /usr/local/bin/oauth2_imap
RUN chmod +x /usr/local/bin/imapsync /usr/local/bin/oauth2_imap
# Copy entrypoint
COPY sync.sh /sync.sh
RUN chmod +x /sync.sh
# Health is liveness plus notification delivery. Sync freshness is exposed by
# accountN.last_success / accountN.last_failure files for the chosen schedule.
HEALTHCHECK --interval=5m --timeout=10s --start-period=60s --retries=3 \
CMD test -f "${LOG_DIR:-/var/log/imapsync}/.heartbeat" && \
test ! -f "${LOG_DIR:-/var/log/imapsync}/.notification_failed" && \
test "$(cat "${LOG_DIR:-/var/log/imapsync}/.heartbeat")" -gt "$(date -d '5 minutes ago' +%s)" || \
{ echo "Health check failed: daemon heartbeat is stale or SMTP notification delivery failed. Check container logs."; exit 1; }
ENTRYPOINT ["/sync.sh"]
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Hoelee Enterprise
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+73
View File
@@ -0,0 +1,73 @@
.PHONY: build up down logs status health authorize sync sync-all shell clean
# Build the Docker image
build:
docker compose build
# Start the sync daemon
up:
docker compose up -d
@echo "Container started. Use 'make logs' to follow logs."
# Stop the container
down:
docker compose down
# Follow logs
logs:
docker logs -f email-sync-oauth2
# Show status of all accounts
status:
@echo "=== Container Status ==="
@docker inspect --format='Status: {{.State.Status}}' email-sync-oauth2 2>/dev/null || echo "Container not running"
@echo ""
@echo "=== Health Status ==="
@docker inspect --format='Health: {{.State.Health.Status}}' email-sync-oauth2 2>/dev/null || echo "No healthcheck"
@echo ""
@echo "=== Last Sync Times ==="
@for f in logs/*.last_success; do \
[ -f "$$f" ] && echo "$$(basename $$f .last_success): $$(date -d @$$(cat $$f) 2>/dev/null || echo 'unknown')"; \
done
@echo ""
@echo "=== Last Failures ==="
@for f in logs/*.last_failure; do \
[ -f "$$f" ] && echo "$$(basename $$f .last_failure): $$(date -d @$$(cat $$f) 2>/dev/null || echo 'unknown')"; \
done
# Check health
health:
@docker inspect --format='{{json .State.Health}}' email-sync-oauth2 | jq
# Authorize account (usage: make authorize ACCOUNT=1)
authorize:
@if [ -z "$(ACCOUNT)" ]; then \
echo "Usage: make authorize ACCOUNT=1"; \
exit 1; \
fi
docker compose run --rm imapsync authorize $(ACCOUNT)
# Force sync account (usage: make sync ACCOUNT=1)
sync:
@if [ -z "$(ACCOUNT)" ]; then \
echo "Usage: make sync ACCOUNT=1"; \
exit 1; \
fi
docker compose exec imapsync sync.sh sync $(ACCOUNT)
# Force a full batch now (all configured accounts)
sync-all:
docker compose exec imapsync sync.sh sync-all
# Enter container shell
shell:
docker compose exec imapsync sync.sh shell
# Clean up
clean:
docker compose down -v
rm -rf logs/*.log logs/*.last_* logs/.heartbeat logs/.notification_failed
# One-shot sync (build, run once, exit)
once:
ONCE=true docker compose up --build --abort-on-container-exit
+525
View File
@@ -0,0 +1,525 @@
# 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
+24
View File
@@ -0,0 +1,24 @@
# Security Policy
## Supported Versions
| Version | Supported |
| ------- | ------------------ |
| latest | :white_check_mark: |
## Reporting a Vulnerability
If you discover a security vulnerability, please report it by opening a GitHub issue with the `security` label. Do NOT open a public issue for security flaws that could expose user data.
We will acknowledge your report within 48 hours and work on a fix.
## Security Considerations for Users
⚠️ **This tool handles OAuth tokens and email passwords. Please follow these security best practices:**
1. **Never commit `.env` files** — they contain passwords and token paths
2. **Protect the `tokens/` directory** — OAuth tokens grant access to your email
3. **Use file permissions**: `chmod 600 tokens/*.token`
4. **Never share log files** — they may contain email content snippets
5. **Rotate tokens** by deleting the token file and reauthorizing periodically
6. **Use a dedicated Azure App** — don't reuse OAuth credentials from other apps
+27
View File
@@ -0,0 +1,27 @@
# ============================================================
# Email Sync OAuth2 - Docker Compose
# ============================================================
# Multi-account IMAP sync with OAuth2 support for
# Office 365, Hotmail, Outlook.com, and Gmail.
# ============================================================
services:
imapsync:
build:
context: .
dockerfile: Dockerfile
container_name: email-sync-oauth2
restart: unless-stopped
# Pass every ACCOUNT_N_* entry from .env, not only accounts 1 and 2.
env_file:
- path: .env
required: false
volumes:
- ./tokens:${TOKEN_DIR:-/tokens}
- ./logs:${LOG_DIR:-/var/log/imapsync}
# Healthcheck is defined in Dockerfile
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
+422
View File
@@ -0,0 +1,422 @@
#!/bin/bash
set -Eeuo pipefail
# IMAP Sync Manager - multi-account OAuth2 and password authentication.
TOKEN_DIR="${TOKEN_DIR:-/tokens}"
LOG_DIR="${LOG_DIR:-/var/log/imapsync}"
PID_DIR="${PID_DIR:-/tmp/imapsync}"
HEARTBEAT_FILE="${LOG_DIR}/.heartbeat"
NUM_ACCOUNTS="${NUM_ACCOUNTS:-1}"
SYNC_INTERVAL="${SYNC_INTERVAL:-21600}" # six hours
SYNC_CRON="${SYNC_CRON:-}"
ONCE="${ONCE:-false}"
# Notifications are disabled unless NOTIFY_MODE is explicitly set.
# Valid modes: none, batch_success, account_success, failure.
NOTIFY_MODE="${NOTIFY_MODE:-none}"
SMTP_HOST="${SMTP_HOST:-}"
SMTP_PORT="${SMTP_PORT:-587}"
SMTP_USER="${SMTP_USER:-}"
SMTP_PASS="${SMTP_PASS:-}"
SMTP_FROM="${SMTP_FROM:-}"
SMTP_TO="${SMTP_TO:-}"
SMTP_TLS="${SMTP_TLS:-starttls}"
NOTIFICATION_FAILURE_FILE="${LOG_DIR}/.notification_failed"
NOTIFICATION_LOG="${LOG_DIR}/notification.log"
mkdir -p "$LOG_DIR" "$PID_DIR" "$TOKEN_DIR"
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"
}
get_var() {
local varname="$1"
echo "${!varname:-}"
}
oauth_authorization_invalid() {
local logfile="$1"
tail -n 200 "$logfile" 2>/dev/null | grep -Eqi \
'invalid_grant|expired.*(token|credential)|token.*expired|refresh token.*(expired|revoked|invalid)|authorization.*(expired|revoked)|AADSTS700082|AADSTS700084|AUTHENTICATE failed|authentication.*failed|authentication unsuccessful|invalid credentials'
}
log_oauth_authorization_guidance() {
local i="$1"
local provider="$2"
local logfile="$3"
if oauth_authorization_invalid "$logfile"; then
log "Account $i: OAuth2 authorization is expired, revoked, or rejected by $provider. Reauthorize with: docker compose run --rm imapsync authorize $i"
log "Account $i: After reauthorization, run immediately with: docker compose exec imapsync sync.sh sync $i"
fi
}
validate_settings() {
if ! [[ "$NUM_ACCOUNTS" =~ ^[1-9][0-9]*$ ]]; then
log "Configuration error: NUM_ACCOUNTS must be a positive integer"
return 1
fi
if [ -z "$SYNC_CRON" ] && ! [[ "$SYNC_INTERVAL" =~ ^[1-9][0-9]*$ ]]; then
log "Configuration error: SYNC_INTERVAL must be a positive number of seconds"
return 1
fi
case "$NOTIFY_MODE" in
none|batch_success|account_success|failure) ;;
*)
log "Configuration error: NOTIFY_MODE must be none, batch_success, account_success, or failure"
return 1
;;
esac
if [ "$NOTIFY_MODE" != "none" ]; then
if [ -z "$SMTP_HOST" ] || [ -z "$SMTP_FROM" ] || [ -z "$SMTP_TO" ]; then
log "Configuration error: SMTP_HOST, SMTP_FROM, and SMTP_TO are required when notifications are enabled"
return 1
fi
if ! [[ "$SMTP_PORT" =~ ^[1-9][0-9]*$ ]]; then
log "Configuration error: SMTP_PORT must be a valid port number"
return 1
fi
if { [ -n "$SMTP_USER" ] && [ -z "$SMTP_PASS" ]; } || { [ -z "$SMTP_USER" ] && [ -n "$SMTP_PASS" ]; }; then
log "Configuration error: SMTP_USER and SMTP_PASS must either both be set or both be empty"
return 1
fi
case "$SMTP_TLS" in starttls|tls|none) ;; *)
log "Configuration error: SMTP_TLS must be starttls, tls, or none"
return 1
;;
esac
fi
}
send_notification() {
local subject="$1"
local body="$2"
local config
local -a recipients
if [ "$NOTIFY_MODE" = "none" ]; then
return 0
fi
config="$(mktemp "${PID_DIR}/msmtp.XXXXXX")"
chmod 600 "$config"
{
printf 'defaults\n'
printf 'host %s\n' "$SMTP_HOST"
printf 'port %s\n' "$SMTP_PORT"
printf 'from %s\n' "$SMTP_FROM"
if [ -n "$SMTP_USER" ]; then
printf 'auth on\nuser %s\npassword %s\n' "$SMTP_USER" "$SMTP_PASS"
else
printf 'auth off\n'
fi
case "$SMTP_TLS" in
starttls) printf 'tls on\ntls_starttls on\n' ;;
tls) printf 'tls on\ntls_starttls off\n' ;;
none) printf 'tls off\n' ;;
esac
} > "$config"
IFS=',' read -r -a recipients <<< "$SMTP_TO"
if printf 'From: %s\nTo: %s\nSubject: %s\n\n%s\n' "$SMTP_FROM" "$SMTP_TO" "$subject" "$body" \
| msmtp --file="$config" "${recipients[@]}" >> "$NOTIFICATION_LOG" 2>&1; then
rm -f "$config" "$NOTIFICATION_FAILURE_FILE"
log "Notification sent: $subject"
return 0
fi
rm -f "$config"
printf '%s %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$subject" > "$NOTIFICATION_FAILURE_FILE"
log "Notification delivery failed; container will remain unhealthy until a notification is sent successfully. Check $NOTIFICATION_LOG"
return 1
}
source_oauth_provider() {
case "$1" in
office365|outlook|hotmail) echo "office365" ;;
gmail|google) echo "gmail" ;;
*) return 1 ;;
esac
}
sync_account() {
local i="$1"
local src_host="$(get_var "ACCOUNT_${i}_SOURCE_HOST")"
local src_user="$(get_var "ACCOUNT_${i}_SOURCE_USER")"
local src_auth="$(get_var "ACCOUNT_${i}_SOURCE_AUTH")"
local src_pass="$(get_var "ACCOUNT_${i}_SOURCE_PASS")"
local src_token="$(get_var "ACCOUNT_${i}_SOURCE_TOKEN")"
local src_provider="$(get_var "ACCOUNT_${i}_SOURCE_PROVIDER")"
local dest_host="$(get_var "ACCOUNT_${i}_DEST_HOST")"
local dest_user="$(get_var "ACCOUNT_${i}_DEST_USER")"
local dest_pass="$(get_var "ACCOUNT_${i}_DEST_PASS")"
local delete_opt="$(get_var "ACCOUNT_${i}_DELETE")"
local dry_run="$(get_var "ACCOUNT_${i}_DRY_RUN")"
local extra_opts="$(get_var "ACCOUNT_${i}_EXTRA_OPTS")"
local logfile="${LOG_DIR}/account${i}.log"
local pidfile="${PID_DIR}/account${i}.pid"
local oauth_provider
local oauth_log
local oauth_authorization_detected=false
local sync_exit
local -a args
SYNC_ACCOUNT_RESULT="skipped"
if [ -z "$src_host" ] && [ -z "$dest_host" ]; then
log "Account $i: SKIP (not configured)"
return 0
fi
if [ -z "$src_host" ] || [ -z "$src_user" ] || [ -z "$dest_host" ] || [ -z "$dest_user" ] || [ -z "$dest_pass" ]; then
log "Account $i: CONFIGURATION FAILED (source host/user and destination host/user/password are required)"
SYNC_ACCOUNT_RESULT="failure"
echo "$(date +%s)" > "${LOG_DIR}/account${i}.last_failure"
return 1
fi
if [ -f "$pidfile" ]; then
local old_pid
old_pid="$(cat "$pidfile")"
if kill -0 "$old_pid" 2>/dev/null; then
log "Account $i: SKIP (already running, PID $old_pid)"
return 0
fi
fi
echo $$ > "$pidfile"
log "Account $i: SYNC START - $src_user ($src_host) -> $dest_user ($dest_host)"
args=(
--host1 "$src_host" --user1 "$src_user" --ssl1
--host2 "$dest_host" --user2 "$dest_user" --password2 "$dest_pass" --ssl2
--noauthmd5
)
case "$src_auth" in
oauth2)
if ! oauth_provider="$(source_oauth_provider "$src_provider")"; then
log "Account $i: CONFIGURATION FAILED (OAuth2 provider must be office365, outlook, hotmail, gmail, or google)"
SYNC_ACCOUNT_RESULT="failure"
echo "$(date +%s)" > "${LOG_DIR}/account${i}.last_failure"
rm -f "$pidfile"
return 1
fi
if [ -z "$src_token" ]; then
log "Account $i: CONFIGURATION FAILED (ACCOUNT_${i}_SOURCE_TOKEN is required for OAuth2)"
SYNC_ACCOUNT_RESULT="failure"
echo "$(date +%s)" > "${LOG_DIR}/account${i}.last_failure"
rm -f "$pidfile"
return 1
fi
if [ ! -s "$src_token" ]; then
log "Account $i: OAuth2 token is missing. Authorize with: docker compose run --rm imapsync authorize $i"
SYNC_ACCOUNT_RESULT="failure"
echo "$(date +%s)" > "${LOG_DIR}/account${i}.last_failure"
rm -f "$pidfile"
return 1
fi
# The helper prints access and refresh tokens. Keep that output in a
# short-lived, root-only file instead of the mounted account log.
oauth_log="$(mktemp "${PID_DIR}/oauth.XXXXXX")"
chmod 600 "$oauth_log"
# The upstream helper opens an interactive browser flow when refresh
# fails. A scheduled container must never wait indefinitely for that.
timeout --kill-after=5s 90s oauth2_imap --provider "$oauth_provider" --application imapsync --token_file "$src_token" "$src_user" > "$oauth_log" 2>&1 || true
if oauth_authorization_invalid "$oauth_log"; then
log_oauth_authorization_guidance "$i" "$src_provider" "$oauth_log"
oauth_authorization_detected=true
fi
rm -f "$oauth_log"
if [ ! -s "$src_token" ]; then
log "Account $i: OAuth2 token refresh or authorization failed"
log_oauth_authorization_guidance "$i" "$src_provider" "$logfile"
SYNC_ACCOUNT_RESULT="failure"
echo "$(date +%s)" > "${LOG_DIR}/account${i}.last_failure"
rm -f "$pidfile"
return 1
fi
args+=(--oauthaccesstoken1 "$src_token")
;;
password)
if [ -z "$src_pass" ]; then
log "Account $i: CONFIGURATION FAILED (ACCOUNT_${i}_SOURCE_PASS is required for password authentication)"
SYNC_ACCOUNT_RESULT="failure"
echo "$(date +%s)" > "${LOG_DIR}/account${i}.last_failure"
rm -f "$pidfile"
return 1
fi
args+=(--password1 "$src_pass")
;;
*)
log "Account $i: CONFIGURATION FAILED (ACCOUNT_${i}_SOURCE_AUTH must be oauth2 or password)"
SYNC_ACCOUNT_RESULT="failure"
echo "$(date +%s)" > "${LOG_DIR}/account${i}.last_failure"
rm -f "$pidfile"
return 1
;;
esac
[ "$delete_opt" = "true" ] && args+=(--delete2)
[ "$dry_run" = "true" ] && args+=(--dry)
if [ -n "$extra_opts" ]; then
# Intentional option splitting: this variable supplies extra CLI flags.
# shellcheck disable=SC2206
args+=($extra_opts)
fi
if imapsync "${args[@]}" >> "$logfile" 2>&1; then
log "Account $i: SYNC OK"
echo "$(date +%s)" > "${LOG_DIR}/account${i}.last_success"
SYNC_ACCOUNT_RESULT="success"
rm -f "$pidfile"
return 0
fi
sync_exit=$?
log "Account $i: SYNC FAILED (exit $sync_exit)"
echo "$(date +%s)" > "${LOG_DIR}/account${i}.last_failure"
if [ "$src_auth" = "oauth2" ] && [ "$oauth_authorization_detected" = "false" ]; then
log_oauth_authorization_guidance "$i" "$src_provider" "$logfile"
fi
SYNC_ACCOUNT_RESULT="failure"
rm -f "$pidfile"
return "$sync_exit"
}
sync_all() {
local i
local configured=0
local failures=0
local notification_failures=0
local successes=""
local failed_accounts=""
log "=== Sync cycle start ==="
for ((i = 1; i <= NUM_ACCOUNTS; i++)); do
if sync_account "$i"; then
if [ "$SYNC_ACCOUNT_RESULT" = "success" ]; then
configured=$((configured + 1))
successes="${successes}${successes:+, }${i}"
if [ "$NOTIFY_MODE" = "account_success" ]; then
send_notification "IMAP sync succeeded for account $i" "Account $i completed successfully at $(date '+%Y-%m-%d %H:%M:%S')." || notification_failures=1
fi
fi
else
configured=$((configured + 1))
failures=$((failures + 1))
failed_accounts="${failed_accounts}${failed_accounts:+, }${i}"
fi
done
if [ "$NOTIFY_MODE" = "batch_success" ] && [ "$configured" -gt 0 ] && [ "$failures" -eq 0 ]; then
send_notification "IMAP sync batch succeeded" "All configured accounts completed successfully at $(date '+%Y-%m-%d %H:%M:%S'): $successes." || notification_failures=1
fi
if [ "$NOTIFY_MODE" = "failure" ] && [ "$failures" -gt 0 ]; then
send_notification "IMAP sync batch failed" "Sync failed for account(s) $failed_accounts at $(date '+%Y-%m-%d %H:%M:%S'). Check ${LOG_DIR}/accountN.log for details." || notification_failures=1
fi
log "=== Sync cycle complete (success: ${successes:-none}; failed: ${failed_accounts:-none}) ==="
[ "$failures" -eq 0 ] && [ "$notification_failures" -eq 0 ]
}
start_heartbeat() {
echo "$(date +%s)" > "$HEARTBEAT_FILE"
while true; do
echo "$(date +%s)" > "$HEARTBEAT_FILE"
sleep 60
done &
HEARTBEAT_PID=$!
}
run_cron_scheduler() {
local wrapper="${PID_DIR}/cron-sync-all.sh"
local crontab_file="${PID_DIR}/sync.crontab"
# Cron starts commands with a minimal environment. Preserve this container's
# configuration in a root-only Bash wrapper so arbitrary ACCOUNT_N_* values work.
{
printf '#!/bin/bash\n'
export -p
printf 'exec /sync.sh sync-all\n'
} > "$wrapper"
chmod 700 "$wrapper"
printf '%s %s >> /proc/1/fd/1 2>&1\n' "$SYNC_CRON" "$wrapper" > "$crontab_file"
if ! crontab "$crontab_file"; then
log "Configuration error: SYNC_CRON is not a valid cron expression: $SYNC_CRON"
return 1
fi
log "Starting cron schedule: $SYNC_CRON"
cron -f &
CRON_PID=$!
wait "$CRON_PID"
}
run_once() {
log "Running in ONCE mode"
local status=0
sync_all || status=$?
log "One-shot sync complete"
return "$status"
}
run_daemon() {
log "Starting daemon mode (accounts: $NUM_ACCOUNTS)"
start_heartbeat
trap 'kill "${HEARTBEAT_PID:-}" "${CRON_PID:-}" 2>/dev/null || true' EXIT
if [ -n "$SYNC_CRON" ]; then
run_cron_scheduler
return
fi
while true; do
sync_all || true
log "Sleeping ${SYNC_INTERVAL}s before the next sync cycle"
sleep "$SYNC_INTERVAL"
done
}
case "${1:-}" in
sync)
shift
validate_settings
sync_account "${1:-1}"
;;
sync-all)
validate_settings
sync_all
;;
authorize)
shift
i="${1:-1}"
src_user="$(get_var "ACCOUNT_${i}_SOURCE_USER")"
src_token="$(get_var "ACCOUNT_${i}_SOURCE_TOKEN")"
src_provider="$(get_var "ACCOUNT_${i}_SOURCE_PROVIDER")"
validate_settings
if ! provider="$(source_oauth_provider "$src_provider")" || [ -z "$src_user" ] || [ -z "$src_token" ]; then
log "Account $i: authorization requires a supported OAuth2 provider, source user, and token path"
exit 1
fi
log "Manual authorize: $src_user"
exec oauth2_imap --startover --provider "$provider" --application imapsync --token_file "$src_token" "$src_user"
;;
shell|bash|sh)
exec /bin/bash
;;
imapsync)
shift
exec imapsync "$@"
;;
*)
validate_settings
if [ "$ONCE" = "true" ]; then
run_once
else
run_daemon
fi
;;
esac