-- 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 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, }) -- 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("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("mt", function() nav("goto_field", "^[Tt]o:", "A") end, "Mail: To:") km("mc", function() nav("goto_field", "^[Cc]c:", "A") end, "Mail: Cc:") km("mf", function() nav("goto_field", "^[Ff]rom:", "A") end, "Mail: From:") km("ms", function() nav("goto_field", "^[Ss]ubject:", "A") end, "Mail: Subject:") km("mb", function() nav("goto_body") end, "Mail: Body") km("mS", function() nav("goto_signature") end, "Mail: Signature") km("mr", function() nav("goto_reply") end, "Mail: Quoted reply") km("me", function() nav("goto_end_of_reply") end, "Mail: End of reply") km("mk", function() nav("kill_quoted_sig") end, "Mail: Kill quoted sig") -- Attachment guardrail: on-demand explicit check (BufWritePre also fires automatically) km("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("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("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