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 +27 -0
- data/db/create.sql +25 -0
- data/lib/vmail.rb +12 -12
- data/lib/vmail.vim +19 -23
- data/lib/vmail/database.rb +30 -0
- data/lib/vmail/flagging_and_moving.rb +91 -0
- data/lib/vmail/imap_client.rb +58 -416
- data/lib/vmail/message_formatter.rb +19 -46
- data/lib/vmail/query.rb +7 -7
- data/lib/vmail/reply_template.rb +1 -1
- data/lib/vmail/searching.rb +61 -0
- data/lib/vmail/showing_headers.rb +103 -0
- data/lib/vmail/showing_message.rb +91 -0
- data/lib/vmail/version.rb +1 -1
- data/vmail.gemspec +2 -0
- metadata +31 -2
@@ -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
|
29
|
-
|
30
|
-
|
31
|
-
|
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
|
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
|
-
|
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
|
-
"
|
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
|
-
|
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
|
data/lib/vmail/query.rb
CHANGED
@@ -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
|
-
[
|
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
|
-
|
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)
|
data/lib/vmail/reply_template.rb
CHANGED
@@ -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.
|
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
|
data/lib/vmail/version.rb
CHANGED
data/vmail.gemspec
CHANGED
metadata
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
name: vmail
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: 1.6.
|
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-
|
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
|