156 lines
5.6 KiB
Lua
156 lines
5.6 KiB
Lua
-- Mail compose mode: NeoMutt + nvim-mail integration.
|
|
-- Activates ONLY when the NEOMUTT environment variable is set.
|
|
-- In neomuttrc: set editor = "env NEOMUTT=1 nvim"
|
|
--
|
|
-- Notmuch: optional indexed search only via <leader>mn. Never initialised or synced.
|
|
-- mbsync: never invoked.
|
|
|
|
local M = {}
|
|
|
|
-- Exact-match guard: only "1" activates mail mode.
|
|
-- Any other value (empty, "yes", "true") leaves this module dormant.
|
|
local function in_neomutt()
|
|
return vim.env.NEOMUTT == "1"
|
|
end
|
|
|
|
if not in_neomutt() then
|
|
return M
|
|
end
|
|
|
|
-- === NeoMutt session active ===
|
|
|
|
local function is_tty()
|
|
return vim.env.TERM == "linux"
|
|
end
|
|
|
|
-- Configure nvim-mail (idempotent: the plugin guards with _configured flag).
|
|
local ok_mail, mail_plugin = pcall(require, "nvim-mail")
|
|
if ok_mail then
|
|
mail_plugin.setup({
|
|
prefix = ",m",
|
|
spell_langs = { "en" },
|
|
from_list = {},
|
|
send_accounts = {},
|
|
contacts = { cmd = "khard", args = { "email", "-p", "--remove-first-line" } },
|
|
snippets = nil,
|
|
})
|
|
end
|
|
|
|
-- FileType autocmd: 72-column compose formatting + quote-depth highlighting.
|
|
-- nvim-mail's attach_buffer() handles spell, wrap, marker extmarks, and
|
|
-- BufWritePre guardrails (empty To/Subject, attachment mention check).
|
|
local au = vim.api.nvim_create_augroup("NeoMuttMail", { clear = true })
|
|
vim.api.nvim_create_autocmd("FileType", {
|
|
group = au,
|
|
pattern = "mail",
|
|
callback = function()
|
|
vim.opt_local.textwidth = 72
|
|
vim.opt_local.formatoptions = "tcqwan"
|
|
vim.opt_local.wrap = true
|
|
vim.opt_local.linebreak = true
|
|
vim.opt_local.spell = true
|
|
vim.opt_local.spelllang = "en"
|
|
-- colorcolumn is meaningful only outside TTY (linux console ignores it)
|
|
if not is_tty() then
|
|
vim.opt_local.colorcolumn = "73"
|
|
end
|
|
-- Quote-depth colouring: works with both cterm (TTY) and gui colours
|
|
vim.cmd([[
|
|
hi def MailQuoted1 ctermfg=6 guifg=#61afef
|
|
hi def MailQuoted2 ctermfg=2 guifg=#98c379
|
|
hi def MailQuoted3 ctermfg=3 guifg=#e5c07b
|
|
syn match MailQuoted3 /^>>>\+.*$/
|
|
syn match MailQuoted2 /^>>[^>].*$\|^>>$/
|
|
syn match MailQuoted1 /^>[^>].*$\|^>$/
|
|
]])
|
|
end,
|
|
})
|
|
|
|
-- <leader>m group: mail compose actions (supplement to nvim-mail's ,m prefix).
|
|
-- Descriptions are plain ASCII so they render correctly in TTY via which-key.
|
|
local km = function(lhs, rhs, desc)
|
|
vim.keymap.set("n", lhs, rhs, { desc = desc })
|
|
end
|
|
|
|
km("<leader>m", "", "Mail")
|
|
|
|
-- NeoMutt's Neovim Lua runtime may be LuaJIT/Lua 5.1, which exposes only global unpack.
|
|
local unpack_args = table.unpack or unpack
|
|
|
|
-- Safe wrapper: pcall so the keymap degrades gracefully if plugin not yet downloaded.
|
|
local function nav(fn, ...)
|
|
local args = { ... }
|
|
local ok, nav_mod = pcall(require, "nvim-mail.navigate")
|
|
if ok then nav_mod[fn](unpack_args(args)) end
|
|
end
|
|
|
|
km("<leader>mt", function() nav("goto_field", "^[Tt]o:", "A") end, "Mail: To:")
|
|
km("<leader>mc", function() nav("goto_field", "^[Cc]c:", "A") end, "Mail: Cc:")
|
|
km("<leader>mf", function() nav("goto_field", "^[Ff]rom:", "A") end, "Mail: From:")
|
|
km("<leader>ms", function() nav("goto_field", "^[Ss]ubject:", "A") end, "Mail: Subject:")
|
|
km("<leader>mb", function() nav("goto_body") end, "Mail: Body")
|
|
km("<leader>mS", function() nav("goto_signature") end, "Mail: Signature")
|
|
km("<leader>mr", function() nav("goto_reply") end, "Mail: Quoted reply")
|
|
km("<leader>me", function() nav("goto_end_of_reply") end, "Mail: End of reply")
|
|
km("<leader>mk", function() nav("kill_quoted_sig") end, "Mail: Kill quoted sig")
|
|
|
|
-- Attachment guardrail: on-demand explicit check (BufWritePre also fires automatically)
|
|
km("<leader>ma", function()
|
|
local lines = vim.api.nvim_buf_get_lines(0, 0, -1, false)
|
|
local ok_att, att = pcall(require, "nvim-mail.attachment")
|
|
if not ok_att then
|
|
vim.notify("[mail] nvim-mail.attachment unavailable", vim.log.levels.ERROR)
|
|
return
|
|
end
|
|
local missing, match = att.check(lines)
|
|
if missing then
|
|
vim.notify(
|
|
string.format('[mail] Attachment mentioned ("%s") but no file attached!', match or "attach"),
|
|
vim.log.levels.WARN
|
|
)
|
|
else
|
|
vim.notify("[mail] Attachment check: OK", vim.log.levels.INFO)
|
|
end
|
|
end, "Mail: Attachment check")
|
|
|
|
-- Header reminder: warn if To: or Subject: are empty
|
|
km("<leader>mR", function()
|
|
local lines = vim.api.nvim_buf_get_lines(0, 0, 20, false)
|
|
local to, subj = false, false
|
|
for _, l in ipairs(lines) do
|
|
if l == "" then break end
|
|
if l:match("^[Tt]o:%s*.+") then to = true end
|
|
if l:match("^[Ss]ubject:%s*.+") then subj = true end
|
|
end
|
|
local issues = {}
|
|
if not to then issues[#issues + 1] = "To: is empty" end
|
|
if not subj then issues[#issues + 1] = "Subject: is empty" end
|
|
if #issues == 0 then
|
|
vim.notify("[mail] Headers OK", vim.log.levels.INFO)
|
|
else
|
|
vim.notify("[mail] " .. table.concat(issues, " | "), vim.log.levels.WARN)
|
|
end
|
|
end, "Mail: Header reminder")
|
|
|
|
-- Notmuch: optional indexed search only. Never initialised, never synced.
|
|
-- Opens results in a scratch buffer; does not touch the notmuch database.
|
|
km("<leader>mn", function()
|
|
if vim.fn.executable("notmuch") == 0 then
|
|
vim.notify("[mail] notmuch not available (optional dependency)", vim.log.levels.WARN)
|
|
return
|
|
end
|
|
vim.ui.input({ prompt = "Notmuch search: " }, function(query)
|
|
if not query or query == "" then return end
|
|
local result = vim.fn.systemlist({ "notmuch", "search", "--output=summary", query })
|
|
vim.cmd("new")
|
|
vim.api.nvim_buf_set_lines(0, 0, -1, false, result)
|
|
vim.bo.buftype = "nofile"
|
|
vim.bo.bufhidden = "wipe"
|
|
vim.bo.modifiable = false
|
|
vim.bo.swapfile = false
|
|
pcall(vim.api.nvim_buf_set_name, 0, "Notmuch: " .. query)
|
|
end)
|
|
end, "Mail: Notmuch search")
|
|
|
|
return M
|