Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

# 2026-05-06
- **Theming with 6 built-in palettes** — pick via `[ui].theme = "kanagawa"` (default, byte-for-byte identical to pre-theme state), `kanagawa-paper`, `kanagawa-light` (only light theme — Lotus palette on a paperwhite `#F2EFE9` background), `rose-pine`, `gruvbox`, or `osaka-jade`. Optional top-level `[theme]` block overrides individual colour slots (`primary`, `unread`, `error`, `bg`, …) on top of any built-in. `ApplyTheme` runs in `ui.New()` before any rendering, so theme switching needs only a config edit + restart. Regression test verifies the kanagawa default does not drift; theme tests cover override merge, unknown-name fallback, and round-trip uniqueness
- **AI handoff key in pre-send (`i`)** — new `[ai].command` config (default `claude`) wires any LLM CLI (`claude`, `codex`, `aichat`, `sgpt`, …) to the pre-send `i` key. neomd writes the current draft to `/tmp/neomd/neomd-ai-*.md` with the standard `# [neomd: ...]` headers, spawns `<command> [args...] <path>`, and re-reads the file on exit so header and body edits round-trip back into the draft (same parser as the regular editor flow). Quit the AI tool to return to neomd's pre-send screen. Pre-send footer surfaces the active command (`i AI (claude, quit to return)`). `nvim` is intentionally not a useful choice here — the compose buffer is already in nvim before pre-send, so spawning nvim on `i` would just re-edit. Set `command = ""` to disable the binding
- **iCalendar RSVP + local calendar handoff** — emails with a `text/calendar` part or `.ics` attachment now show a `📅` summary card in the reader header (`event title · date · location`). Leader chord `<space> v {a|d|t|o}` sends an RFC 5546/6047 (iMIP) accept/decline/tentative reply, or opens the `.ics` in `[calendar].open_command` (default `xdg-open`, set to `morgen`/`khal`/`/usr/bin/gnome-calendar` to force a specific app). MIME envelope: `multipart/mixed > [multipart/alternative > (text/plain + text/calendar;method=REPLY)] + .ics attachment` with `Subject: Accepted: <event>` (matches Gmail's native button format) and bracketed RFC 5322 `In-Reply-To` headers. RSVPs save to the `Sent` folder and mark the original invite `\Answered`. **Reliability note:** Outlook 365 / Exchange / Apple iCloud / CalDAV servers auto-process iMIP REPLIES server-side; Gmail has deprioritized iMIP processing in 2026, so Gmail organizers may need to manually note your reply (or just use Gmail's native Yes/No button for Gmail-originated invites). New self-contained `internal/calendar/` package on top of `arran4/golang-ical`; new `internal/smtp/rsvp.go` for the iMIP MIME envelope. Only the first `VEVENT` is processed; recurring rules, counter-proposals, and cancellations are out of scope
- **OS keyring credential storage** ([#5](https://github.com/ssp-data/neomd/pull/5), thanks [@notthatjesus](https://github.com/notthatjesus)) — set `password = "keyring"` in any `[[accounts]]` block to fetch the IMAP/SMTP password from the OS keyring (macOS Keychain, Linux Secret Service via gnome-keyring/kwallet, Windows Credential Manager) at startup. OAuth2 tokens also persist in the keyring with explicit file fallback for headless/SSH systems where no keyring service is available. Sentinel resolution runs inside `config.Load()` so every consumer — IMAP at boot, SMTP at send, `[[senders]]` aliases that reference an account — sees the resolved password automatically without per-call lookups. New `internal/keyring/` package with mock-backed tests; storage keys are `neomd/account/<name>/{password|oauth2}`

# 2026-05-04
- **Fix: send from `imap_disabled = true` account no longer panics** — sending an email from an account configured as send-only (typically Gmail with `imap_disabled = true`) crashed the TUI with `nil pointer dereference` in `tokenSourceFor` because the helper called `.TokenSource()` on the intentionally-nil IMAP client. The same nil-deref existed in `imapCli`, `imapCliForAccount`, and `primaryIMAPClient` — any code path that resolved an IMAP client for a send-only account would crash. All four helpers now skip nil entries (and fall back to the first non-nil client where appropriate); `sendEmailCmd` also guards `cli.SaveSent` and `replyCli.MarkAnswered` so a fully send-only configuration silently skips the Sent-folder copy instead of panicking. Four regression tests added in `internal/ui/imap_client_helpers_test.go`
- **Fix: notify state key keeps IMAP folder name (preserves baselines across upgrades)** — yesterday's label-normalisation fix accidentally changed the persisted state key from `Personal|INBOX` to `Personal|Inbox`, which would have re-baselined every existing user on upgrade and silently swallowed one round of notifications. `MaybeNotify` now takes both the IMAP name (used for the state key) and the UI label (used only for the allowlist comparison) so existing `notify_state.json` files keep working untouched. Regression test added
Expand Down
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ Folder operations prefer RFC 6851 MOVE; `u` undo uses UIDPLUS destination UIDs c
- `internal/daemon/` — headless background mode (`--headless`): screener loop without TUI
- `internal/mailtls/` — TLS/STARTTLS connection helpers
- `internal/oauth2/` — OAuth2 flow for Gmail/Office365
- `internal/calendar/` — iCalendar (.ics) parsing + iMIP RSVP reply construction (`arran4/golang-ical`); used by reader card and `<space> v {a|d|t}` chord
- `internal/integration_test.go` — integration tests (live IMAP/SMTP); lives at package level, not in a sub-package

**Spy pixel detection** (`internal/imap/tracker_list.go` + `client.go`): Two-layer approach — (1) curated denylist of 150+ tracking services in `KnownTrackers` with `IdentifyTracker()` for attribution ("Mailchimp", "HubSpot"); (2) generic 1×1 pixel heuristic via `detectSpyPixels()` on raw HTML. Results flow through `SpyPixelInfo` struct returned by `FetchBody()` and `ScanSpyPixels()`. Cached to `~/.cache/neomd/spy_pixels` (format: `+key` for spy, `-key` for scanned clean).
Expand Down
5 changes: 4 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ Keep your inbox clean without effort.
### Composing & Sending

- **Pre-send review** — after closing the editor, review To/Subject/body before sending; attach files, save to Drafts, or re-open the editor — no accidental sends [→](https://neomd.ssp.sh/docs/sending/#pre-send-review)
- **AI handoff (`i` in pre-send)** — `[ai].command` (default `claude`) wires any LLM CLI (claude, codex, aichat, …) to the pre-send `i` key; neomd writes the draft to a temp markdown, spawns the command, and re-reads on exit when you quit the tool — header and body edits round-trip back. No in-app AI dependency [→](https://neomd.ssp.sh/docs/configuration/#ai-handoff-pre-send-i-key)
- **Attachments** — attach files from the pre-send screen via yazi (`a`); images appear inline in the email body, other files as attachments; also attach from within Neovim via `<leader>a`; the reader lists all attachments and `1`–`9` downloads and opens them [→](https://neomd.ssp.sh/docs/sending/#attachments)
- **Emoji reactions** — press `ctrl+e` from inbox or reader to react with emoji (👍 ❤️ 😂 🎉 🙏 💯 👀 ✅); instant send with proper threading and quoted message history, no editor needed [→](https://neomd.ssp.sh/docs/sending/#emoji-reactions)
- **Multi-select** — `m` marks emails, then batch-delete, move, or screen them all at once [→](https://neomd.ssp.sh/docs/keybindings/#multi-select--undo)
Expand All @@ -166,6 +167,7 @@ Keep your inbox clean without effort.
### Reading

- **Threaded inbox** — related emails grouped together with a vertical connector line (`│`/`╰`), Twitter-style; threads detected via `In-Reply-To`/`Message-ID` headers with a subject+participant fallback; newest reply on top, root at bottom; `·` reply indicator shows which emails you've answered [→](https://neomd.ssp.sh/docs/reading/#threaded-inbox)
- **iCalendar RSVP** — meeting invites (`text/calendar` / `.ics`) show a `📅` card in the reader; leader chord `<space> v {a|d|t}` sends an RFC 5546/6047 (iMIP) accept/decline/tentative reply; `<space> v o` hands the `.ics` off to your local calendar app via `[calendar].open_command` (default `xdg-open`, set to `morgen`, `khal`, etc.) [→](https://neomd.ssp.sh/docs/configuration/#calendar-invites-icalendar--imip)
- **Conversation view** — `T` or `:thread` shows the full conversation across folders (Inbox, Sent, Archive, etc.) in a temporary tab with `[Folder]` prefix; see your replies alongside received emails [→](https://neomd.ssp.sh/docs/reading/#conversation-view)
- **Link opener** — links in emails are numbered `[1]`–`[0]` in the reader header; press `space+digit` to open in `$BROWSER` [→](https://neomd.ssp.sh/docs/reading/#links)
- **Everything view** — `ge` or `:everything` shows the 50 most recent emails across all folders; find emails that were screened out, moved to spam, or otherwise hard to locate [→](https://neomd.ssp.sh/docs/keybindings/#folders)
Expand All @@ -179,14 +181,15 @@ Keep your inbox clean without effort.
- **CC, BCC, Reply-all** — optional Cc/Bcc fields (toggle with `ctrl+b`); `R` in the reader replies to sender + all CC recipients [→](https://neomd.ssp.sh/docs/sending/#cc-bcc-reply-all-and-forward)
- **Drafts** — `d` in pre-send saves to Drafts (IMAP APPEND); `E` in the reader re-opens a draft as an editable compose; compose sessions are auto-backed up to `~/.cache/neomd/drafts/` so you never lose an unsent email (`:recover` to reopen) [→](https://neomd.ssp.sh/docs/sending/#drafts)
- **Multiple From addresses** — define SMTP-only `[[senders]]` aliases (e.g. `s@ssp.sh` through an existing account); cycle with `ctrl+f` in compose and pre-send; sent copies always land in the Sent folder [→](https://neomd.ssp.sh/docs/sending/#multiple-from-addresses)
- **OS keyring credentials** — set `password = "keyring"` to fetch the IMAP/SMTP password from your OS keyring (macOS Keychain, Linux Secret Service, Windows Credential Manager); OAuth2 tokens also stored in keyring with file fallback for headless/SSH; resolution happens at config load so `[[senders]]` aliases inherit the resolved password automatically [→](https://neomd.ssp.sh/docs/configuration/#storing-passwords-in-the-os-keyring)
- **HTML signatures** — configure separate text and HTML signatures; text signature appears in editor and plain text part, HTML signature in HTML part only; use `[html-signature]` placeholder to control inclusion per-email [→](https://neomd.ssp.sh/docs/configuration/#html-signatures)
- **Address autocomplete** — To/Cc/Bcc fields autocomplete from screener lists; navigate with `ctrl+n`/`ctrl+p`, accept with `tab`

### Under the Hood

- **IMAP + SMTP** — direct connection via RFC 6851 MOVE, no local sync daemon required; stays in sync if you use it on mobile or different device [→](https://neomd.ssp.sh/docs/configuration/)
- **RFC 5322 compliant email delivery** — Message-IDs use sender's domain, proper MIME multipart/alternative structure (text/plain before text/html), quoted-printable encoding, and all required headers; ensures deliverability across all providers, spam filter compatibility, and correct email threading [→](https://neomd.ssp.sh/docs/configuration/email-standards/)
- **Kanagawa theme** — colors from the [kanagawa.nvim](https://github.com/rebelot/kanagawa.nvim) palette
- **Themes** — six built-in palettes (`kanagawa` default, `kanagawa-paper`, `kanagawa-light` for daylight terminals, `rose-pine`, `gruvbox`, `osaka-jade`); pick via `[ui].theme = "..."` and override individual colour slots in an optional `[theme]` block [→](https://neomd.ssp.sh/docs/configuration/#theming)

> [!NOTE]
> neomd's **speed** depends entirely on your IMAP provider. On Hostpoint (the provider I use), a folder switch takes **~33ms** which feels instant. On Gmail, the same operation takes **~570ms** which is noticeably slow. See [Benchmark](#benchmark) for full details and how to test your provider.
Expand Down
5 changes: 4 additions & 1 deletion docs/content/docs/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ Keep your inbox clean without effort.
### Composing & Sending

- **Pre-send review** — after closing the editor, review To/Subject/body before sending; attach files, save to Drafts, or re-open the editor — no accidental sends [→](https://neomd.ssp.sh/docs/sending/#pre-send-review)
- **AI handoff (`i` in pre-send)** — `[ai].command` (default `claude`) wires any LLM CLI (claude, codex, aichat, …) to the pre-send `i` key; neomd writes the draft to a temp markdown, spawns the command, and re-reads on exit when you quit the tool — header and body edits round-trip back. No in-app AI dependency [→](https://neomd.ssp.sh/docs/configuration/#ai-handoff-pre-send-i-key)
- **Attachments** — attach files from the pre-send screen via yazi (`a`); images appear inline in the email body, other files as attachments; also attach from within Neovim via `<leader>a`; the reader lists all attachments and `1`–`9` downloads and opens them [→](https://neomd.ssp.sh/docs/sending/#attachments)
- **Emoji reactions** — press `ctrl+e` from inbox or reader to react with emoji (👍 ❤️ 😂 🎉 🙏 💯 👀 ✅); instant send with proper threading and quoted message history, no editor needed [→](https://neomd.ssp.sh/docs/sending/#emoji-reactions)
- **Multi-select** — `m` marks emails, then batch-delete, move, or screen them all at once [→](https://neomd.ssp.sh/docs/keybindings/#multi-select--undo)
Expand All @@ -169,6 +170,7 @@ Keep your inbox clean without effort.
### Reading

- **Threaded inbox** — related emails grouped together with a vertical connector line (`│`/`╰`), Twitter-style; threads detected via `In-Reply-To`/`Message-ID` headers with a subject+participant fallback; newest reply on top, root at bottom; `·` reply indicator shows which emails you've answered [→](https://neomd.ssp.sh/docs/reading/#threaded-inbox)
- **iCalendar RSVP** — meeting invites (`text/calendar` / `.ics`) show a `📅` card in the reader; leader chord `<space> v {a|d|t}` sends an RFC 5546/6047 (iMIP) accept/decline/tentative reply; `<space> v o` hands the `.ics` off to your local calendar app via `[calendar].open_command` (default `xdg-open`, set to `morgen`, `khal`, etc.) [→](https://neomd.ssp.sh/docs/configuration/#calendar-invites-icalendar--imip)
- **Conversation view** — `T` or `:thread` shows the full conversation across folders (Inbox, Sent, Archive, etc.) in a temporary tab with `[Folder]` prefix; see your replies alongside received emails [→](https://neomd.ssp.sh/docs/reading/#conversation-view)
- **Link opener** — links in emails are numbered `[1]`–`[0]` in the reader header; press `space+digit` to open in `$BROWSER` [→](https://neomd.ssp.sh/docs/reading/#links)
- **Everything view** — `ge` or `:everything` shows the 50 most recent emails across all folders; find emails that were screened out, moved to spam, or otherwise hard to locate [→](https://neomd.ssp.sh/docs/keybindings/#folders)
Expand All @@ -182,14 +184,15 @@ Keep your inbox clean without effort.
- **CC, BCC, Reply-all** — optional Cc/Bcc fields (toggle with `ctrl+b`); `R` in the reader replies to sender + all CC recipients [→](https://neomd.ssp.sh/docs/sending/#cc-bcc-reply-all-and-forward)
- **Drafts** — `d` in pre-send saves to Drafts (IMAP APPEND); `E` in the reader re-opens a draft as an editable compose; compose sessions are auto-backed up to `~/.cache/neomd/drafts/` so you never lose an unsent email (`:recover` to reopen) [→](https://neomd.ssp.sh/docs/sending/#drafts)
- **Multiple From addresses** — define SMTP-only `[[senders]]` aliases (e.g. `s@ssp.sh` through an existing account); cycle with `ctrl+f` in compose and pre-send; sent copies always land in the Sent folder [→](https://neomd.ssp.sh/docs/sending/#multiple-from-addresses)
- **OS keyring credentials** — set `password = "keyring"` to fetch the IMAP/SMTP password from your OS keyring (macOS Keychain, Linux Secret Service, Windows Credential Manager); OAuth2 tokens also stored in keyring with file fallback for headless/SSH; resolution happens at config load so `[[senders]]` aliases inherit the resolved password automatically [→](https://neomd.ssp.sh/docs/configuration/#storing-passwords-in-the-os-keyring)
- **HTML signatures** — configure separate text and HTML signatures; text signature appears in editor and plain text part, HTML signature in HTML part only; use `[html-signature]` placeholder to control inclusion per-email [→](https://neomd.ssp.sh/docs/configuration/#html-signatures)
- **Address autocomplete** — To/Cc/Bcc fields autocomplete from screener lists; navigate with `ctrl+n`/`ctrl+p`, accept with `tab`

### Under the Hood

- **IMAP + SMTP** — direct connection via RFC 6851 MOVE, no local sync daemon required; stays in sync if you use it on mobile or different device [→](https://neomd.ssp.sh/docs/configuration/)
- **RFC 5322 compliant email delivery** — Message-IDs use sender's domain, proper MIME multipart/alternative structure (text/plain before text/html), quoted-printable encoding, and all required headers; ensures deliverability across all providers, spam filter compatibility, and correct email threading [→](https://neomd.ssp.sh/docs/configuration/email-standards/)
- **Kanagawa theme** — colors from the [kanagawa.nvim](https://github.com/rebelot/kanagawa.nvim) palette
- **Themes** — six built-in palettes (`kanagawa` default, `kanagawa-paper`, `kanagawa-light` for daylight terminals, `rose-pine`, `gruvbox`, `osaka-jade`); pick via `[ui].theme = "..."` and override individual colour slots in an optional `[theme]` block [→](https://neomd.ssp.sh/docs/configuration/#theming)

{{< callout type="info" >}}
neomd's **speed** depends entirely on your IMAP provider. On Hostpoint (the provider I use), a folder switch takes **~33ms** which feels instant. On Gmail, the same operation takes **~570ms** which is noticeably slow. See [Benchmark](#benchmark) for full details and how to test your provider.
Expand Down
Loading
Loading