1 if exists("g:loaded_notmuch")
5 if !has("ruby") || version < 700
9 let g:loaded_notmuch = "yep"
11 let g:notmuch_folders_maps = {
12 \ '<Enter>': 'folders_show_search()',
13 \ 's': 'folders_search_prompt()',
14 \ '=': 'folders_refresh()',
18 let g:notmuch_search_maps = {
19 \ 'q': 'kill_this_buffer()',
20 \ '<Enter>': 'search_show_thread(1)',
21 \ '<Space>': 'search_show_thread(2)',
22 \ 'A': 'search_tag("-inbox -unread")',
23 \ 'I': 'search_tag("-unread")',
24 \ 't': 'search_tag("")',
25 \ 's': 'search_search_prompt()',
26 \ '=': 'search_refresh()',
27 \ '?': 'search_info()',
31 let g:notmuch_show_maps = {
32 \ 'q': 'kill_this_buffer()',
33 \ 'A': 'show_tag("-inbox -unread")',
34 \ 'I': 'show_tag("-unread")',
35 \ 't': 'show_tag("")',
36 \ 'o': 'show_open_msg()',
37 \ 'e': 'show_extract_msg()',
38 \ 's': 'show_save_msg()',
39 \ 'p': 'show_save_patches()',
40 \ 'r': 'show_reply()',
42 \ '<Tab>': 'show_next_msg()',
46 let g:notmuch_compose_maps = {
47 \ ',s': 'compose_send()',
48 \ ',q': 'compose_quit()',
51 let s:notmuch_folders_default = [
52 \ [ 'new', 'tag:inbox and tag:unread' ],
53 \ [ 'inbox', 'tag:inbox' ],
54 \ [ 'unread', 'tag:unread' ],
57 let s:notmuch_date_format_default = '%d.%m.%y'
58 let s:notmuch_datetime_format_default = '%d.%m.%y %H:%M:%S'
59 let s:notmuch_reader_default = 'mutt -f %s'
60 let s:notmuch_sendmail_default = 'sendmail'
61 let s:notmuch_folders_count_threads_default = 0
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'))
70 function! s:on_compose_delete()
74 if input('[s]end/[q]uit? ') =~ '^s'
81 function! s:compose_quit()
82 let b:compose_done = 1
83 call s:kill_this_buffer()
86 function! s:compose_send()
87 let b:compose_done = 1
88 let fname = expand('%')
89 let lines = getline(5, '$')
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)
101 let cmdtxt = g:notmuch_sendmail . ' -t -f ' . s:reply_from . ' < ' . fname
102 let out = system(cmdtxt)
103 let err = v:shell_error
106 echo 'Eeek! unable to send mail'
112 echo 'Mail sent successfully.'
113 call s:kill_this_buffer()
116 function! s:show_next_msg()
118 r, c = $curwin.cursor
119 n = $curbuf.line_number
120 i = $messages.index { |m| n >= m.start && n <= m.end }
124 VIM::command("normal #{m.start}zt")
125 $curwin.cursor = r, c
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 <buffer> call s:on_compose_delete()
138 function! s:compose()
140 let b:compose_done = 0
141 call s:set_map(g:notmuch_compose_maps)
142 autocmd BufDelete <buffer> call s:on_compose_delete()
146 function! s:show_info()
147 ruby vim_puts get_message.inspect
150 function! s:show_extract_msg()
153 m.mail.attachments.each do |a|
154 File.open(a.filename, 'w') do |f|
155 f.write a.body.decoded
156 print "Extracted '#{a.filename}'"
162 function! s:show_open_msg()
165 mbox = File.expand_path('~/.notmuch/vim_mbox')
166 cmd = VIM::evaluate('g:notmuch_reader') % mbox
167 system "notmuch show --format=mbox id:#{m.message_id} > #{mbox} && #{cmd}"
171 function! s:show_save_msg()
172 let file = input('File name: ')
174 file = VIM::evaluate('file')
176 system "notmuch show --format=mbox id:#{m.message_id} > #{file}"
180 function! s:show_save_patches()
182 q = $curbuf.query($cur_thread)
183 t = q.search_threads.first
185 t.toplevel_messages.first.replies.each do |m|
186 next if not m['subject'] =~ /^\[PATCH.*\]/
187 file = "%04d.patch" % [n += 1]
188 system "notmuch show --format=mbox id:#{m.message_id} > #{file}"
190 vim_puts "Saved #{n} patches"
194 function! s:show_tag(intags)
196 let tags = input('tags: ')
200 ruby do_tag(get_cur_view, VIM::evaluate('l:tags'))
201 call s:show_next_thread()
204 function! s:search_search_prompt()
205 let text = input('Search: ')
211 $cur_search = VIM::evaluate('text')
213 search_render($cur_search)
215 setlocal nomodifiable
218 function! s:search_info()
219 ruby vim_puts get_thread_id
222 function! s:search_refresh()
225 ruby search_render($cur_search)
226 setlocal nomodifiable
229 function! s:search_tag(intags)
231 let tags = input('tags: ')
235 ruby do_tag(get_thread_id, VIM::evaluate('l:tags'))
239 function! s:folders_search_prompt()
240 let text = input('Search: ')
244 function! s:folders_refresh()
247 ruby folders_render()
248 setlocal nomodifiable
253 function! s:show_cursor_moved()
256 VIM::command('setlocal modifiable')
258 VIM::command('setlocal nomodifiable')
263 function! s:show_next_thread()
264 call s:kill_this_buffer()
265 if line('.') != line('$')
267 call s:search_show_thread(0)
269 echo 'No more messages.'
273 function! s:kill_this_buffer()
276 VIM::command("bdelete!")
280 function! s:set_map(maps)
282 for [key, code] in items(a:maps)
283 let cmd = printf(":call <SID>%s<CR>", code)
284 exec printf('nnoremap <buffer> %s %s', key, cmd)
288 function! s:new_buffer(type)
290 setlocal buftype=nofile bufhidden=hide
292 execute printf('set filetype=notmuch-%s', a:type)
293 execute printf('set syntax=notmuch-%s', a:type)
294 ruby $curbuf.init(VIM::evaluate('a:type'))
297 function! s:set_menu_buffer()
298 setlocal nomodifiable
305 function! s:show(thread_id)
306 call s:new_buffer('show')
309 thread_id = VIM::evaluate('a:thread_id')
310 $cur_thread = thread_id
312 $curbuf.render do |b|
313 q = $curbuf.query(get_cur_view)
314 q.sort = Notmuch::SORT_OLDEST_FIRST
315 msgs = q.search_messages
317 m = Mail.read(msg.filename)
318 part = m.find_first_text
319 nm_m = Message.new(msg, m)
321 date_fmt = VIM::evaluate('g:notmuch_datetime_format')
322 date = Time.at(msg.date).strftime(date_fmt)
324 b << "%s %s (%s)" % [msg['from'], date, msg.tags]
325 b << "Subject: %s" % [msg['subject']]
326 b << "To: %s" % msg['to']
327 b << "Cc: %s" % msg['cc']
328 b << "Date: %s" % msg['date']
329 nm_m.body_start = b.count
330 b << "--- %s ---" % part.mime_type
331 part.convert.each_line do |l|
339 $messages.each_with_index do |msg, i|
340 VIM::command("syntax region nmShowMsg#{i}Desc start='\\%%%il' end='\\%%%il' contains=@nmShowMsgDesc" % [msg.start, msg.start + 1])
341 VIM::command("syntax region nmShowMsg#{i}Head start='\\%%%il' end='\\%%%il' contains=@nmShowMsgHead" % [msg.start + 1, msg.body_start])
342 VIM::command("syntax region nmShowMsg#{i}Body start='\\%%%il' end='\\%%%dl' contains=@nmShowMsgBody" % [msg.body_start, msg.end])
345 setlocal nomodifiable
346 call s:set_map(g:notmuch_show_maps)
349 function! s:search_show_thread(mode)
351 mode = VIM::evaluate('a:mode')
355 when 1; $cur_filter = nil
356 when 2; $cur_filter = $cur_search
358 VIM::command("call s:show('#{id}')")
362 function! s:search(search)
363 call s:new_buffer('search')
365 $cur_search = VIM::evaluate('a:search')
366 search_render($cur_search)
368 call s:set_menu_buffer()
369 call s:set_map(g:notmuch_search_maps)
370 autocmd CursorMoved <buffer> call s:show_cursor_moved()
373 function! s:folders_show_search()
375 n = $curbuf.line_number
377 VIM::command("call s:search('#{s}')")
381 function! s:folders()
382 call s:new_buffer('folders')
383 ruby folders_render()
384 call s:set_menu_buffer()
385 call s:set_map(g:notmuch_folders_maps)
390 function! s:set_defaults()
391 if !exists('g:notmuch_date_format')
392 if exists('g:notmuch_rb_date_format')
393 let g:notmuch_date_format = g:notmuch_rb_date_format
395 let g:notmuch_date_format = s:notmuch_date_format_default
399 if !exists('g:notmuch_datetime_format')
400 if exists('g:notmuch_rb_datetime_format')
401 let g:notmuch_datetime_format = g:notmuch_rb_datetime_format
403 let g:notmuch_datetime_format = s:notmuch_datetime_format_default
407 if !exists('g:notmuch_reader')
408 if exists('g:notmuch_rb_reader')
409 let g:notmuch_reader = g:notmuch_rb_reader
411 let g:notmuch_reader = s:notmuch_reader_default
415 if !exists('g:notmuch_sendmail')
416 if exists('g:notmuch_rb_sendmail')
417 let g:notmuch_sendmail = g:notmuch_rb_sendmail
419 let g:notmuch_sendmail = s:notmuch_sendmail_default
423 if !exists('g:notmuch_folders_count_threads')
424 if exists('g:notmuch_rb_count_threads')
425 let g:notmuch_count_threads = g:notmuch_rb_count_threads
427 let g:notmuch_folders_count_threads = s:notmuch_folders_count_threads_default
431 if !exists('g:notmuch_custom_search_maps') && exists('g:notmuch_rb_custom_search_maps')
432 let g:notmuch_custom_search_maps = g:notmuch_rb_custom_search_maps
435 if !exists('g:notmuch_custom_show_maps') && exists('g:notmuch_rb_custom_show_maps')
436 let g:notmuch_custom_show_maps = g:notmuch_rb_custom_show_maps
439 if exists('g:notmuch_custom_search_maps')
440 call extend(g:notmuch_search_maps, g:notmuch_custom_search_maps)
443 if exists('g:notmuch_custom_show_maps')
444 call extend(g:notmuch_show_maps, g:notmuch_custom_show_maps)
447 if !exists('g:notmuch_folders')
448 if exists('g:notmuch_rb_folders')
449 let g:notmuch_folders = g:notmuch_rb_folders
451 let g:notmuch_folders = s:notmuch_folders_default
456 function! s:NotMuch(...)
457 call s:set_defaults()
470 $email = $email_name = $email_address = nil
475 $mail_installed = defined?(Mail)
479 config = ENV['NOTMUCH_CONFIG'] || '~/.notmuch-config'
480 File.open(File.expand_path(config)).each do |l|
487 key = "%s.%s" % [group, $1]
493 $db_name = $config['database.path']
494 $email_name = $config['user.name']
495 $email_address = $config['user.primary_email']
496 $email = "%s <%s>" % [$email_name, $email_address]
500 VIM::command("echo '#{s.to_s}'")
504 VIM::command("echo '#{s.inspect}'")
508 # TODO email format, aliases
510 a.gsub!(/[\.@].*/, '')
512 a.gsub!(/ \(.*\)/, '')
517 n = $curbuf.line_number - 1
518 return "thread:%s" % $threads[n]
522 n = $curbuf.line_number
523 return $messages.find { |m| n >= m.start && n <= m.end }
528 return "#{$cur_thread} and (#{$cur_filter})"
534 def generate_message_id
536 random_tag = sprintf('%x%x_%x%x%x',
538 $$, Thread.current.object_id.abs, rand(255))
539 return "<#{random_tag}@#{Socket.gethostname}.notmuch>"
542 def open_compose_helper(lines, cur)
544 'Notmuch-Help: Type in your message here; to help you use these bindings:',
545 'Notmuch-Help: ,s - send the message (Notmuch-Help lines will be removed)',
546 'Notmuch-Help: ,q - abort the message',
549 dir = File.expand_path('~/.notmuch/compose')
550 FileUtils.mkdir_p(dir)
551 Tempfile.open(['nm-', '.mail'], dir) do |f|
556 sig_file = File.expand_path('~/.signature')
557 if File.exists?(sig_file)
559 f.write(File.read(sig_file))
564 cur += help_lines.size + 1
566 VIM::command("let s:reply_from='%s'" % $email_address)
567 VIM::command("call s:new_file_buffer('compose', '#{f.path}')")
568 VIM::command("call cursor(#{cur}, 0)")
573 reply = orig.reply do |m|
576 m.to = [orig[:from].to_s, orig[:to].to_s]
587 addr = Mail::Address.new(orig[:from].value)
589 name = addr.local + "@" if name.nil? && !addr.local.nil?
593 name = "somebody" if name.nil?
595 body_lines << "%s wrote:" % name
596 part = orig.find_first_text
597 part.convert.each_line do |l|
598 body_lines << "> %s" % l.chomp
604 reply.body = body_lines.join("\n")
606 lines += reply.present.lines.map { |e| e.chomp }
609 cur = lines.count - 1
611 open_compose_helper(lines, cur)
617 lines << "From: #{$email}"
628 open_compose_helper(lines, cur)
632 $curbuf.render do |b|
633 folders = VIM::evaluate('g:notmuch_folders')
634 count_threads = VIM::evaluate('g:notmuch_folders_count_threads') == 1
636 folders.each do |name, search|
637 q = $curbuf.query(search)
639 count = count_threads ? q.search_threads.count : q.search_messages.count
640 b << "%9d %-20s (%s)" % [count, name, search]
645 def search_render(search)
646 date_fmt = VIM::evaluate('g:notmuch_date_format')
647 q = $curbuf.query(search)
648 q.sort = Notmuch::SORT_NEWEST_FIRST
652 $render = $curbuf.render_staged(t) do |b, items|
654 authors = e.authors.to_utf8.split(/[,|]/).map { |a| author_filter(a) }.join(",")
655 date = Time.at(e.newest_date).strftime(date_fmt)
656 subject = e.messages.first['subject']
658 subject = Mail::Field.new("Subject: " + subject).to_s
660 subject = subject.force_encoding('utf-8')
662 b << "%-12s %3s %-20.20s | %s (%s)" % [date, e.matched_messages, authors, subject, e.tags]
663 $threads << e.thread_id
668 def do_tag(filter, tags)
669 $curbuf.do_write do |db|
671 q.search_messages.each do |e|
673 tags.split.each do |t|
684 e.tags_to_maildir_flags
693 @db = Notmuch::Database.new($db_name)
704 @queries.delete_if { |q| ! q.destroy! }
710 @db = Notmuch::Database.new($db_name)
714 db = Notmuch::Database.new($db_name, :mode => Notmuch::MODE_READ_WRITE)
724 attr_accessor :start, :body_start, :end
725 attr_reader :message_id, :filename, :mail
727 def initialize(msg, mail)
728 @message_id = msg.message_id
729 @filename = msg.filename
733 mail.import_headers(msg) if not $mail_installed
737 "id:%s" % @message_id
741 "id:%s, file:%s" % [@message_id, @filename]
746 def initialize(buffer, enumerable, block)
748 @enumerable = enumerable
752 @b.render { do_next }
756 @last_render - @b.line_number <= $curwin.height
760 items = @enumerable.take($curwin.height * 2)
761 return if items.empty?
762 @block.call @b, items
763 @last_render = @b.count
774 def render_staged(enumerable, &block)
775 StagedRender.new(self, enumerable, block)
781 (1..old_count).each do
793 class Notmuch::Message
799 # workaround for bug in vim's ruby
807 def self.parse(string)
808 return nil if string.empty?
809 return Header.new(string.split(/,\s+/))
817 def initialize(string = nil)
824 if string =~ /(.*?(\r\n|\n))\2/m
825 head, body = $1, $' || '', $2
827 head, body = string, ''
833 @headers[name.to_sym]
837 @headers[name.to_sym] = value
840 def format_header(value)
841 value.to_s.tr('_', '-').gsub(/(\w+)/) { $1.capitalize }
846 @headers.each do |key, value|
847 buffer << "%s: %s\r\n" %
848 [format_header(key), value]
876 r = Mail::Message.new
880 r[:in_reply_to] = self[:message_id]
881 r[:references] = self[:references]
885 HEADERS = [ :from, :to, :cc, :references, :in_reply_to, :reply_to, :message_id ]
887 def import_headers(m)
889 dashed = format_header(e)
890 @headers[e] = Header.parse(m[dashed])
897 if not $mail_installed
898 puts "WARNING: Install the 'mail' gem, without it support is limited"
900 def self.read(filename)
901 Message.new(File.open(filename, 'rb') { |f| f.read })
905 include SimpleMessage
912 return self if not multipart?
913 return text_part || html_part
917 if mime_type != "text/html"
920 IO.popen(VIM::evaluate('exists("g:notmuch_html_converter") ? ' +
921 'g:notmuch_html_converter : "elinks --dump"'), "w+") do |pipe|
922 pipe.write(decode_body)
932 header.fields.each do |f|
933 buffer << "%s: %s\r\n" % [f.name, f.to_s]
944 RUBY_VERSION >= "1.9" ? force_encoding('utf-8') : self
951 call s:search(join(a:000))
957 command -nargs=* NotMuch call s:NotMuch(<f-args>)
959 " vim: set noexpandtab: