net-imap 0.6.3 → 0.6.4.1
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 +1 -1
- data/README.md +1 -1
- data/lib/net/imap/command_data.rb +223 -25
- data/lib/net/imap/config.rb +34 -0
- data/lib/net/imap/data_encoding.rb +57 -6
- data/lib/net/imap/errors.rb +47 -10
- data/lib/net/imap/response_data.rb +108 -11
- data/lib/net/imap/response_parser.rb +25 -12
- data/lib/net/imap/response_reader.rb +30 -16
- data/lib/net/imap/sasl/scram_authenticator.rb +74 -0
- data/lib/net/imap/search_result.rb +5 -1
- data/lib/net/imap.rb +209 -75
- data/rakelib/rdoc.rake +1 -18
- metadata +4 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 032600f694434b77edab2496c4bff9a7adc5c912301c0a7b386706dfccdc2345
|
|
4
|
+
data.tar.gz: 69dd23250cda6c1eb7a9bbeabe10969389c10047f12610d6bec3407d23e16854
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 98ede1ee28c608ceea04bd5a00a114029da96eae454dc222cb046308c625241df423ceac7811322e6f641f3899ba1ca7a7d3f1bbfbf839632d2cf1a01c963c59
|
|
7
|
+
data.tar.gz: 810b3514de99b738216d7d49c917a05ee0c73211530bd81fe55540d158736dd7ba84f841d13f6d959b3dca01c3e32d792e500e7ee8f8eb5a2ab6899a53bf095e
|
data/.document
ADDED
data/.rdoc_options
ADDED
data/Gemfile
CHANGED
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
|
|
@@ -4,6 +4,8 @@ require "date"
|
|
|
4
4
|
|
|
5
5
|
require_relative "errors"
|
|
6
6
|
|
|
7
|
+
# :enddoc:
|
|
8
|
+
|
|
7
9
|
module Net
|
|
8
10
|
class IMAP < Protocol
|
|
9
11
|
|
|
@@ -14,17 +16,19 @@ module Net
|
|
|
14
16
|
when nil
|
|
15
17
|
when String
|
|
16
18
|
when Integer
|
|
17
|
-
|
|
19
|
+
# Covers modseq-valzer, which is the largest valid IMAP integer
|
|
20
|
+
if data.negative?
|
|
21
|
+
raise DataFormatError, "Integer argument must be unsigned: #{data}"
|
|
22
|
+
elsif 0xffff_ffff_ffff_ffff < data
|
|
23
|
+
raise DataFormatError, "Integer argument must fit in 64 bits: #{data}"
|
|
24
|
+
end
|
|
18
25
|
when Array
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
else
|
|
22
|
-
data.each do |i|
|
|
23
|
-
validate_data(i)
|
|
24
|
-
end
|
|
26
|
+
data.each do |i|
|
|
27
|
+
validate_data(i)
|
|
25
28
|
end
|
|
26
29
|
when Time, Date, DateTime
|
|
27
30
|
when Symbol
|
|
31
|
+
Flag.validate(data)
|
|
28
32
|
else
|
|
29
33
|
data.validate
|
|
30
34
|
end
|
|
@@ -45,7 +49,7 @@ module Net
|
|
|
45
49
|
when Date
|
|
46
50
|
send_date_data(data)
|
|
47
51
|
when Symbol
|
|
48
|
-
|
|
52
|
+
Flag[data].send_data(self, tag)
|
|
49
53
|
else
|
|
50
54
|
data.send_data(self, tag)
|
|
51
55
|
end
|
|
@@ -73,13 +77,33 @@ module Net
|
|
|
73
77
|
end
|
|
74
78
|
end
|
|
75
79
|
|
|
76
|
-
def send_quoted_string(str)
|
|
77
|
-
put_string('"' + str.gsub(/["\\]/, "\\\\\\&") + '"')
|
|
78
|
-
end
|
|
80
|
+
def send_quoted_string(str) = QuotedString.new(data: str).send_data(self)
|
|
79
81
|
|
|
80
|
-
def
|
|
82
|
+
def send_binary_literal(*, **) = send_literal(*, **, binary: true)
|
|
83
|
+
|
|
84
|
+
# `non_sync` is an optional tri-state flag:
|
|
85
|
+
# * `true` -> Force non-synchronizing `LITERAL+`/`LITERAL-` behavior.
|
|
86
|
+
# NOTE: raises DataFormatError when server doesn't support
|
|
87
|
+
# non-synchronizing literal, or literal is too large for LITERAL-.
|
|
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)
|
|
91
|
+
bytesize = str.bytesize
|
|
81
92
|
synchronize do
|
|
82
|
-
|
|
93
|
+
if non_sync && !non_sync_literal_allowed?(bytesize)
|
|
94
|
+
# TODO: check in Printer, so we don't need to close the connection.
|
|
95
|
+
@sock.close
|
|
96
|
+
raise DataFormatError, "Connection closed: " \
|
|
97
|
+
"Cannot send non-synchronizing literal without known server support"
|
|
98
|
+
end
|
|
99
|
+
non_sync = non_sync_literal?(bytesize) if non_sync.nil?
|
|
100
|
+
prefix = "~" if binary
|
|
101
|
+
plus = "+" if non_sync
|
|
102
|
+
put_string("#{prefix}{#{bytesize}#{plus}}\r\n")
|
|
103
|
+
if non_sync
|
|
104
|
+
put_string(str)
|
|
105
|
+
return
|
|
106
|
+
end
|
|
83
107
|
@continued_command_tag = tag
|
|
84
108
|
@continuation_request_exception = nil
|
|
85
109
|
begin
|
|
@@ -94,8 +118,23 @@ module Net
|
|
|
94
118
|
end
|
|
95
119
|
end
|
|
96
120
|
|
|
121
|
+
def non_sync_literal?(bytesize)
|
|
122
|
+
bytesize <= config.max_non_synchronizing_literal \
|
|
123
|
+
&& non_sync_literal_allowed?(bytesize)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def non_sync_literal_allowed?(bytesize)
|
|
127
|
+
return unless capabilities_cached?
|
|
128
|
+
return "+" if capable?("LITERAL+")
|
|
129
|
+
return "-" if capable_literal_minus? && bytesize <= 4096
|
|
130
|
+
false
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def capable_literal_minus? = capable?("LITERAL-") || capable?("IMAP4rev2")
|
|
134
|
+
|
|
135
|
+
# NOTE: +num+ should already be an Integer
|
|
97
136
|
def send_number_data(num)
|
|
98
|
-
put_string(num.to_s)
|
|
137
|
+
put_string(Integer(num).to_s)
|
|
99
138
|
end
|
|
100
139
|
|
|
101
140
|
def send_list_data(list, tag = nil)
|
|
@@ -115,11 +154,13 @@ module Net
|
|
|
115
154
|
def send_date_data(date) put_string Net::IMAP.encode_date(date) end
|
|
116
155
|
def send_time_data(time) put_string Net::IMAP.encode_time(time) end
|
|
117
156
|
|
|
118
|
-
def send_symbol_data(symbol)
|
|
119
|
-
put_string("\\" + symbol.to_s)
|
|
120
|
-
end
|
|
121
|
-
|
|
122
157
|
CommandData = Data.define(:data) do # :nodoc:
|
|
158
|
+
def self.validate(...)
|
|
159
|
+
data = new(...)
|
|
160
|
+
data.validate
|
|
161
|
+
data
|
|
162
|
+
end
|
|
163
|
+
|
|
123
164
|
def send_data(imap, tag)
|
|
124
165
|
raise NoMethodError, "#{self.class} must implement #{__method__}"
|
|
125
166
|
end
|
|
@@ -128,27 +169,176 @@ module Net
|
|
|
128
169
|
end
|
|
129
170
|
end
|
|
130
171
|
|
|
172
|
+
# Represents IMAP +text+ or +quoted+ data, which share the same
|
|
173
|
+
# validations of decoded #data, and differ only in how they are formatted.
|
|
174
|
+
#
|
|
175
|
+
# +data+ may contain any 7-bit ASCII character except +NULL+, +CR+, or +LF+.
|
|
176
|
+
# Any multibyte +UTF-8+ character is also allowed when the connection
|
|
177
|
+
# supports UTF8: either +UTF8=ACCEPT+ or +IMAP4rev2+ have been enabled, or
|
|
178
|
+
# the server supports only +IMAP4rev2+ and not earlier IMAP revisions, or
|
|
179
|
+
# the server advertises +UTF8=ONLY+.
|
|
180
|
+
#
|
|
181
|
+
# NOTE: This does not verify whether the connection supports UTF-8, but that
|
|
182
|
+
# may change in future versions.
|
|
183
|
+
#
|
|
184
|
+
# The string's bytes must be valid ASCII or valid UTF-8. The string's
|
|
185
|
+
# reported encoding is ignored, but the string is _not_ transcoded.
|
|
186
|
+
class ValidNonLiteralData < CommandData
|
|
187
|
+
def initialize(data:)
|
|
188
|
+
data = String(data.to_str)
|
|
189
|
+
unless data.encoding in Encoding::ASCII | Encoding::UTF_8
|
|
190
|
+
data = data.dup.force_encoding(data.ascii_only? ? "ASCII" : "UTF-8")
|
|
191
|
+
end
|
|
192
|
+
data = -data
|
|
193
|
+
super
|
|
194
|
+
validate
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def validate
|
|
198
|
+
if !(data.encoding in Encoding::ASCII | Encoding::UTF_8)
|
|
199
|
+
raise DataFormatError, "must use ASCII or UTF-8 encoding"
|
|
200
|
+
elsif !data.valid_encoding?
|
|
201
|
+
raise DataFormatError, "invalid UTF-8 must be literal encoded"
|
|
202
|
+
elsif data.include?("\0")
|
|
203
|
+
raise DataFormatError, "NULL byte must be binary literal encoded"
|
|
204
|
+
elsif /[\r\n]/.match?(data)
|
|
205
|
+
raise DataFormatError, "CR and LF bytes must be literal encoded"
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def ascii_only? = data.ascii_only?
|
|
210
|
+
|
|
211
|
+
def send_data(imap, tag = nil) = imap.__send__(:put_string, formatted)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Represents IMAP +text+ data, which covers everything in the IMAP grammar,
|
|
215
|
+
# except for +literal+, +literal8+, and the concluding +CRLF+.
|
|
216
|
+
#
|
|
217
|
+
# NOTE: The current implementation does not verify that the connection
|
|
218
|
+
# supports UTF-8. Future versions may validate this.
|
|
219
|
+
class RawText < ValidNonLiteralData # :nodoc:
|
|
220
|
+
# raw: no formatting necessary
|
|
221
|
+
alias formatted data
|
|
222
|
+
end
|
|
223
|
+
|
|
131
224
|
class RawData < CommandData # :nodoc:
|
|
132
|
-
def
|
|
133
|
-
|
|
225
|
+
def initialize(data:)
|
|
226
|
+
case data
|
|
227
|
+
in String then data = self.class.split(data)
|
|
228
|
+
in Array if data.all? { _1 in RawText | Literal }
|
|
229
|
+
else
|
|
230
|
+
raise TypeError, "expected String or Array[#{RawText} | #{Literal}]"
|
|
231
|
+
end
|
|
232
|
+
super
|
|
233
|
+
validate
|
|
134
234
|
end
|
|
235
|
+
|
|
236
|
+
def send_data(imap, tag) = data.each do _1.send_data(imap, tag) end
|
|
237
|
+
|
|
238
|
+
def validate
|
|
239
|
+
return unless data.last in RawText(data: text)
|
|
240
|
+
if text.rindex(/\{\d+\+?\}\z/n)
|
|
241
|
+
raise DataFormatError, "RawData cannot end with literal continuation"
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
# Splits an input +string+ into an array of RawText and Literal/Literal8.
|
|
246
|
+
#
|
|
247
|
+
# NOTE: unlike RawData#validate, this does not prevent the final RawText
|
|
248
|
+
# from ending with a literal prefix.
|
|
249
|
+
def self.split(data)
|
|
250
|
+
data = data.b # dups and ensures BINARY encoding
|
|
251
|
+
parts = []
|
|
252
|
+
while data.match(/(~)?\{(0|[1-9]\d*)(\+)?\}\r\n/n)
|
|
253
|
+
text, binary, bytesize, non_sync, data = $`, !!$1, $2, !!$3, $'
|
|
254
|
+
bytesize = NumValidator.coerce_number64 bytesize
|
|
255
|
+
parts << RawText[text] unless text.empty?
|
|
256
|
+
parts << extract_literal(data, binary:, bytesize:, non_sync:)
|
|
257
|
+
data.bytesplice(0, bytesize, "")
|
|
258
|
+
end
|
|
259
|
+
parts << RawText[data] unless data.empty?
|
|
260
|
+
parts
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
def self.extract_literal(data, binary:, bytesize:, non_sync:)
|
|
264
|
+
if data.bytesize < bytesize
|
|
265
|
+
raise DataFormatError, "Too few bytes in string for literal, " \
|
|
266
|
+
"expected: %s, remaining: %s" % [bytesize, data.bytesize]
|
|
267
|
+
end
|
|
268
|
+
literal = data.byteslice(0, bytesize)
|
|
269
|
+
(binary ? Literal8 : Literal).new(data: literal, non_sync:)
|
|
270
|
+
end
|
|
271
|
+
private_class_method :extract_literal
|
|
135
272
|
end
|
|
136
273
|
|
|
137
274
|
class Atom < CommandData # :nodoc:
|
|
275
|
+
def initialize(**)
|
|
276
|
+
super
|
|
277
|
+
validate
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
def validate
|
|
281
|
+
data.to_s.ascii_only? \
|
|
282
|
+
or raise DataFormatError, "#{self.class} must be ASCII only"
|
|
283
|
+
data.match?(ResponseParser::Patterns::ATOM_SPECIALS) \
|
|
284
|
+
and raise DataFormatError, "#{self.class} must not contain atom-specials"
|
|
285
|
+
data.empty? \
|
|
286
|
+
and raise DataFormatError, "#{self.class} must not be empty"
|
|
287
|
+
end
|
|
288
|
+
|
|
138
289
|
def send_data(imap, tag)
|
|
139
|
-
imap.__send__(:put_string, data)
|
|
290
|
+
imap.__send__(:put_string, data.to_s)
|
|
140
291
|
end
|
|
141
292
|
end
|
|
142
293
|
|
|
143
|
-
class
|
|
294
|
+
class Flag < Atom # :nodoc:
|
|
144
295
|
def send_data(imap, tag)
|
|
145
|
-
imap.__send__(:
|
|
296
|
+
imap.__send__(:put_string, "\\#{data}")
|
|
146
297
|
end
|
|
147
298
|
end
|
|
148
299
|
|
|
149
|
-
|
|
300
|
+
# Represents a IMAP +quoted+ string, which can encode any valid ASCII or
|
|
301
|
+
# UTF-8 string, unless it contains any +CR+, +LF+, or +NULL+ bytes.
|
|
302
|
+
#
|
|
303
|
+
# NOTE: The current implementation does not verify that the connection
|
|
304
|
+
# supports UTF-8. Future versions may validate this.
|
|
305
|
+
class QuotedString < ValidNonLiteralData # :nodoc:
|
|
306
|
+
def formatted = %("#{data.gsub(/["\\]/, "\\\\\\&")}")
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
class Literal < Data.define(:data, :non_sync) # :nodoc:
|
|
310
|
+
def self.validate(...)
|
|
311
|
+
data = new(...)
|
|
312
|
+
data.validate
|
|
313
|
+
data
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def initialize(data:, non_sync: nil)
|
|
317
|
+
data = -String(data.to_str).b or
|
|
318
|
+
raise DataFormatError, "#{self.class} expects string input"
|
|
319
|
+
super
|
|
320
|
+
validate
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
def bytesize = data.bytesize
|
|
324
|
+
|
|
325
|
+
def validate
|
|
326
|
+
if data.include?("\0")
|
|
327
|
+
raise DataFormatError, "NULL byte not allowed in #{self.class}. " \
|
|
328
|
+
"Use #{Literal8} or a null-safe encoding."
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
150
332
|
def send_data(imap, tag)
|
|
151
|
-
imap.__send__(:send_literal, data, tag)
|
|
333
|
+
imap.__send__(:send_literal, data, tag, non_sync:)
|
|
334
|
+
end
|
|
335
|
+
end
|
|
336
|
+
|
|
337
|
+
class Literal8 < Literal # :nodoc:
|
|
338
|
+
def validate = nil # all bytes are okay
|
|
339
|
+
|
|
340
|
+
def send_data(imap, tag)
|
|
341
|
+
imap.__send__(:send_binary_literal, data, tag, non_sync:)
|
|
152
342
|
end
|
|
153
343
|
end
|
|
154
344
|
|
|
@@ -221,6 +411,14 @@ module Net
|
|
|
221
411
|
|
|
222
412
|
module_function
|
|
223
413
|
|
|
414
|
+
def literal_or_literal8(input, name: "argument")
|
|
415
|
+
return input if input in Literal | Literal8
|
|
416
|
+
data = String.try_convert(input) \
|
|
417
|
+
or raise TypeError, "expected #{name} to be String, got #{input.class}"
|
|
418
|
+
type = data.include?("\0") ? Literal8 : Literal
|
|
419
|
+
type.new(data:)
|
|
420
|
+
end
|
|
421
|
+
|
|
224
422
|
# Allows symbols in addition to strings
|
|
225
423
|
def valid_string?(str)
|
|
226
424
|
str.is_a?(Symbol) || str.respond_to?(:to_str)
|
data/lib/net/imap/config.rb
CHANGED
|
@@ -281,6 +281,40 @@ module Net
|
|
|
281
281
|
0.5r => true,
|
|
282
282
|
}
|
|
283
283
|
|
|
284
|
+
# The maximum bytesize for sending non-synchronizing literals, when the
|
|
285
|
+
# server supports them. To disable non-synchronizing literals, set the
|
|
286
|
+
# value to +-1+.
|
|
287
|
+
#
|
|
288
|
+
# Non-synchronizing literals are only sent when the server's
|
|
289
|
+
# capabilities[rdoc-ref:IMAP#capabilities] have been
|
|
290
|
+
# cached[rdoc-ref:IMAP#capabilities_cached?] and include either
|
|
291
|
+
# <tt>LITERAL+</tt> [RFC7888[https://www.rfc-editor.org/rfc/rfc7888]],
|
|
292
|
+
# <tt>LITERAL-</tt> [RFC7888[https://www.rfc-editor.org/rfc/rfc7888]], or
|
|
293
|
+
# +IMAP4rev2+ [RFC9051[https://www.rfc-editor.org/rfc/rfc9051]].
|
|
294
|
+
#
|
|
295
|
+
# For <tt>LITERAL+</tt>, this value is the only limit on whether a literal
|
|
296
|
+
# value is sent as non-synchronizing literals. For <tt>LITERAL-</tt> and
|
|
297
|
+
# <tt>IMAP4rev2</tt>, non-synchronizing literals must also be smaller than
|
|
298
|
+
# +4096+ bytes.
|
|
299
|
+
#
|
|
300
|
+
# Non-synchronizing literals avoid the latency of waiting for the server
|
|
301
|
+
# to allow continuation. However, if a client sends a non-synchronizing
|
|
302
|
+
# literal that is too large for the server, the server may need to close
|
|
303
|
+
# the connection. Because <tt>LITERAL+</tt> does not directly indicate
|
|
304
|
+
# the server's limits, it's best to avoid sending very large
|
|
305
|
+
# non-synchronized literals.
|
|
306
|
+
#
|
|
307
|
+
# ==== Versioned Defaults
|
|
308
|
+
#
|
|
309
|
+
# max_non_synchronizing_literal <em>was added in +v0.6.4+.</em>
|
|
310
|
+
#
|
|
311
|
+
# * original: +-1+ (_never_ send non-synchronizing literals)
|
|
312
|
+
# * +0.6+: 16 KiB
|
|
313
|
+
attr_accessor :max_non_synchronizing_literal, type: Integer, defaults: {
|
|
314
|
+
0.0r => -1,
|
|
315
|
+
0.6r => 16 << 16, # 16 KiB
|
|
316
|
+
}
|
|
317
|
+
|
|
284
318
|
# The maximum allowed server response size. When +nil+, there is no limit
|
|
285
319
|
# on response size.
|
|
286
320
|
#
|
|
@@ -155,7 +155,8 @@ module Net
|
|
|
155
155
|
|
|
156
156
|
# Common validators of number and nz_number types
|
|
157
157
|
module NumValidator # :nodoc
|
|
158
|
-
NUMBER_RE = /\A
|
|
158
|
+
NUMBER_RE = /\A\d+\z/
|
|
159
|
+
NZ_NUMBER_RE = /\A[1-9]\d*\z/
|
|
159
160
|
module_function
|
|
160
161
|
|
|
161
162
|
# Check if argument is a valid 'number' according to RFC 3501
|
|
@@ -174,6 +175,22 @@ module Net
|
|
|
174
175
|
0 < num && num <= 0xffff_ffff
|
|
175
176
|
end
|
|
176
177
|
|
|
178
|
+
# Check if argument is a valid 'number64' according to RFC 9051
|
|
179
|
+
# number64 = 1*DIGIT
|
|
180
|
+
# ; Unsigned 63-bit integer
|
|
181
|
+
# ; (0 <= n <= 9,223,372,036,854,775,807)
|
|
182
|
+
def valid_number64?(num)
|
|
183
|
+
0 <= num && num <= 0x7fff_ffff_ffff_ffff
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
# Check if argument is a valid 'number64' according to RFC 9051
|
|
187
|
+
# nz-number64 = digit-nz *DIGIT
|
|
188
|
+
# ; Unsigned 63-bit integer
|
|
189
|
+
# ; (0 < n <= 9,223,372,036,854,775,807)
|
|
190
|
+
def valid_nz_number64?(num)
|
|
191
|
+
0 < num && num <= 0x7fff_ffff_ffff_ffff
|
|
192
|
+
end
|
|
193
|
+
|
|
177
194
|
# Check if argument is a valid 'mod-sequence-value' according to RFC 4551
|
|
178
195
|
# mod-sequence-value = 1*DIGIT
|
|
179
196
|
# ; Positive unsigned 64-bit integer
|
|
@@ -203,6 +220,20 @@ module Net
|
|
|
203
220
|
"nz-number must be non-zero unsigned 32-bit integer: #{num}"
|
|
204
221
|
end
|
|
205
222
|
|
|
223
|
+
# Ensure argument is 'number64' or raise DataFormatError
|
|
224
|
+
def ensure_number64(num)
|
|
225
|
+
return num if valid_number64?(num)
|
|
226
|
+
raise DataFormatError,
|
|
227
|
+
"number64 must be unsigned 63-bit integer: #{num}"
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Ensure argument is 'nz-number64' or raise DataFormatError
|
|
231
|
+
def ensure_nz_number64(num)
|
|
232
|
+
return num if valid_nz_number64?(num)
|
|
233
|
+
raise DataFormatError,
|
|
234
|
+
"nz-number64 must be non-zero unsigned 63-bit integer: #{num}"
|
|
235
|
+
end
|
|
236
|
+
|
|
206
237
|
# Ensure argument is 'mod-sequence-value' or raise DataFormatError
|
|
207
238
|
def ensure_mod_sequence_value(num)
|
|
208
239
|
return num if valid_mod_sequence_value?(num)
|
|
@@ -221,7 +252,7 @@ module Net
|
|
|
221
252
|
def coerce_number(num)
|
|
222
253
|
case num
|
|
223
254
|
when Integer then ensure_number num
|
|
224
|
-
when NUMBER_RE then ensure_number
|
|
255
|
+
when NUMBER_RE then ensure_number num.to_i
|
|
225
256
|
else
|
|
226
257
|
raise DataFormatError, "%p is not a valid number" % [num]
|
|
227
258
|
end
|
|
@@ -230,18 +261,38 @@ module Net
|
|
|
230
261
|
# Like #ensure_nz_number, but usable with numeric String input.
|
|
231
262
|
def coerce_nz_number(num)
|
|
232
263
|
case num
|
|
233
|
-
when Integer
|
|
234
|
-
when
|
|
264
|
+
when Integer then ensure_nz_number num
|
|
265
|
+
when NZ_NUMBER_RE then ensure_nz_number num.to_i
|
|
235
266
|
else
|
|
236
267
|
raise DataFormatError, "%p is not a valid nz-number" % [num]
|
|
237
268
|
end
|
|
238
269
|
end
|
|
239
270
|
|
|
271
|
+
# Like #ensure_number64, but usable with numeric String input.
|
|
272
|
+
def coerce_number64(num)
|
|
273
|
+
case num
|
|
274
|
+
when Integer then ensure_number64 num
|
|
275
|
+
when NUMBER_RE then ensure_number64 num.to_i
|
|
276
|
+
else
|
|
277
|
+
raise DataFormatError, "%p is not a valid number64" % [num]
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
# Like #ensure_nz_number64, but usable with numeric String input.
|
|
282
|
+
def coerce_nz_number64(num)
|
|
283
|
+
case num
|
|
284
|
+
when Integer then ensure_nz_number64 num
|
|
285
|
+
when NZ_NUMBER_RE then ensure_nz_number64 num.to_i
|
|
286
|
+
else
|
|
287
|
+
raise DataFormatError, "%p is not a valid nz-number64" % [num]
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
240
291
|
# Like #ensure_mod_sequence_value, but usable with numeric String input.
|
|
241
292
|
def coerce_mod_sequence_value(num)
|
|
242
293
|
case num
|
|
243
294
|
when Integer then ensure_mod_sequence_value num
|
|
244
|
-
when NUMBER_RE then ensure_mod_sequence_value
|
|
295
|
+
when NUMBER_RE then ensure_mod_sequence_value num.to_i
|
|
245
296
|
else
|
|
246
297
|
raise DataFormatError, "%p is not a valid mod-sequence-value" % [num]
|
|
247
298
|
end
|
|
@@ -251,7 +302,7 @@ module Net
|
|
|
251
302
|
def coerce_mod_sequence_valzer(num)
|
|
252
303
|
case num
|
|
253
304
|
when Integer then ensure_mod_sequence_valzer num
|
|
254
|
-
when NUMBER_RE then ensure_mod_sequence_valzer
|
|
305
|
+
when NUMBER_RE then ensure_mod_sequence_valzer num.to_i
|
|
255
306
|
else
|
|
256
307
|
raise DataFormatError, "%p is not a valid mod-sequence-valzer" % [num]
|
|
257
308
|
end
|
data/lib/net/imap/errors.rb
CHANGED
|
@@ -172,18 +172,11 @@ module Net
|
|
|
172
172
|
]
|
|
173
173
|
end
|
|
174
174
|
if parser_backtrace
|
|
175
|
-
|
|
176
|
-
next if loc.base_label.include? "parse_error"
|
|
177
|
-
break if loc.base_label == "parse"
|
|
178
|
-
if loc.label.include?("#") # => Class#method, since ruby 3.4
|
|
179
|
-
next unless loc.label&.include?(parser_class.name)
|
|
180
|
-
else
|
|
181
|
-
next unless loc.path&.include?("net/imap/response_parser")
|
|
182
|
-
end
|
|
175
|
+
normalized_parser_backtrace.each do |idx, path, lineno, label, base_label|
|
|
183
176
|
msg << "\n %s: %s (%s:%d)" % [
|
|
184
177
|
hl["%{key}caller[%{/key}%{idx}%%2d%{/idx}%{key}]%{/key}"] % idx,
|
|
185
|
-
hl["%{label}%%-30s%{/label}"] %
|
|
186
|
-
File.basename(
|
|
178
|
+
hl["%{label}%%-30s%{/label}"] % base_label,
|
|
179
|
+
File.basename(path, ".rb"), lineno
|
|
187
180
|
]
|
|
188
181
|
end
|
|
189
182
|
end
|
|
@@ -198,12 +191,56 @@ module Net
|
|
|
198
191
|
def processed_string = string && pos && string[...pos]
|
|
199
192
|
def remaining_string = string && pos && string[pos..]
|
|
200
193
|
|
|
194
|
+
# Returns true when all attributes are equal, except for #backtrace and
|
|
195
|
+
# #backtrace_locations which are replaced with #parser_methods. This
|
|
196
|
+
# allows deserialized errors to be compared.
|
|
197
|
+
def ==(other)
|
|
198
|
+
return false if self.class != other.class
|
|
199
|
+
methods = parser_methods
|
|
200
|
+
other_methods = other.parser_methods
|
|
201
|
+
message == other.message &&
|
|
202
|
+
methods == other_methods &&
|
|
203
|
+
string == other.string &&
|
|
204
|
+
pos == other.pos &&
|
|
205
|
+
lex_state == other.lex_state &&
|
|
206
|
+
token == other.token
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Lists the methods (from #backtrace_locations or #backtrace) called on
|
|
210
|
+
# parser_class (since ruby 3.4) or which have "net/imap/response_parser"
|
|
211
|
+
# in the path (before ruby 3.4). Most parser method names are based on
|
|
212
|
+
# rules in the IMAP grammar.
|
|
213
|
+
def parser_methods = normalized_parser_backtrace.map(&:last)
|
|
214
|
+
|
|
201
215
|
private
|
|
202
216
|
|
|
217
|
+
def normalized_parser_backtrace
|
|
218
|
+
normalize_backtrace
|
|
219
|
+
.take_while {|_, _, _, _, base_label| base_label != "parse" }
|
|
220
|
+
.reject {|_, _, _, _, base_label| base_label.nil? }
|
|
221
|
+
.reject {|_, _, _, _, base_label| base_label.include? "parse_error" }
|
|
222
|
+
.select {|_, path, _, label, _|
|
|
223
|
+
if label.include?("#") # => Class#method, since ruby 3.4
|
|
224
|
+
label.include?(parser_class.name)
|
|
225
|
+
else
|
|
226
|
+
path.include?("net/imap/response_parser")
|
|
227
|
+
end
|
|
228
|
+
}
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
def normalize_backtrace
|
|
232
|
+
(backtrace_locations&.each_with_index&.map {|loc, idx|
|
|
233
|
+
[idx, loc.path, loc.lineno, loc.label, loc.base_label]
|
|
234
|
+
} || backtrace&.each_with_index&.map {|bt, idx|
|
|
235
|
+
[idx, *bt.match(/\A(\S+):(\d+):in [`'](.*?([\w]+[?!]?))'\z/)&.captures]
|
|
236
|
+
} || [])
|
|
237
|
+
end
|
|
238
|
+
|
|
203
239
|
def default_highlight_from_env
|
|
204
240
|
(ENV["FORCE_COLOR"] || "") !~ /\A(?:0|)\z/ ||
|
|
205
241
|
(ENV["TERM"] || "") !~ /\A(?:dumb|unknown|)\z/i
|
|
206
242
|
end
|
|
243
|
+
|
|
207
244
|
end
|
|
208
245
|
|
|
209
246
|
# Superclass of all errors used to encapsulate "fail" responses
|