vmail 0.4.5 → 0.4.6

Sign up to get free protection for your applications and to get access to all the features.
@@ -63,6 +63,9 @@ You can omit the password key-value pair if you'd rather not have the password
63
63
  saved in the file. In that case, you'll prompted for the password each time you
64
64
  start vmail.
65
65
 
66
+ If you are behind a firewall that blocks IMAP, there are additional
67
+ configuration options that you can use. See below.
68
+
66
69
  ## Contacts autocompletion
67
70
 
68
71
  vmail uses Vim autocompletion to help you auto-complete email addresses.
@@ -398,6 +401,24 @@ vmail gem is downloaded from).
398
401
  [github]:https://github.com/danchoi/vmail
399
402
  [rubygems]:https://rubygems.org/gems/vmail
400
403
 
404
+ ## Additional configuration options
405
+
406
+ The default IMAP server vmail uses is `imap.gmail.com` and the default port is
407
+ `993`. If you want to change these values, e.g, because you are behind a
408
+ firewall which blocks IMAP, you can change these values by adding two lines in
409
+ your .vmailrc, like so:
410
+
411
+ server: localhost
412
+ port: 2999
413
+
414
+ Then you can create an SSH tunnel, e.g.
415
+
416
+ ssh -f user@example.com -L 2999:imap.gmail.com:993 -N
417
+
418
+ (Thanks to [Dave Bolton][davebolton] for this patch.)
419
+
420
+ [davebolton]:https://github.com/lightningdb
421
+
401
422
  ## Bug reports, feature requests
402
423
 
403
424
  Please file bug reports and feature requests in the [vmail github issue tracker][tracker].
data/Rakefile CHANGED
@@ -12,12 +12,18 @@ task :web do
12
12
  end
13
13
  end
14
14
 
15
+ desc "git push and rake release bumped version"
16
+ task :bumped do
17
+ puts `git commit -a -m'bump' && git push && rake release`
18
+ end
19
+
15
20
  desc "Run tests"
16
21
  task :test do
17
22
  $:.unshift File.expand_path("test")
18
23
  require 'test_helper'
19
24
  require 'time_format_test'
20
25
  require 'message_formatter_test'
26
+ require 'reply_template_test'
21
27
  require 'base64_test'
22
28
  MiniTest::Unit.autorun
23
29
  end
data/bin/vmail CHANGED
File without changes
File without changes
@@ -1,3 +1,4 @@
1
+ require 'vmail/version'
1
2
  require 'vmail/options'
2
3
  require 'vmail/imap_client'
3
4
 
@@ -5,6 +6,7 @@ module Vmail
5
6
  extend self
6
7
 
7
8
  def start
9
+ puts "starting vmail #{Vmail::VERSION}"
8
10
 
9
11
  vim = ENV['VMAIL_VIM'] || 'vim'
10
12
 
@@ -83,9 +83,9 @@ function! s:show_message()
83
83
  " moving up when the next echo statement executes:
84
84
  call feedkeys(":\<cr>")
85
85
  redraw
86
- let selected_uid = matchstr(line, '^\d\+')
87
- let s:current_uid = selected_uid
88
- let command = s:show_message_command . s:current_uid
86
+ " substract 2: because lines numbers start at 1 & messages start at line 2
87
+ let s:current_message_index = line('.') - 2
88
+ let command = s:show_message_command . s:current_message_index
89
89
  echom "Loading message. Please wait..."
90
90
  redrawstatus
91
91
  let res = system(command)
@@ -133,7 +133,7 @@ endfunction
133
133
 
134
134
  " invoked from withint message window
135
135
  function! s:show_raw()
136
- let command = s:show_message_command . s:current_uid . ' raw'
136
+ let command = s:show_message_command . s:current_message_index . ' raw'
137
137
  echo command
138
138
  setlocal modifiable
139
139
  1,$delete
@@ -198,29 +198,21 @@ function! s:update()
198
198
  endfunction
199
199
 
200
200
  function! s:toggle_star() range
201
- let lnum = a:firstline
202
- let n = 0
203
- let uids = []
204
- while lnum <= a:lastline
205
- let line = getline(lnum)
206
- let message_uid = matchstr(line, '^\d\+')
207
- call add(uids, message_uid)
208
- let lnum = lnum + 1
209
- endwhile
210
- let uid_set = join(uids, ",")
201
+ let uid_set = (a:firstline - 2) . '..' . (a:lastline - 2)
211
202
  let flag_symbol = "[*]"
212
203
  " check if starred already
213
204
  let action = " +FLAGS"
214
- if (match(line, flag_symbol) != -1)
205
+ if (match(getline(a:firstline), flag_symbol) != -1)
215
206
  let action = " -FLAGS"
216
207
  endif
217
208
  let command = s:flag_command . uid_set . action . " Flagged"
218
- if len(uids) == 1
219
- echom "toggling flag on message " . uid_set
209
+ if (a:lastline - a:firstline + 1) == 1
210
+ echom "toggling flag on message"
220
211
  else
221
- echom "toggling flags on messages " . join(uid_set, ",")
212
+ echom "toggling flags on " . (a:lastline - a:firstline + 1) . " messages"
222
213
  endif
223
- " toggle [*] on lines
214
+ " toggle [*] on lines; TODO: do this all in vimscript and do the starring
215
+ " in a thread in imap_client
224
216
  let res = system(command)
225
217
  setlocal modifiable
226
218
  exec a:firstline . "," . a:lastline . "delete"
@@ -232,22 +224,19 @@ function! s:toggle_star() range
232
224
  if len(split(res, "\n")) > 2
233
225
  call feedkeys("\<cr>")
234
226
  endif
227
+ echom "done"
235
228
  endfunction
236
229
 
237
230
  " flag can be Deleted or [Gmail]/Spam
238
231
  func! s:delete_messages(flag) range
239
- let lnum = a:firstline
240
- let n = 0
241
- let uids = []
242
- while lnum <= a:lastline
243
- let line = getline(lnum)
244
- let message_uid = matchstr(line, '^\d\+')
245
- call add(uids, message_uid)
246
- let lnum = lnum + 1
247
- endwhile
248
- let uid_set = join(uids, ",")
232
+ let uid_set = (a:firstline - 2) . '..' . (a:lastline - 2)
249
233
  let command = s:flag_command . uid_set . " +FLAGS " . a:flag
250
- echo command
234
+ let nummsgs = (a:lastline - a:firstline + 1)
235
+ if nummsgs == 1
236
+ echom "deleting message"
237
+ else
238
+ echom "deleting " . (a:lastline - a:firstline + 1) . " messages"
239
+ endif
251
240
  let res = system(command)
252
241
  setlocal modifiable
253
242
  exec a:firstline . "," . a:lastline . "delete"
@@ -255,53 +244,39 @@ func! s:delete_messages(flag) range
255
244
  write
256
245
  " if more than 2 lines change, vim forces us to look at a message.
257
246
  " dismiss it.
258
- if len(uids) > 2
247
+ if nummsgs > 2
259
248
  call feedkeys("\<cr>")
260
249
  endif
261
250
  redraw
262
- echo len(uids) . " message" . (len(uids) == 1 ? '' : 's') . " marked " . a:flag
251
+ echo nummsgs . " message" . (nummsgs == 1 ? '' : 's') . " marked " . a:flag
263
252
  endfunc
264
253
 
265
254
  func! s:archive_messages() range
266
- let lnum = a:firstline
267
- let n = 0
268
- let uids = []
269
- while lnum <= a:lastline
270
- let line = getline(lnum)
271
- let message_uid = matchstr(line, '^\d\+')
272
- call add(uids, message_uid)
273
- let lnum = lnum + 1
274
- endwhile
275
- let uid_set = join(uids, ",")
255
+ let uid_set = (a:firstline - 2) . '..' . (a:lastline - 2)
276
256
  let command = s:move_to_command . uid_set . ' ' . "all"
277
- echo "archiving message" . (len(uids) == 1 ? '' : 's')
257
+ let nummsgs = (a:lastline - a:firstline + 1)
258
+ echo "archiving message" . (nummsgs == 1 ? '' : 's')
278
259
  let res = system(command)
279
260
  setlocal modifiable
280
261
  exec a:firstline . "," . a:lastline . "delete"
281
262
  setlocal nomodifiable
282
263
  write
283
- call s:focus_message_window()
284
- close
264
+ if nummsgs > 2
265
+ call feedkeys("\<cr>")
266
+ endif
285
267
  redraw
286
- echo len(uids) . " message" . (len(uids) == 1 ? '' : 's') . " archived"
268
+ echo nummsgs . " message" . (nummsgs == 1 ? '' : 's') . " archived"
287
269
  endfunc
288
270
 
289
271
  " --------------------------------------------------------------------------------
272
+
290
273
  " append text bodies of a set of messages to a file
291
274
  func! s:append_messages_to_file() range
292
- let lnum = a:firstline
293
- let n = 0
294
- let uids = []
295
- while lnum <= a:lastline
296
- let line = getline(lnum)
297
- let message_uid = matchstr(line, '^\d\+')
298
- call add(uids, message_uid)
299
- let lnum = lnum + 1
300
- endwhile
301
- let uid_set = join(uids, ",")
275
+ let uid_set = (a:firstline - 2) . '..' . (a:lastline - 2)
276
+ let nummsgs = (a:lastline - a:firstline + 1)
302
277
  let s:append_file = input("print messages to file: ", s:append_file)
303
278
  let command = s:append_to_file_command . s:append_file . ' ' . uid_set
304
- echo "appending " . len(uids) . " message" . (len(uids) == 1 ? '' : 's') . " to " s:append_file
279
+ echo "appending " . nummsgs . " message" . (nummsgs == 1 ? '' : 's') . " to " . s:append_file . ". please wait..."
305
280
  let res = system(command)
306
281
  echo res
307
282
  redraw
@@ -311,16 +286,8 @@ endfunc
311
286
  " move to another mailbox
312
287
  function! s:move_to_mailbox(copy) range
313
288
  let s:copy_to_mailbox = a:copy
314
- let lnum = a:firstline
315
- let n = 0
316
- let uids = []
317
- while lnum <= a:lastline
318
- let line = getline(lnum)
319
- let message_uid = matchstr(line, '^\d\+')
320
- call add(uids, message_uid)
321
- let lnum = lnum + 1
322
- endwhile
323
- let s:uid_set = join(uids, ",")
289
+ let s:uid_set = (a:firstline - 2) . '..' . (a:lastline - 2)
290
+ let s:nummsgs = (a:lastline - a:firstline + 1)
324
291
  " now prompt use to select mailbox
325
292
  if !exists("s:mailboxes")
326
293
  call s:get_mailbox_list()
@@ -360,8 +327,11 @@ function! s:complete_move_to_mailbox()
360
327
  end
361
328
  setlocal nomodifiable
362
329
  write
330
+ if s:nummsgs > 2
331
+ call feedkeys("\<cr>")
332
+ endif
363
333
  redraw
364
- echo "done"
334
+ echo s:nummsgs . " message" . (s:nummsgs == 1 ? '' : 's') . ' ' . (s:copy_to_mailbox ? 'copied' : 'moved') . ' to ' . mailbox
365
335
  endfunction
366
336
 
367
337
  function! CompleteMoveMailbox(findstart, base)
@@ -453,14 +423,17 @@ function! s:select_mailbox()
453
423
  return
454
424
  endif
455
425
  let s:mailbox = mailbox
426
+ let s:query = "100 all"
456
427
  let command = s:select_mailbox_command . shellescape(s:mailbox)
428
+ echo "selecting mailbox ". s:mailbox ". please wait..."
429
+
457
430
  call system(command)
458
431
  redraw
459
432
  " now get latest 100 messages
460
433
  call s:focus_list_window()
461
434
  setlocal modifiable
462
435
  let command = s:search_command . "100 all"
463
- echo "Please wait. Loading messages..."
436
+ echo "loading messages..."
464
437
  let res = system(command)
465
438
  1,$delete
466
439
  put! =res
@@ -514,7 +487,7 @@ function! s:more_messages()
514
487
  let line = getline(line('.'))
515
488
  let uid = matchstr(line, '^\d\+')
516
489
  let command = s:more_messages_command . uid
517
- echo command
490
+ echo "fetching more messages. please wait..."
518
491
  let res = system(command)
519
492
  setlocal modifiable
520
493
  let lines = split(res, "\n")
@@ -528,7 +501,7 @@ endfunction
528
501
  " compose reply, compose, forward, save draft
529
502
 
530
503
  function! s:compose_reply(all)
531
- let command = s:reply_template_command . s:current_uid
504
+ let command = s:reply_template_command
532
505
  if a:all
533
506
  let command = command . ' 1'
534
507
  endif
@@ -546,7 +519,7 @@ function! s:compose_message()
546
519
  endfunction
547
520
 
548
521
  function! s:compose_forward()
549
- let command = s:forward_template_command . s:current_uid
522
+ let command = s:forward_template_command
550
523
  call s:open_compose_window(command)
551
524
  " call search("^to:")
552
525
  " normal A
@@ -628,7 +601,7 @@ endfunc
628
601
 
629
602
  " call from inside message window with <Leader>h
630
603
  func! s:open_html_part()
631
- let command = s:open_html_part_command . s:current_uid
604
+ let command = s:open_html_part_command
632
605
  " the command saves the html part to a local file
633
606
  let outfile = system(command)
634
607
  " todo: allow user to change open in browser command?
@@ -690,9 +663,9 @@ func! s:message_window_mappings()
690
663
  nnoremap <silent> <buffer> q :close<cr>
691
664
 
692
665
  nnoremap <silent> <buffer> <leader># :close<cr>:call <SID>focus_list_window()<cr>:call <SID>delete_messages("Deleted")<cr>
693
- nmap <silent> <buffer> <leader>d <leader>#
694
666
  nnoremap <silent> <buffer> <leader>* :call <SID>focus_list_window()<cr>:call <SID>toggle_star()<cr>
695
- nmap <silent> <buffer> s <leader>*
667
+ noremap <silent> <buffer> <leader>! :call <SID>focus_list_window()<cr>:call <SID>delete_messages("[Gmail]/Spam")<CR>
668
+ noremap <silent> <buffer> <leader>e :call <SID>focus_list_window()<cr>:call <SID>archive_messages()<CR>
696
669
 
697
670
  nnoremap <silent> <buffer> <Leader>b :call <SID>focus_list_window()<cr>call <SID>move_to_mailbox(0)<CR>
698
671
  nnoremap <silent> <buffer> <Leader>B :call <SID>focus_list_window()<cr>call <SID>move_to_mailbox(1)<CR>
@@ -1,5 +1,6 @@
1
1
  require 'drb'
2
2
  require 'vmail/message_formatter'
3
+ require 'vmail/reply_template'
3
4
  require 'vmail/string_ext'
4
5
  require 'yaml'
5
6
  require 'mail'
@@ -27,11 +28,13 @@ module Vmail
27
28
  @logger = Logger.new(config['logfile'] || STDERR)
28
29
  @logger.level = Logger::DEBUG
29
30
  @current_mail = nil
30
- @current_uid = nil
31
+ @current_message_index = nil
32
+ @imap_server = config['server'] || 'imap.gmail.com'
33
+ @imap_port = config['port'] || 993
31
34
  end
32
35
 
33
36
  def open
34
- @imap = Net::IMAP.new('imap.gmail.com', 993, true, nil, false)
37
+ @imap = Net::IMAP.new(@imap_server, @imap_port, true, nil, false)
35
38
  @imap.login(@username, @password)
36
39
  end
37
40
 
@@ -41,11 +44,11 @@ module Vmail
41
44
  @imap.disconnect
42
45
  end
43
46
 
44
- def select_mailbox(mailbox)
47
+ def select_mailbox(mailbox, force=false)
45
48
  if MailboxAliases[mailbox]
46
49
  mailbox = MailboxAliases[mailbox]
47
50
  end
48
- if mailbox == @mailbox
51
+ if mailbox == @mailbox && !force
49
52
  return
50
53
  end
51
54
  log "selecting mailbox #{mailbox.inspect}"
@@ -53,11 +56,27 @@ module Vmail
53
56
  log @imap.select(mailbox)
54
57
  end
55
58
  @mailbox = mailbox
56
- @all_uids = []
57
- @bad_uids = []
59
+ get_mailbox_status
60
+ get_highest_message_id
58
61
  return "OK"
59
62
  end
60
63
 
64
+ def reload_mailbox
65
+ select_mailbox(@mailbox, true)
66
+ end
67
+
68
+ def get_highest_message_id
69
+ # get highest message ID
70
+ res = @imap.fetch([1,"*"], ["ENVELOPE"])
71
+ @num_messages = res[-1].seqno
72
+ log "HIGHEST ID: #@num_messages"
73
+ end
74
+
75
+ def get_mailbox_status
76
+ @status = @imap.status(@mailbox, ["MESSAGES", "RECENT", "UNSEEN"])
77
+ log "mailbox status: #{@status.inspect}"
78
+ end
79
+
61
80
  def revive_connection
62
81
  log "reviving connection"
63
82
  open
@@ -65,6 +84,18 @@ module Vmail
65
84
  @imap.select(@mailbox)
66
85
  end
67
86
 
87
+ def prime_connection
88
+ reconnect_if_necessary(4) do
89
+ # this is just to prime the IMAP connection
90
+ # It's necessary for some reason before update and deliver.
91
+ log "priming connection for delivering"
92
+ res = @imap.fetch(@ids[-1], ["ENVELOPE"])
93
+ if res.nil?
94
+ raise IOError, "IMAP connection seems broken"
95
+ end
96
+ end
97
+ end
98
+
68
99
  def list_mailboxes
69
100
  @mailboxes ||= (@imap.list("[Gmail]/", "%") + @imap.list("", "%")).
70
101
  select {|struct| struct.attr.none? {|a| a == :Noselect} }.
@@ -83,37 +114,52 @@ module Vmail
83
114
  @mailboxes
84
115
  end
85
116
 
86
- def fetch_headers(uid_set)
87
- if uid_set.is_a?(String)
88
- uid_set = uid_set.split(",").map(&:to_i)
89
- elsif uid_set.is_a?(Integer)
90
- uid_set = [uid_set]
117
+ # id_set may be a range, array, or string
118
+ def fetch_envelopes(id_set, are_uids=false)
119
+ log "fetch_envelopes: #{id_set.inspect}"
120
+ if id_set.is_a?(String)
121
+ id_set = id_set.split(',')
91
122
  end
92
- max_uid = uid_set.max
93
- log "fetch headers for #{uid_set.inspect}"
94
- if uid_set.empty?
123
+ max_id = id_set.to_a[-1]
124
+ if id_set.to_a.empty?
95
125
  log "empty set"
96
126
  return ""
97
127
  end
98
128
  results = reconnect_if_necessary do
99
- @imap.uid_fetch(uid_set, ["FLAGS", "ENVELOPE", "RFC822.SIZE" ])
129
+ if are_uids
130
+ @imap.uid_fetch(id_set, ["FLAGS", "ENVELOPE", "RFC822.SIZE", "UID" ])
131
+ else
132
+ @imap.fetch(id_set, ["FLAGS", "ENVELOPE", "RFC822.SIZE", "UID" ])
133
+ end
134
+ end
135
+ if results.nil?
136
+ error = "expected fetch results but got nil"
137
+ log(error) && raise(error)
100
138
  end
101
139
  log "extracting headers"
102
- lines = results.
103
- sort_by {|x|
104
- begin
105
- Time.parse(x.attr['ENVELOPE'].date)
106
- rescue ArgumentError
107
- Time.now
108
- end
109
- }.
110
- map {|x| format_header(x, max_uid)}
111
- log "returning result"
112
- return lines.join("\n")
140
+ new_message_rows = results.map {|x| extract_row_data(x, max_id) }
141
+ if are_uids
142
+ # replace old row_text values
143
+ new_message_rows.each {|new_row_data|
144
+ @message_list.
145
+ select {|old_row_data| old_row_data[:uid] == new_row_data[:uid]}.
146
+ each {|old_row_data| old_row_data[:row_text] = new_row_data[:row_text]}
147
+ }
148
+ else
149
+ # put new rows before the current ones
150
+ @message_list.unshift(*new_message_rows)
151
+ end
152
+ log "returning #{new_message_rows.size} new rows"
153
+ return new_message_rows.
154
+ map {|x| x[:row_text]}.
155
+ join("\n")
113
156
  end
114
157
 
115
- def format_header(fetch_data, max_uid=nil)
116
- uid = fetch_data.attr["UID"]
158
+ # TODO extract this to another class or module and write unit tests
159
+ def extract_row_data(fetch_data, max_id=nil)
160
+ seqno = fetch_data.seqno
161
+ uid = fetch_data.attr['UID']
162
+ # log "fetched seqno #{seqno} uid #{uid}"
117
163
  envelope = fetch_data.attr["ENVELOPE"]
118
164
  size = fetch_data.attr["RFC822.SIZE"]
119
165
  flags = fetch_data.attr["FLAGS"]
@@ -148,18 +194,22 @@ module Vmail
148
194
  subject = envelope.subject || ''
149
195
  subject = Mail::Encodings.unquote_and_convert_to(subject, 'UTF-8')
150
196
  flags = format_flags(flags)
151
- first_col_width = max_uid.to_s.length
197
+ first_col_width = max_id.to_s.length
152
198
  mid_width = @width - (first_col_width + 33)
153
199
  address_col_width = (mid_width * 0.3).ceil
154
200
  subject_col_width = (mid_width * 0.7).floor
155
- [uid.to_s.col(first_col_width),
156
- (date_formatted || '').col(14),
157
- address.col(address_col_width),
158
- subject.col(subject_col_width),
159
- number_to_human_size(size).rcol(6),
160
- flags.rcol(7)].join(' ')
201
+ row_text = [ seqno.to_s.col(first_col_width),
202
+ (date_formatted || '').col(14),
203
+ address.col(address_col_width),
204
+ subject.col(subject_col_width),
205
+ number_to_human_size(size).rcol(6),
206
+ flags.rcol(7)
207
+ ].join(' ')
208
+ {:uid => uid, :seqno => seqno, :row_text => row_text}
161
209
  rescue
162
- "#{uid.to_s} : error extracting this header"
210
+ log "error extracting header for uid #{uid} seqno #{seqno}: #$!"
211
+ row_text =i "#{seqno.to_s} : error extracting this header"
212
+ {:uid => uid, :seqno => seqno, :row_text => row_text}
163
213
  end
164
214
 
165
215
  UNITS = [:b, :kb, :mb, :gb].freeze
@@ -189,83 +239,120 @@ module Vmail
189
239
  end
190
240
 
191
241
  def search(limit, *query)
192
- log "uid_search limit: #{limit} query: #{@query.inspect}"
193
- limit = 25 if limit.to_s !~ /^\d+$/
242
+ limit = limit.to_i
243
+ limit = 100 if limit.to_s !~ /^\d+$/
194
244
  query = ['ALL'] if query.empty?
195
- @query = query.join(' ')
196
- log "uid_search #@query #{limit}"
197
- @all_uids = reconnect_if_necessary do
198
- @imap.uid_search(@query)
199
- end
200
- uids = @all_uids[-([limit.to_i, @all_uids.size].min)..-1] || []
201
- res = fetch_headers(uids)
202
- add_more_message_line(res, uids)
245
+ if query.size == 1 && query[0].downcase == 'all'
246
+ # form a sequence range
247
+ query.unshift [[@num_messages - limit.to_i + 1 , 1].max, @num_messages].join(':')
248
+ @all_search = true
249
+ else
250
+ # this is a special query search
251
+ # set the target range to the whole set
252
+ query.unshift "1:#@num_messages"
253
+ @all_search = false
254
+ end
255
+ log "@all_search #{@all_search}"
256
+ @query = query
257
+ log "search query: #@query.inspect"
258
+ @ids = reconnect_if_necessary do
259
+ @imap.search(@query.join(' '))
260
+ end
261
+ # save ids in @ids, because filtered search relies on it
262
+ fetch_ids = if @all_search
263
+ @ids
264
+ else #filtered search
265
+ @start_index = [@ids.length - limit, 0].max
266
+ @ids[@start_index..-1]
267
+ end
268
+ log "search query got #{@ids.size} results"
269
+ @message_list = [] # this will hold all the data extracted from these message envelopes
270
+ res = fetch_envelopes(fetch_ids)
271
+ add_more_message_line(res, fetch_ids[0])
203
272
  end
204
273
 
205
274
  def update
206
- reconnect_if_necessary(4) do
207
- # this is just to prime the IMAP connection
208
- # It's necessary for some reason.
209
- log "priming connection for update"
210
- res = @imap.uid_fetch(@all_uids[-1], ["ENVELOPE"])
211
- if res.nil?
212
- raise IOError, "IMAP connection seems broken"
213
- end
214
- end
215
- uids = reconnect_if_necessary {
216
- log "uid_search #@query"
217
- @imap.uid_search(@query)
275
+ prime_connection
276
+ old_num_messages = @num_messages
277
+ # we need to re-select the mailbox to get the new highest id
278
+ reload_mailbox
279
+ update_query = @query
280
+ # set a new range filter
281
+ update_query[0] = "#{old_num_messages}:#{@num_messages}"
282
+ ids = reconnect_if_necessary {
283
+ log "search #update_query"
284
+ @imap.search(update_query.join(' '))
218
285
  }
219
- new_uids = uids - @all_uids
220
- log "UPDATE: NEW UIDS: #{new_uids.inspect}"
221
- if !new_uids.empty?
222
- res = fetch_headers(new_uids)
223
- @all_uids = uids
286
+ # TODO change this. will throw error now
287
+ new_ids = ids.select {|x| x > @ids.max}
288
+ @ids = @ids + new_ids
289
+ log "UPDATE: NEW UIDS: #{new_ids.inspect}"
290
+ if !new_ids.empty?
291
+ res = fetch_envelopes(new_ids)
224
292
  res
225
293
  end
226
294
  end
227
295
 
228
- # gets 100 messages prior to uid
229
- def more_messages(uid, limit=100)
230
- uid = uid.to_i
231
- x = [(@all_uids.index(uid) - limit), 0].max
232
- y = [@all_uids.index(uid) - 1, 0].max
233
- uids = @all_uids[x..y]
234
- res = fetch_headers(uids)
235
- add_more_message_line(res, uids)
236
- end
237
-
238
- def add_more_message_line(res, uids)
239
- return res if uids.empty?
240
- start_index = @all_uids.index(uids[0])
241
- if start_index > 0
242
- remaining = start_index
243
- res = "> Load #{[100, remaining].min} more messages. #{remaining} remaining.\n" + res
296
+ # gets 100 messages prior to id
297
+ def more_messages(message_id, limit=100)
298
+ log "more_messages: message_id #{message_id}"
299
+ message_id = message_id.to_i
300
+ if @all_search
301
+ x = [(message_id - limit), 0].max
302
+ y = [message_id - 1, 0].max
303
+ res = fetch_envelopes((x..y))
304
+ add_more_message_line(res, x)
305
+ else
306
+ # filter search query
307
+ log "@start_index #@start_index"
308
+ x = [(@start_index - limit), 0].max
309
+ y = [@start_index - 1, 0].max
310
+ @start_index = x
311
+ res = fetch_envelopes(@ids[x..y])
312
+ add_more_message_line(res, @ids[x])
313
+ end
314
+ end
315
+
316
+ def add_more_message_line(res, start_id)
317
+ log "add_more_message_line for start_id #{start_id}"
318
+ if @all_search
319
+ return res if start_id.nil?
320
+ if start_id <= 1
321
+ return res
322
+ end
323
+ remaining = start_id - 1
324
+ else # filter search
325
+ remaining = @ids.index(start_id) - 1
326
+ end
327
+ if remaining < 1
328
+ log "none remaining"
329
+ return res
244
330
  end
245
- res
331
+ log "remaining messages: #{remaining}"
332
+ "> Load #{[100, remaining].min} more messages. #{remaining} remaining.\n" + res
246
333
  end
247
334
 
248
- def show_message(uid, raw=false, forwarded=false)
249
- uid = uid.to_i
250
- if forwarded
251
- return @current_message.split(/\n-{20,}\n/, 2)[1]
252
- end
335
+ def show_message(index, raw=false)
336
+ log "showing message at #{index}"
253
337
  return @current_mail.to_s if raw
254
- return @current_message if uid == @current_uid
255
- log "fetching #{uid.inspect}"
338
+ index = index.to_i
339
+ return @current_message if index == @current_message_index
340
+ envelope_data = @message_list[index]
341
+ uid = envelope_data[:uid]
342
+ log "fetching uid #{uid}"
256
343
  fetch_data = reconnect_if_necessary do
257
344
  @imap.uid_fetch(uid, ["FLAGS", "RFC822", "RFC822.SIZE"])[0]
258
345
  end
259
346
  res = fetch_data.attr["RFC822"]
260
347
  mail = Mail.new(res)
261
- @current_uid = uid
348
+ @current_message_index = index
262
349
  @current_mail = mail # used later to show raw message or extract attachments if any
263
350
  log "saving current mail with parts: #{@current_mail.parts.inspect}"
264
351
  formatter = Vmail::MessageFormatter.new(mail)
265
352
  out = formatter.process_body
266
353
  size = fetch_data.attr["RFC822.SIZE"]
267
354
  @current_message = <<-EOF
268
- #{@mailbox} #{uid} #{number_to_human_size size} #{format_parts_info(formatter.list_parts)}
355
+ #{@mailbox} #{index} #{number_to_human_size size} #{format_parts_info(formatter.list_parts)}
269
356
  ---------------------------------------
270
357
  #{format_headers(formatter.extract_headers)}
271
358
 
@@ -283,36 +370,66 @@ EOF
283
370
  end
284
371
  end
285
372
 
286
- # uid_set is a string comming from the vim client
373
+ # id_set is a string comming from the vim client
287
374
  # action is -FLAGS or +FLAGS
288
- def flag(uid_set, action, flg)
289
- if uid_set.is_a?(String)
290
- uid_set = uid_set.split(",").map(&:to_i)
291
- end
292
- # #<struct Net::IMAP::FetchData seqno=17423, attr={"FLAGS"=>[:Seen, "Flagged"], "UID"=>83113}>
293
- log "flag #{uid_set} #{flg} #{action}"
375
+ def flag(index_range, action, flg)
376
+ uid_set = uids_from_index_range(index_range)
377
+ log "flag #{uid_set.inspect} #{flg} #{action}"
294
378
  if flg == 'Deleted'
379
+ log "Deleting index_range: #{index_range.inspect}; uid_set: #{uid_set.inspect}"
295
380
  # for delete, do in a separate thread because deletions are slow
296
381
  Thread.new do
297
382
  unless @mailbox == '[Gmail]/Trash'
298
383
  @imap.uid_copy(uid_set, "[Gmail]/Trash")
299
384
  end
300
385
  res = @imap.uid_store(uid_set, action, [flg.to_sym])
386
+ remove_uid_set_from_cached_lists(uid_set)
387
+ reload_mailbox
301
388
  end
302
- uid_set.each { |uid| @all_uids.delete(uid) }
303
389
  elsif flg == '[Gmail]/Spam'
304
- @imap.uid_copy(uid_set, "[Gmail]/Spam")
305
- res = @imap.uid_store(uid_set, action, [:Deleted])
306
- "#{uid} deleted"
390
+ log "Marking as spam index_range: #{index_range.inspect}; uid_set: #{uid_set.inspect}"
391
+ Thread.new do
392
+ @imap.uid_copy(uid_set, "[Gmail]/Spam")
393
+ res = @imap.uid_store(uid_set, action, [:Deleted])
394
+ remove_uid_set_from_cached_lists(uid_set)
395
+ reload_mailbox
396
+ end
397
+ "#{id} deleted"
307
398
  else
308
- log "Flagging"
399
+ log "Flagging index_range: #{index_range.inspect}; uid_set: #{uid_set.inspect}"
309
400
  res = @imap.uid_store(uid_set, action, [flg.to_sym])
310
- # log res.inspect
311
- fetch_headers(uid_set)
312
- end
401
+ log res.inspect
402
+ fetch_envelopes(uid_set, true).tap {|x| log x}
403
+ end
404
+ end
405
+
406
+ def uids_from_index_range(index_range_as_string)
407
+ raise "expecting String" unless index_range_as_string.is_a?(String)
408
+ raise "expecting a range as string" unless index_range_as_string =~ /^\d+\.\.\d+$/
409
+ log "converting index_range #{index_range_as_string} to uids"
410
+ uids = @message_list[eval(index_range_as_string)].map {|row| row[:uid]}
411
+ log "converted index_range #{index_range_as_string} to uids #{uids.inspect}"
412
+ uids
413
+ end
414
+
415
+ def remove_uid_set_from_cached_lists(uid_set)
416
+ # delete from cached @ids and @message_list
417
+ uid_set.each {|uid|
418
+ @message_list.
419
+ select {|row| row[:uid] == uid}.
420
+ each {|row|
421
+ seqno = row[:seqno]
422
+ log "deleting seqno #{seqno} from @ids"
423
+ @ids.delete seqno
424
+ log "deleting msg uid #{row[:uid]} from @message_list"
425
+ @message_list.delete(row)
426
+ }
427
+ }
428
+
313
429
  end
314
430
 
315
- def move_to(uid_set, mailbox)
431
+ def move_to(id_set, mailbox)
432
+ log "move #{id_set.inspect} to #{mailbox}"
316
433
  if mailbox == 'all'
317
434
  log "archiving messages"
318
435
  end
@@ -320,24 +437,28 @@ EOF
320
437
  mailbox = MailboxAliases[mailbox]
321
438
  end
322
439
  create_if_necessary mailbox
323
- if uid_set.is_a?(String)
324
- uid_set = uid_set.split(",").map(&:to_i)
440
+ log "getting uids form index range #{id_set}"
441
+ uid_set = uids_from_index_range(id_set)
442
+ log "moving uid_set: #{uid_set.inspect} to #{mailbox}"
443
+ Thread.new do
444
+ log @imap.uid_copy(uid_set, mailbox)
445
+ log @imap.uid_store(uid_set, '+FLAGS', [:Deleted])
446
+ reload_mailbox
447
+ log "moved uid_set #{uid_set.inspect} to #{mailbox}"
325
448
  end
326
- log "move_to #{uid_set.inspect} #{mailbox}"
327
- log @imap.uid_copy(uid_set, mailbox)
328
- log @imap.uid_store(uid_set, '+FLAGS', [:Deleted])
329
449
  end
330
450
 
331
- def copy_to(uid_set, mailbox)
451
+ def copy_to(id_set, mailbox)
332
452
  if MailboxAliases[mailbox]
333
453
  mailbox = MailboxAliases[mailbox]
334
454
  end
335
455
  create_if_necessary mailbox
336
- log "copy #{uid_set.inspect} #{mailbox}"
337
- if uid_set.is_a?(String)
338
- uid_set = uid_set.split(",").map(&:to_i)
456
+ uid_set = uids_from_index_range(id_set)
457
+ log "copying #{uid_set.inspect} to #{mailbox}"
458
+ Thread.new do
459
+ log @imap.uid_copy(uid_set, mailbox)
460
+ log "copied uid_set #{uid_set.inspect} to #{mailbox}"
339
461
  end
340
- log @imap.uid_copy(uid_set, mailbox)
341
462
  end
342
463
 
343
464
  def create_if_necessary(mailbox)
@@ -350,18 +471,18 @@ EOF
350
471
  end
351
472
  end
352
473
 
353
- def append_to_file(file, uid_set)
354
- if uid_set.is_a?(String)
355
- uid_set = uid_set.split(",").map(&:to_i)
356
- end
357
- log "append messages to file: #{file}"
358
- uid_set.each do |uid|
359
- message = show_message(uid)
474
+ def append_to_file(file, index_range_as_string)
475
+ raise "expecting a range as string" unless index_range_as_string =~ /^\d+\.\.\d+$/
476
+ index_range = eval(index_range_as_string)
477
+ log "append to file range #{index_range.inspect} to file: #{file}"
478
+ index_range.each do |idx|
479
+ message = show_message(idx)
360
480
  divider = "#{'=' * 39}\n"
361
481
  File.open(file, 'a') {|f| f.puts(divider + message + "\n\n")}
362
- log "appended uid #{uid}"
482
+ subject = (message[/^subject:(.*)/,1] || '').strip
483
+ log "appended message '#{subject}'"
363
484
  end
364
- "printed #{uid_set.size} message#{uid_set.size == 1 ? '' : 's'} to #{file.strip}"
485
+ "printed #{index_range.to_a.size} message#{index_range.to_a.size == 1 ? '' : 's'} to #{file.strip}"
365
486
  end
366
487
 
367
488
 
@@ -384,41 +505,22 @@ EOF
384
505
  lines.join("\n")
385
506
  end
386
507
 
387
- def reply_template(uid, replyall=false)
388
- log "sending reply template for #{uid}"
389
- fetch_data = @imap.uid_fetch(uid.to_i, ["FLAGS", "ENVELOPE", "RFC822"])[0]
390
- envelope = fetch_data.attr['ENVELOPE']
391
- recipient = [envelope.reply_to, envelope.from].flatten.map {|x| address_to_string(x)}[0]
392
- cc = [envelope.to, envelope.cc]
393
- cc = cc.flatten.compact.
394
- select {|x| @username !~ /#{x.mailbox}@#{x.host}/}.
395
- map {|x| address_to_string(x)}.join(", ")
396
- mail = Mail.new fetch_data.attr['RFC822']
397
- formatter = Vmail::MessageFormatter.new(mail)
398
- headers = formatter.extract_headers
399
- subject = headers['subject']
400
- if subject !~ /Re: /
401
- subject = "Re: #{subject}"
402
- end
403
- cc = replyall ? cc : nil
404
- date = headers['date'].is_a?(String) ? Time.parse(headers['date']) : headers['date']
405
- quote_header = "On #{date.strftime('%a, %b %d, %Y at %I:%M %p')}, #{address_to_string(envelope.from[0])} wrote:\n\n"
406
- body = quote_header + formatter.process_body.gsub(/^(?=>)/, ">").gsub(/^(?!>)/, "> ")
407
- reply_headers = { 'from' => "#@name <#@username>", 'to' => recipient, 'cc' => cc, 'subject' => subject}
508
+ def reply_template(replyall=false)
509
+ log "sending reply template"
510
+ # user reply_template class
511
+ reply_headers = Vmail::ReplyTemplate.new(@current_mail, @username, @name, replyall).reply_headers
512
+ body = reply_headers.delete(:body)
408
513
  format_headers(reply_headers) + "\n\n\n" + body + signature
409
514
  end
410
515
 
411
- def address_to_string(x)
412
- x.name ? "#{x.name} <#{x.mailbox}@#{x.host}>" : "#{x.mailbox}@#{x.host}"
413
- end
414
516
 
415
517
  def signature
416
518
  return '' unless @signature
417
519
  "\n\n#@signature"
418
520
  end
419
521
 
420
- def forward_template(uid)
421
- original_body = show_message(uid, false, true)
522
+ def forward_template
523
+ original_body = @current_message.split(/\n-{20,}\n/, 2)[1]
422
524
  new_message_template +
423
525
  "\n---------- Forwarded message ----------\n" +
424
526
  original_body + signature
@@ -427,15 +529,7 @@ EOF
427
529
  def deliver(text)
428
530
  # parse the text. The headers are yaml. The rest is text body.
429
531
  require 'net/smtp'
430
- reconnect_if_necessary(4) do
431
- # this is just to prime the IMAP connection
432
- # It's necessary for some reason.
433
- log "priming connection for delivering"
434
- res = @imap.uid_fetch(@all_uids[-1], ["ENVELOPE"])
435
- if res.nil?
436
- raise IOError, "IMAP connection seems broken"
437
- end
438
- end
532
+ prime_connection
439
533
  mail = new_mail_from_input(text)
440
534
  mail.delivery_method(*smtp_settings)
441
535
  log mail.deliver!
@@ -518,8 +612,8 @@ EOF
518
612
  "saved:\n" + saved.map {|x| "- #{x}"}.join("\n")
519
613
  end
520
614
 
521
- def open_html_part(uid)
522
- log "open_html_part #{uid}"
615
+ def open_html_part
616
+ log "open_html_part"
523
617
  log @current_mail.parts.inspect
524
618
  multipart = @current_mail.parts.detect {|part| part.multipart?}
525
619
  html_part = if multipart
@@ -571,6 +665,9 @@ EOF
571
665
  log(revive_connection)
572
666
  # try just once
573
667
  block.call
668
+ rescue
669
+ log "error: #{$!}"
670
+ raise
574
671
  end
575
672
 
576
673
  def self.start(config)
@@ -0,0 +1,51 @@
1
+ require 'vmail/message_formatter'
2
+ require 'mail'
3
+ require 'time'
4
+
5
+ module Vmail
6
+ class ReplyTemplate
7
+
8
+ def initialize(mail, username, name, replyall)
9
+ @username, @name, @replyall = username, name, replyall
10
+ @mail = Mail.new(mail)
11
+ end
12
+
13
+ def reply_headers
14
+ formatter = Vmail::MessageFormatter.new(@mail)
15
+ headers = formatter.extract_headers
16
+ subject = headers['subject']
17
+ if subject !~ /Re: /
18
+ subject = "Re: #{subject}"
19
+ end
20
+ date = headers['date'].is_a?(String) ? Time.parse(headers['date']) : headers['date']
21
+ quote_header = "On #{date.strftime('%a, %b %d, %Y at %I:%M %p')}, #{sender} wrote:\n\n"
22
+ body = quote_header + formatter.process_body.gsub(/^(?=>)/, ">").gsub(/^(?!>)/, "> ")
23
+ {'from' => "#@name <#@username>", 'to' => primary_recipient, 'cc' => cc, 'subject' => subject, :body => body}
24
+ end
25
+
26
+ def primary_recipient
27
+ from = @mail.header['from']
28
+ reply_to = @mail.header['reply-to']
29
+ [ reply_to, from ].flatten.compact.map(&:to_s)[0]
30
+ end
31
+
32
+ def cc
33
+ return nil unless @replyall
34
+ cc = @mail.header['to'].value.split(/,\s*/) + @mail.header['cc'].value.split(/,\s*/)
35
+ cc = cc.flatten.compact.
36
+ select {|x|
37
+ x.to_s[/<([^>]+)>/, 1] !~ /#{@username}/ && x.to_s[/^[^<]+/, 1] !~ /#{@name}/
38
+ }.join(', ')
39
+ end
40
+
41
+ def sender
42
+ @mail.header['from'].value
43
+ end
44
+
45
+ # deprecated
46
+ def address_to_string(x)
47
+ x.name ? "#{x.name} <#{x.mailbox}@#{x.host}>" : "#{x.mailbox}@#{x.host}"
48
+ end
49
+
50
+ end
51
+ end
@@ -1,3 +1,3 @@
1
1
  module Vmail
2
- VERSION = "0.4.5"
2
+ VERSION = "0.4.6"
3
3
  end
@@ -0,0 +1,58 @@
1
+ Return-Path: <chappy1@gmail.com>
2
+ Received: by 10.213.2.204 with SMTP id 12cs233347ebk; Sun, 12 Dec 2010 10:13:13 -0800
3
+ Received: by 10.91.51.19 with SMTP id d19mr3869811agk.183.1292177592502; Sun, 12 Dec 2010 10:13:12 -0800
4
+ Received: from mail-yx0-f171.google.com (mail-yx0-f171.google.com [209.85.213.171]) by mx.google.com with ESMTP id d7si4681466and.167.2010.12.12.10.13.10; Sun, 12 Dec 2010 10:13:11 -0800
5
+ Received: by mail-yx0-f171.google.com with SMTP id 11so3634988yxi.2 for
6
+ <multiple recipients>; Sun, 12 Dec 2010 10:13:10 -0800 (PST)
7
+ Received: by 10.90.24.10 with SMTP id 10mr3890562agx.179.1292177590623; Sun, 12 Dec 2010 10:13:10 -0800
8
+ Received: from [26.144.14.48] (m482436d0.tmodns.net [208.54.36.72]) by mx.google.com with ESMTPS id c34sm4564739anc.30.2010.12.12.10.13.08 (version=TLSv1/SSLv3 cipher=RC4-MD5); Sun, 12 Dec 2010 10:13:09 -0800
9
+ Date: Sun, 12 Dec 2010 13:13:01 -0500
10
+ From: Chappy Youn <chappy1@gmail.com>
11
+ To: Daniel Choi <dhchoi@gmail.com>,
12
+ Draculette Ko <violinist.ko@gmail.com>,
13
+ Cookiemonster Youn <cookiemonster@gmail.com>
14
+ Cc: Racoon <raycoon@gmail.com>
15
+ Message-ID: <73734601-F016-4F3B-A0C8-0FF48011292F@gmail.com>
16
+ Subject: Holiday potluck at Ray's
17
+ Mime-Version: 1.0
18
+ Content-Type: text/plain;
19
+ charset=us-ascii;
20
+ delsp=yes;
21
+ format=flowed
22
+ Content-Transfer-Encoding: 7bit
23
+ Delivered-To: dhchoi@gmail.com
24
+ Received-SPF: pass (google.com: domain of chappy1@gmail.com designates
25
+ 209.85.213.171 as permitted sender) client-ip=209.85.213.171;
26
+ Authentication-Results: mx.google.com; spf=pass (google.com: domain of
27
+ chappy1@gmail.com designates 209.85.213.171 as permitted sender)
28
+ smtp.mail=chappy1@gmail.com; dkim=pass (test mode) header.i=@gmail.com
29
+ DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=gamma;
30
+ h=domainkey-signature:received:received:message-id:from:to
31
+ :content-type:content-transfer-encoding:x-mailer:mime-version
32
+ :subject:date:cc; bh=2E6xsnWGlAai1MjVchauqPtC2J0dmGnscU+hCRvcScY=;
33
+ b=joH+NKL712kaFO0ThbHEF/7q9qSf1O6Xi9u2TBCp3uQl0aEVwuqGFHcg+id0/Am5qM
34
+ IoxNBItJn9zMUgmzzZcJSl5/LaxKwrj8NOn/eYF8fkKRecCbNUoCYV4Z0T7BkuPwpkWA
35
+ PYPun1PZT8ArAM2aXoMzApEnzOCvf/ZkM78Q4=
36
+ DomainKey-Signature: a=rsa-sha1; c=nofws; d=gmail.com; s=gamma;
37
+ h=message-id:from:to:content-type:content-transfer-encoding:x-mailer
38
+ :mime-version:subject:date:cc;
39
+ b=fCuCk6O03S/wqKttwtK0ePIAy7MFja+qOyIkt9X/ToJg8MiPoHh6UH5o0UaAlAEyE7
40
+ 7C8iRyxjV4EC3vfQhzHUzZHFuXAOtPOF6ZSidHKbXVad3Wvv0zie9dV3pFXCbg0fyC8M
41
+ c1nHTFM+So9DDIulICizW196SVSh65Ds+NVK4=
42
+ X-Mailer: iPhone Mail (7C144)
43
+
44
+ Guys,
45
+ Tonight we will have a potluck at Ray's at 7. Pls bring food for 1.5
46
+ ppl.
47
+
48
+ Ray will provide wine and dessert.
49
+
50
+ Also, we will be having a poor man's Yankee swap. Pls bring something
51
+ gift wrapped from home. Nothing fancy, but something halfway decent or
52
+ funny.
53
+
54
+ El, make sure it's worth more than 50 cents.
55
+
56
+ Chappy
57
+
58
+ Sent from my iPhone
@@ -0,0 +1,32 @@
1
+ require 'test_helper'
2
+ require 'vmail/reply_template'
3
+
4
+ describe Vmail::ReplyTemplate do
5
+ before do
6
+ @raw = read_fixture('reply_all.eml')
7
+ @mail = Mail.new(@raw)
8
+ @rt = Vmail::ReplyTemplate.new(@mail, 'dhchoi@gmail.com', 'Daniel Choi', true)
9
+ end
10
+
11
+ def test_detect_primary_recipient
12
+ assert_equal "Chappy Youn <chappy1@gmail.com>", @rt.primary_recipient
13
+ end
14
+
15
+ def test_detect_cc
16
+ expected = "Draculette Ko <violinist.ko@gmail.com>, Cookiemonster Youn <cookiemonster@gmail.com>, Racoon <raycoon@gmail.com>"
17
+ assert_equal expected, @rt.cc
18
+ end
19
+
20
+ def test_sender
21
+ assert_equal "Chappy Youn <chappy1@gmail.com>", @rt.sender
22
+ end
23
+
24
+ def test_template
25
+ expected = {"from"=>"Daniel Choi <dhchoi@gmail.com>", "to"=>"Chappy Youn <chappy1@gmail.com>", "cc"=>"Draculette Ko <violinist.ko@gmail.com>, Cookiemonster Youn <cookiemonster@gmail.com>, Racoon <raycoon@gmail.com>", "subject"=>"Re: Holiday potluck at Ray's", :body=>"On Sun, Dec 12, 2010 at 01:13 PM, Chappy Youn <chappy1@gmail.com> wrote:\n\n> Guys,\n> Tonight we will have a potluck at Ray's at 7. Pls bring food for 1.5 \n> ppl.\n> \n> Ray will provide wine and dessert.\n> \n> Also, we will be having a poor man's Yankee swap. Pls bring something \n> gift wrapped from home. Nothing fancy, but something halfway decent or \n> funny.\n> \n> El, make sure it's worth more than 50 cents.\n> \n> Chappy\n> \n> Sent from my iPhone"}
26
+
27
+ assert_equal expected, @rt.reply_headers
28
+ end
29
+
30
+
31
+ end
32
+
@@ -23,10 +23,6 @@
23
23
 
24
24
  <div class="sidebar">
25
25
 
26
- <h4>share this</h4>
27
-
28
- <span class="st_twitter_large" displayText="Tweet"></span><span class="st_facebook_large" displayText="Facebook"></span><span class="st_ybuzz_large" displayText="Yahoo! Buzz"></span><span class="st_gbuzz_large" displayText="Google Buzz"></span><span class="st_email_large" displayText="Email"></span><span class="st_sharethis_large" displayText="ShareThis"></span>
29
-
30
26
  <h4>links</h4>
31
27
  <ul>
32
28
  <li><a href="https://github.com/danchoi/vmail">github repo</a></li>
@@ -34,6 +30,10 @@
34
30
  <li><a href="https://github.com/danchoi/vmail/issues">issue tracker</a></li>
35
31
  <li><a href="http://www.google.com/search?sourceid=chrome&ie=UTF-8&q=vmail+vim#hl=en&tbo=1&tbs=mbl:1,mbl_sv:0&q=link:http://danielchoi.com/software/vmail.html&sa=X&ei=iw0JTYnsPMT_lgfgvMC1AQ&ved=0CAUQ6QcwCg&fp=1&cad=b">web reactions</a></li>
36
32
  </ul>
33
+ <h4>share this</h4>
34
+
35
+ <span class="st_twitter_large" displayText="Tweet"></span><span class="st_facebook_large" displayText="Facebook"></span><span class="st_ybuzz_large" displayText="Yahoo! Buzz"></span><span class="st_gbuzz_large" displayText="Google Buzz"></span><span class="st_email_large" displayText="Email"></span><span class="st_sharethis_large" displayText="ShareThis"></span>
36
+
37
37
  </div>
38
38
 
39
39
  </div>
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 4
8
- - 5
9
- version: 0.4.5
8
+ - 6
9
+ version: 0.4.6
10
10
  platform: ruby
11
11
  authors:
12
12
  - Daniel Choi
@@ -71,6 +71,7 @@ files:
71
71
  - lib/vmail/imap_client.rb
72
72
  - lib/vmail/message_formatter.rb
73
73
  - lib/vmail/options.rb
74
+ - lib/vmail/reply_template.rb
74
75
  - lib/vmail/string_ext.rb
75
76
  - lib/vmail/version.rb
76
77
  - test/base64_test.rb
@@ -79,10 +80,12 @@ files:
79
80
  - test/fixtures/google-affiliate.eml
80
81
  - test/fixtures/htmlbody.eml
81
82
  - test/fixtures/moleskine-html.eml
83
+ - test/fixtures/reply_all.eml
82
84
  - test/fixtures/rfc_part.eml
83
85
  - test/fixtures/textbody-nocontenttype.eml
84
86
  - test/fixtures/with-attachments.eml
85
87
  - test/message_formatter_test.rb
88
+ - test/reply_template_test.rb
86
89
  - test/test_helper.rb
87
90
  - test/time_format_test.rb
88
91
  - vmail.gemspec
@@ -158,9 +161,11 @@ test_files:
158
161
  - test/fixtures/google-affiliate.eml
159
162
  - test/fixtures/htmlbody.eml
160
163
  - test/fixtures/moleskine-html.eml
164
+ - test/fixtures/reply_all.eml
161
165
  - test/fixtures/rfc_part.eml
162
166
  - test/fixtures/textbody-nocontenttype.eml
163
167
  - test/fixtures/with-attachments.eml
164
168
  - test/message_formatter_test.rb
169
+ - test/reply_template_test.rb
165
170
  - test/test_helper.rb
166
171
  - test/time_format_test.rb