Something went wrong. Try again.
Keyboard-first TUI email: write in Neovim, render as Markdown, screen senders first, organize emails once. neomd.ssp.sh
email markdown neovim tui
Something went wrong. Try again.
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 rawmarkdown 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 ininternal/smtp/sender.gotherefore never fired — the image was dropped from the MIMEmultipart/relatedenvelope and the recipient just saw the source markdown. Two-line fix: (1)extractInlineAttachmentsnow emits the angle-bracket CommonMark formso 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 beforeos.ReadFilereads 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 testTestBuildMessage_InlineImagePathWithSpacescovers the full pipeline
2026-05-12 #
- Per-trigger Listmonk template override —
[[listmonk.triggers]]entries now accept an optionaltemplate_id = Nfield 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 throughListmonkTrigger(config),listmonk.Trigger+ newResolveTemplateID(hook),campaignRequest.TemplateID(Listmonk REST payload,omitemptyso default triggers send no field at all), and the UI send path. Regression test covers the override/default/no-match cases
2026-05-08 #
Msmove to Sent — addedMschord to move the cursor or marked email(s) to the configured Sent folder, mirroring thegs(go to Sent) shortcut. Previously every other major folder had a matchingM*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 sameM-prefix dstMap as the rest, no behaviour change for other letters/filter searches recipients in Sent folder — the in-memory inbox filter (/) previously matched onFrom + Subjectin every folder. In Sent that meant typing/hussainafter sending Hussain an email returned nothing, becauseFromis always you. The filter now matches onTo + CC + BCC + Subjectwhenever 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 —
bodyLoadedMsgandspyScanProgressMsgpreviously launchedsafeGo(func() { saveSpyPixelCache(copyMap(m.spyPixelKeys), copyMap(m.spyScannedKeys)) }), evaluatingcopyMapinside 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, socopyMapcannot race with subsequent map writes. The// Takes copied maps to avoid concurrent accesscomment onsaveSpyPixelCachewas true of the helper signature but the call sites violated the contract; both are now correct - Concurrency:
Screeneris now safe for concurrent use — addedsync.RWMutexguarding 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, andSnapshottake a read lock;Approve,Block,MarkSpam,MarkFeed,MarkPaperTrail,AddNotify,RemoveNotify, andRestoretake a write lock. RefactoredSnapshot/Restoreinto public-locked + privatesnapshotLocked/restoreLockedvariants so the existing transactional rollback insideApprove/Block/etc. doesn't double-lock. RWMutex keeps the hot read path parallel;make test -raceclean - Security: path traversal in
.icscalendar attachment open (<space> v o) —filepath.Base("..")returns"..", andfilepath.Join("~/.cache/neomd/ical", "..")escapes the cache dir. The previous filter rejected"",".", and"/"but not"..", so a hostileContent-Disposition: filenameof..would letos.WriteFile(att.Data)overwrite~/.cache/neomd/. Now also rejects".."and any embedded path separator, falling back toinvite-<unix-ts>.icswhen the sender-supplied name is unusable - Security: path traversal in attachment download (
xdg-openflow) — same root cause indownloadOpenAttachmentCmd: a malicious.emlpart withContent-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 toattachment-<unix-ts> - Reliability:
?no longer trapped in compose textinputs — the global?→ help binding fired from any state, includingstateCompose. Compose feeds keys into abubbles/textinput(To/CC/BCC/Subject), which never received?because the global handler intercepted first. Now gated to all states exceptstateCompose, so?types verbatim while composing and still toggles help everywhere else - Reliability:
extractAddrno longer issues blocking DNS lookups on send — the helper callednet.LookupHostfor 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/=09in quoted-printable. The previouswriteQPpassed trailing whitespace through verbatim, which many SMTP relays silently strip — mangling Markdown's two-trailing-spaces hard-break syntax (precisely the syntaxnormalizePlainTextadds when extracting plain text from HTML). The encoder now peeks ahead; trailing whitespace before\nor end-of-input is encoded - Reliability: bare
gocalls promoted tosafeGo—saveCmdHistory(:command-line history persistence) was launched as a barego func(), bypassing the project'ssafeGopanic-recovery contract documented inCLAUDE.md. A panic inside the writer would have torn down the TUI without a stack trace. Now wrapped insafeGo, 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 —
runAuthFlowlaunched the callback HTTP server as a barego func(). A panic insidenet/http's request handling would have crashed the process during the OAuth2 dance. Goroutine now has adefer recover()that surfaces the panic as anerrChsend. Additionally,errChandcodeChare 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 aselect/defaultnon-blocking pattern - Reliability: IMAP retry catches more transient failures —
isNetErrpreviously 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 useserrors.Is(err, net.ErrClosed | io.EOF | io.ErrUnexpectedEOF)andnet.Error.Timeout()typed checks plus a widened substring list covering TLS, syscall, and DNS-layer error formats - Reliability:
--headlessno longer panics when account 0 isimap_disabled = true—daemon.New(*cfg, imapClients[0], sc)blindly passed the first IMAP client to the daemon. Accounts withimap_disabled = trueproduce anilclient by design; if account 0 happened to be send-only,--headlesswould crash on the first IMAP call. The launcher now scansimapClientsfor 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
- Concurrency: data race in spy pixel cache save eliminated —
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), ori(AI handoff), or continuing a saved draft, now re-injects tracked attachments as# [attach] /pathlines right under the existing# [neomd: ...]headers instead of silently keeping them only inm.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.extractInlineAttachmentsnow accepts both forms —# [attach] /path(header form, used on re-injection, visually grouped with the metadata headers) and[attach] /path(the form<leader>acontinues to insert at cursor position for inline image placement). The editor body is now the source of truth:editorDoneMsgreplacesm.attachmentswith 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 bywriteAttachmentsTempfrom 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#F2EFE9background),rose-pine,gruvbox, orosaka-jade. Optional top-level[theme]block overrides individual colour slots (primary,unread,error,bg, …) on top of any built-in.ApplyThemeruns inui.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].commandconfig (defaultclaude) wires any LLM CLI (claude,codex,aichat,sgpt, …) to the pre-sendikey. neomd writes the current draft to/tmp/neomd/neomd-ai-*.mdwith 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)).nvimis intentionally not a useful choice here — the compose buffer is already in nvim before pre-send, so spawning nvim oniwould just re-edit. Setcommand = ""to disable the binding - iCalendar RSVP + local calendar handoff — emails with a
text/calendarpart or.icsattachment 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.icsin[calendar].open_command(defaultxdg-open, set tomorgen/khal//usr/bin/gnome-calendarto force a specific app). MIME envelope:multipart/mixed > [multipart/alternative > (text/plain + text/calendar;method=REPLY)] + .ics attachmentwithSubject: Accepted: <event>(matches Gmail's native button format) and bracketed RFC 5322In-Reply-Toheaders. RSVPs save to theSentfolder 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-containedinternal/calendar/package on top ofarran4/golang-ical; newinternal/smtp/rsvp.gofor the iMIP MIME envelope. Only the firstVEVENTis 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 insideconfig.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. Newinternal/keyring/package with mock-backed tests; storage keys areneomd/account/<name>/{password|oauth2}
2026-05-04 #
- Fix: send from
imap_disabled = trueaccount no longer panics — sending an email from an account configured as send-only (typically Gmail withimap_disabled = true) crashed the TUI withnil pointer dereferenceintokenSourceForbecause the helper called.TokenSource()on the intentionally-nil IMAP client. The same nil-deref existed inimapCli,imapCliForAccount, andprimaryIMAPClient— 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);sendEmailCmdalso guardscli.SaveSentandreplyCli.MarkAnsweredso a fully send-only configuration silently skips the Sent-folder copy instead of panicking. Four regression tests added ininternal/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|INBOXtoPersonal|Inbox, which would have re-baselined every existing user on upgrade and silently swallowed one round of notifications.MaybeNotifynow takes both the IMAP name (used for the state key) and the UI label (used only for the allowlist comparison) so existingnotify_state.jsonfiles keep working untouched. Regression test added - Fix:
[notifications].foldersallowlist 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 newFoldersConfig.LabelFor()helper before the allowlist check, so users with non-default IMAP folder names get notifications correctly. Regression test added ininternal/config/config_test.go - Fix: notifier can no longer freeze the TUI —
Sendnow runsnotify-send(or whatever[notifications].commandpoints to) under a 2-secondcontext.WithTimeoutplus a 500 mscmd.WaitDelay. A hung notification daemon (broken DBus, mako restarting, …) returns a clearnotify-send: timed out after 2sstatus 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—loadListpreviously 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 firesnotify-send(or anynotify-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.jsonso 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 (everybg_sync_intervalminutes) 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 indocs/content/docs/notifications.mdincluding a wrapper-script recipe forhyprctl notifymore - Whole-domain screening — any screener list line beginning with
@(e.g.@ssp.sh) now matches every address at that domain; works inscreened_in.txt,screened_out.txt,feed.txt,papertrail.txt,spam.txt, andnotify.txt. Per-address entries always win over a@domainentry across all categories so a single blocked address inside an otherwise-approved domain stays blocked (priority order preserved: spam > out > feed > papertrail > in). NewDi/Dochord (works in inbox and reader) appends@<domain>of the cursor or open email's sender toscreened_in.txt/screened_out.txtafter ay/nconfirmation; complements the existingI/Oper-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 verifynotify-send, the icon theme, and the notification daemon (mako/dunst/swaync) are all working without waiting for a real VIP email to arrive:debugreports notifications — diagnostic report now includes the resolved[notifications]config, the contents ofnotify_state.json(per-folder baseline UIDs), the path tonotify.txt, and the list of non-Inbox folders being polled in the background for VIP mail- Visible
notify-senderrors — 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@domaintonotify.txtdirectly 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:
:debugpanicked on accounts withimap_disabled = true—writeDebugReportdereferenced the nil IMAP client thatimap_disabledaccounts 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_disabledaccount 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.goandinternal/daemon/daemon.goconstructedscreener.Configwithout the newNotifypath field, so the in-memory notify set stayed empty even whennotify.txthad entries;ShouldNotifyalways returned false and no notification ever fired. Both call sites now passNotify: cfg.Screener.Notify. Regression test added so this can't recur
2026-05-02 #
- Fix: emoji reaction sent through wrong SMTP account —
ctrl+ereactions already auto-selected the correct From address (matching whichever of your addresses received the email), butsendReaction()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 tosimon@ssp.sh(Work, account index 1) while in the Personal inbox would send the reaction header assimon@ssp.shbut authenticate via Personal's SMTP and grabSenders[0]; reactions now usepresendSMTPAccount()(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 settingimap_disabled = true; neomd skips IMAP connection, folder fetching, and screening for that account; the account remains available as a From address viactrl+fin compose/pre-send;ctrl+aaccount cycling skips disabled accounts; useful for adding Gmail or other providers purely for sending without fetching thousands of emails;:debugshows "(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_pixelsand persist across restarts; both positive and negative scan results are cached so repeat scans are instant - URL scheme whitelist — email links opened via
space+digitare now validated; onlyhttp://,https://, andmailto: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'snet/http.DetectContentType()catches disguised files (e.g. a script renamed to.pngis detected astext/plainand blocked); status bar warns about dangerous or suspicious file types - Browser view sanitization — pressing
Oto 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
spacein the reader now shows all available actions (1-0 links,d download .eml,l11-99 links 11+) instead of only link info;space+dfor 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.logwith 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
Rrefresh 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:portformat 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) andTestIntegration_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 defaultmailto:handler; clicking amailto: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 bothneomd --mailto "mailto:user@example.com?subject=Hello"andneomd "mailto:..."(positional, for.desktopintegration); registered viaxdg-mimewith aneomd-mailto.desktopfile; 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+din reader) — saves the full raw MIME source as.emlto~/Downloads/with filenameneomd-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 toscheduledstatus with configurable delay (default 30 minutes); authentication via HTTP Basic Auth with environment variable expansion for API token; new self-containedinternal/listmonk/package with full test coverage (httptest mocks); documented indocs/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; usesnet/mail.ParseAddress()for robust RFC 5322 address parsing; validates From address before sending and rejects invalid addresses that would result in@localhostMessage-IDs; added comprehensive test coverage for BuildMessage, BuildDraftMessage, and BuildReactionMessage paths; documented email standards compliance indocs/email-standards.md - Fix: From validation allows local-only addresses —
extractDomain()now returns(domain, ok bool)to distinguish between parsing failures (invalid address) and validuser@localhostaddresses; 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_InvalidFromnow usest.Fatalf()when validation incorrectly succeeds, preventing nil pointer panic onerr.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 ofbgSyncTickMsg{}, preventing tight loop that consumes large amounts of RAM; addedbgSyncInProgressflag that covers the entire fetch-and-screen cycle (kept set untilbgScreenDoneMsg), preventing concurrent background syncs from piling up during slow network or long screening operations - Fix: reply-all excludes all own addresses —
ctrl+rreply-all now excludes both IMAP login addresses (account.User) and send-as addresses (account.From,sender.From) from the CC field; fixes edge cases whereuser != from(e.g., login asuser123@provider.combut send assimon@domain.com) would still leak the login address into CC; previously only excludedFromaddresses. 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 everybg_sync_intervalminutes (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 withlog/slog; use case: run daemon on NAS withbg_sync_interval = 5, disable background sync on laptop/Android withbg_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 indocs/configurations/headless.mdwith Syncthing setup guide, systemd service example, and multi-device workflow;make daemontarget 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 bothfeed.txtandscreened_out.txt, causing duplicates in screener state and sync conflicts across devices; previously onlyApprove,Block, andMarkSpamhad partial cleanup logic, whileMarkFeedandMarkPaperTrailonly 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 buildnow automatically createsneomd-freebsdstatic binary alongside Linux binary; FreeBSD binary can be copied to FreeBSD/OpenBSD servers without needing Go installed;make sync-headlesstarget 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_secsconfig — new[ui]option to control mark-as-read delay in seconds (default 7); set to0for 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 #
Bmove to Work/business — pressBto move marked or cursor email(s) to Work folder (similar toAfor 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 existinggb(go to Work) andMb(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+wwelcome shortcut — pressspacethenwto reopen the welcome screen anytime; useful for reviewing keybindings and getting started guide; documented in help overlay and keybindings referenceNjump to next unread — pressNto 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 existztoggle unread-only view — presszto filter the inbox to show only unread emails (mnemonic: "zero in on unread"); presszagain to show all emails; works alongside text filter (/) and can be cleared withesc; 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") andMc(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-0opens links 1-10,space+l11-99opens links 11-99 using intuitive numeric shortcuts (e.g.space+l26for 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; pressctrl+eto open emoji picker overlay, select with1-8for instant send or navigate withj/kand pressenter; sends minimal reaction email (emoji + italic footer + quoted original message) with proper threading headers; available reactions: 👍 ❤️ 😂 🎉 🙏 💯 👀 ✅; original email marked with\Answeredflag; reaction saved to Sent folder; auto-selects From address matching recipient (same logic as regular replies) - Email threading headers — all replies (regular
r/Rand emoji reactionsctrl+e) now include properIn-Reply-ToandReferencesheaders for conversation threading; ensures replies appear correctly grouped in Gmail, Outlook, and Apple Mail conversation views;Referencesheader extracted from IMAP message body and preserved in reply chain - Fix: refresh not showing new emails immediately — pressing
Rnow correctly displays new emails on first refresh; previously the IMAP client cached the selected mailbox state, so the firstRwould skip re-SELECT and use stale UID SEARCH results (showing the old unread count but no new messages in the list); required a secondRor 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 separatetextandhtmlfields 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 legacysignaturefield - 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 = truefor 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_filesupport for trusting Bridge’s exported self-signed certificate, and added a narrow localhost-only TLS retry fallback for Bridge connections on127.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
ToScreenscreening 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 withesc— pressingescnow reliably clears the in-memory inbox filter even after the filter was already applied - Help overlay improvements —
?help is now scrollable withj/k, arrow keys, andd/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
yaziand the externalcustom.luaintegration as optional requirements for<leader>a, while clarifying that pre-sendastill works independently - UX hints — inbox footer now exposes
, sort; pre-send footer clarifiessas spell-check-and-edit versus plaineedit; compose/pre-sendctrl+fnow 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 inToScreennow 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, orSpamare configured to the same IMAP folder as Trash - Inbox paging clarity — the inbox header now shows the current fetch limit (
loaded/limit) andd/upage movement directly, so the “only 50 emails” behavior is visible without guessing - Discard confirmation for unsent mail —
escin compose andesc/xin 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-allscans the full Inbox on the IMAP server - Compose/draft round-trip preservation — editor/pre-send/draft/recover flows now preserve
Bccand selectedFrom; 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 theBccheader 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
:searchhelp 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 (
Obrowser preview) now sanitizesContentIDandFilenamefrom email MIME headers to prevent attackers from writing files outside/tmp/neomd/via maliciouscid:references (e.g.../../etc/cron.d/evil); all attachment paths now usefilepath.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;imapSearchResultsflag is cleared immediately so the status bar appears instead of the search bar; added general Esc handler foroffTabFolderviews that preserves search context: pressing Esc from thread view returns to IMAP search results if that's where you came from (checked viaimapSearchText), otherwise returns to active folder;imapSearchTextis 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 becauseimapSearchResultsis 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) andMb(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:1143and127.0.0.1:1025); previously hardcoded port-based logic ignored the user'sstarttlsconfig and refused unencrypted connections to any port other than 993/143 (IMAP) or 465/587 (SMTP); new behavior: user's explicitstarttls = truealways 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 setstarttls = trueif 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
elosing email body — pressingein 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 viadraft_backup_countin[ui], set to-1to disable); no more lost emails after crashes or accidental closes :recover/:reccommand — 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-toscreento 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 veton 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 withfrom:/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 somake teststays 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 toctrl+rwhich works from both inbox list and reader (previouslyRconflicted 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\Answeredflag so it works across clients (reply from webmail → neomd shows it) \Answeredflag on reply — after sending a reply, the original email is automatically marked as\Answeredon the IMAP server- Conversation thread view (
T/:thread) — pressTfrom 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) — optionalwork = "Work"in[folders]config; add"work"totab_orderto show as a tab;gbto go,Mbto move; auto-created on first run if configured; included in Everything, Search, and conversation views - Inline images in browser preview — pressing
Oto 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_editorconfig option — optionalcompose_editorin[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"withoauth2_client_id,oauth2_client_secret,oauth2_issuer_url, andoauth2_scopesinstead 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_bccconfig — root-levelauto_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 entryshift+tabin 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.comreplies fromsimon@domain.com);rnow works from inbox list view;# [neomd: from: ...]shown in editor;xin 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
.txtfiles 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-resetnever 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 viaIn-Reply-To/Message-IDIMAP envelope headers with a reply-prefix subject fallback (only emails withRe:,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/[sto navigate errors,z=for suggestions,zgto add to dictionary; corrected body flows back to pre-send :debug/:dbgcommand — 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.logfor sharing; no sensitive data (passwords, full emails) included- Drafts show recipient — Drafts folder now shows
→ recipientinstead of From (same as Sent tab), since all drafts are from you ctrl+bin pre-send — toggle CC/BCC fields from the pre-send review screen (previously only available during compose)u/Urebind —uis now free for page-up (vim-style half-page scroll);Uis undo last move/delete;ctrl+uclears 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 (alongsideL/Handtab/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; pressescto close results - Filter preserves across actions — the local
/filter no longer clears when pressingn(toggle read),m(mark),U(clear marks), or sorting; filter stays active untilesc - Address autocomplete in compose — To, Cc, and Bcc fields show autocomplete suggestions from screener lists (
screened_in.txt,feed.txt,papertrail.txt); navigate withctrl+n/ctrl+p/arrows, accept withtab; supports multi-address fields (autocomplete applies after the last comma) - Everything view (
geor: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-9in reader) — links are extracted from the email body, numbered[1]-[0]in the header; pressspacethen 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 draftnow 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 indocs/android.mdwith 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.shas an alias through an existing account's SMTP); cycle through all accounts + senders withctrl+fin both compose and pre-send screens; theaccount =field matches by accountname =(not email address) - Sent folder — after sending, neomd APPENDs a copy to the configured Sent IMAP folder with
\Seenflag; 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; press1–9to download attachment N to~/Downloads/and open it withxdg-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 with1–9; previously onlyContent-Disposition: attachmentparts 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 —
ureverses 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.txtlist 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 calleditor.ParseHeaderson the saved file content ctrl+ffor cycling From — changed fromf(which conflicts with typing in text fields) toctrl+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 emptyTo: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-shownensures it only appears once - Auto-create IMAP folders on startup —
ensureFoldersCmdruns duringInit()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/Pon 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 installnow prints clear Go installation instructions instead of a cryptic error whengois not found - Pre-send preview (
p) — presspin 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 tofile://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+bin compose; BCC recipients receive the email but are not visible in the message headers (standard BCC privacy) - Reply-all —
Rin the reader replies to the original sender + all CC recipients; your own address is excluded automatically; usesReply-Toheader when present - Pre-send review screen — after closing the editor, neomd shows a summary (To, Subject, body preview) before sending; press
enterto send,ato attach files via yazi (auto-detected, no config needed; override with$NEOMD_FILE_PICKER),Dto remove last attachment,dto save to Drafts,eto re-open the editor,escto cancel; avoids tmux/terminal key-capture issues sinceaneeds no modifier - Save to Drafts —
din the pre-send screen APPENDs the composed message to the configured Drafts IMAP folder with\Draft+\Seenflags; navigate to it withgd - Attachments from neovim —
<leader>ain aneomd-*.mdbuffer opens yazi in a floating terminal; selected files are inserted as[attach] /path/to/filelines (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 #
gdDrafts navigation — jump to Drafts folder withgdeven 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 tohttp/httpsonly before opening in browser (case-insensitive, RFC 3986);SECURITY.mdadded documenting credential storage, TLS guarantees, screener list handling, and temp file lifecycle with links to source - Spam folder —
$marks a sender as spam (writes tospam.txt, moves to Spam IMAP folder). Separate from ScreenedOut so you never have to look at it again. Navigate withgSor: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.txtediting needed :command history —↑/↓cycles through the last 5 distinct commands;→accepts the ghost completion;ctrl+n/ctrl+pcycle forward/backward through completions. Persists across restarts in~/.cache/neomd/cmd_history(outside dotfiles version control)- Leader key —
spaceis the leader;<space>1–<space>9jumps to a folder tab by number - Auto-screen on inbox load — screener applies automatically on every Inbox load (startup,
R). Disable withauto_screen_on_load = falsein[ui] - Background sync — inbox re-fetched and screened every 5 minutes while neomd is open. Configure with
bg_sync_intervalin[ui];0disables it n/mrebind —ntoggles read/unread (wasN);mmarks for batch ops (wasspace)
2026-03-25 #
- Signature — auto-appended to new compose buffers; configure in
[ui]withsignature - Compose abort — closing the editor with
ZQ/:q!cancels the email; onlyZZ/:wqsends - Browser image workflow —
Oopens email as HTML in$BROWSER;ctrl+oopens the canonical web/newsletter URL (extracted fromList-Postheader);oopens in w3m :create-folders/:cf— creates any missing IMAP folders defined in config (idempotent)