Phase 0: scaffold Jerboa Proton Mail reader

ober

8c6c143af3de323ad6a54c0e7c08b9934130857e

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..40643ee
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,15 @@
+.DS_Store
+*.so
+*.dylib
+*.o
+*.wpo
+*.boot
+*.log
+*.eml
+*.mbox
+/protonmail-read
+/protonmail-read-bin
+.protonmail-reader.local
+exports/
+cache/
+tmp/
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..afb8edc
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,36 @@
+JERBOA_HOME ?= $(realpath $(CURDIR)/../jerboa)
+SCHEME      ?= $(JERBOA_HOME)/.chez/bin/scheme
+
+.PHONY: help run test doctor clean
+.DEFAULT_GOAL := help
+
+help:
+	@echo "jerboa-protonmail"
+	@echo ""
+	@echo "Development:"
+	@echo "  bin/protonmail-read --help  Run the checked-in launcher"
+	@echo "  make run ARGS='--help'   Run the CLI under the interpreter"
+	@echo "  make doctor              Run the placeholder doctor command"
+	@echo "  make test                Run smoke tests"
+	@echo "  make clean               Remove generated local artifacts"
+	@echo ""
+	@echo "Environment:"
+	@echo "  JERBOA_HOME = $(JERBOA_HOME)"
+	@echo "  SCHEME      = $(SCHEME)"
+
+run:
+	JERBOA_HOME=$(JERBOA_HOME) \
+		$(SCHEME) -q --libdirs $(CURDIR):$(JERBOA_HOME)/lib \
+			--script main.ss -- $(ARGS)
+
+doctor:
+	$(MAKE) run ARGS='doctor'
+
+test:
+	JERBOA_HOME=$(JERBOA_HOME) \
+		$(SCHEME) -q --libdirs $(CURDIR):$(JERBOA_HOME)/lib \
+			--script test/test-all.ss
+
+clean:
+	rm -f protonmail-read protonmail-read-bin
+	rm -rf exports cache tmp
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..410d080
--- /dev/null
+++ b/README.md
@@ -0,0 +1,51 @@
+# jerboa-protonmail
+
+Read-only Proton Mail access experiments in Jerboa.
+
+The stable first target is a local IMAP client for Proton Mail Bridge. Bridge
+handles Proton authentication, key management, and local decryption; this tool
+will initially list folders, list messages, fetch raw messages, decode common
+email formats, and export `.eml` files without mutating mailbox state.
+
+The native direct-Proton track is documented in `plan.md`, but it is not the
+first implementation path.
+
+## Current Status
+
+Phase 0 is a project scaffold:
+
+- CLI entry point.
+- Environment config helper.
+- Smoke tests.
+- Makefile.
+- Project plan.
+
+No live Proton or Bridge connection is attempted yet.
+
+## Development
+
+```sh
+make run ARGS='--help'
+make doctor
+make test
+```
+
+By default the Makefile expects Jerboa at `../jerboa`. Override with:
+
+```sh
+make JERBOA_HOME=/path/to/jerboa test
+```
+
+## Bridge Configuration
+
+Later phases will read Bridge IMAP settings from environment variables:
+
+```sh
+export PROTON_BRIDGE_HOST=127.0.0.1
+export PROTON_BRIDGE_PORT=1143
+export PROTON_BRIDGE_USER='bridge-generated-user'
+export PROTON_BRIDGE_PASSWORD='bridge-generated-password'
+export PROTON_BRIDGE_TLS=auto
+```
+
+Use Bridge-generated IMAP credentials, not your Proton account password.
diff --git a/bin/protonmail-read b/bin/protonmail-read
new file mode 100755
index 0000000..9dad98a
--- /dev/null
+++ b/bin/protonmail-read
@@ -0,0 +1,9 @@
+#!/bin/sh
+set -eu
+
+repo_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
+jerboa_home=${JERBOA_HOME:-"$repo_dir/../jerboa"}
+scheme=${SCHEME:-"$jerboa_home/.chez/bin/scheme"}
+
+exec "$scheme" -q --libdirs "$repo_dir:$jerboa_home/lib" \
+  --script "$repo_dir/main.ss" -- "$@"
diff --git a/main.ss b/main.ss
new file mode 100644
index 0000000..70f0e11
--- /dev/null
+++ b/main.ss
@@ -0,0 +1,31 @@
+#!chezscheme
+;;; Script entry point for protonmail-read.
+
+(import (except (chezscheme)
+                make-hash-table hash-table?
+                sort sort!
+                printf fprintf
+                path-extension path-absolute?
+                with-input-from-string with-output-to-string
+                iota 1+ 1-
+                partition
+                make-date make-time))
+
+(define home (or (getenv "HOME") "."))
+(define jerboa-dir
+  (or (getenv "JERBOA_HOME")
+      (string-append home "/mine/jerboa")))
+(define project-dir
+  (or (getenv "JERBOA_PROTONMAIL_DIR")
+      (current-directory)))
+
+(library-directories
+  (append
+    (list (cons project-dir project-dir)
+          (cons (string-append jerboa-dir "/lib")
+                (string-append jerboa-dir "/lib")))
+    (library-directories)))
+
+(import (protonmail cli))
+
+(run-cli (command-line-arguments))
diff --git a/plan.md b/plan.md
new file mode 100644
index 0000000..0d39fb2
--- /dev/null
+++ b/plan.md
@@ -0,0 +1,804 @@
+# Jerboa Proton Mail Reader Plan
+
+## Goal
+
+Build a Jerboa program that can access Proton Mail for read-only workflows:
+
+- List folders/mailboxes.
+- List recent messages.
+- Fetch and decode selected messages.
+- Search mail by basic criteria.
+- Export raw messages as `.eml`.
+
+The first version will not send email, delete email, move email, mark email as read, or modify mailbox state.
+
+## Current Recommendation
+
+Use Proton Mail Bridge as the integration boundary.
+
+Proton Mail Bridge handles Proton authentication, key management, and local decryption, then exposes a local IMAP/SMTP interface. Our Jerboa program should talk to Bridge over local IMAP and stay read-only.
+
+This is much lower risk than trying to reimplement Proton's private web API flow. The public client source is useful reference material, and local Jerboa projects already provide TCP/TLS/HTTP and OpenPGP building blocks, but direct API integration would still require tracking Proton auth, key unlocking, encrypted message formats, and upstream API changes.
+
+Treat the project as two tracks:
+
+1. Bridge track: build a read-only IMAP client first.
+2. Native track: research a direct Proton client after the Bridge path works.
+
+The native track is more feasible than initially assumed because this machine already has `jerboa-pgp`, `jerboa-ssl`, `jerboa-https`, and `jerboa-crypto`.
+
+## Source Facts
+
+- Proton states that Proton apps are open source:
+  - https://proton.me/community/open-source
+- Proton Mail web clients are in a public monorepo:
+  - https://github.com/ProtonMail/WebClients
+- Proton Mail Bridge is open source:
+  - https://github.com/ProtonMail/proton-bridge
+  - https://proton.me/blog/bridge-open-source
+- Proton explains that normal IMAP cannot connect directly to Proton Mail because stored mail is end-to-end or zero-access encrypted:
+  - https://proton.me/support/why-you-need-bridge
+- Proton Mail Bridge creates local IMAP and SMTP servers on the user's computer:
+  - https://proton.me/support/why-you-need-bridge
+  - https://github.com/ProtonMail/proton-bridge
+- Proton Bridge supports IMAP/SMTP, not POP3:
+  - https://proton.me/support/imap-smtp-and-pop3-setup
+- Proton Mail Bridge is currently available for paid Proton Mail plans:
+  - https://proton.me/support/imap-smtp-and-pop3-setup
+- Bridge has a CLI mode:
+  - https://proton.me/support/bridge-cli-guide
+
+## Local Jerboa Assets
+
+Local projects under `/Users/user/mine/jerboa*` provide useful pieces:
+
+- `/Users/user/mine/jerboa-pgp`
+  - Jerboa CLI and FFI bindings over a Rust crypto backend.
+  - Uses pure-Rust `rPGP` for OpenPGP interop.
+  - Exposes `jpgp-pgp-decrypt`, `jpgp-pgp-encrypt`, `jpgp-pgp-sign`, and `jpgp-pgp-verify`.
+  - Current inbound OpenPGP decrypt path expects ASCII-armored OpenPGP messages and supports SEIPDv1/RFC 4880 style messages through `rPGP`.
+  - Does not currently advertise support for GnuPG 2.5 proprietary OCB packet/tag 20.
+- `/Users/user/mine/jerboa-ssl`
+  - Provides TCP and TLS client primitives:
+    - `tcp-connect`
+    - `tcp-read`
+    - `tcp-write`
+    - `ssl-connect`
+    - `ssl-read`
+    - `ssl-write`
+- `/Users/user/mine/jerboa-https`
+  - Provides an HTTP/1.1 client over TLS:
+    - `http-get`
+    - `http-post`
+    - `http-put`
+    - `http-delete`
+    - `http-head`
+- `/Users/user/mine/jerboa-crypto`
+  - Provides digest, HMAC, symmetric crypto, AEAD, Ed25519, random bytes, and scrypt bindings.
+- `/Users/user/mine/jerboa-yubikey`
+  - Provides OpenPGP card/YubiKey management support. This is probably optional for Proton Mail, but useful if later workflows involve hardware-held OpenPGP keys.
+
+Implication: a native direct Proton client is not blocked by lack of GPG/OpenPGP primitives. The harder work is Proton-specific auth/session handling, key-unlock flow, API compatibility, payload format mapping, and tests against realistic encrypted message fixtures.
+
+## Non-Goals
+
+- No direct Proton web API client in the first release. A native API client can be a later research track.
+- No SMTP/send support.
+- No mailbox mutation:
+  - No delete.
+  - No archive.
+  - No move.
+  - No copy.
+  - No flag changes.
+  - No expunge.
+- No credential storage in repository files.
+- No background daemon in the first release.
+- No GUI in the first release.
+
+## Assumptions
+
+- Proton Mail Bridge is installed and running on the same machine.
+- The Proton account has a plan that supports Bridge.
+- The user has logged into Bridge through its GUI or CLI.
+- The Jerboa program receives Bridge's local IMAP configuration from environment variables or an ignored local config file.
+- Bridge provides decrypted RFC 5322 messages over IMAP after local decryption.
+- The initial tool can be a CLI.
+
+## Key Risks
+
+### Bridge Compatibility
+
+Bridge is designed for desktop email clients, and Proton warns that unsupported clients may behave differently because IMAP implementations vary. We should keep the IMAP command surface small and conservative.
+
+Mitigation:
+
+- Start with basic commands only.
+- Prefer UID-based operations.
+- Avoid commands that alter state.
+- Use `BODY.PEEK[]` instead of `BODY[]` to avoid marking messages as read.
+- Add transcript tests for IMAP parsing.
+
+### Credential Handling
+
+Bridge uses generated IMAP credentials. These are still sensitive because they allow local access to decrypted mail through Bridge.
+
+Mitigation:
+
+- Read credentials from environment variables first.
+- Support a local config file only if it is gitignored and permission-checked.
+- Never print passwords in logs.
+- Redact credentials in error messages.
+
+### Local Plaintext Exposure
+
+Bridge decrypts messages locally. Our tool will see plaintext email.
+
+Mitigation:
+
+- Keep default behavior streaming/read-only.
+- Do not cache message bodies by default.
+- If caching is added later, make it opt-in and document that it stores plaintext.
+- Store exported `.eml` files only when explicitly requested.
+
+### MIME Complexity
+
+Real email is messy: nested multiparts, encoded headers, quoted-printable bodies, base64 attachments, different charsets, malformed messages, and HTML-only messages.
+
+Mitigation:
+
+- Implement a small MIME parser with fixtures.
+- Preserve raw `.eml` output even when friendly decoding fails.
+- Decode common cases first:
+  - RFC 5322 headers.
+  - RFC 2047 encoded words.
+  - Multipart boundaries.
+  - `text/plain`.
+  - `text/html` fallback.
+  - `base64`.
+  - `quoted-printable`.
+  - UTF-8.
+  - ISO-8859-1 fallback if available.
+
+## Proposed CLI
+
+The binary can be named `protonmail-read`.
+
+```text
+protonmail-read folders
+protonmail-read list --folder INBOX --limit 20
+protonmail-read show --folder INBOX --uid 12345
+protonmail-read raw --folder INBOX --uid 12345
+protonmail-read search --folder INBOX --from someone@example.com
+protonmail-read search --folder INBOX --subject invoice
+protonmail-read search --folder INBOX --since 2026-01-01
+protonmail-read export-eml --folder INBOX --uid 12345 --output message.eml
+protonmail-read doctor
+```
+
+## Configuration
+
+Prefer environment variables:
+
+```text
+PROTON_BRIDGE_HOST=127.0.0.1
+PROTON_BRIDGE_PORT=1143
+PROTON_BRIDGE_USER=bridge-generated-user
+PROTON_BRIDGE_PASSWORD=bridge-generated-password
+PROTON_BRIDGE_TLS=auto
+```
+
+Optional later local config path:
+
+```text
+.protonmail-reader.local
+```
+
+Rules:
+
+- Add local config files to `.gitignore`.
+- Refuse to read config files that are group/world-readable if they contain a password.
+- Redact all secrets from diagnostics.
+
+## Architecture
+
+Bridge track:
+
+```text
+CLI
+ |
+ +-- config
+ |
+ +-- imap-client
+ |    |
+ |    +-- transport
+ |    +-- command writer
+ |    +-- response parser
+ |    +-- literal reader
+ |    +-- mailbox model
+ |
+ +-- message
+ |    |
+ |    +-- rfc5322 parser
+ |    +-- header decoder
+ |    +-- mime parser
+ |    +-- content-transfer decoder
+ |    +-- body selector
+ |
+ +-- render
+      |
+      +-- table output
+      +-- message output
+      +-- raw output
+      +-- eml export
+```
+
+Native research track:
+
+```text
+CLI
+ |
+ +-- config
+ |
+ +-- proton-api
+ |    |
+ |    +-- http transport        (/Users/user/mine/jerboa-https)
+ |    +-- auth/session research
+ |    +-- mailbox/message API
+ |
+ +-- proton-crypto
+ |    |
+ |    +-- key unlock research
+ |    +-- OpenPGP decrypt       (/Users/user/mine/jerboa-pgp)
+ |    +-- digest/HMAC/KDF       (/Users/user/mine/jerboa-crypto)
+ |
+ +-- message
+ |    |
+ |    +-- rfc5322 parser
+ |    +-- header decoder
+ |    +-- mime parser
+ |
+ +-- render
+```
+
+The Bridge track should not depend on `jerboa-pgp`; Bridge already returns decrypted RFC 5322 mail over IMAP. The native track should reuse `jerboa-pgp` where the Proton payload format matches its current OpenPGP decrypt surface, or extend `jerboa-pgp` if Proton needs lower-level packet/session-key operations.
+
+## Module Layout
+
+Proposed files:
+
+```text
+Makefile
+README.md
+.gitignore
+src/main.scm
+src/config.scm
+src/cli.scm
+src/imap/transport.scm
+src/imap/client.scm
+src/imap/parser.scm
+src/imap/model.scm
+src/mail/rfc5322.scm
+src/mail/header.scm
+src/mail/mime.scm
+src/mail/encoding.scm
+src/render.scm
+test/imap-parser-test.scm
+test/mail-header-test.scm
+test/mime-test.scm
+test/fixtures/simple.eml
+test/fixtures/multipart.eml
+test/fixtures/encoded-headers.eml
+test/fixtures/quoted-printable.eml
+test/fixtures/base64.eml
+```
+
+The exact Jerboa module syntax should follow local Jerboa conventions once we scaffold the project.
+
+## Transport Strategy
+
+Use existing local Jerboa transport code first.
+
+For Bridge IMAP:
+
+- Prefer `/Users/user/mine/jerboa-ssl` plain TCP primitives if Bridge exposes local plaintext IMAP.
+- Use `/Users/user/mine/jerboa-ssl` TLS primitives if Bridge requires TLS.
+- Keep an `openssl s_client` fallback only as a debugging tool.
+
+For native Proton API research:
+
+- Prefer `/Users/user/mine/jerboa-https` for HTTPS requests.
+- Add JSON helpers if current Jerboa stdlib JSON support is insufficient.
+- Keep API code isolated from IMAP code.
+
+The transport interface should hide this choice:
+
+```text
+connect(host, port, tls-mode) -> connection
+send-line(connection, line)
+read-line(connection) -> line
+read-bytes(connection, n) -> bytes
+close(connection)
+```
+
+This lets us replace the transport later without rewriting the IMAP client.
+
+## Native Proton Client Research Track
+
+This track is not required for the first usable version, but it is now realistic enough to document.
+
+### What We Already Have
+
+- HTTPS client capability through `jerboa-https`.
+- TCP/TLS primitives through `jerboa-ssl`.
+- OpenPGP operations through `jerboa-pgp`.
+- Crypto primitives through `jerboa-crypto`.
+- CLI/project patterns from nearby Jerboa projects.
+
+### What Still Needs Research
+
+- Proton account authentication flow.
+- Session persistence and refresh.
+- Whether the current official client API can be used without relying on brittle private behavior.
+- How Proton exposes encrypted user keys to clients.
+- How to unlock/decrypt Proton mailbox keys.
+- Exact encrypted message payload format:
+  - ASCII-armored OpenPGP vs binary packet data.
+  - Whole-message encryption vs per-part/session-key workflows.
+  - Attachment encryption format.
+- Whether `jerboa-pgp` needs lower-level OpenPGP APIs beyond its current `secret armor + passphrase + armored message -> plaintext` wrapper.
+- Key transparency or address verification behavior needed for a secure client.
+- Rate limits and API error behavior.
+
+### Native Track Milestones
+
+1. Read official open-source client code paths for login, key unlock, message list, and message body fetch.
+2. Write a short API map:
+   - endpoints
+   - request headers
+   - auth/session tokens
+   - encrypted payload shapes
+   - required crypto operations
+3. Build a fixture-only decrypt test:
+   - no live Proton credentials
+   - fake or exported encrypted payloads only
+   - call `jerboa-pgp` directly
+4. Build a live metadata-only probe:
+   - login
+   - list labels/folders
+   - list message metadata
+   - no body decrypt yet
+5. Build live read-only decrypt:
+   - fetch one message
+   - decrypt
+   - feed plaintext into the shared RFC 5322/MIME parser
+6. Decide whether native mode is worth supporting beside Bridge mode.
+
+### Native Track Exit Criteria
+
+Native mode should only become a supported path if:
+
+- Auth works repeatably without manual browser scraping.
+- Message decrypt works against current Proton payloads.
+- Secret handling is no worse than Bridge mode.
+- Breakage risk is documented.
+- Tests cover payload parsing and failure cases.
+- Bridge mode remains available as the stable path.
+
+## IMAP Command Scope
+
+Milestone 1 requires:
+
+- Server greeting read.
+- Tagged commands.
+- `CAPABILITY`.
+- `LOGIN`.
+- `LIST "" "*"` or equivalent.
+- `SELECT`.
+- `UID SEARCH`.
+- `UID FETCH`.
+- `LOGOUT`.
+
+Use UID-based access:
+
+```text
+A001 CAPABILITY
+A002 LOGIN "user" "password"
+A003 LIST "" "*"
+A004 SELECT "INBOX"
+A005 UID SEARCH ALL
+A006 UID FETCH 12345 BODY.PEEK[]
+A007 LOGOUT
+```
+
+Later commands:
+
+- `NOOP` for health checks.
+- `UID FETCH <uid> (FLAGS INTERNALDATE RFC822.SIZE BODY.PEEK[HEADER.FIELDS (...)])` for efficient listing.
+- `UID SEARCH FROM`.
+- `UID SEARCH SUBJECT`.
+- `UID SEARCH SINCE`.
+
+Avoid for now:
+
+- `STORE`.
+- `EXPUNGE`.
+- `COPY`.
+- `MOVE`.
+- `APPEND`.
+- `CREATE`.
+- `DELETE`.
+- SMTP commands.
+
+## IMAP Parser Requirements
+
+The parser must handle:
+
+- Tagged completion responses:
+  - `A001 OK ...`
+  - `A001 NO ...`
+  - `A001 BAD ...`
+- Untagged responses:
+  - `* OK ...`
+  - `* CAPABILITY ...`
+  - `* LIST (...) "/" "INBOX"`
+  - `* 23 EXISTS`
+  - `* SEARCH 1 2 3`
+  - `* 1 FETCH (...)`
+- Literals:
+  - `{1234}\r\n`
+  - Then exactly 1234 bytes of message data.
+- Quoted strings.
+- Escaped characters inside quoted strings.
+- NIL atoms.
+- Parenthesized lists.
+
+Keep parser output structured enough that command handlers do not parse raw strings repeatedly.
+
+## Message Decoding Requirements
+
+### Header Parser
+
+Support:
+
+- Header/body split on first blank line.
+- Header unfolding.
+- Case-insensitive header names.
+- Multiple headers with same name.
+- Raw header preservation.
+
+Important headers:
+
+- `From`
+- `To`
+- `Cc`
+- `Bcc`
+- `Subject`
+- `Date`
+- `Message-ID`
+- `In-Reply-To`
+- `References`
+- `Content-Type`
+- `Content-Transfer-Encoding`
+
+### Encoded Header Words
+
+Support RFC 2047 forms:
+
+```text
+=?UTF-8?B?...?=
+=?UTF-8?Q?...?=
+```
+
+Initial charsets:
+
+- UTF-8.
+- US-ASCII.
+- ISO-8859-1 if available.
+
+### MIME
+
+Support:
+
+- Single-part text messages.
+- `multipart/alternative`.
+- `multipart/mixed`.
+- Nested multipart.
+- Boundary parsing.
+- Content type parameters.
+- Charset parameters.
+- Attachment metadata.
+
+Body selection:
+
+1. Prefer `text/plain`.
+2. Fall back to simple text extraction from `text/html`.
+3. Indicate attachments by filename/content type without decoding them into the main body view.
+
+### Content-Transfer-Encoding
+
+Support:
+
+- `7bit`.
+- `8bit`.
+- `binary`.
+- `base64`.
+- `quoted-printable`.
+
+## Output Design
+
+### Folder Listing
+
+```text
+Name                  Flags
+INBOX                 \HasNoChildren
+Archive               \HasNoChildren
+Sent                  \HasNoChildren
+```
+
+### Message Listing
+
+```text
+UID       Date                  From                         Subject
+12345     2026-05-28 09:14      person@example.com           Example subject
+```
+
+### Message View
+
+```text
+From: Person <person@example.com>
+To: User <user@proton.me>
+Date: Thu, 28 May 2026 09:14:00 -0600
+Subject: Example subject
+
+Plain text body...
+
+Attachments:
+- invoice.pdf application/pdf 182341 bytes
+```
+
+## Implementation Milestones
+
+### Milestone 0: Project Setup
+
+Deliverables:
+
+- Initialize project structure.
+- Add `README.md`.
+- Add `.gitignore`.
+- Add `Makefile`.
+- Add a placeholder CLI entry point.
+- Verify `jerboa` can run or compile the entry point.
+
+Exit criteria:
+
+- `make test` or equivalent runs a placeholder test.
+- `protonmail-read --help` prints basic usage.
+
+### Milestone 1: Bridge Connectivity Probe
+
+Deliverables:
+
+- Load config from environment variables.
+- Connect to Bridge IMAP.
+- Read greeting.
+- Send `CAPABILITY`.
+- Send `LOGIN`.
+- Send `LIST`.
+- Send `LOGOUT`.
+- Implement `doctor`.
+
+Exit criteria:
+
+- `protonmail-read doctor` confirms:
+  - Config is present.
+  - Bridge host/port is reachable.
+  - Login succeeds.
+  - Mailbox list can be fetched.
+- Secrets are redacted in all output.
+
+### Milestone 2: IMAP Parser
+
+Deliverables:
+
+- Structured parser for tagged/untagged responses.
+- Literal support.
+- Quoted string support.
+- Unit tests from canned transcripts.
+
+Exit criteria:
+
+- Parser handles realistic `LIST`, `SEARCH`, and `FETCH` responses.
+- Parser tests cover error responses and literals.
+
+### Milestone 3: Folder and Message Listing
+
+Deliverables:
+
+- `folders`.
+- `list --folder`.
+- Efficient header-only fetch where possible.
+- UID-based listing.
+
+Exit criteria:
+
+- Can list mailbox folders.
+- Can list recent messages without fetching full message bodies.
+- Does not mark messages as read.
+
+### Milestone 4: Raw Fetch and Export
+
+Deliverables:
+
+- `raw --folder --uid`.
+- `export-eml --folder --uid --output`.
+- Full message fetch using `BODY.PEEK[]`.
+
+Exit criteria:
+
+- Can fetch a message as raw RFC 5322 text.
+- Can export `.eml`.
+- Message remains unread if it was unread before fetching.
+
+### Milestone 5: Header and MIME Decoding
+
+Deliverables:
+
+- RFC 5322 parser.
+- RFC 2047 header decoder.
+- MIME parser.
+- Base64 decoder.
+- Quoted-printable decoder.
+- Body selector.
+- Fixture tests.
+
+Exit criteria:
+
+- `show --folder --uid` prints decoded headers and readable body for common emails.
+- Fixtures cover plain text, multipart, encoded headers, base64, and quoted-printable.
+
+### Milestone 6: Search
+
+Deliverables:
+
+- `search --folder --from`.
+- `search --folder --subject`.
+- `search --folder --since`.
+- Combine criteria where IMAP supports it.
+
+Exit criteria:
+
+- Search returns UIDs and summary rows.
+- Search output can be piped into `show` or `export-eml` workflows manually.
+
+### Milestone 7: Hardening
+
+Deliverables:
+
+- Timeout handling.
+- Better IMAP error messages.
+- Redacted debug logs.
+- Config validation.
+- Tests for malformed server responses.
+- Documentation for Bridge setup.
+
+Exit criteria:
+
+- Common Bridge failures produce useful errors:
+  - Bridge not running.
+  - Wrong port.
+  - Bad credentials.
+  - TLS mismatch.
+  - Folder not found.
+  - UID not found.
+
+## Testing Strategy
+
+### Unit Tests
+
+Test without Proton Mail or Bridge:
+
+- IMAP response parser.
+- IMAP literal handling.
+- Header parsing.
+- Header decoding.
+- MIME boundary parsing.
+- Content-transfer decoding.
+- CLI option parsing.
+- Config loading and redaction.
+
+### Fixture Tests
+
+Use committed fake `.eml` files only. Do not commit real email.
+
+Fixture categories:
+
+- Simple plain text.
+- Multipart alternative.
+- Multipart mixed with attachment.
+- Encoded subject.
+- Encoded sender name.
+- Base64 body.
+- Quoted-printable body.
+- HTML-only body.
+- Malformed but recoverable message.
+
+### Integration Tests
+
+Run only when explicitly enabled:
+
+```text
+PROTON_BRIDGE_INTEGRATION=1 make integration-test
+```
+
+Rules:
+
+- Never require real credentials in CI.
+- Never print message bodies in integration test logs by default.
+- Prefer a dedicated test mailbox/folder.
+- Use read-only commands only.
+
+## Security Checklist
+
+- No secrets committed.
+- `.gitignore` covers local config and exported mail.
+- No password printed in normal output.
+- No password printed in debug output.
+- No raw message printed unless user explicitly runs `raw`.
+- No message body cached by default.
+- No mailbox mutation commands.
+- Use `BODY.PEEK[]`.
+- Treat exported `.eml` files as sensitive.
+- Document that Bridge exposes decrypted mail to local clients.
+
+## Documentation Needed
+
+README sections:
+
+- What this tool does.
+- What this tool does not do.
+- Proton Bridge requirement.
+- Paid plan requirement.
+- How to get Bridge IMAP settings.
+- Environment variable setup.
+- Example commands.
+- Security notes.
+- Troubleshooting.
+- Development setup.
+- Test commands.
+
+## Open Questions
+
+- Does local Bridge require TLS, STARTTLS, or plain local IMAP on this machine?
+- What exact port is Bridge configured to use?
+- Does Bridge return stable UID behavior for all target folders?
+- Should we support a local keychain lookup later, or keep environment variables only?
+- Should `show` render HTML as stripped text, or should it optionally write HTML to a file?
+- Should attachment export be included later?
+- For native mode, does Proton's current message payload fit `jerboa-pgp`'s existing armored-message decrypt API?
+- For native mode, what Proton API/auth surfaces are stable enough to rely on?
+
+## Future Enhancements
+
+- Attachment export.
+- Local encrypted cache.
+- Incremental sync metadata.
+- JSON output for scripting.
+- Notmuch/Maildir export.
+- Read-only TUI.
+- Optional SMTP support after read-only behavior is solid.
+- Direct Proton API mode using `jerboa-https`, `jerboa-crypto`, and `jerboa-pgp`.
+- Lower-level `jerboa-pgp` APIs if Proton payloads need packet/session-key operations instead of whole armored-message decrypt.
+
+## First Implementation Slice
+
+Build the smallest useful vertical path:
+
+1. `protonmail-read doctor`
+2. Read environment config.
+3. Connect to Bridge.
+4. Login.
+5. List folders.
+6. Logout.
+7. Add transcript tests for the parser.
+
+This proves the local Proton Bridge integration before spending time on MIME decoding and richer CLI behavior.
diff --git a/protonmail/cli.ss b/protonmail/cli.ss
new file mode 100644
index 0000000..f67cbf8
--- /dev/null
+++ b/protonmail/cli.ss
@@ -0,0 +1,83 @@
+#!chezscheme
+;;; (protonmail cli) - subcommand dispatch for protonmail-read.
+
+(library (protonmail cli)
+  (export run-cli usage-string)
+
+  (import (except (chezscheme)
+                  make-hash-table hash-table?
+                  sort sort!
+                  printf fprintf
+                  path-extension path-absolute?
+                  with-input-from-string with-output-to-string
+                  iota 1+ 1-
+                  partition
+                  make-date make-time)
+          (protonmail config))
+
+  (define version "0.0.0-phase0")
+
+  (define usage-string
+    (string-append
+      "usage: protonmail-read <command> [options]\n"
+      "\n"
+      "commands:\n"
+      "  folders                         List folders (phase 3)\n"
+      "  list --folder INBOX --limit N   List messages (phase 3)\n"
+      "  show --folder INBOX --uid UID   Decode one message (phase 5)\n"
+      "  raw --folder INBOX --uid UID    Print raw RFC 5322 message (phase 4)\n"
+      "  export-eml --uid UID --output F Export raw message (phase 4)\n"
+      "  search --folder INBOX ...       Search messages (phase 6)\n"
+      "  doctor                          Check local configuration\n"
+      "  version                         Print version\n"
+      "  help                            Print this help\n"))
+
+  (define (println s)
+    (display s)
+    (newline))
+
+  (define (die code msg)