# 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) ```bash 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: ```bash 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 ```bash 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 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`).