vmail 1.6.8 → 1.6.9

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 ADDED
@@ -0,0 +1,27 @@
1
+
2
+ ------------------------------------------------------------------------
3
+ Sun Jun 26 18:07:59 EDT 2011
4
+
5
+ Sqlite3 branch
6
+
7
+ Use sqlite3 as a cache.
8
+ fetch UIDS, and then check and cache.
9
+
10
+ Also, log output to primary thread and let Vmail echo via Vim. Change vmail.vim
11
+ How do make this output progressive?
12
+
13
+ What if there is a queue for IMAP tasks instead of multiple untracked threads?
14
+
15
+ Check for new messages can just reload the current mailbox
16
+ => simplify the code
17
+
18
+ Build queue of stuff for Vmail to do
19
+ => this may lead to fewer errors than multithreaded delete (maybe too many
20
+ threads are spawned)
21
+
22
+ TODO
23
+
24
+ No number number limit inbox 100. NOTE IN DOCUMENTATION
25
+
26
+
27
+
@@ -0,0 +1,25 @@
1
+ drop table if exists version;
2
+ create table version (
3
+ vmail_version text
4
+ );
5
+ create table if not exists messages (
6
+ message_id text PRIMARY KEY,
7
+ size integer,
8
+ flags text,
9
+ subject text,
10
+ sender text,
11
+ recipients text,
12
+ date text,
13
+ plaintext text,
14
+ rfc822 text
15
+ );
16
+ create table if not exists labelings (
17
+ label_id integer,
18
+ message_id text,
19
+ uid integer
20
+ );
21
+ create table if not exists labels (
22
+ label_id integer PRIMARY KEY,
23
+ name text UNIQUE
24
+ );
25
+
@@ -58,20 +58,18 @@ module Vmail
58
58
  server.select_mailbox mailbox
59
59
 
60
60
  STDERR.puts "Mailbox: #{mailbox}"
61
- STDERR.puts "Query: #{query.inspect} => #{query_string}"
61
+ STDERR.puts "Query: #{query.inspect}"
62
+ STDERR.puts "Query String: #{String.shellescape(query_string)}"
62
63
 
63
64
  buffer_file = "vmailbuffer"
64
65
  # invoke vim
65
66
  vimscript = File.expand_path("../vmail.vim", __FILE__)
66
- 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}"
67
+ vim_command = "DRB_URI=#{drb_uri} VMAIL_CONTACTS_FILE=#{contacts_file} VMAIL_MAILBOX=#{String.shellescape(mailbox)} VMAIL_QUERY=\"#{query_string}\" #{vim} -S #{vimscript} #{buffer_file}"
67
68
  STDERR.puts vim_command
68
69
  STDERR.puts "Using buffer file: #{buffer_file}"
69
70
  File.open(buffer_file, "w") do |file|
70
- file.puts "Vmail starting with values:\n"
71
- file.puts "- drb uri: #{drb_uri}"
72
- file.puts "- mailbox: #{mailbox}"
73
- file.puts "- query: #{query_string}\n"
74
- file.puts "Fetching messages. please wait..."
71
+ file.puts "\n\nVmail #{Vmail::VERSION}\n\n"
72
+ file.puts "Please wait while I fetch your messages.\n\n\n"
75
73
  end
76
74
 
77
75
  system(vim_command)
@@ -157,13 +155,15 @@ module Vmail
157
155
  end
158
156
 
159
157
  def parse_query
160
- mailbox = if ARGV[0] =~ /^\d+/
161
- "INBOX"
162
- else
163
- ARGV.shift || 'INBOX'
164
- end
158
+ if ARGV[0] =~ /^\d+/
159
+ ARGV.shift
160
+ end
161
+ mailbox = ARGV.shift || 'INBOX'
165
162
  query = Vmail::Query.parse(ARGV)
166
163
  [mailbox, query]
167
164
  end
168
165
  end
169
166
 
167
+ if __FILE__ == $0
168
+ Vmail.start
169
+ end
@@ -75,7 +75,7 @@ function! s:show_message(stay_in_message_list)
75
75
  call s:more_messages()
76
76
  return
77
77
  endif
78
- let s:uid = matchstr(line, '\d\+$')
78
+ let s:uid = shellescape(matchstr(line, '\S\+$'))
79
79
  if s:uid == ""
80
80
  return
81
81
  end
@@ -219,7 +219,6 @@ func! s:close_message_window()
219
219
  endif
220
220
  endfunc
221
221
 
222
-
223
222
  " gets new messages since last update
224
223
  function! s:update()
225
224
  let command = s:update_command
@@ -227,13 +226,11 @@ function! s:update()
227
226
  let res = system(command)
228
227
  if len(split(res, "\n", '')) > 0
229
228
  setlocal modifiable
230
- let line = line('$')
231
- silent $put =res
229
+ call append(0, res)
232
230
  setlocal nomodifiable
233
231
  write!
234
232
  let num = len(split(res, '\n', ''))
235
- call cursor(line + 1, 0)
236
- normal z.
233
+ call cursor(0, 0)
237
234
  redraw
238
235
  echom "You have " . num . " new message" . (num == 1 ? '' : 's') . "!"
239
236
  else
@@ -251,7 +248,7 @@ function! s:toggle_star() range
251
248
  if (match(getline(a:firstline), flag_symbol) != -1)
252
249
  let action = " -FLAGS"
253
250
  endif
254
- let command = s:flag_command . join(uid_set, ',') . action . " Flagged"
251
+ let command = s:flag_command . shellescape(join(uid_set, ',')) . action . " Flagged"
255
252
  if nummsgs == 1
256
253
  echom "Toggling flag on message"
257
254
  else
@@ -287,7 +284,7 @@ endfunction
287
284
  func! s:delete_messages(flag) range
288
285
  let uid_set = s:collect_uids(a:firstline, a:lastline)
289
286
  let nummsgs = len(uid_set)
290
- let command = s:flag_command . join(uid_set, ',') . " +FLAGS " . a:flag
287
+ let command = s:flag_command . shellescape(join(uid_set, ',')) . " +FLAGS " . a:flag
291
288
  if nummsgs == 1
292
289
  echom "Deleting message"
293
290
  else
@@ -305,7 +302,7 @@ endfunc
305
302
  func! s:archive_messages() range
306
303
  let uid_set = s:collect_uids(a:firstline, a:lastline)
307
304
  let nummsgs = len(uid_set)
308
- let command = s:move_to_command . join(uid_set, ',') . ' ' . "all"
305
+ let command = s:move_to_command . shellescape(join(uid_set, ',')) . ' ' . "all"
309
306
  echo "Archiving message" . (nummsgs == 1 ? '' : 's')
310
307
  let res = system(command)
311
308
  setlocal modifiable
@@ -328,7 +325,7 @@ func! s:append_messages_to_file() range
328
325
  return
329
326
  endif
330
327
  let s:append_file = append_file
331
- let command = s:append_to_file_command . join(uid_set, ',') . ' ' . s:append_file
328
+ let command = s:append_to_file_command . shellescape(join(uid_set, ',')) . ' ' . s:append_file
332
329
  echo "Appending " . nummsgs . " message" . (nummsgs == 1 ? '' : 's') . " to " . s:append_file . ". Please wait..."
333
330
  let res = system(command)
334
331
  echo res
@@ -341,7 +338,7 @@ function! s:move_to_mailbox(copy) range
341
338
  let s:copy_to_mailbox = a:copy
342
339
  let uid_set = s:collect_uids(a:firstline, a:lastline)
343
340
  let s:nummsgs = len(uid_set)
344
- let s:uid_set = join(uid_set, ',')
341
+ let s:uid_set = shellescape(join(uid_set, ','))
345
342
  " now prompt use to select mailbox
346
343
  if !exists("s:mailboxes")
347
344
  call s:get_mailbox_list()
@@ -474,7 +471,7 @@ function! s:select_mailbox()
474
471
  return
475
472
  endif
476
473
  let s:mailbox = mailbox
477
- let s:query = "100 all"
474
+ let s:query = "all"
478
475
  let command = s:select_mailbox_command . shellescape(s:mailbox)
479
476
  redraw
480
477
  echom "Selecting mailbox: ". s:mailbox . ". Please wait..."
@@ -483,16 +480,15 @@ function! s:select_mailbox()
483
480
  " now get latest 100 messages
484
481
  call s:focus_list_window()
485
482
  setlocal modifiable
486
- let command = s:search_command . shellescape("100 all")
483
+ let command = s:search_command . shellescape("all")
487
484
  echo "Loading messages..."
488
485
  let res = system(command)
489
486
  silent 1,$delete
490
487
  silent! put! =res
491
488
  execute "normal Gdd\<c-y>"
492
- normal G
493
489
  setlocal nomodifiable
494
490
  write
495
- normal z.
491
+ normal gg
496
492
  redraw
497
493
  echom "Current mailbox: ". s:mailbox
498
494
  endfunction
@@ -524,20 +520,19 @@ function! s:do_search()
524
520
  execute "silent normal Gdd\<c-y>"
525
521
  setlocal nomodifiable
526
522
  write
527
- normal z.
523
+ normal gg
528
524
  endfunction
529
525
 
530
526
  function! s:more_messages()
531
- let line = getline(line('.'))
532
- let seqno = get(split(matchstr(line, '\d\+:\d\+$'), ':'), 0)
533
- let command = s:more_messages_command . seqno
527
+ let command = s:more_messages_command
534
528
  echo "Fetching more messages. Please wait..."
535
529
  let res = system(command)
536
530
  setlocal modifiable
537
531
  let lines = split(res, "\n")
538
- call append(0, lines)
532
+ call append(line('$'), lines)
539
533
  " execute "normal Gdd\<c-y>"
540
534
  setlocal nomodifiable
535
+ normal j
541
536
  endfunction
542
537
 
543
538
  " --------------------------------------------------------------------------------
@@ -576,8 +571,9 @@ func! s:open_compose_window(command)
576
571
  setlocal modifiable
577
572
  " TODO maybe later save backups?
578
573
  setlocal buftype=nowrite
574
+ "
579
575
  if winnr('$') > 1
580
- wincmd p
576
+ call s:focus_list_window()
581
577
  close!
582
578
  endif
583
579
  silent 1,$delete
@@ -736,7 +732,7 @@ function! s:collect_uids(startline, endline)
736
732
  let uid_set = []
737
733
  let lnum = a:startline
738
734
  while lnum <= a:endline
739
- let uid = matchstr(getline(lnum), '\d\+$')
735
+ let uid = matchstr(getline(lnum), '\S\+$')
740
736
  call add(uid_set, uid)
741
737
  let lnum += 1
742
738
  endwhile
@@ -857,7 +853,7 @@ call system(s:set_window_width_command . winwidth(1))
857
853
  autocmd VimResized <buffer> call system(s:set_window_width_command . winwidth(1))
858
854
 
859
855
  autocmd bufreadpost *.txt call <SID>turn_into_compose_window()
860
-
856
+ normal G
861
857
  call system(s:select_mailbox_command . shellescape(s:mailbox))
862
858
  call s:do_search()
863
859
 
@@ -0,0 +1,30 @@
1
+ require 'sequel'
2
+
3
+ DB = Sequel.connect 'sqlite://vmail.db'
4
+
5
+
6
+ if !File.exists?("vmail.db")
7
+ create_table_script = File.expand_path("../db/create.sql", __FILE__)
8
+ DB.run create_table_script
9
+ end
10
+
11
+ if DB[:version].count == 0
12
+ DB[:version].insert(:vmail_version => Vmail::VERSION)
13
+ end
14
+
15
+ class Vmail::Message < Sequel::Model
16
+ set_primary_key :message_id
17
+ one_to_many :labelings
18
+ many_to_many :labels, :join_table => 'labelings'
19
+ end
20
+
21
+ class Vmail::Label < Sequel::Model
22
+ set_primary_key :label_id
23
+ one_to_many :labelings
24
+ many_to_many :messages, :join_table => 'labelings'
25
+ end
26
+
27
+ class Vmail::Labeling < Sequel::Model
28
+ end
29
+
30
+
@@ -0,0 +1,91 @@
1
+ module Vmail
2
+ module FlaggingAndMoving
3
+
4
+ def convert_to_message_ids(message_ids)
5
+ message_ids.split(',').map {|message_id|
6
+ labeling = Labeling[message_id: message_id, label_id: @label.label_id]
7
+ labeling.uid
8
+ }
9
+ end
10
+
11
+ # uid_set is a string comming from the vim client
12
+ # action is -FLAGS or +FLAGS
13
+ def flag(message_ids, action, flg)
14
+ uid_set = convert_to_message_ids(message_ids)
15
+ log "Flag #{uid_set} #{flg} #{action}"
16
+ if flg == 'Deleted'
17
+ log "Deleting uid_set: #{uid_set.inspect}"
18
+ decrement_max_seqno(uid_set.size)
19
+ # for delete, do in a separate thread because deletions are slow
20
+ spawn_thread_if_tty do
21
+ unless @mailbox == mailbox_aliases['trash']
22
+ log "imap.uid_copy #{uid_set.inspect} to #{mailbox_aliases['trash']}"
23
+ log @imap.uid_copy(uid_set, mailbox_aliases['trash'])
24
+ end
25
+ log "imap.uid_store #{uid_set.inspect} #{action} [#{flg.to_sym}]"
26
+ log @imap.uid_store(uid_set, action, [flg.to_sym])
27
+ reload_mailbox
28
+ clear_cached_message
29
+ end
30
+ elsif flg == 'spam' || flg == mailbox_aliases['spam']
31
+ log "Marking as spam uid_set: #{uid_set.inspect}"
32
+ decrement_max_seqno(uid_set.size)
33
+ spawn_thread_if_tty do
34
+ log "imap.uid_copy #{uid_set.inspect} to #{mailbox_aliases['spam']}"
35
+ log @imap.uid_copy(uid_set, mailbox_aliases['spam'])
36
+ log "imap.uid_store #{uid_set.inspect} #{action} [:Deleted]"
37
+ log @imap.uid_store(uid_set, action, [:Deleted])
38
+ reload_mailbox
39
+ clear_cached_message
40
+ end
41
+ else
42
+ log "Flagging uid_set: #{uid_set.inspect}"
43
+ spawn_thread_if_tty do
44
+ log "imap.uid_store #{uid_set.inspect} #{action} [#{flg.to_sym}]"
45
+ log @imap.uid_store(uid_set, action, [flg.to_sym])
46
+ end
47
+ end
48
+ rescue
49
+ log $!
50
+ end
51
+
52
+ def move_to(message_ids, mailbox)
53
+ uid_set = convert_to_message_ids(message_ids)
54
+ decrement_max_seqno(uid_set.size)
55
+ log "Move #{uid_set.inspect} to #{mailbox}"
56
+ if mailbox == 'all'
57
+ log "Archiving messages"
58
+ end
59
+ if mailbox_aliases[mailbox]
60
+ mailbox = mailbox_aliases[mailbox]
61
+ end
62
+ create_if_necessary mailbox
63
+ log "Moving uid_set: #{uid_set.inspect} to #{mailbox}"
64
+ spawn_thread_if_tty do
65
+ log @imap.uid_copy(uid_set, mailbox)
66
+ log @imap.uid_store(uid_set, '+FLAGS', [:Deleted])
67
+ reload_mailbox
68
+ clear_cached_message
69
+ log "Moved uid_set #{uid_set.inspect} to #{mailbox}"
70
+ end
71
+ rescue
72
+ log $!
73
+ end
74
+
75
+ def copy_to(message_ids, mailbox)
76
+ uid_set = convert_to_message_ids(message_ids)
77
+ if mailbox_aliases[mailbox]
78
+ mailbox = mailbox_aliases[mailbox]
79
+ end
80
+ create_if_necessary mailbox
81
+ log "Copying #{uid_set.inspect} to #{mailbox}"
82
+ spawn_thread_if_tty do
83
+ log @imap.uid_copy(uid_set, mailbox)
84
+ log "Copied uid_set #{uid_set.inspect} to #{mailbox}"
85
+ end
86
+ rescue
87
+ log $!
88
+ end
89
+ end
90
+
91
+ end
@@ -6,13 +6,22 @@ require 'mail'
6
6
  require 'net/imap'
7
7
  require 'time'
8
8
  require 'logger'
9
+ require 'vmail/helpers'
9
10
  require 'vmail/address_quoter'
11
+ require 'vmail/database'
12
+ require 'vmail/searching'
13
+ require 'vmail/showing_headers'
14
+ require 'vmail/showing_message'
15
+ require 'vmail/flagging_and_moving'
10
16
 
11
17
  module Vmail
12
18
  class ImapClient
19
+ include Vmail::Helpers
13
20
  include Vmail::AddressQuoter
14
-
15
- DIVIDER_WIDTH = 46
21
+ include Vmail::Searching
22
+ include Vmail::ShowingHeaders
23
+ include Vmail::ShowingMessage
24
+ include Vmail::FlaggingAndMoving
16
25
 
17
26
  attr_accessor :max_seqno # of current mailbox
18
27
 
@@ -26,22 +35,10 @@ module Vmail
26
35
  @logger.level = Logger::DEBUG
27
36
  @imap_server = config['server'] || 'imap.gmail.com'
28
37
  @imap_port = config['port'] || 993
29
- @current_mail = nil
30
- @current_message_uid = nil
31
- @width = 140
32
- end
33
-
34
- # holds mail objects keyed by [mailbox, uid]
35
- def message_cache
36
- @message_cache ||= {}
37
- size = @message_cache.values.reduce(0) {|sum, x| sum + x[:size]}
38
- if size > 2_000_000 # TODO make this configurable
39
- log "Pruning message cache; message cache is consuming #{number_to_human_size size}"
40
- @message_cache.keys[0, @message_cache.size / 2].each {|k| @message_cache.delete(k)}
41
- end
42
- @message_cache
38
+ current_message = nil
43
39
  end
44
40
 
41
+
45
42
  def open
46
43
  @imap = Net::IMAP.new(@imap_server, @imap_port, true, nil, false)
47
44
  log @imap.login(@username, @password)
@@ -69,19 +66,23 @@ module Vmail
69
66
  if mailbox_aliases[mailbox]
70
67
  mailbox = mailbox_aliases[mailbox]
71
68
  end
72
- if mailbox == @mailbox && !force
73
- return
74
- end
75
69
  log "Selecting mailbox #{mailbox.inspect}"
76
70
  reconnect_if_necessary(15) do
77
71
  log @imap.select(mailbox)
78
72
  end
79
73
  log "Done"
74
+
80
75
  @mailbox = mailbox
76
+ @label = Label[name: @mailbox] || Label.create(name: @mailbox)
77
+
81
78
  log "Getting mailbox status"
82
79
  get_mailbox_status
83
80
  log "Getting highest message id"
84
81
  get_highest_message_id
82
+ if @next_window_width
83
+ @width = @next_window_width
84
+ end
85
+
85
86
  return "OK"
86
87
  end
87
88
 
@@ -90,12 +91,12 @@ module Vmail
90
91
  select_mailbox(@mailbox, true)
91
92
  end
92
93
 
94
+ # TODO no need for this if all shown messages are stored in SQLITE3
95
+ # and keyed by UID.
93
96
  def clear_cached_message
94
97
  return unless STDIN.tty?
95
- log "CLEARING CACHED MESSAGE"
96
- @current_mail = nil
97
- @current_message_uid = nil
98
- @current_message = nil
98
+ log "Clearing cached message"
99
+ current_message = nil
99
100
  end
100
101
 
101
102
  def get_highest_message_id
@@ -103,7 +104,7 @@ module Vmail
103
104
  res = @imap.fetch([1,"*"], ["ENVELOPE"])
104
105
  if res
105
106
  @num_messages = res[-1].seqno
106
- log "HIGHEST ID: #@num_messages"
107
+ log "Highest seqno: #@num_messages"
107
108
  else
108
109
  @num_messages = 1
109
110
  log "NO HIGHEST ID: setting @num_messages to 1"
@@ -180,178 +181,18 @@ module Vmail
180
181
  @mailboxes
181
182
  end
182
183
 
183
- # id_set may be a range, array, or string
184
- def fetch_row_text(id_set, are_uids=false, is_update=false)
185
- log "Fetch_row_text: #{id_set.inspect}"
186
- if id_set.is_a?(String)
187
- id_set = id_set.split(',')
188
- end
189
- if id_set.to_a.empty?
190
- log "- empty set"
191
- return ""
192
- end
193
- new_message_rows = fetch_envelopes(id_set, are_uids, is_update)
194
- new_message_rows.map {|x| x[:row_text]}.join("\n")
195
- rescue # Encoding::CompatibilityError (only in 1.9.2)
196
- log "Error in fetch_row_text:\n#{$!}\n#{$!.backtrace}"
197
- new_message_rows.map {|x| Iconv.conv('US-ASCII//TRANSLIT//IGNORE', 'UTF-8', x[:row_text])}.join("\n")
198
- end
199
-
200
- def fetch_envelopes(id_set, are_uids, is_update)
201
- results = reconnect_if_necessary do
202
- if are_uids
203
- @imap.uid_fetch(id_set, ["FLAGS", "ENVELOPE", "RFC822.SIZE", "UID" ])
204
- else
205
- @imap.fetch(id_set, ["FLAGS", "ENVELOPE", "RFC822.SIZE", "UID" ])
206
- end
207
- end
208
- if results.nil?
209
- error = "Expected fetch results but got nil"
210
- log(error) && raise(error)
211
- end
212
- log "- extracting headers"
213
- new_message_rows = results.map {|x| extract_row_data(x) }
214
- log "- returning #{new_message_rows.size} new rows and caching result"
215
- new_message_rows
216
- end
217
-
218
- # TODO extract this to another class or module and write unit tests
219
- def extract_row_data(fetch_data)
220
- seqno = fetch_data.seqno
221
- uid = fetch_data.attr['UID']
222
- # log "fetched seqno #{seqno} uid #{uid}"
223
- envelope = fetch_data.attr["ENVELOPE"]
224
- size = fetch_data.attr["RFC822.SIZE"]
225
- flags = fetch_data.attr["FLAGS"]
226
- address_struct = if @mailbox == mailbox_aliases['sent']
227
- structs = envelope.to || envelope.cc
228
- structs.nil? ? nil : structs.first
229
- else
230
- envelope.from.first
231
- end
232
- address = if address_struct.nil?
233
- "Unknown"
234
- elsif address_struct.name
235
- "#{Mail::Encodings.unquote_and_convert_to(address_struct.name, 'UTF-8')} <#{[address_struct.mailbox, address_struct.host].join('@')}>"
236
- else
237
- [Mail::Encodings.unquote_and_convert_to(address_struct.mailbox, 'UTF-8'), Mail::Encodings.unquote_and_convert_to(address_struct.host, 'UTF-8')].join('@')
238
- end
239
- if @mailbox == mailbox_aliases['sent'] && envelope.to && envelope.cc
240
- total_recips = (envelope.to + envelope.cc).size
241
- address += " + #{total_recips - 1}"
242
- end
243
- date = begin
244
- Time.parse(envelope.date).localtime
245
- rescue ArgumentError
246
- Time.now
247
- end
248
-
249
- date_formatted = if date.year != Time.now.year
250
- date.strftime "%b %d %Y" rescue envelope.date.to_s
251
- else
252
- date.strftime "%b %d %I:%M%P" rescue envelope.date.to_s
253
- end
254
- subject = envelope.subject || ''
255
- subject = Mail::Encodings.unquote_and_convert_to(subject, 'UTF-8')
256
- flags = format_flags(flags)
257
- mid_width = @width - 38
258
- address_col_width = (mid_width * 0.3).ceil
259
- subject_col_width = (mid_width * 0.7).floor
260
- identifier = [seqno.to_i, uid.to_i].join(':')
261
- row_text = [ flags.col(2),
262
- (date_formatted || '').col(14),
263
- address.col(address_col_width),
264
- subject.col(subject_col_width),
265
- number_to_human_size(size).rcol(7),
266
- identifier.to_s
267
- ].join(' | ')
268
- {:uid => uid, :seqno => seqno, :row_text => row_text}
269
- rescue
270
- log "Error extracting header for uid #{uid} seqno #{seqno}: #$!\n#{$!.backtrace}"
271
- row_text = "#{seqno.to_s} : error extracting this header"
272
- {:uid => uid, :seqno => seqno, :row_text => row_text}
273
- end
274
-
275
- UNITS = [:b, :kb, :mb, :gb].freeze
276
-
277
- # borrowed from ActionView/Helpers
278
- def number_to_human_size(number)
279
- if number.to_i < 1024
280
- "<1kb" # round up to 1kh
281
- else
282
- max_exp = UNITS.size - 1
283
- exponent = (Math.log(number) / Math.log(1024)).to_i # Convert to base 1024
284
- exponent = max_exp if exponent > max_exp # we need this to avoid overflow for the highest unit
285
- number /= 1024 ** exponent
286
- unit = UNITS[exponent]
287
- "#{number}#{unit}"
288
- end
289
- end
290
-
291
- FLAGMAP = {:Flagged => '*'}
292
- # flags is an array like [:Flagged, :Seen]
293
- def format_flags(flags)
294
- # other flags like "Old" should be hidden here
295
- flags = flags.map {|flag| FLAGMAP[flag] || flag}
296
- flags.delete("Old")
297
- if flags.delete(:Seen).nil?
298
- flags << '+' # unread
299
- end
300
- flags.join('')
301
- end
302
-
303
- def search(query)
304
- query = Vmail::Query.parse(query)
305
- @limit = query.shift.to_i
306
- # a limit of zero is effectively no limit
307
- if @limit == 0
308
- @limit = @num_messages
309
- end
310
- if query.size == 1 && query[0].downcase == 'all'
311
- # form a sequence range
312
- query.unshift [[@num_messages - @limit + 1 , 1].max, @num_messages].join(':')
313
- @all_search = true
314
- else # this is a special query search
315
- # set the target range to the whole set
316
- query.unshift "1:#@num_messages"
317
- @all_search = false
318
- end
319
- @query = query.map {|x| x.to_s.downcase}
320
- query_string = Vmail::Query.args2string(@query)
321
- log "Search query: #{@query} > #{query_string.inspect}"
322
- log "- @all_search #{@all_search}"
323
- @query = query
324
- @ids = reconnect_if_necessary(180) do # increase timeout to 3 minutes
325
- @imap.search(query_string)
326
- end
327
- # save ids in @ids, because filtered search relies on it
328
- fetch_ids = if @all_search
329
- @ids
330
- else #filtered search
331
- @start_index = [@ids.length - @limit, 0].max
332
- @ids[@start_index..-1]
333
- end
334
- self.max_seqno = @ids[-1]
335
- log "- search query got #{@ids.size} results; max seqno: #{self.max_seqno}"
336
- clear_cached_message
337
- res = fetch_row_text(fetch_ids)
338
- if STDOUT.tty?
339
- add_more_message_line(res, fetch_ids[0])
340
- else
341
- # non interactive mode
342
- puts [@mailbox, res].join("\n")
343
- end
344
- rescue
345
- log "ERROR:\n#{$!.inspect}\n#{$!.backtrace.join("\n")}"
346
- end
347
-
348
184
  def decrement_max_seqno(num)
349
185
  return unless STDIN.tty?
350
186
  log "Decremented max seqno from #{self.max_seqno} to #{self.max_seqno - num}"
351
187
  self.max_seqno -= num
352
188
  end
353
189
 
190
+ # TODO why not just reload the current page?
354
191
  def update
192
+ if search_query?
193
+ log "Update aborted because query is search query: #{@query.inspect}"
194
+ return ""
195
+ end
355
196
  prime_connection
356
197
  old_num_messages = @num_messages
357
198
  # we need to re-select the mailbox to get the new highest id
@@ -371,7 +212,7 @@ module Vmail
371
212
  log "- update: new uids: #{new_ids.inspect}"
372
213
  if !new_ids.empty?
373
214
  self.max_seqno = new_ids[-1]
374
- res = fetch_row_text(new_ids, false, true)
215
+ res = get_message_headers(new_ids.reverse, false, true)
375
216
  res
376
217
  else
377
218
  ''
@@ -379,202 +220,15 @@ module Vmail
379
220
  end
380
221
 
381
222
  # gets 100 messages prior to id
382
- def more_messages(message_id, limit=100)
383
- log "More_messages: message_id #{message_id}"
384
- message_id = message_id.to_i
385
- if @all_search
386
- x = [(message_id - limit), 0].max
387
- y = [message_id - 1, 0].max
388
-
389
- res = fetch_row_text((x..y))
390
- add_more_message_line(res, x)
391
- else # filter search query
392
- log "@start_index #@start_index"
393
- x = [(@start_index - limit), 0].max
394
- y = [@start_index - 1, 0].max
395
- @start_index = x
396
- res = fetch_row_text(@ids[x..y])
397
- add_more_message_line(res, @ids[x])
398
- end
399
- end
400
-
401
- def add_more_message_line(res, start_seqno)
402
- log "Add_more_message_line for start_seqno #{start_seqno}"
403
- if @all_search
404
- return res if start_seqno.nil?
405
- remaining = start_seqno - 1
406
- else # filter search
407
- remaining = (@ids.index(start_seqno) || 1) - 1
408
- end
409
- if remaining < 1
410
- log "None remaining"
411
- return "Showing all matches\n" + res
412
- end
413
- log "Remaining messages: #{remaining}"
414
- "> Load #{[100, remaining].min} more messages. #{remaining} remaining.\n" + res
415
- end
416
-
417
- def show_message(uid, raw=false)
418
- log "Show message: #{uid}"
419
- return @current_mail.to_s if raw
420
- uid = uid.to_i
421
- if uid == @current_message_uid
422
- return @current_message
423
- end
424
-
425
- #prefetch_adjacent(index) # deprecated
426
-
427
- # TODO keep state in vim buffers, instead of on Vmail Ruby client
428
- # envelope_data[:row_text] = envelope_data[:row_text].gsub(/^\+ /, ' ').gsub(/^\*\+/, '* ') # mark as read in cache
429
- #seqno = envelope_data[:seqno]
430
-
431
- log "Showing message uid: #{uid}"
432
- data = if x = message_cache[[@mailbox, uid]]
433
- log "- message cache hit"
434
- x
435
- else
436
- log "- fetching and storing to message_cache[[#{@mailbox}, #{uid}]]"
437
- fetch_and_cache(uid)
438
- end
439
- if data.nil?
440
- # retry, though this is a hack!
441
- log "- data is nil. retrying..."
442
- return show_message(uid, raw)
443
- end
444
- # make this more DRY later by directly using a ref to the hash
445
- mail = data[:mail]
446
- size = data[:size]
447
- @current_message_uid = uid
448
- log "- setting @current_mail"
449
- @current_mail = mail # used later to show raw message or extract attachments if any
450
- @current_message = data[:message_text]
451
- rescue
452
- log "Parsing error"
453
- "Error encountered parsing this message:\n#{$!}\n#{$!.backtrace.join("\n")}"
454
- end
455
-
456
- def fetch_and_cache(uid)
457
- if data = message_cache[[@mailbox, uid]]
458
- return data
459
- end
460
- fetch_data = reconnect_if_necessary do
461
- res = @imap.uid_fetch(uid, ["FLAGS", "RFC822", "RFC822.SIZE"])
462
- if res.nil?
463
- # retry one more time ( find a more elegant way to do this )
464
- res = @imap.uid_fetch(uid, ["FLAGS", "RFC822", "RFC822.SIZE"])
465
- end
466
- res[0]
467
- end
468
- # USE THIS
469
- size = fetch_data.attr["RFC822.SIZE"]
470
- flags = fetch_data.attr["FLAGS"]
471
- mail = Mail.new(fetch_data.attr['RFC822'])
472
- formatter = Vmail::MessageFormatter.new(mail)
473
- message_text = <<-EOF
474
- #{@mailbox} uid:#{uid} #{number_to_human_size size} #{flags.inspect} #{format_parts_info(formatter.list_parts)}
475
- #{divider '-'}
476
- #{format_headers(formatter.extract_headers)}
477
-
478
- #{formatter.process_body}
479
- EOF
480
- # log "Storing message_cache[[#{@mailbox}, #{uid}]]"
481
- d = {:mail => mail, :size => size, :message_text => message_text, :seqno => fetch_data.seqno, :flags => flags}
482
- message_cache[[@mailbox, uid]] = d
483
- rescue
484
- msg = "Error encountered parsing message uid #{uid}:\n#{$!}\n#{$!.backtrace.join("\n")}" +
485
- "\n\nRaw message:\n\n" + mail.to_s
486
- log msg
487
- log message_text
488
- {:message_text => msg}
489
- end
490
-
491
- # deprecated
492
- def prefetch_adjacent(index)
493
- Thread.new do
494
- [index + 1, index - 1].each do |idx|
495
- fetch_and_cache(idx)
496
- end
497
- end
498
- end
499
-
500
- def format_parts_info(parts)
501
- lines = parts.select {|part| part !~ %r{text/plain}}
502
- if lines.size > 0
503
- "\n#{lines.join("\n")}"
504
- end
505
- end
506
-
507
- # id_set is a string comming from the vim client
508
- # action is -FLAGS or +FLAGS
509
- def flag(uid_set, action, flg)
510
- log "Flag #{uid_set} #{flg} #{action}"
511
- uid_set = uid_set.split(',').map(&:to_i)
512
- if flg == 'Deleted'
513
- log "Deleting uid_set: #{uid_set.inspect}"
514
- decrement_max_seqno(uid_set.size)
515
- # for delete, do in a separate thread because deletions are slow
516
- spawn_thread_if_tty do
517
- unless @mailbox == mailbox_aliases['trash']
518
- log "@imap.uid_copy #{uid_set.inspect} to #{mailbox_aliases['trash']}"
519
- log @imap.uid_copy(uid_set, mailbox_aliases['trash'])
520
- end
521
- log "@imap.uid_store #{uid_set.inspect} #{action} [#{flg.to_sym}]"
522
- log @imap.uid_store(uid_set, action, [flg.to_sym])
523
- reload_mailbox
524
- clear_cached_message
525
- end
526
- elsif flg == 'spam' || flg == mailbox_aliases['spam']
527
- log "Marking as spam uid_set: #{uid_set.inspect}"
528
- decrement_max_seqno(uid_set.size)
529
- spawn_thread_if_tty do
530
- log "@imap.uid_copy #{uid_set.inspect} to #{mailbox_aliases['spam']}"
531
- log @imap.uid_copy(uid_set, mailbox_aliases['spam'])
532
- log "@imap.uid_store #{uid_set.inspect} #{action} [:Deleted]"
533
- log @imap.uid_store(uid_set, action, [:Deleted])
534
- reload_mailbox
535
- clear_cached_message
536
- end
537
- else
538
- log "Flagging uid_set: #{uid_set.inspect}"
539
- spawn_thread_if_tty do
540
- log "@imap.uid_store #{uid_set.inspect} #{action} [#{flg.to_sym}]"
541
- log @imap.uid_store(uid_set, action, [flg.to_sym])
542
- end
543
- end
544
- end
545
-
546
- def move_to(uid_set, mailbox)
547
- uid_set = uid_set.split(',').map(&:to_i)
548
- decrement_max_seqno(uid_set.size)
549
- log "Move #{uid_set.inspect} to #{mailbox}"
550
- if mailbox == 'all'
551
- log "Archiving messages"
552
- end
553
- if mailbox_aliases[mailbox]
554
- mailbox = mailbox_aliases[mailbox]
555
- end
556
- create_if_necessary mailbox
557
- log "Moving uid_set: #{uid_set.inspect} to #{mailbox}"
558
- spawn_thread_if_tty do
559
- log @imap.uid_copy(uid_set, mailbox)
560
- log @imap.uid_store(uid_set, '+FLAGS', [:Deleted])
561
- reload_mailbox
562
- clear_cached_message
563
- log "Moved uid_set #{uid_set.inspect} to #{mailbox}"
564
- end
565
- end
566
-
567
- def copy_to(uid_set, mailbox)
568
- uid_set = uid_set.split(',').map(&:to_i)
569
- if mailbox_aliases[mailbox]
570
- mailbox = mailbox_aliases[mailbox]
571
- end
572
- create_if_necessary mailbox
573
- log "Copying #{uid_set.inspect} to #{mailbox}"
574
- spawn_thread_if_tty do
575
- log @imap.uid_copy(uid_set, mailbox)
576
- log "Copied uid_set #{uid_set.inspect} to #{mailbox}"
577
- end
223
+ def more_messages
224
+ log "Getting more_messages"
225
+ x = [(@start_index - @limit), 0].max
226
+ y = [@start_index - 1, 0].max
227
+ @start_index = x
228
+ fetch_ids = search_query? ? @ids[x..y] : (x..y).to_a
229
+ message_ids = fetch_and_cache_headers(fetch_ids)
230
+ res = get_message_headers message_ids
231
+ with_more_message_line(res)
578
232
  end
579
233
 
580
234
  def spawn_thread_if_tty(&block)
@@ -632,11 +286,7 @@ EOF
632
286
 
633
287
  def reply_template(replyall=false)
634
288
  log "Sending reply template"
635
- if @current_mail.nil?
636
- log "- missing @current mail!"
637
- return nil
638
- end
639
- reply_headers = Vmail::ReplyTemplate.new(@current_mail, @username, @name, replyall, @always_cc).reply_headers
289
+ reply_headers = Vmail::ReplyTemplate.new(current_message.rfc822, @username, @name, replyall, @always_cc).reply_headers
640
290
  body = reply_headers.delete(:body)
641
291
  format_headers(reply_headers) + "\n\n\n" + body + signature
642
292
  end
@@ -647,8 +297,8 @@ EOF
647
297
  end
648
298
 
649
299
  def forward_template
650
- original_body = @current_message.split(/\n-{20,}\n/, 2)[1]
651
- formatter = Vmail::MessageFormatter.new(@current_mail)
300
+ original_body = current_message.split(/\n-{20,}\n/, 2)[1]
301
+ formatter = Vmail::MessageFormatter.new(current_mail)
652
302
  headers = formatter.extract_headers
653
303
  subject = headers['subject']
654
304
  if subject !~ /Fwd: /
@@ -660,12 +310,6 @@ EOF
660
310
  original_body + signature
661
311
  end
662
312
 
663
- def divider(str)
664
- str * DIVIDER_WIDTH
665
- end
666
-
667
- SENT_MESSAGES_FILE = "sent-messages.txt"
668
-
669
313
  def format_sent_message(mail)
670
314
  formatter = Vmail::MessageFormatter.new(mail)
671
315
  message_text = <<-EOF
@@ -673,14 +317,10 @@ Sent Message #{self.format_parts_info(formatter.list_parts)}
673
317
 
674
318
  #{format_headers(formatter.extract_headers)}
675
319
 
676
- #{formatter.process_body}
320
+ #{formatter.plaintext_part}
677
321
  EOF
678
322
  end
679
323
 
680
- def backup_sent_message(message)
681
- File.open(SENT_MESSAGES_FILE, "a") {|f| f.write(divider('-') + "\n" + format_sent_message(message))}
682
- end
683
-
684
324
  def deliver(text)
685
325
  # parse the text. The headers are yaml. The rest is text body.
686
326
  require 'net/smtp'
@@ -691,8 +331,7 @@ EOF
691
331
  log res.inspect
692
332
  log "\n"
693
333
  msg = if res.is_a?(Mail::Message)
694
- backup_sent_message mail
695
- "Message '#{mail.subject}' sent and saved to #{SENT_MESSAGES_FILE}"
334
+ "Message '#{mail.subject}' sent"
696
335
  else
697
336
  "Failed to deliver message '#{mail.subject}'!"
698
337
  end
@@ -746,11 +385,11 @@ EOF
746
385
 
747
386
  def save_attachments(dir)
748
387
  log "Save_attachments #{dir}"
749
- if !@current_mail
388
+ if !current_mail
750
389
  log "Missing a current message"
751
390
  end
752
- return unless dir && @current_mail
753
- attachments = @current_mail.attachments
391
+ return unless dir && current_mail
392
+ attachments = current_mail.attachments
754
393
  `mkdir -p #{dir}`
755
394
  saved = attachments.map do |x|
756
395
  path = File.join(dir, x.filename)
@@ -763,14 +402,14 @@ EOF
763
402
 
764
403
  def open_html_part
765
404
  log "Open_html_part"
766
- log @current_mail.parts.inspect
767
- multipart = @current_mail.parts.detect {|part| part.multipart?}
405
+ log current_mail.parts.inspect
406
+ multipart = current_mail.parts.detect {|part| part.multipart?}
768
407
  html_part = if multipart
769
408
  multipart.parts.detect {|part| part.header["Content-Type"].to_s =~ /text\/html/}
770
- elsif ! @current_mail.parts.empty?
771
- @current_mail.parts.detect {|part| part.header["Content-Type"].to_s =~ /text\/html/}
409
+ elsif ! current_mail.parts.empty?
410
+ current_mail.parts.detect {|part| part.header["Content-Type"].to_s =~ /text\/html/}
772
411
  else
773
- @current_mail.body
412
+ current_mail.body
774
413
  end
775
414
  return if html_part.nil?
776
415
  outfile = 'part.html'
@@ -780,8 +419,11 @@ EOF
780
419
  end
781
420
 
782
421
  def window_width=(width)
783
- log "Setting window width to #{width}"
784
- @width = width.to_i
422
+ @next_window_width = width.to_i
423
+ if @width.nil?
424
+ @width = @next_window_width
425
+ end
426
+ log "Setting next window width to #{width}"
785
427
  end
786
428
 
787
429
  def smtp_settings