imapcli 2.0.0 → 2.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 251ac4bd70a607d8891eb4035bf69ba10017995436d2af15138e8647ac042530
4
- data.tar.gz: 57072fa35d2eb0e53a85e27464638844fedb83807c8e4f98ad59c502cbd07be3
3
+ metadata.gz: 0736f0dbe2cb473770fb5f92f0f37546b9f26a6e892bbca3c69b072c1f06db9f
4
+ data.tar.gz: '0609a383ac74799eb2aade3d52fa0c5c811fc5cb4b253f1607e0b6f2dbe7cf01'
5
5
  SHA512:
6
- metadata.gz: 5cfc0a536bb6ae0b71e4262cf9ae03d7213c093c10b6c47597a02ee2bac101357423ec0df5a66038d2e6673fcf03973cef1d564015dc56dd3c665bcb31521da4
7
- data.tar.gz: b2f986bd44dfe6e3374b272bf50fd238ebff681665a32ddad07c2930e8842f5132c27df861dd31f13486b167179649b94ea2e788679721f952662a6620170102
6
+ metadata.gz: 69d818a1780fda1549bbd83581c2d0abbc4ecda7274cb411bedd450a8365e322153263c1f5b490b03a183e7c8cb02e5d382c82c8628ee43e1c1c1294f6e76afa
7
+ data.tar.gz: e664577a0949609bebd90a7bc08b94e4559001fcb7b26429958fe38801acd835089b030cad3d3b1fa09a95ccc51b400f6cb8f66befc4903ebe1cb9b4bb56746d
data/CHANGELOG.md CHANGED
@@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
+ ## [Version 2.1.0 (2025-03-16)][v2.1.0]
9
+
10
+ ### Added
11
+
12
+ - New command-line flag `-l`/`--limit` to limit the statistics output to
13
+ a given number of mailboxes (folders).
14
+
15
+ ## [Version 2.0.1 (2025-03-11)][v2.0.1]
16
+
17
+ ### Fixed
18
+
19
+ - Do not crash with empty IMAP mailboxes.
20
+
8
21
  ## [Version 2.0.0 (2025-03-10)][v2.0.0]
9
22
 
10
23
  ### Added
@@ -66,6 +79,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
66
79
  - Fix: Docker image.
67
80
  - Fix: Options error message.
68
81
 
82
+ [v2.1.0]: https://github.com/bovender/imapcli/releases/tag/v2.1.0
83
+ [v2.0.1]: https://github.com/bovender/imapcli/releases/tag/v2.0.1
69
84
  [v2.0.0]: https://github.com/bovender/imapcli/releases/tag/v2.0.0
70
85
  [v1.0.7]: https://github.com/bovender/imapcli/releases/tag/v1.0.7
71
86
  [v1.0.5]: https://github.com/bovender/imapcli/releases/tag/v1.0.5
data/Dockerfile CHANGED
@@ -1,4 +1,4 @@
1
- FROM ruby:alpine
1
+ FROM ruby:3.4-alpine
2
2
  LABEL maintainer="bovender@bovender.de"
3
3
  LABEL description="Command-line tool to query IMAP servers, collect stats etc."
4
4
 
@@ -7,11 +7,12 @@ ADD . /imapcli
7
7
  RUN apk add --no-cache git build-base && \
8
8
  gem update --system && \
9
9
  cd imapcli && \
10
- # bundle config set --local without development test && \
10
+ bundle config set --local without development test && \
11
+ bundle config set --local deployment true && \
11
12
  bundle install && \
12
13
  apk del build-base && \
13
14
  rm -rf /var/cache/apk/*
14
15
 
15
16
  WORKDIR /imapcli
16
- ENTRYPOINT ["exe/imapcli"]
17
+ ENTRYPOINT ["bundle", "exec", "exe/imapcli"]
17
18
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- imapcli (1.0.7)
4
+ imapcli (2.0.1)
5
5
  activesupport (~> 8.0)
6
6
  csv
7
7
  descriptive_statistics (~> 2.5)
@@ -16,7 +16,7 @@ PATH
16
16
  GEM
17
17
  remote: https://rubygems.org/
18
18
  specs:
19
- activesupport (8.0.1)
19
+ activesupport (8.0.2)
20
20
  base64
21
21
  benchmark (>= 0.3)
22
22
  bigdecimal
@@ -83,7 +83,7 @@ GEM
83
83
  pp (>= 0.6.0)
84
84
  rdoc (>= 4.0.0)
85
85
  reline (>= 0.4.2)
86
- json (2.10.1)
86
+ json (2.10.2)
87
87
  language_server-protocol (3.17.0.4)
88
88
  lint_roller (1.1.0)
89
89
  listen (3.9.0)
@@ -92,7 +92,7 @@ GEM
92
92
  logger (1.6.6)
93
93
  lumberjack (1.2.10)
94
94
  method_source (1.1.0)
95
- minitest (5.25.4)
95
+ minitest (5.25.5)
96
96
  nenv (0.3.0)
97
97
  net-imap (0.5.6)
98
98
  date
@@ -142,7 +142,7 @@ GEM
142
142
  diff-lcs (>= 1.2.0, < 2.0)
143
143
  rspec-support (~> 3.13.0)
144
144
  rspec-support (3.13.2)
145
- rubocop (1.73.2)
145
+ rubocop (1.74.0)
146
146
  json (~> 2.3)
147
147
  language_server-protocol (~> 3.17.0.2)
148
148
  lint_roller (~> 1.1.0)
@@ -153,7 +153,7 @@ GEM
153
153
  rubocop-ast (>= 1.38.0, < 2.0)
154
154
  ruby-progressbar (~> 1.7)
155
155
  unicode-display_width (>= 2.4.0, < 4.0)
156
- rubocop-ast (1.38.1)
156
+ rubocop-ast (1.39.0)
157
157
  parser (>= 3.3.1.0)
158
158
  rubocop-performance (1.24.0)
159
159
  lint_roller (~> 1.1)
@@ -237,4 +237,4 @@ DEPENDENCIES
237
237
  simplecov
238
238
 
239
239
  BUNDLED WITH
240
- 2.5.22
240
+ 2.6.5
data/README.md CHANGED
@@ -15,7 +15,6 @@ IMAP mailbox sizes.
15
15
  - [Terminology](#terminology)
16
16
  - [Usage](#usage)
17
17
  - [Alternative resources](#alternative-resources)
18
- - [State of the project](#state-of-the-project)
19
18
  - [Credits](#credits)
20
19
  - [License](#license)
21
20
 
@@ -264,7 +263,18 @@ keyword, as shown below.
264
263
 
265
264
  Example:
266
265
 
267
- bundle exec -it bin/imapcli -s yourserver.example.com -u myusername -P stats -r -o max_size Archive
266
+ docker run -it bin/imapcli -s yourserver.example.com -u myusername -P stats -r -o max_size Archive
267
+
268
+ #### Limiting the number of mailboxes
269
+
270
+ You can limit the number of mailboxes (folders) in the output table with the
271
+ `-l`/`--limit` flag. This flag takes an argument that indicates the maximum
272
+ number of mailboxes. THe default value for this argument is 10.
273
+
274
+ Use case: Say you have a very large IMAP account, and you would like to know the
275
+ 5 folders that contain the largest messages. The command should be:
276
+
277
+ imapcli -s yourserver.example.com -u myusername -P stats -o max_size --limit 5
268
278
 
269
279
  #### Obtaining comma-separated values (CSV)
270
280
 
@@ -292,20 +302,13 @@ following:
292
302
 
293
303
  Pull down e-mails from an IMAP server to your local disk
294
304
 
295
- ## State of the project
296
-
297
- I have not been able to work on this project for quite some time. It still
298
- serves me well when I occasionally need it. Pull requests are of course welcome.
299
-
300
- This project is [semantically versioned](https://semver.org).
301
-
302
305
  ### To do
303
306
 
304
- [x] More human-friendly number formatting (e.g., MiB/GiB as appropriate)
305
- [ ] Output to file
306
- [ ] Deal with server-specific mailbox separator characters (e.g. '.' vs. '/')
307
- [ ] Man page
308
- [ ] More commands?
307
+ - [x] More human-friendly number formatting (e.g., MiB/GiB as appropriate)
308
+ - [ ] Output to file
309
+ - [ ] Deal with server-specific mailbox separator characters (e.g. '.' vs. '/')
310
+ - [ ] Man page
311
+ - [ ] More commands?
309
312
 
310
313
  ## Credits
311
314
 
data/imapcli.gemspec CHANGED
@@ -22,10 +22,10 @@ Gem::Specification.new do |s|
22
22
  s.bindir = 'exe'
23
23
  s.executables = ['imapcli']
24
24
 
25
+ s.add_dependency('activesupport', '~> 8.0')
25
26
  s.add_dependency('csv')
26
27
  s.add_dependency('descriptive_statistics', '~> 2.5')
27
28
  s.add_dependency('dotenv', '~> 3.1')
28
- s.add_dependency('activesupport', '~> 8.0')
29
29
  s.add_dependency('gli', '~> 2.22')
30
30
  s.add_dependency('net-imap')
31
31
  s.add_dependency('tty-progressbar', '~> 0.18')
data/lib/imapcli/cli.rb CHANGED
@@ -74,6 +74,10 @@ module Imapcli
74
74
  default: 'total_size'
75
75
  c.switch %i[reverse], desc: 'Reverse sort order (largest first)', negatable: false
76
76
  c.switch [:csv], desc: 'Output comma-separated values (CSV)'
77
+ c.flag %i[l limit],
78
+ desc: 'Limit the results to n folders (IMAP mailboxes)',
79
+ arg_name: 'max_lines',
80
+ default: 10
77
81
 
78
82
  c.action do |_global_options, options, args| # rubocop:disable Metrics/BlockLength
79
83
  raise unless @validator.stats_options_valid?(options, args)
@@ -68,7 +68,12 @@ module Imapcli
68
68
  yield current_count if block_given?
69
69
  end
70
70
  end
71
- output = sorted_list(list, options).map do |mailbox|
71
+
72
+ output = if options[:limit]
73
+ sorted_list(list, options).last(options[:limit].to_i)
74
+ else
75
+ sorted_list(list, options)
76
+ end.map do |mailbox|
72
77
  stats_to_table(mailbox.full_name, mailbox.stats)
73
78
  end
74
79
  output << stats_to_table('Total', total_stats) if list.length > 1
@@ -138,28 +143,30 @@ module Imapcli
138
143
  end
139
144
  end
140
145
 
141
- def sorted_list(list, options = {}) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
142
- sorted = case options[:sort]&.to_sym
143
- when :count
144
- list.sort_by { |mailbox| mailbox.stats.count }
145
- when :total_size
146
- list.sort_by { |mailbox| mailbox.stats.total_size }
147
- when :median_size
148
- list.sort_by { |mailbox| mailbox.stats.median_size }
149
- when :min_size
150
- list.sort_by { |mailbox| mailbox.stats.min_size }
151
- when :q1_size
152
- list.sort_by { |mailbox| mailbox.stats.q1 }
153
- when :q3_size
154
- list.sort_by { |mailbox| mailbox.stats.q3 }
155
- when :max_size
156
- list.sort_by { |mailbox| mailbox.stats.max_size }
157
- when nil
158
- list
159
- else
160
- raise "invalid sort option: #{options[:sort]}"
161
- end
162
- options[:reverse] ? sorted.reverse : sorted
146
+ def sorted_list(list, options)
147
+ return list if options.nil? || options[:sort].nil?
148
+ sort_property = options[:sort]&.to_sym
149
+ if %i[count total_size min_size q1 median_size q3 max_size].include? sort_property
150
+ sorted = sort_mailboxes(list, sort_property, options[:reverse])
151
+ else
152
+ raise "invalid sort option: #{options[:sort]}"
153
+ end
154
+ end
155
+
156
+ private
157
+
158
+ # Sorts a list of mailboxes according to a given property
159
+ # such as total count of e-mails, total size of all e-mails etc.
160
+ # Some mailboxes may not have this property, e.g., an empty
161
+ # mailbox will have `nil` values for median_size etc.
162
+ # We always sort mailboxes with `nil` values to the bottom.
163
+ def sort_mailboxes(mailboxes, property, reverse)
164
+ # Devide ary into two parts, one part where the property has a value
165
+ # and the other where the property is nil.
166
+ partitions = mailboxes.partition { |mailbox| mailbox.stats.send(property) }
167
+ sorted = partitions[0].sort_by { |mailbox| mailbox.stats.send(property) }
168
+ sorted = sorted.reverse if reverse
169
+ sorted + partitions[1]
163
170
  end
164
171
 
165
172
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Imapcli
4
- VERSION = '2.0.0'
4
+ VERSION = '2.1.0'
5
5
  end
@@ -22,9 +22,11 @@ RSpec.describe Imapcli::Command do
22
22
  Net::IMAP::MailboxList.new(nil, '/', 'Inbox'),
23
23
  Net::IMAP::MailboxList.new(nil, '/', 'Inbox/Foo'),
24
24
  Net::IMAP::MailboxList.new(nil, '/', 'Inbox/Foo/Sub'),
25
+ Net::IMAP::MailboxList.new(nil, '/', 'Inbox/Many/Messages'),
25
26
  Net::IMAP::MailboxList.new(nil, '/', 'Inbox/Bar'),
26
- Net::IMAP::MailboxList.new(nil, '/', 'Inbox/Bar/Sub'),
27
- Net::IMAP::MailboxList.new(nil, '/', 'Inbox/Bar/Sub/Subsub'),
27
+ Net::IMAP::MailboxList.new(nil, '/', 'Inbox/Largest/Messages'),
28
+ Net::IMAP::MailboxList.new(nil, '/', 'Inbox/Smallest/Messages'),
29
+ Net::IMAP::MailboxList.new(nil, '/', 'Inbox/Empty'),
28
30
  ])
29
31
  end
30
32
 
@@ -57,17 +59,19 @@ RSpec.describe Imapcli::Command do
57
59
  allow(client).to receive(:message_sizes) do |mailbox|
58
60
  case mailbox
59
61
  when 'Inbox'
60
- (1..4).map { |i| 1024 * i }
62
+ [200, 300, 400, 500]
61
63
  when 'Inbox/Foo'
62
- (3..10).map { |i| 1024 * i }
63
- when 'Inbox/Foo/Sub'
64
- Array.new(10, 1024)
64
+ [355, 360, 380, 200]
65
+ when 'Inbox/Many/Messages'
66
+ Array.new(10, 100)
65
67
  when 'Inbox/Bar'
66
- (1..2).map { |i| 1024 * i }
67
- when 'Inbox/Bar/Sub'
68
- [1, 1024 * 20]
69
- when 'Inbox/Bar/Sub/Subsub' # rubocop:disable Lint/DuplicateBranch
70
- (1..4).map { |i| 1024 * i }
68
+ [2_000, 3_000, 4_000]
69
+ when 'Inbox/Largest/Messages'
70
+ [2_500_000, 100_000, 200_000]
71
+ when 'Inbox/Smallest/Messages'
72
+ [10, 20, 30]
73
+ when 'Inbox/Empty'
74
+ []
71
75
  else
72
76
  [1024, 2048, 4096, 8192]
73
77
  end
@@ -77,8 +81,8 @@ RSpec.describe Imapcli::Command do
77
81
  it 'for all folders' do
78
82
  output = command.stats
79
83
  expect(output).to be_a Array
80
- expect(output.length).to eq 7
81
- expect(output[0][0]).to eq 'Inbox'
84
+ expect(output.length).to eq 9
85
+ expect(output[0][0]).to eq 'Inbox' # sorted before 'Inbox'
82
86
  expect(output[0][1]).to eq 4
83
87
  end
84
88
 
@@ -87,7 +91,15 @@ RSpec.describe Imapcli::Command do
87
91
  expect(output).to be_a Array
88
92
  expect(output.length).to eq 1
89
93
  expect(output[0][0]).to eq 'Inbox/Foo'
90
- expect(output[0][1]).to eq 8 # depends on message_sizes stub (see above)
94
+ expect(output[0][1]).to eq 4 # depends on message_sizes stub (see above)
95
+ end
96
+
97
+ it 'for an empty folder' do
98
+ output = command.stats('Inbox/Empty')
99
+ expect(output).to be_a Array
100
+ expect(output.length).to eq 1
101
+ expect(output[0][0]).to eq 'Inbox/Empty'
102
+ expect(output[0][1]).to eq 0
91
103
  end
92
104
 
93
105
  it 'for a given folder and subfolders' do
@@ -100,26 +112,31 @@ RSpec.describe Imapcli::Command do
100
112
  it 'sorts by number of messages' do
101
113
  output = command.stats('Inbox', depth: -1, sort: :count, reverse: true)
102
114
  expect(output).to be_a Array
103
- expect(output[0][0]).to eq 'Inbox/Foo/Sub'
115
+ expect(output[0][0]).to eq 'Inbox/Many/Messages'
104
116
  end
105
117
 
106
118
  it 'sorts by total message size' do
107
- output = command.stats('Inbox', depth: -1, sort: :total_size, reverse: true)
119
+ output = command.stats('Inbox', depth: -1, sort: :total_size, reverse: false)
108
120
  expect(output).to be_a Array
109
- expect(output[0][0]).to eq 'Inbox/Foo'
121
+ expect(output[-2][0]).to eq 'Inbox/Largest/Messages'
110
122
  end
111
123
 
112
- it 'sorts by largest message' do
124
+ it 'reverse-sorts largest message first' do
113
125
  output = command.stats('Inbox', depth: -1, sort: :max_size, reverse: true)
114
126
  expect(output).to be_a Array
115
- expect(output[0][0]).to eq 'Inbox/Bar/Sub'
127
+ expect(output[0][0]).to eq 'Inbox/Largest/Messages'
128
+ expect(output[-2][0]).to eq 'Inbox/Empty'
116
129
  end
117
130
 
118
- it 'sorts by smallest message' do
119
- output = command.stats('Inbox', depth: -1, sort: :min_size, reverse: true)
131
+ it 'sorts smallest message first' do
132
+ output = command.stats('Inbox', depth: -1, sort: :min_size, reverse: false)
120
133
  expect(output).to be_a Array
121
- # binding.pry
122
- expect(output[0][0]).to eq 'Inbox/Foo'
134
+ expect(output[0][0]).to eq 'Inbox/Smallest/Messages'
135
+ end
136
+
137
+ it 'returns only the first n mailboxes' do
138
+ output = command.stats('Inbox', depth: -1, limit: 2)
139
+ expect(output.length).to eq 3 # including summary line
123
140
  end
124
141
  end
125
142
 
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: imapcli
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Kraus (bovender)
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-03-10 00:00:00.000000000 Z
11
+ date: 2025-03-16 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '8.0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '8.0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: csv
15
29
  requirement: !ruby/object:Gem::Requirement
@@ -52,20 +66,6 @@ dependencies:
52
66
  - - "~>"
53
67
  - !ruby/object:Gem::Version
54
68
  version: '3.1'
55
- - !ruby/object:Gem::Dependency
56
- name: activesupport
57
- requirement: !ruby/object:Gem::Requirement
58
- requirements:
59
- - - "~>"
60
- - !ruby/object:Gem::Version
61
- version: '8.0'
62
- type: :runtime
63
- prerelease: false
64
- version_requirements: !ruby/object:Gem::Requirement
65
- requirements:
66
- - - "~>"
67
- - !ruby/object:Gem::Version
68
- version: '8.0'
69
69
  - !ruby/object:Gem::Dependency
70
70
  name: gli
71
71
  requirement: !ruby/object:Gem::Requirement