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

Configure Feed

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

reply-all will exclude all configured accounts, not only current

+13 -4
+3
CHANGELOG.md
··· 1 1 # Changelog 2 2 3 + # 2026-04-21 4 + - **Fix: reply-all excludes all own addresses** — `ctrl+r` reply-all now excludes all configured email addresses (accounts + sender aliases) from the CC field; previously only excluded the active account's address, causing your own email to appear in CC when the original email was sent to one of your other addresses 5 + 3 6 # 2026-04-18 4 7 - **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 5 8 - **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
+10 -4
internal/ui/model.go
··· 3884 3884 3885 3885 cc := "" 3886 3886 if replyAll { 3887 - // Collect original To + CC, exclude own address 3888 - own := strings.ToLower(extractEmailAddr(m.activeAccount().User)) 3887 + // Collect original To + CC, exclude all own addresses 3888 + ownAddrs := make(map[string]bool) 3889 + for _, from := range m.presendFroms() { 3890 + ownAddrs[strings.ToLower(extractEmailAddr(from))] = true 3891 + } 3889 3892 var parts []string 3890 3893 for _, addr := range splitAddrs(e.To + "," + e.CC) { 3891 - if a := strings.TrimSpace(addr); a != "" && strings.ToLower(extractEmailAddr(a)) != own { 3892 - parts = append(parts, a) 3894 + if a := strings.TrimSpace(addr); a != "" { 3895 + addrLower := strings.ToLower(extractEmailAddr(a)) 3896 + if !ownAddrs[addrLower] { 3897 + parts = append(parts, a) 3898 + } 3893 3899 } 3894 3900 } 3895 3901 cc = strings.Join(parts, ", ")