net-imap 0.5.6 → 0.5.8
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/lib/net/imap/config/attr_type_coercion.rb +23 -22
- data/lib/net/imap/config.rb +101 -19
- data/lib/net/imap/connection_state.rb +48 -0
- data/lib/net/imap/errors.rb +33 -0
- data/lib/net/imap/response_reader.rb +73 -0
- data/lib/net/imap/sequence_set.rb +253 -97
- data/lib/net/imap.rb +218 -47
- metadata +5 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b6d2d79f907ce00d056ead396fafe73bbdf84fc54b01e1f2536d6490c001c723
|
4
|
+
data.tar.gz: 6fba3cd04144fedd7b178959451631ea14cff8f248ca963961b3945b636cec4b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 80f15f967d4260638d423b802204360e25704cc47c59d110a85f401c1554882521097d5014ecc3e7a0a17f87e8995ae87f553d73a26f3c8abc84744ad5d236ed
|
7
|
+
data.tar.gz: 2790cbc5ee60b3da3648e8bd91df90ecac7c72e0febca11464400460fd5c34db3123d307f39901ea7c313942f2feb11bb3c0f7dbc06b96094b6d1738426b278a
|
@@ -18,6 +18,8 @@ module Net
|
|
18
18
|
super(attr)
|
19
19
|
AttrTypeCoercion.attr_accessor(attr, type: type)
|
20
20
|
end
|
21
|
+
|
22
|
+
module_function def Integer? = NilOrInteger
|
21
23
|
end
|
22
24
|
private_constant :Macros
|
23
25
|
|
@@ -26,34 +28,33 @@ module Net
|
|
26
28
|
end
|
27
29
|
private_class_method :included
|
28
30
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
elsif Array === type then enum attr, type
|
34
|
-
else raise ArgumentError, "unknown type coercion %p" % [type]
|
35
|
-
end
|
31
|
+
if defined?(Ractor.make_shareable)
|
32
|
+
def self.safe(...) Ractor.make_shareable nil.instance_eval(...).freeze end
|
33
|
+
else
|
34
|
+
def self.safe(...) nil.instance_eval(...).freeze end
|
36
35
|
end
|
36
|
+
private_class_method :safe
|
37
37
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
end
|
38
|
+
Types = Hash.new do |h, type| type => Proc | nil; safe{type} end
|
39
|
+
Types[:boolean] = Boolean = safe{-> {!!_1}}
|
40
|
+
Types[Integer] = safe{->{Integer(_1)}}
|
42
41
|
|
43
|
-
def self.
|
44
|
-
|
42
|
+
def self.attr_accessor(attr, type: nil)
|
43
|
+
type = Types[type] or return
|
44
|
+
define_method :"#{attr}=" do |val| super type[val] end
|
45
|
+
define_method :"#{attr}?" do send attr end if type == Boolean
|
45
46
|
end
|
46
47
|
|
47
|
-
|
48
|
-
|
48
|
+
NilOrInteger = safe{->val { Integer val unless val.nil? }}
|
49
|
+
|
50
|
+
Enum = ->(*enum) {
|
51
|
+
enum = safe{enum}
|
49
52
|
expected = -"one of #{enum.map(&:inspect).join(", ")}"
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
end
|
56
|
-
end
|
53
|
+
safe{->val {
|
54
|
+
return val if enum.include?(val)
|
55
|
+
raise ArgumentError, "expected %s, got %p" % [expected, val]
|
56
|
+
}}
|
57
|
+
}
|
57
58
|
|
58
59
|
end
|
59
60
|
end
|
data/lib/net/imap/config.rb
CHANGED
@@ -131,8 +131,25 @@ module Net
|
|
131
131
|
def self.global; @global if defined?(@global) end
|
132
132
|
|
133
133
|
# A hash of hard-coded configurations, indexed by version number or name.
|
134
|
+
# Values can be accessed with any object that responds to +to_sym+ or
|
135
|
+
# +to_r+/+to_f+ with a non-zero number.
|
136
|
+
#
|
137
|
+
# Config::[] gets named or numbered versions from this hash.
|
138
|
+
#
|
139
|
+
# For example:
|
140
|
+
# Net::IMAP::Config.version_defaults[0.5] == Net::IMAP::Config[0.5]
|
141
|
+
# Net::IMAP::Config[0.5] == Net::IMAP::Config[0.5r] # => true
|
142
|
+
# Net::IMAP::Config["current"] == Net::IMAP::Config[:current] # => true
|
143
|
+
# Net::IMAP::Config["0.5.6"] == Net::IMAP::Config[0.5r] # => true
|
134
144
|
def self.version_defaults; @version_defaults end
|
135
|
-
@version_defaults = {
|
145
|
+
@version_defaults = Hash.new {|h, k|
|
146
|
+
# NOTE: String responds to both so the order is significant.
|
147
|
+
# And ignore non-numeric conversion to zero, because: "wat!?".to_r == 0
|
148
|
+
(h.fetch(k.to_r, nil) || h.fetch(k.to_f, nil) if k.is_a?(Numeric)) ||
|
149
|
+
(h.fetch(k.to_sym, nil) if k.respond_to?(:to_sym)) ||
|
150
|
+
(h.fetch(k.to_r, nil) if k.respond_to?(:to_r) && k.to_r != 0r) ||
|
151
|
+
(h.fetch(k.to_f, nil) if k.respond_to?(:to_f) && k.to_f != 0.0)
|
152
|
+
}
|
136
153
|
|
137
154
|
# :call-seq:
|
138
155
|
# Net::IMAP::Config[number] -> versioned config
|
@@ -155,18 +172,17 @@ module Net
|
|
155
172
|
elsif config.nil? && global.nil? then nil
|
156
173
|
elsif config.respond_to?(:to_hash) then new(global, **config).freeze
|
157
174
|
else
|
158
|
-
version_defaults
|
175
|
+
version_defaults[config] or
|
159
176
|
case config
|
160
177
|
when Numeric
|
161
178
|
raise RangeError, "unknown config version: %p" % [config]
|
162
|
-
when Symbol
|
179
|
+
when String, Symbol
|
163
180
|
raise KeyError, "unknown config name: %p" % [config]
|
164
181
|
else
|
165
182
|
raise TypeError, "no implicit conversion of %s to %s" % [
|
166
183
|
config.class, Config
|
167
184
|
]
|
168
185
|
end
|
169
|
-
end
|
170
186
|
end
|
171
187
|
end
|
172
188
|
|
@@ -193,10 +209,13 @@ module Net
|
|
193
209
|
|
194
210
|
# Seconds to wait until a connection is opened.
|
195
211
|
#
|
212
|
+
# Applied separately for establishing TCP connection and starting a TLS
|
213
|
+
# connection.
|
214
|
+
#
|
196
215
|
# If the IMAP object cannot open a connection within this time,
|
197
216
|
# it raises a Net::OpenTimeout exception.
|
198
217
|
#
|
199
|
-
# See Net::IMAP.new.
|
218
|
+
# See Net::IMAP.new and Net::IMAP#starttls.
|
200
219
|
#
|
201
220
|
# The default value is +30+ seconds.
|
202
221
|
attr_accessor :open_timeout, type: Integer
|
@@ -245,10 +264,44 @@ module Net
|
|
245
264
|
# present. When capabilities are unknown, Net::IMAP will automatically
|
246
265
|
# send a +CAPABILITY+ command first before sending +LOGIN+.
|
247
266
|
#
|
248
|
-
attr_accessor :enforce_logindisabled, type: [
|
267
|
+
attr_accessor :enforce_logindisabled, type: Enum[
|
249
268
|
false, :when_capabilities_cached, true
|
250
269
|
]
|
251
270
|
|
271
|
+
# The maximum allowed server response size. When +nil+, there is no limit
|
272
|
+
# on response size.
|
273
|
+
#
|
274
|
+
# The default value (512 MiB, since +v0.5.7+) is <em>very high</em> and
|
275
|
+
# unlikely to be reached. A _much_ lower value should be used with
|
276
|
+
# untrusted servers (for example, when connecting to a user-provided
|
277
|
+
# hostname). When using a lower limit, message bodies should be fetched
|
278
|
+
# in chunks rather than all at once.
|
279
|
+
#
|
280
|
+
# <em>Please Note:</em> this only limits the size per response. It does
|
281
|
+
# not prevent a flood of individual responses and it does not limit how
|
282
|
+
# many unhandled responses may be stored on the responses hash. See
|
283
|
+
# Net::IMAP@Unbounded+memory+use.
|
284
|
+
#
|
285
|
+
# Socket reads are limited to the maximum remaining bytes for the current
|
286
|
+
# response: max_response_size minus the bytes that have already been read.
|
287
|
+
# When the limit is reached, or reading a +literal+ _would_ go over the
|
288
|
+
# limit, ResponseTooLargeError is raised and the connection is closed.
|
289
|
+
#
|
290
|
+
# Note that changes will not take effect immediately, because the receiver
|
291
|
+
# thread may already be waiting for the next response using the previous
|
292
|
+
# value. Net::IMAP#noop can force a response and enforce the new setting
|
293
|
+
# immediately.
|
294
|
+
#
|
295
|
+
# ==== Versioned Defaults
|
296
|
+
#
|
297
|
+
# Net::IMAP#max_response_size <em>was added in +v0.2.5+ and +v0.3.9+ as an
|
298
|
+
# attr_accessor, and in +v0.4.20+ and +v0.5.7+ as a delegator to this
|
299
|
+
# config attribute.</em>
|
300
|
+
#
|
301
|
+
# * original: +nil+ <em>(no limit)</em>
|
302
|
+
# * +0.5+: 512 MiB
|
303
|
+
attr_accessor :max_response_size, type: Integer?
|
304
|
+
|
252
305
|
# Controls the behavior of Net::IMAP#responses when called without any
|
253
306
|
# arguments (+type+ or +block+).
|
254
307
|
#
|
@@ -275,7 +328,7 @@ module Net
|
|
275
328
|
# Raise an ArgumentError with the deprecation warning.
|
276
329
|
#
|
277
330
|
# Note: #responses_without_args is an alias for #responses_without_block.
|
278
|
-
attr_accessor :responses_without_block, type: [
|
331
|
+
attr_accessor :responses_without_block, type: Enum[
|
279
332
|
:silence_deprecation_warning, :warn, :frozen_dup, :raise,
|
280
333
|
]
|
281
334
|
|
@@ -320,7 +373,7 @@ module Net
|
|
320
373
|
#
|
321
374
|
# [+false+ <em>(planned default for +v0.6+)</em>]
|
322
375
|
# ResponseParser _only_ uses AppendUIDData and CopyUIDData.
|
323
|
-
attr_accessor :parser_use_deprecated_uidplus_data, type: [
|
376
|
+
attr_accessor :parser_use_deprecated_uidplus_data, type: Enum[
|
324
377
|
true, :up_to_max_size, false
|
325
378
|
]
|
326
379
|
|
@@ -427,6 +480,7 @@ module Net
|
|
427
480
|
idle_response_timeout: 5,
|
428
481
|
sasl_ir: true,
|
429
482
|
enforce_logindisabled: true,
|
483
|
+
max_response_size: 512 << 20, # 512 MiB
|
430
484
|
responses_without_block: :warn,
|
431
485
|
parser_use_deprecated_uidplus_data: :up_to_max_size,
|
432
486
|
parser_max_deprecated_uidplus_data_size: 100,
|
@@ -435,36 +489,64 @@ module Net
|
|
435
489
|
@global = default.new
|
436
490
|
|
437
491
|
version_defaults[:default] = Config[default.send(:defaults_hash)]
|
438
|
-
version_defaults[:current] = Config[:default]
|
439
492
|
|
440
|
-
version_defaults[
|
493
|
+
version_defaults[0r] = Config[:default].dup.update(
|
441
494
|
sasl_ir: false,
|
442
495
|
responses_without_block: :silence_deprecation_warning,
|
443
496
|
enforce_logindisabled: false,
|
497
|
+
max_response_size: nil,
|
444
498
|
parser_use_deprecated_uidplus_data: true,
|
445
499
|
parser_max_deprecated_uidplus_data_size: 10_000,
|
446
500
|
).freeze
|
447
|
-
version_defaults[0.
|
448
|
-
version_defaults[0.
|
449
|
-
version_defaults[0.
|
450
|
-
version_defaults[0.
|
501
|
+
version_defaults[0.0r] = Config[0r]
|
502
|
+
version_defaults[0.1r] = Config[0r]
|
503
|
+
version_defaults[0.2r] = Config[0r]
|
504
|
+
version_defaults[0.3r] = Config[0r]
|
451
505
|
|
452
|
-
version_defaults[0.
|
506
|
+
version_defaults[0.4r] = Config[0.3r].dup.update(
|
453
507
|
sasl_ir: true,
|
454
508
|
parser_max_deprecated_uidplus_data_size: 1000,
|
455
509
|
).freeze
|
456
510
|
|
457
|
-
version_defaults[0.
|
511
|
+
version_defaults[0.5r] = Config[0.4r].dup.update(
|
512
|
+
enforce_logindisabled: true,
|
513
|
+
max_response_size: 512 << 20, # 512 MiB
|
514
|
+
responses_without_block: :warn,
|
515
|
+
parser_use_deprecated_uidplus_data: :up_to_max_size,
|
516
|
+
parser_max_deprecated_uidplus_data_size: 100,
|
517
|
+
).freeze
|
458
518
|
|
459
|
-
version_defaults[0.
|
519
|
+
version_defaults[0.6r] = Config[0.5r].dup.update(
|
460
520
|
responses_without_block: :frozen_dup,
|
461
521
|
parser_use_deprecated_uidplus_data: false,
|
462
522
|
parser_max_deprecated_uidplus_data_size: 0,
|
463
523
|
).freeze
|
464
|
-
|
465
|
-
version_defaults[
|
524
|
+
|
525
|
+
version_defaults[0.7r] = Config[0.6r].dup.update(
|
526
|
+
).freeze
|
527
|
+
|
528
|
+
# Safe conversions one way only:
|
529
|
+
# 0.6r.to_f == 0.6 # => true
|
530
|
+
# 0.6 .to_r == 0.6r # => false
|
531
|
+
version_defaults.to_a.each do |k, v|
|
532
|
+
next unless k in Rational
|
533
|
+
version_defaults[k.to_f] = v
|
534
|
+
end
|
535
|
+
|
536
|
+
current = VERSION.to_r
|
537
|
+
version_defaults[:original] = Config[0]
|
538
|
+
version_defaults[:current] = Config[current]
|
539
|
+
version_defaults[:next] = Config[current + 0.1r]
|
540
|
+
version_defaults[:future] = Config[current + 0.2r]
|
466
541
|
|
467
542
|
version_defaults.freeze
|
543
|
+
|
544
|
+
if ($VERBOSE || $DEBUG) && self[:current].to_h != self[:default].to_h
|
545
|
+
warn "Misconfigured Net::IMAP::Config[:current] => %p,\n" \
|
546
|
+
" not equal to Net::IMAP::Config[:default] => %p" % [
|
547
|
+
self[:current].to_h, self[:default].to_h
|
548
|
+
]
|
549
|
+
end
|
468
550
|
end
|
469
551
|
end
|
470
552
|
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Net
|
4
|
+
class IMAP
|
5
|
+
class ConnectionState < Net::IMAP::Data # :nodoc:
|
6
|
+
def self.define(symbol, *attrs)
|
7
|
+
symbol => Symbol
|
8
|
+
state = super(*attrs)
|
9
|
+
state.const_set :NAME, symbol
|
10
|
+
state
|
11
|
+
end
|
12
|
+
|
13
|
+
def symbol; self.class::NAME end
|
14
|
+
def name; self.class::NAME.name end
|
15
|
+
alias to_sym symbol
|
16
|
+
|
17
|
+
def deconstruct; [symbol, *super] end
|
18
|
+
|
19
|
+
def deconstruct_keys(names)
|
20
|
+
hash = super
|
21
|
+
hash[:symbol] = symbol if names.nil? || names.include?(:symbol)
|
22
|
+
hash[:name] = name if names.nil? || names.include?(:name)
|
23
|
+
hash
|
24
|
+
end
|
25
|
+
|
26
|
+
def to_h(&block)
|
27
|
+
hash = deconstruct_keys(nil)
|
28
|
+
block ? hash.to_h(&block) : hash
|
29
|
+
end
|
30
|
+
|
31
|
+
def not_authenticated?; to_sym == :not_authenticated end
|
32
|
+
def authenticated?; to_sym == :authenticated end
|
33
|
+
def selected?; to_sym == :selected end
|
34
|
+
def logout?; to_sym == :logout end
|
35
|
+
|
36
|
+
NotAuthenticated = define(:not_authenticated)
|
37
|
+
Authenticated = define(:authenticated)
|
38
|
+
Selected = define(:selected)
|
39
|
+
Logout = define(:logout)
|
40
|
+
|
41
|
+
class << self
|
42
|
+
undef :define
|
43
|
+
end
|
44
|
+
freeze
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
data/lib/net/imap/errors.rb
CHANGED
@@ -17,6 +17,39 @@ module Net
|
|
17
17
|
class DataFormatError < Error
|
18
18
|
end
|
19
19
|
|
20
|
+
# Error raised when the socket cannot be read, due to a Config limit.
|
21
|
+
class ResponseReadError < Error
|
22
|
+
end
|
23
|
+
|
24
|
+
# Error raised when a response is larger than IMAP#max_response_size.
|
25
|
+
class ResponseTooLargeError < ResponseReadError
|
26
|
+
attr_reader :bytes_read, :literal_size
|
27
|
+
attr_reader :max_response_size
|
28
|
+
|
29
|
+
def initialize(msg = nil, *args,
|
30
|
+
bytes_read: nil,
|
31
|
+
literal_size: nil,
|
32
|
+
max_response_size: nil,
|
33
|
+
**kwargs)
|
34
|
+
@bytes_read = bytes_read
|
35
|
+
@literal_size = literal_size
|
36
|
+
@max_response_size = max_response_size
|
37
|
+
msg ||= [
|
38
|
+
"Response size", response_size_msg, "exceeds max_response_size",
|
39
|
+
max_response_size && "(#{max_response_size}B)",
|
40
|
+
].compact.join(" ")
|
41
|
+
super(msg, *args, **kwargs)
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def response_size_msg
|
47
|
+
if bytes_read && literal_size
|
48
|
+
"(#{bytes_read}B read + #{literal_size}B literal)"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
20
53
|
# Error raised when a response from the server is non-parsable.
|
21
54
|
class ResponseParseError < Error
|
22
55
|
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Net
|
4
|
+
class IMAP
|
5
|
+
# See https://www.rfc-editor.org/rfc/rfc9051#section-2.2.2
|
6
|
+
class ResponseReader # :nodoc:
|
7
|
+
attr_reader :client
|
8
|
+
|
9
|
+
def initialize(client, sock)
|
10
|
+
@client, @sock = client, sock
|
11
|
+
end
|
12
|
+
|
13
|
+
def read_response_buffer
|
14
|
+
@buff = String.new
|
15
|
+
catch :eof do
|
16
|
+
while true
|
17
|
+
read_line
|
18
|
+
break unless (@literal_size = get_literal_size)
|
19
|
+
read_literal
|
20
|
+
end
|
21
|
+
end
|
22
|
+
buff
|
23
|
+
ensure
|
24
|
+
@buff = nil
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_reader :buff, :literal_size
|
30
|
+
|
31
|
+
def bytes_read = buff.bytesize
|
32
|
+
def empty? = buff.empty?
|
33
|
+
def done? = line_done? && !get_literal_size
|
34
|
+
def line_done? = buff.end_with?(CRLF)
|
35
|
+
def get_literal_size = /\{(\d+)\}\r\n\z/n =~ buff && $1.to_i
|
36
|
+
|
37
|
+
def read_line
|
38
|
+
buff << (@sock.gets(CRLF, read_limit) or throw :eof)
|
39
|
+
max_response_remaining! unless line_done?
|
40
|
+
end
|
41
|
+
|
42
|
+
def read_literal
|
43
|
+
# check before allocating memory for literal
|
44
|
+
max_response_remaining!
|
45
|
+
literal = String.new(capacity: literal_size)
|
46
|
+
buff << (@sock.read(read_limit(literal_size), literal) or throw :eof)
|
47
|
+
ensure
|
48
|
+
@literal_size = nil
|
49
|
+
end
|
50
|
+
|
51
|
+
def read_limit(limit = nil)
|
52
|
+
[limit, max_response_remaining!].compact.min
|
53
|
+
end
|
54
|
+
|
55
|
+
def max_response_size = client.max_response_size
|
56
|
+
def max_response_remaining = max_response_size &.- bytes_read
|
57
|
+
def response_too_large? = max_response_size &.< min_response_size
|
58
|
+
def min_response_size = bytes_read + min_response_remaining
|
59
|
+
|
60
|
+
def min_response_remaining
|
61
|
+
empty? ? 3 : done? ? 0 : (literal_size || 0) + 2
|
62
|
+
end
|
63
|
+
|
64
|
+
def max_response_remaining!
|
65
|
+
return max_response_remaining unless response_too_large?
|
66
|
+
raise ResponseTooLargeError.new(
|
67
|
+
max_response_size:, bytes_read:, literal_size:,
|
68
|
+
)
|
69
|
+
end
|
70
|
+
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|