net-imap 0.5.9 → 0.6.4
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/.document +3 -0
- data/.rdoc_options +7 -0
- data/Gemfile +7 -5
- data/README.md +1 -1
- data/lib/net/imap/command_data.rb +170 -80
- data/lib/net/imap/config/attr_accessors.rb +8 -9
- data/lib/net/imap/config/attr_inheritance.rb +64 -1
- data/lib/net/imap/config/attr_type_coercion.rb +18 -6
- data/lib/net/imap/config/attr_version_defaults.rb +90 -0
- data/lib/net/imap/config.rb +244 -122
- data/lib/net/imap/connection_state.rb +1 -1
- data/lib/net/imap/data_encoding.rb +126 -27
- data/lib/net/imap/errors.rb +189 -0
- data/lib/net/imap/esearch_result.rb +48 -3
- data/lib/net/imap/flags.rb +1 -1
- data/lib/net/imap/response_data.rb +110 -14
- data/lib/net/imap/response_parser/parser_utils.rb +14 -23
- data/lib/net/imap/response_parser.rb +40 -17
- data/lib/net/imap/response_reader.rb +25 -16
- data/lib/net/imap/sasl/scram_authenticator.rb +74 -0
- data/lib/net/imap/search_result.rb +13 -4
- data/lib/net/imap/sequence_set.rb +715 -326
- data/lib/net/imap/uidplus_data.rb +2 -63
- data/lib/net/imap/vanished_data.rb +10 -1
- data/lib/net/imap.rb +201 -86
- data/net-imap.gemspec +1 -1
- data/rakelib/rdoc.rake +1 -18
- metadata +6 -4
- data/lib/net/imap/data_lite.rb +0 -226
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ffcec5e3cc42d7f72c63520110ec442f760c35472401de495001f420cfa059c5
|
|
4
|
+
data.tar.gz: aedd703997fc651cb6c67635a3d20a4159720c6e1bda41c07322f2bd67a627d6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1fa95302f29607d152b991535a9d14cda5dae1a1fce06b1d831dd2b09f69b2fd845367e0ebed73dd4e79d04c064a801e59865e76947060a4811304728458f3c7
|
|
7
|
+
data.tar.gz: 2dc9a56758d6f370affc9540a8ca26b8ad866ca5ef0e5b9bc2c30ff6257e3461e5ed23173e1c5e98a2e95c1e0f0a844c96f9e34d41151e9196ab513ac6987d33
|
data/.document
ADDED
data/.rdoc_options
ADDED
data/Gemfile
CHANGED
|
@@ -4,21 +4,23 @@ source "https://rubygems.org"
|
|
|
4
4
|
|
|
5
5
|
gemspec
|
|
6
6
|
|
|
7
|
-
gem "digest"
|
|
7
|
+
# gem "digest" # not included as a workaround for #576
|
|
8
8
|
gem "strscan"
|
|
9
9
|
gem "base64"
|
|
10
|
+
gem "psych", ">= 5.3.0" # 5.2.5 for Data serialization, 5.3.0 for TruffleRuby
|
|
10
11
|
|
|
11
12
|
gem "irb"
|
|
12
13
|
gem "rake"
|
|
13
|
-
gem "rdoc"
|
|
14
|
+
gem "rdoc", ">= 7.2.0"
|
|
14
15
|
gem "test-unit"
|
|
15
16
|
gem "test-unit-ruby-core", git: "https://github.com/ruby/test-unit-ruby-core"
|
|
16
17
|
|
|
17
18
|
gem "benchmark", require: false
|
|
18
19
|
gem "benchmark-driver", require: false
|
|
20
|
+
gem "vernier", require: false, platform: :mri
|
|
19
21
|
|
|
20
22
|
group :test do
|
|
21
|
-
gem "simplecov", require: false
|
|
22
|
-
gem "simplecov-html", require: false
|
|
23
|
-
gem "simplecov-json", require: false
|
|
23
|
+
gem "simplecov", require: false, platforms: %i[mri windows]
|
|
24
|
+
gem "simplecov-html", require: false, platforms: %i[mri windows]
|
|
25
|
+
gem "simplecov-json", require: false, platforms: %i[mri windows]
|
|
24
26
|
end
|
data/README.md
CHANGED
|
@@ -61,9 +61,9 @@ imap.search(["BEFORE", "30-Apr-2003", "SINCE", "1-Apr-2003"]).each do |message_i
|
|
|
61
61
|
else
|
|
62
62
|
imap.copy(message_id, "Mail/sent-apr03")
|
|
63
63
|
imap.store(message_id, "+FLAGS", [:Deleted])
|
|
64
|
+
imap.expunge
|
|
64
65
|
end
|
|
65
66
|
end
|
|
66
|
-
imap.expunge
|
|
67
67
|
```
|
|
68
68
|
|
|
69
69
|
## Development
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
require "date"
|
|
4
4
|
|
|
5
5
|
require_relative "errors"
|
|
6
|
-
|
|
6
|
+
|
|
7
|
+
# :enddoc:
|
|
7
8
|
|
|
8
9
|
module Net
|
|
9
10
|
class IMAP < Protocol
|
|
@@ -26,6 +27,7 @@ module Net
|
|
|
26
27
|
end
|
|
27
28
|
when Time, Date, DateTime
|
|
28
29
|
when Symbol
|
|
30
|
+
Flag.validate(data)
|
|
29
31
|
else
|
|
30
32
|
data.validate
|
|
31
33
|
end
|
|
@@ -46,7 +48,7 @@ module Net
|
|
|
46
48
|
when Date
|
|
47
49
|
send_date_data(data)
|
|
48
50
|
when Symbol
|
|
49
|
-
|
|
51
|
+
Flag[data].send_data(self, tag)
|
|
50
52
|
else
|
|
51
53
|
data.send_data(self, tag)
|
|
52
54
|
end
|
|
@@ -78,9 +80,23 @@ module Net
|
|
|
78
80
|
put_string('"' + str.gsub(/["\\]/, "\\\\\\&") + '"')
|
|
79
81
|
end
|
|
80
82
|
|
|
81
|
-
def
|
|
83
|
+
def send_binary_literal(*, **) = send_literal(*, **, binary: true)
|
|
84
|
+
|
|
85
|
+
# `non_sync` is an optional tri-state flag:
|
|
86
|
+
# * `true` -> Force non-synchronizing `LITERAL+`/`LITERAL-` behavior.
|
|
87
|
+
# TODO: raise or warn when capabilities don't allow non_sync.
|
|
88
|
+
# * `false` -> Force normal synchronizing literal behavior.
|
|
89
|
+
# * `nil` -> (default) Currently behaves like `false` (will be dynamic).
|
|
90
|
+
def send_literal(str, tag = nil, binary: false, non_sync: nil)
|
|
82
91
|
synchronize do
|
|
83
|
-
|
|
92
|
+
non_sync = non_sync_literal?(str.bytesize) if non_sync.nil?
|
|
93
|
+
prefix = "~" if binary
|
|
94
|
+
plus = "+" if non_sync
|
|
95
|
+
put_string("#{prefix}{#{str.bytesize}#{plus}}\r\n")
|
|
96
|
+
if non_sync
|
|
97
|
+
put_string(str)
|
|
98
|
+
return
|
|
99
|
+
end
|
|
84
100
|
@continued_command_tag = tag
|
|
85
101
|
@continuation_request_exception = nil
|
|
86
102
|
begin
|
|
@@ -95,6 +111,13 @@ module Net
|
|
|
95
111
|
end
|
|
96
112
|
end
|
|
97
113
|
|
|
114
|
+
def non_sync_literal?(bytesize)
|
|
115
|
+
capabilities_cached? &&
|
|
116
|
+
bytesize <= config.max_non_synchronizing_literal &&
|
|
117
|
+
(capable?("LITERAL+") ||
|
|
118
|
+
bytesize <= 4096 && (capable?("IMAP4rev2") || capable?("LITERAL-")))
|
|
119
|
+
end
|
|
120
|
+
|
|
98
121
|
def send_number_data(num)
|
|
99
122
|
put_string(num.to_s)
|
|
100
123
|
end
|
|
@@ -116,11 +139,13 @@ module Net
|
|
|
116
139
|
def send_date_data(date) put_string Net::IMAP.encode_date(date) end
|
|
117
140
|
def send_time_data(time) put_string Net::IMAP.encode_time(time) end
|
|
118
141
|
|
|
119
|
-
def send_symbol_data(symbol)
|
|
120
|
-
put_string("\\" + symbol.to_s)
|
|
121
|
-
end
|
|
122
|
-
|
|
123
142
|
CommandData = Data.define(:data) do # :nodoc:
|
|
143
|
+
def self.validate(...)
|
|
144
|
+
data = new(...)
|
|
145
|
+
data.validate
|
|
146
|
+
data
|
|
147
|
+
end
|
|
148
|
+
|
|
124
149
|
def send_data(imap, tag)
|
|
125
150
|
raise NoMethodError, "#{self.class} must implement #{__method__}"
|
|
126
151
|
end
|
|
@@ -129,15 +154,109 @@ module Net
|
|
|
129
154
|
end
|
|
130
155
|
end
|
|
131
156
|
|
|
157
|
+
# Represents IMAP +text+ data, which may contain any 7-bit ASCII character,
|
|
158
|
+
# except for +NULL+, +CR+, or +LF+. +text+ is extended to allow any
|
|
159
|
+
# multibyte +UTF-8+ character when either +UTF8=ACCEPT+ or +IMAP4rev2+ have
|
|
160
|
+
# been enabled, or when the server supports only +IMAP4rev2+ and not earlier
|
|
161
|
+
# IMAP revisions, or when the server advertises +UTF8=ONLY+.
|
|
162
|
+
#
|
|
163
|
+
# NOTE: The current implementation does not validate whether the connection
|
|
164
|
+
# currently supports UTF-8. Future versions may change.
|
|
165
|
+
#
|
|
166
|
+
# The string's bytes must be valid ASCII or valid UTF-8. The string's
|
|
167
|
+
# reported encoding is ignored, but the string is _not_ transcoded.
|
|
168
|
+
class RawText < CommandData # :nodoc:
|
|
169
|
+
def initialize(data:)
|
|
170
|
+
data = String(data.to_str)
|
|
171
|
+
data = if data.encoding in Encoding::ASCII | Encoding::UTF_8
|
|
172
|
+
-data
|
|
173
|
+
elsif data.ascii_only?
|
|
174
|
+
-(data.dup.force_encoding("ASCII"))
|
|
175
|
+
else
|
|
176
|
+
-(data.dup.force_encoding("UTF-8"))
|
|
177
|
+
end
|
|
178
|
+
super
|
|
179
|
+
validate
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def validate
|
|
183
|
+
if data.include?("\0")
|
|
184
|
+
raise DataFormatError, "NULL byte must be binary literal encoded"
|
|
185
|
+
elsif !data.valid_encoding?
|
|
186
|
+
raise DataFormatError, "invalid UTF-8 must be literal encoded"
|
|
187
|
+
elsif /[\r\n]/.match?(data)
|
|
188
|
+
raise DataFormatError, "CR and LF bytes must be literal encoded"
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def ascii_only? = data.ascii_only?
|
|
193
|
+
|
|
194
|
+
def send_data(imap, tag) = imap.__send__(:put_string, data)
|
|
195
|
+
end
|
|
196
|
+
|
|
132
197
|
class RawData < CommandData # :nodoc:
|
|
133
|
-
def
|
|
134
|
-
|
|
198
|
+
def initialize(data:)
|
|
199
|
+
data = split_parts(data)
|
|
200
|
+
super
|
|
201
|
+
validate
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def send_data(imap, tag) = data.each do _1.send_data(imap, tag) end
|
|
205
|
+
|
|
206
|
+
def validate
|
|
207
|
+
return unless data.last in RawText(data: text)
|
|
208
|
+
if text.rindex(/~?\{[1-9]\d*\+?\}\z/n)
|
|
209
|
+
raise DataFormatError, "RawData cannot end with literal continuation"
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
private
|
|
214
|
+
|
|
215
|
+
def split_parts(data)
|
|
216
|
+
data = data.b # dups and ensures BINARY encoding
|
|
217
|
+
parts = []
|
|
218
|
+
while data.match(/(~)?\{(0|[1-9]\d*)(\+)?\}\r\n/n)
|
|
219
|
+
text, binary, bytesize, non_sync, data = $`, !!$1, $2, !!$3, $'
|
|
220
|
+
bytesize = NumValidator.coerce_number64 bytesize
|
|
221
|
+
parts << RawText[text] unless text.empty?
|
|
222
|
+
parts << extract_literal(data, binary:, bytesize:, non_sync:)
|
|
223
|
+
data.bytesplice(0, bytesize, "")
|
|
224
|
+
end
|
|
225
|
+
parts << RawText[data] unless data.empty?
|
|
226
|
+
parts
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def extract_literal(data, binary:, bytesize:, non_sync:)
|
|
230
|
+
if data.bytesize < bytesize
|
|
231
|
+
raise DataFormatError, "Too few bytes in string for literal, " \
|
|
232
|
+
"expected: %s, remaining: %s" % [bytesize, data.bytesize]
|
|
233
|
+
end
|
|
234
|
+
literal = data.byteslice(0, bytesize)
|
|
235
|
+
(binary ? Literal8 : Literal).new(data: literal, non_sync:)
|
|
135
236
|
end
|
|
136
237
|
end
|
|
137
238
|
|
|
138
239
|
class Atom < CommandData # :nodoc:
|
|
240
|
+
def initialize(**)
|
|
241
|
+
super
|
|
242
|
+
validate
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
def validate
|
|
246
|
+
data.to_s.ascii_only? \
|
|
247
|
+
or raise DataFormatError, "#{self.class} must be ASCII only"
|
|
248
|
+
data.match?(ResponseParser::Patterns::ATOM_SPECIALS) \
|
|
249
|
+
and raise DataFormatError, "#{self.class} must not contain atom-specials"
|
|
250
|
+
end
|
|
251
|
+
|
|
139
252
|
def send_data(imap, tag)
|
|
140
|
-
imap.__send__(:put_string, data)
|
|
253
|
+
imap.__send__(:put_string, data.to_s)
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
class Flag < Atom # :nodoc:
|
|
258
|
+
def send_data(imap, tag)
|
|
259
|
+
imap.__send__(:put_string, "\\#{data}")
|
|
141
260
|
end
|
|
142
261
|
end
|
|
143
262
|
|
|
@@ -147,9 +266,39 @@ module Net
|
|
|
147
266
|
end
|
|
148
267
|
end
|
|
149
268
|
|
|
150
|
-
class Literal <
|
|
269
|
+
class Literal < Data.define(:data, :non_sync) # :nodoc:
|
|
270
|
+
def self.validate(...)
|
|
271
|
+
data = new(...)
|
|
272
|
+
data.validate
|
|
273
|
+
data
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
def initialize(data:, non_sync: nil)
|
|
277
|
+
data = -String(data.to_str).b or
|
|
278
|
+
raise DataFormatError, "#{self.class} expects string input"
|
|
279
|
+
super
|
|
280
|
+
validate
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
def bytesize = data.bytesize
|
|
284
|
+
|
|
285
|
+
def validate
|
|
286
|
+
if data.include?("\0")
|
|
287
|
+
raise DataFormatError, "NULL byte not allowed in #{self.class}. " \
|
|
288
|
+
"Use #{Literal8} or a null-safe encoding."
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def send_data(imap, tag)
|
|
293
|
+
imap.__send__(:send_literal, data, tag, non_sync:)
|
|
294
|
+
end
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
class Literal8 < Literal # :nodoc:
|
|
298
|
+
def validate = nil # all bytes are okay
|
|
299
|
+
|
|
151
300
|
def send_data(imap, tag)
|
|
152
|
-
imap.__send__(:
|
|
301
|
+
imap.__send__(:send_binary_literal, data, tag, non_sync:)
|
|
153
302
|
end
|
|
154
303
|
end
|
|
155
304
|
|
|
@@ -185,73 +334,6 @@ module Net
|
|
|
185
334
|
end
|
|
186
335
|
end
|
|
187
336
|
|
|
188
|
-
# *DEPRECATED*. Replaced by SequenceSet.
|
|
189
|
-
class MessageSet < CommandData # :nodoc:
|
|
190
|
-
def send_data(imap, tag)
|
|
191
|
-
imap.__send__(:put_string, format_internal(data))
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
def validate
|
|
195
|
-
validate_internal(data)
|
|
196
|
-
end
|
|
197
|
-
|
|
198
|
-
private
|
|
199
|
-
|
|
200
|
-
def initialize(data:)
|
|
201
|
-
super
|
|
202
|
-
warn("DEPRECATED: #{MessageSet} should be replaced with #{SequenceSet}.",
|
|
203
|
-
uplevel: 1, category: :deprecated)
|
|
204
|
-
begin
|
|
205
|
-
# to ensure the input works with SequenceSet, too
|
|
206
|
-
SequenceSet.new(data)
|
|
207
|
-
rescue
|
|
208
|
-
warn "MessageSet input is incompatible with SequenceSet: [%s] %s" % [
|
|
209
|
-
$!.class, $!.message
|
|
210
|
-
]
|
|
211
|
-
end
|
|
212
|
-
end
|
|
213
|
-
|
|
214
|
-
def format_internal(data)
|
|
215
|
-
case data
|
|
216
|
-
when "*"
|
|
217
|
-
return data
|
|
218
|
-
when Integer
|
|
219
|
-
if data == -1
|
|
220
|
-
return "*"
|
|
221
|
-
else
|
|
222
|
-
return data.to_s
|
|
223
|
-
end
|
|
224
|
-
when Range
|
|
225
|
-
return format_internal(data.first) +
|
|
226
|
-
":" + format_internal(data.last)
|
|
227
|
-
when Array
|
|
228
|
-
return data.collect {|i| format_internal(i)}.join(",")
|
|
229
|
-
when ThreadMember
|
|
230
|
-
return data.seqno.to_s +
|
|
231
|
-
":" + data.children.collect {|i| format_internal(i).join(",")}
|
|
232
|
-
end
|
|
233
|
-
end
|
|
234
|
-
|
|
235
|
-
def validate_internal(data)
|
|
236
|
-
case data
|
|
237
|
-
when "*"
|
|
238
|
-
when Integer
|
|
239
|
-
NumValidator.ensure_nz_number(data)
|
|
240
|
-
when Range
|
|
241
|
-
when Array
|
|
242
|
-
data.each do |i|
|
|
243
|
-
validate_internal(i)
|
|
244
|
-
end
|
|
245
|
-
when ThreadMember
|
|
246
|
-
data.children.each do |i|
|
|
247
|
-
validate_internal(i)
|
|
248
|
-
end
|
|
249
|
-
else
|
|
250
|
-
raise DataFormatError, data.inspect
|
|
251
|
-
end
|
|
252
|
-
end
|
|
253
|
-
end
|
|
254
|
-
|
|
255
337
|
class ClientID < CommandData # :nodoc:
|
|
256
338
|
|
|
257
339
|
def send_data(imap, tag)
|
|
@@ -289,6 +371,14 @@ module Net
|
|
|
289
371
|
|
|
290
372
|
module_function
|
|
291
373
|
|
|
374
|
+
def literal_or_literal8(input, name: "argument")
|
|
375
|
+
return input if input in Literal | Literal8
|
|
376
|
+
data = String.try_convert(input) \
|
|
377
|
+
or raise TypeError, "expected #{name} to be String, got #{input.class}"
|
|
378
|
+
type = data.include?("\0") ? Literal8 : Literal
|
|
379
|
+
type.new(data:)
|
|
380
|
+
end
|
|
381
|
+
|
|
292
382
|
# Allows symbols in addition to strings
|
|
293
383
|
def valid_string?(str)
|
|
294
384
|
str.is_a?(Symbol) || str.respond_to?(:to_str)
|
|
@@ -27,24 +27,23 @@ module Net
|
|
|
27
27
|
|
|
28
28
|
def self.attr_accessor(name) # :nodoc: internal API
|
|
29
29
|
name = name.to_sym
|
|
30
|
+
raise ArgumentError, "already defined #{name}" if attributes.include?(name)
|
|
31
|
+
attributes << name
|
|
30
32
|
def_delegators :data, name, :"#{name}="
|
|
31
33
|
end
|
|
32
34
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
private_class_method :attributes
|
|
35
|
+
# An array of Config attribute names
|
|
36
|
+
singleton_class.attr_reader :attributes
|
|
37
|
+
@attributes = []
|
|
37
38
|
|
|
38
39
|
def self.struct # :nodoc: internal API
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
end
|
|
42
|
-
self::Struct
|
|
40
|
+
attributes.freeze
|
|
41
|
+
Struct.new(*attributes)
|
|
43
42
|
end
|
|
44
43
|
|
|
45
44
|
def initialize # :notnew:
|
|
46
45
|
super()
|
|
47
|
-
@data =
|
|
46
|
+
@data = Config::Struct.new
|
|
48
47
|
end
|
|
49
48
|
|
|
50
49
|
# Freezes the internal attributes struct, in addition to +self+.
|
|
@@ -54,9 +54,72 @@ module Net
|
|
|
54
54
|
# Creates a new config, which inherits from +self+.
|
|
55
55
|
def new(**attrs) self.class.new(self, **attrs) end
|
|
56
56
|
|
|
57
|
+
# :call-seq:
|
|
58
|
+
# inherited?(attr) -> true or false
|
|
59
|
+
# inherited?(*attrs) -> true or false
|
|
60
|
+
# inherited? -> true or false
|
|
61
|
+
#
|
|
57
62
|
# Returns +true+ if +attr+ is inherited from #parent and not overridden
|
|
58
63
|
# by this config.
|
|
59
|
-
|
|
64
|
+
#
|
|
65
|
+
# When multiple +attrs+ are given, returns +true+ if *all* of them are
|
|
66
|
+
# inherited, or +false+ if any of them are overriden. When no +attrs+
|
|
67
|
+
# are given, returns +true+ if *all* attributes are inherited, or
|
|
68
|
+
# +false+ if any attribute is overriden.
|
|
69
|
+
#
|
|
70
|
+
# Related: #overrides?
|
|
71
|
+
def inherited?(*attrs)
|
|
72
|
+
attrs = data.members if attrs.empty?
|
|
73
|
+
attrs.all? { data[_1] == INHERITED }
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# :call-seq:
|
|
77
|
+
# inherits_defaults?(*attrs) -> true | Rational | nil | false
|
|
78
|
+
#
|
|
79
|
+
# Returns whether all +attrs+ are inherited from a default config.
|
|
80
|
+
# When no +attrs+ are given, returns whether *all* attributes are
|
|
81
|
+
# inherited from a default config.
|
|
82
|
+
#
|
|
83
|
+
# Returns +true+ when all attributes inherit from Config.default, the
|
|
84
|
+
# version number (as a Rational) when all attributes inherit from a
|
|
85
|
+
# versioned default (see Config@Versioned+defaults), +nil+ if any
|
|
86
|
+
# attributes inherit from Config.global overrides (but not from
|
|
87
|
+
# non-global ancestors), or +false+ when any attributes have been
|
|
88
|
+
# overridden by +self+ or an ancestor (besides global or default
|
|
89
|
+
# configs),
|
|
90
|
+
#
|
|
91
|
+
# Related: #overrides?
|
|
92
|
+
def inherits_defaults?(*attrs)
|
|
93
|
+
if equal?(Config.default)
|
|
94
|
+
true
|
|
95
|
+
elsif equal?(Config.global)
|
|
96
|
+
true if inherited?(*attrs)
|
|
97
|
+
elsif (v = AttrVersionDefaults::VERSIONS.find { equal? Config[_1] })
|
|
98
|
+
attrs = DEFAULT_TO_INHERIT if attrs.empty?
|
|
99
|
+
attrs &= DEFAULT_TO_INHERIT
|
|
100
|
+
(attrs.empty? || parent.inherits_defaults?(*attrs)) && v
|
|
101
|
+
else
|
|
102
|
+
inherited?(*attrs) && parent.inherits_defaults?(*attrs)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# :call-seq:
|
|
107
|
+
# overrides?(attr) -> true or false
|
|
108
|
+
# overrides?(*attrs) -> true or false
|
|
109
|
+
# overrides? -> true or false
|
|
110
|
+
#
|
|
111
|
+
# Returns +true+ if +attr+ is defined on this config and not inherited
|
|
112
|
+
# from #parent.
|
|
113
|
+
#
|
|
114
|
+
# When multiple +attrs+ are given, returns +true+ if
|
|
115
|
+
# *any* of them are defined on +self+. When no +attrs+ are given,
|
|
116
|
+
# returns +true+ if *any* attribute is overriden.
|
|
117
|
+
#
|
|
118
|
+
# Related: #inherited?
|
|
119
|
+
def overrides?(*attrs)
|
|
120
|
+
attrs = data.members if attrs.empty?
|
|
121
|
+
attrs.any? { data[_1] != INHERITED }
|
|
122
|
+
end
|
|
60
123
|
|
|
61
124
|
# :call-seq:
|
|
62
125
|
# reset -> self
|
|
@@ -28,10 +28,22 @@ module Net
|
|
|
28
28
|
end
|
|
29
29
|
private_class_method :included
|
|
30
30
|
|
|
31
|
-
if defined?(Ractor.
|
|
32
|
-
def self.safe(
|
|
31
|
+
if defined?(Ractor.shareable_proc)
|
|
32
|
+
def self.safe(&b)
|
|
33
|
+
case obj = b.call
|
|
34
|
+
when Proc
|
|
35
|
+
Ractor.shareable_proc(&obj)
|
|
36
|
+
else
|
|
37
|
+
Ractor.make_shareable obj
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
elsif defined?(Ractor.make_shareable)
|
|
41
|
+
def self.safe(&b)
|
|
42
|
+
obj = nil.instance_eval(&b).freeze
|
|
43
|
+
Ractor.make_shareable obj
|
|
44
|
+
end
|
|
33
45
|
else
|
|
34
|
-
def self.safe(
|
|
46
|
+
def self.safe(&b) nil.instance_eval(&b).freeze end
|
|
35
47
|
end
|
|
36
48
|
private_class_method :safe
|
|
37
49
|
|
|
@@ -48,10 +60,10 @@ module Net
|
|
|
48
60
|
NilOrInteger = safe{->val { Integer val unless val.nil? }}
|
|
49
61
|
|
|
50
62
|
Enum = ->(*enum) {
|
|
51
|
-
|
|
52
|
-
expected = -"one of #{
|
|
63
|
+
safe_enum = safe{enum}
|
|
64
|
+
expected = -"one of #{safe_enum.map(&:inspect).join(", ")}"
|
|
53
65
|
safe{->val {
|
|
54
|
-
return val if
|
|
66
|
+
return val if safe_enum.include?(val)
|
|
55
67
|
raise ArgumentError, "expected %s, got %p" % [expected, val]
|
|
56
68
|
}}
|
|
57
69
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "forwardable"
|
|
4
|
+
|
|
5
|
+
module Net
|
|
6
|
+
class IMAP
|
|
7
|
+
class Config
|
|
8
|
+
# >>>
|
|
9
|
+
# *NOTE:* This module is an internal implementation detail, with no
|
|
10
|
+
# guarantee of backward compatibility.
|
|
11
|
+
#
|
|
12
|
+
# Adds a +defaults+ parameter to +attr_accessor+, which is used to compile
|
|
13
|
+
# Config.version_defaults.
|
|
14
|
+
module AttrVersionDefaults
|
|
15
|
+
# The <tt>x.y</tt> part of Net::IMAP::VERSION, as a Rational number.
|
|
16
|
+
CURRENT_VERSION = VERSION.to_r
|
|
17
|
+
|
|
18
|
+
# The config version used for <tt>Config[:next]</tt>.
|
|
19
|
+
NEXT_VERSION = CURRENT_VERSION + 0.1r
|
|
20
|
+
|
|
21
|
+
# The config version used for <tt>Config[:future]</tt>.
|
|
22
|
+
FUTURE_VERSION = 1.0r
|
|
23
|
+
|
|
24
|
+
VERSIONS = ((0.0r..FUTURE_VERSION) % 0.1r).to_a.freeze
|
|
25
|
+
|
|
26
|
+
# See Config.version_defaults.
|
|
27
|
+
singleton_class.attr_reader :version_defaults
|
|
28
|
+
|
|
29
|
+
@version_defaults = Hash.new {|h, k|
|
|
30
|
+
# NOTE: String responds to both so the order is significant.
|
|
31
|
+
# And ignore non-numeric conversion to zero, because: "wat!?".to_r == 0
|
|
32
|
+
(h.fetch(k.to_r, nil) || h.fetch(k.to_f, nil) if k.is_a?(Numeric)) ||
|
|
33
|
+
(h.fetch(k.to_sym, nil) if k.respond_to?(:to_sym)) ||
|
|
34
|
+
(h.fetch(k.to_r, nil) if k.respond_to?(:to_r) && k.to_r != 0r) ||
|
|
35
|
+
(h.fetch(k.to_f, nil) if k.respond_to?(:to_f) && k.to_f != 0.0)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# :stopdoc: internal APIs only
|
|
39
|
+
|
|
40
|
+
def attr_accessor(name, defaults: nil, default: (unset = true), **kw)
|
|
41
|
+
unless unset
|
|
42
|
+
version = DEFAULT_TO_INHERIT.include?(name) ? nil : 0.0r
|
|
43
|
+
defaults = { version => default }
|
|
44
|
+
end
|
|
45
|
+
defaults&.each_pair do |version, default|
|
|
46
|
+
AttrVersionDefaults.version_defaults[version] ||= {}
|
|
47
|
+
AttrVersionDefaults.version_defaults[version][name] = default
|
|
48
|
+
end
|
|
49
|
+
super(name, **kw)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.compile_default!
|
|
53
|
+
raise "Config.default already compiled" if Config.default
|
|
54
|
+
default = VERSIONS.select { _1 <= CURRENT_VERSION }
|
|
55
|
+
.filter_map { version_defaults[_1] }
|
|
56
|
+
.prepend(version_defaults.delete(nil))
|
|
57
|
+
.inject(&:merge)
|
|
58
|
+
Config.new(**default).freeze
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.compile_version_defaults!
|
|
62
|
+
version_defaults[0.0r] = Config[version_defaults.fetch(0.0r)]
|
|
63
|
+
|
|
64
|
+
VERSIONS.each_cons(2) do |prior, version|
|
|
65
|
+
updates = version_defaults[version]
|
|
66
|
+
version_defaults[version] = version_defaults[prior]
|
|
67
|
+
.then { updates ? _1.dup.update(**updates).freeze : _1 }
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Safe conversions one way only:
|
|
71
|
+
# 0.6r.to_f == 0.6 # => true
|
|
72
|
+
# 0.6 .to_r == 0.6r # => false
|
|
73
|
+
version_defaults.to_a.each do |k, v|
|
|
74
|
+
next unless k in Rational
|
|
75
|
+
version_defaults[k.to_f] = v
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
version_defaults[:original] = Config[0.0r]
|
|
79
|
+
version_defaults[:current] = Config[CURRENT_VERSION]
|
|
80
|
+
version_defaults[:default] = Config[CURRENT_VERSION]
|
|
81
|
+
version_defaults[:next] = Config[NEXT_VERSION]
|
|
82
|
+
version_defaults[:future] = Config[FUTURE_VERSION]
|
|
83
|
+
|
|
84
|
+
version_defaults.freeze
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|