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.

disable threading in sent emails

sspaeti 62ba94f7 16606984

+28 -3
+3
CHANGELOG.md
··· 1 1 # Changelog 2 2 3 + # 2026-04-24 4 + - **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 5 + 3 6 # 2026-04-23 4 7 - **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 5 8 - **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](https://listmonk.app) 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`
+7 -2
internal/ui/inbox.go
··· 238 238 // It threads emails before display — grouped conversations appear together 239 239 // with tree-drawing prefixes (┌─>) on reply rows. 240 240 // Sorting respects the user's chosen sortField and sortReverse preferences. 241 - func setEmails(l *list.Model, emails []imap.Email, marked map[uint32]bool, prefixFolders bool, sortField string, sortReverse bool) tea.Cmd { 242 - threaded := threadEmails(emails, sortField, sortReverse) 241 + func setEmails(l *list.Model, emails []imap.Email, marked map[uint32]bool, prefixFolders bool, sortField string, sortReverse bool, disableThreading bool) tea.Cmd { 242 + var threaded []threadedEmail 243 + if disableThreading { 244 + threaded = flatEmails(emails, sortField, sortReverse) 245 + } else { 246 + threaded = threadEmails(emails, sortField, sortReverse) 247 + } 243 248 items := make([]list.Item, len(threaded)) 244 249 for i, te := range threaded { 245 250 displaySubj := te.email.Subject
+2 -1
internal/ui/model.go
··· 2784 2784 filtered = m.emails 2785 2785 } 2786 2786 2787 - return setEmails(&m.inbox, filtered, m.markedUIDs, m.shouldPrefixFolderInSubject(), m.sortField, m.sortReverse) 2787 + noThread := len(m.folders) > 0 && m.activeFolder() == m.cfg.Folders.Sent 2788 + return setEmails(&m.inbox, filtered, m.markedUIDs, m.shouldPrefixFolderInSubject(), m.sortField, m.sortReverse, noThread) 2788 2789 } 2789 2790 2790 2791 // handleChord dispatches two-key sequences (g<x>, M<x>, space<x>).
+16
internal/ui/thread.go
··· 94 94 threadPrefix string // "│" = continuation, "╰" = root, "" = not threaded 95 95 } 96 96 97 + // flatEmails returns emails sorted without any threading/grouping. 98 + func flatEmails(emails []imap.Email, sortField string, sortReverse bool) []threadedEmail { 99 + result := make([]threadedEmail, len(emails)) 100 + for i, e := range emails { 101 + result[i] = threadedEmail{email: e} 102 + } 103 + sort.SliceStable(result, func(i, j int) bool { 104 + cmp := compareEmails(result[i].email, result[j].email, sortField) 105 + if sortReverse { 106 + return cmp > 0 107 + } 108 + return cmp < 0 109 + }) 110 + return result 111 + } 112 + 97 113 // threadEmails groups and reorders emails into threaded display order. 98 114 // 99 115 // Threading uses two strategies: