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 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
- - map u update from message window
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
- - sort contacts by frequency, then take first 10 or so of any match
499
- group
500
- - enhance contacts auto fix with more advance vim script
501
- - mvim = allow using through VMAIL_EDITOR env variable
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
- task :environment do
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 => :environment do
9
+ task :test do
12
10
  $:.unshift File.expand_path("test")
13
11
  require 'test_helper'
14
12
  require 'time_format_test'
@@ -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
- config.merge! 'logfile' => "vmail.log"
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
- query = ARGV.empty? ? [100, 'ALL'] : nil
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 = "vmail-buffer.txt"
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 server.search(*query)
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
 
@@ -1,13 +1,12 @@
1
- let s:mailbox = ''
2
- let s:num_msgs = 0 " number of messages
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:window_width_command = s:client_script . "window_width= "
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:lookup_command = s:client_script . "lookup "
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
- " message window bindings
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:lookup_command . s:current_uid
105
- echo "Loading message. Please wait..."
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:lookup_command . s:current_uid . ' raw'
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 command
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
- redraw
193
- echo "you have " . num . " new message" . (num == 1 ? '' : 's') . "!"
172
+ echom "you have " . num . " new message" . (num == 1 ? '' : 's') . "!"
173
+ redrawstatus
194
174
  else
195
- redraw
196
- echo "no new messages"
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
- echo command
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
- function! s:search_window()
404
- topleft split SearchWindow
405
- setlocal buftype=nofile
406
- setlocal noswapfile
407
- resize 1
408
- setlocal modifiable
409
- noremap! <silent> <buffer> <cr> <Esc>:call <SID>do_search()<CR>
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
- " TODO should we really hardcode 100 as the quantity?
424
- let command = s:search_command . "100 " . shellescape(s:query)
425
- echo command
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
- noremap <silent> <buffer> <Leader>d :call <SID>deliver_message()<CR>
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
- call s:create_message_window()
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
- noremap <silent> <buffer> s :call <SID>toggle_star()<CR>
589
- noremap <silent> <buffer> <leader>d :call <SID>delete_messages("Deleted")<CR>
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
- " TODO the range doesn't quite work as expect, need <line1> <line2>
592
- " trying to make user defined commands that work from : prompt
593
- " command -buffer -range VmailDelete call s:toggle_star("Deleted")
594
- " command -buffer -range VmailStar call s:toggle_star("Flagged")
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
- noremap <silent> <buffer> <leader>! :call <SID>delete_messages("[Gmail]/Spam")<CR>
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
- "open a link browser (os x)
599
- "autocmd CursorMoved <buffer> call <SID>show_message()
624
+ call s:create_list_window()
600
625
 
601
- noremap <silent> <buffer> u :call <SID>update()<CR>
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
- noremap <silent> <buffer> <Leader>c :call <SID>compose_message()<CR>
628
+ call s:focus_list_window() " to go list window
607
629
 
608
- noremap <silent> <buffer> <Leader>r :call <SID>show_message()<cr>:call <SID>compose_reply(0)<CR>
609
- noremap <silent> <buffer> <Leader>a :call <SID>show_message()<cr>:call <SID>compose_reply(1)<CR>
630
+ " send window width
631
+ call system(s:set_window_width_command . winwidth(1))
610
632
 
611
- " go fullscreen
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
- " press double return in list view to go full screen on a message; then
615
- " return? again to restore the list view
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
 
@@ -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
- @current_message = nil
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 + 14 + 2) - (10 + 2) - 5
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 lookup(uid, raw=false, forwarded=false)
226
- if raw
227
- return @current_message.to_s
228
- end
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.to_i, ["FLAGS", "RFC822", "RFC822.SIZE"])[0]
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
- @current_message = mail # used later to show raw message or extract attachments if any
236
- formatter = MessageFormatter.new(mail)
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
- message = <<-EOF
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 = lookup(uid, false, true)
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 !@current_message
406
+ if !@current_mail
405
407
  log "missing a current message"
406
408
  end
407
- return unless dir && @current_message
408
- attachments = @current_message.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
- fetch_data = @imap.uid_fetch(uid.to_i, ["RFC822"])[0]
420
- mail = Mail.new(fetch_data.attr['RFC822'])
421
- multipart = mail.parts.detect {|part| part.multipart?}
422
- html_part = (multipart || mail).parts.detect {|part| part.header["Content-Type"].to_s =~ /text\/html/}
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
- class MessageFormatter
6
- # initialize with a Mail object
7
- def initialize(mail, uid = nil)
8
- @mail = mail
9
- @uid = uid
10
- end
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
- lines = parts.map do |part|
17
- if part.multipart?
18
- list_parts(part.parts)
19
- else
20
- # part.charset could be used
21
- "- #{part.content_type}"
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
- def process_body
28
- part = find_text_part(@mail.parts)
29
- body = if part && part.respond_to?(:header)
30
- if part.header["Content-Type"].to_s =~ /text\/plain/
31
- format_text_body(part)
32
- elsif part.header["Content-Type"].to_s =~ /text\/html/
33
- format_html_body(part)
34
- else
35
- format_text_body(part)
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
- else
38
- "NO BODY"
41
+ rescue
42
+ puts $!
43
+ body
39
44
  end
40
- rescue
41
- puts $!
42
- body
43
- end
44
45
 
45
- def find_text_part(parts = @mail.parts)
46
- if parts.empty?
47
- return @mail
48
- end
49
- part = parts.detect {|part| part.multipart?}
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
- return part
52
+ find_text_part(part.parts)
57
53
  else
58
- parts.first
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
- def format_text_body(part)
64
- text = part.body.decoded.gsub("\r", '')
65
- charset = part.content_type_parameters && part.content_type_parameters['charset']
66
- if charset
67
- Iconv.conv('utf-8//translit//ignore', charset, text)
68
- else
69
- text
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
- def extract_headers(mail = @mail)
85
- headers = {'from' => utf8(mail['from'].decoded),
86
- 'date' => (mail.date.strftime('%a, %b %d %I:%M %p %Z %Y') rescue mail.date),
87
- 'to' => mail['to'].nil? ? nil : utf8(mail['to'].decoded),
88
- 'subject' => utf8(mail.subject)
89
- }
90
- if !mail.cc.nil?
91
- headers['cc'] = utf8(mail['cc'].decoded.to_s)
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
- if !mail.reply_to.nil?
94
- headers['reply_to'] = utf8(mail['reply_to'].decoded)
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
- def encoding
102
- @encoding ||= @mail.header.charset || 'utf-8'
103
- end
102
+ def encoding
103
+ @encoding ||= @mail.header.charset || 'utf-8'
104
+ end
104
105
 
105
- def utf8(string)
106
- return '' unless string
107
- return string unless encoding
108
- Iconv.conv('utf-8//translit/ignore', encoding, string)
109
- rescue
110
- puts $!
111
- string
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
@@ -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
 
@@ -1,3 +1,3 @@
1
1
  module Vmail
2
- VERSION = "0.0.3"
2
+ VERSION = "0.0.4"
3
3
  end
@@ -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__))
@@ -1,5 +1,3 @@
1
- require File.expand_path('../../config/environment', __FILE__)
2
-
3
1
  require 'minitest/spec'
4
2
  require 'minitest/unit'
5
3
 
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 0
8
- - 3
9
- version: 0.0.3
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-12 00:00:00 -05:00
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
@@ -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'
@@ -1,8 +0,0 @@
1
- login: dhchoi@gmail.com
2
- name: Daniel Choi
3
- password: somepassword
4
- signature: |
5
- --
6
- Sent via vmail. http://danielchoi.com
7
- drb_uri: druby://127.0.0.1:61676
8
-
@@ -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