Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
6010880
feat(view): add support for viewing calendar file attachments
jugarpeupv Jan 19, 2026
3c83f8b
feat: add ability to supply email adress to Inbox command
antinomie8 Jan 10, 2026
d2b313a
fix: cannot compare tables in Inbox command
antinomie8 Jan 17, 2026
18e9a3e
style: apply consistent formatting to init.lua
yousefakbar Feb 1, 2026
0b1ab70
docs: document email address argument for Inbox command
yousefakbar Feb 1, 2026
bb6917b
refactor(thread): migrate to JSON parsing for thread display
yousefakbar Jan 14, 2026
b9fc182
feat(thread): add optional HTML body rendering via w3m
yousefakbar Jan 15, 2026
b9fc362
feat(thread): export thread metadata to buffer-local variable
yousefakbar Jan 16, 2026
b570007
feat(thread): add per-message metadata buffer variable
yousefakbar Jan 18, 2026
91c9379
feat(thread): add cursor-tracked current message buffer variables
yousefakbar Jan 24, 2026
1fee263
refactor: replace `util.find_cursor_msg_id()` with buffer-local access
yousefakbar Jan 24, 2026
8db16c0
docs(changelog): document buffer-local variables and message lookup i…
yousefakbar Jan 26, 2026
ecd72ff
fix(thread): process all root-level nodes in thread display
yousefakbar Jan 28, 2026
d6b1bd2
fix(thread): find next message when cursor is between boundaries
yousefakbar Feb 1, 2026
078e261
docs: add buffer-local variables and render_html_body documentation
yousefakbar Feb 4, 2026
09ea44a
refactor: port completion functions from vimscript to lua
antinomie8 Jan 21, 2026
a1da87f
fix(cmdline_completions): filter search results
antinomie8 Feb 6, 2026
d5453f5
refactor(completion): rename cmdline_completions module to completion
yousefakbar Feb 6, 2026
b4bf8c8
docs(changelog): document vimscript to lua migration
yousefakbar Feb 6, 2026
84b1104
docs: update documentation for vimscript to lua migration
yousefakbar Feb 6, 2026
5374489
refactor(ftplugin): migrate tag completion to lua module
yousefakbar Feb 6, 2026
7c9b1d9
Merge remote-tracking branch 'upstream'
jugarpeupv Feb 7, 2026
4d4188b
Merge branch 'fix/previous_list_messages'
jugarpeupv Feb 7, 2026
169125e
Merge remote-tracking branch 'upstream'
jugarpeupv Feb 11, 2026
07b63c9
Merge remote-tracking branch 'upstream'
jugarpeupv Feb 16, 2026
23245c9
feat(thread): add configurable message order in thread view
jugarpeupv Feb 18, 2026
91ada04
feat(compose): add oil.nvim-style attachment buffer
jugarpeupv Feb 19, 2026
4a54161
feat(thread): improve thread buffer UX and navigation
jugarpeupv Apr 14, 2026
286f696
feat(attach): render images inline with image.nvim
jugarpeupv Apr 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 73 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ with the [Notmuch mail indexer](https://notmuchmail.org).
3. [Requirements](#requirements)
4. [Installation](#installation)
5. [Usage](#usage)
6. [Configuration Options](#configuration-options)
7. [License](#license)
6. [Composing and Replying](#composing-and-replying)
7. [Configuration Options](#configuration-options)
8. [License](#license)

## Introduction

Expand All @@ -30,7 +31,10 @@ the familiar Vim interface and motions.
- 📧 **Email Browsing**: Navigate emails with Vim-like movements.
- 🔍 **Search Your Email**: Leverage `notmuch` to search your email interactively.
- 🔗 **Thread Viewing**: Messages are loaded with folding and threading intact.
- 📎 **Attachment Management**: View, open and save attachments easily.
- 📎 **Attachment Management**: View, open and save attachments from received mail.
- ✉️ **Compose and Reply**: Write new emails and replies with MIME support.
- 📂 **Outgoing Attachments**: Attach files via commands, prompts, or paste from
[oil.nvim](https://github.com/stevearc/oil.nvim) buffers.
- 🌐 **Inline HTML Rendering**: Render HTML email bodies as text via `w3m`.
- ⬇️ **Offline Mail Sync**: Supports `mbsync` for efficient sync processes, with buffer, background, and interactive terminal modes.
- 🔓 **Async Search**: Large mailboxes with thousands of email? No problem.
Expand Down Expand Up @@ -123,6 +127,72 @@ Here are the core commands within Notmuch.nvim:
:Inbox work@example.com
```

## Composing and Replying

### Commands

- **`:ComposeMail [to]`**: Open a compose buffer for a new email. Optionally
provide a recipient address.
- **`C`**: Compose a new email (mapped in hello, threads, and thread view buffers).
- **`R`**: Reply to the message under the cursor (mapped in thread view).
- **`<C-g><C-g>`**: Send the email from the compose buffer (configurable via
`keymaps.sendmail`).
- **`<C-g><C-a>`**: Toggle the attachment window (configurable via
`keymaps.attachment_window`).

The compose buffer is a standard editable buffer with email headers (`From`,
`To`, `Cc`, `Subject`) at the top and the message body below. Sending uses
`msmtp` under the hood.

### Attachments

There are several ways to attach files to an outgoing email:

#### 1. `:Attach` command

From the compose buffer, run `:Attach <path>` with file completion:

```vim
:Attach ~/Documents/report.pdf
:Attach /tmp/screenshot.png
```

Use `:AttachRemove <path>` to remove an attachment (with completion from the
current attachment list), and `:AttachList` to open the attachment window.

#### 2. Attachment window

Press `<C-g><C-a>` (or run `:AttachList`) to open the attachment buffer. This
is an editable buffer (similar to oil.nvim) where each line below the header
is a file path:

| Key | Action |
| :--- | :--------------------------- |
| `a` | Add attachment via prompt |
| `dd` | Delete attachment |
| `p` | Paste (with oil.nvim support)|
| `:w` | Save/validate attachments |
| `q` | Close window |

You can also type or paste absolute paths directly and press `:w` to validate
and save them.

#### 3. Paste from oil.nvim

If you use [oil.nvim](https://github.com/stevearc/oil.nvim), you can yank a
line from an oil buffer (`yy`) and paste it into the attachment window with
`p`. The plugin uses oil's API (`oil.get_entry_on_line()` and
`oil.get_current_dir()`) to resolve the actual file path regardless of your
oil column configuration.

This also works as a fallback when saving (`:w`) the attachment buffer -- if a
line doesn't resolve as a direct path, the plugin checks open oil buffers to
see if it matches a rendered oil line and resolves it automatically.

> **Note**: oil.nvim is entirely optional. All attachment features work without
> it. The oil integration is a convenience for users who already use oil as
> their file manager.

## Configuration Options

You can configure several global options to tailor the plugin's behavior:
Expand Down
125 changes: 124 additions & 1 deletion doc/notmuch.txt
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ CONTENTS *notmuch-contents*
Other Notmuch Plugins |notmuch-other-plugins|
Usage |notmuch-usage|
Commands |notmuch-commands|
Composing and Replying |notmuch-compose|
Attachments |notmuch-attachments|
Options |notmuch-options|
Completion |notmuch-completion|
Behavior |notmuch-behavior|
Expand Down Expand Up @@ -235,6 +237,117 @@ Notmuch.nvim plugin and the local mail system to search and browse.
Provides autocomplete for email addresses from the notmuch
database. Essentially implemented by the |NmSearch| command.

------------------------------------------------------------------------------
COMPOSING AND REPLYING *notmuch-compose*

Notmuch.nvim supports composing new emails and replying to existing messages.
The compose buffer is a standard editable buffer with email headers at the
top and the message body below. Sending is handled by `msmtp`.

*:ComposeMail*
:ComposeMail [to] Opens a compose buffer for a new email. Optionally
provide a recipient address. The buffer is populated
with headers (From, To, Cc, Subject) and a body area.

Available from any notmuch buffer via the `C` keymap,
or as a command from the command line: >

:ComposeMail user@example.com
<

Replying~

In thread view, press `R` to reply to the message under the cursor. This
creates a compose buffer pre-populated with the reply headers and quoted
message text (via `notmuch reply`).

Sending~

Once you have composed your email, press `<C-g><C-g>` (configurable via
|keymaps|) to send it. The plugin will prompt for confirmation, build the
MIME message (including any attachments), and send it via `msmtp`.

Keymaps available in the compose buffer~

`<C-g><C-g>` Send the email
`<C-g><C-a>` Toggle the attachment window

These keymaps are configurable via the |keymaps| option:

`keymaps.sendmail` Default: `<C-g><C-g>`
`keymaps.attachment_window` Default: `<C-g><C-a>`

------------------------------------------------------------------------------
ATTACHMENTS *notmuch-attachments*

There are several ways to attach files to an outgoing email. Attachments are
encoded as base64 MIME parts in the outgoing message.

Commands~
*:Attach*
:Attach {path} Attach a file to the email being composed. Supports
file completion. The path is expanded and converted to
an absolute path before validation. >

:Attach ~/Documents/report.pdf
:Attach /tmp/screenshot.png
<
*:AttachRemove*
:AttachRemove {path} Remove a previously attached file. Provides completion
from the current attachment list.

*:AttachList*
:AttachList Open the attachment buffer window. Same as pressing
`<C-g><C-a>` in the compose buffer.

Attachment Buffer~
*notmuch-attachment-buffer*
The attachment buffer is an editable buffer where each line below the
header represents a file path. It behaves similarly to oil.nvim: you can
freely edit lines, and changes are applied when you save with `:w`.

The buffer has a protected header area (title, hints, and separator line)
and a file area below it. Only the file area is editable.

Keymaps in the attachment buffer:

`a` Add attachment via prompt with file completion
`dd` Delete the attachment under the cursor
`p` Paste below (with oil.nvim line resolution)
`P` Paste above (with oil.nvim line resolution)
`gg` Jump to the first file line (skips header)
`i` Insert mode (cursor moved below header if needed)
`o` Open a new line below
`O` Open a new line above
`:w` Validate all paths, remove invalid ones, and save
`q` Close the attachment window

When saving with `:w`, each line is expanded and validated. Invalid paths
are removed from the buffer and a warning is shown. Valid paths are stored
in the `vim.b.notmuch_attachments` buffer variable on the compose buffer.

Pasting from oil.nvim~
*notmuch-oil-integration*
If you use oil.nvim (https://github.com/stevearc/oil.nvim), you can yank
a line from an oil buffer and paste it into the attachment buffer with `p`
or `P`. The plugin uses oil's public API to resolve the file path:

- `oil.get_current_dir()` to get the directory of the oil buffer
- `oil.get_entry_on_line()` to get the actual filename for a line

This works regardless of how your oil columns are configured (icons,
permissions, dates, sizes, etc.) because the plugin matches against the
rendered buffer lines and asks oil for the real entry, rather than trying
to parse the display format.

The oil resolution also runs as a fallback when saving (`:w`) the
attachment buffer. If a line does not resolve as a direct file path, the
plugin checks open oil buffers to see if it matches a rendered line.

Note: oil.nvim is entirely optional. All attachment features work without
it. The `:Attach` command, the `a` prompt keymap, and typing absolute
paths directly into the buffer all work independently of oil.

------------------------------------------------------------------------------
OPTIONS *notmuch-options*

Expand Down Expand Up @@ -647,12 +760,22 @@ plugin's project codebase.
- Directly interacts with the Notmuch database via LuaJIT bindings.

- **attach.lua**:
- Manages email attachments, providing functions to:
- Manages received email attachments, providing functions to:
- List attachments in a thread.
- Save attachments to a directory.
- Open attachments using external programs.
- Supports extracting URLs from messages and following GitHub patch links.

- **attach_buffer.lua**:
- Implements the oil.nvim-style attachment buffer for composing emails.
- Handles buffer creation, display, keymaps, and sync on `:w`.
- Resolves pasted oil.nvim lines to file paths using oil's public API.
- Validates and stores attachment paths in buffer-local variables.

- **attach_cmd.lua**:
- Defines the `:Attach`, `:AttachRemove`, and `:AttachList` buffer-local
commands available in compose buffers.

- **handlers.lua**:
- Defines default handlers for opening and viewing attachments.
- **default_open_handler()**: OS-aware external opener using open,
Expand Down
4 changes: 2 additions & 2 deletions ftplugin/mail.vim
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ if match(bufname("%"), "^thread:") != -1
command -buffer FollowPatch :call v:lua.require('notmuch.attach').follow_github_patch(getline('.'))

nnoremap <buffer> U <Cmd>call v:lua.require('notmuch.attach').get_urls_from_cursor_msg()<CR>
nnoremap <buffer> <silent> <Tab> zj
nnoremap <buffer> <silent> <S-Tab> zk
nnoremap <buffer> <silent> <Tab> <Cmd>call v:lua.require('notmuch.thread').next_message()<CR>
nnoremap <buffer> <silent> <S-Tab> <Cmd>call v:lua.require('notmuch.thread').prev_message()<CR>
nnoremap <buffer> <silent> <Enter> za
nnoremap <buffer> a <Cmd>call v:lua.require('notmuch.attach').get_attachments_from_cursor_msg()<CR>
nnoremap <buffer> r <Cmd>call v:lua.require('notmuch.refresh').refresh_thread_buffer()<CR>
Expand Down
24 changes: 24 additions & 0 deletions ftplugin/notmuch-attachments.vim
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
" ftplugin for notmuch-attachments buffers
" This buffer works like oil.nvim: each line after the header is a file path.
" - Use dd to delete an attachment
" - Paste file paths with p (e.g. from oil.nvim)
" - Use a/b/t for add/browse/telescope

if exists("b:did_ftplugin")
finish
endif
let b:did_ftplugin = 1

" Buffer settings - NOTE: buffer IS modifiable (oil.nvim style)
setlocal buftype=acwrite
setlocal bufhidden=hide
setlocal noswapfile
setlocal nowrap
setlocal cursorline

" Disable some common plugins that might interfere
let b:loaded_coc = 1
let b:coc_enabled = 0
let b:copilot_enabled = 0

" Keybindings are set in the Lua module (attach_buffer.lua)
Loading