vmail 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- data/NOTES +17 -13
- data/Rakefile +2 -4
- data/lib/vmail.rb +14 -10
- data/lib/vmail.vim +107 -91
- data/lib/vmail/imap_client.rb +30 -22
- data/lib/vmail/message_formatter.rb +91 -89
- data/lib/vmail/string_ext.rb +18 -0
- data/lib/vmail/version.rb +1 -1
- data/test/message_formatter_test.rb +2 -1
- data/test/test_helper.rb +0 -2
- metadata +3 -6
- data/config/environment.rb +0 -18
- data/config/gmail.orig.yml +0 -8
- data/lib/gmail.rb +0 -145
data/NOTES
CHANGED
@@ -467,6 +467,14 @@ DONE
|
|
467
467
|
window. redirect streams from daemon to a log file
|
468
468
|
- package gem
|
469
469
|
- vmail (client)
|
470
|
+
- mvim = allow using through VMAIL_EDITOR env variable
|
471
|
+
- need to put mailbox and search on status line from startup
|
472
|
+
- c-j c-k to go to prev/next message
|
473
|
+
- turn search into an input() style prompt
|
474
|
+
- remember and show last search
|
475
|
+
- map u update from message window
|
476
|
+
- do search after loading vim or mvim and send size first
|
477
|
+
- vim resize event
|
470
478
|
|
471
479
|
next:
|
472
480
|
- sending an attachment
|
@@ -474,11 +482,9 @@ next:
|
|
474
482
|
attached and sent
|
475
483
|
- forwarding attachments
|
476
484
|
- could be "attach: [uid] attachments to signal"
|
477
|
-
- use :wq to send compose message, not ,d (easy to confuse with delete!)
|
478
|
-
- close! to cancel
|
479
|
-
- fewer mappings to remember this way
|
480
485
|
- put instructions in message window; put in a top section
|
481
|
-
-
|
486
|
+
- help /readme
|
487
|
+
- help document should just be readme? or part of it.
|
482
488
|
- follow mysql and use -u and -p flags on startup of server?
|
483
489
|
- omitting -p flag forces prompt
|
484
490
|
- get rid of config file
|
@@ -486,21 +492,18 @@ next:
|
|
486
492
|
- tempname()
|
487
493
|
- system() allows a parameters that is written to tmp file and passed
|
488
494
|
to stdin
|
489
|
-
- help document should just be readme? or part of it.
|
490
495
|
- print all selected messages into file (append)
|
491
496
|
- can pipe through a command line tool, so we don't need to clutter
|
492
497
|
viewer.vim with more mappings and functions
|
493
|
-
- put "move to >" and "select mailbox >" in command window
|
494
|
-
- make compat with ruby 1.8
|
495
|
-
- help
|
496
|
-
- readme
|
497
498
|
- ,s s confusion? Star vs search
|
498
|
-
-
|
499
|
-
|
500
|
-
|
501
|
-
- mvim
|
499
|
+
- enhance contacts auto fix with more advanced vim script
|
500
|
+
- sort contacts by frequency, then take first 10 or so of any match
|
501
|
+
- mvim - window width not correct
|
502
|
+
- mvim - starred messages not syntax colored
|
502
503
|
|
503
504
|
later:
|
505
|
+
- mvim redrawstatus line bug
|
506
|
+
http://vim.1045645.n5.nabble.com/Redrawing-bug-in-MacVim-Command-T-since-commit-ba44868-td3248742.html
|
504
507
|
- allow one daemon, multiple clients (select mailbox?)
|
505
508
|
- remember searches?
|
506
509
|
- archive function (shortcut for move to all mail)
|
@@ -518,6 +521,7 @@ later:
|
|
518
521
|
- do fast action like deletes
|
519
522
|
- reload after window resize
|
520
523
|
- sometimes update doesn't work - bug
|
524
|
+
- show total messages from a search, showing 100
|
521
525
|
- message threads
|
522
526
|
|
523
527
|
|
data/Rakefile
CHANGED
@@ -3,12 +3,10 @@ require 'rake/testtask'
|
|
3
3
|
require 'bundler'
|
4
4
|
Bundler::GemHelper.install_tasks
|
5
5
|
|
6
|
-
|
7
|
-
require(File.join(File.dirname(__FILE__), 'config', 'environment'))
|
8
|
-
end
|
6
|
+
$LOAD_PATH.unshift File.join(File.dirname(__FILE__), 'lib')
|
9
7
|
|
10
8
|
desc "Run tests"
|
11
|
-
task :test
|
9
|
+
task :test do
|
12
10
|
$:.unshift File.expand_path("test")
|
13
11
|
require 'test_helper'
|
14
12
|
require 'time_format_test'
|
data/lib/vmail.rb
CHANGED
@@ -4,33 +4,37 @@ module Vmail
|
|
4
4
|
extend self
|
5
5
|
|
6
6
|
def start
|
7
|
+
vim = ENV['VMAIL_VIM'] || 'vim'
|
8
|
+
|
7
9
|
config = YAML::load(File.read(File.expand_path("~/gmail.yml")))
|
8
|
-
|
10
|
+
logfile = (vim == 'mvim') ? STDERR : 'vmail.log'
|
11
|
+
config.merge! 'logfile' => logfile
|
9
12
|
|
10
13
|
puts "starting vmail imap client with config #{config}"
|
11
14
|
|
12
15
|
drb_uri = Vmail::ImapClient.daemon config
|
13
16
|
|
14
17
|
server = DRbObject.new_with_uri drb_uri
|
18
|
+
|
19
|
+
# TODO this is useless if we're using mvim
|
15
20
|
server.window_width = `stty size`.strip.split(' ')[1]
|
16
|
-
server.select_mailbox ARGV.shift || 'INBOX'
|
17
21
|
|
18
|
-
|
22
|
+
mailbox = ARGV.shift || 'INBOX'
|
23
|
+
server.select_mailbox mailbox
|
19
24
|
|
25
|
+
query = ARGV.empty? ? [100, 'ALL'] : ARGV
|
26
|
+
puts "mailbox: #{mailbox}"
|
27
|
+
puts "query: #{query.inspect}"
|
20
28
|
|
21
|
-
buffer_file = "
|
29
|
+
buffer_file = "vmailbuffer.txt"
|
22
30
|
puts "using buffer file: #{buffer_file}"
|
23
31
|
File.open(buffer_file, "w") do |file|
|
24
|
-
file.puts
|
32
|
+
file.puts "just a moment..."
|
25
33
|
end
|
26
34
|
|
27
35
|
# invoke vim
|
28
|
-
# TODO
|
29
|
-
# - mvim; move viewer.vim to new file
|
30
|
-
|
31
|
-
vim = ENV['VMAIL_VIM'] || 'vim'
|
32
36
|
vimscript = File.expand_path("../vmail.vim", __FILE__)
|
33
|
-
vim_command = "DRB_URI='#{drb_uri}' #{vim} -S #{vimscript} #{buffer_file}"
|
37
|
+
vim_command = "DRB_URI='#{drb_uri}' VMAIL_MAILBOX=#{String.shellescape(mailbox)} VMAIL_QUERY=#{String.shellescape(query.join(' '))} #{vim} -S #{vimscript} #{buffer_file}"
|
34
38
|
puts vim_command
|
35
39
|
system(vim_command)
|
36
40
|
|
data/lib/vmail.vim
CHANGED
@@ -1,13 +1,12 @@
|
|
1
|
-
let s:mailbox =
|
2
|
-
let s:
|
3
|
-
let s:query = ''
|
1
|
+
let s:mailbox = $VMAIL_MAILBOX
|
2
|
+
let s:query = $VMAIL_QUERY
|
4
3
|
|
5
4
|
let s:drb_uri = $DRB_URI
|
6
5
|
|
7
6
|
let s:client_script = "vmail_client " . s:drb_uri . " "
|
8
|
-
let s:
|
7
|
+
let s:set_window_width_command = s:client_script . "window_width= "
|
9
8
|
let s:list_mailboxes_command = s:client_script . "list_mailboxes "
|
10
|
-
let s:
|
9
|
+
let s:show_message_command = s:client_script . "show_message "
|
11
10
|
let s:update_command = s:client_script . "update"
|
12
11
|
let s:fetch_headers_command = s:client_script . "fetch_headers "
|
13
12
|
let s:select_mailbox_command = s:client_script . "select_mailbox "
|
@@ -26,7 +25,7 @@ let s:message_bufname = "MessageWindow"
|
|
26
25
|
let s:list_bufname = "MessageListWindow"
|
27
26
|
|
28
27
|
function! VmailStatusLine()
|
29
|
-
return "%<%f\ " . s:mailbox . "%r%=%-14.(%l,%c%V%)\ %P"
|
28
|
+
return "%<%f\ " . s:mailbox . " " . s:query . "%r%=%-14.(%l,%c%V%)\ %P"
|
30
29
|
endfunction
|
31
30
|
|
32
31
|
function! s:create_list_window()
|
@@ -47,6 +46,7 @@ function! s:create_list_window()
|
|
47
46
|
" we need the bufnr to find the window later
|
48
47
|
let s:listbufnr = bufnr('%')
|
49
48
|
setlocal statusline=%!VmailStatusLine()
|
49
|
+
call s:message_list_window_mappings()
|
50
50
|
endfunction
|
51
51
|
|
52
52
|
" the message display buffer window
|
@@ -56,28 +56,7 @@ function! s:create_message_window()
|
|
56
56
|
" setlocal noswapfile
|
57
57
|
" setlocal nobuflisted
|
58
58
|
let s:message_window_bufnr = bufnr('%')
|
59
|
-
|
60
|
-
noremap <silent> <buffer> <cr> :call <SID>focus_list_window()<CR>
|
61
|
-
noremap <silent> <buffer> <Leader>q :call <SID>focus_list_window()<CR>
|
62
|
-
noremap <silent> <buffer> q <Leader>q
|
63
|
-
noremap <silent> <buffer> <Leader>r :call <SID>compose_reply(0)<CR>
|
64
|
-
noremap <silent> <buffer> <Leader>a :call <SID>compose_reply(1)<CR>
|
65
|
-
noremap <silent> <buffer> <Leader>R :call <SID>show_raw()<cr>
|
66
|
-
noremap <silent> <buffer> <Leader>R :call <SID>show_raw()<cr>
|
67
|
-
noremap <silent> <buffer> <Leader>f :call <SID>compose_forward()<CR><cr>
|
68
|
-
" TODO improve this
|
69
|
-
noremap <silent> <buffer> <Leader>o yE :!open '<C-R>"'<CR><CR>
|
70
|
-
noremap <silent> <buffer> <leader>j :call <SID>show_next_message()<CR>
|
71
|
-
noremap <silent> <buffer> <leader>k :call <SID>show_previous_message()<CR>
|
72
|
-
noremap <silent> <buffer> <Leader>c :call <SID>compose_message()<CR>
|
73
|
-
noremap <silent> <buffer> <Leader>h :call <SID>open_html_part()<CR><cr>
|
74
|
-
nnoremap <silent> <buffer> q :close<cr>
|
75
|
-
nnoremap <silent> <buffer> <leader>d :call <SID>focus_list_window()<cr>:call <SID>delete_messages("Deleted")<cr>
|
76
|
-
nnoremap <silent> <buffer> s :call <SID>focus_list_window()<cr>:call <SID>toggle_star()<cr>
|
77
|
-
nnoremap <silent> <buffer> <Leader>m :call <SID>focus_list_window()<cr>:call <SID>mailbox_window()<CR>
|
78
|
-
nnoremap <silent> <buffer> <Leader>A :call <SID>save_attachments()<cr>
|
79
|
-
" go fullscreen
|
80
|
-
nnoremap <silent> <buffer> <Space> :call <SID>toggle_fullscreen()<cr>
|
59
|
+
call s:message_window_mappings()
|
81
60
|
close
|
82
61
|
endfunction
|
83
62
|
|
@@ -101,8 +80,9 @@ function! s:show_message()
|
|
101
80
|
redraw
|
102
81
|
let selected_uid = matchstr(line, '^\d\+')
|
103
82
|
let s:current_uid = selected_uid
|
104
|
-
let command = s:
|
105
|
-
|
83
|
+
let command = s:show_message_command . s:current_uid
|
84
|
+
echom "Loading message. Please wait..."
|
85
|
+
redrawstatus
|
106
86
|
let res = system(command)
|
107
87
|
call s:focus_message_window()
|
108
88
|
setlocal modifiable
|
@@ -133,7 +113,7 @@ endfunction
|
|
133
113
|
|
134
114
|
" invoked from withint message window
|
135
115
|
function! s:show_raw()
|
136
|
-
let command = s:
|
116
|
+
let command = s:show_message_command . s:current_uid . ' raw'
|
137
117
|
echo command
|
138
118
|
setlocal modifiable
|
139
119
|
1,$delete
|
@@ -178,7 +158,7 @@ endfunction
|
|
178
158
|
" gets new messages since last update
|
179
159
|
function! s:update()
|
180
160
|
let command = s:update_command
|
181
|
-
echo
|
161
|
+
echo "checking for new messages. please wait..."
|
182
162
|
let res = system(command)
|
183
163
|
if match(res, '^\d\+') != -1
|
184
164
|
setlocal modifiable
|
@@ -189,11 +169,11 @@ function! s:update()
|
|
189
169
|
redraw
|
190
170
|
call cursor(line + 1, 0)
|
191
171
|
normal z.
|
192
|
-
|
193
|
-
|
172
|
+
echom "you have " . num . " new message" . (num == 1 ? '' : 's') . "!"
|
173
|
+
redrawstatus
|
194
174
|
else
|
195
|
-
|
196
|
-
|
175
|
+
echom "no new messages"
|
176
|
+
redrawstatus
|
197
177
|
endif
|
198
178
|
endfunction
|
199
179
|
|
@@ -215,7 +195,11 @@ function! s:toggle_star() range
|
|
215
195
|
let action = " -FLAGS"
|
216
196
|
endif
|
217
197
|
let command = s:flag_command . uid_set . action . " Flagged"
|
218
|
-
|
198
|
+
if len(uids) == 1
|
199
|
+
echom "toggling flag on message " . uid_set
|
200
|
+
else
|
201
|
+
echom "toggling flags on messages " . join(uid_set, ",")
|
202
|
+
endif
|
219
203
|
" toggle [*] on lines
|
220
204
|
let res = system(command)
|
221
205
|
setlocal modifiable
|
@@ -383,7 +367,6 @@ function! s:select_mailbox()
|
|
383
367
|
endif
|
384
368
|
let s:mailbox = mailbox
|
385
369
|
let command = s:select_mailbox_command . shellescape(s:mailbox)
|
386
|
-
echo command
|
387
370
|
call system(command)
|
388
371
|
redraw
|
389
372
|
" now get latest 100 messages
|
@@ -400,19 +383,15 @@ function! s:select_mailbox()
|
|
400
383
|
normal z.
|
401
384
|
endfunction
|
402
385
|
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
call feedkeys("i")
|
411
|
-
endfunction
|
386
|
+
func! s:search_query()
|
387
|
+
if !exists("s:query")
|
388
|
+
let s:query = ""
|
389
|
+
endif
|
390
|
+
let s:query = input("search query: ", s:query)
|
391
|
+
call s:do_search()
|
392
|
+
endfunc
|
412
393
|
|
413
394
|
function! s:do_search()
|
414
|
-
let s:query = getline(line('.'))
|
415
|
-
close
|
416
395
|
" empty query
|
417
396
|
if match(s:query, '^\s*$') != -1
|
418
397
|
return
|
@@ -420,12 +399,21 @@ function! s:do_search()
|
|
420
399
|
" close message window if open
|
421
400
|
call s:focus_message_window()
|
422
401
|
close
|
423
|
-
"
|
424
|
-
let
|
425
|
-
|
402
|
+
" if query doesn't start with a number, set max returned to 100
|
403
|
+
let limit = 100
|
404
|
+
let imap_query = s:query
|
405
|
+
if match(s:query, '^\d') == 0
|
406
|
+
let query_chunks = split(s:query, '\s')
|
407
|
+
let limit = remove(query_chunks, 0)
|
408
|
+
let imap_query = join(query_chunks, ' ')
|
409
|
+
end
|
410
|
+
let s:query = limit . ' ' . imap_query
|
411
|
+
let command = s:search_command . limit . ' ' . shellescape(imap_query)
|
412
|
+
redraw
|
426
413
|
call s:focus_list_window()
|
427
|
-
let res = system(command)
|
428
414
|
setlocal modifiable
|
415
|
+
echo "running query on " . s:mailbox . ": " . s:query . ". please wait..."
|
416
|
+
let res = system(command)
|
429
417
|
1,$delete
|
430
418
|
put! =res
|
431
419
|
execute "normal Gdd\<c-y>"
|
@@ -491,10 +479,7 @@ func! s:open_compose_window(command)
|
|
491
479
|
1,$delete
|
492
480
|
put! =res
|
493
481
|
normal 1G
|
494
|
-
|
495
|
-
nnoremap <silent> <buffer> q :call <SID>cancel_compose()<cr>
|
496
|
-
nnoremap <silent> <buffer> <leader>q :call <SID>cancel_compose()<cr>
|
497
|
-
nnoremap <silent> <buffer> <Leader>s :call <SID>save_draft()<CR>
|
482
|
+
call s:compose_window_mappings()
|
498
483
|
set completefunc=CompleteContact
|
499
484
|
endfunc
|
500
485
|
|
@@ -514,7 +499,7 @@ function! CompleteContact(findstart, base)
|
|
514
499
|
return start
|
515
500
|
else
|
516
501
|
" find contacts matching with "a:base"
|
517
|
-
let matches = system("grep " . shellescape(a:base) . " contacts.txt")
|
502
|
+
let matches = system("grep -i " . shellescape(a:base) . " contacts.txt")
|
518
503
|
return split(matches, "\n")
|
519
504
|
endif
|
520
505
|
endfun
|
@@ -575,49 +560,80 @@ func! s:toggle_fullscreen()
|
|
575
560
|
endif
|
576
561
|
endfunc
|
577
562
|
|
578
|
-
call s:create_list_window()
|
579
563
|
|
580
|
-
|
581
|
-
|
582
|
-
call s:focus_list_window() " to go list window
|
583
|
-
" this are list window bindings
|
584
|
-
|
585
|
-
noremap <silent> <buffer> <cr> :call <SID>show_message()<CR>
|
586
|
-
noremap <silent> <buffer> q :qal!<cr>
|
564
|
+
" --------------------------------------------------------------------------------
|
565
|
+
" MAPPINGS
|
587
566
|
|
588
|
-
|
589
|
-
noremap <silent> <buffer> <
|
567
|
+
func! s:message_window_mappings()
|
568
|
+
noremap <silent> <buffer> <cr> :call <SID>focus_list_window()<CR>
|
569
|
+
noremap <silent> <buffer> <Leader>r :call <SID>compose_reply(0)<CR>
|
570
|
+
noremap <silent> <buffer> <Leader>a :call <SID>compose_reply(1)<CR>
|
571
|
+
noremap <silent> <buffer> <Leader>R :call <SID>show_raw()<cr>
|
572
|
+
noremap <silent> <buffer> <Leader>R :call <SID>show_raw()<cr>
|
573
|
+
noremap <silent> <buffer> <Leader>f :call <SID>compose_forward()<CR><cr>
|
574
|
+
" TODO improve this
|
575
|
+
noremap <silent> <buffer> <Leader>o yE :!open '<C-R>"'<CR><CR>
|
576
|
+
noremap <silent> <buffer> <c-j> :call <SID>show_next_message()<CR>
|
577
|
+
noremap <silent> <buffer> <c-k> :call <SID>show_previous_message()<CR>
|
578
|
+
nmap <silent> <buffer> <leader>j <c-j>
|
579
|
+
nmap <silent> <buffer> <leader>k <c-k>
|
580
|
+
noremap <silent> <buffer> <Leader>c :call <SID>compose_message()<CR>
|
581
|
+
noremap <silent> <buffer> <Leader>h :call <SID>open_html_part()<CR><cr>
|
582
|
+
nnoremap <silent> <buffer> q :close<cr>
|
583
|
+
nnoremap <silent> <buffer> <leader>d :call <SID>focus_list_window()<cr>:call <SID>delete_messages("Deleted")<cr>
|
584
|
+
nnoremap <silent> <buffer> s :call <SID>focus_list_window()<cr>:call <SID>toggle_star()<cr>
|
585
|
+
nnoremap <silent> <buffer> u :call <SID>focus_list_window()<cr>:call <SID>update()<CR>
|
586
|
+
nnoremap <silent> <buffer> <Leader>m :call <SID>focus_list_window()<cr>:call <SID>mailbox_window()<CR>
|
587
|
+
nnoremap <silent> <buffer> <Leader>A :call <SID>save_attachments()<cr>
|
588
|
+
" go fullscreen
|
589
|
+
nnoremap <silent> <buffer> <Space> :call <SID>toggle_fullscreen()<cr>
|
590
|
+
endfunc
|
590
591
|
|
591
|
-
|
592
|
-
|
593
|
-
|
594
|
-
|
592
|
+
func! s:message_list_window_mappings()
|
593
|
+
noremap <silent> <buffer> <cr> :call <SID>show_message()<CR>
|
594
|
+
noremap <silent> <buffer> q :qal!<cr>
|
595
|
+
noremap <silent> <buffer> s :call <SID>toggle_star()<CR>
|
596
|
+
noremap <silent> <buffer> <leader>d :call <SID>delete_messages("Deleted")<CR>
|
597
|
+
" TODO the range doesn't quite work as expect, need <line1> <line2>
|
598
|
+
" trying to make user defined commands that work from : prompt
|
599
|
+
" command -buffer -range VmailDelete call s:toggle_star("Deleted")
|
600
|
+
" command -buffer -range VmailStar call s:toggle_star("Flagged")
|
601
|
+
noremap <silent> <buffer> <leader>! :call <SID>delete_messages("[Gmail]/Spam")<CR>
|
602
|
+
"open a link browser (os x)
|
603
|
+
"autocmd CursorMoved <buffer> call <SID>show_message()
|
604
|
+
noremap <silent> <buffer> u :call <SID>update()<CR>
|
605
|
+
noremap <silent> <buffer> <Leader>s :call <SID>search_query()<CR>
|
606
|
+
noremap <silent> <buffer> <Leader>m :call <SID>mailbox_window()<CR>
|
607
|
+
noremap <silent> <buffer> <Leader>v :call <SID>move_to_mailbox()<CR>
|
608
|
+
noremap <silent> <buffer> <Leader>c :call <SID>compose_message()<CR>
|
609
|
+
noremap <silent> <buffer> <Leader>r :call <SID>show_message()<cr>:call <SID>compose_reply(0)<CR>
|
610
|
+
noremap <silent> <buffer> <Leader>a :call <SID>show_message()<cr>:call <SID>compose_reply(1)<CR>
|
611
|
+
" go fullscreen
|
612
|
+
nnoremap <silent> <buffer> <Space> :call <SID>toggle_fullscreen()<cr>
|
613
|
+
endfunc
|
595
614
|
|
596
|
-
|
615
|
+
func! s:compose_window_mappings()
|
616
|
+
" NOTE deliver_message is a global mapping, so user can load a saved
|
617
|
+
" message from a file and send it
|
618
|
+
nnoremap <silent> <Leader>vd :call <SID>deliver_message()<CR>
|
619
|
+
nnoremap <silent> <buffer> <Leader>vs :call <SID>save_draft()<CR>
|
620
|
+
noremap <silent> <buffer> <leader>q :call <SID>cancel_compose()<cr>
|
621
|
+
nmap <silent> <buffer> q <leader>q
|
622
|
+
endfunc
|
597
623
|
|
598
|
-
|
599
|
-
"autocmd CursorMoved <buffer> call <SID>show_message()
|
624
|
+
call s:create_list_window()
|
600
625
|
|
601
|
-
|
602
|
-
noremap <silent> <buffer> <Leader>s :call <SID>search_window()<CR>
|
603
|
-
noremap <silent> <buffer> <Leader>m :call <SID>mailbox_window()<CR>
|
604
|
-
noremap <silent> <buffer> <Leader>v :call <SID>move_to_mailbox()<CR>
|
626
|
+
call s:create_message_window()
|
605
627
|
|
606
|
-
|
628
|
+
call s:focus_list_window() " to go list window
|
607
629
|
|
608
|
-
|
609
|
-
|
630
|
+
" send window width
|
631
|
+
call system(s:set_window_width_command . winwidth(1))
|
610
632
|
|
611
|
-
|
612
|
-
nnoremap <silent> <buffer> <Space> :call <SID>toggle_fullscreen()<cr>
|
633
|
+
autocmd VimResized <buffer> call system(s:set_window_width_command . winwidth(1))
|
613
634
|
|
614
|
-
|
615
|
-
|
635
|
+
call system(s:select_mailbox_command . shellescape(s:mailbox))
|
636
|
+
call s:do_search()
|
616
637
|
|
617
|
-
" go to bottom and center cursorline
|
618
|
-
normal G
|
619
|
-
normal z.
|
620
638
|
|
621
|
-
" send window width
|
622
|
-
" system(s:set_window_width_command . winwidth(1))
|
623
639
|
|
data/lib/vmail/imap_client.rb
CHANGED
@@ -26,7 +26,8 @@ module Vmail
|
|
26
26
|
@mailbox = nil
|
27
27
|
@logger = Logger.new(config['logfile'] || STDERR)
|
28
28
|
@logger.level = Logger::DEBUG
|
29
|
-
@
|
29
|
+
@current_mail = nil
|
30
|
+
@current_uid = nil
|
30
31
|
end
|
31
32
|
|
32
33
|
def open
|
@@ -127,7 +128,7 @@ module Vmail
|
|
127
128
|
subject = Mail::Encodings.unquote_and_convert_to(subject, 'utf-8')
|
128
129
|
flags = format_flags(flags)
|
129
130
|
first_col_width = max_uid.to_s.length
|
130
|
-
mid_width = @width - (first_col_width +
|
131
|
+
mid_width = @width - (first_col_width + 33)
|
131
132
|
address_col_width = (mid_width * 0.3).ceil
|
132
133
|
subject_col_width = (mid_width * 0.7).floor
|
133
134
|
[uid.to_s.col(first_col_width),
|
@@ -154,7 +155,6 @@ module Vmail
|
|
154
155
|
end
|
155
156
|
end
|
156
157
|
|
157
|
-
|
158
158
|
FLAGMAP = {:Flagged => '[*]'}
|
159
159
|
# flags is an array like [:Flagged, :Seen]
|
160
160
|
def format_flags(flags)
|
@@ -222,22 +222,24 @@ module Vmail
|
|
222
222
|
res
|
223
223
|
end
|
224
224
|
|
225
|
-
def
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
log "fetching #{uid.inspect}"
|
225
|
+
def show_message(uid, raw=false, forwarded=false)
|
226
|
+
uid = uid.to_i
|
227
|
+
return @current_mail.to_s if raw
|
228
|
+
return @current_message if uid == @current_uid
|
229
|
+
log "fetching #{uid.inspect}"
|
230
230
|
fetch_data = reconnect_if_necessary do
|
231
|
-
@imap.uid_fetch(uid
|
231
|
+
@imap.uid_fetch(uid, ["FLAGS", "RFC822", "RFC822.SIZE"])[0]
|
232
232
|
end
|
233
233
|
res = fetch_data.attr["RFC822"]
|
234
|
-
mail = Mail.new(res)
|
235
|
-
@
|
236
|
-
|
234
|
+
mail = Mail.new(res)
|
235
|
+
@current_uid = uid
|
236
|
+
@current_mail = mail # used later to show raw message or extract attachments if any
|
237
|
+
log "saving current mail with parts: #{@current_mail.parts.inspect}"
|
238
|
+
formatter = Vmail::MessageFormatter.new(mail)
|
237
239
|
part = formatter.find_text_part
|
238
240
|
out = formatter.process_body
|
239
241
|
size = fetch_data.attr["RFC822.SIZE"]
|
240
|
-
|
242
|
+
@current_message = <<-EOF
|
241
243
|
#{@mailbox} #{uid} #{number_to_human_size size} #{forwarded ? nil : format_parts_info(formatter.list_parts)}
|
242
244
|
----------------------------------------
|
243
245
|
#{format_headers(formatter.extract_headers)}
|
@@ -324,7 +326,7 @@ EOF
|
|
324
326
|
select {|x| @username !~ /#{x.mailbox}@#{x.host}/}.
|
325
327
|
map {|x| address_to_string(x)}.join(", ")
|
326
328
|
mail = Mail.new fetch_data.attr['RFC822']
|
327
|
-
formatter = MessageFormatter.new(mail)
|
329
|
+
formatter = Vmail::MessageFormatter.new(mail)
|
328
330
|
headers = formatter.extract_headers
|
329
331
|
subject = headers['subject']
|
330
332
|
if subject !~ /Re: /
|
@@ -349,7 +351,7 @@ EOF
|
|
349
351
|
|
350
352
|
# TODO, forward with attachments
|
351
353
|
def forward_template(uid)
|
352
|
-
original_body =
|
354
|
+
original_body = show_message(uid, false, true)
|
353
355
|
new_message_template +
|
354
356
|
"\n---------- Forwarded message ----------\n" +
|
355
357
|
original_body + signature
|
@@ -401,11 +403,11 @@ EOF
|
|
401
403
|
|
402
404
|
def save_attachments(dir)
|
403
405
|
log "save_attachments #{dir}"
|
404
|
-
if !@
|
406
|
+
if !@current_mail
|
405
407
|
log "missing a current message"
|
406
408
|
end
|
407
|
-
return unless dir && @
|
408
|
-
attachments = @
|
409
|
+
return unless dir && @current_mail
|
410
|
+
attachments = @current_mail.attachments
|
409
411
|
`mkdir -p #{dir}`
|
410
412
|
attachments.each do |x|
|
411
413
|
path = File.join(dir, x.filename)
|
@@ -416,10 +418,16 @@ EOF
|
|
416
418
|
|
417
419
|
def open_html_part(uid)
|
418
420
|
log "open_html_part #{uid}"
|
419
|
-
|
420
|
-
|
421
|
-
|
422
|
-
|
421
|
+
log @current_mail.parts.inspect
|
422
|
+
multipart = @current_mail.parts.detect {|part| part.multipart?}
|
423
|
+
html_part = if multipart
|
424
|
+
multipart.parts.detect {|part| part.header["Content-Type"].to_s =~ /text\/html/}
|
425
|
+
elsif ! @current_mail.parts.empty?
|
426
|
+
@current_mail.parts.detect {|part| part.header["Content-Type"].to_s =~ /text\/html/}
|
427
|
+
else
|
428
|
+
@current_mail.body
|
429
|
+
end
|
430
|
+
return if html_part.nil?
|
423
431
|
outfile = 'htmlpart.html'
|
424
432
|
File.open(outfile, 'w') {|f| f.puts(html_part.decoded)}
|
425
433
|
# client should handle opening the html file
|
@@ -2,112 +2,114 @@ require 'mail'
|
|
2
2
|
require 'open3'
|
3
3
|
require 'iconv'
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
def list_parts(parts = (@mail.parts.empty? ? [@mail] : @mail.parts))
|
13
|
-
if parts.empty?
|
14
|
-
return []
|
5
|
+
module Vmail
|
6
|
+
class MessageFormatter
|
7
|
+
# initialize with a Mail object
|
8
|
+
def initialize(mail, uid = nil)
|
9
|
+
@mail = mail
|
10
|
+
@uid = uid
|
15
11
|
end
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
12
|
+
|
13
|
+
def list_parts(parts = (@mail.parts.empty? ? [@mail] : @mail.parts))
|
14
|
+
if parts.empty?
|
15
|
+
return []
|
16
|
+
end
|
17
|
+
lines = parts.map do |part|
|
18
|
+
if part.multipart?
|
19
|
+
list_parts(part.parts)
|
20
|
+
else
|
21
|
+
# part.charset could be used
|
22
|
+
"- #{part.content_type}"
|
23
|
+
end
|
22
24
|
end
|
25
|
+
lines.flatten
|
23
26
|
end
|
24
|
-
lines.flatten
|
25
|
-
end
|
26
27
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
28
|
+
def process_body
|
29
|
+
part = find_text_part(@mail.parts)
|
30
|
+
body = if part && part.respond_to?(:header)
|
31
|
+
if part.header["Content-Type"].to_s =~ /text\/plain/
|
32
|
+
format_text_body(part)
|
33
|
+
elsif part.header["Content-Type"].to_s =~ /text\/html/
|
34
|
+
format_html_body(part)
|
35
|
+
else
|
36
|
+
format_text_body(part)
|
37
|
+
end
|
38
|
+
else
|
39
|
+
"NO BODY"
|
36
40
|
end
|
37
|
-
|
38
|
-
|
41
|
+
rescue
|
42
|
+
puts $!
|
43
|
+
body
|
39
44
|
end
|
40
|
-
rescue
|
41
|
-
puts $!
|
42
|
-
body
|
43
|
-
end
|
44
45
|
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
if part
|
51
|
-
find_text_part(part.parts)
|
52
|
-
else
|
53
|
-
# no multipart part
|
54
|
-
part = parts.detect {|part| (part.header["Content-Type"].to_s =~ /text\/plain/) }
|
46
|
+
def find_text_part(parts = @mail.parts)
|
47
|
+
if parts.empty?
|
48
|
+
return @mail
|
49
|
+
end
|
50
|
+
part = parts.detect {|part| part.multipart?}
|
55
51
|
if part
|
56
|
-
|
52
|
+
find_text_part(part.parts)
|
57
53
|
else
|
58
|
-
|
54
|
+
# no multipart part
|
55
|
+
part = parts.detect {|part| (part.header["Content-Type"].to_s =~ /text\/plain/) }
|
56
|
+
if part
|
57
|
+
return part
|
58
|
+
else
|
59
|
+
parts.first
|
60
|
+
end
|
59
61
|
end
|
60
62
|
end
|
61
|
-
end
|
62
63
|
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
64
|
+
def format_text_body(part)
|
65
|
+
text = part.body.decoded.gsub("\r", '')
|
66
|
+
charset = part.content_type_parameters && part.content_type_parameters['charset']
|
67
|
+
if charset
|
68
|
+
Iconv.conv('utf-8//translit//ignore', charset, text)
|
69
|
+
else
|
70
|
+
text
|
71
|
+
end
|
70
72
|
end
|
71
|
-
end
|
72
|
-
|
73
|
-
# depend on lynx
|
74
|
-
def format_html_body(part)
|
75
|
-
html = part.body.decoded.gsub("\r", '')
|
76
|
-
stdin, stdout, stderr = Open3.popen3("lynx -stdin -dump")
|
77
|
-
stdin.puts html
|
78
|
-
stdin.close
|
79
|
-
output = stdout.read
|
80
|
-
charset = part.content_type_parameters && part.content_type_parameters['charset']
|
81
|
-
charset ? Iconv.conv('utf-8//translit//ignore', charset, output) : output
|
82
|
-
end
|
83
73
|
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
74
|
+
# depend on lynx
|
75
|
+
def format_html_body(part)
|
76
|
+
html = part.body.decoded.gsub("\r", '')
|
77
|
+
stdin, stdout, stderr = Open3.popen3("lynx -stdin -dump")
|
78
|
+
stdin.puts html
|
79
|
+
stdin.close
|
80
|
+
output = stdout.read
|
81
|
+
charset = part.content_type_parameters && part.content_type_parameters['charset']
|
82
|
+
charset ? Iconv.conv('utf-8//translit//ignore', charset, output) : output
|
92
83
|
end
|
93
|
-
|
94
|
-
|
84
|
+
|
85
|
+
def extract_headers(mail = @mail)
|
86
|
+
headers = {'from' => utf8(mail['from'].decoded),
|
87
|
+
'date' => (mail.date.strftime('%a, %b %d %I:%M %p %Z %Y') rescue mail.date),
|
88
|
+
'to' => mail['to'].nil? ? nil : utf8(mail['to'].decoded),
|
89
|
+
'subject' => utf8(mail.subject)
|
90
|
+
}
|
91
|
+
if !mail.cc.nil?
|
92
|
+
headers['cc'] = utf8(mail['cc'].decoded.to_s)
|
93
|
+
end
|
94
|
+
if !mail.reply_to.nil?
|
95
|
+
headers['reply_to'] = utf8(mail['reply_to'].decoded)
|
96
|
+
end
|
97
|
+
headers
|
98
|
+
rescue
|
99
|
+
{'error' => $!}
|
95
100
|
end
|
96
|
-
headers
|
97
|
-
rescue
|
98
|
-
{'error' => $!}
|
99
|
-
end
|
100
101
|
|
101
|
-
|
102
|
-
|
103
|
-
|
102
|
+
def encoding
|
103
|
+
@encoding ||= @mail.header.charset || 'utf-8'
|
104
|
+
end
|
104
105
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
106
|
+
def utf8(string)
|
107
|
+
return '' unless string
|
108
|
+
return string unless encoding
|
109
|
+
Iconv.conv('utf-8//translit/ignore', encoding, string)
|
110
|
+
rescue
|
111
|
+
puts $!
|
112
|
+
string
|
113
|
+
end
|
112
114
|
end
|
113
115
|
end
|
data/lib/vmail/string_ext.rb
CHANGED
@@ -6,6 +6,24 @@ class String
|
|
6
6
|
def rcol(width) #right justified
|
7
7
|
self[0,width].rjust(width)
|
8
8
|
end
|
9
|
+
|
10
|
+
def self.shellescape(str)
|
11
|
+
# An empty argument will be skipped, so return empty quotes.
|
12
|
+
return "''" if str.empty?
|
13
|
+
|
14
|
+
str = str.dup
|
15
|
+
|
16
|
+
# Process as a single byte sequence because not all shell
|
17
|
+
# implementations are multibyte aware.
|
18
|
+
str.gsub!(/([^A-Za-z0-9_\-.,:\/@\n])/n, "\\\\\\1")
|
19
|
+
|
20
|
+
# A LF cannot be escaped with a backslash because a backslash + LF
|
21
|
+
# combo is regarded as line continuation and simply ignored.
|
22
|
+
str.gsub!(/\n/, "'\n'")
|
23
|
+
|
24
|
+
return str
|
25
|
+
end
|
26
|
+
|
9
27
|
end
|
10
28
|
|
11
29
|
|
data/lib/vmail/version.rb
CHANGED
@@ -1,7 +1,8 @@
|
|
1
1
|
# encoding: utf-8
|
2
2
|
require 'test_helper'
|
3
|
+
require 'vmail/message_formatter'
|
3
4
|
|
4
|
-
describe MessageFormatter do
|
5
|
+
describe Vmail::MessageFormatter do
|
5
6
|
describe "message with email addresses along with names" do
|
6
7
|
before do
|
7
8
|
@raw = File.read(File.expand_path('../fixtures/google-affiliate.eml', __FILE__))
|
data/test/test_helper.rb
CHANGED
metadata
CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
|
|
5
5
|
segments:
|
6
6
|
- 0
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
version: 0.0.
|
8
|
+
- 4
|
9
|
+
version: 0.0.4
|
10
10
|
platform: ruby
|
11
11
|
authors:
|
12
12
|
- Daniel Choi
|
@@ -14,7 +14,7 @@ autorequire:
|
|
14
14
|
bindir: bin
|
15
15
|
cert_chain: []
|
16
16
|
|
17
|
-
date: 2010-12-
|
17
|
+
date: 2010-12-13 00:00:00 -05:00
|
18
18
|
default_executable:
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
@@ -49,11 +49,8 @@ files:
|
|
49
49
|
- bin/mvmail
|
50
50
|
- bin/vmail
|
51
51
|
- bin/vmail_client
|
52
|
-
- config/environment.rb
|
53
|
-
- config/gmail.orig.yml
|
54
52
|
- gmail.vim
|
55
53
|
- lib/contacts_extractor.rb
|
56
|
-
- lib/gmail.rb
|
57
54
|
- lib/vmail.rb
|
58
55
|
- lib/vmail.vim
|
59
56
|
- lib/vmail/imap_client.rb
|
data/config/environment.rb
DELETED
@@ -1,18 +0,0 @@
|
|
1
|
-
# configure activerecord to use mysql
|
2
|
-
#require 'active_record'
|
3
|
-
require 'logger'
|
4
|
-
require 'yaml'
|
5
|
-
require 'net/imap'
|
6
|
-
require 'mail'
|
7
|
-
$LOAD_PATH.unshift File.join(File.dirname(__FILE__), '..', 'lib')
|
8
|
-
|
9
|
-
#config_file = File.join(File.dirname(__FILE__), 'database.yml')
|
10
|
-
#config = YAML::load(File.read(config_file))['development']
|
11
|
-
#ActiveRecord::Base.establish_connection config
|
12
|
-
|
13
|
-
gmail_config_file = File.join(File.dirname(__FILE__), 'gmail.yml')
|
14
|
-
gmail_config = YAML::load(File.read(gmail_config_file))
|
15
|
-
require 'gmail'
|
16
|
-
$gmail = Gmail.new gmail_config['login'], gmail_config['password']
|
17
|
-
|
18
|
-
require 'message_formatter'
|
data/config/gmail.orig.yml
DELETED
data/lib/gmail.rb
DELETED
@@ -1,145 +0,0 @@
|
|
1
|
-
require 'net/imap'
|
2
|
-
|
3
|
-
class Gmail
|
4
|
-
DEMARC = "------=gmail-tool="
|
5
|
-
|
6
|
-
def initialize(username, password)
|
7
|
-
@username, @password = username, password
|
8
|
-
end
|
9
|
-
|
10
|
-
def open
|
11
|
-
raise "block missing" unless block_given?
|
12
|
-
@imap = Net::IMAP.new('imap.gmail.com', 993, true, nil, false)
|
13
|
-
@imap.login(@username, @password)
|
14
|
-
yield @imap
|
15
|
-
rescue Exception => ex
|
16
|
-
raise
|
17
|
-
ensure
|
18
|
-
if @imap
|
19
|
-
@imap.close rescue Net::IMAP::BadResponseError
|
20
|
-
@imap.disconnect
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
# lists mailboxes
|
25
|
-
def mailboxes
|
26
|
-
open do |imap|
|
27
|
-
imap.list("[Gmail]/", "%") + imap.list("", "%")
|
28
|
-
end
|
29
|
-
end
|
30
|
-
|
31
|
-
# selects the mailbox and returns self
|
32
|
-
def mailbox(x)
|
33
|
-
@mailbox = x
|
34
|
-
# allow chaining
|
35
|
-
return self
|
36
|
-
end
|
37
|
-
|
38
|
-
def fetch(opts = {})
|
39
|
-
num_messages = opts[:num_messages] || 10
|
40
|
-
mailbox_label = opts[:mailbox] || @mailbox || 'inbox'
|
41
|
-
query = opts[:query] || ["ALL"]
|
42
|
-
open do |imap|
|
43
|
-
imap.select(mailbox_label)
|
44
|
-
all_uids = imap.uid_search(query)
|
45
|
-
STDERR.puts "#{all_uids.size} UIDS TOTAL"
|
46
|
-
uids = all_uids[-([num_messages, all_uids.size].min)..-1] || []
|
47
|
-
STDERR.puts "imap process uids #{uids.inspect}"
|
48
|
-
yield imap, uids
|
49
|
-
end
|
50
|
-
end
|
51
|
-
|
52
|
-
# generic mailbox operations
|
53
|
-
def imap
|
54
|
-
open do |imap|
|
55
|
-
imap.select((@mailbox || 'inbox'))
|
56
|
-
yield imap
|
57
|
-
end
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
|
62
|
-
class String
|
63
|
-
def col(width)
|
64
|
-
self[0,width].ljust(width)
|
65
|
-
end
|
66
|
-
end
|
67
|
-
|
68
|
-
def format_time(x)
|
69
|
-
Time.parse(x.to_s).localtime.strftime "%D %I:%M%P"
|
70
|
-
end
|
71
|
-
|
72
|
-
require 'time'
|
73
|
-
|
74
|
-
def search
|
75
|
-
mailbox = ARGV.shift
|
76
|
-
num_messages = ARGV.shift.to_i
|
77
|
-
#query = ["BODY", "politics"]
|
78
|
-
query = ARGV
|
79
|
-
puts mailbox
|
80
|
-
$gmail.mailbox(mailbox).fetch(:num_messages => num_messages, :query => query) do |imap,uids|
|
81
|
-
uids.each do |uid|
|
82
|
-
res = imap.uid_fetch(uid, ["FLAGS", "BODY", "ENVELOPE", "RFC822.HEADER"])[0]
|
83
|
-
#puts res.inspect
|
84
|
-
#puts res
|
85
|
-
header = res.attr["RFC822.HEADER"]
|
86
|
-
mail = Mail.new(header)
|
87
|
-
mail_id = uid
|
88
|
-
flags = res.attr["FLAGS"]
|
89
|
-
puts "#{mail_id} #{format_time(mail.date.to_s)} #{mail.from[0][0,30].ljust(30)} #{mail.subject.to_s[0,70].ljust(70)} #{flags.inspect.col(30)}"
|
90
|
-
|
91
|
-
next
|
92
|
-
|
93
|
-
mail = Mail.new(res)
|
94
|
-
foldline = [mail[:from], mail[:date], mail[:subject]].join(" ")
|
95
|
-
puts foldline + " {{{1"
|
96
|
-
if mail.parts.empty?
|
97
|
-
puts mail.body.decoded
|
98
|
-
else
|
99
|
-
puts mail.parts.inspect
|
100
|
-
end
|
101
|
-
end
|
102
|
-
end
|
103
|
-
end
|
104
|
-
|
105
|
-
def lookup(raw=false)
|
106
|
-
mailbox, uid = *ARGV[0,2]
|
107
|
-
$gmail.mailbox(mailbox).imap do |imap|
|
108
|
-
res = imap.uid_fetch(uid.to_i, ["FLAGS", "RFC822"])[0].attr["RFC822"]
|
109
|
-
if raw
|
110
|
-
puts res
|
111
|
-
return
|
112
|
-
end
|
113
|
-
mail = Mail.new(res)
|
114
|
-
if mail.parts.empty?
|
115
|
-
puts mail.header["Content-Type"]
|
116
|
-
puts mail.body.charset
|
117
|
-
puts mail.body.decoded
|
118
|
-
else
|
119
|
-
puts mail.parts.inspect
|
120
|
-
part = mail.parts.detect {|part|
|
121
|
-
(part.header["Content-Type"].to_s =~ /text\/plain/)
|
122
|
-
}
|
123
|
-
if part
|
124
|
-
puts "PART"
|
125
|
-
puts part.header["Content-Type"]
|
126
|
-
puts part.charset
|
127
|
-
puts part.body.decoded
|
128
|
-
end
|
129
|
-
end
|
130
|
-
end
|
131
|
-
end
|
132
|
-
|
133
|
-
if __FILE__ == $0
|
134
|
-
require 'yaml'
|
135
|
-
require 'mail'
|
136
|
-
config = YAML::load(File.read(File.expand_path("../../config/gmail.yml", __FILE__)))
|
137
|
-
$gmail = Gmail.new(config['login'], config['password'])
|
138
|
-
if ARGV.length == 2
|
139
|
-
lookup
|
140
|
-
elsif ARGV[2] == 'raw'
|
141
|
-
lookup(raw=true)
|
142
|
-
else
|
143
|
-
search
|
144
|
-
end
|
145
|
-
end
|