ADD now probes both connections, lists folders on each side, and opens a
mapping modal to route source->destination folders (e.g. Спам -> Spam) so we
append into the existing folder instead of creating a duplicate.
- store: SetTaskFolderMapping (+ round-trip test)
- httpapi: POST /tasks/{id}/probe (test both, return folder lists),
PUT /tasks/{id}/folder-mapping
- web: FolderMappingModal (reuses Modal, size=lg), submitAccount probes then
opens the modal; confirm creates the account and saves the task mapping
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01MMHQTtnQtQqL8muAXHr9kd
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 intointernal/httpapi/webdistat image build time (seeDockerfile).