Skip to content

bulwarkmail/legacy-proxy

legacy-proxy

Not production ready. This is a v0.1 scaffold. Some JMAP methods are still missing (see Status), there are no security audits, and the API and storage formats may change without notice. Use it for development, testing, and self-hosted experiments only.

IMAP / SMTP / ManageSieve to JMAP gateway. Lets any JMAP client talk to Gmail, your hosting provider's mailbox, or any RFC 3501 IMAP server.

Front: JMAP for Mail (RFC 8620 + RFC 8621). Back: IMAP4rev1, SMTP Submission, ManageSieve.

Status

v0.1. Working:

  • JMAP Session with the full capability table
  • Batched methodCalls, back-references (#ref, including * wildcard)
  • Mailbox/get|query|queryChanges|changes|set (folder create/rename/delete/subscribe)
  • Email/get|query|set|changes|queryChanges|copy|parse|import
  • EmailSubmission/get|query|changes|set (recent submissions cached for /get)
  • Thread/get|changes (degenerate one-message-per-thread until we add an index)
  • SearchSnippet/get (returns null snippets — IMAP doesn't surface match offsets)
  • Identity/get|set|changes (name, replyTo, signatures persisted in SQLite)
  • VacationResponse/get|set|changes (full body, dates, htmlBody round-tripped via Sieve)
  • AddressBook/get|changes, ContactCard/get|query|changes|queryChanges via CardDAV
  • PushSubscription/get|set (stub: empty list, creates rejected — no webhook delivery yet)
  • File upload at /jmap/upload/{accountId}/, advertised via uploadUrl
  • IMAP via imapflow, single connection per account
  • ManageSieve (RFC 5804), used by VacationResponse
  • SMTP submission (nodemailer)
  • Auth: PLAIN, LOGIN, XOAUTH2. HMAC session tokens. AES-256-GCM vault
  • better-sqlite3 state store
  • EventSource endpoint (connect + keepalive ping; no change events yet)
  • Docker image + compose files (prod, integration)
  • Vitest unit suite, jmap-test-suite runner

Not done yet:

  • IMAP IDLE → SSE publishing: the EventSource endpoint accepts connections but never emits change events.
  • WebSocket transport: @fastify/websocket is installed but no /jmap/ws handler is registered.
  • */changes and Email/queryChanges return cannotCalculateChanges whenever the state has moved (no CONDSTORE-backed log yet). Listed in the compliance allowlist.
  • ContactCard/set and AddressBook/set return forbidden — CardDAV writes (MKCOL/PUT/DELETE) aren't implemented.
  • Multi-mailbox membership: messages can live in exactly one IMAP folder.
  • Attachments in Email/set { create }: the upload endpoint exists but the MIME builder doesn't yet pull attachment blobs into the message body. Use Email/import for that path today.

Sort: only receivedAt is advertised and supported (IMAP without SORT can only deliver UID order). hasAttachment filter is rejected for the same reason.

Quickstart

You need Docker. Two ways to run it:

Pull the published image

mkdir legacy-proxy && cd legacy-proxy

cat > .env <<EOF
VAULT_KEY=$(openssl rand -base64 32)
SESSION_HMAC_KEY=$(openssl rand -base64 32)
EOF
chmod 600 .env

curl -fsSLo providers.json   https://raw.githubusercontent.com/bulwarkmail/legacy-proxy/main/providers.example.json
curl -fsSLo compose.prod.yml https://raw.githubusercontent.com/bulwarkmail/legacy-proxy/main/compose.prod.yml

$EDITOR providers.json    # point the `generic` entry at your IMAP/SMTP host

docker compose -f compose.prod.yml up -d

curl http://localhost:8080/healthz returns ok if it's up. JMAP clients connect to http://localhost:8080/.well-known/jmap.

Build from source

git clone https://github.com/bulwarkmail/legacy-proxy.git
cd legacy-proxy
npm run setup
docker compose up -d

npm run setup writes .env and providers.json. It won't clobber existing files. Pass -- --force to overwrite.

Logging in

Trade IMAP credentials for a JMAP session token:

curl -s http://localhost:8080/api/login \
  -H 'content-type: application/json' \
  -d '{"username":"you@example.com","password":"...","provider":"generic"}'

Use the returned token as Authorization: Bearer ... on JMAP requests.

Gmail

Gmail wants an App Password (2FA must be on). Use "provider": "gmail". XOAUTH2 works too if you bring your own tokens.

TLS and public hosts

The proxy serves plain HTTP. Put Caddy, Traefik, or nginx in front of it and set PUBLIC_URL to the URL clients see. The Session resource bakes URLs from PUBLIC_URL, so a wrong value breaks every JMAP client silently.

Configuration

env var default notes
PORT 8080 HTTP listen port
PUBLIC_URL http://localhost:$PORT URL clients see; baked into Session
DATA_DIR /data SQLite and blob cache
VAULT_KEY required base64 32-byte AES-GCM key
SESSION_HMAC_KEY required base64 32-byte HMAC-SHA-256 key
DEFAULT_PROVIDER generic provider key when /api/login omits
PROVIDERS_FILE /etc/legacy-proxy/providers.json provider catalogue
LOG_LEVEL info pino level

providers.example.json ships Gmail and a generic $IMAP_HOST/$SMTP_HOST/$SIEVE_HOST template.

Tests

npm test                  # unit tests
npm run test:integration  # needs compose.test.yml; gated by RUN_INTEGRATION=1
npm run test:compliance   # jmap-test-suite against a live proxy
npm run test:all

test:compliance clones jmap-test-suite into vendor/jmap-test-suite/ and runs it against PROXY_URL. Allow-listed upstream failures live in test/compliance/known-failures.txt, mostly things the IMAP server can't offer (e.g. queryChanges without CONDSTORE).

Architecture

src/
  server.ts        fastify bootstrap
  jmap/            session, router, capabilities, methods
  imap/            imapflow pool, fetcher, search compiler
  smtp/            nodemailer submission
  sieve/           ManageSieve client, vacation
  auth/            session tokens, credential vault, providers
  mapping/         IMAP <-> JMAP id codecs, body structure, flags
  state/           SQLite store, opaque state strings
  push/            eventsource, websocket

License

AGPL-3.0

About

No description, website, or topics provided.

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors