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.
@@ -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
@@ -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, @user, @pass = server_with_optional_port, user, pass
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
- def server
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 > 0
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 => error
87
- log_error 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.logout if @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
- if supports_quota
122
- @quota ||= begin
123
- info = query_server { @connection.getquotaroot('INBOX')[1] }
124
- percent = info.quota.to_i > 0 ? info.usage.to_i.fdiv(info.quota.to_i) * 100 : nil
125
- [ info.usage, info.quota, percent ]
126
- end
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.length > 0
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(&block)
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 # class Client
215
- end # module Imapcli
214
+ end
215
+ end
@@ -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 && client.is_a?(Imapcli::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 = Filesize.from(@client.quota[0] + ' kB').pretty
27
- available = Filesize.from(@client.quota[1] + ' kB').pretty
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 << "quota: IMAP QUOTA extension not supported by this server"
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 > 0
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).each do |mailbox|
69
- output << stats_to_table(mailbox.full_name, mailbox.stats)
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
- if @client.login
85
- yield
86
- else
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.is_imap_mailbox? ? ["#{' ' * [depth - 1, 0].max}- #{mailbox.name}"] : []
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 && names.length > 0
113
+ if names&.length&.positive?
114
114
  Imapcli::Mailbox.consolidate(
115
- names.map { |name| @client.find_mailbox(name) }.compact
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.is_root?
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
- 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
152
- list.sort_by { |mailbox| mailbox.stats.q1 }
153
- when :q3
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[:sort_order] == :desc ? sorted.reverse : sorted
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
@@ -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, :children, :imap_mailbox_list, :name, :stats
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 is_imap_mailbox?
23
- not imap_mailbox_list.nil?
24
+ def imap_mailbox?
25
+ !imap_mailbox_list.nil?
24
26
  end
25
27
 
26
- def is_root?
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
- if max_level.nil? || level < max_level
36
- @children.values.inject(sum) do |count, child|
37
- count + child.count(max_level)
38
- end
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 get_max_level
44
- if has_children?
45
- @children.values.map { |child| child.get_max_level }.max
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 has_children?
56
- @children.length > 0
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 > 0
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
- if full_name # proceed only if this is a mailbox of its own
109
- @stats = Stats.new(client.message_sizes(full_name))
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
- if max_level.nil? || level < max_level
113
- @children.values.each do |child|
114
- child.collect_stats(client, max_level) { |child_stats| yield child_stats}
115
- end
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 = is_imap_mailbox? ? [self] : []
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 { |e| e.full_name }
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
- def level=(level)
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 == 0 && key.upcase == 'INBOX')
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(count total_size min_size q1 median_size q3 max_size)
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 > 0
66
+ @warnings.count.positive?
67
67
  end
68
68
 
69
69
  end