A powerful and flexible mail reader interface for NeoVim. This plugin bridges your email and text editing experiences directly within NeoVim by interfacing with the Notmuch mail indexer.
Notmuch.nvim is a NeoVim plugin that serves as a front-end for the Notmuch mail indexer, enabling users to read, compose, and manage their emails from within NeoVim. It facilitates a streamlined workflow for handling emails using the familiar Vim interface and motions.
- π§ Email Browsing: Navigate emails with Vim-like movements.
- π Search Your Email: Leverage
notmuchto search your email interactively. - π Thread Viewing: Messages are loaded with folding and threading intact.
- π Attachment Management: View/open/save received attachments, and manage outgoing draft attachments with both commands and an editable scratch window.
- π Inline HTML Rendering: Render HTML email bodies as text via
w3m. - β¬οΈ Offline Mail Sync: Supports
mbsyncfor efficient sync processes, with buffer, background, and interactive terminal modes. - π Async Search: Large mailboxes with thousands of email? No problem.
- π·οΈ Tag Management: Conveniently add, remove, or toggle email tags.
- π» Pure Lua: Fully implemented in Lua for performance and maintainability.
- π (WIP)
Telescope.nvim Integration: Search interactively, extract URL's, jump efficiently, with the powerful file picker of choice.
- NeoVim: Version 0.10 or later is
required (uses
vim.system(),vim.bbuffer variables, and other modern APIs). - Notmuch: Ensure Notmuch and libnotmuch library are installed
- w3m (optional): Required for inline HTML
email rendering when
render_html_body = true - (WIP)
Telescope.nvim: File picker of choice for many use cases.
You can install Notmuch.nvim using your favorite NeoVim plugin manager.
{
'yousefakbar/notmuch.nvim',
config = function()
-- Configuration goes here
local opts = {}
require('notmuch').setup(opts)
end,
}If you are using nvim v0.12, or above, you can install using the builtin
package manager:
vim.pack.add({
'https://github.com/yousefakbar/notmuch.nvim',
})
-- Or to pin to a specific tag/version:
vim.pack.add({
{
src = 'https://github.com/yousefakbar/notmuch.nvim',
version = 'v0.3.0', -- Or git commit, etc.
},
})Clone the repository and add the directory to your runtimepath:
git clone https://github.com/yousefakbar/notmuch.nvim.gitHere are the core commands within Notmuch.nvim:
-
:Notmuch: Lists available tags in your Notmuch database in a buffer. Setup key bindings for easy access. Example:-- Define a keymap to run `:Notmuch` and launch the plugin landing page vim.keymap.set("n", "<leader>n", "<CMD>Notmuch<CR>")
-
:NmSearch <query>: Executes an asynchronous search based on provided Notmuch query terms." Loads the threads in your inbox received today :NmSearch tag:inbox and date:today
-
:Inbox [email]: Quick access to your inbox. Optionally filter by recipient email address (Useful for multi-account setups.)" Open all inbox messages :Inbox " Open inbox for a specific account :Inbox work@example.com
-
:ComposeMail [address...]: Opens a persistent compose draft. Drafts are saved underdrafts.folderand can be reopened later. -
:NotmuchDrafts: Opens a global draft picker for compose and reply drafts.
You can configure several global options to tailor the plugin's behavior:
| Option | Description | Default |
|---|---|---|
notmuch_db_path |
Directory containing the .notmuch/ dir |
From notmuch config |
maildir_sync_cmd |
Bash command to run for syncing maildir | mbsync -a |
sync.sync_mode |
Sync display mode: "buffer", "background", or "terminal" (PTY with stdin) |
buffer |
send.send_mode |
Send mode: "terminal" (PTY with stdin) or "background" |
terminal |
queries |
Saved/pinned queries shown at top of :Notmuch dashboard; hidden when empty |
{} |
keymaps |
Configure any (WIP) command's keymap | See config.lua1 |
attachments.cache_dir |
Shorthand for received attachment open/view cache directory | stdpath("cache")/notmuch.nvim/attachments |
attachments.open |
List of received attachment open rules tried before defaults | {} |
attachments.view |
List of received attachment view rules tried before defaults | {} |
attachments.window |
Shorthand for floating attachment preview window options | { width = 0.8, height = 0.8, border = "rounded" } |
attach.incoming.* |
Advanced received attachment rule patch API | See below |
render_html_body |
Render HTML email bodies inline using w3m (requires w3m installed) |
false |
thread_view_mode |
Thread view mode: "threaded", "newest-first", or "oldest-first" |
"threaded" |
drafts.folder |
Directory used for persistent compose/reply draft .eml files and JSON metadata |
stdpath("data")/notmuch.nvim/drafts |
drafts.delete_sent |
Delete the persistent draft after a successful send instead of marking it sent | false |
drafts.show_sent_drafts |
Include sent drafts in draft pickers | false |
drafts.auto_open_attachment_window |
Automatically open the draft attachment scratch window when a draft opens | false |
suppress_deprecation_warning |
Suppress the warning shown when using deprecated notmuch API (< 0.32) | false |
Example configuration in plugin manager (lazy.nvim):
{
"yousefakbar/notmuch.nvim",
opts = {
notmuch_db_path = "/home/xxx/Documents/Mail",
maildir_sync_cmd = "mbsync personal",
sync = {
sync_mode = "buffer" -- OR "background" OR "terminal"
},
send = {
send_mode = "terminal" -- OR "background"
},
keymaps = {
sendmail = "<C-g><C-g>",
},
drafts = {
folder = vim.fn.stdpath("data") .. "/notmuch.nvim/drafts",
delete_sent = false,
show_sent_drafts = false,
auto_open_attachment_window = false,
},
render_html_body = true, -- Render HTML emails inline (requires w3m)
queries = {
{ name = "π€ Sent today", query = "tag:sent and date:today" },
{ name = "β οΈ IMPORTANT", query = "tag:flagged or tag:pr or tag:urgent" },
{ name = "β Overdue (+3d)", query = "tag:inbox and date:..3d" },
},
thread_view_mode = "threaded", -- OR "newest-first" OR "oldest-first"
},
},Compose and reply buffers are backed by persistent drafts. Each draft stores the
editable message in an .eml file and metadata, including outgoing attachment
paths, in a JSON sidecar. The JSON sidecar is the persistent source of truth for
draft attachments, mirrored while the draft is open in vim.b.notmuch_attachments.
Outgoing attachments can be managed in either of two synchronized ways:
- Edit the attachment scratch window, opened with the configured
keymaps.attachment_windowmapping or:AttachOpen. - Use buffer-local commands in compose/reply draft buffers:
:Attach {path}adds a validated attachment path.:AttachRemove {path}removes an attachment, with completion from the current attachment list.:AttachListprints the current attachment list.:AttachOpenopens the editable attachment scratch window.
The scratch window and commands update the same draft attachment state. Set
drafts.auto_open_attachment_window = true if you want the scratch window to
open automatically whenever a draft opens.
Received-message attachments use rule registries instead of monolithic handler
callbacks. Open/view actions extract the selected MIME part to
attach.incoming.cache_dir, build a structured attachment object, then resolve
open or view rules. Save actions use the same extractor but write atomically to
the user-selected path.
For everyday customization, use the attachments shorthand. Rules listed in
attachments.open and attachments.view are tried before the defaults. The old
received attachment open_handler and view_handler setup callbacks have been
removed; use open/view rules instead.
require('notmuch').setup({
attachments = {
open = {
{
name = 'pdf-zathura',
match = { ext = 'pdf' },
command = { 'zathura', '$path' },
detach = true,
fallback = 'Could not open PDF with zathura.',
},
},
view = {
{
name = 'pdf',
match = { content_type = 'application/pdf' },
commands = {
{ 'pdftotext', '-raw', '$path', '-' },
},
filetype = 'text',
fallback = 'Install pdftotext to preview PDFs.',
},
},
window = {
width = 0.9,
height = 0.9,
border = 'rounded',
},
},
})Advanced users can still use attach.incoming.open.rules and
attach.incoming.view.rules directly as patch tables:
prepend: try rules before defaults;append: try rules after defaults;replace: replace a default rule by name;disable: disable default rules by name.
Example: replace the default PDF preview rule:
require('notmuch').setup({
attach = {
incoming = {
view = {
rules = {
replace = {
pdf = {
name = 'pdf',
match = { content_type = 'application/pdf' },
commands = {
{ 'pdftotext', '-raw', '$path', '-' },
},
filetype = 'text',
fallback = 'Install pdftotext to preview PDFs.',
},
},
},
},
},
},
})The default open rule prefers vim.ui.open() when available and falls back to
the OS opener command. Default view rules cover HTML, PDF, images, Office
documents, Markdown, archives, text, and binary fallbacks.
When viewing a thread, the plugin exposes buffer-local variables that can be used for statusline integration or other extensibility purposes:
| Variable | Description |
|---|---|
vim.b.notmuch_thread |
Thread metadata (ID, subject, tags, authors, message count) |
vim.b.notmuch_messages |
Array of all messages with line positions and metadata |
vim.b.notmuch_current |
Cursor-tracked current message (updates on CursorMoved) |
vim.b.notmuch_status |
Pre-formatted statusline string (e.g., "2/5 John Doe π1") |
Example statusline integration with lualine:
require('lualine').setup({
sections = {
lualine_c = {
{
function() return vim.b.notmuch_status or '' end,
cond = function() return vim.bo.filetype == 'mail' end,
},
},
},
})This project is licensed under the MIT License, granting you the freedom to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell copies. The
MIT License's full text can be found in the LICENSE section of the project's
documentation.
For more details on usage and advanced configuration options, please refer to
the in-depth plugin help within NeoVim: :help notmuch.