vmail 1.2.3 → 1.2.4

Sign up to get free protection for your applications and to get access to all the features.
data/README.markdown CHANGED
@@ -120,6 +120,11 @@ this number by passing in a number after the mailbox name:
120
120
 
121
121
  vmail inbox 700 subject unix
122
122
 
123
+ Passing in 0 as the number of messages returns all messages that match
124
+ the query:
125
+
126
+ vmail inbox 0 subject unix # => returns all matching messages
127
+
123
128
  ## Viewing messages
124
129
 
125
130
  The first screen vmail shows you is a list of messages. You can view a message
data/bin/vmail CHANGED
@@ -7,5 +7,11 @@ rescue LoadError
7
7
  require 'vmail'
8
8
  end
9
9
 
10
- Vmail.start
10
+ if !STDOUT.tty?
11
+ Vmail.noninteractive_list_messages
12
+ elsif !STDIN.tty?
13
+ Vmail.batch_run
14
+ else
15
+ Vmail.start
16
+ end
11
17
 
data/lib/vmail.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require 'vmail/version'
2
2
  require 'vmail/options'
3
3
  require 'vmail/imap_client'
4
+ require 'vmail/query'
4
5
  require 'vmail/message_formatter'
5
6
  require 'vmail/reply_template'
6
7
 
@@ -11,18 +12,14 @@ module Vmail
11
12
  puts "starting vmail #{Vmail::VERSION}"
12
13
 
13
14
  vim = ENV['VMAIL_VIM'] || 'vim'
14
-
15
15
  ENV['VMAIL_BROWSER'] ||= 'open'
16
16
 
17
- # check for lynx
18
- if `which lynx` == ''
19
- puts "You need to install lynx on your system in order to see html-only messages"
20
- sleep 3
21
- end
17
+ check_lynx
18
+
22
19
  opts = Vmail::Options.new(ARGV)
23
20
  opts.config
24
-
25
21
  config = opts.config
22
+
26
23
  contacts_file = opts.contacts_file
27
24
 
28
25
  logfile = (vim == 'mvim') ? STDERR : 'vmail.log'
@@ -39,33 +36,25 @@ module Vmail
39
36
 
40
37
  server = DRbObject.new_with_uri drb_uri
41
38
 
42
- mailbox = if ARGV[0] =~ /^\d+/
43
- "INBOX"
44
- else
45
- ARGV.shift || 'INBOX'
46
- end
47
-
39
+ mailbox, query = parse_query
40
+ query_string = Vmail::Query.args2string query
48
41
  server.select_mailbox mailbox
49
42
 
50
- query = ARGV.empty? ? [100, 'ALL'] : ARGV
51
- if query.size == 1 && query[0] =~ /^\d/
52
- query << "ALL"
53
- end
54
- puts "mailbox: #{mailbox}"
55
- puts "query: #{query.inspect}"
43
+ STDERR.puts "mailbox: #{mailbox}"
44
+ STDERR.puts "query: #{query.inspect} => #{query_string}"
56
45
 
57
46
  buffer_file = "vmailbuffer"
58
47
  # invoke vim
59
48
  vimscript = File.expand_path("../vmail.vim", __FILE__)
60
- vim_command = "DRB_URI=#{drb_uri} VMAIL_CONTACTS_FILE=#{contacts_file} VMAIL_MAILBOX=#{String.shellescape(mailbox)} VMAIL_QUERY=#{String.shellescape(query.join(' '))} #{vim} -S #{vimscript} #{buffer_file}"
61
- puts vim_command
49
+ vim_command = "DRB_URI=#{drb_uri} VMAIL_CONTACTS_FILE=#{contacts_file} VMAIL_MAILBOX=#{String.shellescape(mailbox)} VMAIL_QUERY=#{String.shellescape(query_string)} #{vim} -S #{vimscript} #{buffer_file}"
50
+ STDERR.puts vim_command
62
51
 
63
- puts "using buffer file: #{buffer_file}"
52
+ STDERR.puts "using buffer file: #{buffer_file}"
64
53
  File.open(buffer_file, "w") do |file|
65
54
  file.puts "vmail starting with values:"
66
55
  file.puts "- drb uri: #{drb_uri}"
67
56
  file.puts "- mailbox: #{mailbox}"
68
- file.puts "- query: #{query.join(' ')}"
57
+ file.puts "- query: #{query_string}"
69
58
  file.puts
70
59
  file.puts "fetching messages. please wait..."
71
60
  end
@@ -78,7 +67,7 @@ module Vmail
78
67
 
79
68
  File.delete(buffer_file)
80
69
 
81
- puts "closing imap connection"
70
+ STDERR.puts "closing imap connection"
82
71
  begin
83
72
  Timeout::timeout(10) do
84
73
  $gmail.close
@@ -89,5 +78,77 @@ module Vmail
89
78
  puts "bye"
90
79
  exit
91
80
  end
81
+
82
+ # non-interactive mode
83
+ def noninteractive_list_messages
84
+ check_lynx
85
+ opts = Vmail::Options.new(ARGV)
86
+ opts.config
87
+ config = opts.config.merge 'logfile' => 'vmail.log'
88
+ mailbox, query = parse_query
89
+ query_string = Vmail::Query.args2string query
90
+ imap_client = Vmail::ImapClient.new config
91
+ imap_client.with_open do |vmail|
92
+ vmail.select_mailbox mailbox
93
+ vmail.search query_string
94
+ end
95
+ end
96
+
97
+ # batch processing mode
98
+ def batch_run
99
+ check_lynx
100
+ opts = Vmail::Options.new(ARGV)
101
+ opts.config
102
+ config = opts.config.merge 'logfile' => 'vmail.log'
103
+ # no search query args, but command args
104
+ imap_client = Vmail::ImapClient.new config
105
+ lines = STDIN.readlines# .reverse
106
+ mailbox = lines.shift.chomp
107
+ puts "mailbox: #{mailbox}"
108
+ uid_set = lines.map do |line|
109
+ line[/(\d+)\s*$/,1].to_i
110
+ end
111
+ commands = {
112
+ 'rm' => ["flag", "+FLAGS", "Deleted"],
113
+ 'spam' => ["flag", "+FLAGS", "spam"],
114
+ 'mv' => ["move_to"],
115
+ 'cp' => ["copy_to"],
116
+ 'cat' => ["append_to_file"]
117
+ }
118
+ args = commands[ARGV.first]
119
+ if args.nil?
120
+ abort "command '#{args.inspect}' not recognized"
121
+ end
122
+ command = args.shift
123
+ imap_client.with_open do |vmail|
124
+ puts "selecting mailbox: #{mailbox}"
125
+ vmail.select_mailbox mailbox
126
+ uid_set.each_slice(5) do |uid_set|
127
+ params = [uid_set.join(',')] + args + ARGV[1..-1]
128
+ puts "executing: #{command} #{params.join(' ')}"
129
+ vmail.send command, *params
130
+ end
131
+ end
132
+ end
133
+
134
+ private
135
+
136
+ def check_lynx
137
+ # TODO check for elinks, or firefox (how do we parse VMAIL_HTML_PART_REDAER to determine?)
138
+ if `which lynx` == ''
139
+ STDERR.puts "You need to install lynx on your system in order to see html-only messages"
140
+ sleep 3
141
+ end
142
+ end
143
+
144
+ def parse_query
145
+ mailbox = if ARGV[0] =~ /^\d+/
146
+ "INBOX"
147
+ else
148
+ ARGV.shift || 'INBOX'
149
+ end
150
+ query = Vmail::Query.parse(ARGV)
151
+ [mailbox, query]
152
+ end
92
153
  end
93
154
 
data/lib/vmail.vim CHANGED
@@ -286,7 +286,7 @@ function! s:toggle_star() range
286
286
  endif
287
287
  endfunction
288
288
 
289
- " flag can be Deleted or [Gmail]/Spam
289
+ " flag can be Deleted or spam
290
290
  func! s:delete_messages(flag) range
291
291
  let uid_set = s:collect_uids(a:firstline, a:lastline)
292
292
  let nummsgs = len(uid_set)
@@ -339,7 +339,7 @@ func! s:append_messages_to_file() range
339
339
  return
340
340
  endif
341
341
  let s:append_file = append_file
342
- let command = s:append_to_file_command . s:append_file . ' ' . join(uid_set, ',')
342
+ let command = s:append_to_file_command . join(uid_set, ',') . ' ' . s:append_file
343
343
  echo "appending " . nummsgs . " message" . (nummsgs == 1 ? '' : 's') . " to " . s:append_file . ". please wait..."
344
344
  let res = system(command)
345
345
  echo res
@@ -497,7 +497,7 @@ function! s:select_mailbox()
497
497
  " now get latest 100 messages
498
498
  call s:focus_list_window()
499
499
  setlocal modifiable
500
- let command = s:search_command . "100 all"
500
+ let command = s:search_command . shellescape("100 all")
501
501
  echo "loading messages..."
502
502
  let res = system(command)
503
503
  1,$delete
@@ -527,16 +527,7 @@ function! s:do_search()
527
527
  " close message window if open
528
528
  call s:focus_message_window()
529
529
  close
530
- " if query doesn't start with a number, set max returned to 100
531
- let limit = 100
532
- let imap_query = s:query
533
- if match(s:query, '^\d') == 0
534
- let query_chunks = split(s:query, '\s')
535
- let limit = remove(query_chunks, 0)
536
- let imap_query = join(query_chunks, ' ')
537
- end
538
- let s:query = limit . ' ' . imap_query
539
- let command = s:search_command . limit . ' ' . shellescape(imap_query)
530
+ let command = s:search_command . shellescape(s:query)
540
531
  redraw
541
532
  call s:focus_list_window()
542
533
  setlocal modifiable
@@ -778,7 +769,7 @@ func! s:message_window_mappings()
778
769
 
779
770
  nnoremap <silent> <buffer> <leader># :close<cr>:call <SID>focus_list_window()<cr>:call <SID>delete_messages("Deleted")<cr>
780
771
  nnoremap <silent> <buffer> <leader>* :call <SID>focus_list_window()<cr>:call <SID>toggle_star()<cr>
781
- noremap <silent> <buffer> <leader>! :call <SID>focus_list_window()<cr>:call <SID>delete_messages("[Gmail]/Spam")<CR>
772
+ noremap <silent> <buffer> <leader>! :call <SID>focus_list_window()<cr>:call <SID>delete_messages("spam")<CR>
782
773
  noremap <silent> <buffer> <leader>e :call <SID>focus_list_window()<cr>:call <SID>archive_messages()<CR>
783
774
  " alt mappings for lazy hands
784
775
  nmap <silent> <buffer> <leader>8 <leader>*
@@ -805,12 +796,12 @@ func! s:message_list_window_mappings()
805
796
 
806
797
  noremap <silent> <buffer> <leader>* :call <SID>toggle_star()<CR>
807
798
  noremap <silent> <buffer> <leader># :call <SID>delete_messages("Deleted")<CR>
808
- noremap <silent> <buffer> <leader>! :call <SID>delete_messages("[Gmail]/Spam")<CR>
799
+ noremap <silent> <buffer> <leader>! :call <SID>delete_messages("spam")<CR>
809
800
  noremap <silent> <buffer> <leader>e :call <SID>archive_messages()<CR>
810
801
  " alt mappings for lazy hands
811
802
  noremap <silent> <buffer> <leader>8 :call <SID>toggle_star()<CR>
812
803
  noremap <silent> <buffer> <leader>3 :call <SID>delete_messages("Deleted")<CR>
813
- noremap <silent> <buffer> <leader>1 :call <SID>delete_messages("[Gmail]/Spam")<CR>
804
+ noremap <silent> <buffer> <leader>1 :call <SID>delete_messages("spam")<CR>
814
805
  " nmap <silent> <buffer> <leader>8 <leader>*
815
806
  " nmap <silent> <buffer> <leader>3 <leader>#
816
807
  " nmap <silent> <buffer> <leader>1 <leader>!
@@ -33,6 +33,7 @@ module Vmail
33
33
  @imap_port = config['port'] || 993
34
34
  @current_mail = nil
35
35
  @current_message_uid = nil
36
+ @width = 140
36
37
  end
37
38
 
38
39
  # holds mail objects keyed by [mailbox, uid]
@@ -52,6 +53,14 @@ module Vmail
52
53
  list_mailboxes # prefetch mailbox list
53
54
  end
54
55
 
56
+ # expects a block, closes on finish
57
+ def with_open
58
+ @imap = Net::IMAP.new(@imap_server, @imap_port, true, nil, false)
59
+ log @imap.login(@username, @password)
60
+ yield self
61
+ close
62
+ end
63
+
55
64
  def close
56
65
  log "closing connection"
57
66
  Timeout::timeout(10) do
@@ -82,10 +91,12 @@ module Vmail
82
91
  end
83
92
 
84
93
  def reload_mailbox
94
+ return unless STDIN.tty?
85
95
  select_mailbox(@mailbox, true)
86
96
  end
87
97
 
88
98
  def clear_cached_message
99
+ return unless STDIN.tty?
89
100
  log "CLEARING CACHED MESSAGE"
90
101
  @current_mail = nil
91
102
  @current_message_uid = nil
@@ -164,6 +175,9 @@ module Vmail
164
175
  end
165
176
  new_message_rows = fetch_envelopes(id_set, are_uids, is_update)
166
177
  new_message_rows.map {|x| x[:row_text]}.join("\n")
178
+ rescue # Encoding::CompatibilityError (only in 1.9.2)
179
+ log "Error in fetch_row_text:\n#{$!}\n#{$!.backtrace}"
180
+ new_message_rows.map {|x| Iconv.conv('US-ASCII//TRANSLIT//IGNORE', 'UTF-8', x[:row_text])}.join("\n")
167
181
  end
168
182
 
169
183
  def fetch_envelopes(id_set, are_uids, is_update)
@@ -226,7 +240,7 @@ module Vmail
226
240
  mid_width = @width - 38
227
241
  address_col_width = (mid_width * 0.3).ceil
228
242
  subject_col_width = (mid_width * 0.7).floor
229
- identifier = [@mailbox, [seqno.to_i, uid.to_i].join(':')].join(';')
243
+ identifier = [seqno.to_i, uid.to_i].join(':')
230
244
  row_text = [ flags.col(2),
231
245
  (date_formatted || '').col(14),
232
246
  address.col(address_col_width),
@@ -267,13 +281,16 @@ module Vmail
267
281
  flags.join('')
268
282
  end
269
283
 
270
- def search(limit, *query)
271
- limit = limit.to_i
272
- limit = 100 if limit.to_s !~ /^\d+$/
273
- query = ['ALL'] if query.empty?
284
+ def search(query)
285
+ query = Vmail::Query.parse(query)
286
+ @limit = query.shift.to_i
287
+ # a limit of zero is effectively no limit
288
+ if @limit == 0
289
+ @limit = @num_messages
290
+ end
274
291
  if query.size == 1 && query[0].downcase == 'all'
275
292
  # form a sequence range
276
- query.unshift [[@num_messages - limit.to_i + 1 , 1].max, @num_messages].join(':')
293
+ query.unshift [[@num_messages - @limit + 1 , 1].max, @num_messages].join(':')
277
294
  @all_search = true
278
295
  else # this is a special query search
279
296
  # set the target range to the whole set
@@ -281,28 +298,36 @@ module Vmail
281
298
  @all_search = false
282
299
  end
283
300
  @query = query.map {|x| x.to_s.downcase}
284
- @limit = limit
285
- log "search query: #{@query.inspect}"
301
+ query_string = Vmail::Query.args2string(@query)
302
+ log "search query: #{@query} > #{query_string.inspect}"
286
303
  log "- @all_search #{@all_search}"
287
304
  @query = query
288
305
  @ids = reconnect_if_necessary(180) do # increase timeout to 3 minutes
289
- @imap.search(@query.join(' '))
306
+ @imap.search(query_string)
290
307
  end
291
308
  # save ids in @ids, because filtered search relies on it
292
309
  fetch_ids = if @all_search
293
310
  @ids
294
311
  else #filtered search
295
- @start_index = [@ids.length - limit, 0].max
312
+ @start_index = [@ids.length - @limit, 0].max
296
313
  @ids[@start_index..-1]
297
314
  end
298
315
  self.max_seqno = @ids[-1]
299
316
  log "- search query got #{@ids.size} results; max seqno: #{self.max_seqno}"
300
317
  clear_cached_message
301
318
  res = fetch_row_text(fetch_ids)
302
- add_more_message_line(res, fetch_ids[0])
319
+ if STDOUT.tty?
320
+ add_more_message_line(res, fetch_ids[0])
321
+ else
322
+ # non interactive mode
323
+ puts [@mailbox, res].join("\n")
324
+ end
325
+ rescue
326
+ log "ERROR:\n#{$!.inspect}\n#{$!.backtrace.join("\n")}"
303
327
  end
304
328
 
305
329
  def decrement_max_seqno(num)
330
+ return unless STDIN.tty?
306
331
  log "Decremented max seqno from #{self.max_seqno} to #{self.max_seqno - num}"
307
332
  self.max_seqno -= num
308
333
  end
@@ -318,7 +343,7 @@ module Vmail
318
343
  update_query[0] = "#{old_num_messages}:#{@num_messages}"
319
344
  ids = reconnect_if_necessary {
320
345
  log "search #update_query"
321
- @imap.search(update_query.join(' '))
346
+ @imap.search(Vmail::Query.args2string(update_query))
322
347
  }
323
348
  log "- got seqnos: #{ids.inspect}"
324
349
  log "- getting seqnos > #{self.max_seqno}"
@@ -462,13 +487,13 @@ EOF
462
487
  # id_set is a string comming from the vim client
463
488
  # action is -FLAGS or +FLAGS
464
489
  def flag(uid_set, action, flg)
490
+ log "flag #{uid_set} #{flg} #{action}"
465
491
  uid_set = uid_set.split(',').map(&:to_i)
466
- log "flag #{uid_set.inspect} #{flg} #{action}"
467
492
  if flg == 'Deleted'
468
493
  log "Deleting uid_set: #{uid_set.inspect}"
469
494
  decrement_max_seqno(uid_set.size)
470
495
  # for delete, do in a separate thread because deletions are slow
471
- Thread.new do
496
+ spawn_thread_if_tty do
472
497
  unless @mailbox == '[Gmail]/Trash'
473
498
  log "@imap.uid_copy #{uid_set.inspect} to trash"
474
499
  log @imap.uid_copy(uid_set, "[Gmail]/Trash")
@@ -478,10 +503,10 @@ EOF
478
503
  reload_mailbox
479
504
  clear_cached_message
480
505
  end
481
- elsif flg == '[Gmail]/Spam'
506
+ elsif flg == 'spam' || flg == '[Gmail]/Spam'
482
507
  log "Marking as spam uid_set: #{uid_set.inspect}"
483
508
  decrement_max_seqno(uid_set.size)
484
- Thread.new do
509
+ spawn_thread_if_tty do
485
510
  log "@imap.uid_copy #{uid_set.inspect} to spam"
486
511
  log @imap.uid_copy(uid_set, "[Gmail]/Spam")
487
512
  log "@imap.uid_store #{uid_set.inspect} #{action} [:Deleted]"
@@ -489,10 +514,9 @@ EOF
489
514
  reload_mailbox
490
515
  clear_cached_message
491
516
  end
492
- "#{id} deleted"
493
517
  else
494
518
  log "Flagging uid_set: #{uid_set.inspect}"
495
- Thread.new do
519
+ spawn_thread_if_tty do
496
520
  log "@imap.uid_store #{uid_set.inspect} #{action} [#{flg.to_sym}]"
497
521
  log @imap.uid_store(uid_set, action, [flg.to_sym])
498
522
  end
@@ -511,7 +535,7 @@ EOF
511
535
  end
512
536
  create_if_necessary mailbox
513
537
  log "moving uid_set: #{uid_set.inspect} to #{mailbox}"
514
- Thread.new do
538
+ spawn_thread_if_tty do
515
539
  log @imap.uid_copy(uid_set, mailbox)
516
540
  log @imap.uid_store(uid_set, '+FLAGS', [:Deleted])
517
541
  reload_mailbox
@@ -527,12 +551,22 @@ EOF
527
551
  end
528
552
  create_if_necessary mailbox
529
553
  log "copying #{uid_set.inspect} to #{mailbox}"
530
- Thread.new do
554
+ spawn_thread_if_tty do
531
555
  log @imap.uid_copy(uid_set, mailbox)
532
556
  log "copied uid_set #{uid_set.inspect} to #{mailbox}"
533
557
  end
534
558
  end
535
559
 
560
+ def spawn_thread_if_tty
561
+ if STDIN.tty?
562
+ Thread.new do
563
+ yield
564
+ end
565
+ else
566
+ yield
567
+ end
568
+ end
569
+
536
570
  def create_if_necessary(mailbox)
537
571
  current_mailboxes = mailboxes.map {|m| MailboxAliases[m] || m}
538
572
  if !current_mailboxes.include?(mailbox)
@@ -544,7 +578,7 @@ EOF
544
578
  end
545
579
  end
546
580
 
547
- def append_to_file(file, uid_set)
581
+ def append_to_file(uid_set, file)
548
582
  uid_set = uid_set.split(',').map(&:to_i)
549
583
  log "append to file uid set #{uid_set.inspect} to file: #{file}"
550
584
  uid_set.each do |uid|
data/lib/vmail/options.rb CHANGED
@@ -48,9 +48,9 @@ module Vmail
48
48
  begin
49
49
  opts.parse!(argv)
50
50
  if @config_file && File.exists?(@config_file)
51
- puts "using config file: #{@config_file}"
51
+ STDERR.puts "using config file: #{@config_file}"
52
52
  else
53
- puts <<EOF
53
+ STDERR.puts <<EOF
54
54
 
55
55
  Missing config file!
56
56
 
@@ -59,11 +59,13 @@ EOF
59
59
  exit(1)
60
60
  end
61
61
 
62
- if @contacts_file.nil?
63
- puts "No contacts file found for auto-completion. See help for how to generate it."
64
- sleep 0.5
65
- else
66
- puts "using contacts file: #{@contacts_file}"
62
+ if STDOUT.tty?
63
+ if @contacts_file.nil?
64
+ STDERR.puts "No contacts file found for auto-completion. See help for how to generate it."
65
+ sleep 0.5
66
+ else
67
+ STDERR.puts "using contacts file: #{@contacts_file}"
68
+ end
67
69
  end
68
70
 
69
71
  @config = YAML::load(File.read(@config_file))
@@ -0,0 +1,28 @@
1
+ require 'shellwords'
2
+ module Vmail
3
+ class Query
4
+ # args is an array like ARGV
5
+ def self.parse(args)
6
+ if args.is_a?(String)
7
+ args = Shellwords.shellwords args
8
+ end
9
+ query = if args.empty?
10
+ [100, 'ALL']
11
+ elsif args.size == 1 && args[0] =~ /^\d+/
12
+ [args.shift, "ALL"]
13
+ elsif args[0] =~ /^\d+/
14
+ args
15
+ else
16
+ [100] + args
17
+ end
18
+ query
19
+ end
20
+
21
+ def self.args2string(array)
22
+ array.map {|x|
23
+ x.to_s.split(/\s+/).size > 1 ? "\"#{x}\"" : x.to_s
24
+ }.join(' ')
25
+ end
26
+
27
+ end
28
+ end
data/lib/vmail/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Vmail
2
- VERSION = "1.2.3"
2
+ VERSION = "1.2.4"
3
3
  end
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 1
7
7
  - 2
8
- - 3
9
- version: 1.2.3
8
+ - 4
9
+ version: 1.2.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: 2011-01-06 00:00:00 -05:00
17
+ date: 2011-01-08 00:00:00 -05:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
@@ -73,6 +73,7 @@ files:
73
73
  - lib/vmail/imap_client.rb
74
74
  - lib/vmail/message_formatter.rb
75
75
  - lib/vmail/options.rb
76
+ - lib/vmail/query.rb
76
77
  - lib/vmail/reply_template.rb
77
78
  - lib/vmail/send_options.rb
78
79
  - lib/vmail/sender.rb