diff --git a/autoload/notmuch.vim b/autoload/notmuch.vim index 67522e9..dc5ad4b 100644 --- a/autoload/notmuch.vim +++ b/autoload/notmuch.vim @@ -1,12 +1,38 @@ let s:search_terms_list = [ "attachment:", "folder:", "id:", "mimetype:", \ "property:", "subject:", "thread:", "date:", "from:", "lastmod:", - \ "path:", "query:", "tag:", "to:" ] + \ "path:", "query:", "tag:", "is:", "to:", "body:", "and ", "or ", "not " ] function! notmuch#CompSearchTerms(ArgLead, CmdLine, CursorPos) abort if match(a:ArgLead, "tag:") != -1 let l:tag_list = split(system('notmuch search --output=tags "*"'), '\n') return "tag:" .. join(l:tag_list, "\ntag:") endif + if match(a:ArgLead, "is:") != -1 + let l:is_list = split(system('notmuch search --output=tags "*"'), '\n') + return "is:" .. join(l:is_list, "\nis:") + endif + if match(a:ArgLead, "mimetype:") != -1 + let l:mimetype_list = ["application/", "audio/", "chemical/", + \ "font/", "image/", "inode/", "message/", "model/", + \ "multipart/", "text/", "video/"] + return "mimetype:" .. join(l:mimetype_list, "\nmimetype:") + endif + if match(a:ArgLead, "from:") != -1 + let l:from_list = split(system('notmuch address "*"'), '\n') + return "from:" .. join(l:from_list, "\nfrom:") + endif + if match(a:ArgLead, "to:") != -1 + let l:to_list = split(system('notmuch address "*"'), '\n') + return "to:" .. join(l:to_list, "\nto:") + endif + if match(a:ArgLead, "folder:") != -1 + let l:folder_list = split(system('find ' .. g:notmuch_mailroot .. ' -type d -name cur -print0| sed -n -z "s|^' .. g:notmuch_mailroot .. '/*||p" | xargs -0 dirname | sort | uniq'), '\n') + return "folder:" .. join(l:folder_list, "\nfolder:") + endif + if match(a:ArgLead, "path:") != -1 + let l:path_list = split(system('find ' .. g:notmuch_mailroot .. ' -type d -print0| sed -n -z "s|^' .. g:notmuch_mailroot .. '/*||p" | sort -z | uniq -z | tr "\0" "\n"'), '\n') + return "path:" .. join(l:path_list, "\npath:") + endif return join(s:search_terms_list, "\n") endfunction @@ -14,4 +40,7 @@ function! notmuch#CompTags(ArgLead, CmdLine, CursorPos) abort return system('notmuch search --output=tags "*"') endfunction +function! notmuch#CompAddress(ArgLead, CmdLine, CursorPos) abort + return system('notmuch address "*"') +endfunction " vim: tabstop=2:shiftwidth=2:expandtab diff --git a/ftplugin/notmuch-threads.vim b/ftplugin/notmuch-threads.vim index ffcab7e..2208ca6 100644 --- a/ftplugin/notmuch-threads.vim +++ b/ftplugin/notmuch-threads.vim @@ -5,18 +5,30 @@ let r = v:lua.require('notmuch.refresh') let s = v:lua.require('notmuch.sync') let tag = v:lua.require('notmuch.tag') -command -buffer -complete=custom,notmuch#CompTags -nargs=+ TagAdd :call tag.thread_add_tag("") -command -buffer -complete=custom,notmuch#CompTags -nargs=+ TagRm :call tag.thread_rm_tag("") -command -buffer -complete=custom,notmuch#CompTags -nargs=+ TagToggle :call tag.thread_toggle_tag("") +command -buffer -range -complete=custom,notmuch#CompTags -nargs=+ TagAdd :call tag.thread_add_tag(, , ) +command -buffer -range -complete=custom,notmuch#CompTags -nargs=+ TagRm :call tag.thread_rm_tag(, , ) +command -buffer -range -complete=custom,notmuch#CompTags -nargs=+ TagToggle :call tag.thread_toggle_tag(, , ) +command -buffer -range DelThread :call tag.thread_add_tag("del", , ) | :call tag.thread_rm_tag("inbox", , ) nmap :call nm.show_thread() nmap r :call r.refresh_search_buffer() nmap q :bwipeout nmap % :call s.sync_maildir() nmap + :TagAdd +xmap + :TagAdd nmap - :TagRm +xmap - :TagRm nmap = :TagToggle +xmap = :TagToggle nmap a :TagToggle inboxj +xmap a :TagToggle inbox nmap A :TagRm inbox unreadj -nmap x :TagToggle unreadj +xmap A :TagRm inbox unread +nmap x :TagToggle unread +xmap x :TagToggle unread +nmap f :TagToggle flaggedj +xmap f :TagToggle flagged nmap C :call v:lua.require('notmuch.send').compose() +nmap dd :DelThreadj +xmap d :DelThread +nmap D :lua require('notmuch.delete').purge_del() diff --git a/lua/notmuch/config.lua b/lua/notmuch/config.lua index 167d571..fc4e71d 100644 --- a/lua/notmuch/config.lua +++ b/lua/notmuch/config.lua @@ -6,10 +6,15 @@ local C = {} -- including keymaps. The defaults can be overridden with options `opts` passed -- by the user in the `setup()` function. C.defaults = function() + local name = vim.fn.system('notmuch config get user.name'):gsub('\n', '') or '' + local email = vim.fn.system('notmuch config get user.primary_email'):gsub('\n', '') or '' local defaults = { - notmuch_db_path = os.getenv('HOME') .. '/Mail', + notmuch_db_path = vim.fn.system('notmuch config get database.path'):gsub('\n', ''), + from = name .. ' <' .. email .. '>', maildir_sync_cmd = 'mbsync -a', open_cmd = 'xdg-open', + logfile = nil, + sent_folder = nil, keymaps = { -- This should capture all notmuch.nvim related keymappings sendmail = '', }, diff --git a/lua/notmuch/delete.lua b/lua/notmuch/delete.lua new file mode 100644 index 0000000..c7a6346 --- /dev/null +++ b/lua/notmuch/delete.lua @@ -0,0 +1,31 @@ +local d = {} +local v = vim.api +local nm = require("notmuch") +local r = require("notmuch.refresh") + +local confirm_purge = function() + -- remove keymap + vim.keymap.del("n", "DD", { buffer = true }) + -- Confirm + local choice = v.nvim_call_function("confirm", { + "Purge deleted emails?", + "&Yes\n&No", + 2, -- Default to no + }) + + if choice == 1 then + v.nvim_command("silent ! notmuch search --output=files --format=text0 tag:del and tag:/./ | xargs -0 rm") + v.nvim_command("silent ! notmuch new") + r.refresh_search_buffer() + end +end + +d.purge_del = function() + nm.search_terms("tag:del and tag:/./") + -- Set keymap for purgin + vim.keymap.set("n", "DD", function() + confirm_purge() + end, { buffer = true }) +end + +return d diff --git a/lua/notmuch/init.lua b/lua/notmuch/init.lua index 2e8697d..79fa4d6 100644 --- a/lua/notmuch/init.lua +++ b/lua/notmuch/init.lua @@ -57,10 +57,11 @@ end -- -- @param search string: search terms matching format from -- `notmuch-search-terms(7)` +-- @param jumptothreadid string: jump to thread id after search -- -- @usage -- lua require('notmuch').search_terms('tag:inbox') -nm.search_terms = function(search) +nm.search_terms = function(search, jumptothreadid) local num_threads_found = 0 if search == '' then return nil @@ -77,7 +78,7 @@ nm.search_terms = function(search) v.nvim_buf_set_name(buf, search) v.nvim_win_set_buf(0, buf) - local hint_text = "Hints: : Open thread | q: Close | r: Refresh | %: Sync maildir | a: Archive | A: Archive and Read | +: Add tag | -: Remove tag | =: Toggle tag" + local hint_text = "Hints: : Open thread | q: Close | r: Refresh | %: Sync maildir | a: Archive | A: Archive and Read | +/-/=: Add, remove, toggle tag | dd: Delete" v.nvim_buf_set_lines(buf, 0, 2, false, { hint_text , "" }) -- Async notmuch search to make the UX non blocking @@ -85,6 +86,7 @@ nm.search_terms = function(search) -- Completion logic if vim.fn.getline(2) ~= '' then num_threads_found = vim.fn.line('$') - 1 end print('Found ' .. num_threads_found .. ' threads') + vim.fn.search(jumptothreadid) end) -- Set cursor at head of buffer, declare filetype, and disable modifying diff --git a/lua/notmuch/refresh.lua b/lua/notmuch/refresh.lua index 8980a23..8a2d353 100644 --- a/lua/notmuch/refresh.lua +++ b/lua/notmuch/refresh.lua @@ -12,9 +12,12 @@ local nm = require('notmuch') -- -- Normally invoked by pressing `r` in the search results buffer -- lua require('notmuch.refresh').refresh_search_buffer() r.refresh_search_buffer = function() + local line = v.nvim_get_current_line() + local threadid = string.match(line, "%S+", 8) local search = string.match(v.nvim_buf_get_name(0), '%a+:%C+') v.nvim_command('bwipeout') - nm.search_terms(search) + nm.search_terms(search, threadid) + vim.fn.search(threadid) end -- Refreshes the thread view buffer diff --git a/lua/notmuch/send.lua b/lua/notmuch/send.lua index 42703de..dfa300f 100644 --- a/lua/notmuch/send.lua +++ b/lua/notmuch/send.lua @@ -4,12 +4,37 @@ local v = vim.api local config = require('notmuch.config') +-- Save a completed message in the sent folder +-- +-- This inserts the email stored under `filename` in the notmuch library under +-- `folder`, and removes the tags `inbox` and `unread`, which are automatically +-- added by `notmuch insert`. +-- +-- @param filename string: path to the email message you would like to send +-- +-- @param folder string: folder name of location where to store the email +s.savemail = function(filename, folder) + os.execute("notmuch insert --folder=" .. folder .. " -inbox -unread < " .. filename) +end + -- Prompt confirmation for sending an email -- -- This function utilizes vim's builtin `confirm()` to prompt the user and -- confirm the action of sending an email. This is applicable for sending newly -- composed mails or replies by passing the mail file path. -- +-- If the user specified a sent folder in the configuration +-- `config.options.sent_folder`, then this function will prepend two header +-- fields to the email before it is sent: `Date` and `Message-ID`. The former +-- is set to now, the latter to a random uuid @localhost. Then it sends the +-- email. On successful transmission, this function calls `savemail` to insert +-- the email in the notmuch library. If we would not add these header fields, +-- then the datetime will be unspecified, and the email will not be correctly +-- assigned to the thread it belongs to. +-- +-- This function also parses the log message returned by `s.sendmail` to +-- determine if the message transmission was successful or not. +-- -- @param filename string: path to the email message you would like to send -- -- @usage @@ -25,8 +50,24 @@ local confirm_sendmail = function(filename) }) if choice == 1 then + if config.options.sent_folder then + local date_line = "Date: " .. vim.fn.system("date -R"):gsub("\n", "") + v.nvim_buf_set_lines(0, 0, 0, false, { date_line }) + local messageid_line = "Message-ID: " .. vim.fn.system('echo "<$(uuidgen)@localhost>"'):gsub("\n", "") + v.nvim_buf_set_lines(0, 0, 0, false, { messageid_line }) + end vim.cmd.write() - s.sendmail(filename) + local log_message = s.sendmail(filename) + local smtpstatus = log_message:match("smtpstatus=([^ ]*)") + local exitcode = log_message:match("exitcode=([^ ]*)") + if exitcode == "EX_OK" and smtpstatus == "250" then + vim.notify("📨 email sent successfully") + if config.options.sent_folder then + s.savemail(filename, config.options.sent_folder) + end + else + vim.notify("❌ failed to send email: " .. smtpstatus, vim.log.levels.ERROR) + end end end @@ -34,15 +75,23 @@ end -- -- This function takes a file containing a completed message and send it to the -- recipient(s) using `msmtp`. Typically you will invoke this function after --- confirming from a reply or newly composed email message +-- confirming from a reply or newly composed email message. The invocation of +-- `msmtp` determines by itself the recipient and the sender. +-- +-- If the configuration `config.options.logfile` is set, then it invokes +-- `msmtp` with logging capability to that file. Otherwise, it logs to +-- temporary file. -- -- @param filename string: path to the email message you would like to send -- +-- @return string: The log message provided by `msmtp` +-- -- @usage -- require('notmuch.send').sendmail('/tmp/my_new_email.eml') s.sendmail = function(filename) - os.execute('msmtp -t <' .. filename) - print('Successfully sent email: ' .. filename) + local logfile = config.options.logfile or os.tmpname() + os.execute("msmtp -t --read-envelope-from --logfile=" .. logfile .. " <" .. filename) + return vim.fn.system("tail -1 " .. logfile):gsub("\n", "") end -- Reply to an email message @@ -88,15 +137,19 @@ end -- message headers and body. The mail content is stored in `/tmp/` so the user -- can come back to it later if needed. -- +-- @param to string: recipient address (optionaal argument) +-- -- @usage -- -- Typically you can run this with `:ComposeMail` or pressing `C` -- require('notmuch.send').compose() -s.compose = function() +s.compose = function(to) + to = to or '' local compose_filename = '/tmp/compose.eml' -- TODO: Add ability to modify default body message and signature local headers = { - 'To: ', + 'From: ' .. config.options.from, + 'To: ' .. to, 'Cc: ', 'Subject: ', '', diff --git a/lua/notmuch/tag.lua b/lua/notmuch/tag.lua index 7dc0c08..f0d2613 100644 --- a/lua/notmuch/tag.lua +++ b/lua/notmuch/tag.lua @@ -49,52 +49,64 @@ t.msg_toggle_tag = function(tags) db.close() end -t.thread_add_tag = function(tags) +t.thread_add_tag = function(tags, startlinenr, endlinenr) + startlinenr = startlinenr or v.nvim_win_get_cursor(0)[1] + endlinenr = endlinenr or startlinenr local t = u.split(tags, '%S+') - local line = v.nvim_get_current_line() - local threadid = string.match(line, '%S+', 8) - local db = require'notmuch.cnotmuch'(config.options.notmuch_db_path, 1) - local query = db.create_query('thread:' .. threadid) - local thread = query.get_threads()[1] - for i,tag in pairs(t) do - thread:add_tag(tag) + for linenr = startlinenr, endlinenr do + local line = vim.fn.getline(linenr) + local threadid = string.match(line, "%S+", 8) + local db = require("notmuch.cnotmuch")(config.options.notmuch_db_path, 1) + local query = db.create_query("thread:" .. threadid) + local thread = query.get_threads()[1] + for i,tag in pairs(t) do + thread:add_tag(tag) + end + db.close() end - db.close() print('+(' .. tags .. ')') end -t.thread_rm_tag = function(tags) +t.thread_rm_tag = function(tags, startlinenr, endlinenr) + startlinenr = startlinenr or v.nvim_win_get_cursor(0)[1] + endlinenr = endlinenr or startlinenr local t = u.split(tags, '%S+') - local line = v.nvim_get_current_line() - local threadid = string.match(line, '%S+', 8) - local db = require'notmuch.cnotmuch'(config.options.notmuch_db_path, 1) - local query = db.create_query('thread:' .. threadid) - local thread = query.get_threads()[1] - for i,tag in pairs(t) do - thread:rm_tag(tag) + for linenr = startlinenr, endlinenr do + local line = vim.fn.getline(linenr) + local threadid = string.match(line, "%S+", 8) + local db = require("notmuch.cnotmuch")(config.options.notmuch_db_path, 1) + local query = db.create_query("thread:" .. threadid) + local thread = query.get_threads()[1] + for i,tag in pairs(t) do + thread:rm_tag(tag) + end + db.close() end - db.close() print('-(' .. tags .. ')') end -t.thread_toggle_tag = function(tags) +t.thread_toggle_tag = function(tags, startlinenr, endlinenr) + startlinenr = startlinenr or v.nvim_win_get_cursor(0)[1] + endlinenr = endlinenr or startlinenr local t = u.split(tags, '%S+') - local line = v.nvim_get_current_line() - local threadid = string.match(line, '%S+', 8) - local db = require'notmuch.cnotmuch'(config.options.notmuch_db_path, 1) - local query = db.create_query('thread:' .. threadid) - local thread = query.get_threads()[1] - local curr_tags = thread:get_tags() - for i,tag in pairs(t) do - if curr_tags[tag] == true then - thread:rm_tag(tag) - print('-' .. tag) - else - thread:add_tag(tag) - print('+' .. tag) + for linenr = startlinenr, endlinenr do + local line = vim.fn.getline(linenr) + local threadid = string.match(line, "%S+", 8) + local db = require("notmuch.cnotmuch")(config.options.notmuch_db_path, 1) + local query = db.create_query("thread:" .. threadid) + local thread = query.get_threads()[1] + local curr_tags = thread:get_tags() + for i,tag in pairs(t) do + if curr_tags[tag] == true then + thread:rm_tag(tag) + print("-" .. tag) + else + thread:add_tag(tag) + print("+" .. tag) + end end + db.close() end - db.close() end return t diff --git a/plugin/notmuch.vim b/plugin/notmuch.vim index aeaf817..b64ae2f 100644 --- a/plugin/notmuch.vim +++ b/plugin/notmuch.vim @@ -1,4 +1,5 @@ -command -complete=custom,notmuch#CompSearchTerms -nargs=* NmSearch :call v:lua.require('notmuch').search_terms("") -command ComposeMail :call v:lua.require('notmuch.send').compose() +let g:notmuch_mailroot = trim(system('notmuch config get database.mail_root')) +command -complete=custom,notmuch#CompSearchTerms -nargs=* NmSearch :call v:lua.require('notmuch').search_terms() +command -complete=custom,notmuch#CompAddress -nargs=* ComposeMail :call v:lua.require('notmuch.send').compose() " vim: tabstop=2:shiftwidth=2:expandtab diff --git a/syntax/notmuch-threads.vim b/syntax/notmuch-threads.vim index 1b40ffd..9c582a4 100644 --- a/syntax/notmuch-threads.vim +++ b/syntax/notmuch-threads.vim @@ -10,7 +10,7 @@ syntax region nmHints start=/^Hints:/ end=/$/ oneline contains=nmHintsIdentifi syntax match nmHintsIdentifier "^Hints:" contained nextgroup=nmHintsKey syntax match nmHintsKey "\s\+[^:\s]\+" contained nextgroup=nmHintsKVDelimiter syntax match nmHintsKVDelimiter ":" contained nextgroup=nmHintsValue -syntax match nmHintsValue "\s\+[A-Za-z0-9\ ]\+" contained nextgroup=nmHintsDelimiter +syntax match nmHintsValue "\s\+[A-Za-z0-9\ ,.]\+" contained nextgroup=nmHintsDelimiter syntax match nmHintsDelimiter "|" contained nextgroup=nmHintsKey highlight link nmHintsIdentifier Comment