--- /dev/null
+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