···11+# CLAUDE.md
22+33+This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
44+55+## Build & Dev Commands
66+77+- `make build` — compile to `./neomd` (also regenerates `docs/keybindings.md` from `internal/ui/keys.go`)
88+- `make run ARGS="..."` — build and run
99+- `make install` — install to `~/.local/bin/neomd`
1010+- `make test` — `go test ./...`
1111+- `make vet` / `make fmt` / `make tidy`
1212+- `make docs` — regenerate keybindings doc from `internal/ui/keys.go` (runs as part of `build`)
1313+- `make send-test TO=addr` — run `./cmd/sendtest` to send a test email
1414+- `make demo` / `make demo-hp` — run with demo configs at `~/.config/neomd-demo/` and `~/.config/neomd-demo-hostpoint/`
1515+- `make benchmark` — IMAP latency benchmark (requires `IMAP_PASS_SIMU`, `IMAP_APPPASS_GMAIL_NEOMD` env vars)
1616+- `make android` — cross-compile ARM64 for Termux
1717+- Single test: `go test ./internal/smtp -run TestBuildMessage`
1818+1919+Requires Go 1.22+. Binary version is injected via `-ldflags -X main.version=$(git describe)`.
2020+2121+## Architecture
2222+2323+**Entry point:** `cmd/neomd/main.go` wires config → IMAP client → bubbletea program. Two side CLIs: `cmd/docs` (regenerates keybindings doc) and `cmd/sendtest` (sender smoke test).
2424+2525+**TUI is a single bubbletea Model** in `internal/ui/model.go` with a `viewState` enum state machine: `stateInbox`, `stateReading`, `stateCompose`, `statePresend`, `stateHelp`. All state transitions flow through `Update()`. Sibling files specialize views but share the one model:
2626+- `inbox.go` — folder tabs, list, threading connector rendering
2727+- `reader.go` — glamour-rendered email, attachments, numbered links
2828+- `compose.go` — multi-step compose form (To/CC/BCC/Subject)
2929+- `search.go`, `cmdline.go`, `thread.go`, `keys.go`, `styles.go`
3030+3131+**Keybindings are declared once** in `internal/ui/keys.go` and drive both the in-app help overlay and the generated `docs/keybindings.md`. When adding a binding, edit that table — do not hand-edit the markdown docs.
3232+3333+**Compose flow:** user's `$EDITOR` (nvim) opens a temp `neomd-*.md` file with a prelude of `# [neomd: to: ...]` / `# [neomd: subject: ...]` headers built by `internal/editor/editor.go`. On editor exit, parsing extracts headers and `[attach] /path` inline lines (plain-text marker, NOT HTML comments — treesitter hides those). Then `statePresend` shows a review screen before sending.
3434+3535+**MIME structure** (`internal/smtp/sender.go`, `BuildMessage` is the exported entry point, reused by draft-save):
3636+- no attachments → `multipart/alternative` (text/plain + goldmark HTML)
3737+- file attachments only → `multipart/mixed > multipart/alternative`
3838+- inline images only → `multipart/related > (alternative + image parts with Content-ID)`
3939+- both → `multipart/mixed > (multipart/related > alt+images) + file parts`
4040+- Image extensions: `.png .jpg .jpeg .gif .webp .svg`. `imgSrcRe` rewrites local `<img src="/abs/path">` to `cid:` refs.
4141+4242+**IMAP client** (`internal/imap/client.go`) uses `go-imap/v2` (beta). Known API quirks to preserve:
4343+- `imapclient.FetchMessageBuffer` (not `FetchedMessage`)
4444+- `conn.Copy()` / `conn.Store()` (not UID-prefixed variants)
4545+- `BodySection[0].Bytes` for raw MIME
4646+- APPEND pattern: `conn.Append(folder, size, opts)` → `.Write(raw)` → `.Close()` → `.Wait()`
4747+- `go-message` v0.18.2: `mail.PartHeader` lacks `ContentType()` — type-assert to `*mail.InlineHeader` / `*mail.AttachmentHeader`
4848+- `bubbletea` v1.3.10: key type is `tea.KeyMsg` (not `KeyPressMsg`)
4949+5050+Folder operations prefer RFC 6851 MOVE; `u` undo uses UIDPLUS destination UIDs captured on move/delete.
5151+5252+**Screener** (`internal/screener/`) reads line-based lists of email addresses from paths defined in config. Default paths are under `~/.config/neomd/lists/`. Classification (`I`/`O`/`F`/`P`) appends to the corresponding list file and moves the message to the matching folder. Auto-screening runs on Inbox load and on a 5-minute background timer (`ui.background_sync_interval`).
5353+5454+**Config** (`internal/config/`) — TOML at `~/.config/neomd/config.toml`, auto-created with placeholders. Supports multiple `[[accounts]]` and SMTP-only `[[senders]]` aliases (cycled with `ctrl+f` in compose/pre-send). `-config PATH` flag overrides location.
5555+5656+## Project-Specific Conventions
5757+5858+- **Keep diffs minimal** — fix the specific thing asked; do not refactor adjacent code.
5959+- **Avoid modifier keys for new bindings** — user's tmux prefix is `C-t`, and `ctrl+a`/`ctrl+e` collide with bubbles textinput line-start/end. Prefer plain letters, especially on the pre-send screen.
6060+- **Inline markers must be visible plain text** — use `[attach] /path`, never HTML comments (hidden by treesitter in the neovim compose buffer).
6161+- **Neovim integration lives in dotfiles**, not in this repo: `/home/sspaeti/git/general/dotfiles/nvim/.config/nvim/lua/sspaeti/custom.lua` defines the `<leader>a` yazi picker scoped to `BufEnter neomd-*.md`.
6262+- Screener list files historically live at `~/.dotfiles/neomd/.lists/` for this user (overridden in their config), though the default for new installs is `~/.config/neomd/lists/`.