Pure Lua replacement for vim-mail. Designed for the neomutt + nvr + notmuch workflow.
| Feature | Description |
|---|---|
| 📎 Attachment awareness | Warns on save if To:/Subject: empty, headers in body, or "attach" mentioned without attachment |
| 🔗 Muttlook markers | Shows ↩ replying to: and 🔗 thread: as virtual text |
| 📜 Thread context | Opens replied-to message rendered in terminal split below |
| 📇 Contact completion | blink-cmp provider for khard, scoped by account |
| 👁️ Markdown preview | Renders draft via muttlook and opens in browser |
| ✂️ Smart snippets | Context-aware snippets by recipient domain |
| 🧭 Navigation | Jump to headers, body, signature, quotes |
| 🔄 Switch From | Select sender from configured address list |
| 🗑️ Kill quoted sig | Remove quoted signatures from replies |
| 🌐 Spell cycling | Cycle through configured spell languages |
| 📅 Calendar picker | Telescope calendar via kcal (EventKit, ~85ms), MoM mail compose |
| 🔍 Contact picker | Telescope khard+notmuch search, insert/edit/create contacts |
| ✉️ Smart contact resolve | ,mC resolves display names to emails: khard→notmuch→ldap, auto-saves |
graph LR
A[nvim-mail] --> B[neomutt]
A --> C[notmuch]
A --> D[muttlook]
A --> E[khard]
D --> C
D --> F[pandoc]
B --> C
style A fill:#57A143,color:#fff
style D fill:#e06c75,color:#fff
| Tool | Used by | Required |
|---|---|---|
| muttlook | Thread context, Preview, Telescope view | Yes |
| notmuch | Thread context, Contacts, Telescope | Yes |
| nm-livesearch | Telescope async search | Yes (for telescope) |
| nm-html-extract | Telescope preview, Thread context | Yes |
| icalpal | Calendar picker | No (replaced by kcal) |
| kcal | Calendar picker (fast, native EventKit) | Yes (for calendar) |
| khard | Contact completion | Yes |
| telescope.nvim | Mail search | Optional |
| blink.cmp | Completion framework | Optional |
| luasnip | Snippet expansion | Optional |
lazy.nvim:
{
'monkeyxite/nvim-mail',
opts = {
from_list = {
'Your Name <you@company.com>',
'Your Name <you@gmail.com>',
},
spell_langs = { 'en', 'sv' },
send_accounts = {
['work'] = '-e "source ~/.config/mutt/accounts/work.muttrc"',
['personal'] = '-e "source ~/.config/mutt/accounts/personal.muttrc"',
},
contacts = {
from_map = { ['company%.com'] = 'work', ['gmail%.com'] = 'personal' },
notmuch = true,
accounts = {
work = {
cmd = 'khard', args = { 'email', '-p', '--remove-first-line', '-A', 'work' },
notmuch_path = 'work',
from = 'Your Name <you@company.com>',
},
personal = {
cmd = 'khard', args = { 'email', '-p', '--remove-first-line', '-A', 'personal' },
notmuch_path = 'personal@gmail.com',
from = 'Your Name <you@gmail.com>',
},
},
},
},
}All under configurable prefix (default ,m):
| Key | Action |
|---|---|
,mt |
Go to To: |
,mc |
Go to Cc: |
,mb |
Go to Bcc: |
,ms |
Go to Subject: |
,mf |
Go to From: |
,mF |
Switch From address |
,mR |
Go to Reply-To: |
,mB |
Jump to body |
,mS |
Jump to signature |
,mr |
Jump to first quote |
,mE |
End of reply |
,mk |
Kill quoted sig |
,ml |
Cycle spell lang |
| Key | Action |
|---|---|
,mm |
Send mail (account detection + muttlook + neomutt) |
,mq |
Quote selection (normal + visual) |
,mi |
Paste image from clipboard (CID path) |
,ma |
Sync contacts (khard) |
,mT |
Thread context (terminal split) |
,mp |
Preview as HTML (muttlook) |
,mC |
Resolve display names → emails in To/Cc/Bcc (khard→notmuch→ldap) |
,mK |
Contact picker (telescope) |
| Trigger | Action |
|---|---|
:w |
Warns if To: empty, Subject: empty, headers found in body, or "attach" mentioned without attachment |
| Buffer open | Muttlook markers shown as virtual text |
| Buffer open | Treesitter markdown, spell, luasnip |
To:/Cc:/Bcc: |
Contact completion (blink-cmp: khard + notmuch) |
.eml / /tmp/neomutt-* |
Auto-detected as mail filetype |
Via vscode JSON format (snips/snippets/mail.json):
| Trigger | Expands to |
|---|---|
mbr |
Best regards,\nname |
mty |
Thanks for the update. |
mpfa |
Please find attached. |
mfyi |
FYI — context. |
mack |
Acknowledged, will follow up by date. |
mch |
Cheers,\nname |
mlmk |
Let me know what you think. |
msig |
Best,\nname |
require('nvim-mail').setup({
prefix = ',m',
from_list = {},
spell_langs = { 'en' },
snippets = {
name = 'Your Name', -- used in signature snippet placeholders
domains = {
['work%.com'] = 'work', -- domain pattern → snippet context
['gmail%.com'] = 'personal',
},
-- Optional: override snippet definitions per context
-- snippets = { work = { ... }, personal = { ... }, general = { ... } }
},
contacts = {
cmd = 'khard',
args = { 'email', '-p', '--remove-first-line' },
from_map = {},
accounts = {},
},
})sources = {
per_filetype = {
mail = { 'mail_contacts', 'snippets', 'buffer', 'spell', 'path' },
},
providers = {
mail_contacts = {
name = 'Contacts',
module = 'nvim-mail.contacts',
score_offset = 10,
enabled = function() return vim.bo.filetype == 'mail' end,
},
},
}Fuzzy search your maildir via nm-livesearch (same engine as nms):
require('telescope').load_extension('nvim_mail'):Telescope nvim_mail search
:Telescope nvim_mail calendarOr bind:
vim.keymap.set('n', '<leader>sm', require('telescope').extensions.nvim_mail.search, { desc = '[S]earch [M]ail' })
vim.keymap.set('n', '<leader>sc', require('telescope').extensions.nvim_mail.calendar, { desc = '[S]earch [C]alendar' })
vim.keymap.set('n', '<leader>sK', require('telescope').extensions.nvim_mail.contacts, { desc = '[S]earch [K]ontacts' })| Key | Action |
|---|---|
Enter |
Open thread in neomutt |
Ctrl+o |
View in browser (muttlook) |
Ctrl+r |
Reply — opens draft in nvim buffer (use ,m to send) |
Ctrl+t |
GTD tag (archive/action/waiting/defer/done) |
Ctrl+y |
Copy message-id to clipboard |
Ctrl+l |
Full styled preview in split (ANSI colors) |
Ctrl+n/p |
Next/previous item |
Ctrl+d/u |
Scroll preview down/up |
| Key | Action |
|---|---|
Enter |
Start MoM from template |
Ctrl+o |
Open conference URL |
Ctrl+r |
Compose MoM mail — instant buffer with display names, cleaned notes, agenda |
Ctrl+s |
Switch date (type date in prompt first) |
Calendar supports: today, tomorrow, +N, -N, YYYY-MM-DD in prompt.
Powered by kcal — native EventKit, ~85ms vs ~3s for icalpal.
When creating a MoM or reply from the calendar picker, contacts are resolved using the account associated with the event's calendar. Configure the mapping:
require('nvim-mail').setup({
contacts = {
-- Map calendar names (from kcal) → account names
calendar_map = {
['Calendar'] = 'work', -- Exchange default calendar
['you@company.com'] = 'work',
['you@gmail.com'] = 'personal',
},
-- Map account → From address (injected into reply .eml)
account_from = {
work = 'Your Name <you@company.com>',
personal = 'Your Name <you@gmail.com>',
},
-- Map From: patterns → account (for ,mC resolver in compose buffers)
from_map = {
['company%.com'] = 'work',
['gmail%.com'] = 'personal',
},
-- Per-account khard address books
accounts = {
work = {
cmd = 'khard',
args = { 'email', '-p', '--remove-first-line', '-A', 'work' },
notmuch_path = 'work',
},
personal = {
cmd = 'khard',
args = { 'email', '-p', '--remove-first-line', '-A', 'personal' },
notmuch_path = 'you@gmail.com',
},
},
work_domain = 'company.com',
},
})Resolution priority per account:
- khard — account-scoped address book (
-A workor-A personal) - notmuch — scoped by
path:work/**orpath:you@gmail.com/** - Corporate pattern —
first.last@work_domain(work account only) - LDAP — DavMail corporate directory (work account only)
:Telescope nvim_mail contacts| Key | Action |
|---|---|
Enter |
Insert Name <email> at cursor |
Ctrl+e |
Expand with notmuch addresses (async) |
Ctrl+o |
Edit contact in khard |
Ctrl+n |
Create new khard contact from entry |
Ctrl+y |
Yank email to clipboard |
Loads khard contacts instantly (~300ms). Ctrl+e merges notmuch addresses on demand.
Resolves display names in To:/Cc:/Bcc: to emails using a 3-stage pipeline:
- khard — local vcard store, 3 name variants (original, suffix-stripped, first+last)
- notmuch — corporate email pattern (
first.last@company.com) with transliteration (ä→a, ö→o) and name validation - ldap — DavMail LDAP as last resort, shows
⏳ LDAP lookup for: <name>warning
Successful notmuch and ldap results are auto-saved to khard for instant lookup next time.
Features:
- Live results as you type (nm-livesearch async streaming)
- Preview: styled meeting details (attendees, time, location, notes)
- MoM template with attendees pre-filled
- Account-scoped mail search via
notmuch_pathconfig
nvim --headless --clean -u tests/minimal_init.lua \
-c "PlenaryBustedDirectory tests/mail/ {minimal_init = 'tests/minimal_init.lua'}"52 tests covering: attachment detection, marker parsing, thread commands, navigation, contacts, snippets, preview.
lua/nvim-mail/
├── init.lua — setup, keymaps, autocmds, ,mC resolver
├── attachment.lua — attachment mention detection
├── marker.lua — muttlook marker extmarks
├── thread.lua — nm-html-extract thread in terminal split
├── contacts.lua — blink-cmp provider for khard
├── contacts_picker.lua — telescope contact picker (khard+notmuch)
├── calendar.lua — telescope calendar picker (kcal)
├── preview.lua — muttlook draft preview
├── snippets.lua — context detection
└── navigate.lua — header/body/signature navigation
Drop-in replacement for dbeniamine/vim-mail. All navigation keys preserved under the same ,m prefix. Remove vim-mail from your plugin manager and add nvim-mail instead.
MIT