imapcli 0.0.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Imapcli
2
- VERSION = '0.0.1'
2
+ VERSION = '1.0.0'
3
3
  end
@@ -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'] + 'invalid') }
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
- # mailbox_tree = Imapcli::MailboxTree.new(mailbox_list)
8
- # expect(mailbox_tree.tree.length).to eq 1
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,4 @@
1
+ require 'imapcli'
2
+
3
+ RSpec.describe Imapcli::OptionValidator do
4
+ 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