imapcli 0.0.1 → 1.0.0

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.
@@ -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