vmail 1.6.8 → 1.6.9

Sign up to get free protection for your applications and to get access to all the features.
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