Keyboard-first TUI email: write in Neovim, render as Markdown, screen senders first, organize emails once. neomd.ssp.sh
email markdown neovim tui
neomd CHANGELOG.md
68 kB · 263 lines
Markdown
at main

Changelog #

2026-05-17 #

  • Fix: inline images with spaces in their path silently lost — attaching an image whose absolute path contained spaces (e.g. /home/.../Seat Alhambra Limousine/Fhz-Ausweis Seat Alhambra Limousine.jpg) caused both the pre-send browser preview and the sent mail to show the raw ![](/path with spaces.jpg) markdown text instead of rendering the image. CommonMark/goldmark rejects unescaped spaces in a link destination, so the parser left the literal text in place, no <img> tag was produced, and the inline-image rewrite step in internal/smtp/sender.go therefore never fired — the image was dropped from the MIME multipart/related envelope and the recipient just saw the source markdown. Two-line fix: (1) extractInlineAttachments now emits the angle-bracket CommonMark form ![](<path>) so paths containing spaces are accepted as a valid image destination; (2) goldmark percent-encodes the destination on render, so the captured <img src="..."> attribute is now URL-decoded before os.ReadFile reads the file off disk (otherwise the read would fail and the inline part would silently be skipped again). Non-image attachments and image paths without spaces take the exact same code path as before; regression test TestBuildMessage_InlineImagePathWithSpaces covers the full pipeline

2026-05-12 #

  • Per-trigger Listmonk template override — [[listmonk.triggers]] entries now accept an optional template_id = N field that pins the Listmonk template for campaigns created from that trigger; triggers without the field fall through to Listmonk's own default template, so existing configs keep working unchanged. Useful when one virtual address should always use a specialised template (e.g. listmonk-book@ssp.sh → "Book Update Template", ID 5) while the rest stay on the generic newsletter template. The pre-send "Newsletter via Listmonk" review now surfaces the resolved template ID alongside the target list IDs and schedule delay, so it's visible before scheduling. Plumbed through ListmonkTrigger (config), listmonk.Trigger + new ResolveTemplateID (hook), campaignRequest.TemplateID (Listmonk REST payload, omitempty so default triggers send no field at all), and the UI send path. Regression test covers the override/default/no-match cases

2026-05-08 #

  • Ms move to Sent — added Ms chord to move the cursor or marked email(s) to the configured Sent folder, mirroring the gs (go to Sent) shortcut. Previously every other major folder had a matching M* mover (Mi/Ma/Mt/…) but Sent was missing, so manually filing already-sent correspondence forwarded from another client required : commands or a multi-step copy. Lives in the same M-prefix dstMap as the rest, no behaviour change for other letters
  • / filter searches recipients in Sent folder — the in-memory inbox filter (/) previously matched on From + Subject in every folder. In Sent that meant typing /hussain after sending Hussain an email returned nothing, because From is always you. The filter now matches on To + CC + BCC + Subject whenever the active folder is the configured Sent folder; all other folders are unchanged. The server-side IMAP search (<space>/ / :search) already searches To and is unaffected
  • Review and stability
    • Concurrency: data race in spy pixel cache save eliminated — bodyLoadedMsg and spyScanProgressMsg previously launched safeGo(func() { saveSpyPixelCache(copyMap(m.spyPixelKeys), copyMap(m.spyScannedKeys)) }), evaluating copyMap inside the goroutine — the closure captured the model and read the live maps while the main goroutine continued mutating them on the next message. Both call sites now snapshot the maps on the main goroutine before launching the writer, so copyMap cannot race with subsequent map writes. The // Takes copied maps to avoid concurrent access comment on saveSpyPixelCache was true of the helper signature but the call sites violated the contract; both are now correct
    • Concurrency: Screener is now safe for concurrent use — added sync.RWMutex guarding every map (screenedIn, screenedOut, feed, paperTrail, spam, notify). Auto-screen, background sync, search, notify, and TUI mutation paths all share one *Screener, so concurrent map writes would have panicked at runtime. Classify, ClassifyDebug, IsEmpty, AllAddresses, ShouldNotify, and Snapshot take a read lock; Approve, Block, MarkSpam, MarkFeed, MarkPaperTrail, AddNotify, RemoveNotify, and Restore take a write lock. Refactored Snapshot/Restore into public-locked + private snapshotLocked/restoreLocked variants so the existing transactional rollback inside Approve/Block/etc. doesn't double-lock. RWMutex keeps the hot read path parallel; make test -race clean
    • Security: path traversal in .ics calendar attachment open (<space> v o) — filepath.Base("..") returns "..", and filepath.Join("~/.cache/neomd/ical", "..") escapes the cache dir. The previous filter rejected "", ".", and "/" but not "..", so a hostile Content-Disposition: filename of .. would let os.WriteFile(att.Data) overwrite ~/.cache/neomd/. Now also rejects ".." and any embedded path separator, falling back to invite-<unix-ts>.ics when the sender-supplied name is unusable
    • Security: path traversal in attachment download (xdg-open flow) — same root cause in downloadOpenAttachmentCmd: a malicious .eml part with Content-Disposition: filename=".." would write the attachment bytes to ~/Downloads/.. (i.e. ~). This is reached by every reader-driven attachment open, so a hostile sender controlled the destination path. The filter now rejects "", ".", "..", "/", and any embedded path separator, falling back to attachment-<unix-ts>
    • Reliability: ? no longer trapped in compose textinputs — the global ? → help binding fired from any state, including stateCompose. Compose feeds keys into a bubbles/textinput (To/CC/BCC/Subject), which never received ? because the global handler intercepted first. Now gated to all states except stateCompose, so ? types verbatim while composing and still toggles help everywhere else
    • Reliability: extractAddr no longer issues blocking DNS lookups on send — the helper called net.LookupHost for every recipient and From address inside the SMTP send hot path, blocking the bubbletea Update loop on flaky DNS for seconds at a time. The lookup was also functionally pointless: an || short-circuit returned the address whenever it contained @, so DNS only mattered for malformed-but-bracket-shaped strings. Replaced with pure string parsing — extracts the <addr> form when present, otherwise returns the trimmed input. Unused "net" import dropped
    • Reliability: outbound emails preserve trailing whitespace on hard-break lines — RFC 2045 §6.7 requires that any space or tab immediately before a CRLF be encoded as =20 / =09 in quoted-printable. The previous writeQP passed trailing whitespace through verbatim, which many SMTP relays silently strip — mangling Markdown's two-trailing-spaces hard-break syntax (precisely the syntax normalizePlainText adds when extracting plain text from HTML). The encoder now peeks ahead; trailing whitespace before \n or end-of-input is encoded
    • Reliability: bare go calls promoted to safeGo — saveCmdHistory (: command-line history persistence) was launched as a bare go func(), bypassing the project's safeGo panic-recovery contract documented in CLAUDE.md. A panic inside the writer would have torn down the TUI without a stack trace. Now wrapped in safeGo, with a snapshot of the history slice taken on the main goroutine before launch so the writer cannot race with the next command-history mutation
    • Reliability: OAuth2 callback server panic recovery + non-blocking sends — runAuthFlow launched the callback HTTP server as a bare go func(). A panic inside net/http's request handling would have crashed the process during the OAuth2 dance. Goroutine now has a defer recover() that surfaces the panic as an errCh send. Additionally, errCh and codeCh are buffered size 1, but a re-fired callback (browser refresh, double redirect) would block on subsequent sends and leak the handler goroutine; all sends now use a select/default non-blocking pattern
    • Reliability: IMAP retry catches more transient failures — isNetErr previously matched only four substrings (use of closed network connection, connection reset by peer, broken pipe, EOF), missing real-world transient failures: tls: connection lost, i/o timeout, connection timed out, connection refused, no route to host, network is unreachable. Those errors fell through the retry path, surfacing as user-visible failures instead of a clean reconnect. Now uses errors.Is(err, net.ErrClosed | io.EOF | io.ErrUnexpectedEOF) and net.Error.Timeout() typed checks plus a widened substring list covering TLS, syscall, and DNS-layer error formats
    • Reliability: --headless no longer panics when account 0 is imap_disabled = true — daemon.New(*cfg, imapClients[0], sc) blindly passed the first IMAP client to the daemon. Accounts with imap_disabled = true produce a nil client by design; if account 0 happened to be send-only, --headless would crash on the first IMAP call. The launcher now scans imapClients for the first non-nil entry and fails fast with a clear error message ("--headless requires at least one IMAP-enabled account") if no IMAP-enabled account exists, instead of panicking deep in the screening loop

2026-05-06 #

  • Fix: attachments invisible after pre-send round-trip — re-opening the editor from pre-send via e (re-edit), s (spell-check), or i (AI handoff), or continuing a saved draft, now re-injects tracked attachments as # [attach] /path lines right under the existing # [neomd: ...] headers instead of silently keeping them only in m.attachments. Previously the [attach] lines were extracted on the way to pre-send (correctly — they should never reach the recipient as text) but the editor reopened with the cleaned body, leaving no signal that PDFs/files were attached; users would re-attach and end up with duplicates. extractInlineAttachments now accepts both forms — # [attach] /path (header form, used on re-injection, visually grouped with the metadata headers) and [attach] /path (the form <leader>a continues to insert at cursor position for inline image placement). The editor body is now the source of truth: editorDoneMsg replaces m.attachments with whatever [attach] lines come back, instead of appending — so removing a line in the editor cleanly removes the attachment. For draft continuation the injected paths are the temp-extracted /tmp/neomd/draft-<name>-<random> files written by writeAttachmentsTemp from the saved MIME parts; the user can replace them with the original local paths if those files moved
  • 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, thanks @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
  • Fix: [notifications].folders allowlist now matches custom IMAP folder names — the allowlist compared user-configured labels (e.g. "PaperTrail") against runtime IMAP folder names (e.g. "HEY/Paper Trail" or "[Gmail]/All Mail") and silently dropped notifications when the two differed. Folders are now normalised to their UI label via a new FoldersConfig.LabelFor() helper before the allowlist check, so users with non-default IMAP folder names get notifications correctly. Regression test added in internal/config/config_test.go
  • Fix: notifier can no longer freeze the TUI — Send now runs notify-send (or whatever [notifications].command points to) under a 2-second context.WithTimeout plus a 500 ms cmd.WaitDelay. A hung notification daemon (broken DBus, mako restarting, …) returns a clear notify-send: timed out after 2s status instead of blocking the bubbletea Update loop. Test exercises the timeout path with a script that sleeps 60 s
  • Fix: screener lists now strip inline # comments — loadList previously only skipped full-line comments, so @ssp.sh # everyone at ssp.sh (as shown in the docs) stored the entire line as the entry and never matched. The loader now strips everything after the first # (after trimming) so inline comments work as documented in any screener list (screened_in, feed, notify, …)

2026-05-03 #

  • Desktop notifications for VIP senders — new opt-in [notifications] config block fires notify-send (or any notify-send-compatible command) only for senders explicitly listed in ~/.config/neomd/lists/notify.txt; independent of the screener categories so being "screened in" does not automatically mean "notify me", and a sender on a Feed/PaperTrail list can still page you when their mail arrives. Defaults: enabled = false, command = "notify-send", icon = "mail-message-new", expire_ms = 5000, folders = ["Inbox"]. First fetch silently records a per-folder UID baseline at ~/.cache/neomd/notify_state.json so the existing inbox does not flood you on enable. Status bar reports activity ("Notified N VIP sender(s) in Inbox") so you can verify the pipeline is wired up. TUI-only — the headless daemon never fires notifications so a NAS does not pop popups no one will see. Hooks into background sync (every bg_sync_interval minutes) and into manual folder loads, so notifications also fire when the daemon screens a VIP email out of Inbox before the TUI sees it (the TUI then catches it on the destination folder load). Documented in docs/content/docs/notifications.md including a wrapper-script recipe for hyprctl notify more
  • Whole-domain screening — any screener list line beginning with @ (e.g. @ssp.sh) now matches every address at that domain; works in screened_in.txt, screened_out.txt, feed.txt, papertrail.txt, spam.txt, and notify.txt. Per-address entries always win over a @domain entry across all categories so a single blocked address inside an otherwise-approved domain stays blocked (priority order preserved: spam > out > feed > papertrail > in). New Di / Do chord (works in inbox and reader) appends @<domain> of the cursor or open email's sender to screened_in.txt / screened_out.txt after a y/n confirmation; complements the existing I / O per-address keys. Domain entries can also be added to the other lists by hand-editing — the matching is shared
  • :notify-test (:nt) command — fires a single test desktop notification with the current [notifications] config so you can verify notify-send, the icon theme, and the notification daemon (mako/dunst/swaync) are all working without waiting for a real VIP email to arrive
  • :debug reports notifications — diagnostic report now includes the resolved [notifications] config, the contents of notify_state.json (per-folder baseline UIDs), the path to notify.txt, and the list of non-Inbox folders being polled in the background for VIP mail
  • Visible notify-send errors — the notifier now captures the underlying command's stderr (mako not running, missing icon theme, etc.) and surfaces it on the status bar instead of swallowing the error silently — easier to diagnose when notifications stop firing
  • <space>n / <space>N (reader) — append the open email's sender (exact email, lowercase) or its @domain to notify.txt directly from the reader; instant write with confirmation in the status bar; complements the [notifications] opt-in flow so you can curate the VIP list while reading without leaving neomd
  • Fix: :debug panicked on accounts with imap_disabled = true — writeDebugReport dereferenced the nil IMAP client that imap_disabled accounts intentionally produce, taking the whole TUI down. Now those accounts render as "Name — IMAP disabled (send-only)" in the report instead of crashing
  • Fix: nil-client guard in VIP folder polling — the new background notification poll used to crash if the active tab was an imap_disabled account when the bg sync ticked; now skips the fetch silently and waits for the next tick
  • Notification diagnostics — when a notification doesn't fire, the status bar now explains why instead of staying silent: Notify baseline set for INBOX (UID 604221) on first run; Notify check INBOX: 5 new email(s), 0 from VIPs (baseline UID 604232 → 604239) when nothing matches the notify list; Notify check INBOX: 2 VIP email(s), but destination not in allowlist (folders=[Inbox PaperTrail]) when the VIP mail landed in a folder you didn't list. Removes the guesswork around "did it just not fire?"
  • Fix: notify.txt was never loaded into the screener — cmd/neomd/main.go and internal/daemon/daemon.go constructed screener.Config without the new Notify path field, so the in-memory notify set stayed empty even when notify.txt had entries; ShouldNotify always returned false and no notification ever fired. Both call sites now pass Notify: cfg.Screener.Notify. Regression test added so this can't recur

2026-05-02 #

  • Fix: emoji reaction sent through wrong SMTP account — ctrl+e reactions already auto-selected the correct From address (matching whichever of your addresses received the email), but sendReaction() then resolved the SMTP account with a buggy custom check (presendFromI > 0 && presendFromI-1 < len(senders)) that misinterpreted account indices as sender-alias indices in multi-account setups; e.g. mail to simon@ssp.sh (Work, account index 1) while in the Personal inbox would send the reaction header as simon@ssp.sh but authenticate via Personal's SMTP and grab Senders[0]; reactions now use presendSMTPAccount() (same helper as the regular send path), so SMTP credentials, the Sent-folder destination, and the From header all match the address that received the original email

2026-04-30 #

  • Send-only accounts (imap_disabled = true) — accounts can be marked as send-only by setting imap_disabled = true; neomd skips IMAP connection, folder fetching, and screening for that account; the account remains available as a From address via ctrl+f in compose/pre-send; ctrl+a account cycling skips disabled accounts; useful for adding Gmail or other providers purely for sending without fetching thousands of emails; :debug shows "(imap disabled)" label

2026-04-28 #

  • Spy pixel blocking — neomd automatically detects and blocks tracking pixels in emails using a two-layer approach (same as HEY): (1) a curated denylist of 150+ tracking services sourced from Simplify (BSD-3-Clause), LeaveMeAlone (CC-BY 3.0), and DHH's original HEY list (MIT) — matches are attributed by service name (e.g. "Mailchimp", "HubSpot", "SendGrid"); (2) a generic 1×1 pixel heuristic (empty alt + tiny dimensions or CSS hiding) catches custom/branded tracking domains not on the list; ° indicator in the inbox list for emails with tracking pixels; reader header shows ° N spy pixel(s) blocked (ServiceName) with tracker attribution; senders cannot tell if you read their email in the TUI since glamour never fetches remote resources
  • Spy pixel scan (<space>S / :scan-spy-pixels) — scan all emails in the current folder for tracking pixels in the background; fetches full UID list from IMAP server, skips already-scanned emails, uses IMAP PEEK (won't mark as read); results cached in ~/.cache/neomd/spy_pixels and persist across restarts; both positive and negative scan results are cached so repeat scans are instant
  • URL scheme whitelist — email links opened via space+digit are now validated; only http://, https://, and mailto: schemes are allowed; javascript:, data:, and other dangerous schemes are blocked with an error in the status bar
  • Dangerous attachment warning — two layers of protection: (1) files with executable extensions (.sh, .exe, .desktop, .bat, .py, .jar, etc.) are saved but not auto-opened; (2) magic-byte verification using Go's net/http.DetectContentType() catches disguised files (e.g. a script renamed to .png is detected as text/plain and blocked); status bar warns about dangerous or suspicious file types
  • Browser view sanitization — pressing O to open email in browser now injects a Content-Security-Policy that blocks JavaScript, iframes, and embedded objects (script-src 'none'; frame-src 'none'; object-src 'none') while allowing remote images
  • Reader space chord hints — pressing space in the reader now shows all available actions (1-0 links, d download .eml, l11-99 links 11+) instead of only link info; space+d for EML download now works even when no links are present
  • Colored attachments in reader — attachment filenames in the reader header are now rendered in waveAqua2 color instead of dim gray for better visibility
  • Panic recovery — all background goroutines (mark-as-read, spy pixel cache, temp file cleanup) are now wrapped with safeGo() which recovers panics instead of crashing the TUI; panics are logged to ~/.cache/neomd/crash.log with timestamp and full stack trace for post-mortem debugging
  • IMAP connection health check — after 2+ minutes of inactivity (e.g. laptop suspend/resume), neomd probes the connection with IMAP NOOP before the next operation; if the connection is dead, it automatically reconnects — no more manual R refresh needed after sleep
  • IMAP retry for read-only operations — read-only IMAP commands (FETCH, SEARCH, STATUS) automatically retry once after reconnecting on network error; mutating operations (MOVE, APPEND, STORE) are NOT retried to prevent duplicate emails or replayed mutations
  • MIME charset/encoding fallback — emails with unknown charsets (ISO-8859-15, Windows-1256) or unknown transfer encodings no longer fail; neomd continues with raw bytes instead of crashing, matching aerc's graceful degradation pattern
  • Config validation — config is now validated on load: IMAP/SMTP addresses checked for valid host:port format with port range 1-65535, required fields enforced, UI values checked for non-negative ranges; clear error messages instead of silent failures
  • Integration tests for security features — new TestIntegration_SecurityFeatures (disguised attachment + callout) and TestIntegration_BrowserSanitization (CSP script/iframe blocking) send real test emails for live inspection

2026-04-27 #

  • Mailto handler (--mailto / positional URI) — neomd can now be used as the system default mailto: handler; clicking a mailto: link in any browser opens a foot terminal with neomd in compose mode, pre-filled with To, CC, BCC, Subject, and Body from the URI; supports both neomd --mailto "mailto:user@example.com?subject=Hello" and neomd "mailto:..." (positional, for .desktop integration); registered via xdg-mime with a neomd-mailto.desktop file; after sending or cancelling, neomd continues as normal

2026-04-24 #

  • Disable threading in Sent folder — the Sent tab now shows each email individually without thread grouping, ordered by date; threading remains active in all other folders; useful because Sent emails are your own outgoing messages and don't benefit from conversation grouping

2026-04-23 #

  • Download raw email source (space+d in reader) — saves the full raw MIME source as .eml to ~/Downloads/ with filename neomd-YYYYMMDD-<subject>.eml; useful for archiving, debugging email headers, or importing into other clients; status bar shows download progress and completion; filenames deduplicated automatically
  • Listmonk newsletter integration — send newsletters to subscribers by composing an email to a virtual trigger address (e.g. listmonk@ssp.sh); neomd intercepts the send and creates a scheduled campaign in Listmonk via its REST API instead of delivering via SMTP; configure multiple trigger addresses in [[listmonk.triggers]] to target different subscriber lists (newsletter, book, all); pre-send screen shows "Newsletter via Listmonk" with target list IDs and schedule delay; campaigns are created as draft then set to scheduled status with configurable delay (default 30 minutes); authentication via HTTP Basic Auth with environment variable expansion for API token; new self-contained internal/listmonk/ package with full test coverage (httptest mocks); documented in docs/integrations/listmonk.md

2026-04-21 #

  • RFC 5322 compliant Message-ID — Message-IDs now use the sender's domain instead of hardcoded @neomd; ensures proper email threading, spam filter compatibility, and domain reputation consistency; uses net/mail.ParseAddress() for robust RFC 5322 address parsing; validates From address before sending and rejects invalid addresses that would result in @localhost Message-IDs; added comprehensive test coverage for BuildMessage, BuildDraftMessage, and BuildReactionMessage paths; documented email standards compliance in docs/email-standards.md
  • Fix: From validation allows local-only addresses — extractDomain() now returns (domain, ok bool) to distinguish between parsing failures (invalid address) and valid user@localhost addresses; validation only rejects unparseable addresses, not legitimate local mail system configurations; prevents regression that would have blocked valid RFC 5322 addresses
  • Fix: test nil dereference guard — TestBuildMessage_InvalidFrom now uses t.Fatalf() when validation incorrectly succeeds, preventing nil pointer panic on err.Error() if test regresses
  • Fix: potential memory leak in background sync — fixed infinite loop that occurred when IMAP errors (e.g., after suspend/resume) triggered immediate retry instead of waiting for next scheduled interval; bgFetchInboxCmd() now returns nil on error instead of bgSyncTickMsg{}, preventing tight loop that consumes large amounts of RAM; added bgSyncInProgress flag that covers the entire fetch-and-screen cycle (kept set until bgScreenDoneMsg), preventing concurrent background syncs from piling up during slow network or long screening operations
  • Fix: reply-all excludes all own addresses — ctrl+r reply-all now excludes both IMAP login addresses (account.User) and send-as addresses (account.From, sender.From) from the CC field; fixes edge cases where user != from (e.g., login as user123@provider.com but send as simon@domain.com) would still leak the login address into CC; previously only excluded From addresses. Added test suite added covering single/multi-account, sender aliases, case sensitivity, and named addresses

2026-04-18 #

  • Headless daemon mode (--headless) — run neomd as a background daemon on a server (NAS, VPS, always-on device) to continuously screen emails without launching the TUI; daemon fetches inbox and auto-screens every bg_sync_interval minutes (configurable in config); watches screener list files (~/.config/neomd/lists/*.txt) and reloads when they change (designed for Syncthing multi-device sync); graceful shutdown on SIGTERM/SIGINT; structured logging to stdout with log/slog; use case: run daemon on NAS with bg_sync_interval = 5, disable background sync on laptop/Android with bg_sync_interval = 0, classify senders in TUI, Syncthing syncs lists to NAS, daemon auto-screens incoming emails so mobile IMAP apps see correctly filtered folders; perfect for using neomd's screener with native mobile email clients; daemon only reads screener lists and moves emails (never modifies lists), all sender classification happens in TUI; includes comprehensive tests for classification logic and daemon lifecycle; documented in docs/configurations/headless.md with Syncthing setup guide, systemd service example, and multi-device workflow; make daemon target for testing
  • Fix: headless screening paused when lists empty — daemon now checks screener.IsEmpty() and skips screening when all lists are empty, mirroring TUI behavior; prevents sweeping entire inbox to ToScreen on first run or before Syncthing completes initial sync; logs "screening paused: screener lists are empty (classify your first sender to activate)" until first classification exists; regression test added
  • Fix: cross-list cleanup on reclassification — all 5 screener classification functions (Approve, Block, MarkSpam, MarkFeed, MarkPaperTrail) now remove email addresses from ALL conflicting lists before adding to the target list; fixes bug where reclassifying a sender (e.g., Feed → ScreenedOut) would leave the address in both feed.txt and screened_out.txt, causing duplicates in screener state and sync conflicts across devices; previously only Approve, Block, and MarkSpam had partial cleanup logic, while MarkFeed and MarkPaperTrail only appended without removing; reclassification is now atomic using snapshot/restore: captures screener state before starting, checks all file operation errors (no longer silently ignored), and restores the snapshot on any failure to prevent partial writes or lost classifications; comprehensive regression test added covering Feed→ScreenedOut, PaperTrail→Feed, full reclassification chain (Inbox→Feed→PaperTrail→ScreenedOut→Spam→Inbox), and persistence after reload; all cleanup operations verified both in-memory and on-disk
  • Cross-compile FreeBSD binary on build — make build now automatically creates neomd-freebsd static binary alongside Linux binary; FreeBSD binary can be copied to FreeBSD/OpenBSD servers without needing Go installed; make sync-headless target copies to remote server via scp

2026-04-17 #

  • GitHub/Obsidian-style callouts in emails — compose emails with callout syntax > [!note], > [!tip], > [!warning] for styled alert boxes in HTML emails; rendered with colored left borders, subtle backgrounds, and emoji icons using Kanagawa theme colors (crystalBlue, springGreen, carpYellow, oniViolet, autumnRed); compact spacing with emoji and title matching body text size (15px) for minimal visual intrusion; supports custom titles (> [!note] Custom Title), multiple paragraphs, and nested callouts; always expanded (no collapsible behavior), no JavaScript required; works in both syntaxes: > [!note] (with space) or >[!note] (without space); plain text emails format callouts as emoji text without blockquote markers (readable in neomd reader and plain text clients); uses local fork of goldmark-obsidian-callout with email-optimized rendering; same syntax used in neomd's README now works in your composed emails
  • Timer-based mark-as-read — emails are no longer marked as read immediately when opened; instead, a configurable timer (default 7 seconds) starts when you enter the reader; if you stay for the full duration, the email is marked as \Seen; if you exit early (quick peek), it stays unread; prevents accidental marking when browsing through emails
  • mark_as_read_after_secs config — new [ui] option to control mark-as-read delay in seconds (default 7); set to 0 for immediate marking (old behavior); set to any value to customize the delay
  • Fix: local UI state sync on mark-as-read — inbox list now updates immediately when an email is marked as read, either via timer or manual toggle (n); previously the server was updated but the local UI showed stale unread indicators until manual refresh

2026-04-16 #

  • B move to Work/business — press B to move marked or cursor email(s) to Work folder (similar to A for Archive); quick single-key action without screener list updates; shows friendly error if Work folder not configured; useful for rapid GTD-style email processing; complements existing gb (go to Work) and Mb (move to Work) shortcuts
  • Redesigned welcome screen — new two-column layout with ASCII art logo, philosophy/getting started guide on the left, and essential shortcuts organized by category on the right; wider box (100 chars) with cleaner spacing; maintains kanagawa color scheme; more scannable and visually appealing for new users
  • ASCII logo in help overlay — pressing ? now shows the neomd ASCII art logo overlaid on the top-right corner of the help screen; shortcuts start immediately at the top without vertical space taken by the logo; logo only appears when scrolled to the top
  • space+w welcome shortcut — press space then w to reopen the welcome screen anytime; useful for reviewing keybindings and getting started guide; documented in help overlay and keybindings reference
  • N jump to next unread — press N to jump to the next unread email in the current folder; wraps around to the beginning if no unread found after cursor; displays status message if no unread emails exist
  • z toggle unread-only view — press z to filter the inbox to show only unread emails (mnemonic: "zero in on unread"); press z again to show all emails; works alongside text filter (/) and can be cleared with esc; status bar indicates current view mode
  • Fix: help menu search — all keys (including j, k, d, u) are now available for typing when searching in help overlay (? then /); scroll keys only work when not in search mode

2026-04-15 #

  • Scheduled folder keybindings — added gc (go to Scheduled, mnemonic: "calendar") and Mc (move to Scheduled) shortcuts; Scheduled folder now accessible via dedicated keybindings alongside existing tab navigation ([]HL, space+1-9); help overlay and generated keybindings documentation updated

2026-04-14 #

  • Extended link support (99 links) — link opener now supports up to 99 links per email (previously limited to 10); space+1-0 opens links 1-10, space+l11-99 opens links 11-99 using intuitive numeric shortcuts (e.g. space+l26 for link [26]); status line provides progressive feedback during multi-key input; footer help and ? overlay updated
  • Fix: link extraction with brackets in text — markdown link regex now correctly matches links with brackets inside the link text (e.g. [[Watch the studio tour here]](url)); changed from [^\]]+ (anything except ]) to non-greedy .+? to handle nested brackets; fixes newsletter links from Beehiiv and similar services

2026-04-13 #

  • Emoji reactions (ctrl+e) — fast, keyboard-driven emoji reactions from inbox or reader; press ctrl+e to open emoji picker overlay, select with 1-8 for instant send or navigate with j/k and press enter; sends minimal reaction email (emoji + italic footer + quoted original message) with proper threading headers; available reactions: 👍 ❤️ 😂 🎉 🙏 💯 👀 ✅; original email marked with \Answered flag; reaction saved to Sent folder; auto-selects From address matching recipient (same logic as regular replies)
  • Email threading headers — all replies (regular r/R and emoji reactions ctrl+e) now include proper In-Reply-To and References headers for conversation threading; ensures replies appear correctly grouped in Gmail, Outlook, and Apple Mail conversation views; References header extracted from IMAP message body and preserved in reply chain
  • Fix: refresh not showing new emails immediately — pressing R now correctly displays new emails on first refresh; previously the IMAP client cached the selected mailbox state, so the first R would skip re-SELECT and use stale UID SEARCH results (showing the old unread count but no new messages in the list); required a second R or tab switch to see new emails; now forces a fresh SELECT to ensure mailbox state is current; also fixed background sync path to prevent stale cache; added regression test (internal/imap/client_test.go:410)

2026-04-10 #

  • HTML signature support — new [ui.signature_block] config with separate text and html fields for dual-format signatures; text signature appears in the editor and text/plain MIME part, HTML signature appends to the text/html part only; use [html-signature] placeholder in text signature to control HTML signature inclusion per-email (visible in preview, deletable before sending); backward compatible with legacy signature field
  • Fix: draft formatting corruption — drafts are now stored as plain text only instead of multipart/alternative to prevent HTML→markdown conversion artifacts; fixes line break addition, pipe escaping (| → \|), and italic style changes (* → _) when reopening saved drafts
  • Sent/Drafts primary-account default restored — in multi-account setups, Sent and Drafts now default back to the first configured IMAP account while SMTP still uses the selected sending identity; added store_sent_drafts_in_sending_account = true for users who want Sent/Drafts to follow the sending account instead
  • Proton Mail Bridge compatibility — documented that Proton Mail works with neomd only via Proton Mail Bridge (paid Proton feature), added optional tls_cert_file support for trusting Bridge’s exported self-signed certificate, and added a narrow localhost-only TLS retry fallback for Bridge connections on 127.0.0.1/localhost; normal remote IMAP/SMTP providers keep their existing strict certificate verification behavior
  • Issue #6 verification pass — reviewed the user report against the current code and specifically verified that startup auto-screening does not route Inbox mail to Trash in the current implementation, while manual ToScreen screening remains message-by-message by design
  • Fix: Drafts/Spam reload off-tab folder mismatch — reloading while viewing an off-tab folder now reloads that actual mailbox instead of the currently selected tab's folder; fixes the confusing case where Drafts could show Inbox content after pressing R
  • Fix: committed / filter now clears with esc — pressing esc now reliably clears the in-memory inbox filter even after the filter was already applied
  • Help overlay improvements — ? help is now scrollable with j/k, arrow keys, and d/u; search begins only after pressing /, so opening help no longer immediately behaves like a search prompt
  • Attachment workflow guidance — startup/welcome messaging now warns when the optional inline Neovim attachment integration is unavailable; README install docs now list yazi and the external custom.lua integration as optional requirements for <leader>a, while clarifying that pre-send a still works independently
  • UX hints — inbox footer now exposes , sort; pre-send footer clarifies s as spell-check-and-edit versus plain e edit; compose/pre-send ctrl+f now shows a message when only one From identity is configured
  • Fix: sender-level screening from ToScreen — approving/blocking/feed/papertrail/spam on a single unmarked message in ToScreen now expands to all currently queued mail from that sender, matching the intended HEY-style workflow
  • Safety guard: screener destinations may not point to Trash — screening now refuses to run if ToScreen, ScreenedOut, Feed, PaperTrail, or Spam are configured to the same IMAP folder as Trash
  • Inbox paging clarity — the inbox header now shows the current fetch limit (loaded/limit) and d/u page movement directly, so the “only 50 emails” behavior is visible without guessing
  • Discard confirmation for unsent mail — esc in compose and esc/x in pre-send now ask for confirmation before dropping the message; recovery hints still point to :recover
  • Default Inbox load raised to 200 — new configs now use inbox_count = 200; README, config docs, and welcome text now clarify that normal loads/auto-screening only process that loaded Inbox slice, while :screen-all scans the full Inbox on the IMAP server
  • Compose/draft round-trip preservation — editor/pre-send/draft/recover flows now preserve Bcc and selected From; continuing a draft also restores its attachments back into the compose session
  • Correct IMAP account for Sent/Drafts — sent copies and saved drafts now use the IMAP account that matches the selected sending identity / [[senders]] alias instead of always using the currently active inbox account
  • Draft MIME keeps Bcc — Drafts saved via IMAP now retain the Bcc header so reopening a draft does not silently lose hidden recipients
  • Search/Everything/Thread subjects no longer mutate — folder prefixes are now display-only in list rendering, so reply/forward/thread logic keeps using the real RFC subject
  • Screener rollback safety — screener actions now snapshot list state and roll back both list files and already-moved emails if a later move fails, keeping mailbox state and screener files consistent
  • :search help text fixed — the command description now correctly says it searches across configured folders, not just the current folder

Roborev #

  • Security: path traversal vulnerability fixed — inline image handling (O browser preview) now sanitizes ContentID and Filename from email MIME headers to prevent attackers from writing files outside /tmp/neomd/ via malicious cid: references (e.g. ../../etc/cron.d/evil); all attachment paths now use filepath.Base() and verify the result stays under temp directory before writing
  • Fix: conversation view navigation — pressing T (thread view) now correctly shows error messages and empty-result warnings; imapSearchResults flag is cleared immediately so the status bar appears instead of the search bar; added general Esc handler for offTabFolder views that preserves search context: pressing Esc from thread view returns to IMAP search results if that's where you came from (checked via imapSearchText), otherwise returns to active folder; imapSearchText is cleared when navigating away from search via tab, clicks, or go-to commands (gi/ga/etc) to prevent stale search context from affecting unrelated views; search retry errors are now visible because imapSearchResults is only set by the handler on success
  • Fix: Work folder move guard — Mb (move to Work) is now disabled when the Work folder is not configured, preventing moves to an empty folder name; previously caused silent failures
  • Fix: Work folder keybindings in help — gb (go to Work) and Mb (move to Work) now appear in the ? help overlay and generated keybindings documentation, marked as "(if configured)" to indicate they're optional
  • Test coverage: expandEnv edge cases — added unit tests for environment variable expansion covering unset variables (silently return empty), bare $ alone, empty ${}, whitespace trimming, and variables with text suffixes/prefixes; documents current behavior for config password/user fields
  • Fix: welcome message formatting — onboarding screen instruction now reads clearly ("Go to Inbox tab; once screener is active, use ToScreen") instead of the previous formatting regression ("ToScreentab")

2026-04-09 #

  • Fix: non-standard IMAP/SMTP ports — neomd now correctly handles non-standard ports (e.g., Proton Mail Bridge on 127.0.0.1:1143 and 127.0.0.1:1025); previously hardcoded port-based logic ignored the user's starttls config and refused unencrypted connections to any port other than 993/143 (IMAP) or 465/587 (SMTP); new behavior: user's explicit starttls = true always forces STARTTLS, standard ports use their defaults (993→TLS, 143→STARTTLS, 465→TLS, 587→STARTTLS), non-standard ports default to TLS for security (user must set starttls = true if their provider uses STARTTLS on a custom port); fixes "refusing unencrypted connection to 127.0.0.1:1143" error reported by Proton Bridge users; comprehensive test coverage added for all port/config combinations

2026-04-08 #

  • Fix: pre-send e losing email body — pressing e in the pre-send review to re-edit now correctly reopens the editor with the existing body; previously it opened a blank compose with only the signature, silently discarding the email content (including reply history)
  • Draft backups — every compose session is automatically backed up to ~/.cache/neomd/drafts/ before the temp file is deleted; keeps a rolling 20 backups (configurable via draft_backup_count in [ui], set to -1 to disable); no more lost emails after crashes or accidental closes
  • :recover / :rec command — reopens the most recent draft backup as a compose session; To/Cc/Bcc/Subject are parsed from the backup and pre-filled automatically
  • Screener docs: "screening happens once" — documented that auto-screening only runs on the Inbox folder; emails moved to ToScreen by another device are not re-classified; use :reset-toscreen to move them back for re-screening
  • Test suite — 147 unit tests across 8 packages covering screener classification, MIME message building, editor parsing, config loading, IMAP search, OAuth2 token handling, rendering, and security invariants (file permissions, BCC privacy, credential leak prevention); CI workflow runs go test + go vet on every PR
  • Integration tests (make test-integration) — end-to-end tests against a real IMAP/SMTP server: send plain email and verify From/To/Subject/HTML body round-trip, CC header, file attachment content, non-ASCII subject encoding (umlauts + emoji), IMAP search with from:/subject: prefixes, move + undo, inline images, signature HTML rendering, SaveSent IMAP APPEND, comma-separated multiple recipients, and reply-all with 3 distinct addresses; all test emails cleaned up automatically; skipped without credentials so make test stays fast and offline
  • Fix: multiple To recipients — Send() now correctly splits comma-separated To addresses into individual SMTP RCPT TO commands; previously the entire "a@x.com, b@x.com" string was passed as a single address, causing delivery failures
  • Fix: To/CC display — reader and inbox now show all To and CC addresses, not just the first; FetchHeadersByUID (used by search/everything) now also populates To and CC fields
  • Reply-all rebind to ctrl+r — R (Shift+R) is now consistently reload/refresh in all views; reply-all moved to ctrl+r which works from both inbox list and reader (previously R conflicted between reload in inbox and reply-all in reader)
  • Default signature for new users — new installs get *sent from [neomd](https://neomd.ssp.sh)* as the default signature
  • Reply indicator (·) — emails you've replied to show a · dot in the inbox list between the flag and thread columns; uses the standard IMAP \Answered flag so it works across clients (reply from webmail → neomd shows it)
  • \Answered flag on reply — after sending a reply, the original email is automatically marked as \Answered on the IMAP server
  • Conversation thread view (T / :thread) — press T from inbox list or reader to see the full conversation across folders (Inbox, Sent, Archive, Waiting, Work, etc.); searches by normalized subject + participant overlap; displays in a temporary "Thread" tab with [Folder] prefix and │/╰ threading connectors; esc returns to previous view
  • Custom folder support (work) — optional work = "Work" in [folders] config; add "work" to tab_order to show as a tab; gb to go, Mb to move; auto-created on first run if configured; included in Everything, Search, and conversation views
  • Inline images in browser preview — pressing O to open an email in the browser now shows inline images from other senders; cid: references are rewritten to temp files so the browser can display them; previously only your own sent emails rendered images correctly
  • compose_editor config option — optional compose_editor in [ui] to use a different editor for compose/reply/forward (e.g. "nvim --appname nvim-wp"); defaults to $EDITOR / nvim

2026-04-05 #

  • OAuth2 authentication (#3, thanks @notthatjesus) — accounts can set auth_type = "oauth2" with oauth2_client_id, oauth2_client_secret, oauth2_issuer_url, and oauth2_scopes instead of a password; on first launch neomd opens the browser for the authorization code flow, persists the token to ~/.config/neomd/tokens/<account>.json, and refreshes it automatically; works with Gmail, Office365, and any OIDC-discoverable provider via XOAUTH2 over IMAP and SMTP; password auth paths unchanged for existing accounts
  • auto_bcc config — root-level auto_bcc = "addr@example.com" appends an address to every outgoing email's Bcc so you keep a copy in an external mailbox (e.g. a hey.com archive); visible in the composer and pre-send review (no silent BCC), deduped against any manual Bcc entry
  • shift+tab in compose — navigate back through To/Cc/Bcc/Subject fields (previously could only move forward with tab/enter)
  • Reader shows local time — email dates in the reader header now convert to your system's local timezone and include the clock time (e.g. Apr 05, 00:51); previously showed the sender's timezone date without time

2026-04-02 #

  • Auto From on reply — replying auto-selects the From address that matches the email's To/CC field (e.g. email sent to simon@domain.com replies from simon@domain.com); r now works from inbox list view; # [neomd: from: ...] shown in editor; x in pre-send discards the email
  • Email safety hardening — bulk operations show live progress counter ("Screening: 42/1000…") for batches >10; screener now moves emails before updating list files (no inconsistent state on failure); SaveSent failure shown as warning instead of silently swallowed; batch failures report exact moved/total counts; partial batch undo info preserved on error; undo stack capped at 20
  • Screener lists created on startup — all 5 screener .txt files are created as empty files on first run (alongside directories), consistent with IMAP folder creation
  • Config-isolated cache — demo and production configs use separate cache directories (derived from config dir name), so make demo-reset never touches production data
  • Added benchmark to readme as Gmail was considerly slower than my IMAP provider here from Switzerland.

2026-04-01 #

  • Threaded inbox — related emails are automatically grouped in the inbox list with a Twitter-style vertical connector line (│/╰); threads detected via In-Reply-To/Message-ID IMAP envelope headers with a reply-prefix subject fallback (only emails with Re:, AW:, Fwd: etc. are grouped by subject — recurring notifications/invoices stay separate); newest reply on top, root at bottom; threads sorted by most recent email so active conversations float to the top
  • Clickable tabs — folder tabs in the top bar are clickable with the mouse; click any tab to switch folders
  • Spell check in pre-send (s) — opens nvim with spell checking enabled (en_us + de), cursor jumps to the first misspelled word; use ]s/[s to navigate errors, z= for suggestions, zg to add to dictionary; corrected body flows back to pre-send
  • :debug / :dbg command — writes a diagnostic report covering IMAP connectivity (ping test), account config (emails masked), folder mapping, screener list status, UI config, and current state; opens in the reader and saves to /tmp/neomd/debug.log for sharing; no sensitive data (passwords, full emails) included
  • Drafts show recipient — Drafts folder now shows → recipient instead of From (same as Sent tab), since all drafts are from you
  • ctrl+b in pre-send — toggle CC/BCC fields from the pre-send review screen (previously only available during compose)
  • u / U rebind — u is now free for page-up (vim-style half-page scroll); U is undo last move/delete; ctrl+u clears all marks
  • Temp files in /tmp/neomd/ — all temp files (compose, preview, spell check) now live in /tmp/neomd/ subdirectory for easy recovery after crashes and less clutter
  • Improved onboarding — auto-screening is now paused when screener lists are empty (first run), preventing all emails from being moved to ToScreen; activates automatically once the user classifies their first sender; welcome screen rewritten with step-by-step getting-started guide explaining the screener workflow, batch operations (m + I), and config hints (:debug, auto_screen_on_load)
  • ] / [ folder navigation — bracket keys now switch to next/previous folder tab (alongside L/H and tab/shift+tab)

2026-03-31 #

  • fix showing recipient in SENT tab (instead of from)
  • IMAP search across all folders (space / or :search) — server-side IMAP SEARCH across all configured folders (Inbox, Sent, Archive, Feed, etc.); results displayed in a temporary "Search" tab with [Folder] prefix on each subject; supports query prefixes: from:simon, subject:invoice, to:team@, or plain text to search all three fields; press esc to close results
  • Filter preserves across actions — the local / filter no longer clears when pressing n (toggle read), m (mark), U (clear marks), or sorting; filter stays active until esc
  • Address autocomplete in compose — To, Cc, and Bcc fields show autocomplete suggestions from screener lists (screened_in.txt, feed.txt, papertrail.txt); navigate with ctrl+n/ctrl+p/arrows, accept with tab; supports multi-address fields (autocomplete applies after the last comma)
  • Everything view (ge or :everything) — shows the 50 most recent emails across all folders in a temporary "Everything" tab, sorted by date descending; each subject prefixed with [Folder]; useful for finding emails that were screened out or moved to spam
  • Link opener (space+1-9 in reader) — links are extracted from the email body, numbered [1]-[0] in the header; press space then a digit to open in $BROWSER; up to 10 links per email, deduplicated by URL
  • Draft signature fix — re-opening a draft (E) no longer appends a duplicate signature; the draft body already contains it from the first compose
  • Draft reader footer — E draft now appears in the reader footer when viewing an email from the Drafts folder
  • Android support (make android) — cross-compile for Android ARM64; runs in Termux; documented in docs/android.md with install instructions and useful shortcuts
  • Docs restructure — detailed documentation moved from README to docs/ folder: docs/keybindings.md (auto-generated), docs/screener.md, docs/sending.md, docs/configuration.md, docs/android.md; README kept concise with links

2026-03-30 #

  • added preview email in $BROWSER (images rendered, same as recipient sees) with p
  • Multiple From addresses / SMTP aliases — add [[senders]] blocks to config to define extra From identities (e.g. s@ssp.sh as an alias through an existing account's SMTP); cycle through all accounts + senders with ctrl+f in both compose and pre-send screens; the account = field matches by account name = (not email address)
  • Sent folder — after sending, neomd APPENDs a copy to the configured Sent IMAP folder with \Seen flag; the same raw MIME bytes used for SMTP delivery are reused for the APPEND (no double-build)
  • Attachment column in inbox — @ appears in a dedicated column next to the date when an email has attachments (detected from IMAP BODYSTRUCTURE including inline images)
  • Attachment downloads in reader — the email header now lists all attachments as [1] report.pdf [2] photo.png; press 1–9 to download attachment N to ~/Downloads/ and open it with xdg-open; filenames are deduplicated automatically
  • Inline images as downloads — images embedded inline in emails (Content-Disposition: inline, e.g. PNG screenshots) are now shown alongside regular attachments in the reader header and downloadable with 1–9; previously only Content-Disposition: attachment parts were listed
  • Inline image placeholders in reader body — <img src="cid:..."> tags now show [Image: filename.png] at their position in the body text instead of being silently stripped; uses Content-ID → filename mapping from MIME parts
  • Undo move / delete — u reverses the last single or batch move/delete (x, A, M*); uses the UIDPLUS destination UID so undo still works even when the server reassigns UIDs on MOVE; screener actions (I, O, F, P, $) are intentionally excluded because they also modify .txt list files
  • Subject (and headers) re-parsed from editor — editing # [neomd: subject: ...], # [neomd: to: ...], etc. in neovim now correctly updates those fields; previously the values were captured in a closure before the editor opened and changes were silently discarded; all three editor entry points (new compose, reply, continue draft) now call editor.ParseHeaders on the saved file content
  • ctrl+f for cycling From — changed from f (which conflicts with typing in text fields) to ctrl+f; works in both the compose form and the pre-send review screen
  • Forward (f) — forward an email from the reader or inbox; opens the editor with the original message quoted, Fwd: subject prefix, and empty To: field; from inbox the body is fetched automatically before opening the editor
  • Permanent delete (X, Trash only) — permanently deletes marked or cursor email(s) from the Trash folder via IMAP STORE \Deleted + UID EXPUNGE; blocked in other folders with a warning message
  • :empty-trash / :et — permanently delete all emails in Trash with y/n confirmation; works from any folder without navigating to Trash first
  • First-run welcome popup — on the very first launch, a centered popup shows quick-start keybindings and screener basics; any key dismisses it; marker at ~/.cache/neomd/welcome-shown ensures it only appears once
  • Auto-create IMAP folders on startup — ensureFoldersCmd runs during Init() so new users don't need to manually run :create-folders; idempotent for existing users
  • Auto-create screener list directories — parent directories for screener list paths are created automatically during config load; prevents errors when pressing I/O/F/P on a fresh install
  • Default screener paths — changed from ~/.config/mutt/ to ~/.config/neomd/lists/ for new installs; existing configs with custom paths are unaffected
  • Go prerequisite check in Makefile — make build/make install now prints clear Go installation instructions instead of a cryptic error when go is not found
  • Pre-send preview (p) — press p in the pre-send screen to open a browser preview of the composed email; renders through the same goldmark pipeline as sending, with local image paths converted to file:// URLs so inline images from [attach] lines display correctly

2026-03-29 #

  • CC field — compose and reply forms now include an optional Cc field (Tab/Enter to skip); CC recipients receive the email and appear in the Cc: header
  • BCC field — hidden by default; toggle with ctrl+b in compose; BCC recipients receive the email but are not visible in the message headers (standard BCC privacy)
  • Reply-all — R in the reader replies to the original sender + all CC recipients; your own address is excluded automatically; uses Reply-To header when present
  • Pre-send review screen — after closing the editor, neomd shows a summary (To, Subject, body preview) before sending; press enter to send, a to attach files via yazi (auto-detected, no config needed; override with $NEOMD_FILE_PICKER), D to remove last attachment, d to save to Drafts, e to re-open the editor, esc to cancel; avoids tmux/terminal key-capture issues since a needs no modifier
  • Save to Drafts — d in the pre-send screen APPENDs the composed message to the configured Drafts IMAP folder with \Draft + \Seen flags; navigate to it with gd
  • Attachments from neovim — <leader>a in a neomd-*.md buffer opens yazi in a floating terminal; selected files are inserted as [attach] /path/to/file lines (visible in markdown, not hidden HTML comments); neomd strips them before sending and adds them as MIME attachments
  • Inline code and code blocks — `inline code` and fenced ``` blocks are rendered in HTML emails (goldmark CommonMark + GFM; styled with monospace font and light grey background)

2026-03-27 #

  • gd Drafts navigation — jump to Drafts folder with gd even when it's not in the tab rotation
  • Off-tab folder indicator — when viewing Spam (gS) or Drafts (gd), the folder name appears highlighted in the tab bar with a │ separator; no regular tab stays falsely active
  • Security hardening — IMAP refuses unencrypted connections (non-993/143 ports error out instead of DialInsecure); email-extracted URLs validated to http/https only before opening in browser (case-insensitive, RFC 3986); SECURITY.md added documenting credential storage, TLS guarantees, screener list handling, and temp file lifecycle with links to source
  • Spam folder — $ marks a sender as spam (writes to spam.txt, moves to Spam IMAP folder). Separate from ScreenedOut so you never have to look at it again. Navigate with gS or :go-spam — kept out of the tab rotation intentionally
  • Cross-list cleanup — reclassifying a sender removes them from conflicting lists automatically: I (approve) removes from screened_out + spam; O (block) removes from screened_in; $ (spam) removes from screened_in + screened_out. No manual .txt editing needed
  • : command history — ↑/↓ cycles through the last 5 distinct commands; → accepts the ghost completion; ctrl+n/ctrl+p cycle forward/backward through completions. Persists across restarts in ~/.cache/neomd/cmd_history (outside dotfiles version control)
  • Leader key — space is the leader; <space>1–<space>9 jumps to a folder tab by number
  • Auto-screen on inbox load — screener applies automatically on every Inbox load (startup, R). Disable with auto_screen_on_load = false in [ui]
  • Background sync — inbox re-fetched and screened every 5 minutes while neomd is open. Configure with bg_sync_interval in [ui]; 0 disables it
  • n / m rebind — n toggles read/unread (was N); m marks for batch ops (was space)

2026-03-25 #

  • Signature — auto-appended to new compose buffers; configure in [ui] with signature
  • Compose abort — closing the editor with ZQ / :q! cancels the email; only ZZ / :wq sends
  • Browser image workflow — O opens email as HTML in $BROWSER; ctrl+o opens the canonical web/newsletter URL (extracted from List-Post header); o opens in w3m
  • :create-folders / :cf — creates any missing IMAP folders defined in config (idempotent)