├── LICENSE ├── README.asciidoc ├── doc └── notmuch.txt ├── plugin └── notmuch.vim └── syntax ├── notmuch-compose.vim ├── notmuch-folders.vim ├── notmuch-git-diff.vim ├── notmuch-search.vim └── notmuch-show.vim /LICENSE: -------------------------------------------------------------------------------- 1 | notmuch-vim is a vim plugin to handle email using notmuch 2 | 3 | This program is free software: you can redistribute it and/or modify 4 | it under the terms of the GNU General Public License as published by 5 | the Free Software Foundation, either version 3 of the License, or 6 | (at your option) any later version. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU General Public License for more details. 12 | 13 | You should have received a copy of the GNU General Public License 14 | along with this program. If not, see . 15 | -------------------------------------------------------------------------------- /README.asciidoc: -------------------------------------------------------------------------------- 1 | == notmuch vim == 2 | 3 | This is a vim plug-in that provides a fully usable mail client interface, 4 | utilizing the notmuch framework. 5 | 6 | image::https://asciinema.org/a/oo4yUOQDDF2CrWZbzhZURFtTW.svg[link="https://asciinema.org/a/oo4yUOQDDF2CrWZbzhZURFtTW"] 7 | 8 | == Install == 9 | 10 | You can use any vim plugin manager: 11 | 12 | % git clone https://github.com/felipec/notmuch-vim.git ~/.vim/pack/plugins/start/notmuch-vim 13 | 14 | === vim +ruby === 15 | 16 | Make sure your vim version has ruby support: check for +ruby in 'vim --version' 17 | features. 18 | 19 | % vim --version | grep +ruby 20 | 21 | === ruby bindings === 22 | 23 | Check if you are able to run the following command cleanly: 24 | 25 | % ruby -e "require 'notmuch'" 26 | 27 | If you don't see any errors it means it's working and you can go to the next 28 | section, if not, you would need to compile the bindings yourself, or 29 | contact your distribution so they package notmuch correctly. 30 | 31 | === mail gem === 32 | 33 | Since libnotmuch library concentrates on things other than handling mail, we 34 | need a library to do that, and for Ruby the best library for that is called 35 | 'mail': 36 | 37 | % gem install mail 38 | 39 | This gem is not mandatory, but it's extremely recommended. 40 | 41 | == Running == 42 | 43 | Simple: 44 | 45 | % gvim -c ':NotMuch' 46 | 47 | You might want to write a wrapper script (e.g. `vnm`) 48 | 49 | #!/bin/sh 50 | gvim -c ":NotMuch $*" 51 | 52 | So you can run: 53 | 54 | vnm is:inbox date:yesterday.. 55 | 56 | Enjoy ;) 57 | 58 | == More stuff == 59 | 60 | As an example to configure a key mapping to add the tag 'to-do' and archive, 61 | this is what I use: 62 | 63 | ---- 64 | let g:notmuch_custom_search_maps = { 65 | \ 't': 'search_tag("+to-do -inbox")', 66 | \ } 67 | 68 | let g:notmuch_custom_show_maps = { 69 | \ 't': 'show_tag("+to-do -inbox")', 70 | \ } 71 | ---- 72 | -------------------------------------------------------------------------------- /doc/notmuch.txt: -------------------------------------------------------------------------------- 1 | *notmuch.txt* Plug-in to make vim a nice email client using notmuch 2 | 3 | Author: Felipe Contreras 4 | 5 | Overview |notmuch-intro| 6 | Usage |notmuch-usage| 7 | Mappings |notmuch-mappings| 8 | Configuration |notmuch-config| 9 | 10 | ============================================================================== 11 | OVERVIEW *notmuch-intro* 12 | 13 | This is a vim plug-in that provides a fully usable mail client interface, 14 | utilizing the notmuch framework. 15 | 16 | It has three main views: folders, search, and thread. In the folder view you 17 | can find a summary of saved searches, In the search view you can see all the 18 | threads that comprise the selected search, and in the thread view you can read 19 | every mail in the thread. 20 | 21 | ============================================================================== 22 | USAGE *notmuch-usage* 23 | 24 | To use it, simply run the `:NotMuch` command. 25 | 26 | By default you start in the folder view which shows you default searches and 27 | the number of threads that match those: 28 | > 29 | 10 new (tag:inbox and tag:unread) 30 | 20 inbox (tag:inbox) 31 | 30 unread (tag:unread) 32 | < 33 | You can see the threads of each by clicking `enter`, which sends you to the 34 | search view. In both the search and folder views you can type `s` to type a 35 | new search, or `=` to refresh. To see a thread, type `enter` again. 36 | 37 | To exit a view, click `q`. 38 | 39 | Also, you can specify a search directly: 40 | > 41 | :NotMuch is:inbox and date:yesterday.. 42 | < 43 | ============================================================================== 44 | MAPPINGS *notmuch-mappings* 45 | 46 | ------------------------------------------------------------------------------ 47 | Folder view~ 48 | 49 | Show selected search 50 | s Enter a new search 51 | = Refresh 52 | c Compose a new mail 53 | 54 | ------------------------------------------------------------------------------ 55 | Search view~ 56 | 57 | q Quit view 58 | Show selected search 59 | Show selected search with filter 60 | A Archive (-inbox -unread) 61 | I Mark as read (-unread) 62 | t Tag (prompted) 63 | s Search 64 | = Refresh 65 | ? Show search information 66 | c Compose a new mail 67 | > 68 | ------------------------------------------------------------------------------ 69 | Thread view~ 70 | 71 | q Quit view 72 | A Archive (-inbox -unread) 73 | I Mark as read (-unread) 74 | t Tag (prompted) 75 | s Search 76 | p Save patches 77 | r Reply 78 | ? Show thread information 79 | Show next message 80 | c Compose a new mail 81 | 82 | ------------------------------------------------------------------------------ 83 | Compose view~ 84 | 85 | q Quit view 86 | s Send 87 | 88 | ============================================================================== 89 | CONFIGURATION *notmuch-config* 90 | 91 | You can add the following configurations to your `.vimrc`, or 92 | `~/.vim/after/plugin/notmuch.vim`. 93 | 94 | *g:notmuch_folders* 95 | 96 | The first thing you might want to do is set your custom searches. 97 | > 98 | let g:notmuch_folders = [ 99 | \ [ 'new', 'tag:inbox and tag:unread' ], 100 | \ [ 'inbox', 'tag:inbox' ], 101 | \ [ 'unread', 'tag:unread' ], 102 | \ [ 'to-do', 'tag:to-do' ], 103 | \ [ 'to-me', 'to:john.doe and tag:new' ], 104 | \ ] 105 | < 106 | 107 | *g:notmuch_custom_folders_maps* 108 | *g:notmuch_custom_search_maps* 109 | *g:notmuch_custom_show_maps* 110 | 111 | You can also configure the keyboard mappings for the different views: 112 | > 113 | let g:notmuch_custom_search_maps = { 114 | \ 't': 'search_tag("+to-do -inbox")', 115 | \ 'd': 'search_tag("+deleted -inbox -unread")', 116 | \ } 117 | 118 | let g:notmuch_custom_show_maps = { 119 | \ 't': 'show_tag("+to-do -inbox")', 120 | \ 'd': 'show_tag("+deleted -inbox -unread")', 121 | \ } 122 | < 123 | 124 | *g:notmuch_date_format* 125 | 126 | To configure the date format you want in the search view: 127 | > 128 | let g:notmuch_date_format = '%d.%m.%y' 129 | < 130 | 131 | *g:notmuch_datetime_format* 132 | 133 | You can do the same for the thread view: 134 | > 135 | let g:notmuch_datetime_format = '%d.%m.%y %H:%M:%S' 136 | < 137 | *g:notmuch_reply_quote_format* 138 | 139 | If you want to change the reply quote format to show the email address: 140 | > 141 | let g:notmuch_reply_quote_format = '%s <%s>' 142 | < 143 | 144 | *g:notmuch_reply_quote_datetime_format* 145 | 146 | There's also a way to specy the reply quote datetime format: 147 | > 148 | let g:notmuch_reply_quote_datetime_format = '%a %b %e' 149 | < 150 | 151 | *g:notmuch_folders_count_threads* 152 | 153 | If you want to count the threads instead of the messages in the folder view: 154 | > 155 | let g:notmuch_folders_count_threads = 1 156 | < 157 | 158 | *g:notmuch_reader* 159 | *g:notmuch_sendmail* 160 | 161 | You can also configure your external mail reader and sendmail program: 162 | > 163 | let g:notmuch_reader = 'mutt -f %s' 164 | let g:notmuch_sendmail = 'sendmail' 165 | < 166 | 167 | vim:tw=78:ts=8:noet:ft=help: 168 | -------------------------------------------------------------------------------- /plugin/notmuch.vim: -------------------------------------------------------------------------------- 1 | if exists('g:loaded_notmuch') || v:version < 700 || !has('ruby') 2 | finish 3 | endif 4 | let g:loaded_notmuch = 1 5 | 6 | let g:notmuch_folders_maps = { 7 | \ '': 'folders_show_search()', 8 | \ 's': 'folders_search_prompt()', 9 | \ '=': 'folders_refresh()', 10 | \ 'c': 'compose()', 11 | \ } 12 | 13 | let g:notmuch_search_maps = { 14 | \ 'q': 'kill_this_buffer()', 15 | \ '': 'search_show_thread(1)', 16 | \ '': 'search_show_thread(2)', 17 | \ 'A': 'search_tag("-inbox -unread")', 18 | \ 'I': 'search_tag("-unread")', 19 | \ 't': 'search_tag("")', 20 | \ 's': 'search_search_prompt()', 21 | \ '=': 'search_refresh()', 22 | \ '?': 'search_info()', 23 | \ 'c': 'compose()', 24 | \ } 25 | 26 | let g:notmuch_show_maps = { 27 | \ 'q': 'kill_this_buffer()', 28 | \ 'A': 'show_tag("-inbox -unread")', 29 | \ 'I': 'show_tag("-unread")', 30 | \ 't': 'show_tag("")', 31 | \ 'o': 'show_open_msg()', 32 | \ 'e': 'show_extract_msg()', 33 | \ 's': 'show_save_msg()', 34 | \ 'p': 'show_save_patches()', 35 | \ 'r': 'show_reply()', 36 | \ '?': 'show_info()', 37 | \ '.': 'show_copy_id()', 38 | \ '': 'show_next_msg()', 39 | \ '': 'show_message_tag("-inbox -unread")', 40 | \ 'c': 'compose()', 41 | \ } 42 | 43 | let g:notmuch_compose_maps = { 44 | \ ',s': 'compose_send()', 45 | \ ',q': 'compose_quit()', 46 | \ } 47 | 48 | let s:notmuch_folders_default = [ 49 | \ [ 'new', 'tag:inbox and tag:unread' ], 50 | \ [ 'inbox', 'tag:inbox' ], 51 | \ [ 'unread', 'tag:unread' ], 52 | \ ] 53 | 54 | let s:notmuch_date_format_default = '%d.%m.%y' 55 | let s:notmuch_datetime_format_default = '%d.%m.%y %H:%M:%S' 56 | let s:notmuch_reply_quote_format_default = '%s' 57 | let s:notmuch_reply_quote_datetime_format_default = '' 58 | let s:notmuch_reader_default = 'mutt -f %s' 59 | let s:notmuch_sendmail_default = 'sendmail' 60 | let s:notmuch_html_converter_default = 'elinks -dump -eval "set document.browse.margin_width=0"' 61 | let s:notmuch_folders_count_threads_default = 0 62 | 63 | function! s:new_file_buffer(type, fname) 64 | exec printf('edit %s', a:fname) 65 | execute printf('set filetype=notmuch-%s', a:type) 66 | execute printf('set syntax=notmuch-%s', a:type) 67 | ruby $curbuf.init(VIM::evaluate('a:type')) 68 | endfunction 69 | 70 | function! s:on_compose_delete() 71 | if b:compose_done 72 | return 73 | endif 74 | if input('[s]end/[q]uit? ') =~ '^s' 75 | call s:compose_send() 76 | endif 77 | endfunction 78 | 79 | "" actions 80 | 81 | function! s:compose_quit() 82 | let b:compose_done = 1 83 | call s:kill_this_buffer() 84 | endfunction 85 | 86 | function! s:compose_send() 87 | let b:compose_done = 1 88 | let fname = expand('%') 89 | let lines = getline(5, '$') 90 | 91 | ruby << EOF 92 | # Generate proper mail to send 93 | text = VIM::evaluate('lines').join("\n") 94 | fname = VIM::evaluate('fname') 95 | transport = Mail.new(text) 96 | transport.message_id = generate_message_id 97 | transport.charset = 'utf-8' 98 | File.write(fname, transport.to_s) 99 | EOF 100 | 101 | let cmdtxt = g:notmuch_sendmail . ' -t -f ' . s:reply_from . ' < ' . fname 102 | let out = system(cmdtxt) 103 | let err = v:shell_error 104 | if err 105 | echohl Error 106 | echo 'Eeek! unable to send mail' 107 | echo out 108 | echohl None 109 | return 110 | endif 111 | call delete(fname) 112 | echo 'Mail sent successfully.' 113 | call s:kill_this_buffer() 114 | endfunction 115 | 116 | function! s:show_next_msg() 117 | ruby << EOF 118 | r, c = $curwin.cursor 119 | n = $curbuf.line_number 120 | i = $messages.index { |m| n >= m.start && n <= m.end } 121 | m = $messages[i + 1] 122 | if m 123 | r = m.body_start + 1 124 | VIM::command("normal #{m.start}zt") 125 | $curwin.cursor = r, c 126 | end 127 | EOF 128 | endfunction 129 | 130 | function! s:show_reply() 131 | ruby open_reply get_message.mail 132 | let b:compose_done = 0 133 | call s:set_map(g:notmuch_compose_maps) 134 | autocmd BufDelete call s:on_compose_delete() 135 | startinsert! 136 | endfunction 137 | 138 | function! s:compose() 139 | ruby open_compose 140 | let b:compose_done = 0 141 | call s:set_map(g:notmuch_compose_maps) 142 | autocmd BufDelete call s:on_compose_delete() 143 | startinsert! 144 | endfunction 145 | 146 | function! s:show_info() 147 | ruby puts get_message.inspect 148 | endfunction 149 | 150 | function! s:show_copy_id() 151 | ruby << EOF 152 | VIM::command("let @+='%s'" % get_message.message_id) 153 | EOF 154 | endfunction 155 | 156 | function! s:show_extract_msg() 157 | ruby << EOF 158 | m = get_message 159 | m.mail.attachments.each do |a| 160 | File.open(a.filename, 'w') do |f| 161 | f.write a.body.decoded 162 | print "Extracted '#{a.filename}'" 163 | end 164 | end 165 | EOF 166 | endfunction 167 | 168 | function! s:show_open_msg() 169 | ruby << EOF 170 | m = get_message 171 | mbox = File.expand_path('~/.notmuch/vim_mbox') 172 | cmd = VIM::evaluate('g:notmuch_reader') % mbox 173 | system "notmuch show --format=mbox id:#{m.message_id} > #{mbox} && #{cmd}" 174 | EOF 175 | endfunction 176 | 177 | function! s:show_save_msg() 178 | let file = input('File name: ') 179 | ruby << EOF 180 | file = VIM::evaluate('file') 181 | m = get_message 182 | system "notmuch show --format=mbox id:#{m.message_id} > #{file}" 183 | EOF 184 | endfunction 185 | 186 | function! s:show_save_patches() 187 | ruby << EOF 188 | q = $curbuf.query($cur_thread) 189 | t = q.search_threads.first 190 | n = 0 191 | t.toplevel_messages.first.replies.each do |m| 192 | next if not m['subject'] =~ /^\[PATCH.*\]/ 193 | file = "%04d.patch" % [n += 1] 194 | system "notmuch show --format=mbox id:#{m.message_id} > #{file}" 195 | end 196 | puts "Saved #{n} patches" 197 | EOF 198 | endfunction 199 | 200 | function! s:show_tag(intags) 201 | if empty(a:intags) 202 | let tags = input('tags: ') 203 | else 204 | let tags = a:intags 205 | endif 206 | ruby do_tag(get_cur_view, VIM::evaluate('l:tags')) 207 | call s:show_next_thread() 208 | endfunction 209 | 210 | function! s:show_message_tag(intags) 211 | if empty(a:intags) 212 | let tags = input('tags: ') 213 | else 214 | let tags = a:intags 215 | endif 216 | ruby do_tag('id:' + get_message.message_id, VIM::evaluate('l:tags')) 217 | call s:show_next_msg() 218 | endfunction 219 | 220 | function! s:search_search_prompt() 221 | let text = input('Search: ') 222 | if text == "" 223 | return 224 | endif 225 | setlocal modifiable 226 | ruby << EOF 227 | $cur_search = VIM::evaluate('text') 228 | $curbuf.reopen 229 | search_render($cur_search) 230 | EOF 231 | setlocal nomodifiable 232 | endfunction 233 | 234 | function! s:search_info() 235 | ruby puts get_thread_id 236 | endfunction 237 | 238 | function! s:search_refresh() 239 | setlocal modifiable 240 | ruby $curbuf.reopen 241 | ruby search_render($cur_search) 242 | setlocal nomodifiable 243 | endfunction 244 | 245 | function! s:search_tag(intags) 246 | if empty(a:intags) 247 | let tags = input('tags: ') 248 | else 249 | let tags = a:intags 250 | endif 251 | ruby do_tag(get_thread_id, VIM::evaluate('l:tags')) 252 | norm j 253 | endfunction 254 | 255 | function! s:folders_search_prompt() 256 | let text = input('Search: ') 257 | call s:search(text) 258 | endfunction 259 | 260 | function! s:folders_refresh() 261 | setlocal modifiable 262 | ruby $curbuf.reopen 263 | ruby folders_render() 264 | setlocal nomodifiable 265 | endfunction 266 | 267 | function! s:system(cmd) 268 | execute '!' . a:cmd 269 | endfunction 270 | 271 | "" basic 272 | 273 | function! s:show_cursor_moved() 274 | ruby << EOF 275 | if $render.is_ready? 276 | VIM::command('setlocal modifiable') 277 | $render.do_next 278 | VIM::command('setlocal nomodifiable') 279 | end 280 | EOF 281 | endfunction 282 | 283 | function! s:show_next_thread() 284 | call s:kill_this_buffer() 285 | if line('.') != line('$') 286 | norm j 287 | call s:search_show_thread(0) 288 | else 289 | echo 'No more messages.' 290 | endif 291 | endfunction 292 | 293 | function! s:kill_this_buffer() 294 | ruby << EOF 295 | $curbuf.close 296 | VIM::command("bdelete!") 297 | EOF 298 | endfunction 299 | 300 | function! s:set_map(maps) 301 | nmapclear 302 | for [key, code] in items(a:maps) 303 | let cmd = printf(":call %s", code) 304 | exec printf('nnoremap %s %s', key, cmd) 305 | endfor 306 | endfunction 307 | 308 | function! s:new_buffer(type) 309 | enew 310 | setlocal buftype=nofile bufhidden=hide 311 | keepjumps 0d 312 | execute printf('set filetype=notmuch-%s', a:type) 313 | execute printf('set syntax=notmuch-%s', a:type) 314 | ruby $curbuf.init(VIM::evaluate('a:type')) 315 | endfunction 316 | 317 | function! s:set_menu_buffer() 318 | setlocal nomodifiable 319 | setlocal cursorline 320 | setlocal nowrap 321 | endfunction 322 | 323 | "" main 324 | 325 | function! s:show(thread_id) 326 | call s:new_buffer('show') 327 | setlocal modifiable 328 | ruby << EOF 329 | thread_id = VIM::evaluate('a:thread_id') 330 | $cur_thread = thread_id 331 | $messages.clear 332 | $curbuf.render do |b| 333 | q = $curbuf.query(get_cur_view) 334 | q.sort = Notmuch::SORT_OLDEST_FIRST 335 | msgs = q.search_messages 336 | msgs.each do |msg| 337 | m = Mail.read(msg.filename) 338 | part = m.find_first_text 339 | nm_m = Message.new(msg, m) 340 | $messages << nm_m 341 | date_fmt = VIM::evaluate('g:notmuch_datetime_format') 342 | date = Time.at(msg.date).strftime(date_fmt) 343 | nm_m.start = b.count 344 | b << "%s %s (%s)" % [msg['from'], date, msg.tags] 345 | b << "Subject: %s" % [msg['subject']] 346 | b << "To: %s" % msg['to'] 347 | b << "Cc: %s" % msg['cc'] 348 | b << "Date: %s" % msg['date'] 349 | nm_m.body_start = b.count 350 | if part 351 | b << "--- %s ---" % [part.mime_type || 'none'] 352 | part.convert.each_line do |l| 353 | b << l.chomp 354 | end 355 | else 356 | b << "--- %s ---" % 'missing' 357 | end 358 | b << "" 359 | nm_m.end = b.count 360 | end 361 | b.delete(b.count) 362 | end 363 | $messages.each_with_index do |msg, i| 364 | VIM::command("syntax region nmShowMsg#{i}Desc start='\\%%%il' end='\\%%%il' contains=@nmShowMsgDesc" % [msg.start, msg.start + 1]) 365 | VIM::command("syntax region nmShowMsg#{i}Head start='\\%%%il' end='\\%%%il' contains=@nmShowMsgHead" % [msg.start + 1, msg.body_start]) 366 | VIM::command("syntax region nmShowMsg#{i}Body start='\\%%%il' end='\\%%%dl' contains=@nmShowMsgBody" % [msg.body_start, msg.end]) 367 | end 368 | EOF 369 | setlocal nomodifiable 370 | call s:set_map(g:notmuch_show_maps) 371 | endfunction 372 | 373 | function! s:search_show_thread(mode) 374 | ruby << EOF 375 | mode = VIM::evaluate('a:mode') 376 | id = get_thread_id 377 | case mode 378 | when 0; 379 | when 1; $cur_filter = nil 380 | when 2; $cur_filter = $cur_search 381 | end 382 | VIM::command("call s:show('#{id}')") 383 | EOF 384 | endfunction 385 | 386 | function! s:search(search) 387 | call s:new_buffer('search') 388 | ruby << EOF 389 | $cur_search = VIM::evaluate('a:search') 390 | search_render($cur_search) 391 | EOF 392 | call s:set_menu_buffer() 393 | call s:set_map(g:notmuch_search_maps) 394 | autocmd CursorMoved call s:show_cursor_moved() 395 | endfunction 396 | 397 | function! s:folders_show_search() 398 | ruby << EOF 399 | n = $curbuf.line_number 400 | s = $searches[n - 1] 401 | VIM::command("call s:search('#{s}')") 402 | EOF 403 | endfunction 404 | 405 | function! s:folders() 406 | call s:new_buffer('folders') 407 | ruby folders_render() 408 | call s:set_menu_buffer() 409 | call s:set_map(g:notmuch_folders_maps) 410 | endfunction 411 | 412 | "" root 413 | 414 | function! s:set_defaults() 415 | if !exists('g:notmuch_folders') 416 | let g:notmuch_folders = s:notmuch_folders_default 417 | endif 418 | 419 | if !exists('g:notmuch_date_format') 420 | let g:notmuch_date_format = s:notmuch_date_format_default 421 | endif 422 | 423 | if !exists('g:notmuch_datetime_format') 424 | let g:notmuch_datetime_format = s:notmuch_datetime_format_default 425 | endif 426 | 427 | if !exists('g:notmuch_reply_quote_format') 428 | let g:notmuch_reply_quote_format = s:notmuch_reply_quote_format_default 429 | endif 430 | 431 | if !exists('g:notmuch_reply_quote_datetime_format') 432 | let g:notmuch_reply_quote_datetime_format = s:notmuch_reply_quote_datetime_format_default 433 | endif 434 | 435 | if !exists('g:notmuch_reader') 436 | let g:notmuch_reader = s:notmuch_reader_default 437 | endif 438 | 439 | if !exists('g:notmuch_sendmail') 440 | let g:notmuch_sendmail = s:notmuch_sendmail_default 441 | endif 442 | 443 | if !exists('g:notmuch_html_converter') 444 | let g:notmuch_html_converter = s:notmuch_html_converter_default 445 | endif 446 | 447 | if !exists('g:notmuch_folders_count_threads') 448 | let g:notmuch_folders_count_threads = s:notmuch_folders_count_threads_default 449 | endif 450 | 451 | if exists('g:notmuch_custom_folders_maps') 452 | call extend(g:notmuch_folders_maps, g:notmuch_custom_folders_maps) 453 | endif 454 | 455 | if exists('g:notmuch_custom_search_maps') 456 | call extend(g:notmuch_search_maps, g:notmuch_custom_search_maps) 457 | endif 458 | 459 | if exists('g:notmuch_custom_show_maps') 460 | call extend(g:notmuch_show_maps, g:notmuch_custom_show_maps) 461 | endif 462 | endfunction 463 | 464 | function! s:NotMuch(...) 465 | call s:set_defaults() 466 | 467 | ruby << EOF 468 | require 'notmuch' 469 | require 'socket' 470 | require 'tempfile' 471 | begin 472 | require 'mail' 473 | rescue LoadError 474 | end 475 | 476 | $db_name = nil 477 | $email = $email_name = $email_address = nil 478 | $exclude_tags = [] 479 | $searches = [] 480 | $threads = [] 481 | $messages = [] 482 | $mail_installed = defined?(Mail) 483 | 484 | def get_config 485 | config = IO.popen(%w[notmuch config list]) do |io| 486 | io.each(chomp: true).map { |e| e.split('=', 2) }.to_h 487 | end 488 | 489 | $db_name = config['database.path'] 490 | $email_name = config['user.name'] 491 | $email_address = config['user.primary_email'] 492 | $exclude_tags = config['search.exclude_tags']&.split(';') || [] 493 | $email = "%s <%s>" % [$email_name, $email_address] 494 | end 495 | 496 | def author_filter(a) 497 | # TODO email format, aliases 498 | a.strip! 499 | a.gsub!(/[\.@].*/, '') 500 | a.gsub!(/^ext /, '') 501 | a.gsub!(/ \(.*\)/, '') 502 | a 503 | end 504 | 505 | def get_thread_id 506 | n = $curbuf.line_number - 1 507 | return "thread:%s" % $threads[n] 508 | end 509 | 510 | def get_message 511 | n = $curbuf.line_number 512 | return $messages.find { |m| n >= m.start && n <= m.end } 513 | end 514 | 515 | def get_cur_view 516 | if $cur_filter 517 | return "#{$cur_thread} and (#{$cur_filter})" 518 | else 519 | return $cur_thread 520 | end 521 | end 522 | 523 | def generate_message_id 524 | t = Time.now 525 | random_tag = sprintf('%x%x_%x%x%x', 526 | t.to_i, t.tv_usec, 527 | $$, Thread.current.object_id.abs, rand(255)) 528 | return "<#{random_tag}@#{Socket.gethostname}.notmuch>" 529 | end 530 | 531 | def open_compose_helper(lines, cur) 532 | help_lines = [ 533 | 'Notmuch-Help: Type in your message here; to help you use these bindings:', 534 | 'Notmuch-Help: ,s - send the message (Notmuch-Help lines will be removed)', 535 | 'Notmuch-Help: ,q - abort the message', 536 | ] 537 | 538 | dir = File.expand_path('~/.notmuch/compose') 539 | FileUtils.mkdir_p(dir) 540 | Tempfile.open(['nm-', '.mail'], dir) do |f| 541 | f.puts(help_lines) 542 | f.puts 543 | f.puts(lines) 544 | 545 | sig_file = File.expand_path('~/.signature') 546 | if File.exists?(sig_file) 547 | f.puts("-- ") 548 | f.write(File.read(sig_file)) 549 | end 550 | 551 | f.flush 552 | 553 | cur += help_lines.size + 1 554 | 555 | VIM::command("let s:reply_from='%s'" % $email_address) 556 | VIM::command("call s:new_file_buffer('compose', '#{f.path}')") 557 | VIM::command("call cursor(#{cur}, 0)") 558 | end 559 | end 560 | 561 | def open_reply(orig) 562 | reply = orig.reply do |m| 563 | # fix headers 564 | if not m[:reply_to] 565 | m.to = [orig[:from].to_s, orig[:to].to_s] 566 | end 567 | m.cc = orig[:cc] 568 | m.from = $email 569 | m.charset = 'utf-8' 570 | end 571 | 572 | lines = [] 573 | 574 | body_lines = [] 575 | if $mail_installed 576 | addr = Mail::Address.new(orig[:from].value) 577 | name = addr.name 578 | name = addr.local + "@" if name.nil? && !addr.local.nil? 579 | name_format = VIM::evaluate('g:notmuch_reply_quote_format') 580 | name = name_format % [name, addr.address] if !addr.address.nil? 581 | date_format = VIM::evaluate('g:notmuch_reply_quote_datetime_format') 582 | quote_datetime = orig.date.strftime(date_format) if !date_format.empty? and orig.date 583 | else 584 | name = orig[:from] 585 | end 586 | name = "somebody" if name.nil? 587 | 588 | quote = [] 589 | quote << "On %s" % quote_datetime if quote_datetime 590 | quote << "%s wrote:" % name 591 | body_lines << quote.join(', ') 592 | part = orig.find_first_text 593 | part.convert.each_line do |l| 594 | body_lines << "> %s" % l.chomp 595 | end 596 | body_lines << "" 597 | body_lines << "" 598 | body_lines << "" 599 | 600 | reply.body = body_lines.join("\n") 601 | 602 | lines += reply.present.lines.map { |e| e.chomp } 603 | lines << "" 604 | 605 | cur = lines.count - 1 606 | 607 | open_compose_helper(lines, cur) 608 | end 609 | 610 | def open_compose() 611 | lines = [] 612 | 613 | lines << "From: #{$email}" 614 | lines << "To: " 615 | cur = lines.count 616 | 617 | lines << "Cc: " 618 | lines << "Bcc: " 619 | lines << "Subject: " 620 | lines << "" 621 | lines << "" 622 | lines << "" 623 | 624 | open_compose_helper(lines, cur) 625 | end 626 | 627 | def folders_render() 628 | $curbuf.render do |b| 629 | folders = VIM::evaluate('g:notmuch_folders') 630 | count_threads = VIM::evaluate('g:notmuch_folders_count_threads') == 1 631 | $searches.clear 632 | folders.each do |name, search| 633 | q = $curbuf.query(search) 634 | $exclude_tags.each { |e| q.add_tag_exclude(e) } 635 | $searches << search 636 | count = count_threads ? q.count_threads : q.count_messages 637 | b << "%9d %-20s (%s)" % [count, name, search] 638 | end 639 | end 640 | end 641 | 642 | def search_render(search) 643 | date_fmt = VIM::evaluate('g:notmuch_date_format') 644 | q = $curbuf.query(search) 645 | q.sort = Notmuch::SORT_NEWEST_FIRST 646 | $exclude_tags.each { |e| q.add_tag_exclude(e) } 647 | $threads.clear 648 | t = q.search_threads 649 | 650 | $render = $curbuf.render_staged(t) do |b, items| 651 | items.each do |e| 652 | authors = e.authors.force_encoding('utf-8').split(/[,|]/).map { |a| author_filter(a) }.join(",") 653 | date = Time.at(e.newest_date).strftime(date_fmt) 654 | subject = e.messages.first['subject'] 655 | if $mail_installed 656 | subject = Mail::Field.new('subject', subject).to_s 657 | else 658 | subject = subject.force_encoding('utf-8') 659 | end 660 | b << "%-12s %3s %-20.20s | %s (%s)" % [date, e.matched_messages, authors, subject, e.tags] 661 | $threads << e.thread_id 662 | end 663 | end 664 | end 665 | 666 | def do_tag(filter, tags) 667 | $curbuf.do_write do |db| 668 | q = db.query(filter) 669 | db.begin_atomic 670 | q.search_messages.each do |e| 671 | tags.split.each do |t| 672 | case t 673 | when /^-(.*)/ 674 | e.remove_tag($1) 675 | when /^\+(.*)/ 676 | e.add_tag($1) 677 | when /^([^\+^-].*)/ 678 | e.add_tag($1) 679 | end 680 | end 681 | e.tags_to_maildir_flags 682 | end 683 | db.end_atomic 684 | q.destroy! 685 | end 686 | end 687 | 688 | module DbHelper 689 | def init(name) 690 | @name = name 691 | @db = Notmuch::Database.new($db_name) 692 | @queries = [] 693 | end 694 | 695 | def query(*args) 696 | q = @db.query(*args) 697 | @queries << q 698 | q 699 | end 700 | 701 | def close 702 | @queries.delete_if { |q| ! q.destroy! } 703 | @db.close 704 | end 705 | 706 | def reopen 707 | close if @db 708 | @db = Notmuch::Database.new($db_name) 709 | end 710 | 711 | def do_write 712 | db = Notmuch::Database.new($db_name, :mode => Notmuch::MODE_READ_WRITE) 713 | begin 714 | yield db 715 | ensure 716 | db.close 717 | end 718 | end 719 | end 720 | 721 | class Message 722 | attr_accessor :start, :body_start, :end 723 | attr_reader :message_id, :filename, :mail 724 | 725 | def initialize(msg, mail) 726 | @message_id = msg.message_id 727 | @filename = msg.filename 728 | @mail = mail 729 | @start = 0 730 | @end = 0 731 | mail.import_headers(msg) if not $mail_installed 732 | end 733 | 734 | def to_s 735 | "id:%s" % @message_id 736 | end 737 | 738 | def inspect 739 | "id:%s, file:%s" % [@message_id, @filename] 740 | end 741 | end 742 | 743 | class StagedRender 744 | def initialize(buffer, enumerable, block) 745 | @b = buffer 746 | @enumerable = enumerable 747 | @block = block 748 | 749 | @b.render { do_next } 750 | end 751 | 752 | def is_ready? 753 | @b.line_number > @b.count - $curwin.height 754 | end 755 | 756 | def do_next 757 | items = @enumerable.take($curwin.height * 2) 758 | return if items.empty? 759 | @block.call @b, items 760 | end 761 | end 762 | 763 | class VIM::Buffer 764 | include DbHelper 765 | 766 | def <<(a) 767 | append(count(), a) 768 | end 769 | 770 | def render_staged(enumerable, &block) 771 | StagedRender.new(self, enumerable, block) 772 | end 773 | 774 | def render 775 | old_count = count 776 | yield self 777 | (1..old_count).each do 778 | delete(1) 779 | end 780 | end 781 | end 782 | 783 | class Notmuch::Tags 784 | def to_s 785 | to_a.join(" ") 786 | end 787 | end 788 | 789 | class Notmuch::Message 790 | def to_s 791 | "id:%s" % message_id 792 | end 793 | end 794 | 795 | # workaround for bug in vim's ruby 796 | class Object 797 | def flush 798 | end 799 | end 800 | 801 | module SimpleMessage 802 | class Header < Array 803 | def self.parse(string) 804 | return nil if string.empty? 805 | return Header.new(string.split(/,\s+/)) 806 | end 807 | 808 | def to_s 809 | self.join(', ') 810 | end 811 | end 812 | 813 | def initialize(string = nil) 814 | @raw_source = string 815 | @body = nil 816 | @headers = {} 817 | 818 | return if not string 819 | 820 | if string =~ /(.*?(\r\n|\n))\2/m 821 | head, body = $1, $' || '', $2 822 | else 823 | head, body = string, '' 824 | end 825 | @body = body 826 | end 827 | 828 | def [](name) 829 | @headers[name.to_sym] 830 | end 831 | 832 | def []=(name, value) 833 | @headers[name.to_sym] = value 834 | end 835 | 836 | def format_header(value) 837 | value.to_s.tr('_', '-').gsub(/(\w+)/) { $1.capitalize } 838 | end 839 | 840 | def to_s 841 | buffer = '' 842 | @headers.each do |key, value| 843 | buffer << "%s: %s\r\n" % 844 | [format_header(key), value] 845 | end 846 | buffer << "\r\n" 847 | buffer << @body 848 | buffer 849 | end 850 | 851 | def body=(value) 852 | @body = value 853 | end 854 | 855 | def from 856 | @headers[:from] 857 | end 858 | 859 | def decoded 860 | @body 861 | end 862 | 863 | def mime_type 864 | 'text/plain' 865 | end 866 | 867 | def multipart? 868 | false 869 | end 870 | 871 | def reply 872 | r = Mail::Message.new 873 | r[:from] = self[:to] 874 | r[:to] = self[:from] 875 | r[:cc] = self[:cc] 876 | r[:in_reply_to] = self[:message_id] 877 | r[:references] = self[:references] 878 | r 879 | end 880 | 881 | HEADERS = [ :from, :to, :cc, :references, :in_reply_to, :reply_to, :message_id ] 882 | 883 | def import_headers(m) 884 | HEADERS.each do |e| 885 | dashed = format_header(e) 886 | @headers[e] = Header.parse(m[dashed]) 887 | end 888 | end 889 | end 890 | 891 | module Mail 892 | 893 | if not $mail_installed 894 | puts "WARNING: Install the 'mail' gem, without it support is limited" 895 | 896 | def self.read(filename) 897 | Message.new(File.open(filename, 'rb') { |f| f.read }) 898 | end 899 | 900 | class Message 901 | include SimpleMessage 902 | end 903 | end 904 | 905 | class Message 906 | 907 | def find_first_text 908 | return self if not multipart? 909 | return text_part || html_part || parts.first 910 | end 911 | 912 | def convert 913 | if mime_type != "text/html" 914 | text = decoded 915 | else 916 | IO.popen(VIM::evaluate('g:notmuch_html_converter'), "w+") do |pipe| 917 | pipe.write(decode_body) 918 | pipe.close_write 919 | text = pipe.read 920 | end 921 | end 922 | text 923 | end 924 | 925 | def present 926 | buffer = '' 927 | header.fields.each do |f| 928 | buffer << "%s: %s\r\n" % [f.name, f.to_s] 929 | end 930 | buffer << "\r\n" 931 | buffer << body.to_s 932 | buffer 933 | end 934 | end 935 | end 936 | 937 | get_config 938 | EOF 939 | if a:0 940 | call s:search(join(a:000)) 941 | else 942 | call s:folders() 943 | endif 944 | endfunction 945 | 946 | command -nargs=* NotMuch call s:NotMuch() 947 | 948 | " vim: set noexpandtab: 949 | -------------------------------------------------------------------------------- /syntax/notmuch-compose.vim: -------------------------------------------------------------------------------- 1 | runtime! syntax/mail.vim 2 | 3 | syntax region nmComposeHelp contains=nmComposeHelpLine start='^Notmuch-Help:\%1l' end='^\(Notmuch-Help:\)\@!' 4 | syntax match nmComposeHelpLine /Notmuch-Help:/ contained 5 | 6 | highlight link nmComposeHelp Include 7 | highlight link nmComposeHelpLine Error 8 | -------------------------------------------------------------------------------- /syntax/notmuch-folders.vim: -------------------------------------------------------------------------------- 1 | " notmuch folders mode syntax file 2 | 3 | syntax region nmFoldersCount start='^' end='\%10v' 4 | syntax region nmFoldersName start='\%11v' end='\%31v' 5 | syntax match nmFoldersSearch /([^()]\+)$/ 6 | 7 | highlight link nmFoldersCount Statement 8 | highlight link nmFoldersName Type 9 | highlight link nmFoldersSearch String 10 | 11 | highlight CursorLine term=reverse cterm=reverse gui=reverse 12 | 13 | -------------------------------------------------------------------------------- /syntax/notmuch-git-diff.vim: -------------------------------------------------------------------------------- 1 | syn match diffRemoved "^-.*" 2 | syn match diffAdded "^+.*" 3 | 4 | syn match diffSeparator "^---$" 5 | syn match diffSubname " @@..*"ms=s+3 contained 6 | syn match diffLine "^@.*" contains=diffSubname 7 | 8 | syn match diffFile "^diff .*" 9 | syn match diffNewFile "^+++ .*" 10 | syn match diffOldFile "^--- .*" 11 | 12 | hi def link diffOldFile diffFile 13 | hi def link diffNewFile diffFile 14 | 15 | hi def link diffFile Type 16 | hi def link diffRemoved Special 17 | hi def link diffAdded Identifier 18 | hi def link diffLine Statement 19 | hi def link diffSubname PreProc 20 | 21 | syntax match gitDiffStatLine /^ .\{-}\zs[+-]\+$/ contains=gitDiffStatAdd,gitDiffStatDelete 22 | syntax match gitDiffStatAdd /+/ contained 23 | syntax match gitDiffStatDelete /-/ contained 24 | 25 | hi def link gitDiffStatAdd diffAdded 26 | hi def link gitDiffStatDelete diffRemoved 27 | -------------------------------------------------------------------------------- /syntax/notmuch-search.vim: -------------------------------------------------------------------------------- 1 | syntax region nmSearch start=/^/ end=/$/ oneline contains=nmSearchDate 2 | syntax match nmSearchDate /^.\{-13}/ contained nextgroup=nmSearchNum 3 | syntax match nmSearchNum /.\{-4}/ contained nextgroup=nmSearchFrom 4 | syntax match nmSearchFrom /.\{-21}/ contained nextgroup=nmSearchSubject 5 | syntax match nmSearchSubject /.\{0,}\(([^()]\+)$\)\@=/ contained nextgroup=nmSearchTags 6 | syntax match nmSearchTags /.\+$/ contained 7 | 8 | highlight link nmSearchDate Statement 9 | highlight link nmSearchNum Type 10 | highlight link nmSearchFrom Include 11 | highlight link nmSearchSubject Normal 12 | highlight link nmSearchTags String 13 | -------------------------------------------------------------------------------- /syntax/notmuch-show.vim: -------------------------------------------------------------------------------- 1 | " notmuch show mode syntax file 2 | 3 | syntax cluster nmShowMsgDesc contains=nmShowMsgDescWho,nmShowMsgDescDate,nmShowMsgDescTags 4 | syntax match nmShowMsgDescWho /[^)]\+>/ contained 5 | syntax match nmShowMsgDescDate / [^(]\+ / contained 6 | syntax match nmShowMsgDescTags /([^)]\+)$/ contained 7 | 8 | syntax cluster nmShowMsgHead contains=nmShowMsgHeadKey,nmShowMsgHeadVal 9 | syntax match nmShowMsgHeadKey /^[^:]\+: / contained 10 | syntax match nmShowMsgHeadVal /^\([^:]\+: \)\@<=.*/ contained 11 | 12 | syntax cluster nmShowMsgBody contains=@nmShowMsgBodyMail,@nmShowMsgBodyGit 13 | syntax include @nmShowMsgBodyMail syntax/mail.vim 14 | 15 | silent! syntax include @nmShowMsgBodyGit syntax/notmuch-git-diff.vim 16 | 17 | highlight nmShowMsgDescWho term=reverse cterm=reverse gui=reverse 18 | highlight link nmShowMsgDescDate Type 19 | highlight link nmShowMsgDescTags String 20 | 21 | highlight link nmShowMsgHeadKey Macro 22 | "highlight link nmShowMsgHeadVal NONE 23 | 24 | highlight Folded term=reverse ctermfg=LightGrey ctermbg=Black guifg=LightGray guibg=Black 25 | --------------------------------------------------------------------------------