98 lines
3.5 KiB
Markdown
98 lines
3.5 KiB
Markdown
# 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`).
|