A minimal email TUI where you read with Markdown and write in Neovim. neomd.ssp.sh/docs
email markdown neovim tui
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

add claude initial

sspaeti e4bf8532 8886a601

+62
+62
CLAUDE.md
··· 1 + # CLAUDE.md 2 + 3 + This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. 4 + 5 + ## Build & Dev Commands 6 + 7 + - `make build` — compile to `./neomd` (also regenerates `docs/keybindings.md` from `internal/ui/keys.go`) 8 + - `make run ARGS="..."` — build and run 9 + - `make install` — install to `~/.local/bin/neomd` 10 + - `make test` — `go test ./...` 11 + - `make vet` / `make fmt` / `make tidy` 12 + - `make docs` — regenerate keybindings doc from `internal/ui/keys.go` (runs as part of `build`) 13 + - `make send-test TO=addr` — run `./cmd/sendtest` to send a test email 14 + - `make demo` / `make demo-hp` — run with demo configs at `~/.config/neomd-demo/` and `~/.config/neomd-demo-hostpoint/` 15 + - `make benchmark` — IMAP latency benchmark (requires `IMAP_PASS_SIMU`, `IMAP_APPPASS_GMAIL_NEOMD` env vars) 16 + - `make android` — cross-compile ARM64 for Termux 17 + - Single test: `go test ./internal/smtp -run TestBuildMessage` 18 + 19 + Requires Go 1.22+. Binary version is injected via `-ldflags -X main.version=$(git describe)`. 20 + 21 + ## Architecture 22 + 23 + **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). 24 + 25 + **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: 26 + - `inbox.go` — folder tabs, list, threading connector rendering 27 + - `reader.go` — glamour-rendered email, attachments, numbered links 28 + - `compose.go` — multi-step compose form (To/CC/BCC/Subject) 29 + - `search.go`, `cmdline.go`, `thread.go`, `keys.go`, `styles.go` 30 + 31 + **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. 32 + 33 + **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. 34 + 35 + **MIME structure** (`internal/smtp/sender.go`, `BuildMessage` is the exported entry point, reused by draft-save): 36 + - no attachments → `multipart/alternative` (text/plain + goldmark HTML) 37 + - file attachments only → `multipart/mixed > multipart/alternative` 38 + - inline images only → `multipart/related > (alternative + image parts with Content-ID)` 39 + - both → `multipart/mixed > (multipart/related > alt+images) + file parts` 40 + - Image extensions: `.png .jpg .jpeg .gif .webp .svg`. `imgSrcRe` rewrites local `<img src="/abs/path">` to `cid:` refs. 41 + 42 + **IMAP client** (`internal/imap/client.go`) uses `go-imap/v2` (beta). Known API quirks to preserve: 43 + - `imapclient.FetchMessageBuffer` (not `FetchedMessage`) 44 + - `conn.Copy()` / `conn.Store()` (not UID-prefixed variants) 45 + - `BodySection[0].Bytes` for raw MIME 46 + - APPEND pattern: `conn.Append(folder, size, opts)` → `.Write(raw)` → `.Close()` → `.Wait()` 47 + - `go-message` v0.18.2: `mail.PartHeader` lacks `ContentType()` — type-assert to `*mail.InlineHeader` / `*mail.AttachmentHeader` 48 + - `bubbletea` v1.3.10: key type is `tea.KeyMsg` (not `KeyPressMsg`) 49 + 50 + Folder operations prefer RFC 6851 MOVE; `u` undo uses UIDPLUS destination UIDs captured on move/delete. 51 + 52 + **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`). 53 + 54 + **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. 55 + 56 + ## Project-Specific Conventions 57 + 58 + - **Keep diffs minimal** — fix the specific thing asked; do not refactor adjacent code. 59 + - **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. 60 + - **Inline markers must be visible plain text** — use `[attach] /path`, never HTML comments (hidden by treesitter in the neovim compose buffer). 61 + - **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`. 62 + - 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/`.