├── 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 |
--------------------------------------------------------------------------------