imapcli 1.0.5 → 2.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.
- checksums.yaml +4 -4
- data/.devcontainer/devcontainer.json +22 -0
- data/.github/dependabot.yml +12 -0
- data/.github/workflows/ci.yml +57 -0
- data/.gitignore +5 -0
- data/.rubocop.yml +53 -0
- data/.vscode/launch.json +59 -0
- data/CHANGELOG.md +71 -0
- data/Dockerfile +8 -5
- data/Gemfile +13 -4
- data/Gemfile.lock +189 -67
- data/Guardfile +8 -6
- data/README.md +68 -101
- data/Rakefile +5 -7
- data/bin/bundle +109 -0
- data/bin/rspec +27 -0
- data/bin/rubocop +27 -0
- data/exe/imapcli +8 -0
- data/imapcli.gemspec +26 -19
- data/lib/imapcli/cli.rb +180 -0
- data/lib/imapcli/client.rb +32 -32
- data/lib/imapcli/command.rb +44 -44
- data/lib/imapcli/mailbox.rb +34 -38
- data/lib/imapcli/option_validator.rb +6 -6
- data/lib/imapcli/stats.rb +9 -11
- data/lib/imapcli/version.rb +3 -1
- data/lib/imapcli.rb +20 -6
- data/spec/lib/imapcli/client_spec.rb +34 -22
- data/spec/lib/imapcli/command_spec.rb +35 -31
- data/spec/lib/imapcli/mailbox_spec.rb +36 -23
- data/spec/lib/imapcli/stats_spec.rb +18 -16
- data/spec/spec_helper.rb +22 -70
- metadata +64 -44
- data/.rspec +0 -1
- data/NEWS +0 -12
- data/bin/imapcli +0 -164
- data/spec/lib/imapcli/option_validator_spec.rb +0 -4
data/lib/imapcli/cli.rb
ADDED
@@ -0,0 +1,180 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Imapcli
|
4
|
+
class Cli # rubocop:disable Metrics/ClassLength,Style/Documentation
|
5
|
+
extend GLI::App
|
6
|
+
|
7
|
+
program_desc 'Command-line interface for IMAP servers'
|
8
|
+
|
9
|
+
version Imapcli::VERSION
|
10
|
+
|
11
|
+
subcommand_option_handling :normal
|
12
|
+
arguments :strict
|
13
|
+
sort_help :manually
|
14
|
+
wrap_help_text :tty_only
|
15
|
+
|
16
|
+
desc 'Domain name (FQDN) of the IMAP server'
|
17
|
+
default_value ENV.fetch('IMAP_SERVER', nil)
|
18
|
+
arg_name 'imap.example.com'
|
19
|
+
flag %i[s server]
|
20
|
+
|
21
|
+
desc 'Log-in name (username/email)'
|
22
|
+
default_value ENV.fetch('IMAP_USER', nil)
|
23
|
+
arg_name 'user'
|
24
|
+
flag %i[u user]
|
25
|
+
|
26
|
+
desc 'Log-in password'
|
27
|
+
# default_value ENV['IMAP_PASS']
|
28
|
+
arg_name 'password'
|
29
|
+
flag %i[p password]
|
30
|
+
|
31
|
+
desc 'Prompt for password'
|
32
|
+
switch %i[P prompt], negatable: false
|
33
|
+
|
34
|
+
desc 'Verbose output (e.g., response values from Rubys Net::IMAP)'
|
35
|
+
switch %i[v verbose], negatable: false
|
36
|
+
|
37
|
+
desc 'Tests if the server is available and log-in succeeds with the credentials'
|
38
|
+
command :check do |c|
|
39
|
+
c.action do |_global_options, _options, _args|
|
40
|
+
@command.check ? @prompt.ok('login successful') : @prompt.error('login failed')
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
desc 'Prints information about the server'
|
45
|
+
command :info do |c|
|
46
|
+
c.action do |_global_options, _options, _args|
|
47
|
+
@command.info.each { |line| @prompt.say line }
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
desc 'Lists mailboxes (folders)'
|
52
|
+
command :list do |c|
|
53
|
+
c.action do |_global_options, _options, _args|
|
54
|
+
@command.list.each { |line| @prompt.say line }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
desc 'Collects mailbox statistics'
|
59
|
+
arg_name :mailbox, optional: true, multiple: true
|
60
|
+
command :stats do |c| # rubocop:disable Metrics/BlockLength
|
61
|
+
c.switch %i[r recurse],
|
62
|
+
desc: 'Recurse into sub mailboxes',
|
63
|
+
negatable: false
|
64
|
+
c.switch %i[R no_recurse],
|
65
|
+
desc: 'Do not recurse into sub mailboxes',
|
66
|
+
negatable: false
|
67
|
+
c.switch %i[H human],
|
68
|
+
desc: 'Convert byte counts to human-friendly formats',
|
69
|
+
negatable: false
|
70
|
+
c.flag %i[o sort],
|
71
|
+
desc: 'Ordered (sorted) results',
|
72
|
+
arg_name: 'sort_property',
|
73
|
+
must_match: %w[count total_size median_size min_size q1_size q3_size max_size],
|
74
|
+
default: 'total_size'
|
75
|
+
c.switch %i[reverse], desc: 'Reverse sort order (largest first)', negatable: false
|
76
|
+
c.switch [:csv], desc: 'Output comma-separated values (CSV)'
|
77
|
+
|
78
|
+
c.action do |_global_options, options, args| # rubocop:disable Metrics/BlockLength
|
79
|
+
raise unless @validator.stats_options_valid?(options, args)
|
80
|
+
|
81
|
+
progress_bar = nil
|
82
|
+
|
83
|
+
head = ['Mailbox', 'Count', 'Total size', 'Min', 'Q1', 'Median', 'Q3', 'Max']
|
84
|
+
body = @command.stats(args, options) do |n|
|
85
|
+
if progress_bar
|
86
|
+
progress_bar.advance
|
87
|
+
else
|
88
|
+
@prompt.say "info: collecting stats for #{n} folders" if n > 1
|
89
|
+
progress_bar = TTY::ProgressBar.new(
|
90
|
+
'collecting stats... :current/:total (:percent, :eta remaining)',
|
91
|
+
total: n, clear: true
|
92
|
+
)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
formatted_body = body.map do |row|
|
96
|
+
row[0..1] + row[2..].map { |cell| format_bytes(cell, options[:human]) }
|
97
|
+
end
|
98
|
+
|
99
|
+
if options[:csv]
|
100
|
+
unless options[:human]
|
101
|
+
@prompt.warn 'notice: BREAKING CHANGE IN VERSION 2: messages sizes in CSV output are now given in bytes, not kiB'
|
102
|
+
end
|
103
|
+
@prompt.say head.to_csv
|
104
|
+
last_mailbox_line = body.length == 1 ? -1 : -2 # skip grand total if present
|
105
|
+
formatted_body[0..last_mailbox_line].each { |row| @prompt.say row.to_csv }
|
106
|
+
else
|
107
|
+
formatted_body = formatted_body.insert(0, :separator).insert(-2, :separator)
|
108
|
+
|
109
|
+
if options[:human]
|
110
|
+
@prompt.say "notice: -H/--human flag present, message sizes are given with SI prefixes"
|
111
|
+
else
|
112
|
+
@prompt.say "notice: message sizes are given in bytes"
|
113
|
+
end
|
114
|
+
|
115
|
+
table = TTY::Table.new(head, formatted_body)
|
116
|
+
rendered_table = table.render(:unicode) do |renderer|
|
117
|
+
renderer.alignments = [:left] + Array.new(7, :right)
|
118
|
+
renderer.border.style = :blue
|
119
|
+
end
|
120
|
+
@prompt.say rendered_table
|
121
|
+
|
122
|
+
# If any unknown mailboxes were requested, print an informative footer
|
123
|
+
if body.any? { |line| line[0].start_with? Imapcli::Command.unknown_mailbox_prefix }
|
124
|
+
@prompt.warn "#{Imapcli::Command.unknown_mailbox_prefix}unknown mailbox"
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def self.format_bytes(bytes, human = false)
|
131
|
+
human ? ActiveSupport::NumberHelper.number_to_human_size(bytes) : bytes
|
132
|
+
end
|
133
|
+
|
134
|
+
pre do |global, _command, _options, _args|
|
135
|
+
@prompt = TTY::Prompt.new
|
136
|
+
@validator = Imapcli::OptionValidator.new
|
137
|
+
raise unless @validator.global_options_valid?(global)
|
138
|
+
|
139
|
+
global[:p] = @prompt.mask 'Enter password:' if global[:P]
|
140
|
+
global[:p] ||= ENV.fetch('IMAP_PASS', nil)
|
141
|
+
|
142
|
+
client = Imapcli::Client.new(global[:s], global[:u], global[:p])
|
143
|
+
@prompt.say "server: #{global[:s]}"
|
144
|
+
@prompt.say "user: #{global[:u]}"
|
145
|
+
raise 'invalid server name' unless client.server_valid?
|
146
|
+
raise 'invalid user name' unless client.user_valid?
|
147
|
+
|
148
|
+
@prompt.warn 'warning: no password was provided (missing -p/-P option)' unless global[:p]
|
149
|
+
raise 'unable to connect to server' unless client.connection
|
150
|
+
|
151
|
+
@command = Imapcli::Command.new(client)
|
152
|
+
|
153
|
+
true
|
154
|
+
end
|
155
|
+
|
156
|
+
post do |global, _command, _options, _args|
|
157
|
+
@client&.logout
|
158
|
+
if global[:v]
|
159
|
+
@prompt.say "\n>>> --verbose switch on, listing server responses <<<"
|
160
|
+
@client.responses.each do |response|
|
161
|
+
@prompt.say response
|
162
|
+
end
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
on_error do |exception|
|
167
|
+
@client&.logout
|
168
|
+
if @validator&.errors&.any?
|
169
|
+
@validator.errors.each { |error| @prompt.error error }
|
170
|
+
else
|
171
|
+
@prompt&.error "error: #{exception}"
|
172
|
+
end
|
173
|
+
@prompt.nil? # if we do not have a prompt yet, let GLI handle the exception
|
174
|
+
end
|
175
|
+
|
176
|
+
def print_warnings
|
177
|
+
@validator.warnings.each { |warning| @prompt.warn warning }
|
178
|
+
end
|
179
|
+
end
|
180
|
+
end
|
data/lib/imapcli/client.rb
CHANGED
@@ -1,10 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Imapcli
|
2
4
|
# Wrapper for Net::IMAP
|
3
|
-
class Client
|
4
|
-
require 'net/imap'
|
5
|
-
require 'filesize'
|
6
|
-
require 'descriptive_statistics'
|
7
|
-
|
5
|
+
class Client # rubocop:disable Metrics/ClassLength
|
8
6
|
attr_accessor :port, :user, :pass
|
9
7
|
attr_reader :responses
|
10
8
|
|
@@ -16,14 +14,14 @@ module Imapcli
|
|
16
14
|
## +pass+ is the password to log into the server.
|
17
15
|
def initialize(server_with_optional_port, user, pass)
|
18
16
|
@port = 993 # default
|
19
|
-
self.server
|
17
|
+
self.server = server_with_optional_port
|
18
|
+
@user = user
|
19
|
+
@pass = pass
|
20
20
|
clear_log
|
21
21
|
end
|
22
22
|
|
23
23
|
# Attribute reader for the server domain name
|
24
|
-
|
25
|
-
@server
|
26
|
-
end
|
24
|
+
attr_reader :server
|
27
25
|
|
28
26
|
# Attribute writer for the server domain name; a port may be appended with
|
29
27
|
# a colon.
|
@@ -44,13 +42,13 @@ module Imapcli
|
|
44
42
|
# Note that a propery regex for an FQDN is hard to achieve.
|
45
43
|
# See https://stackoverflow.com/a/106223/270712 and elsewhere.
|
46
44
|
def server_valid?
|
47
|
-
@server.match? '^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$'
|
45
|
+
@server.match? '^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])$' # rubocop:disable Layout/LineLength
|
48
46
|
end
|
49
47
|
|
50
48
|
# Perform *very* basic sanity check on user name
|
51
49
|
#
|
52
50
|
def user_valid?
|
53
|
-
@user&.length
|
51
|
+
@user&.length&.> 0
|
54
52
|
end
|
55
53
|
|
56
54
|
# Returns true if both server and user name are valid.
|
@@ -81,17 +79,18 @@ module Imapcli
|
|
81
79
|
# credentials).
|
82
80
|
def login
|
83
81
|
raise('no connection to a server') unless connection
|
82
|
+
|
84
83
|
begin
|
85
84
|
response_ok? connection.login(@user, @pass)
|
86
|
-
rescue Net::IMAP::NoResponseError =>
|
87
|
-
log_error
|
85
|
+
rescue Net::IMAP::NoResponseError => e
|
86
|
+
log_error e
|
88
87
|
end
|
89
88
|
end
|
90
89
|
|
91
90
|
# Logs out of the server.
|
92
91
|
def logout
|
93
92
|
# access instance variable to avoid creating a new connection
|
94
|
-
@connection
|
93
|
+
@connection&.logout
|
95
94
|
end
|
96
95
|
|
97
96
|
# Returns the server's greeting (which may reveal the server software name
|
@@ -118,12 +117,12 @@ module Imapcli
|
|
118
117
|
# If the server +supports_quota+, returns an array containing the current
|
119
118
|
# usage (in kiB), the total quota (in kiB), and the percent usage.
|
120
119
|
def quota
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
120
|
+
return unless supports_quota
|
121
|
+
|
122
|
+
@quota ||= begin
|
123
|
+
info = query_server { @connection.getquotaroot('INBOX')[1] }
|
124
|
+
percent = info.quota.to_i.positive? ? info.usage.to_i.fdiv(info.quota.to_i) * 100 : nil
|
125
|
+
[info.usage, info.quota, percent]
|
127
126
|
end
|
128
127
|
end
|
129
128
|
|
@@ -146,17 +145,17 @@ module Imapcli
|
|
146
145
|
# * :q3: Third quartile of messages sizes.
|
147
146
|
# * :max: Size of largest message.
|
148
147
|
def message_sizes(mailbox)
|
149
|
-
# Could use the EXAMINE command to get the number of messages in a mailbox,
|
150
|
-
# but we need to retrieve an array of message indexes anyway (to compute
|
151
|
-
# the total mailbox size), so we can save one roundtrip to the server.
|
152
|
-
# query_server { connection.examine(mailbox) }
|
153
|
-
# total = connection.responses['EXISTS'][0]
|
154
|
-
# unseen = query_server { connection.search('UNSEEN') }.length
|
155
148
|
messages = messages(mailbox)
|
156
|
-
if messages.
|
157
|
-
query_server { connection.fetch(messages, 'RFC822.SIZE').map { |f| f.attr['RFC822.SIZE'] } }
|
158
|
-
else
|
149
|
+
if messages.empty?
|
159
150
|
[]
|
151
|
+
else
|
152
|
+
query_server do
|
153
|
+
messages.each_slice(1000).map do |some_messages|
|
154
|
+
connection.fetch(some_messages, 'RFC822.SIZE').map do |f|
|
155
|
+
f.attr['RFC822.SIZE']
|
156
|
+
end
|
157
|
+
end.flatten
|
158
|
+
end
|
160
159
|
end
|
161
160
|
end
|
162
161
|
|
@@ -204,12 +203,13 @@ module Imapcli
|
|
204
203
|
# error if not. The code that queries the server must be contained in the
|
205
204
|
# +block+, and the +block+'s return value is returned by this function.
|
206
205
|
# The +connection+'s responses are logged.
|
207
|
-
def query_server
|
206
|
+
def query_server
|
208
207
|
raise('no connection to a server') unless connection
|
208
|
+
|
209
209
|
result = yield
|
210
210
|
@log << connection.responses
|
211
211
|
result
|
212
212
|
end
|
213
213
|
|
214
|
-
end
|
215
|
-
end
|
214
|
+
end
|
215
|
+
end
|
data/lib/imapcli/command.rb
CHANGED
@@ -1,10 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Imapcli
|
2
4
|
# Provides entry points for Imapcli.
|
3
5
|
#
|
4
6
|
# Most of the methods in this class return
|
5
|
-
class Command
|
7
|
+
class Command # rubocop:disable Metrics/ClassLength
|
6
8
|
def initialize(client)
|
7
|
-
raise ArgumentError, 'Imapcli::Client is required' unless client
|
9
|
+
raise ArgumentError, 'Imapcli::Client is required' unless client.is_a?(Imapcli::Client)
|
10
|
+
|
8
11
|
@client = client
|
9
12
|
end
|
10
13
|
|
@@ -16,18 +19,18 @@ module Imapcli
|
|
16
19
|
end
|
17
20
|
|
18
21
|
# Collects basic information about the server.
|
19
|
-
def info
|
22
|
+
def info # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
|
20
23
|
perform do
|
21
24
|
output = []
|
22
25
|
output << "greeting: #{@client.greeting}"
|
23
26
|
output << "capability: #{@client.capability.join(' ')}"
|
24
27
|
output << "hierarchy separator: #{@client.separator}"
|
25
28
|
if @client.supports_quota
|
26
|
-
usage =
|
27
|
-
available =
|
29
|
+
usage = ActiveSupport::NumberHelper.number_to_human_size(@client.quota[0])
|
30
|
+
available = ActiveSupport::NumberHelper.number_to_human_size(@client.quota[1])
|
28
31
|
output << "quota: #{usage} used, #{available} available (#{@client.quota[2].round(1)}%)"
|
29
32
|
else
|
30
|
-
output <<
|
33
|
+
output << 'quota: IMAP QUOTA extension not supported by this server'
|
31
34
|
end
|
32
35
|
end
|
33
36
|
end
|
@@ -43,16 +46,16 @@ module Imapcli
|
|
43
46
|
#
|
44
47
|
# If a block is given, it is called with the current mailbox count and the
|
45
48
|
# total mailbox count so that current progress can be computed.
|
46
|
-
def stats(mailbox_names = [], options = {})
|
49
|
+
def stats(mailbox_names = [], options = {}) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
47
50
|
mailbox_names = [mailbox_names] unless mailbox_names.is_a? Array
|
48
51
|
perform do
|
49
|
-
output = []
|
50
52
|
# Map the command line arguments to Imapcli::Mailbox objects
|
51
53
|
mailboxes = find_mailboxes(mailbox_names)
|
52
54
|
list = mailboxes.inject([]) do |ary, mailbox|
|
53
55
|
ary + mailbox.to_list(determine_max_level(mailbox, options))
|
54
56
|
end
|
55
|
-
raise 'mailbox not found' unless list.count
|
57
|
+
raise 'mailbox not found' unless list.count.positive?
|
58
|
+
|
56
59
|
current_count = 0
|
57
60
|
yield list.length if block_given?
|
58
61
|
total_stats = Stats.new
|
@@ -65,10 +68,9 @@ module Imapcli
|
|
65
68
|
yield current_count if block_given?
|
66
69
|
end
|
67
70
|
end
|
68
|
-
sorted_list(list, options).
|
69
|
-
|
71
|
+
output = sorted_list(list, options).map do |mailbox|
|
72
|
+
stats_to_table(mailbox.full_name, mailbox.stats)
|
70
73
|
end
|
71
|
-
# output << Array.new(8, '======')
|
72
74
|
output << stats_to_table('Total', total_stats) if list.length > 1
|
73
75
|
output
|
74
76
|
end
|
@@ -81,15 +83,13 @@ module Imapcli
|
|
81
83
|
private
|
82
84
|
|
83
85
|
def perform
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
raise 'unable to log into server'
|
88
|
-
end
|
86
|
+
raise 'unable to log into server' unless @client.login
|
87
|
+
|
88
|
+
yield
|
89
89
|
end
|
90
90
|
|
91
91
|
def traverse_mailbox_tree(mailbox, depth = 0)
|
92
|
-
this = mailbox.
|
92
|
+
this = mailbox.imap_mailbox? ? ["#{' ' * [depth - 1, 0].max}- #{mailbox.name}"] : []
|
93
93
|
mailbox.children.inject(this) do |ary, child|
|
94
94
|
ary + traverse_mailbox_tree(child, depth + 1)
|
95
95
|
end
|
@@ -104,15 +104,15 @@ module Imapcli
|
|
104
104
|
stats.quartile_1_size,
|
105
105
|
stats.median_size,
|
106
106
|
stats.quartile_3_size,
|
107
|
-
stats.max_size
|
107
|
+
stats.max_size,
|
108
108
|
]
|
109
109
|
end
|
110
110
|
|
111
111
|
# Finds and returns mailboxes based on mailbox names.
|
112
112
|
def find_mailboxes(names)
|
113
|
-
if names
|
113
|
+
if names&.length&.positive?
|
114
114
|
Imapcli::Mailbox.consolidate(
|
115
|
-
names.
|
115
|
+
names.filter_map { |name| @client.find_mailbox(name) }
|
116
116
|
)
|
117
117
|
else
|
118
118
|
[@client.mailbox_root]
|
@@ -130,7 +130,7 @@ module Imapcli
|
|
130
130
|
# Options:
|
131
131
|
# * +:depth+ maximum depth of recursion
|
132
132
|
def determine_max_level(mailbox, options = {})
|
133
|
-
if mailbox.
|
133
|
+
if mailbox.root?
|
134
134
|
options[:depth]
|
135
135
|
else
|
136
136
|
depth = options[:depth] || 0
|
@@ -138,28 +138,28 @@ module Imapcli
|
|
138
138
|
end
|
139
139
|
end
|
140
140
|
|
141
|
-
def sorted_list(list, options = {})
|
142
|
-
sorted = case options[:sort]
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
options[:
|
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
|
163
163
|
end
|
164
164
|
|
165
165
|
end
|
data/lib/imapcli/mailbox.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Imapcli
|
2
4
|
# In IMAP speak, a mailbox is what one would commonly call a 'folder'
|
3
|
-
class Mailbox
|
5
|
+
class Mailbox # rubocop:disable Metrics/ClassLength
|
4
6
|
attr_accessor :options
|
5
|
-
attr_reader :level, :
|
7
|
+
attr_reader :level, :imap_mailbox_list, :name, :stats
|
6
8
|
|
7
9
|
# Creates a new root Mailbox object and optionally adds sub mailboxes from
|
8
10
|
# an array of Net::IMAP::MailboxList items.
|
@@ -19,11 +21,11 @@ module Imapcli
|
|
19
21
|
|
20
22
|
# Determines if this mailbox represents a dedicated IMAP mailbox with an
|
21
23
|
# associated Net::IMAP::MailboxList structure.
|
22
|
-
def
|
23
|
-
|
24
|
+
def imap_mailbox?
|
25
|
+
!imap_mailbox_list.nil?
|
24
26
|
end
|
25
27
|
|
26
|
-
def
|
28
|
+
def root?
|
27
29
|
name.respond_to?(:empty?) ? !!name.empty? : !name
|
28
30
|
end
|
29
31
|
|
@@ -32,17 +34,17 @@ module Imapcli
|
|
32
34
|
# The result includes the current mailbox.
|
33
35
|
def count(max_level = nil)
|
34
36
|
sum = 1
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
37
|
+
return unless max_level.nil? || level < max_level
|
38
|
+
|
39
|
+
@children.values.inject(sum) do |count, child|
|
40
|
+
count + child.count(max_level)
|
39
41
|
end
|
40
42
|
end
|
41
43
|
|
42
44
|
# Determines the maximum level in the mailbox tree
|
43
|
-
def
|
44
|
-
if
|
45
|
-
@children.values.map
|
45
|
+
def max_level
|
46
|
+
if children?
|
47
|
+
@children.values.map(&:max_level).max
|
46
48
|
else
|
47
49
|
level
|
48
50
|
end
|
@@ -52,8 +54,8 @@ module Imapcli
|
|
52
54
|
imap_mailbox_list&.name
|
53
55
|
end
|
54
56
|
|
55
|
-
def
|
56
|
-
|
57
|
+
def children?
|
58
|
+
!@children.empty?
|
57
59
|
end
|
58
60
|
|
59
61
|
def children
|
@@ -67,7 +69,8 @@ module Imapcli
|
|
67
69
|
|
68
70
|
# Adds a sub mailbox designated by the +name+ of a Net::IMAP::MailboxList.
|
69
71
|
def add_mailbox(imap_mailbox_list)
|
70
|
-
return unless imap_mailbox_list&.name&.length
|
72
|
+
return unless imap_mailbox_list&.name&.length&.> 0
|
73
|
+
|
71
74
|
recursive_add(0, imap_mailbox_list, imap_mailbox_list.name)
|
72
75
|
end
|
73
76
|
|
@@ -82,13 +85,13 @@ module Imapcli
|
|
82
85
|
#
|
83
86
|
# Returns nil of none exists with the given name.
|
84
87
|
# Name must be relative to the current mailbox.
|
85
|
-
def find_sub_mailbox(relative_name, delimiter)
|
88
|
+
def find_sub_mailbox(relative_name, delimiter) # rubocop:disable Metrics/MethodLength
|
86
89
|
if relative_name
|
87
90
|
sub_mailbox_name, subs_subs = relative_name.split(delimiter, 2)
|
88
91
|
key = normalize_key(sub_mailbox_name, level)
|
89
|
-
if sub_mailbox = @children[key]
|
92
|
+
if (sub_mailbox = @children[key])
|
90
93
|
sub_mailbox.find_sub_mailbox(subs_subs, delimiter)
|
91
|
-
else
|
94
|
+
else # rubocop:disable Style/EmptyElse
|
92
95
|
nil # no matching sub mailbox found, stop searching the tree
|
93
96
|
end
|
94
97
|
else
|
@@ -103,16 +106,15 @@ module Imapcli
|
|
103
106
|
#
|
104
107
|
# If a block is given, it is called with the Imapcli::Stats object for this
|
105
108
|
# mailbox.
|
106
|
-
def collect_stats(client, max_level = nil)
|
109
|
+
def collect_stats(client, max_level = nil, &block)
|
107
110
|
return if @stats
|
108
|
-
|
109
|
-
|
110
|
-
end
|
111
|
+
|
112
|
+
@stats = Stats.new(client.message_sizes(full_name)) if full_name # proceed only if this is a mailbox of its own
|
111
113
|
yield @stats if block_given?
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
114
|
+
return unless max_level.nil? || level < max_level
|
115
|
+
|
116
|
+
@children.each_value do |child|
|
117
|
+
child.collect_stats(client, max_level, &block)
|
116
118
|
end
|
117
119
|
end
|
118
120
|
|
@@ -121,11 +123,11 @@ module Imapcli
|
|
121
123
|
# Mailbox objects that do not represent IMAP mailboxes (such as the root
|
122
124
|
# mailbox) are not included.
|
123
125
|
def to_list(max_level = nil)
|
124
|
-
me =
|
126
|
+
me = imap_mailbox? ? [self] : []
|
125
127
|
if max_level.nil? || level < max_level
|
126
128
|
@children.values.inject(me) do |ary, child|
|
127
129
|
ary + child.to_list(max_level)
|
128
|
-
end.sort_by
|
130
|
+
end.sort_by(&:full_name)
|
129
131
|
else
|
130
132
|
me
|
131
133
|
end
|
@@ -143,21 +145,15 @@ module Imapcli
|
|
143
145
|
|
144
146
|
protected
|
145
147
|
|
146
|
-
|
147
|
-
@level = level
|
148
|
-
end
|
149
|
-
|
150
|
-
def name=(name)
|
151
|
-
@name = name
|
152
|
-
end
|
148
|
+
attr_writer :level, :name
|
153
149
|
|
154
|
-
def recursive_add(level, imap_mailbox_list, relative_name = nil)
|
150
|
+
def recursive_add(level, imap_mailbox_list, relative_name = nil) # rubocop:disable Metrics/MethodLength
|
155
151
|
delimiter = options[:delimiter] || imap_mailbox_list.delim
|
156
152
|
if relative_name
|
157
153
|
sub_mailbox_name, subs_subs = relative_name.split(delimiter, 2)
|
158
154
|
key = normalize_key(sub_mailbox_name, level)
|
159
155
|
# Create a new mailbox if there does not exist one by the name
|
160
|
-
unless sub_mailbox = @children[key]
|
156
|
+
unless (sub_mailbox = @children[key])
|
161
157
|
sub_mailbox = Mailbox.new
|
162
158
|
sub_mailbox.level = level
|
163
159
|
sub_mailbox.name = sub_mailbox_name
|
@@ -172,7 +168,7 @@ module Imapcli
|
|
172
168
|
|
173
169
|
# Normalizes a mailbox name for use as the key in the children hash.
|
174
170
|
def normalize_key(key, level)
|
175
|
-
if options[:case_insensitive] || (level
|
171
|
+
if options[:case_insensitive] || (level.zero? && key.upcase == 'INBOX')
|
176
172
|
key.upcase
|
177
173
|
else
|
178
174
|
key
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module Imapcli
|
2
4
|
# Utility class to validate user options
|
3
5
|
#
|
@@ -17,9 +19,7 @@ module Imapcli
|
|
17
19
|
if global_options[:u].nil? || global_options[:u].empty?
|
18
20
|
@errors << 'missing user name (use -u option or set IMAP_USER environment variable)'
|
19
21
|
end
|
20
|
-
if global_options[:P] && global_options[:p]
|
21
|
-
@errors << '-p and -P options do not agree'
|
22
|
-
end
|
22
|
+
@errors << '-p and -P options do not agree' if global_options[:P] && global_options[:p]
|
23
23
|
|
24
24
|
pass?
|
25
25
|
end
|
@@ -27,7 +27,7 @@ module Imapcli
|
|
27
27
|
# Validates options for the stats command.
|
28
28
|
#
|
29
29
|
# @return [true false] indicating success or failure; warnings can be accessed as attribute
|
30
|
-
def stats_options_valid?(options, args)
|
30
|
+
def stats_options_valid?(options, args) # rubocop:disable Metrics/MethodLength,Metrics/AbcSize,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
|
31
31
|
@options = {}
|
32
32
|
raise 'incompatible options -r/--recurse and -R/--no_recurse' if options[:r] && options[:R]
|
33
33
|
|
@@ -46,7 +46,7 @@ module Imapcli
|
|
46
46
|
end
|
47
47
|
|
48
48
|
if options[:sort]
|
49
|
-
available_sort_options = %w
|
49
|
+
available_sort_options = %w[count total_size min_size q1 median_size q3 max_size]
|
50
50
|
if available_sort_options.include? options[:sort].downcase
|
51
51
|
@options[:sort] = options[:sort].to_sym
|
52
52
|
else
|
@@ -63,7 +63,7 @@ module Imapcli
|
|
63
63
|
end
|
64
64
|
|
65
65
|
def warnings?
|
66
|
-
@warnings.count
|
66
|
+
@warnings.count.positive?
|
67
67
|
end
|
68
68
|
|
69
69
|
end
|