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.
@@ -11,9 +11,6 @@ module Vmail
11
11
  end
12
12
 
13
13
  def list_parts(parts = (@mail.parts.empty? ? [@mail] : @mail.parts))
14
- if parts.empty?
15
- return []
16
- end
17
14
  lines = parts.map do |part|
18
15
  if part.multipart?
19
16
  list_parts(part.parts)
@@ -25,23 +22,24 @@ module Vmail
25
22
  lines.flatten
26
23
  end
27
24
 
28
- def process_body(target = @mail)
29
- out = if target.header['Content-Type'].to_s =~ /multipart\/mixed/
30
- target.parts.map {|part|
31
- if part.multipart?
32
- part = find_text_or_html_part(target.parts)
33
- format_part(part)
34
- else
35
- format_part(part)
36
- end
37
- }.join("\n#{'-' * 39}\n")
38
- elsif target.header['Content-Type'].to_s =~ /multipart\/alternative/
39
- part = find_text_or_html_part(target.parts)
40
- format_part(part)
25
+ def plaintext_part(mail=@mail)
26
+ part = find_text_part2(mail.body, mail.content_type)
27
+ if part.nil?
28
+ "[No message body]"
41
29
  else
42
- format_part(target)
30
+ format_part part
31
+ end
32
+ end
33
+
34
+ # helper method
35
+ def find_text_part2(part, content_type)
36
+ if part.multipart?
37
+ part.parts.map {|p| find_text_part2(p, p.content_type)}.compact.first
38
+ elsif content_type =~ %r[^text/plain] ||
39
+ content_type =~ %r[text/plain] ||
40
+ content_type =~ %r[message/rfc]
41
+ part
43
42
  end
44
- out
45
43
  end
46
44
 
47
45
  def format_part(part)
@@ -53,45 +51,20 @@ module Vmail
53
51
  format_text_body(part)
54
52
  when /message\/rfc/
55
53
  m = Mail.new(part.body.decoded)
56
- process_body(m)
54
+ plaintext_part(m)
57
55
  else # just format_text on it anyway
58
56
  format_text_body(part)
59
57
  end
60
58
  else
61
- "[NO BODY]"
59
+ part.decoded.gsub("\r", '')
62
60
  end
63
61
  rescue
64
62
  puts $!
65
63
  "[error:] #{$!}"
66
64
  end
67
65
 
68
- def find_text_or_html_part(parts = @mail.parts)
69
- if parts.empty?
70
- return @mail
71
- end
72
- part = parts.detect {|part| part.multipart?}
73
- if part
74
- find_text_or_html_part(part.parts)
75
- else
76
- # no multipart part
77
- part = parts.detect {|part| (part.header["Content-Type"].to_s =~ /text\/plain/) }
78
- if part
79
- return part
80
- else
81
- parts.first
82
- end
83
- end
84
- end
85
-
86
66
  def format_text_body(part)
87
- text = part.body.decoded.gsub("\r", '')
88
- charset = (part.content_type_parameters && part.content_type_parameters['charset']) || encoding
89
- if (charset && charset != 'UTF-8')
90
- text.force_encoding(charset)
91
- else
92
- text
93
- end
94
- text.encode("UTF-8", undef: :replace, replace: "??", invalid: :replace)
67
+ part.body.decoded.gsub("\r", '')
95
68
  end
96
69
 
97
70
  # depend on lynx or whatever is set by the VMAIL_HTML_PART_READER
@@ -3,19 +3,19 @@ module Vmail
3
3
  class Query
4
4
  # args is an array like ARGV
5
5
  def self.parse(args)
6
+ args = args.dup
6
7
  if args.is_a?(String)
7
8
  args = Shellwords.shellwords args
8
9
  end
10
+ if args.size > 0 && args.first =~ /^\d+/
11
+ args.shift
12
+ end
9
13
  query = if args.empty?
10
- [100, 'ALL']
11
- elsif args.size == 1 && args[0] =~ /^\d+/
12
- [args.shift, "ALL"]
13
- elsif args[0] =~ /^\d+/
14
- args
14
+ ['ALL']
15
15
  else
16
- [100] + args
16
+ args
17
17
  end
18
- query
18
+ query.map {|x| x.to_s.downcase}
19
19
  end
20
20
 
21
21
  def self.args2string(array)
@@ -19,7 +19,7 @@ module Vmail
19
19
  end
20
20
  date = @orig_headers['date'].is_a?(String) ? Time.parse(@orig_headers['date']) : @orig_headers['date']
21
21
  quote_header = date ? "On #{date.strftime('%a, %b %d, %Y at %I:%M %p')}, #{sender} wrote:\n\n" : "#{sender} wrote:\n\n"
22
- body = quote_header + formatter.process_body
22
+ body = quote_header + formatter.plaintext_part
23
23
  begin
24
24
  body = body.gsub(/^(?=>)/, ">").gsub(/^(?!>)/, "> ")
25
25
  rescue
@@ -0,0 +1,61 @@
1
+ module Vmail
2
+ module Searching
3
+ # The main function called by the client to retrieve messages
4
+ def search(query)
5
+ @query = Vmail::Query.parse(query)
6
+ # customizable @limit is Deprecated
7
+ @limit = 100
8
+
9
+ if search_query?
10
+ #@num_messages = @all_ids.size
11
+ query_string = Vmail::Query.args2string(@query)
12
+ @ids = reconnect_if_necessary(180) do # timeout of 3 minutes
13
+ @imap.search(query_string)
14
+ end
15
+ @start_index = [@ids.size - @limit, 0].max
16
+ else
17
+ # set the target range to the whole set, unless it is too big
18
+ @start_index = [@num_messages - @limit, 0].max + 1
19
+ @query.unshift "#{@start_index}:#@num_messages"
20
+ query_string = Vmail::Query.args2string(@query)
21
+ log "Query: #{query_string.inspect}"
22
+ @ids = reconnect_if_necessary(180) do # timeout of 3 minutes
23
+ @imap.search(query_string)
24
+ end
25
+ end
26
+
27
+ if @ids.empty?
28
+ return "No messages"
29
+ end
30
+
31
+ self.max_seqno = @ids[-1] # this is a instance var
32
+ log "- Query got #{@ids.size} results; max seqno: #{self.max_seqno}"
33
+ clear_cached_message
34
+
35
+ select_ids = search_query? ? @ids[-@limit,@limit] : @ids
36
+
37
+ if select_ids.size > @limit
38
+ raise "Too many messages to fetch headers for"
39
+ end
40
+
41
+ message_ids = fetch_and_cache_headers select_ids
42
+ res = get_message_headers message_ids
43
+
44
+ if STDOUT.tty?
45
+ with_more_message_line(res)
46
+ else
47
+ # non interactive mode
48
+ puts [@mailbox, res].join("\n")
49
+ end
50
+ rescue
51
+ log "ERROR:\n#{$!.inspect}\n#{$!.backtrace.join("\n")}"
52
+ end
53
+
54
+ def search_query?
55
+ x = @query[-1] != 'all'
56
+ log "Search query? #{x} #{@query.inspect}"
57
+ x
58
+ end
59
+ end
60
+ end
61
+
@@ -0,0 +1,103 @@
1
+ module Vmail
2
+ module ShowingHeaders
3
+
4
+ def get_message_headers(message_ids)
5
+ messages = message_ids.map {|message_id| Message[message_id] }
6
+ messages.map {|m| format_header_for_list(m)}.join("\n")
7
+ end
8
+
9
+ def fetch_and_cache_headers(id_set)
10
+ log "Fetching headers for #{id_set.size} messages"
11
+ results = reconnect_if_necessary do
12
+ @imap.fetch(id_set, ["FLAGS", "ENVELOPE", "RFC822.SIZE", "UID"])
13
+ end
14
+ results.reverse.map do |x|
15
+ envelope = x.attr["ENVELOPE"]
16
+ message_id = envelope.message_id
17
+ subject = Mail::Encodings.unquote_and_convert_to(envelope.subject, 'UTF-8')
18
+ recipients = ((envelope.to || []) + (envelope.cc || [])).map {|a| extract_address(a)}.join(', ')
19
+ sender = extract_address envelope.from.first
20
+ uid = x.attr["UID"]
21
+ message = Message[message_id]
22
+ unless message
23
+ message = Message.new
24
+ message.message_id = message_id
25
+ message.save
26
+ end
27
+ params = {
28
+ subject: (subject || ''),
29
+ flags: x.attr['FLAGS'].join(','),
30
+ date: DateTime.parse(envelope.date).to_s,
31
+ size: x.attr['RFC822.SIZE'],
32
+ sender: sender,
33
+ recipients: recipients
34
+ }
35
+ # We really just need to update the flags, buy let's update everything
36
+ message.update params
37
+
38
+ unless message.labels.include?(@label)
39
+ params = {message_id: message.message_id, uid: uid, label_id: @label.label_id}
40
+
41
+ Labeling.create params
42
+ end
43
+ message_id
44
+ end
45
+ end
46
+
47
+ def extract_address(address_struct)
48
+ address = if address_struct.nil?
49
+ "Unknown"
50
+ elsif address_struct.name
51
+ "#{Mail::Encodings.unquote_and_convert_to(address_struct.name, 'UTF-8')} <#{[address_struct.mailbox, address_struct.host].join('@')}>"
52
+ else
53
+ [Mail::Encodings.unquote_and_convert_to(address_struct.mailbox, 'UTF-8'), Mail::Encodings.unquote_and_convert_to(address_struct.host, 'UTF-8')].join('@')
54
+ end
55
+
56
+ end
57
+
58
+ def format_header_for_list(message)
59
+ date = DateTime.parse(message.date)
60
+ formatted_date = if date.year != Time.now.year
61
+ date.strftime "%b %d %Y"
62
+ else
63
+ date.strftime "%b %d %I:%M%P"
64
+ end
65
+ address = if @mailbox == mailbox_aliases['sent']
66
+ message.recipients
67
+ else
68
+ message.sender
69
+ end
70
+
71
+ mid_width = @width - 38
72
+ address_col_width = (mid_width * 0.3).ceil
73
+ subject_col_width = (mid_width * 0.7).floor
74
+ row_text = [ format_flags(message.flags).col(2),
75
+ (formatted_date || '').col(14),
76
+ address.col(address_col_width),
77
+ message.subject.col(subject_col_width),
78
+ number_to_human_size(message.size).rcol(7),
79
+ message.message_id ].join(' | ')
80
+ end
81
+
82
+ FLAGMAP = {'Flagged' => '*', 'Answered' => 'A'}
83
+
84
+ def format_flags(flags)
85
+ # other flags like "Old" should be hidden here
86
+ flags = flags.split(',').map {|flag| FLAGMAP[flag] || flag}
87
+ flags.delete("Old")
88
+ if flags.delete('Seen').nil?
89
+ flags << '+' # unread
90
+ end
91
+ flags.join('')
92
+ end
93
+
94
+ def with_more_message_line(res)
95
+ remaining = @start_index
96
+ if remaining < 1
97
+ return res
98
+ end
99
+ res + "\n> Load #{[100, remaining].min} more messages. #{remaining} remaining."
100
+ end
101
+
102
+ end
103
+ end
@@ -0,0 +1,91 @@
1
+ module Vmail
2
+ module ShowingMessage
3
+
4
+ def current_message
5
+ return unless @cur_message_id
6
+ Message[@cur_message_id]
7
+ end
8
+
9
+ def current_mail
10
+ (c = current_message) && Mail.new(c.rfc822)
11
+ end
12
+
13
+ def cached_full_message?(message_id)
14
+ m = Message[message_id]
15
+ log "- found message #{message_id}"
16
+ log "- message has plaintext? #{!m.plaintext.nil?}"
17
+ m && !m.plaintext.nil? && m
18
+ end
19
+
20
+ def show_message(message_id, raw=false)
21
+ log "Show message: #{message_id}"
22
+ return current_message.rfc822 if raw
23
+ log "Showing message message_id: #{message_id}"
24
+ res = fetch_and_cache(message_id)
25
+ if res.nil?
26
+ # retry, though this is a hack!
27
+ log "- data is nil. retrying..."
28
+ return show_message(message_id, raw)
29
+ end
30
+ res
31
+ end
32
+
33
+ def fetch_and_cache(message_id)
34
+ if message = cached_full_message?(message_id)
35
+ log "- full message cache hit"
36
+ return message.plaintext
37
+ end
38
+ log "- full message cache miss"
39
+ labeling = Labeling[message_id: message_id, label_id: @label.label_id]
40
+ uid = labeling.uid
41
+
42
+ log "- fetching message uid #{uid}"
43
+ fetch_data = reconnect_if_necessary do
44
+ res = retry_if_needed do
45
+ @imap.uid_fetch(uid, ["FLAGS", "RFC822", "RFC822.SIZE"])
46
+ end
47
+ res[0]
48
+ end
49
+ seqno = fetch_data.seqno
50
+ rfc822 = Mail.new(fetch_data.attr['RFC822'])
51
+ formatter = Vmail::MessageFormatter.new rfc822
52
+
53
+ message = Message[message_id]
54
+ message_text = <<-EOF
55
+ #{message_id} #{number_to_human_size message.size} #{message.flags} #{format_parts_info(formatter.list_parts)}
56
+ #{divider '-'}
57
+ #{format_headers(formatter.extract_headers)}
58
+
59
+ #{formatter.plaintext_part}
60
+ EOF
61
+ # 2 calls so we can see more fine grained exceptions
62
+ message.update(:rfc822 => rfc822)
63
+ if !message_text.valid_encoding?
64
+ ic = Iconv.new('UTF-8//IGNORE', 'UTF-8')
65
+ message_text = ic.iconv(message_text)
66
+ end
67
+
68
+ begin
69
+ message.update(:plaintext => message_text)
70
+ rescue
71
+ log message_text.encoding
72
+ #log message_text
73
+ raise
74
+ end
75
+ @cur_message_id = message.message_id
76
+ message_text
77
+ rescue
78
+ msg = "Error encountered in fetch_and_cache(), message_id #{message_id} [#{@mailbox}]:\n#{$!}\n#{$!.backtrace.join("\n")}"
79
+ log msg
80
+ msg
81
+ end
82
+
83
+ def format_parts_info(parts)
84
+ lines = parts.select {|part| part !~ %r{text/plain}}
85
+ if lines.size > 0
86
+ "\n#{lines.join("\n")}"
87
+ end
88
+ end
89
+
90
+ end
91
+ end
@@ -1,3 +1,3 @@
1
1
  module Vmail
2
- VERSION = '1.6.8'
2
+ VERSION = '1.6.9'
3
3
  end
@@ -23,4 +23,6 @@ Gem::Specification.new do |s|
23
23
 
24
24
  s.add_dependency 'mail', '>= 2.2.12'
25
25
  s.add_dependency 'highline', '>= 1.6.1'
26
+ s.add_dependency 'sequel'
27
+ s.add_dependency 'sqlite3'
26
28
  end
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: vmail
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 1.6.8
5
+ version: 1.6.9
6
6
  platform: ruby
7
7
  authors:
8
8
  - Daniel Choi
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2011-06-07 00:00:00 -04:00
13
+ date: 2011-06-28 00:00:00 -04:00
14
14
  default_executable:
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
@@ -35,6 +35,28 @@ dependencies:
35
35
  version: 1.6.1
36
36
  type: :runtime
37
37
  version_requirements: *id002
38
+ - !ruby/object:Gem::Dependency
39
+ name: sequel
40
+ prerelease: false
41
+ requirement: &id003 !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: "0"
47
+ type: :runtime
48
+ version_requirements: *id003
49
+ - !ruby/object:Gem::Dependency
50
+ name: sqlite3
51
+ prerelease: false
52
+ requirement: &id004 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: "0"
58
+ type: :runtime
59
+ version_requirements: *id004
38
60
  description: Manage your email with Vim
39
61
  email:
40
62
  - dhchoi@gmail.com
@@ -49,23 +71,30 @@ extra_rdoc_files: []
49
71
  files:
50
72
  - .gitignore
51
73
  - MIT-LICENSE.txt
74
+ - NOTES
52
75
  - README.markdown
53
76
  - Rakefile
54
77
  - TODO
55
78
  - bin/vmail
56
79
  - bin/vmail_client
57
80
  - bin/vmailsend
81
+ - db/create.sql
58
82
  - lib/vmail.rb
59
83
  - lib/vmail.vim
60
84
  - lib/vmail/address_quoter.rb
61
85
  - lib/vmail/contacts_extractor.rb
86
+ - lib/vmail/database.rb
87
+ - lib/vmail/flagging_and_moving.rb
62
88
  - lib/vmail/imap_client.rb
63
89
  - lib/vmail/message_formatter.rb
64
90
  - lib/vmail/options.rb
65
91
  - lib/vmail/query.rb
66
92
  - lib/vmail/reply_template.rb
93
+ - lib/vmail/searching.rb
67
94
  - lib/vmail/send_options.rb
68
95
  - lib/vmail/sender.rb
96
+ - lib/vmail/showing_headers.rb
97
+ - lib/vmail/showing_message.rb
69
98
  - lib/vmail/string_ext.rb
70
99
  - lib/vmail/version.rb
71
100
  - test/address_quoter_test.rb