[RFC/PATCH] Vim client rewrite
authoranton <anton@khirnov.net>
Sun, 15 May 2011 19:15:31 +0000 (21:15 +0200)
committerW. Trevor King <wking@tremily.us>
Fri, 7 Nov 2014 17:38:15 +0000 (09:38 -0800)
55/2cbeb2213b5bc5d186744fa499704bf38cc666 [new file with mode: 0644]

diff --git a/55/2cbeb2213b5bc5d186744fa499704bf38cc666 b/55/2cbeb2213b5bc5d186744fa499704bf38cc666
new file mode 100644 (file)
index 0000000..570b713
--- /dev/null
@@ -0,0 +1,1687 @@
+Return-Path: <anton@khirnov.net>\r
+X-Original-To: notmuch@notmuchmail.org\r
+Delivered-To: notmuch@notmuchmail.org\r
+Received: from localhost (localhost [127.0.0.1])\r
+       by olra.theworths.org (Postfix) with ESMTP id 8D147431FD0\r
+       for <notmuch@notmuchmail.org>; Sun, 15 May 2011 12:21:07 -0700 (PDT)\r
+X-Virus-Scanned: Debian amavisd-new at olra.theworths.org\r
+X-Spam-Flag: NO\r
+X-Spam-Score: 1.845\r
+X-Spam-Level: *\r
+X-Spam-Status: No, score=1.845 tagged_above=-999 required=5\r
+       tests=[LONGWORDS=1.844, WEIRD_QUOTING=0.001] autolearn=disabled\r
+Received: from olra.theworths.org ([127.0.0.1])\r
+       by localhost (olra.theworths.org [127.0.0.1]) (amavisd-new, port 10024)\r
+       with ESMTP id lo3fDz2gKhhp for <notmuch@notmuchmail.org>;\r
+       Sun, 15 May 2011 12:21:03 -0700 (PDT)\r
+X-Greylist: delayed 323 seconds by postgrey-1.32 at olra;\r
+       Sun, 15 May 2011 12:21:03 PDT\r
+Received: from lain.khirnov.net (lain.khirnov.net [193.85.154.54])\r
+       by olra.theworths.org (Postfix) with ESMTP id 19A0F431FB6\r
+       for <notmuch@notmuchmail.org>; Sun, 15 May 2011 12:21:03 -0700 (PDT)\r
+Received: from localhost (localhost [127.0.0.1])\r
+       by lain.khirnov.net (Postfix) with ESMTP id 01412C127B\r
+       for <notmuch@notmuchmail.org>; Sun, 15 May 2011 21:15:39 +0200 (CEST)\r
+Received: from lain.khirnov.net ([127.0.0.1])\r
+       by localhost (mail.khirnov.net [127.0.0.1]) (amavisd-new, port 10024)\r
+       with ESMTP id 5IEjlamK6tts for <notmuch@notmuchmail.org>;\r
+       Sun, 15 May 2011 21:15:32 +0200 (CEST)\r
+Received: from zohar.localdomain (unknown [192.168.0.1])\r
+       by lain.khirnov.net (Postfix) with ESMTP id C47E7C0DEF\r
+       for <notmuch@notmuchmail.org>; Sun, 15 May 2011 21:15:32 +0200 (CEST)\r
+Received: from zohar.khirnov.net (localhost [127.0.0.1])\r
+       by zohar.localdomain (Postfix) with ESMTP id 97FCC7F43E\r
+       for <notmuch@notmuchmail.org>; Sun, 15 May 2011 21:15:32 +0200 (CEST)\r
+Content-Type: multipart/mixed; boundary="===============0474093823=="\r
+MIME-Version: 1.0\r
+User-Agent: Notmuch-vim EXPERIMENTAL\r
+Message-ID: <20110515191531.25709.21869@zohar.khirnov.net>\r
+Date: Sun, 15 May 2011 21:15:31 +0200\r
+To: notmuch@notmuchmail.org\r
+From: anton@khirnov.net\r
+Subject: [RFC/PATCH] Vim client rewrite\r
+X-BeenThere: notmuch@notmuchmail.org\r
+X-Mailman-Version: 2.1.13\r
+Precedence: list\r
+List-Id: "Use and development of the notmuch mail system."\r
+       <notmuch.notmuchmail.org>\r
+List-Unsubscribe: <http://notmuchmail.org/mailman/options/notmuch>,\r
+       <mailto:notmuch-request@notmuchmail.org?subject=unsubscribe>\r
+List-Archive: <http://notmuchmail.org/pipermail/notmuch>\r
+List-Post: <mailto:notmuch@notmuchmail.org>\r
+List-Help: <mailto:notmuch-request@notmuchmail.org?subject=help>\r
+List-Subscribe: <http://notmuchmail.org/mailman/listinfo/notmuch>,\r
+       <mailto:notmuch-request@notmuchmail.org?subject=subscribe>\r
+X-List-Received-Date: Sun, 15 May 2011 19:21:07 -0000\r
+\r
+--===============0474093823==\r
+Content-Type: text/plain; charset="utf-8"\r
+MIME-Version: 1.0\r
+Content-Transfer-Encoding: quoted-printable\r
+\r
+\r
+Hi,\r
+\r
+my attempts to make the vim client more usable somehow spiraled out of\r
+control and turned into a huge rewrite. The intermediate results I\r
+hereby present for your amusement and comments.\r
+(attached as whole files, since the patch would be unreadable)\r
+\r
+The main point of the rewrite is splitting of a large part of the code\r
+into Python. This should have the following advantages:\r
+1) python-notmuch bindings can be used, which should allow for cleaner\r
+   and more reliable code than running the binary and parsing its output\r
+   with regexps.\r
+   (also provides a nice use case for python-notmuch)\r
+2) Python's huge standard library makes implementing some features MUCH eas=\r
+ier.\r
+3) More people know Python than vimscript, thus making the client\r
+   development easier\r
+\r
+The code is =CE=B1 quality, but should be close to usable.\r
+It already has some features not present in the mainline vim client,\r
+like attachments (viewing and sending, saving to file should be trivial\r
+to add, will be done when I have some time), better support for unicode\r
+and more.\r
+\r
+Some UI features from the mainline versions that I didn't use were\r
+removed and customization options are somewhat lacking atm. This is of\r
+course to be improved later, depending on the responses.\r
+\r
+Comments, bugreports and fixes very much welcome.\r
+\r
+--\r
+Anton Khirnov\r
+--===============0474093823==\r
+Content-Type: text/plain; charset="utf-8"\r
+MIME-Version: 1.0\r
+Content-Transfer-Encoding: quoted-printable\r
+Content-Disposition: attachment; filename="notmuch.vim"\r
+\r
+" notmuch.vim plugin --- run notmuch within vim\r
+"\r
+" Copyright =C2=A9 Carl Worth\r
+"\r
+" This file is part of Notmuch.\r
+"\r
+" Notmuch is free software: you can redistribute it and/or modify it\r
+" under the terms of the GNU General Public License as published by\r
+" the Free Software Foundation, either version 3 of the License, or\r
+" (at your option) any later version.\r
+"\r
+" Notmuch is distributed in the hope that it will be useful, but\r
+" WITHOUT ANY WARRANTY; without even the implied warranty of\r
+" MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\r
+" General Public License for more details.\r
+"\r
+" You should have received a copy of the GNU General Public License\r
+" along with Notmuch.  If not, see <http://www.gnu.org/licenses/>.\r
+"\r
+" Authors: Bart Trojanowski <bart@jukie.net>\r
+" Contributors: Felipe Contreras <felipe.contreras@gmail.com>,\r
+"   Peter Hartman <peterjohnhartman@gmail.com>\r
+"\r
+\r
+if exists('s:notmuch_loaded') || &cp\r
+    finish\r
+endif\r
+let s:notmuch_loaded =3D 1\r
+\r
+\r
+" --- configuration defaults {{{1\r
+\r
+let s:notmuch_defaults =3D {\r
+        \ 'g:notmuch_cmd':                           'notmuch'             =\r
+       ,\r
+        \\r
+        \ 'g:notmuch_search_newest_first':           1                     =\r
+       ,\r
+        \\r
+        \ 'g:notmuch_compose_insert_mode_start':     1                     =\r
+       ,\r
+        \ 'g:notmuch_compose_header_help':           1                     =\r
+       ,\r
+        \ 'g:notmuch_compose_temp_file_dir':         '~/.notmuch/compose/' =\r
+       ,\r
+        \ 'g:notmuch_fcc_maildir':                   'sent'                =\r
+       ,\r
+        \ }\r
+\r
+" defaults for g:notmuch_folders\r
+" override with: let g:notmuch_folders =3D [ ... ]\r
+let s:notmuch_folders_defaults =3D [\r
+        \ [ 'new',    'tag:inbox and tag:unread' ],\r
+        \ [ 'inbox',  'tag:inbox'                ],\r
+        \ [ 'unread', 'tag:unread'               ],\r
+        \ ]\r
+\r
+let s:notmuch_show_headers_defaults =3D [\r
+    \ 'From',\r
+    \ 'To',\r
+    \ 'Cc',\r
+    \ 'Subject',\r
+    \ 'Date',\r
+    \ 'Reply-To',\r
+    \ 'Message-Id',\r
+    \]\r
+\r
+" defaults for g:notmuch_compose_headers\r
+" override with: let g:notmuch_compose_headers =3D [ ... ]\r
+let s:notmuch_compose_headers_defaults =3D [\r
+        \ 'From',\r
+        \ 'To',\r
+        \ 'Cc',\r
+        \ 'Bcc',\r
+        \ 'Subject'\r
+        \ ]\r
+\r
+" --- keyboard mapping definitions {{{1\r
+\r
+" --- --- bindings for folders mode {{{2\r
+\r
+let g:notmuch_folders_maps =3D {\r
+        \ 'm':          ':call <SID>NM_new_mail()<CR>',\r
+        \ 's':          ':call <SID>NM_search_prompt(0)<CR>',\r
+        \ 'q':          ':call <SID>NM_kill_this_buffer()<CR>',\r
+        \ '=3D':          ':call <SID>NM_folders_refresh_view()<CR>',\r
+        \ '<Enter>':    ':call <SID>NM_folders_show_search('''')<CR>',\r
+        \ '<Space>':    ':call <SID>NM_folders_show_search(''tag:unread'')<=\r
+CR>',\r
+        \ 'tt':         ':call <SID>NM_folders_from_tags()<CR>',\r
+        \ }\r
+\r
+" --- --- bindings for search screen {{{2\r
+let g:notmuch_search_maps =3D {\r
+        \ '<Space>':    ':call <SID>NM_search_show_thread()<CR>',\r
+        \ '<Enter>':    ':call <SID>NM_search_show_thread()<CR>',\r
+        \ '<C-]>':      ':call <SID>NM_search_expand(''<cword>'')<CR>',\r
+        \ 'a':          ':call <SID>NM_search_archive_thread()<CR>',\r
+        \ 'A':          ':call <SID>NM_search_mark_read_then_archive_thread=\r
+()<CR>',\r
+        \ 'D':          ':call <SID>NM_search_delete_thread()<CR>',\r
+        \ 'f':          ':call <SID>NM_search_filter()<CR>',\r
+        \ 'm':          ':call <SID>NM_new_mail()<CR>',\r
+        \ 'o':          ':call <SID>NM_search_toggle_order()<CR>',\r
+        \ 'r':          ':call <SID>NM_search_reply_to_thread()<CR>',\r
+        \ 's':          ':call <SID>NM_search_prompt(0)<CR>',\r
+        \ ',s':         ':call <SID>NM_search_prompt(1)<CR>',\r
+        \ 'q':          ':call <SID>NM_kill_this_buffer()<CR>',\r
+        \ '+':          ':call <SID>NM_search_add_tags([])<CR>',\r
+        \ '-':          ':call <SID>NM_search_remove_tags([])<CR>',\r
+        \ '=3D':          ':call <SID>NM_search_refresh_view()<CR>',\r
+        \ }\r
+\r
+" --- --- bindings for show screen {{{2\r
+let g:notmuch_show_maps =3D {\r
+        \ '<C-P>':      ':call <SID>NM_jump_message(-1)<CR>',\r
+        \ '<C-N>':      ':call <SID>NM_jump_message(+1)<CR>',\r
+        \ '<C-]>':      ':call <SID>NM_search_expand(''<cword>'')<CR>',\r
+        \ 'q':          ':call <SID>NM_kill_this_buffer()<CR>',\r
+        \ 's':          ':call <SID>NM_search_prompt(0)<CR>',\r
+        \\r
+        \\r
+        \ 'a':          ':call <SID>NM_show_archive_thread()<CR>',\r
+        \ 'A':          ':call <SID>NM_show_mark_read_then_archive_thread()=\r
+<CR>',\r
+        \ 'N':          ':call <SID>NM_show_mark_read_then_next_open_messag=\r
+e()<CR>',\r
+        \ 'v':          ':call <SID>NM_show_view_all_mime_parts()<CR>',\r
+        \ '+':          ':call <SID>NM_show_add_tag()<CR>',\r
+        \ '-':          ':call <SID>NM_show_remove_tag()<CR>',\r
+        \ '<Space>':    ':call <SID>NM_show_advance()<CR>',\r
+        \ '\|':         ':call <SID>NM_show_pipe_message()<CR>',\r
+        \\r
+        \ '<S-Tab>':    ':call <SID>NM_show_previous_fold()<CR>',\r
+        \ '<Tab>':      ':call <SID>NM_show_next_fold()<CR>',\r
+        \ '<Enter>':    ':call <SID>NM_show_view_attachment()<CR>',\r
+        \\r
+        \ 'r':          ':call <SID>NM_show_reply()<CR>',\r
+        \ 'm':          ':call <SID>NM_new_mail()<CR>',\r
+        \ }\r
+\r
+" --- --- bindings for compose screen {{{2\r
+let g:notmuch_compose_nmaps =3D {\r
+        \ ',s':         ':call <SID>NM_compose_send()<CR>',\r
+        \ ',a':         ':call <SID>NM_compose_attach()<CR>',\r
+        \ ',q':         ':call <SID>NM_kill_this_buffer()<CR>',\r
+        \ '<Tab>':      ':call <SID>NM_compose_next_entry_area()<CR>',\r
+        \ }\r
+let g:notmuch_compose_imaps =3D {\r
+        \ '<Tab>':      '<C-r>=3D<SID>NM_compose_next_entry_area()<CR>',\r
+        \ }\r
+\r
+" --- implement folders screen {{{1\r
+\r
+" Create the folders buffer.\r
+" Takes a list of [ folder name, query string]\r
+" TODO decorate (help on the first line?)\r
+function! s:NM_cmd_folders(folders)\r
+    call <SID>NM_create_buffer('folders')\r
+    silent 0put!=3D'    Notmuch plugin.'\r
+    python nm_vim.SavedSearches(vim.eval("a:folders"))\r
+    call <SID>NM_finalize_menu_buffer()\r
+    call <SID>NM_set_map('n', g:notmuch_folders_maps)\r
+endfunction\r
+\r
+" Show a folder for each existing tag.\r
+function! s:NM_folders_from_tags()\r
+    let folders =3D []\r
+    python nm_vim.vim_get_tags()\r
+    for tag in split(taglist, '\n')\r
+        call add(folders, [tag, 'tag:' . tag ])\r
+    endfor\r
+\r
+    call <SID>NM_cmd_folders(folders)\r
+endfunction\r
+\r
+" --- --- folders screen action functions {{{2\r
+\r
+" Refresh the folders screen\r
+function! s:NM_folders_refresh_view()\r
+        let lno =3D line('.')\r
+        setlocal modifiable\r
+        silent norm 3GdG\r
+        python nm_vim.get_current_buffer().refresh()\r
+        setlocal nomodifiable\r
+        exec printf('norm %dG', lno)\r
+endfunction\r
+\r
+" Show contents of the folder corresponding to current line AND query\r
+function! s:NM_folders_show_search(query)\r
+    exec printf('python nm_vim.vim_get_object(%d, 0)', line('.'))\r
+    if exists('obj')\r
+        if len(a:query)\r
+            let querystr =3D '(' . obj['id'] . ') and ' . a:query\r
+        else\r
+            let querystr =3D obj['id']\r
+        endif\r
+\r
+        call <SID>NM_cmd_search(querystr, 0)\r
+    endif\r
+endfunction\r
+\r
+" Create the search buffer corresponding to querystr.\r
+" If relative is 1, the search is relative to current buffer\r
+function! s:NM_cmd_search(querystr, relative)\r
+    let cur_buf =3D bufnr('%')\r
+    call <SID>NM_create_buffer('search')\r
+    if a:relative\r
+        exec printf('python nm_vim.Search(querystr =3D "%s", parent =3D nm_=\r
+vim.nm_buffers["%d"])', a:querystr, cur_buf)\r
+    else\r
+        exec printf('python nm_vim.Search(querystr =3D "%s")', a:querystr)\r
+    endif\r
+    call <SID>NM_finalize_menu_buffer()\r
+    call <SID>NM_set_map('n', g:notmuch_search_maps)\r
+endfunction\r
+\r
+" --- --- search screen action functions {{{2\r
+\r
+" Show the thread corresponding to current line\r
+function! s:NM_search_show_thread()\r
+    let querystr =3D <SID>NM_search_thread_id()\r
+    if len(querystr)\r
+        call <SID>NM_cmd_show(querystr)\r
+    endif\r
+endfunction\r
+\r
+" Search according to input from user.\r
+" If edit is 1, current query string is inserted to prompt for editing.\r
+function! s:NM_search_prompt(edit)\r
+    if a:edit\r
+        python nm_vim.vim_get_id()\r
+    else\r
+        let buf_id =3D ''\r
+    endif\r
+    let querystr =3D input('Search: ', buf_id, 'custom,Search_type_completi=\r
+on')\r
+    if len(querystr)\r
+        call <SID>NM_cmd_search(querystr, 0)\r
+    endif\r
+endfunction\r
+\r
+" Filter current search, i.e. search for\r
+" (current querystr) AND (user input)\r
+function! s:NM_search_filter()\r
+    let querystr =3D input('Filter: ', '', 'custom,Search_type_completion')\r
+    if len(querystr)\r
+        call <SID>NM_cmd_search(querystr, 1)\r
+    endif\r
+endfunction\r
+\r
+""""""""""""""""""""""'' TODO\r
+function! s:NM_search_archive_thread()\r
+        call <SID>NM_tag([], ['-inbox'])\r
+        norm j\r
+endfunction\r
+\r
+function! s:NM_search_mark_read_then_archive_thread()\r
+        call <SID>NM_tag([], ['-unread', '-inbox'])\r
+        norm j\r
+endfunction\r
+\r
+function! s:NM_search_delete_thread()\r
+        call <SID>NM_tag([], ['+junk','-inbox','-unread'])\r
+        norm j\r
+endfunction\r
+\r
+"""""""""""""""""""""""""""""""""""""""""""""""""""""\r
+\r
+" XXX This function is broken\r
+function! s:NM_search_toggle_order()\r
+        let g:notmuch_search_newest_first =3D !g:notmuch_search_newest_first\r
+        " FIXME: maybe this would be better done w/o reading re-reading the=\r
+ lines\r
+        "         reversing the b:nm_raw_lines and the buffer lines would b=\r
+e better\r
+        call <SID>NM_search_refresh_view()\r
+endfunction\r
+\r
+"XXX this function is broken\r
+function! s:NM_search_reply_to_thread()\r
+    python vim.command('let querystr =3D "%s"'%nm_vim.get_current_buffer().=\r
+id)\r
+    let cmd =3D ['reply']\r
+    call add(cmd, <SID>NM_search_thread_id())\r
+    call add(cmd, 'AND')\r
+    call extend(cmd, [querystr])\r
+\r
+    let data =3D <SID>NM_run(cmd)\r
+    let lines =3D split(data, "\n")\r
+    call <SID>NM_newComposeBuffer(lines, 0)\r
+endfunction\r
+\r
+function! s:NM_search_add_tags(tags)\r
+        call <SID>NM_search_add_remove_tags('Add Tag(s): ', '+', a:tags)\r
+endfunction\r
+\r
+function! s:NM_search_remove_tags(tags)\r
+        call <SID>NM_search_add_remove_tags('Remove Tag(s): ', '-', a:tags)\r
+endfunction\r
+\r
+function! s:NM_search_refresh_view()\r
+        let lno =3D line('.')\r
+        setlocal modifiable\r
+        norm ggdG\r
+        python nm_vim.get_current_buffer().refresh()\r
+        setlocal nomodifiable\r
+        " FIXME: should find the line of the thread we were on if possible\r
+        exec printf('norm %dG', lno)\r
+endfunction\r
+\r
+" --- --- search screen helper functions {{{2\r
+\r
+function! s:NM_search_thread_id()\r
+    exec printf('python nm_vim.vim_get_object(%d, 0)', line('.'))\r
+    if exists('obj')\r
+        return 'thread:' . obj['id']\r
+    endif\r
+    return ''\r
+endfunction\r
+\r
+function! s:NM_search_add_remove_tags(prompt, prefix, intags)\r
+        if type(a:intags) !=3D type([]) || len(a:intags) =3D=3D 0\r
+                " TODO: input() can support completion\r
+                let text =3D input(a:prompt)\r
+                if !strlen(text)\r
+                        return\r
+                endif\r
+                let tags =3D split(text, ' ')\r
+        else\r
+                let tags =3D a:intags\r
+        endif\r
+        call map(tags, 'a:prefix . v:val')\r
+        call <SID>NM_tag([], tags)\r
+endfunction\r
+\r
+" --- implement show screen {{{1\r
+\r
+function! s:NM_cmd_show(querystr)\r
+    "TODO: folding, syntax\r
+    call <SID>NM_create_buffer('show')\r
+    exec printf('python nm_vim.ShowThread("%s")', a:querystr)\r
+\r
+    call <SID>NM_set_map('n', g:notmuch_show_maps)\r
+    setlocal fillchars=3D\r
+    setlocal foldtext=3DNM_show_foldtext()\r
+    setlocal foldcolumn=3D6\r
+    setlocal foldmethod=3Dsyntax\r
+endfunction\r
+\r
+function! s:NM_jump_message(offset)\r
+    "TODO implement can_change_thread and find_matching, nicer positioning\r
+    exec printf('python nm_vim.vim_get_object(%d, %d)', line('.'), a:offset)\r
+    if exists('obj')\r
+        silent norm zc\r
+        exec printf('norm %dGzt', obj['start'])\r
+        silent norm zo\r
+    endif\r
+endfunction\r
+\r
+function! s:NM_show_next_thread()\r
+        call <SID>NM_kill_this_buffer()\r
+        if line('.') !=3D line('$')\r
+                norm j\r
+                call <SID>NM_search_show_thread()\r
+        else\r
+                echo 'No more messages.'\r
+        endif\r
+endfunction\r
+\r
+function! s:NM_show_archive_thread()\r
+        call <SID>NM_tag('', ['-inbox'])\r
+        call <SID>NM_show_next_thread()\r
+endfunction\r
+\r
+function! s:NM_show_mark_read_then_archive_thread()\r
+        call <SID>NM_tag('', ['-unread', '-inbox'])\r
+        call <SID>NM_show_next_thread()\r
+endfunction\r
+\r
+function! s:NM_show_mark_read_then_next_open_message()\r
+        echo 'not implemented'\r
+endfunction\r
+\r
+function! s:NM_show_previous_message()\r
+        echo 'not implemented'\r
+endfunction\r
+\r
+"XXX pythonise\r
+function! s:NM_show_reply()\r
+    let cmd =3D ['reply']\r
+    call add(cmd, 'id:' . <SID>NM_show_message_id())\r
+\r
+    let data =3D <SID>NM_run(cmd)\r
+    let lines =3D split(data, "\n")\r
+    call <SID>NM_newComposeBuffer(lines, 0)\r
+endfunction\r
+\r
+function! s:NM_show_view_all_mime_parts()\r
+        echo 'not implemented'\r
+endfunction\r
+\r
+function! s:NM_show_view_raw_message()\r
+        echo 'not implemented'\r
+endfunction\r
+\r
+function! s:NM_show_add_tag()\r
+        echo 'not implemented'\r
+endfunction\r
+\r
+function! s:NM_show_remove_tag()\r
+        echo 'not implemented'\r
+endfunction\r
+\r
+function! s:NM_show_advance()\r
+    let advance_tags =3D ['-unread']\r
+\r
+    exec printf('python nm_vim.vim_get_object(%d, 0)', line('.'))\r
+    if !exists('obj')\r
+        return\r
+    endif\r
+\r
+    call <SID>NM_tag(['id:' . obj['id']], advance_tags)\r
+    if obj['end'] =3D=3D line('$')\r
+        call <SID>NM_kill_this_buffer()\r
+    else\r
+        call <SID>NM_jump_message(1)\r
+    endif\r
+endfunction\r
+\r
+function! s:NM_show_pipe_message()\r
+        echo 'not implemented'\r
+endfunction\r
+\r
+function! s:NM_show_view_attachment()\r
+    exec printf('python nm_vim.vim_view_attachment(%d)', line('.'))\r
+endfunction\r
+\r
+" --- --- show screen helper functions {{{2\r
+\r
+function! s:NM_show_message_id()\r
+    exec printf('python nm_vim.vim_get_object(%d, 0)', line('.'))\r
+    if exists('obj')\r
+        return obj['id']\r
+    else\r
+        return ''\r
+endfunction\r
+\r
+" --- implement compose screen {{{1\r
+\r
+function! s:NM_cmd_compose(words, body_lines)\r
+        let lines =3D []\r
+        let start_on_line =3D 0\r
+\r
+        let hdrs =3D { }\r
+\r
+        if !has_key(hdrs, 'From') || !len(hdrs['From'])\r
+                let me =3D <SID>NM_compose_get_user_email()\r
+                let hdrs['From'] =3D [ me ]\r
+        endif\r
+\r
+        for key in g:notmuch_compose_headers\r
+                let text =3D has_key(hdrs, key) ? join(hdrs[key], ', ') : ''\r
+                call add(lines, key . ': ' . text)\r
+                if !start_on_line && !strlen(text)\r
+                        let start_on_line =3D len(lines)\r
+                endif\r
+        endfor\r
+\r
+        for [key,val] in items(hdrs)\r
+                if match(g:notmuch_compose_headers, key) =3D=3D -1\r
+                        let line =3D key . ': ' . join(val, ', ')\r
+                        call add(lines, line)\r
+                endif\r
+        endfor\r
+\r
+        call add(lines, '')\r
+        if !start_on_line\r
+                let start_on_line =3D len(lines) + 1\r
+        endif\r
+\r
+        call extend(lines, [ '', '' ])\r
+\r
+        call <SID>NM_newComposeBuffer(lines, start_on_line)\r
+endfunction\r
+\r
+function! s:NM_compose_send()\r
+    let fname =3D expand('%')\r
+\r
+    try\r
+        python nm_vim.get_current_buffer().send()\r
+        call <SID>NM_kill_this_buffer()\r
+\r
+        call delete(fname)\r
+        echo 'Mail sent successfully.'\r
+    endtry\r
+endfunction\r
+\r
+function! s:NM_compose_attach()\r
+    let attachment =3D input('Enter attachment filename: ', '', 'file')\r
+    if len(attachment)\r
+        exec printf('python nm_vim.get_current_buffer().attach("%s")', atta=\r
+chment)\r
+    endif\r
+endfunction\r
+\r
+function! s:NM_compose_next_entry_area()\r
+        let lnum =3D line('.')\r
+        let hdr_end =3D <SID>NM_compose_find_line_match(1,'^$',1)\r
+        if lnum < hdr_end\r
+                let lnum =3D lnum + 1\r
+                let line =3D getline(lnum)\r
+                if match(line, '^\([^:]\+\):\s*$') =3D=3D -1\r
+                        call cursor(lnum, strlen(line) + 1)\r
+                        return ''\r
+                endif\r
+                while match(getline(lnum+1), '^\s') !=3D -1\r
+                        let lnum =3D lnum + 1\r
+                endwhile\r
+                call cursor(lnum, strlen(getline(lnum)) + 1)\r
+                return ''\r
+\r
+        elseif lnum =3D=3D hdr_end\r
+                call cursor(lnum+1, strlen(getline(lnum+1)) + 1)\r
+                return ''\r
+        endif\r
+        if mode() =3D=3D 'i'\r
+                if !getbufvar(bufnr('.'), '&et')\r
+                        return "\t"\r
+                endif\r
+               let space =3D ''\r
+               let shiftwidth =3D a:shiftwidth\r
+               let shiftwidth =3D shiftwidth - ((virtcol('.')-1) % shiftwidth)\r
+                " we assume no one has shiftwidth set to more than 40 :)\r
+                return '                                        '[0:shiftwi=\r
+dth]\r
+        endif\r
+endfunction\r
+\r
+" --- --- compose screen helper functions {{{2\r
+\r
+function! s:NM_compose_get_user_email()\r
+        " TODO: do this properly (still), i.e., allow for multiple email ac=\r
+counts\r
+        let email =3D substitute(system('notmuch config get user.primary_em=\r
+ail'), '\v(^\s*|\s*$|\n)', '', 'g')\r
+       return email\r
+endfunction\r
+\r
+function! s:NM_compose_find_line_match(start, pattern, failure)\r
+        let lnum =3D a:start\r
+        let lend =3D line('$')\r
+        while lnum < lend\r
+                if match(getline(lnum), a:pattern) !=3D -1\r
+                        return lnum\r
+                endif\r
+                let lnum =3D lnum + 1\r
+        endwhile\r
+        return a:failure\r
+endfunction\r
+\r
+\r
+" --- notmuch helper functions {{{1\r
+function! s:NM_create_buffer(type)\r
+    let prev_bufnr =3D bufnr('%')\r
+\r
+    enew\r
+    setlocal buftype=3Dnofile\r
+    execute printf('set filetype=3Dnotmuch-%s', a:type)\r
+    execute printf('set syntax=3Dnotmuch-%s', a:type)\r
+    "XXX this should probably go\r
+    let b:nm_prev_bufnr =3D prev_bufnr\r
+endfunction\r
+\r
+"set some options for "menu"-like buffers -- folders/searches\r
+function! s:NM_finalize_menu_buffer()\r
+    setlocal nomodifiable\r
+    setlocal cursorline\r
+    setlocal nowrap\r
+endfunction\r
+\r
+function! s:NM_newBuffer(how, type, content)\r
+        if strlen(a:how)\r
+                exec a:how\r
+        else\r
+                enew\r
+        endif\r
+        setlocal buftype=3Dnofile readonly modifiable scrolloff=3D0 sidescr=\r
+olloff=3D0\r
+        silent put=3Da:content\r
+        keepjumps 0d\r
+        setlocal nomodifiable\r
+        execute printf('set filetype=3Dnotmuch-%s', a:type)\r
+        execute printf('set syntax=3Dnotmuch-%s', a:type)\r
+endfunction\r
+\r
+function! s:NM_newFileBuffer(fdir, fname, type, lines)\r
+        let fdir =3D expand(a:fdir)\r
+        if !isdirectory(fdir)\r
+                call mkdir(fdir, 'p')\r
+        endif\r
+        let file_name =3D <SID>NM_mktemp(fdir, a:fname)\r
+        if writefile(a:lines, file_name)\r
+                throw 'Eeek! couldn''t write to temporary file ' . file_name\r
+        endif\r
+        exec printf('edit %s', file_name)\r
+        setlocal buftype=3D noreadonly modifiable scrolloff=3D0 sidescrollo=\r
+ff=3D0\r
+        execute printf('set filetype=3Dnotmuch-%s', a:type)\r
+        execute printf('set syntax=3Dnotmuch-%s', a:type)\r
+endfunction\r
+\r
+function! s:NM_newComposeBuffer(lines, start_on_line)\r
+        let lines =3D a:lines\r
+        let start_on_line =3D a:start_on_line\r
+        let real_hdr_start =3D 1\r
+        if g:notmuch_compose_header_help\r
+                let help_lines =3D [\r
+                  \ 'Notmuch-Help: Type in your message here; to help you u=\r
+se these bindings:',\r
+                  \ 'Notmuch-Help:   ,a    - attach a file',\r
+                  \ 'Notmuch-Help:   ,s    - send the message (Notmuch-Help=\r
+ lines will be removed)',\r
+                  \ 'Notmuch-Help:   ,q    - abort the message',\r
+                  \ 'Notmuch-Help:   <Tab> - skip through header lines',\r
+                  \ ]\r
+                call extend(lines, help_lines, 0)\r
+                let real_hdr_start =3D len(help_lines)\r
+                if start_on_line > 0\r
+                        let start_on_line =3D start_on_line + len(help_line=\r
+s)\r
+                endif\r
+        endif\r
+        if exists('g:notmuch_signature')\r
+                call extend(lines, ['', '--'])\r
+                call extend(lines, g:notmuch_signature)\r
+        endif\r
+\r
+\r
+        let prev_bufnr =3D bufnr('%')\r
+        call <SID>NM_newFileBuffer(g:notmuch_compose_temp_file_dir, '%s.mai=\r
+l',\r
+                                  \ 'compose', lines)\r
+        let b:nm_prev_bufnr =3D prev_bufnr\r
+\r
+        call <SID>NM_set_map('n', g:notmuch_compose_nmaps)\r
+        call <SID>NM_set_map('i', g:notmuch_compose_imaps)\r
+\r
+        if start_on_line > 0 && start_on_line <=3D len(lines)\r
+                call cursor(start_on_line, strlen(getline(start_on_line)) +=\r
+ 1)\r
+        else\r
+                call cursor(real_hdr_start, strlen(getline(real_hdr_start))=\r
+ + 1)\r
+                call <SID>NM_compose_next_entry_area()\r
+        endif\r
+\r
+        if g:notmuch_compose_insert_mode_start\r
+                startinsert!\r
+        endif\r
+\r
+        python nm_vim.Compose()\r
+endfunction\r
+\r
+function! s:NM_mktemp(dir, name)\r
+        let time_stamp =3D strftime('%Y%m%d-%H%M%S')\r
+        let file_name =3D substitute(a:dir,'/*$','/','') . printf(a:name, t=\r
+ime_stamp)\r
+        " TODO: check if it exists, try again\r
+        return file_name\r
+endfunction\r
+\r
+function! s:NM_shell_escape(word)\r
+        " TODO: use shellescape()\r
+        let word =3D substitute(a:word, '''', '\\''', 'g')\r
+        return '''' . word . ''''\r
+endfunction\r
+\r
+function! s:NM_run(args)\r
+    let words =3D a:args\r
+    call map(words, 's:NM_shell_escape(v:val)')\r
+    let cmd =3D g:notmuch_cmd . ' ' . join(words) . '< /dev/null'\r
+\r
+    let out =3D system(cmd)\r
+    let err =3D v:shell_error\r
+\r
+    if err\r
+        echohl Error\r
+        echo substitute(out, '\n*$', '', '')\r
+        echohl None\r
+        return ''\r
+    else\r
+        return out\r
+    endif\r
+endfunction\r
+\r
+" --- external mail handling helpers {{{1\r
+\r
+function! s:NM_new_mail()\r
+        call <SID>NM_cmd_compose([], [])\r
+endfunction\r
+\r
+" --- tag manipulation helpers {{{1\r
+\r
+" used to combine an array of words with prefixes and separators\r
+" example:\r
+"     NM_combine_tags('tag:', ['one', 'two', 'three'], 'OR', '()')\r
+"  -> ['(', 'tag:one', 'OR', 'tag:two', 'OR', 'tag:three', ')']\r
+function! s:NM_combine_tags(word_prefix, words, separator, brackets)\r
+        let res =3D []\r
+        for word in a:words\r
+                if len(res) && strlen(a:separator)\r
+                        call add(res, a:separator)\r
+                endif\r
+                call add(res, a:word_prefix . word)\r
+        endfor\r
+        if len(res) > 1 && strlen(a:brackets)\r
+                if strlen(a:brackets) !=3D 2\r
+                        throw 'Eeek! brackets arg to NM_combine_tags must b=\r
+e 2 chars'\r
+                endif\r
+                call insert(res, a:brackets[0])\r
+                call add(res, a:brackets[1])\r
+        endif\r
+        return res\r
+endfunction\r
+\r
+" --- other helpers {{{1\r
+\r
+function! s:NM_kill_this_buffer()\r
+    let prev_bufnr =3D b:nm_prev_bufnr\r
+    python nm_vim.delete_current_buffer()\r
+    bdelete!\r
+    exec printf("buffer %d", prev_bufnr)\r
+endfunction\r
+\r
+function! s:NM_search_expand(arg)\r
+        let word =3D expand(a:arg)\r
+        let prev_bufnr =3D bufnr('%')\r
+        call <SID>NM_cmd_search(word, 0)\r
+        let b:nm_prev_bufnr =3D prev_bufnr\r
+endfunction\r
+\r
+function! s:NM_tag(filter, tags)\r
+    let filter =3D len(a:filter) ? a:filter : [<SID>NM_search_thread_id()]\r
+    if !len(filter)\r
+        throw 'Eeek! I couldn''t find the thead id!'\r
+    endif\r
+    exec printf('python nm_vim.get_current_buffer().tag(tags =3D vim.eval("=\r
+a:tags"), querystr =3D "%s")', join(filter))\r
+endfunction\r
+\r
+" --- process and set the defaults {{{1\r
+\r
+function! NM_set_defaults(force)\r
+    setlocal bufhidden=3Dhide\r
+    for [key, dflt] in items(s:notmuch_defaults)\r
+        let cmd =3D ''\r
+        if !a:force && exists(key) && type(dflt) =3D=3D type(eval(key))\r
+            continue\r
+        elseif type(dflt) =3D=3D type(0)\r
+            let cmd =3D printf('let %s =3D %d', key, dflt)\r
+        elseif type(dflt) =3D=3D type('')\r
+            let cmd =3D printf('let %s =3D ''%s''', key, dflt)\r
+            " FIXME: not sure why this didn't work when dflt is an array\r
+            "elseif type(dflt) =3D=3D type([])\r
+            "        let cmd =3D printf('let %s =3D %s', key, string(dflt))\r
+        else\r
+            echoe printf('E: Unknown type in NM_set_defaults(%d) using [%s,=\r
+%s]',\r
+                        \ a:force, key, string(dflt))\r
+            continue\r
+        endif\r
+        exec cmd\r
+    endfor\r
+endfunction\r
+call NM_set_defaults(0)\r
+\r
+" for some reason NM_set_defaults() didn't work for arrays...\r
+if !exists('g:notmuch_folders')\r
+    let g:notmuch_folders =3D s:notmuch_folders_defaults\r
+endif\r
+\r
+if !exists('g:notmuch_show_headers')\r
+    let g:notmuch_show_headers =3D s:notmuch_show_headers_defaults\r
+endif\r
+\r
+if !exists('g:notmuch_signature')\r
+        if filereadable(glob('~/.signature'))\r
+            let g:notmuch_signature =3D readfile(glob('~/.signature'))\r
+        endif\r
+endif\r
+if !exists('g:notmuch_compose_headers')\r
+        let g:notmuch_compose_headers =3D s:notmuch_compose_headers_defaults\r
+endif\r
+\r
+" --- assign keymaps {{{1\r
+\r
+function! s:NM_set_map(type, maps)\r
+        for [key, code] in items(a:maps)\r
+                exec printf('%snoremap <buffer> %s %s', a:type, key, code)\r
+        endfor\r
+endfunction\r
+\r
+" --- command handler {{{1\r
+\r
+function! NotMuch()\r
+    if !exists('s:notmuch_inited')\r
+        " init the python layer\r
+        python import sys\r
+        exec "python sys.path +=3D [r'" . s:python_path . "']"\r
+        python import vim, nm_vim\r
+\r
+        let s:notmuch_inited =3D 1\r
+    endif\r
+\r
+    call <SID>NM_cmd_folders(g:notmuch_folders)\r
+endfunction\r
+\r
+"Custom foldtext() for show buffers, which indents folds to\r
+"represent thread structure\r
+function! NM_show_foldtext()\r
+    if v:foldlevel !=3D 1\r
+        return foldtext()\r
+    endif\r
+    let numlines =3D v:foldend - v:foldstart + 1\r
+    let indentlevel =3D matchstr(getline(v:foldstart), '^[0-9]\+')\r
+    return repeat('  ', indentlevel) . getline(v:foldstart + 1)\r
+endfunction\r
+\r
+"Completion of search prompt\r
+"TODO properly deal with complex queries\r
+function! Search_type_completion(arg_lead, cmd_line, cursor_pos)\r
+    let idx =3D stridx(a:arg_lead, ':')\r
+    if idx < 0\r
+        return 'from:' .       "\n" .\r
+             \ 'to:' .         "\n" .\r
+             \ 'subject:' .    "\n" .\r
+             \ 'attachment:' . "\n" .\r
+             \ 'tag:' .        "\n" .\r
+             \ 'id:' .         "\n" .\r
+             \ 'thread:' .     "\n" .\r
+             \ 'folder:'\r
+    endif\r
+    if stridx(a:arg_lead, 'tag:') >=3D 0\r
+        python nm_vim.vim_get_tags()\r
+        return 'tag:' . substitute(taglist, "\n", "\ntag:", "g")\r
+    endif\r
+    return ''\r
+endfunction\r
+\r
+" --- glue {{{1\r
+\r
+command! NotMuch call NotMuch()\r
+let s:python_path =3D expand('<sfile>:p:h')\r
+\r
+--===============0474093823==\r
+Content-Type: text/x-python; charset="utf-8"\r
+MIME-Version: 1.0\r
+Content-Transfer-Encoding: quoted-printable\r
+Content-Disposition: attachment; filename="nm_vim.py"\r
+\r
+#!/usr/bin/python\r
+#\r
+# This file is part of Notmuch.\r
+#\r
+# Notmuch is free software: you can redistribute it and/or modify it\r
+# under the terms of the GNU General Public License as published by\r
+# the Free Software Foundation, either version 3 of the License, or\r
+# (at your option) any later version.\r
+#\r
+# Notmuch is distributed in the hope that it will be useful, but\r
+# WITHOUT ANY WARRANTY; without even the implied warranty of\r
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU\r
+# General Public License for more details.\r
+#\r
+# You should have received a copy of the GNU General Public License\r
+# along with Notmuch.  If not, see <http://www.gnu.org/licenses/>.\r
+#\r
+\r
+# python-notmuch wrapper for the notmuch vim client\r
+\r
+import datetime\r
+import email, email.header, email.charset, email.utils, email.message\r
+import email.mime.text, email.mime.audio, email.mime.image, email.mime.mult=\r
+ipart\r
+import mailbox\r
+import mailcap\r
+import mimetypes\r
+import notmuch\r
+import os, os.path\r
+import shlex\r
+import smtplib\r
+import subprocess\r
+import tempfile\r
+import vim\r
+\r
+#### classes ####\r
+\r
+class NMBuffer(object):\r
+    """\r
+    An object mapping line ranges in current buffer to notmuch structures.\r
+    """\r
+\r
+    # a string identifying the buffer\r
+    id =3D None\r
+\r
+    # a list of NMBufferElement subclasses representing objects in current =\r
+buffer\r
+    objects =3D None\r
+\r
+    def __new__(cls, *args, **kwargs):\r
+        bufnr =3D vim.eval('bufnr("%")')\r
+        if bufnr in nm_buffers:\r
+            return nm_buffers[bufnr]\r
+        ret =3D object.__new__(cls)\r
+        nm_buffers[bufnr] =3D ret\r
+        return ret\r
+\r
+    def __init__(self):\r
+        self.objects =3D []\r
+\r
+    def get_object(self, line, offset):\r
+        """\r
+        Get an object that's offset objects away from given line or None.\r
+        E.g. offset =3D 0 gets the object on the line, offset =3D 1 gets\r
+        next, offset =3D -1 previous.\r
+        """\r
+        if not self.objects or line < self.objects[0].start or line > self.=\r
+objects[-1].end:\r
+            return\r
+\r
+        for i in xrange(len(self.objects)):\r
+            obj =3D self.objects[i]\r
+            if line >=3D obj.start and line <=3D obj.end:\r
+                return self.objects[ max(min(len(self.objects) - 1, i + off=\r
+set), 0) ]\r
+\r
+    def tag(self, tags, querystr =3D None):\r
+        if querystr:\r
+            querystr =3D '( %s ) and ( %s )'%(self.id, querystr)\r
+        else:\r
+            querystr =3D self.id\r
+\r
+        db =3D notmuch.Database(mode =3D notmuch.Database.MODE.READ_WRITE)\r
+        map_query(db, querystr, lambda m, l: self._tag_message(m, tags))\r
+\r
+    def _tag_message(self, message, tags):\r
+        for tag in tags:\r
+            if tag[0] =3D=3D '+':\r
+                message.add_tag(tag[1:])\r
+            elif tag[0] =3D=3D '-':\r
+                message.remove_tag(tag[1:])\r
+\r
+class SavedSearches(NMBuffer):\r
+    """\r
+    This buffer displays a list of saved searches ('folders').\r
+    """\r
+\r
+    def __init__(self, folders):\r
+        """\r
+        @param folders A list of (folder name, query string) tuples.\r
+        """\r
+        super(SavedSearches, self).__init__()\r
+        for folder in folders:\r
+            self.objects.append(Folder(0, 0, folder[1], folder[0]))\r
+        self.refresh()\r
+\r
+    def refresh(self):\r
+        b =3D vim.current.buffer\r
+        db =3D notmuch.Database()\r
+        for obj in self.objects:\r
+            q  =3D db.create_query(obj.id)\r
+            q1 =3D db.create_query('( %s ) and tag:unread'%(obj.id))\r
+            b.append('{0:>7} {1:7} {2: <30s} ({3})'.format(q.count_messages=\r
+(), '({0})'.format(q1.count_messages()),\r
+                                                           obj.name, obj.id=\r
+))\r
+            obj.start =3D obj.end =3D len(b) - 1\r
+\r
+    def __repr__(self):\r
+        return '<Vim-notmuch saved searches buffer.>'\r
+\r
+    def tag(self):\r
+        raise TypeError('Attempted to tag in folders view.')\r
+\r
+class Search(NMBuffer):\r
+    """\r
+    This buffer displays results of a db search -- a list of found threads.\r
+    """\r
+\r
+    def __init__(self, querystr =3D '', parent =3D None):\r
+        super(Search, self).__init__()\r
+\r
+        assert(querystr or parent)\r
+\r
+        # FIXME simplify\r
+        if parent:\r
+            self.id =3D parent.id\r
+        if querystr:\r
+            if self.id:\r
+                self.id =3D '( %s ) and ( %s )'%(self.id, querystr)\r
+            else:\r
+                self.id =3D querystr\r
+\r
+        self.refresh()\r
+\r
+    def refresh(self):\r
+        b  =3D vim.current.buffer\r
+        self.objects =3D []\r
+        db =3D notmuch.Database()\r
+        q  =3D db.create_query(self.id)\r
+        q.set_sort(q.SORT.NEWEST_FIRST)     # FIXME allow different search =\r
+orders\r
+        for t in q.search_threads():\r
+            start =3D len(b)\r
+            # we have to decode the unicode strings to get the alignment ri=\r
+ght\r
+            datestr =3D get_relative_date(t.get_newest_date())\r
+            authors =3D t.get_authors().decode('utf-8')\r
+            subj    =3D t.get_subject().decode('utf-8')\r
+            tags    =3D str(t.get_tags()).decode('utf-8')\r
+            b.append((u'%-12s %3s/%3s %-20.20s | %s (%s)'%(datestr, t.get_m=\r
+atched_messages(), t.get_total_messages(),\r
+                                                           authors, subj, t=\r
+ags)).encode('utf-8'))\r
+            self.objects.append(NMBufferElement(start, len(b) - 1, t.get_th=\r
+read_id()))\r
+\r
+    def __repr__(self):\r
+        return '<Vim-notmuch search buffer: %s>'%(self.id)\r
+\r
+class ShowThread(NMBuffer):\r
+    """\r
+    This buffer represents a thread view.\r
+    """\r
+\r
+    # a list of temporary files for viewing attachments\r
+    # they will be automagically closed and deleted on this object's demise\r
+    _tmpfiles =3D None\r
+\r
+    # a list of headers to show\r
+    _headers =3D None\r
+\r
+    def __init__(self, querystr):\r
+        self.id =3D querystr\r
+        self._tmpfiles =3D []\r
+        self.refresh()\r
+\r
+    def refresh(self):\r
+        self._headers =3D vim.eval('g:notmuch_show_headers')\r
+        self.objects =3D []\r
+        db          =3D notmuch.Database()\r
+        map_query(db, self.id, self._print_message)\r
+\r
+    def view_attachment(self, line):\r
+        """\r
+        View attachment corresponding to given line.\r
+        """\r
+        message =3D self.get_object(line, 0)\r
+        if not message:\r
+            print 'No message on this line.'\r
+            return\r
+\r
+        data =3D None\r
+        for a in message.attachments:\r
+            if line =3D=3D a.start:\r
+                data =3D a.data\r
+                break\r
+        if not data:\r
+            return\r
+\r
+        f =3D tempfile.NamedTemporaryFile()\r
+        f.write(data.get_payload(decode =3D True))\r
+        f.flush()\r
+        os.fsync(f.fileno())\r
+\r
+        caps =3D mailcap.getcaps()\r
+        ret  =3D mailcap.findmatch(caps, data.get_content_type(), filename =\r
+=3D f.name)\r
+        if ret[0]:\r
+            with open(os.devnull, 'w') as null:\r
+                subprocess.Popen(shlex.split(ret[0]), stderr =3D null, stdo=\r
+ut =3D null)\r
+        self._tmpfiles.append(f)\r
+\r
+    def _read_text_payload(self, part):\r
+        """\r
+        Try converting the payload of the MIME part into utf-8.\r
+        """\r
+        p  =3D part.get_payload(decode =3D True)\r
+        ch =3D part.get_content_charset('utf-8')\r
+        if ch !=3D 'utf-8':\r
+            try:\r
+                p =3D p.decode(ch).encode('utf-8')\r
+            except LookupError:\r
+                # if the encoding is unknown, try utf-8\r
+                try:\r
+                    p.decode('utf-8')\r
+                except UnicodeDecodeError:\r
+                    return 'Unknown encoding: %s, cannot decode message'%ch\r
+        return p\r
+\r
+    def _print_part(self, part):\r
+        """\r
+        Walk through the part recursively and print it\r
+        and its subparts to current buffer.\r
+        """\r
+        if part.is_multipart():\r
+            if part.get_content_subtype() =3D=3D 'alternative':\r
+                # try to find the plaintext version, if that fails just try=\r
+ to print the first\r
+                for subpart in part.get_payload():\r
+                    if subpart.get_content_type() =3D=3D 'text/plain':\r
+                        return self._print_part(subpart)\r
+                self._print_part(part.get_payload()[0])\r
+            else:\r
+                for subpart in part.get_payload():\r
+                    self._print_part(subpart)\r
+        else:\r
+            b =3D vim.current.buffer\r
+            if part.get('Content-Disposition', '').lower().startswith('atta=\r
+chment'):\r
+                self.objects[-1].attachments.append(Attachment(len(b), len(=\r
+b), part))\r
+                b.append(('[ Attachment: %s (%s)]'%(part.get_filename(), pa=\r
+rt.get_content_type())).split('\n'))\r
+\r
+            if part.get_content_maintype() =3D=3D 'text':\r
+                p =3D self._read_text_payload(part)\r
+                b.append(p.split('\n'))\r
+\r
+    def _print_message(self, message, level):\r
+        b     =3D vim.current.buffer\r
+        msg =3D Message(len(b), 0, message.get_message_id())\r
+        self.objects.append(msg)\r
+\r
+        fp        =3D open(message.get_filename())\r
+        email_msg =3D email.message_from_file(fp)\r
+        fp.close()\r
+\r
+        # print the title\r
+        b.append('%d/'%level + 20*'-' + 'message start' + 20*'-' + '\\')\r
+        b.append('%s: %s (%s) (%s)'%(get_author(message.get_header('from'))=\r
+, message.get_header('subject'),\r
+                                 get_relative_date(message.get_date()), ' '=\r
+.join(message.get_tags())))\r
+\r
+        # print the headers\r
+        # TODO toggle all (like in mutt)\r
+        for header in self._headers:\r
+            if header in email_msg:\r
+                b.append(('%s: %s'%(header, decode_header(email_msg[header]=\r
+))).split('\n'))\r
+        b.append('')\r
+\r
+        self._print_part(email_msg)\r
+\r
+        b.append('\\' + 20*'-' + 'message end' + 20*'-' + '/')\r
+        msg.end =3D len(b) - 1\r
+\r
+    def __repr__(self):\r
+        return '<Vim-notmuch thread buffer: %s.>'%self.id\r
+\r
+class Compose(NMBuffer):\r
+\r
+    _attachment_prefix =3D 'Notmuch-Attachment: '\r
+\r
+    def __init__(self):\r
+        super(Compose, self).__init__()\r
+        # python wants to use base64 for some reason, force quoted-printable\r
+        email.charset.add_charset('utf-8', email.charset.QP, email.charset.=\r
+QP, 'utf-8')\r
+\r
+    def _encode_header(self, text):\r
+        try:\r
+            text.decode('ascii')\r
+            return text\r
+        except UnicodeDecodeError:\r
+            return email.header.Header(text, 'utf-8').encode()\r
+\r
+    def attach(self, filename):\r
+        type, encoding =3D mimetypes.guess_type(filename)\r
+        if encoding or not type:\r
+            type =3D 'application/octet-stream'\r
+        vim.current.buffer.append('%s%s:%s'%(self._attachment_prefix, filen=\r
+ame, type), 0)\r
+\r
+    def send(self):\r
+        """\r
+        Send the message in current buffer.\r
+        """\r
+        b =3D vim.current.buffer\r
+        i =3D 0\r
+\r
+        # parse attachments\r
+        attachments =3D []\r
+        while b[i].startswith(self._attachment_prefix):\r
+            filename, sep, type =3D b[i][len(self._attachment_prefix):].rpa=\r
+rtition(':')\r
+            attachments.append((os.path.expanduser(filename), type))\r
+            i +=3D 1\r
+        # skip the inline help\r
+        while b[i].startswith('Notmuch-Help:'):\r
+            i +=3D 1\r
+\r
+        # add the headers\r
+        headers    =3D {}\r
+        recipients =3D []\r
+        from_addr  =3D None\r
+        while i < len(b):\r
+            if not b[i]:\r
+                break\r
+            key, sep, val =3D b[i].partition(':')\r
+            i +=3D 1\r
+\r
+            try:\r
+                key.decode('ascii')\r
+            except UnicodeDecodeError:\r
+                raise ValueError('Header name must be ASCII only.')\r
+\r
+            if not val.strip():\r
+                # skip empty headers\r
+                continue\r
+\r
+            if key.lower() in ('to', 'cc', 'bcc'):\r
+                names, addrs =3D zip(*email.utils.getaddresses([val]))\r
+                names =3D map(self._encode_header, names)\r
+\r
+                recipients +=3D addrs\r
+                if key.lower() =3D=3D 'bcc':\r
+                    continue\r
+                val =3D ','.join(map(email.utils.formataddr, zip(names, add=\r
+rs)))\r
+            else:\r
+                if key.lower() =3D=3D 'from':\r
+                    from_addr =3D email.utils.parseaddr(val)[1]\r
+                val =3D self._encode_header(val)\r
+\r
+            headers[key] =3D val\r
+\r
+        body =3D email.mime.text.MIMEText('\n'.join(b[i:]), 'plain', 'utf-8=\r
+')\r
+        # add the body\r
+        if not attachments:\r
+            msg =3D body\r
+        else:\r
+            msg  =3D email.mime.multipart.MIMEMultipart()\r
+            msg.attach(body)\r
+            for attachment in attachments:\r
+                maintype, subtype =3D attachment[1].split('/', 1)\r
+\r
+                if maintype =3D=3D 'text':\r
+                    with open(attachment[0]) as f:\r
+                        part =3D email.mime.text.MIMEText(f.read(), subtype=\r
+, 'utf-8')\r
+                else:\r
+                    if maintype =3D=3D 'image':\r
+                        obj =3D email.mime.image.MIMEImage\r
+                    elif maintype =3D=3D 'audio':\r
+                        obj =3D email.mime.audio.MIMEAudio\r
+                    else:\r
+                        obj =3D email.mime.application.MIMEApplication\r
+\r
+                    with open(attachment[0]) as f:\r
+                        part =3D obj(f.read(), subtype)\r
+\r
+                part.add_header('Content-Disposition', 'attachment',\r
+                                filename =3D os.path.basename(attachment[0]=\r
+))\r
+                msg.attach(part)\r
+\r
+        msg['User-Agent'] =3D 'Notmuch-vim EXPERIMENTAL'\r
+        msg['Message-ID'] =3D email.utils.make_msgid()\r
+        msg['Date']       =3D email.utils.formatdate(localtime =3D True)\r
+        for key in headers:\r
+            msg[key] =3D headers[key]\r
+\r
+        # sanity checks\r
+        if not from_addr:\r
+            # XXX notmuch-python should export the user email address\r
+            raise ValueError('No sender address specified.')\r
+        if not recipients:\r
+            raise ValueError('No recipient specified.')\r
+\r
+        # send\r
+        fcc =3D vim.eval('g:notmuch_fcc_maildir')\r
+        if fcc:\r
+            dbroot =3D notmuch.Database().get_path()\r
+            mdir   =3D mailbox.Maildir(os.path.join(dbroot, fcc))\r
+            mdir.add(msg)\r
+            mdir.close()\r
+        # TODO configurable host\r
+        s =3D smtplib.SMTP('localhost')\r
+        ret =3D s.sendmail(from_addr, recipients, msg.as_string())\r
+        for key in ret:\r
+            print 'Error sending mail to %s: %s'%(key, ret[key])\r
+        s.quit()\r
+\r
+class NMBufferElement(object):\r
+    """\r
+    This object represents a structure (e.g. folder, thread or message)\r
+    corresponding to a range of lines in NMBuffer.\r
+    """\r
+    # start and end lines, 0-based\r
+    start =3D None\r
+    end   =3D None\r
+    # a string identifying the element\r
+    id    =3D None\r
+\r
+    def __init__(self, start, end, id):\r
+        self.start =3D start\r
+        self.end   =3D end\r
+        self.id    =3D id\r
+\r
+class Folder(NMBufferElement):\r
+    """\r
+    A saved search / 'folder'.\r
+    """\r
+    # folder name\r
+    name =3D None\r
+\r
+    def __init__(self, start, end, id, name):\r
+        super(Folder, self).__init__(start, end, id)\r
+        self.name =3D name\r
+\r
+class Message(NMBufferElement):\r
+\r
+    attachments =3D None\r
+\r
+    def __init__(self, start, end, id):\r
+        super(Message, self).__init__(start, end, id)\r
+        self.attachments =3D []\r
+\r
+class Attachment(NMBufferElement):\r
+    data =3D None\r
+\r
+    def __init__(self, start, end, data):\r
+        super(Attachment, self).__init__(start, end, '')\r
+        self. data =3D data\r
+\r
+#### global variables ####\r
+# this dictionary stores the Python objects corresponding to notmuch-managed\r
+# buffers, each indexed by the buffer number\r
+nm_buffers =3D {}\r
+\r
+#### utility functions ####\r
+\r
+def get_current_buffer():\r
+    """\r
+    Get the NMBuffer object associated with current buffer or None.\r
+    """\r
+    try:\r
+        return nm_buffers[vim.eval('bufnr("%")')]\r
+    except KeyError:\r
+        return None\r
+\r
+def delete_current_buffer():\r
+    """\r
+    Delete the NMBuffer associated with current buffer.\r
+    """\r
+    del nm_buffers[vim.eval('bufnr("%")')]\r
+\r
+def get_relative_date(timestamp):\r
+    """\r
+    Format a nice representation of 'time' relative to the current time.\r
+\r
+     Examples include:\r
+\r
+        5 mins. ago    (For times less than 60 minutes ago)\r
+        Today 12:30    (For times >60 minutes but still today)\r
+        Yest. 12:30\r
+        Mon.  12:30    (Before yesterday but fewer than 7 days ago)\r
+        October 12     (Between 7 and 180 days ago (about 6 months))\r
+        2008-06-30     (More than 180 days ago)\r
+\r
+     Shamelessly lifted from notmuch-time\r
+     TODO: this should probably be in python-notmuch\r
+     """\r
+    try:\r
+        now  =3D datetime.datetime.now()\r
+        then =3D datetime.datetime.fromtimestamp(timestamp)\r
+    except ValueError:\r
+        return "when?"\r
+\r
+\r
+    if then > now:\r
+        return "the future"\r
+\r
+    delta =3D now - then\r
+\r
+    if delta.days > 180:\r
+        return then.strftime("%F") # 2008-06-30\r
+\r
+    total_seconds =3D delta.seconds + delta.days * 24 * 3600\r
+    if total_seconds  < 3600:\r
+        return "%d min. ago"%(total_seconds / 60)\r
+\r
+    if delta.days < 7:\r
+        if then.day =3D=3D now.day:\r
+            return then.strftime("Today %R") # Today 12:30\r
+        if then.day + 1 =3D=3D now.day:\r
+            return then.strftime("Yest. %R") # Yest. 12:30\r
+        return then.strftime("%a. %R")   # Mon. 12:30\r
+\r
+    return then.strftime("%B %d")        # October 12\r
+\r
+def map_query(db, query, run):\r
+    """\r
+    Execute runnable run on every message found for the specified query.\r
+    """\r
+    def walk_query(message, run, level):\r
+        run(message, level)\r
+        level +=3D 1\r
+        replies =3D message.get_replies()\r
+        if replies is not None:\r
+            for reply in replies:\r
+                walk_query(reply, run, level)\r
+\r
+    q =3D db.create_query(query)\r
+    for t in q.search_threads():\r
+        for m in t.get_toplevel_messages():\r
+            walk_query(m, run, 0)\r
+\r
+def decode_header(header):\r
+    """\r
+    Decode a RFC 2822 header into a utf-8 string.\r
+    """\r
+    ret =3D []\r
+    for part in email.header.decode_header(header):\r
+        if part[1]:\r
+            ret.append(part[0].decode(part[1]).encode('utf-8'))\r
+        else:\r
+            ret.append(part[0])\r
+    return ''.join(ret)\r
+\r
+def get_author(address):\r
+    """\r
+    Extract the author name from an address if possible.\r
+    """\r
+    #XXX should be in the bindings\r
+    if address.endswith('>'):\r
+        try:\r
+            ret =3D address[:address.rindex('<') - 1].strip()\r
+            if ret:\r
+                return ret\r
+        except ValueError:\r
+            pass\r
+    return address\r
+\r
+#### functions for exporting stuff to viml ####\r
+\r
+def vim_get_tags():\r
+    """\r
+    Export a string listing all tags in the database, one per line into\r
+    a vim variable named 'taglist'.\r
+    """\r
+    db =3D notmuch.Database()\r
+    tags =3D '\n'.join(db.get_all_tags())\r
+    vim.command('let taglist =3D \'%s\''%tags)\r
+\r
+def vim_get_object(line, offset):\r
+    """\r
+    Export start/end lines and id of the object that's offset objects\r
+    aways from given line into a dict named 'obj' in viml.\r
+    E.g. offset =3D 0 gets the object on the line, offset =3D 1 gets\r
+    next, offset =3D -1 previous.\r
+    This method is a noop if current line doesn't correspond to anything.\r
+    """\r
+    # vim lines are 1-based\r
+    line -=3D 1\r
+    assert line >=3D 0\r
+\r
+    obj =3D get_current_buffer().get_object(line, offset)\r
+    if obj:\r
+        vim.command('let obj =3D { "start" : %d, "end" : %d, "id" : "%s" }'=\r
+%(\r
+                    obj.start + 1, obj.end + 1, obj.id))\r
+\r
+def vim_get_id():\r
+    """\r
+    Export the id string of current buffer into a vim string named 'buf_id'.\r
+    """\r
+    id =3D get_current_buffer().id\r
+    vim.command('let buf_id =3D "%s"'%(id if id else ''))\r
+\r
+def vim_view_attachment(line):\r
+    """\r
+    View an attachment corresponding to the given vim line\r
+    in an external viewer.\r
+    """\r
+    line -=3D 1       # vim lines are 1-based\r
+    get_current_buffer().view_attachment(line)\r
+\r
+--===============0474093823==\r
+Content-Type: text/plain; charset="utf-8"\r
+MIME-Version: 1.0\r
+Content-Transfer-Encoding: quoted-printable\r
+Content-Disposition: attachment; filename="notmuch-folders.vim"\r
+\r
+" notmuch folders mode syntax file\r
+\r
+syntax region nmFolfers             start=3D/^/ end=3D/$/         oneline c=\r
+ontains=3DnmFoldersMessageCount\r
+syntax match  nmFoldersMessageCount /^ *[0-9]\+ */            contained nex=\r
+tgroup=3DnmFoldersUnreadCount\r
+syntax match  nmFoldersUnreadCount  /(.\{-}) */               contained nex=\r
+tgroup=3DnmFoldersName\r
+syntax match  nmFoldersName         /.*\ze(/                  contained nex=\r
+tgroup=3DnmFoldersSearch\r
+syntax match  nmFoldersSearch       /([^()]\+)$/\r
+\r
+highlight link nmFoldersMessageCount Statement\r
+highlight link nmFoldersUnreadCount  Underlined\r
+highlight link nmFoldersName         Type\r
+highlight link nmFoldersSearch       String\r
+\r
+highlight CursorLine term=3Dreverse cterm=3Dreverse gui=3Dreverse\r
+\r
+\r
+--===============0474093823==\r
+Content-Type: text/plain; charset="utf-8"\r
+MIME-Version: 1.0\r
+Content-Transfer-Encoding: quoted-printable\r
+Content-Disposition: attachment; filename="notmuch-search.vim"\r
+\r
+syntax region nmSearch          start=3D/^/ end=3D/$/       oneline contain=\r
+s=3DnmSearchDate keepend\r
+syntax match nmSearchDate       /^.\{-13}/              contained nextgroup=\r
+=3DnmSearchNum skipwhite\r
+syntax match nmSearchNum        "[0-9]\+\/"             contained nextgroup=\r
+=3DnmSearchTotal skipwhite\r
+syntax match nmSearchTotal      /[0-9]\+/               contained nextgroup=\r
+=3DnmSearchFrom skipwhite\r
+syntax match nmSearchFrom       /.\{-}\ze|/             contained nextgroup=\r
+=3DnmSearchSubject skipwhite\r
+"XXX this fails on some messages with multiple authors\r
+syntax match nmSearchSubject    /.*\ze(/                contained nextgroup=\r
+=3DnmSearchTags\r
+syntax match nmSearchTags       /.\+$/                  contained\r
+\r
+syntax match nmUnread           /^.*(.*\<unread\>.*)$/\r
+\r
+highlight link nmSearchDate    Statement\r
+highlight link nmSearchNum     Number\r
+highlight link nmSearchTotal   Type\r
+highlight link nmSearchFrom    Include\r
+highlight link nmSearchSubject Normal\r
+highlight link nmSearchTags    String\r
+\r
+highlight link nmUnread        Underlined\r
+\r
+--===============0474093823==\r
+Content-Type: text/plain; charset="utf-8"\r
+MIME-Version: 1.0\r
+Content-Transfer-Encoding: quoted-printable\r
+Content-Disposition: attachment; filename="notmuch-show.vim"\r
+\r
+" notmuch show mode syntax file\r
+\r
+setlocal conceallevel=3D2\r
+setlocal concealcursor=3Dvinc\r
+\r
+syntax region  nmMessage     matchgroup=3DIgnore concealends start=3D'[0-9]=\r
+\+\/-*message start-*\\' end=3D'\\-*message end-*\/' fold contains=3D@nmSho=\r
+wMsgBody keepend\r
+\r
+"TODO what about those\r
+syntax cluster nmShowMsgDesc contains=3DnmShowMsgDescWho,nmShowMsgDescDate,=\r
+nmShowMsgDescTags\r
+syntax match   nmShowMsgDescWho /[^)]\+)/ contained\r
+syntax match   nmShowMsgDescDate / ([^)]\+[0-9]) / contained\r
+syntax match   nmShowMsgDescTags /([^)]\+)$/ contained\r
+\r
+syntax cluster nmShowMsgBody contains=3D@nmShowMsgBodyMail,@nmShowMsgBodyGit\r
+syntax include @nmShowMsgBodyMail syntax/mail.vim\r
+silent! syntax include @nmShowMsgBodyGit syntax/notmuch-git-diff.vim\r
+\r
+highlight nmShowMsgDescWho term=3Dreverse cterm=3Dreverse gui=3Dreverse\r
+highlight link nmShowMsgDescDate Type\r
+highlight link nmShowMsgDescTags String\r
+\r
+"TODO what about this?\r
+highlight Folded term=3Dreverse ctermfg=3DLightGrey ctermbg=3DBlack guifg=\r
+=3DLightGray guibg=3DBlack\r
+\r
+--===============0474093823==--\r