vmail 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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