Skip to content

Commit caf825d

Browse files
authored
Merge pull request #14 from ssp-data/feature-themes-ai-calendar
Add theming, calendar RSVP and Claude AI integration
2 parents 54d6ae4 + 56e2d6e commit caf825d

21 files changed

Lines changed: 1819 additions & 83 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Changelog
22

3+
# 2026-05-06
4+
- **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
5+
- **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
6+
- **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
7+
- **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}`
8+
39
# 2026-05-04
410
- **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`
511
- **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

CLAUDE.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ Folder operations prefer RFC 6851 MOVE; `u` undo uses UIDPLUS destination UIDs c
7272
- `internal/daemon/` — headless background mode (`--headless`): screener loop without TUI
7373
- `internal/mailtls/` — TLS/STARTTLS connection helpers
7474
- `internal/oauth2/` — OAuth2 flow for Gmail/Office365
75+
- `internal/calendar/` — iCalendar (.ics) parsing + iMIP RSVP reply construction (`arran4/golang-ical`); used by reader card and `<space> v {a|d|t}` chord
7576
- `internal/integration_test.go` — integration tests (live IMAP/SMTP); lives at package level, not in a sub-package
7677

7778
**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).

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,7 @@ Keep your inbox clean without effort.
158158
### Composing & Sending
159159

160160
- **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)
161+
- **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)
161162
- **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)
162163
- **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)
163164
- **Multi-select**`m` marks emails, then batch-delete, move, or screen them all at once [](https://neomd.ssp.sh/docs/keybindings/#multi-select--undo)
@@ -166,6 +167,7 @@ Keep your inbox clean without effort.
166167
### Reading
167168

168169
- **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)
170+
- **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)
169171
- **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)
170172
- **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)
171173
- **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)
@@ -179,14 +181,15 @@ Keep your inbox clean without effort.
179181
- **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)
180182
- **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)
181183
- **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)
184+
- **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)
182185
- **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)
183186
- **Address autocomplete** — To/Cc/Bcc fields autocomplete from screener lists; navigate with `ctrl+n`/`ctrl+p`, accept with `tab`
184187

185188
### Under the Hood
186189

187190
- **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/)
188191
- **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/)
189-
- **Kanagawa theme**colors from the [kanagawa.nvim](https://github.com/rebelot/kanagawa.nvim) palette
192+
- **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)
190193

191194
> [!NOTE]
192195
> 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.

docs/content/docs/_index.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,7 @@ Keep your inbox clean without effort.
161161
### Composing & Sending
162162

163163
- **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)
164+
- **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)
164165
- **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)
165166
- **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)
166167
- **Multi-select**`m` marks emails, then batch-delete, move, or screen them all at once [](https://neomd.ssp.sh/docs/keybindings/#multi-select--undo)
@@ -169,6 +170,7 @@ Keep your inbox clean without effort.
169170
### Reading
170171

171172
- **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)
173+
- **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)
172174
- **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)
173175
- **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)
174176
- **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)
@@ -182,14 +184,15 @@ Keep your inbox clean without effort.
182184
- **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)
183185
- **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)
184186
- **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)
187+
- **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)
185188
- **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)
186189
- **Address autocomplete** — To/Cc/Bcc fields autocomplete from screener lists; navigate with `ctrl+n`/`ctrl+p`, accept with `tab`
187190

188191
### Under the Hood
189192

190193
- **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/)
191194
- **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/)
192-
- **Kanagawa theme**colors from the [kanagawa.nvim](https://github.com/rebelot/kanagawa.nvim) palette
195+
- **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)
193196

194197
{{< callout type="info" >}}
195198
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.

0 commit comments

Comments
 (0)