vmail 1.2.3 → 1.2.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/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