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.

sending emails and drafts: toggle sender

sspaeti b0593e2e fad76c4f

+86 -2
+1
CHANGELOG.md
··· 1 1 # Changelog 2 2 3 3 # 2026-04-10 4 + - **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 4 5 - **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 5 6 - **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 6 7 - **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`
+1
README.md
··· 149 149 from = "Me <me@example.com>" 150 150 starttls = false 151 151 tls_cert_file = "" # optional PEM cert/CA for self-signed local bridges 152 + store_sent_drafts_in_sending_account = false 152 153 153 154 [screener] 154 155 screened_in = "~/.config/neomd/lists/screened_in.txt"
+19
docs/configuration.md
··· 29 29 30 30 # Multiple accounts supported — add more [[accounts]] blocks 31 31 # Switch between them with `ctrl+a` in the inbox 32 + store_sent_drafts_in_sending_account = false # default: Sent/Drafts stay in the first IMAP account 32 33 33 34 # Optional: SMTP-only aliases — cycle with ctrl+f in compose/pre-send 34 35 # [[senders]] ··· 150 151 on a loopback host (`127.0.0.1`, `::1`, `localhost`), neomd retries once with a 151 152 localhost-only fallback so existing Bridge setups keep working. If you want 152 153 strict verification, export the Bridge certificate and set `tls_cert_file`. 154 + 155 + ### Sent and Drafts Storage 156 + 157 + When multiple accounts or `[[senders]]` aliases are configured, SMTP delivery 158 + always uses the selected sending identity's account. 159 + 160 + By default, IMAP storage for Sent and Drafts uses the first configured account, 161 + so one primary mailbox owns your sent/draft archive: 162 + 163 + ```toml 164 + store_sent_drafts_in_sending_account = false 165 + ``` 166 + 167 + If you want Sent and Drafts to follow the selected sending account instead, set: 168 + 169 + ```toml 170 + store_sent_drafts_in_sending_account = true 171 + ``` 153 172 154 173 ## Sending and Discarding 155 174
+6
internal/config/config.go
··· 160 160 Accounts []AccountConfig `toml:"accounts"` 161 161 Account AccountConfig `toml:"account"` // legacy single-account fallback 162 162 163 + // StoreSentDraftsInSendingAccount controls where Sent/Drafts are stored when 164 + // multiple SMTP identities are configured. Default false: always use the 165 + // primary IMAP account (the first configured account). When true, Sent/Drafts 166 + // follow the selected sending account. 167 + StoreSentDraftsInSendingAccount bool `toml:"store_sent_drafts_in_sending_account"` 168 + 163 169 // Senders is a list of extra "From" aliases (use [[senders]] in config.toml). 164 170 // These share the active account's SMTP connection — no IMAP or credentials needed. 165 171 Senders []SenderConfig `toml:"senders"`
+16 -2
internal/ui/model.go
··· 624 624 return m.imapCli() 625 625 } 626 626 627 + func (m Model) primaryIMAPClient() *imap.Client { 628 + if len(m.clients) > 0 { 629 + return m.clients[0] 630 + } 631 + return m.imapCli() 632 + } 633 + 634 + func (m Model) sentDraftsIMAPClient() *imap.Client { 635 + if m.cfg != nil && m.cfg.StoreSentDraftsInSendingAccount { 636 + return m.imapCliForAccount(m.presendSMTPAccount().Name) 637 + } 638 + return m.primaryIMAPClient() 639 + } 640 + 627 641 func (m Model) presendIMAPClient() *imap.Client { 628 - return m.imapCliForAccount(m.presendSMTPAccount().Name) 642 + return m.sentDraftsIMAPClient() 629 643 } 630 644 631 645 func (m *Model) applyEditedFrom(from string) { ··· 726 740 TLSCertFile: smtpAcct.TLSCertFile, 727 741 TokenSource: m.tokenSourceFor(smtpAcct.Name), 728 742 } 729 - cli := m.imapCliForAccount(smtpAcct.Name) 743 + cli := m.sentDraftsIMAPClient() 730 744 sentFolder := m.cfg.Folders.Sent 731 745 replyCli := m.imapCliForAccount(replyToAccount) 732 746 return func() tea.Msg {
+43
internal/ui/model_test.go
··· 142 142 }) 143 143 } 144 144 145 + func TestSentDraftsIMAPClient_DefaultsToPrimaryAccount(t *testing.T) { 146 + cfg := &config.Config{ 147 + Accounts: []config.AccountConfig{ 148 + {Name: "Personal", From: "me@example.com"}, 149 + {Name: "Work", From: "me@work.example"}, 150 + }, 151 + } 152 + personal := imap.New(imap.Config{Host: "personal"}) 153 + work := imap.New(imap.Config{Host: "work"}) 154 + m := Model{ 155 + cfg: cfg, 156 + accounts: cfg.ActiveAccounts(), 157 + clients: []*imap.Client{personal, work}, 158 + presendFromI: 1, // sending as Work 159 + } 160 + 161 + if got := m.sentDraftsIMAPClient(); got != personal { 162 + t.Fatal("sentDraftsIMAPClient() should default to the primary IMAP account") 163 + } 164 + } 165 + 166 + func TestSentDraftsIMAPClient_FollowsSendingAccountWhenEnabled(t *testing.T) { 167 + cfg := &config.Config{ 168 + Accounts: []config.AccountConfig{ 169 + {Name: "Personal", From: "me@example.com"}, 170 + {Name: "Work", From: "me@work.example"}, 171 + }, 172 + StoreSentDraftsInSendingAccount: true, 173 + } 174 + personal := imap.New(imap.Config{Host: "personal"}) 175 + work := imap.New(imap.Config{Host: "work"}) 176 + m := Model{ 177 + cfg: cfg, 178 + accounts: cfg.ActiveAccounts(), 179 + clients: []*imap.Client{personal, work}, 180 + presendFromI: 1, // sending as Work 181 + } 182 + 183 + if got := m.sentDraftsIMAPClient(); got != work { 184 + t.Fatal("sentDraftsIMAPClient() should follow the selected sending account when enabled") 185 + } 186 + } 187 + 145 188 func TestMatchFromAddress(t *testing.T) { 146 189 cfg := &config.Config{ 147 190 Accounts: []config.AccountConfig{