imapcli 0.0.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +1 -0
- data/Gemfile.lock +4 -0
- data/NEWS +9 -0
- data/README.md +181 -53
- data/bin/imapcli +58 -18
- data/imapcli.gemspec +2 -1
- data/lib/imapcli.rb +2 -0
- data/lib/imapcli/client.rb +11 -22
- data/lib/imapcli/command.rb +106 -39
- data/lib/imapcli/mailbox.rb +95 -20
- data/lib/imapcli/option_validator.rb +70 -0
- data/lib/imapcli/stats.rb +62 -0
- data/lib/imapcli/version.rb +1 -1
- data/spec/lib/imapcli/client_spec.rb +3 -3
- data/spec/lib/imapcli/command_spec.rb +123 -0
- data/spec/lib/imapcli/mailbox_spec.rb +61 -3
- data/spec/lib/imapcli/option_validator_spec.rb +4 -0
- data/spec/lib/imapcli/stats_spec.rb +68 -0
- data/spec/spec_helper.rb +2 -0
- metadata +11 -4
@@ -0,0 +1,70 @@
|
|
1
|
+
module Imapcli
|
2
|
+
# Utility class to validate user options
|
3
|
+
#
|
4
|
+
# Invalid options will trigger a runtime exception (which is handled by GLI).
|
5
|
+
class OptionValidator
|
6
|
+
attr_reader :errors, :warnings, :options
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@errors = []
|
10
|
+
@warnings = []
|
11
|
+
end
|
12
|
+
|
13
|
+
def global_options_valid?(global_options)
|
14
|
+
if global_options[:s].nil? || global_options[:s].empty?
|
15
|
+
@errors << 'missing server name (use -s option or set IMAP_SERVER environment variable)'
|
16
|
+
end
|
17
|
+
if global_options[:s].nil? || global_options[:s].empty?
|
18
|
+
@errors << 'missing server name (use -s option or set IMAP_SERVER environment variable)'
|
19
|
+
end
|
20
|
+
if global_options[:P] && global_options[:p]
|
21
|
+
@errors << '-p and -P options do not agree'
|
22
|
+
end
|
23
|
+
|
24
|
+
pass?
|
25
|
+
end
|
26
|
+
|
27
|
+
# Validates options for the stats command.
|
28
|
+
#
|
29
|
+
# @return [true false] indicating success or failure; warnings can be accessed as attribute
|
30
|
+
def stats_options_valid?(options, args)
|
31
|
+
@options = {}
|
32
|
+
raise 'incompatible options -r/--recurse and -R/--no_recurse' if options[:r] && options[:R]
|
33
|
+
|
34
|
+
if options[:recurse]
|
35
|
+
if args.empty?
|
36
|
+
@warnings << 'warning: superfluous -r/--recurse option; will recurse from root by default'
|
37
|
+
else
|
38
|
+
@options[:depth] = -1
|
39
|
+
end
|
40
|
+
elsif options[:no_recurse]
|
41
|
+
if args.empty?
|
42
|
+
@options[:depth] = 0
|
43
|
+
else
|
44
|
+
@warnings << 'warning: superfluous -R/--no_recurse option; will not recurse from non-root mailbox by default'
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
if options[:sort]
|
49
|
+
available_sort_options = %w(count total_size min_size q1 median_size q3 max_size)
|
50
|
+
if available_sort_options.include? options[:sort].downcase
|
51
|
+
@options[:sort] = options[:sort].to_sym
|
52
|
+
else
|
53
|
+
# GLI did not print the available options even with the :must_match option
|
54
|
+
@errors << "sort option must be one of: #{available_sort_options.join(', ')}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
pass?
|
59
|
+
end
|
60
|
+
|
61
|
+
def pass?
|
62
|
+
@errors.empty?
|
63
|
+
end
|
64
|
+
|
65
|
+
def warnings?
|
66
|
+
@warnings.count > 0
|
67
|
+
end
|
68
|
+
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
module Imapcli
|
2
|
+
# Handles mailbox statistics.
|
3
|
+
#
|
4
|
+
# +message_sizes+ is an array of message sizes in bytes.
|
5
|
+
class Stats
|
6
|
+
|
7
|
+
def initialize(message_sizes = [])
|
8
|
+
@message_sizes = message_sizes
|
9
|
+
end
|
10
|
+
|
11
|
+
# Adds other statistics.
|
12
|
+
def add(other_stats)
|
13
|
+
return unless other_stats
|
14
|
+
@message_sizes += other_stats.message_sizes
|
15
|
+
invalidate
|
16
|
+
end
|
17
|
+
|
18
|
+
def count
|
19
|
+
@count ||= @message_sizes&.length
|
20
|
+
end
|
21
|
+
|
22
|
+
def total_size
|
23
|
+
@total_size ||= convert_bytes(@message_sizes.sum)
|
24
|
+
end
|
25
|
+
|
26
|
+
def min_size
|
27
|
+
@min ||= convert_bytes(@message_sizes.min)
|
28
|
+
end
|
29
|
+
|
30
|
+
def quartile_1_size
|
31
|
+
@q1 ||= convert_bytes(@message_sizes.percentile(25))
|
32
|
+
end
|
33
|
+
|
34
|
+
def median_size
|
35
|
+
@median ||= convert_bytes(@message_sizes.median)
|
36
|
+
end
|
37
|
+
|
38
|
+
def quartile_3_size
|
39
|
+
@q3 ||= convert_bytes(@message_sizes.percentile(75))
|
40
|
+
end
|
41
|
+
|
42
|
+
def max_size
|
43
|
+
@max ||= convert_bytes(@message_sizes.max)
|
44
|
+
end
|
45
|
+
|
46
|
+
protected
|
47
|
+
|
48
|
+
attr_accessor :message_sizes
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
# Converts a number of bytes to kiB.
|
53
|
+
def convert_bytes(bytes)
|
54
|
+
bytes.fdiv(1024).round if bytes
|
55
|
+
end
|
56
|
+
|
57
|
+
def invalidate
|
58
|
+
@count, @total_size, @min, @max, @q1, @q3, @median = nil # sets others to nil too
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
data/lib/imapcli/version.rb
CHANGED
@@ -34,7 +34,7 @@ RSpec.describe Imapcli::Client do
|
|
34
34
|
end
|
35
35
|
end
|
36
36
|
|
37
|
-
context 'with valid credentials for an actual server' do
|
37
|
+
context 'with valid credentials for an actual server', network: true do
|
38
38
|
before :all do
|
39
39
|
Dotenv.load
|
40
40
|
end
|
@@ -55,12 +55,12 @@ RSpec.describe Imapcli::Client do
|
|
55
55
|
end
|
56
56
|
end
|
57
57
|
|
58
|
-
context 'with invalid credentials for an actual server' do
|
58
|
+
context 'with invalid credentials for an actual server', network: true do
|
59
59
|
before :all do
|
60
60
|
Dotenv.load
|
61
61
|
end
|
62
62
|
|
63
|
-
let(:client) { Imapcli::Client.new(ENV['IMAP_SERVER'], ENV['IMAP_USER'], ENV['IMAP_PASS']
|
63
|
+
let(:client) { Imapcli::Client.new(ENV['IMAP_SERVER'], ENV['IMAP_USER'], "#{ENV['IMAP_PASS']}invalid") }
|
64
64
|
|
65
65
|
it 'cannot log in to the server' do
|
66
66
|
expect(client.login).to eq false
|
@@ -0,0 +1,123 @@
|
|
1
|
+
require 'imapcli'
|
2
|
+
|
3
|
+
RSpec.describe Imapcli::Command do
|
4
|
+
|
5
|
+
context 'without a client' do
|
6
|
+
it 'cannot be instantiated' do
|
7
|
+
expect { Imapcli::Command.new('foobar') }.to raise_error(ArgumentError)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
context 'with a mock client' do
|
12
|
+
let(:client) do
|
13
|
+
client = Imapcli::Client.new('server', 'user', 'pass')
|
14
|
+
allow(client).to receive(:login).and_return(true)
|
15
|
+
client
|
16
|
+
end
|
17
|
+
let(:command) { Imapcli::Command.new(client) }
|
18
|
+
let(:mailbox_root) do
|
19
|
+
Imapcli::Mailbox.new([
|
20
|
+
Net::IMAP::MailboxList.new(nil, '/', 'Inbox'),
|
21
|
+
Net::IMAP::MailboxList.new(nil, '/', 'Inbox/Foo'),
|
22
|
+
Net::IMAP::MailboxList.new(nil, '/', 'Inbox/Foo/Sub'),
|
23
|
+
Net::IMAP::MailboxList.new(nil, '/', 'Inbox/Bar'),
|
24
|
+
Net::IMAP::MailboxList.new(nil, '/', 'Inbox/Bar/Sub'),
|
25
|
+
Net::IMAP::MailboxList.new(nil, '/', 'Inbox/Bar/Sub/Subsub'),
|
26
|
+
])
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'can be instantiated' do
|
30
|
+
expect { Imapcli::Command.new(client) }.to_not raise_error
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'logs in' do
|
34
|
+
expect(command.check).to eq true
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'collects information about the server' do
|
38
|
+
allow(client).to receive(:greeting).and_return 'hello'
|
39
|
+
allow(client).to receive(:capability).and_return ['lots', 'of', 'capabilities']
|
40
|
+
allow(client).to receive(:separator).and_return '/'
|
41
|
+
allow(client).to receive(:supports_quota).and_return true
|
42
|
+
allow(client).to receive(:quota).and_return [ '1024', '2048', 50.00 ]
|
43
|
+
output = command.info
|
44
|
+
expect(output).to be_a Array
|
45
|
+
expect(output[0]).to eq 'greeting: hello'
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'lists mailboxes' do
|
49
|
+
allow(client).to receive(:mailbox_root).and_return mailbox_root
|
50
|
+
output = command.list
|
51
|
+
expect(output).to be_a Array
|
52
|
+
expect(output[0]).to eq '- Inbox'
|
53
|
+
end
|
54
|
+
|
55
|
+
context 'collecting statistics' do
|
56
|
+
before :each do
|
57
|
+
allow(client).to receive(:mailbox_root).and_return mailbox_root
|
58
|
+
allow(client).to receive(:separator).and_return '/'
|
59
|
+
allow(client).to receive(:message_sizes) do |mailbox|
|
60
|
+
case mailbox
|
61
|
+
when 'Inbox'
|
62
|
+
(1..4).map { |i| 1024 * i }
|
63
|
+
when 'Inbox/Foo'
|
64
|
+
(3..10).map { |i| 1024 * i }
|
65
|
+
when 'Inbox/Foo/Sub'
|
66
|
+
Array.new(10, 1024)
|
67
|
+
when 'Inbox/Bar'
|
68
|
+
(1..2).map { |i| 1024 * i }
|
69
|
+
when 'Inbox/Bar/Sub'
|
70
|
+
[ 1, 1024 * 20 ]
|
71
|
+
when 'Inbox/Bar/Sub/Subsub'
|
72
|
+
(1..4).map { |i| 1024 * i }
|
73
|
+
else
|
74
|
+
[1024, 2048, 4096, 8192]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'for all folders' do
|
80
|
+
output = command.stats()
|
81
|
+
expect(output).to be_a Array
|
82
|
+
expect(output.length).to eq 8 # including divider and summary line
|
83
|
+
expect(output[0][0]).to eq 'Inbox'
|
84
|
+
expect(output[0][1]).to eq 4
|
85
|
+
end
|
86
|
+
it 'for a given folder' do
|
87
|
+
output = command.stats('Inbox/Foo')
|
88
|
+
expect(output).to be_a Array
|
89
|
+
expect(output.length).to eq 3 # including divider and summary line
|
90
|
+
expect(output[0][0]).to eq 'Inbox/Foo'
|
91
|
+
expect(output[0][1]).to eq 8 # depends on message_sizes stub (see above)
|
92
|
+
end
|
93
|
+
it 'for a given folder and subfolders' do
|
94
|
+
output = command.stats('Inbox/Foo', depth: -1)
|
95
|
+
expect(output).to be_a Array
|
96
|
+
expect(output.length).to eq 4 # including divider and summary line
|
97
|
+
expect(output[1][0]).to eq 'Inbox/Foo/Sub'
|
98
|
+
end
|
99
|
+
it 'sorts by number of messages' do
|
100
|
+
output = command.stats('Inbox', depth: -1, sort: :count, sort_order: :desc)
|
101
|
+
expect(output).to be_a Array
|
102
|
+
expect(output[0][0]).to eq 'Inbox/Foo/Sub'
|
103
|
+
end
|
104
|
+
it 'sorts by total message size' do
|
105
|
+
output = command.stats('Inbox', depth: -1, sort: :total_size, sort_order: :desc)
|
106
|
+
expect(output).to be_a Array
|
107
|
+
expect(output[0][0]).to eq 'Inbox/Foo'
|
108
|
+
end
|
109
|
+
it 'sorts by largest message' do
|
110
|
+
output = command.stats('Inbox', depth: -1, sort: :max_size, sort_order: :desc)
|
111
|
+
expect(output).to be_a Array
|
112
|
+
expect(output[0][0]).to eq 'Inbox/Bar/Sub'
|
113
|
+
end
|
114
|
+
it 'sorts by smallest message' do
|
115
|
+
output = command.stats('Inbox', depth: -1, sort: :min_size, sort_order: :desc)
|
116
|
+
expect(output).to be_a Array
|
117
|
+
# binding.pry
|
118
|
+
expect(output[0][0]).to eq 'Inbox/Foo'
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
end
|
123
|
+
end
|
@@ -2,13 +2,22 @@ require 'imapcli'
|
|
2
2
|
require 'net/imap'
|
3
3
|
|
4
4
|
RSpec.describe Imapcli::Mailbox do
|
5
|
+
let(:mailbox) do
|
6
|
+
Imapcli::Mailbox.new([
|
7
|
+
Net::IMAP::MailboxList.new(nil, '/', 'Inbox'),
|
8
|
+
Net::IMAP::MailboxList.new(nil, '/', 'Inbox/Foo'),
|
9
|
+
Net::IMAP::MailboxList.new(nil, '/', 'Inbox/Foo/Sub'),
|
10
|
+
Net::IMAP::MailboxList.new(nil, '/', 'Inbox/Bar'),
|
11
|
+
Net::IMAP::MailboxList.new(nil, '/', 'Inbox/Bar/Sub'),
|
12
|
+
Net::IMAP::MailboxList.new(nil, '/', 'Inbox/Bar/Sub/Subsub'),
|
13
|
+
])
|
14
|
+
end
|
5
15
|
# it 'parses a mailbox list' do
|
6
16
|
# mailbox_list = Net::IMAP::MailboxList.new(attr: nil, delim: '/', name: 'Root/Subfolder')
|
7
|
-
#
|
8
|
-
# expect(
|
17
|
+
# mailbox_root = Imapcli::MailboxTree.new(mailbox_list)
|
18
|
+
# expect(mailbox_root.tree.length).to eq 1
|
9
19
|
# end
|
10
20
|
it 'returns nil if a given sub mailbox does not exist' do
|
11
|
-
mailbox = Imapcli::Mailbox.new
|
12
21
|
expect(mailbox.find_sub_mailbox('INBOX.does.not.exist', '.')).to eq nil
|
13
22
|
end
|
14
23
|
it 'adds and retrieves an existing sub mailbox' do
|
@@ -18,4 +27,53 @@ RSpec.describe Imapcli::Mailbox do
|
|
18
27
|
mailbox.add_mailbox(imap_mailbox_list)
|
19
28
|
expect(mailbox.find_sub_mailbox(name, '/').imap_mailbox_list).to eq imap_mailbox_list
|
20
29
|
end
|
30
|
+
it 'counts the number of mailboxes' do
|
31
|
+
expect(mailbox.count).to eq 7 # includes virtual root mailbox
|
32
|
+
end
|
33
|
+
it 'determines the maximum level in the subtree' do
|
34
|
+
expect(mailbox.get_max_level).to eq 3
|
35
|
+
end
|
36
|
+
it 'converts a tree to a list' do
|
37
|
+
list = mailbox.to_list
|
38
|
+
list_names = list.map { |m| m.full_name }
|
39
|
+
expect(list_names).to eq [
|
40
|
+
'Inbox',
|
41
|
+
'Inbox/Bar',
|
42
|
+
'Inbox/Bar/Sub',
|
43
|
+
'Inbox/Bar/Sub/Subsub',
|
44
|
+
'Inbox/Foo',
|
45
|
+
'Inbox/Foo/Sub',
|
46
|
+
]
|
47
|
+
end
|
48
|
+
it 'converts a tree to a list up to a certain level' do
|
49
|
+
list = mailbox.to_list(1)
|
50
|
+
list_names = list.map { |m| m.full_name }
|
51
|
+
expect(list_names).to eq [
|
52
|
+
'Inbox',
|
53
|
+
'Inbox/Bar',
|
54
|
+
'Inbox/Foo'
|
55
|
+
]
|
56
|
+
end
|
57
|
+
|
58
|
+
context 'identification and consolidation' do
|
59
|
+
let(:parent) { mailbox.find_sub_mailbox('Inbox', '/') }
|
60
|
+
let(:child) { mailbox.find_sub_mailbox('Inbox/Bar/Sub/Subsub', '/') }
|
61
|
+
|
62
|
+
it 'knows if it is contains another mailbox' do
|
63
|
+
expect(parent.contains? child).to eq true
|
64
|
+
end
|
65
|
+
it 'knows if it is does not contain another mailbox' do
|
66
|
+
expect(child.contains? mailbox).to eq false
|
67
|
+
end
|
68
|
+
# it 'consolidates several of the same mailboxes' do
|
69
|
+
# expect(Imapcli::Mailbox.consolidate([parent, parent])).to eq [ parent ]
|
70
|
+
# end
|
71
|
+
it 'consolidates several of the same mailbox' do
|
72
|
+
expect(Imapcli::Mailbox.consolidate([child, child])).to eq [ child ]
|
73
|
+
end
|
74
|
+
it 'consolidates several different mailboxes' do
|
75
|
+
expect(Imapcli::Mailbox.consolidate([child, parent])).to eq [ parent ]
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
21
79
|
end
|
@@ -0,0 +1,68 @@
|
|
1
|
+
require 'imapcli'
|
2
|
+
|
3
|
+
RSpec.describe Imapcli::Stats do
|
4
|
+
let(:array) { (1..12).map { |i| i * 1024 } }
|
5
|
+
let(:other_array) { (13..24).map { |i| i * 1024 } }
|
6
|
+
let(:stats) { Imapcli::Stats.new(array) }
|
7
|
+
let(:other_stats) { Imapcli::Stats.new(other_array) }
|
8
|
+
|
9
|
+
it 'knows the number of items' do
|
10
|
+
expect(stats.count).to eq 12
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'computes the minimum' do
|
14
|
+
expect(stats.min_size).to eq 1
|
15
|
+
end
|
16
|
+
|
17
|
+
it 'computes the maximum' do
|
18
|
+
expect(stats.max_size).to eq 12
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'computes the median' do
|
22
|
+
expect(stats.median_size).to eq 7 # response is rounded
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'computes the first quartile' do
|
26
|
+
expect(stats.quartile_1_size).to eq 4 # response is rounded
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'computes the third quartile' do
|
30
|
+
expect(stats.quartile_3_size).to eq 9 # response is rounded
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'adds nothing if other stats are nil' do
|
34
|
+
expect { stats.add nil }.to_not raise_error
|
35
|
+
end
|
36
|
+
|
37
|
+
context 'adding other stats' do
|
38
|
+
before :each do
|
39
|
+
stats.add other_stats
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'can add other stats' do
|
43
|
+
expect(stats.count).to eq 24
|
44
|
+
end
|
45
|
+
|
46
|
+
it 'computes the minimum' do
|
47
|
+
expect(stats.min_size).to eq 1
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'computes the maximum' do
|
51
|
+
expect(stats.max_size).to eq 24
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'computes the median' do
|
55
|
+
expect(stats.median_size).to eq 13 # response is rounded
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'computes the first quartile' do
|
59
|
+
expect(stats.quartile_1_size).to eq 7 # response is rounded
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'computes the third quartile' do
|
63
|
+
expect(stats.quartile_3_size).to eq 18 # response is rounded
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
|
68
|
+
end
|
data/spec/spec_helper.rb
CHANGED
@@ -46,6 +46,8 @@ RSpec.configure do |config|
|
|
46
46
|
# triggering implicit auto-inclusion in groups with matching metadata.
|
47
47
|
config.shared_context_metadata_behavior = :apply_to_host_groups
|
48
48
|
|
49
|
+
config.filter_run_excluding network: true
|
50
|
+
|
49
51
|
# The settings below are suggested to provide a good initial experience
|
50
52
|
# with RSpec, but feel free to customize to your heart's content.
|
51
53
|
=begin
|