vasyansk e1911ef13b feat: real-time progress with per-folder bar, speed and ETA
- orchestrator: progress events now carry account-level cumulative copied/
  skipped plus current folder done/total, throttled to ~3/sec per account
- web: RUN CONTROL counters and account copied/skipped read live WS values
  (DB only advances per folder, so the summary lagged); new Progress column
  shows a bar, percent, avg messages/sec and folder ETA while running

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMHQTtnQtQqL8muAXHr9kd
2026-07-02 13:14:55 +07:00
2026-07-02 09:04:45 +07:00

imap-copier

Single-binary Go server (embeds the React SPA) that copies IMAP mailboxes between a source and a destination account. Non-destructive (copy only, never deletes), deduplicated by Message-ID, resumable/idempotent re-runs.

Quick start (plain HTTP on :80)

cp .env.example .env
# generate a real 32-byte key for ENC_KEY:
sed -i '' "s|ENC_KEY=|ENC_KEY=$(openssl rand -base64 32)|" .env
# edit .env: set POSTGRES_PASSWORD, AUTH_USER, AUTH_PASS, SESSION_SECRET

docker compose build
docker compose up -d
curl -fsS http://localhost/healthz

The app is served through Caddy on port 80 by default — no domain or TLS required. Login with AUTH_USER/AUTH_PASS from .env.

Enabling HTTPS (Let's Encrypt)

Set a real, publicly resolvable domain and an ACME contact email in .env:

DOMAIN=copier.example.com
ACME_EMAIL=you@example.com

Then start with the TLS override instead of the plain compose file:

docker compose -f docker-compose.yml -f docker-compose.tls.yml up -d
# or: make up-tls

This swaps Caddy's config to Caddyfile.tls, which requests and renews a Let's Encrypt certificate automatically for DOMAIN (ports 80/443 must be reachable from the internet for the ACME HTTP-01 challenge). Switch back to plain HTTP with docker compose -f docker-compose.yml up -d (or make up).

Environment variables (.env)

Var Required Notes
POSTGRES_PASSWORD yes Postgres password, also used in DATABASE_URL
AUTH_USER / AUTH_PASS yes Single operator login (no user table)
ENC_KEY yes 32 bytes, base64: openssl rand -base64 32
SESSION_SECRET yes Signs the session cookie
WORKER_CONCURRENCY no (default 4) Parallel accounts copied per run
HTTP_PORT no (default 80) Host port Caddy binds for HTTP
DOMAIN / ACME_EMAIL only for HTTPS See above

Makefile targets

make build    # docker compose build
make up       # docker compose up -d (plain HTTP)
make up-tls   # docker compose up -d with the Let's Encrypt override
make down     # docker compose down
make logs     # docker compose logs -f
make test     # go test ./...
make e2e      # scripts/e2e.sh (full-stack E2E against greenmail)

E2E test

scripts/e2e.sh builds and starts the full stack (postgres, app, caddy) plus a throwaway greenmail IMAP server acting as both the source and destination mailbox host, then drives the real REST API: login, create endpoints/task/account, /test, /run, and a second /run to prove idempotency (nothing is re-copied). It tears everything down afterwards.

bash scripts/e2e.sh
# or: make e2e

Known limitations

Deduplication key is UNIQUE(account_id, message_key) without folder. If the same message appears in multiple source folders (e.g. Gmail INBOX + [Gmail]/All Mail + labels-as-folders), it is copied only into whichever destination folder is processed first; folder placement for such duplicated messages is not guaranteed. This is intentional per the design spec.

Architecture

  • cmd/server — entrypoint: runs DB migrations, then serves HTTP.
  • internal/httpapi — REST API + WebSocket + embedded SPA (webdist/).
  • internal/orchestrator — test/run coordination, per-account concurrency.
  • internal/imapx — IMAP connect/list/copy primitives.
  • internal/store — Postgres access (endpoints, tasks, accounts, runs).
  • web/ — React/Vite SPA, built into internal/httpapi/webdist at image build time (see Dockerfile).
S
Description
No description provided
Readme 191 KiB
Languages
Go 54.9%
TypeScript 30.9%
CSS 9.1%
Shell 4.2%
Dockerfile 0.4%
Other 0.5%