net-imap 0.6.4 → 0.6.5
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/.simplecov +39 -0
- data/Gemfile +1 -3
- data/Rakefile +11 -0
- data/lib/net/imap/command_data.rb +127 -60
- data/lib/net/imap/config.rb +1 -1
- data/lib/net/imap/data_encoding.rb +10 -9
- data/lib/net/imap/errors.rb +66 -1
- data/lib/net/imap/response_parser.rb +12 -12
- data/lib/net/imap/response_reader.rb +6 -1
- data/lib/net/imap.rb +239 -130
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cd2bbaf7d6564739894405f3066c772976b48062ea2c696b741d10aa34ebbed6
|
|
4
|
+
data.tar.gz: 1fa2b00d9869e60e44be3a3aea1e058f0974705aef5a41a62f4ea25ee4fda48d
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: dc1083920d0f196a21db61ef8282be1aa214e43b38ba18bf5ada58fa16d9b007f2169332fc0df2e5799e2ec1e76f15b1469d0c2401e851feb0409237b7f60ee2
|
|
7
|
+
data.tar.gz: 4b9c9ba8892b67ec86c39f4ad3f28c5dd91ad785def8b9324175a0f6bac6940dffde3499135f045a911dde98646afff687c2bd6381117f14bdf68de244f03dea
|
data/.simplecov
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
SimpleCov.configure do
|
|
4
|
+
formatter SimpleCov::Formatter::HTMLFormatter
|
|
5
|
+
|
|
6
|
+
enable_coverage :branch
|
|
7
|
+
enable_coverage :method
|
|
8
|
+
enable_coverage :eval
|
|
9
|
+
|
|
10
|
+
skip "/test/"
|
|
11
|
+
skip "/rakelib/"
|
|
12
|
+
cover "lib/**/*.rb"
|
|
13
|
+
|
|
14
|
+
group "Parser", %w[lib/net/imap/response_parser.rb
|
|
15
|
+
lib/net/imap/response_parser]
|
|
16
|
+
|
|
17
|
+
group "Client", %w[lib/net/imap.rb
|
|
18
|
+
lib/net/imap/connection_state.rb
|
|
19
|
+
lib/net/imap/deprecated_client_options.rb
|
|
20
|
+
lib/net/imap/errors.rb
|
|
21
|
+
lib/net/imap/response_reader.rb]
|
|
22
|
+
|
|
23
|
+
group "Config", %w[lib/net/imap/config.rb
|
|
24
|
+
lib/net/imap/config]
|
|
25
|
+
|
|
26
|
+
group "Data Types", %w[lib/net/imap/flags.rb
|
|
27
|
+
lib/net/imap/sequence_set.rb] +
|
|
28
|
+
[%r{lib/net/imap/\w*_data\.rb},
|
|
29
|
+
%r{lib/net/imap/\w*_result.rb},
|
|
30
|
+
%r{lib/net/imap/data_\w+\.rb}]
|
|
31
|
+
|
|
32
|
+
group "SASL", %w[lib/net/imap/sasl.rb
|
|
33
|
+
lib/net/imap/sasl
|
|
34
|
+
lib/net/imap/sasl_adapter.rb
|
|
35
|
+
lib/net/imap/authenticators.rb]
|
|
36
|
+
|
|
37
|
+
group "StringPrep", %w[lib/net/imap/stringprep.rb
|
|
38
|
+
lib/net/imap/stringprep]
|
|
39
|
+
end
|
data/Gemfile
CHANGED
|
@@ -20,7 +20,5 @@ gem "benchmark-driver", require: false
|
|
|
20
20
|
gem "vernier", require: false, platform: :mri
|
|
21
21
|
|
|
22
22
|
group :test do
|
|
23
|
-
gem "simplecov",
|
|
24
|
-
gem "simplecov-html", require: false, platforms: %i[mri windows]
|
|
25
|
-
gem "simplecov-json", require: false, platforms: %i[mri windows]
|
|
23
|
+
gem "simplecov", ">= 1.0.0", require: false, platforms: %i[mri windows]
|
|
26
24
|
end
|
data/Rakefile
CHANGED
|
@@ -11,3 +11,14 @@ Rake::TestTask.new(:test) do |t|
|
|
|
11
11
|
end
|
|
12
12
|
|
|
13
13
|
task :default => :test
|
|
14
|
+
|
|
15
|
+
desc "Output coverage data report, and error when threshholds aren't met"
|
|
16
|
+
task "coverage:report" do
|
|
17
|
+
require "simplecov"
|
|
18
|
+
SimpleCov.collate "coverage/.resultset.json" do
|
|
19
|
+
coverage(:line) do
|
|
20
|
+
minimum 90
|
|
21
|
+
minimum_per_file 40
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -15,15 +15,19 @@ module Net
|
|
|
15
15
|
case data
|
|
16
16
|
when nil
|
|
17
17
|
when String
|
|
18
|
+
if data.include?("\0")
|
|
19
|
+
raise DataFormatError, "String argument contains NULL byte"
|
|
20
|
+
end
|
|
18
21
|
when Integer
|
|
19
|
-
|
|
22
|
+
# Covers modseq-valzer, which is the largest valid IMAP integer
|
|
23
|
+
if data.negative?
|
|
24
|
+
raise DataFormatError, "Integer argument must be unsigned: #{data}"
|
|
25
|
+
elsif 0xffff_ffff_ffff_ffff < data
|
|
26
|
+
raise DataFormatError, "Integer argument must fit in 64 bits: #{data}"
|
|
27
|
+
end
|
|
20
28
|
when Array
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
else
|
|
24
|
-
data.each do |i|
|
|
25
|
-
validate_data(i)
|
|
26
|
-
end
|
|
29
|
+
data.each do |i|
|
|
30
|
+
validate_data(i)
|
|
27
31
|
end
|
|
28
32
|
when Time, Date, DateTime
|
|
29
33
|
when Symbol
|
|
@@ -54,45 +58,62 @@ module Net
|
|
|
54
58
|
end
|
|
55
59
|
end
|
|
56
60
|
|
|
61
|
+
UNQUOTABLE_CHARS = /\0\r\n/
|
|
62
|
+
ASTRING_SPECIALS = /[(){ \x00-\x1f\x7f%*"\\]/
|
|
63
|
+
private_constant :UNQUOTABLE_CHARS, :ASTRING_SPECIALS
|
|
64
|
+
|
|
65
|
+
# Sends generic strings formatted as +astring+:
|
|
66
|
+
# * as an atom (note: this is based on +ASTRING-CHAR+, not +ATOM-CHAR+)
|
|
67
|
+
# * as a quoted string
|
|
68
|
+
# * as a literal (may send non-synchronizing)
|
|
69
|
+
#
|
|
70
|
+
# NOTE: This does not validate that +str+ contains no +NULL+ bytes.
|
|
57
71
|
def send_string_data(str, tag = nil)
|
|
58
72
|
if str.empty?
|
|
73
|
+
# same as send_quoted_string(str), but incompatible encoding is allowed
|
|
59
74
|
put_string('""')
|
|
60
|
-
elsif str.match?(
|
|
61
|
-
#
|
|
62
|
-
|
|
63
|
-
elsif !str.
|
|
64
|
-
if @utf8_strings
|
|
65
|
-
# quoted string
|
|
66
|
-
send_quoted_string(str)
|
|
67
|
-
else
|
|
68
|
-
# literal, because of non-ASCII bytes
|
|
69
|
-
send_literal(str, tag)
|
|
70
|
-
end
|
|
71
|
-
elsif str.match?(/[(){ \x00-\x1f\x7f%*"\\]/n)
|
|
72
|
-
# quoted string
|
|
75
|
+
elsif str.ascii_only? && !str.match?(ASTRING_SPECIALS)
|
|
76
|
+
# valid +astring+ atom: non-empty, ASCII only, no ASTRING_SPECIALS
|
|
77
|
+
put_string(str)
|
|
78
|
+
elsif text_encodable?(str) && !str.match?(UNQUOTABLE_CHARS)
|
|
73
79
|
send_quoted_string(str)
|
|
74
80
|
else
|
|
75
|
-
|
|
81
|
+
send_literal(str, tag)
|
|
76
82
|
end
|
|
77
83
|
end
|
|
78
84
|
|
|
79
|
-
|
|
80
|
-
|
|
85
|
+
# Encodable as +text+ (which is a superset of +quoted+):
|
|
86
|
+
# * ASCII only (for any ASCII compatible encoding)
|
|
87
|
+
# * or valid UTF-8 (when the connection supports it)
|
|
88
|
+
def text_encodable?(str)
|
|
89
|
+
str.ascii_only? || (utf8_enabled? &&
|
|
90
|
+
str.encoding == Encoding::UTF_8 &&
|
|
91
|
+
str.valid_encoding?)
|
|
81
92
|
end
|
|
82
93
|
|
|
94
|
+
def send_quoted_string(str) = QuotedString.new(data: str).send_data(self)
|
|
95
|
+
|
|
83
96
|
def send_binary_literal(*, **) = send_literal(*, **, binary: true)
|
|
84
97
|
|
|
85
98
|
# `non_sync` is an optional tri-state flag:
|
|
86
99
|
# * `true` -> Force non-synchronizing `LITERAL+`/`LITERAL-` behavior.
|
|
87
|
-
#
|
|
100
|
+
# NOTE: raises DataFormatError when server doesn't support
|
|
101
|
+
# non-synchronizing literal, or literal is too large for LITERAL-.
|
|
88
102
|
# * `false` -> Force normal synchronizing literal behavior.
|
|
89
103
|
# * `nil` -> (default) Currently behaves like `false` (will be dynamic).
|
|
90
104
|
def send_literal(str, tag = nil, binary: false, non_sync: nil)
|
|
105
|
+
bytesize = str.bytesize
|
|
91
106
|
synchronize do
|
|
92
|
-
non_sync
|
|
107
|
+
if non_sync && !non_sync_literal_allowed?(bytesize)
|
|
108
|
+
# TODO: check in Printer, so we don't need to close the connection.
|
|
109
|
+
@sock.close
|
|
110
|
+
raise DataFormatError, "Connection closed: " \
|
|
111
|
+
"Cannot send non-synchronizing literal without known server support"
|
|
112
|
+
end
|
|
113
|
+
non_sync = non_sync_literal?(bytesize) if non_sync.nil?
|
|
93
114
|
prefix = "~" if binary
|
|
94
115
|
plus = "+" if non_sync
|
|
95
|
-
put_string("#{prefix}{#{
|
|
116
|
+
put_string("#{prefix}{#{bytesize}#{plus}}\r\n")
|
|
96
117
|
if non_sync
|
|
97
118
|
put_string(str)
|
|
98
119
|
return
|
|
@@ -101,8 +122,7 @@ module Net
|
|
|
101
122
|
@continuation_request_exception = nil
|
|
102
123
|
begin
|
|
103
124
|
@continuation_request_arrival.wait
|
|
104
|
-
|
|
105
|
-
raise e if e
|
|
125
|
+
reraise @continuation_request_exception || @exception
|
|
106
126
|
put_string(str)
|
|
107
127
|
ensure
|
|
108
128
|
@continued_command_tag = nil
|
|
@@ -112,14 +132,22 @@ module Net
|
|
|
112
132
|
end
|
|
113
133
|
|
|
114
134
|
def non_sync_literal?(bytesize)
|
|
115
|
-
|
|
116
|
-
bytesize
|
|
117
|
-
|
|
118
|
-
|
|
135
|
+
bytesize <= config.max_non_synchronizing_literal \
|
|
136
|
+
&& non_sync_literal_allowed?(bytesize)
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def non_sync_literal_allowed?(bytesize)
|
|
140
|
+
return unless capabilities_cached?
|
|
141
|
+
return "+" if capable?("LITERAL+")
|
|
142
|
+
return "-" if capable_literal_minus? && bytesize <= 4096
|
|
143
|
+
false
|
|
119
144
|
end
|
|
120
145
|
|
|
146
|
+
def capable_literal_minus? = capable?("LITERAL-") || capable?("IMAP4rev2")
|
|
147
|
+
|
|
148
|
+
# NOTE: +num+ should already be an Integer
|
|
121
149
|
def send_number_data(num)
|
|
122
|
-
put_string(num.to_s)
|
|
150
|
+
put_string(Integer(num).to_s)
|
|
123
151
|
end
|
|
124
152
|
|
|
125
153
|
def send_list_data(list, tag = nil)
|
|
@@ -139,6 +167,9 @@ module Net
|
|
|
139
167
|
def send_date_data(date) put_string Net::IMAP.encode_date(date) end
|
|
140
168
|
def send_time_data(time) put_string Net::IMAP.encode_time(time) end
|
|
141
169
|
|
|
170
|
+
# NOTE: Currently for internal use only. The API is expected to change.
|
|
171
|
+
Command = Data.define(:tag, :name) # :nodoc:
|
|
172
|
+
|
|
142
173
|
CommandData = Data.define(:data) do # :nodoc:
|
|
143
174
|
def self.validate(...)
|
|
144
175
|
data = new(...)
|
|
@@ -154,36 +185,38 @@ module Net
|
|
|
154
185
|
end
|
|
155
186
|
end
|
|
156
187
|
|
|
157
|
-
# Represents IMAP +text+ data, which
|
|
158
|
-
#
|
|
159
|
-
#
|
|
160
|
-
#
|
|
161
|
-
#
|
|
188
|
+
# Represents IMAP +text+ or +quoted+ data, which share the same
|
|
189
|
+
# validations of decoded #data, and differ only in how they are formatted.
|
|
190
|
+
#
|
|
191
|
+
# +data+ may contain any 7-bit ASCII character except +NULL+, +CR+, or +LF+.
|
|
192
|
+
# Any multibyte +UTF-8+ character is also allowed when the connection
|
|
193
|
+
# supports UTF8: either +UTF8=ACCEPT+ or +IMAP4rev2+ have been enabled, or
|
|
194
|
+
# the server supports only +IMAP4rev2+ and not earlier IMAP revisions, or
|
|
195
|
+
# the server advertises +UTF8=ONLY+.
|
|
162
196
|
#
|
|
163
|
-
# NOTE:
|
|
164
|
-
#
|
|
197
|
+
# NOTE: This does not verify whether the connection supports UTF-8, but that
|
|
198
|
+
# may change in future versions.
|
|
165
199
|
#
|
|
166
200
|
# The string's bytes must be valid ASCII or valid UTF-8. The string's
|
|
167
201
|
# reported encoding is ignored, but the string is _not_ transcoded.
|
|
168
|
-
class
|
|
202
|
+
class ValidNonLiteralData < CommandData
|
|
169
203
|
def initialize(data:)
|
|
170
204
|
data = String(data.to_str)
|
|
171
|
-
|
|
172
|
-
-
|
|
173
|
-
elsif data.ascii_only?
|
|
174
|
-
-(data.dup.force_encoding("ASCII"))
|
|
175
|
-
else
|
|
176
|
-
-(data.dup.force_encoding("UTF-8"))
|
|
205
|
+
unless data.encoding in Encoding::ASCII | Encoding::UTF_8
|
|
206
|
+
data = data.dup.force_encoding(data.ascii_only? ? "ASCII" : "UTF-8")
|
|
177
207
|
end
|
|
208
|
+
data = -data
|
|
178
209
|
super
|
|
179
210
|
validate
|
|
180
211
|
end
|
|
181
212
|
|
|
182
213
|
def validate
|
|
183
|
-
if data.
|
|
184
|
-
raise DataFormatError, "
|
|
214
|
+
if !(data.encoding in Encoding::ASCII | Encoding::UTF_8)
|
|
215
|
+
raise DataFormatError, "must use ASCII or UTF-8 encoding"
|
|
185
216
|
elsif !data.valid_encoding?
|
|
186
217
|
raise DataFormatError, "invalid UTF-8 must be literal encoded"
|
|
218
|
+
elsif data.include?("\0")
|
|
219
|
+
raise DataFormatError, "NULL byte must be binary literal encoded"
|
|
187
220
|
elsif /[\r\n]/.match?(data)
|
|
188
221
|
raise DataFormatError, "CR and LF bytes must be literal encoded"
|
|
189
222
|
end
|
|
@@ -191,12 +224,27 @@ module Net
|
|
|
191
224
|
|
|
192
225
|
def ascii_only? = data.ascii_only?
|
|
193
226
|
|
|
194
|
-
def send_data(imap, tag) = imap.__send__(:put_string,
|
|
227
|
+
def send_data(imap, tag = nil) = imap.__send__(:put_string, formatted)
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Represents IMAP +text+ data, which covers everything in the IMAP grammar,
|
|
231
|
+
# except for +literal+, +literal8+, and the concluding +CRLF+.
|
|
232
|
+
#
|
|
233
|
+
# NOTE: The current implementation does not verify that the connection
|
|
234
|
+
# supports UTF-8. Future versions may validate this.
|
|
235
|
+
class RawText < ValidNonLiteralData # :nodoc:
|
|
236
|
+
# raw: no formatting necessary
|
|
237
|
+
alias formatted data
|
|
195
238
|
end
|
|
196
239
|
|
|
197
240
|
class RawData < CommandData # :nodoc:
|
|
198
241
|
def initialize(data:)
|
|
199
|
-
|
|
242
|
+
case data
|
|
243
|
+
in String then data = self.class.split(data)
|
|
244
|
+
in Array if data.all? { _1 in RawText | Literal }
|
|
245
|
+
else
|
|
246
|
+
raise TypeError, "expected String or Array[#{RawText} | #{Literal}]"
|
|
247
|
+
end
|
|
200
248
|
super
|
|
201
249
|
validate
|
|
202
250
|
end
|
|
@@ -205,14 +253,16 @@ module Net
|
|
|
205
253
|
|
|
206
254
|
def validate
|
|
207
255
|
return unless data.last in RawText(data: text)
|
|
208
|
-
if text.rindex(
|
|
256
|
+
if text.rindex(/\{\d+\+?\}\z/n)
|
|
209
257
|
raise DataFormatError, "RawData cannot end with literal continuation"
|
|
210
258
|
end
|
|
211
259
|
end
|
|
212
260
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
261
|
+
# Splits an input +string+ into an array of RawText and Literal/Literal8.
|
|
262
|
+
#
|
|
263
|
+
# NOTE: unlike RawData#validate, this does not prevent the final RawText
|
|
264
|
+
# from ending with a literal prefix.
|
|
265
|
+
def self.split(data)
|
|
216
266
|
data = data.b # dups and ensures BINARY encoding
|
|
217
267
|
parts = []
|
|
218
268
|
while data.match(/(~)?\{(0|[1-9]\d*)(\+)?\}\r\n/n)
|
|
@@ -226,7 +276,7 @@ module Net
|
|
|
226
276
|
parts
|
|
227
277
|
end
|
|
228
278
|
|
|
229
|
-
def extract_literal(data, binary:, bytesize:, non_sync:)
|
|
279
|
+
def self.extract_literal(data, binary:, bytesize:, non_sync:)
|
|
230
280
|
if data.bytesize < bytesize
|
|
231
281
|
raise DataFormatError, "Too few bytes in string for literal, " \
|
|
232
282
|
"expected: %s, remaining: %s" % [bytesize, data.bytesize]
|
|
@@ -234,8 +284,20 @@ module Net
|
|
|
234
284
|
literal = data.byteslice(0, bytesize)
|
|
235
285
|
(binary ? Literal8 : Literal).new(data: literal, non_sync:)
|
|
236
286
|
end
|
|
287
|
+
private_class_method :extract_literal
|
|
237
288
|
end
|
|
238
289
|
|
|
290
|
+
# Please note that +ATOM-CHAR+ and +atom+ are not the same as +ASTRING-char+
|
|
291
|
+
# and the atom form of +astring+. +astring+ allows <tt>]</tt>, but +atom+
|
|
292
|
+
# does not.
|
|
293
|
+
#
|
|
294
|
+
# Generic string arguments in Net::IMAP use +astring+, so they will send as
|
|
295
|
+
# atoms even if they contain <tt>]</tt>.
|
|
296
|
+
#
|
|
297
|
+
# This class is used by:
|
|
298
|
+
# * to validate generic symbol arguments, as the superclass of Flag
|
|
299
|
+
# * to validate #enable +capabilities+
|
|
300
|
+
# * to validate #store (and #uid_store) +attr+
|
|
239
301
|
class Atom < CommandData # :nodoc:
|
|
240
302
|
def initialize(**)
|
|
241
303
|
super
|
|
@@ -247,6 +309,8 @@ module Net
|
|
|
247
309
|
or raise DataFormatError, "#{self.class} must be ASCII only"
|
|
248
310
|
data.match?(ResponseParser::Patterns::ATOM_SPECIALS) \
|
|
249
311
|
and raise DataFormatError, "#{self.class} must not contain atom-specials"
|
|
312
|
+
data.empty? \
|
|
313
|
+
and raise DataFormatError, "#{self.class} must not be empty"
|
|
250
314
|
end
|
|
251
315
|
|
|
252
316
|
def send_data(imap, tag)
|
|
@@ -260,10 +324,13 @@ module Net
|
|
|
260
324
|
end
|
|
261
325
|
end
|
|
262
326
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
327
|
+
# Represents a IMAP +quoted+ string, which can encode any valid ASCII or
|
|
328
|
+
# UTF-8 string, unless it contains any +CR+, +LF+, or +NULL+ bytes.
|
|
329
|
+
#
|
|
330
|
+
# NOTE: The current implementation does not verify that the connection
|
|
331
|
+
# supports UTF-8. Future versions may validate this.
|
|
332
|
+
class QuotedString < ValidNonLiteralData # :nodoc:
|
|
333
|
+
def formatted = %("#{data.gsub(/["\\]/, "\\\\\\&")}")
|
|
267
334
|
end
|
|
268
335
|
|
|
269
336
|
class Literal < Data.define(:data, :non_sync) # :nodoc:
|
data/lib/net/imap/config.rb
CHANGED
|
@@ -310,7 +310,7 @@ module Net
|
|
|
310
310
|
#
|
|
311
311
|
# * original: +-1+ (_never_ send non-synchronizing literals)
|
|
312
312
|
# * +0.6+: 16 KiB
|
|
313
|
-
attr_accessor :max_non_synchronizing_literal, type: Integer
|
|
313
|
+
attr_accessor :max_non_synchronizing_literal, type: Integer, defaults: {
|
|
314
314
|
0.0r => -1,
|
|
315
315
|
0.6r => 16 << 16, # 16 KiB
|
|
316
316
|
}
|
|
@@ -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
|
|
@@ -251,7 +252,7 @@ module Net
|
|
|
251
252
|
def coerce_number(num)
|
|
252
253
|
case num
|
|
253
254
|
when Integer then ensure_number num
|
|
254
|
-
when NUMBER_RE then ensure_number
|
|
255
|
+
when NUMBER_RE then ensure_number num.to_i
|
|
255
256
|
else
|
|
256
257
|
raise DataFormatError, "%p is not a valid number" % [num]
|
|
257
258
|
end
|
|
@@ -260,8 +261,8 @@ module Net
|
|
|
260
261
|
# Like #ensure_nz_number, but usable with numeric String input.
|
|
261
262
|
def coerce_nz_number(num)
|
|
262
263
|
case num
|
|
263
|
-
when Integer
|
|
264
|
-
when
|
|
264
|
+
when Integer then ensure_nz_number num
|
|
265
|
+
when NZ_NUMBER_RE then ensure_nz_number num.to_i
|
|
265
266
|
else
|
|
266
267
|
raise DataFormatError, "%p is not a valid nz-number" % [num]
|
|
267
268
|
end
|
|
@@ -271,7 +272,7 @@ module Net
|
|
|
271
272
|
def coerce_number64(num)
|
|
272
273
|
case num
|
|
273
274
|
when Integer then ensure_number64 num
|
|
274
|
-
when NUMBER_RE then ensure_number64
|
|
275
|
+
when NUMBER_RE then ensure_number64 num.to_i
|
|
275
276
|
else
|
|
276
277
|
raise DataFormatError, "%p is not a valid number64" % [num]
|
|
277
278
|
end
|
|
@@ -280,8 +281,8 @@ module Net
|
|
|
280
281
|
# Like #ensure_nz_number64, but usable with numeric String input.
|
|
281
282
|
def coerce_nz_number64(num)
|
|
282
283
|
case num
|
|
283
|
-
when Integer
|
|
284
|
-
when
|
|
284
|
+
when Integer then ensure_nz_number64 num
|
|
285
|
+
when NZ_NUMBER_RE then ensure_nz_number64 num.to_i
|
|
285
286
|
else
|
|
286
287
|
raise DataFormatError, "%p is not a valid nz-number64" % [num]
|
|
287
288
|
end
|
|
@@ -291,7 +292,7 @@ module Net
|
|
|
291
292
|
def coerce_mod_sequence_value(num)
|
|
292
293
|
case num
|
|
293
294
|
when Integer then ensure_mod_sequence_value num
|
|
294
|
-
when NUMBER_RE then ensure_mod_sequence_value
|
|
295
|
+
when NUMBER_RE then ensure_mod_sequence_value num.to_i
|
|
295
296
|
else
|
|
296
297
|
raise DataFormatError, "%p is not a valid mod-sequence-value" % [num]
|
|
297
298
|
end
|
|
@@ -301,7 +302,7 @@ module Net
|
|
|
301
302
|
def coerce_mod_sequence_valzer(num)
|
|
302
303
|
case num
|
|
303
304
|
when Integer then ensure_mod_sequence_valzer num
|
|
304
|
-
when NUMBER_RE then ensure_mod_sequence_valzer
|
|
305
|
+
when NUMBER_RE then ensure_mod_sequence_valzer num.to_i
|
|
305
306
|
else
|
|
306
307
|
raise DataFormatError, "%p is not a valid mod-sequence-valzer" % [num]
|
|
307
308
|
end
|
data/lib/net/imap/errors.rb
CHANGED
|
@@ -279,7 +279,11 @@ module Net
|
|
|
279
279
|
#
|
|
280
280
|
# This is different from UnknownResponseError: the response has been
|
|
281
281
|
# rejected. Although it may be parsable, the server is forbidden from
|
|
282
|
-
# sending it in the current context.
|
|
282
|
+
# sending it in the current context.
|
|
283
|
+
#
|
|
284
|
+
# This could be caused by a bug in the server or in Net::IMAP. Or it
|
|
285
|
+
# might indicate a malicious server, a man-in-the-middle attack, or
|
|
286
|
+
# client-side command injection. So the client should automatically
|
|
283
287
|
# disconnect, abruptly (without logout).
|
|
284
288
|
#
|
|
285
289
|
# Note that InvalidResponseError does not inherit from ResponseError: it
|
|
@@ -288,6 +292,67 @@ module Net
|
|
|
288
292
|
class InvalidResponseError < Error
|
|
289
293
|
end
|
|
290
294
|
|
|
295
|
+
# Error raised when the server sends a tagged #response that is invalid for
|
|
296
|
+
# the #command #state.
|
|
297
|
+
#
|
|
298
|
+
# This could be caused by a bug in the server or in Net::IMAP. Or it
|
|
299
|
+
# might indicate a malicious server, a man-in-the-middle attack, or
|
|
300
|
+
# client-side command injection, so the client should disconnect
|
|
301
|
+
# automatically and abruptly (without logout).
|
|
302
|
+
class InvalidTaggedResponseError < InvalidResponseError
|
|
303
|
+
# A symbol representing the state of the matching tagged command.
|
|
304
|
+
#
|
|
305
|
+
# +:unknown+::
|
|
306
|
+
# #response does not match any known command (#command will be +nil+).
|
|
307
|
+
# +:unstarted+::
|
|
308
|
+
# Any tagged #response is invalid before #command starts sending.
|
|
309
|
+
# +:incomplete+::
|
|
310
|
+
# A tagged +OK+ #response is invalid before #command is fully sent.
|
|
311
|
+
# +:completed+::
|
|
312
|
+
# Multiple tagged responses were received for the same command.
|
|
313
|
+
#
|
|
314
|
+
# NOTE: Command state is neither anticipated nor remembered indefinitely.
|
|
315
|
+
# +:unknown+ is used before the matching command is called and after the
|
|
316
|
+
# it is forgotten.
|
|
317
|
+
#
|
|
318
|
+
# NOTE: This version of Net::IMAP does not detect or raise an exception
|
|
319
|
+
# for all of these states.
|
|
320
|
+
attr_reader :state
|
|
321
|
+
|
|
322
|
+
# Metadata about the matching IMAP command.
|
|
323
|
+
#
|
|
324
|
+
# Returns +nil+ when #state is +:unknown+.
|
|
325
|
+
#
|
|
326
|
+
# NOTE: The non-nil return type is an unstable API, for debug only. It
|
|
327
|
+
# may be changed by any release, without warning or deprecation.
|
|
328
|
+
attr_reader :command
|
|
329
|
+
|
|
330
|
+
# The TaggedResponse which triggered this error
|
|
331
|
+
attr_reader :response
|
|
332
|
+
|
|
333
|
+
def initialize(state, response:, command: nil)
|
|
334
|
+
response => TaggedResponse[tag:, name: status]
|
|
335
|
+
case [state, status, command]
|
|
336
|
+
in :unknown, _, nil
|
|
337
|
+
in :incomplete, "OK", {tag: ^tag, name:}
|
|
338
|
+
in :unstarted | :completed, _, {tag: ^tag, name:}
|
|
339
|
+
end
|
|
340
|
+
@state, @command, @response = state, command, response
|
|
341
|
+
cmd_desc = name ? "#{state} #{name}" : state
|
|
342
|
+
super "Received tagged #{status} to #{cmd_desc} command (tag=#{tag})"
|
|
343
|
+
rescue NoMatchingPatternError => err
|
|
344
|
+
raise ArgumentError, err.message
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def detailed_message(**)
|
|
348
|
+
"#{message}.\n" \
|
|
349
|
+
"Disconnecting: This could indicate a malicious server, a " \
|
|
350
|
+
"man-in-the-middle attack, a client-side command injection, or " \
|
|
351
|
+
"a bug in net-imap.\n" \
|
|
352
|
+
"response.data=#{response.data.inspect}"
|
|
353
|
+
end
|
|
354
|
+
end
|
|
355
|
+
|
|
291
356
|
# Error raised upon an unknown response from the server.
|
|
292
357
|
#
|
|
293
358
|
# This is different from InvalidResponseError: the response may be a
|
|
@@ -2212,10 +2212,7 @@ module Net
|
|
|
2212
2212
|
if $1
|
|
2213
2213
|
return Token.new(T_SPACE, $+)
|
|
2214
2214
|
elsif $2
|
|
2215
|
-
|
|
2216
|
-
val = @str[@pos, len]
|
|
2217
|
-
@pos += len
|
|
2218
|
-
return Token.new(T_LITERAL8, val)
|
|
2215
|
+
literal_token($+, T_LITERAL8)
|
|
2219
2216
|
elsif $3 && $7
|
|
2220
2217
|
# greedily match ATOM, prefixed with NUMBER, NIL, or PLUS.
|
|
2221
2218
|
return Token.new(T_ATOM, $3)
|
|
@@ -2243,10 +2240,7 @@ module Net
|
|
|
2243
2240
|
elsif $15
|
|
2244
2241
|
return Token.new(T_RBRA, $+)
|
|
2245
2242
|
elsif $16
|
|
2246
|
-
|
|
2247
|
-
val = @str[@pos, len]
|
|
2248
|
-
@pos += len
|
|
2249
|
-
return Token.new(T_LITERAL, val)
|
|
2243
|
+
literal_token($+)
|
|
2250
2244
|
elsif $17
|
|
2251
2245
|
return Token.new(T_PERCENT, $+)
|
|
2252
2246
|
elsif $18
|
|
@@ -2272,10 +2266,7 @@ module Net
|
|
|
2272
2266
|
elsif $4
|
|
2273
2267
|
return Token.new(T_QUOTED, Patterns.unescape_quoted($+))
|
|
2274
2268
|
elsif $5
|
|
2275
|
-
|
|
2276
|
-
val = @str[@pos, len]
|
|
2277
|
-
@pos += len
|
|
2278
|
-
return Token.new(T_LITERAL, val)
|
|
2269
|
+
literal_token($+)
|
|
2279
2270
|
elsif $6
|
|
2280
2271
|
return Token.new(T_LPAR, $+)
|
|
2281
2272
|
elsif $7
|
|
@@ -2290,6 +2281,15 @@ module Net
|
|
|
2290
2281
|
else
|
|
2291
2282
|
parse_error("invalid @lex_state - %s", @lex_state.inspect)
|
|
2292
2283
|
end
|
|
2284
|
+
rescue DataFormatError => error
|
|
2285
|
+
parse_error error.message
|
|
2286
|
+
end
|
|
2287
|
+
|
|
2288
|
+
def literal_token(len, type = T_LITERAL)
|
|
2289
|
+
len = NumValidator.coerce_number64 len
|
|
2290
|
+
val = @str[@pos, len]
|
|
2291
|
+
@pos += len
|
|
2292
|
+
Token.new(type, val)
|
|
2293
2293
|
end
|
|
2294
2294
|
|
|
2295
2295
|
end
|
|
@@ -4,6 +4,8 @@ module Net
|
|
|
4
4
|
class IMAP
|
|
5
5
|
# See https://www.rfc-editor.org/rfc/rfc9051#section-2.2.2
|
|
6
6
|
class ResponseReader # :nodoc:
|
|
7
|
+
include NumValidator
|
|
8
|
+
|
|
7
9
|
attr_reader :client
|
|
8
10
|
|
|
9
11
|
def initialize(client, sock)
|
|
@@ -46,7 +48,10 @@ module Net
|
|
|
46
48
|
def line_done? = buff.end_with?(CRLF)
|
|
47
49
|
|
|
48
50
|
def get_literal_size(buff)
|
|
49
|
-
buff.end_with?("}\r\n") && buff.rindex(/\{(\d+)\}\r\n\z/n) &&
|
|
51
|
+
buff.end_with?("}\r\n") && buff.rindex(/\{(\d+)\}\r\n\z/n) &&
|
|
52
|
+
coerce_number64($1)
|
|
53
|
+
rescue DataFormatError
|
|
54
|
+
raise DataFormatError, format("invalid response literal size (%s)", $1)
|
|
50
55
|
end
|
|
51
56
|
|
|
52
57
|
def read_line
|
data/lib/net/imap.rb
CHANGED
|
@@ -326,6 +326,11 @@ module Net
|
|
|
326
326
|
#
|
|
327
327
|
# <em>In general, #capable? should be used rather than explicitly sending a
|
|
328
328
|
# +CAPABILITY+ command to the server.</em>
|
|
329
|
+
# - #enable: Enables backwards incompatible server extensions.
|
|
330
|
+
# <em>Requires the +ENABLE+ or +IMAP4rev2+ capability.</em>
|
|
331
|
+
# - #enabled: Returns a set of enabled server extensions.
|
|
332
|
+
# - #enabled?: Returns whether a server extension has been enabled.
|
|
333
|
+
# - #utf8_enabled?: Returns whether UTF-8 string encoding has been enabled.
|
|
329
334
|
#
|
|
330
335
|
# === Handling server responses
|
|
331
336
|
#
|
|
@@ -552,6 +557,7 @@ module Net
|
|
|
552
557
|
# ==== RFC6855: <tt>UTF8=ACCEPT</tt>, <tt>UTF8=ONLY</tt>
|
|
553
558
|
#
|
|
554
559
|
# - See #enable for information about support for UTF-8 string encoding.
|
|
560
|
+
# - #utf8_enabled?: Returns whether UTF-8 string encoding has been enabled.
|
|
555
561
|
#
|
|
556
562
|
# ==== RFC7162: +CONDSTORE+
|
|
557
563
|
#
|
|
@@ -813,7 +819,7 @@ module Net
|
|
|
813
819
|
# * {IMAP URLAUTH Authorization Mechanism Registry}[https://www.iana.org/assignments/urlauth-authorization-mechanism-registry/urlauth-authorization-mechanism-registry.xhtml]
|
|
814
820
|
#
|
|
815
821
|
class IMAP < Protocol
|
|
816
|
-
VERSION = "0.6.
|
|
822
|
+
VERSION = "0.6.5"
|
|
817
823
|
|
|
818
824
|
# Aliases for supported capabilities, to be used with the #enable command.
|
|
819
825
|
ENABLE_ALIASES = {
|
|
@@ -821,6 +827,11 @@ module Net
|
|
|
821
827
|
"UTF8=ONLY" => "UTF8=ACCEPT",
|
|
822
828
|
}.freeze
|
|
823
829
|
|
|
830
|
+
# Aliases for supported capabilities, to be used with #enabled?.
|
|
831
|
+
ENABLED_ALIASES = {
|
|
832
|
+
utf8: Set.new(%w[UTF8=ACCEPT IMAP4REV2]).freeze,
|
|
833
|
+
}.freeze
|
|
834
|
+
|
|
824
835
|
dir = File.expand_path("imap", __dir__)
|
|
825
836
|
autoload :ConnectionState, "#{dir}/connection_state"
|
|
826
837
|
autoload :ResponseReader, "#{dir}/response_reader"
|
|
@@ -1110,11 +1121,11 @@ module Net
|
|
|
1110
1121
|
@ssl_ctx_params, @ssl_ctx = build_ssl_ctx(ssl)
|
|
1111
1122
|
|
|
1112
1123
|
# Basic Client State
|
|
1113
|
-
@utf8_strings = false
|
|
1114
1124
|
@debug_output_bol = true
|
|
1115
1125
|
@exception = nil
|
|
1116
1126
|
@greeting = nil
|
|
1117
1127
|
@capabilities = nil
|
|
1128
|
+
@enabled = Set.new
|
|
1118
1129
|
@tls_verified = false
|
|
1119
1130
|
@connection_state = ConnectionState::NotAuthenticated.new
|
|
1120
1131
|
|
|
@@ -1160,16 +1171,29 @@ module Net
|
|
|
1160
1171
|
# imap.logout
|
|
1161
1172
|
# imap.inspect #=> "#<Net::IMAP imap.example.net:993 TLS logout>"
|
|
1162
1173
|
#
|
|
1174
|
+
# imap = Net::IMAP.new(hostname, ssl: false)
|
|
1175
|
+
# imap.inspect #=> "#<Net::IMAP imap.example.net:143 PLAINTEXT not_authenticated>"
|
|
1176
|
+
#
|
|
1177
|
+
# imap.starttls verify_mode: OpenSSL::SSL::VERIFY_NONE
|
|
1178
|
+
# imap.inspect #=> "#<Net::IMAP imap.example.net:993 TLS (NOT VERIFIED) not_authenticated>"
|
|
1179
|
+
#
|
|
1163
1180
|
def inspect
|
|
1164
|
-
tls_state = tls_verified? ? "TLS" :
|
|
1165
|
-
ssl_ctx ? "TLS (NOT VERIFIED)" :
|
|
1166
|
-
"PLAINTEXT"
|
|
1167
1181
|
conn_state = disconnected? ? "disconnected" : connection_state.to_sym
|
|
1168
1182
|
"#<%s:0x%08x %s:%s %s %s>" % [
|
|
1169
|
-
self.class.name, __id__, host, port,
|
|
1183
|
+
self.class.name, __id__, host, port, inspect_tls_state, conn_state
|
|
1170
1184
|
]
|
|
1171
1185
|
end
|
|
1172
1186
|
|
|
1187
|
+
private def inspect_tls_state
|
|
1188
|
+
if tls_verified?
|
|
1189
|
+
"TLS"
|
|
1190
|
+
elsif ssl_ctx && @sock.kind_of?(OpenSSL::SSL::SSLSocket)
|
|
1191
|
+
"TLS (#{@sock.session ? "NOT VERIFIED" : "NOT ESTABLISHED"})"
|
|
1192
|
+
else
|
|
1193
|
+
"PLAINTEXT#{" (TLS NOT STARTED)" if ssl_ctx}"
|
|
1194
|
+
end
|
|
1195
|
+
end
|
|
1196
|
+
|
|
1173
1197
|
# Returns true after the TLS negotiation has completed and the remote
|
|
1174
1198
|
# hostname has been verified. Returns false when TLS has been established
|
|
1175
1199
|
# but peer verification was disabled.
|
|
@@ -1177,22 +1201,25 @@ module Net
|
|
|
1177
1201
|
|
|
1178
1202
|
# Disconnects from the server.
|
|
1179
1203
|
#
|
|
1180
|
-
# Waits for receiver thread to close before returning
|
|
1181
|
-
#
|
|
1204
|
+
# Waits for receiver thread to close before returning, except when called
|
|
1205
|
+
# from inside the connection mutex such as from a response handler. Slow or
|
|
1206
|
+
# stuck response handlers can cause #disconnect to hang until they complete.
|
|
1207
|
+
# Use +timeout+ to limit how long to wait for the receiver thread to exit.
|
|
1182
1208
|
#
|
|
1183
1209
|
# Related: #logout, #logout!
|
|
1184
|
-
def disconnect
|
|
1210
|
+
def disconnect(timeout: nil)
|
|
1185
1211
|
in_logout_state = try_state_logout?
|
|
1186
1212
|
return if disconnected?
|
|
1213
|
+
in_receiver_thread = Thread.current == @receiver_thread
|
|
1187
1214
|
begin
|
|
1188
1215
|
@sock.to_io.shutdown
|
|
1189
1216
|
rescue Errno::ENOTCONN
|
|
1190
1217
|
# ignore `Errno::ENOTCONN: Socket is not connected' on some platforms.
|
|
1191
1218
|
rescue Exception => e
|
|
1192
|
-
@receiver_thread.raise(e)
|
|
1219
|
+
@receiver_thread.raise(e) unless in_receiver_thread
|
|
1193
1220
|
end
|
|
1194
1221
|
@sock.close
|
|
1195
|
-
@receiver_thread.join
|
|
1222
|
+
@receiver_thread.join(timeout) unless mon_owned? || in_receiver_thread
|
|
1196
1223
|
raise e if e
|
|
1197
1224
|
ensure
|
|
1198
1225
|
# Try again after shutting down the receiver thread. With no reciever
|
|
@@ -1454,7 +1481,7 @@ module Net
|
|
|
1454
1481
|
end
|
|
1455
1482
|
if error
|
|
1456
1483
|
disconnect
|
|
1457
|
-
|
|
1484
|
+
reraise error
|
|
1458
1485
|
end
|
|
1459
1486
|
unless handled
|
|
1460
1487
|
disconnect
|
|
@@ -2242,6 +2269,7 @@ module Net
|
|
|
2242
2269
|
# provided as an array or a string.
|
|
2243
2270
|
# See {"Argument translation"}[rdoc-ref:#search@Argument+translation]
|
|
2244
2271
|
# and {"Search criteria"}[rdoc-ref:#search@Search+criteria], below.
|
|
2272
|
+
# <em>Please note</em> the warning for when +criteria+ is a String.
|
|
2245
2273
|
#
|
|
2246
2274
|
# +return+ options control what kind of information is returned about
|
|
2247
2275
|
# messages matching the search +criteria+. Specifying +return+ should force
|
|
@@ -2652,7 +2680,8 @@ module Net
|
|
|
2652
2680
|
# backward compatibility) but adds SearchResult#modseq when the +CONDSTORE+
|
|
2653
2681
|
# capability has been enabled.
|
|
2654
2682
|
#
|
|
2655
|
-
# See #search for documentation of parameters.
|
|
2683
|
+
# See #search for documentation of parameters. <em>Please note</em> the
|
|
2684
|
+
# warning for when +criteria+ is a String.
|
|
2656
2685
|
#
|
|
2657
2686
|
# ==== Capabilities
|
|
2658
2687
|
#
|
|
@@ -2738,7 +2767,8 @@ module Net
|
|
|
2738
2767
|
# {SequenceSet[...]}[rdoc-ref:SequenceSet@Creating+sequence+sets].
|
|
2739
2768
|
# (For message sequence numbers, use #fetch instead.)
|
|
2740
2769
|
#
|
|
2741
|
-
# +attr+ behaves the same as with #fetch.
|
|
2770
|
+
# +attr+ behaves the same as with #fetch. <em>Please note</em> the #fetch
|
|
2771
|
+
# warning on the +attr+ argument.
|
|
2742
2772
|
# >>>
|
|
2743
2773
|
# *Note:* Servers _MUST_ implicitly include the +UID+ message data item as
|
|
2744
2774
|
# part of any +FETCH+ response caused by a +UID+ command, regardless of
|
|
@@ -2950,8 +2980,10 @@ module Net
|
|
|
2950
2980
|
|
|
2951
2981
|
# Sends a {SORT command [RFC5256 §3]}[https://www.rfc-editor.org/rfc/rfc5256#section-3]
|
|
2952
2982
|
# to search a mailbox for messages that match +search_keys+ and return an
|
|
2953
|
-
# array of message sequence numbers, sorted by +sort_keys+.
|
|
2954
|
-
#
|
|
2983
|
+
# array of message sequence numbers, sorted by +sort_keys+.
|
|
2984
|
+
#
|
|
2985
|
+
# +search_keys+ are interpreted the same as the +criteria+ argument for
|
|
2986
|
+
# #search. <em>Please note</em> the #search warning for String +criteria+.
|
|
2955
2987
|
#
|
|
2956
2988
|
#--
|
|
2957
2989
|
# TODO: describe +sort_keys+
|
|
@@ -2976,8 +3008,10 @@ module Net
|
|
|
2976
3008
|
|
|
2977
3009
|
# Sends a {UID SORT command [RFC5256 §3]}[https://www.rfc-editor.org/rfc/rfc5256#section-3]
|
|
2978
3010
|
# to search a mailbox for messages that match +search_keys+ and return an
|
|
2979
|
-
# array of unique identifiers, sorted by +sort_keys+.
|
|
2980
|
-
#
|
|
3011
|
+
# array of unique identifiers, sorted by +sort_keys+.
|
|
3012
|
+
#
|
|
3013
|
+
# +search_keys+ are interpreted the same as the +criteria+ argument for
|
|
3014
|
+
# #search. <em>Please note</em> the #search warning for String +criteria+.
|
|
2981
3015
|
#
|
|
2982
3016
|
# Related: #sort, #search, #uid_search, #thread, #uid_thread
|
|
2983
3017
|
#
|
|
@@ -2991,8 +3025,10 @@ module Net
|
|
|
2991
3025
|
|
|
2992
3026
|
# Sends a {THREAD command [RFC5256 §3]}[https://www.rfc-editor.org/rfc/rfc5256#section-3]
|
|
2993
3027
|
# to search a mailbox and return message sequence numbers in threaded
|
|
2994
|
-
# format, as a ThreadMember tree.
|
|
2995
|
-
#
|
|
3028
|
+
# format, as a ThreadMember tree.
|
|
3029
|
+
#
|
|
3030
|
+
# +search_keys+ are interpreted the same as the +criteria+ argument for
|
|
3031
|
+
# #search. <em>Please note</em> the #search warning for String +criteria+.
|
|
2996
3032
|
#
|
|
2997
3033
|
# The supported algorithms are:
|
|
2998
3034
|
#
|
|
@@ -3018,6 +3054,9 @@ module Net
|
|
|
3018
3054
|
# Similar to #thread, but returns unique identifiers instead of
|
|
3019
3055
|
# message sequence numbers.
|
|
3020
3056
|
#
|
|
3057
|
+
# +search_keys+ are interpreted the same as the +criteria+ argument for
|
|
3058
|
+
# #search. <em>Please note</em> the #search warning for String +criteria+.
|
|
3059
|
+
#
|
|
3021
3060
|
# Related: #thread, #search, #uid_search, #sort, #uid_sort
|
|
3022
3061
|
#
|
|
3023
3062
|
# ==== Capabilities
|
|
@@ -3127,17 +3166,54 @@ module Net
|
|
|
3127
3166
|
capabilities = capabilities
|
|
3128
3167
|
.flatten
|
|
3129
3168
|
.map {|e| ENABLE_ALIASES[e] || e }
|
|
3169
|
+
.flat_map { _1.is_a?(String) && !_1.empty? ? _1.split(/ /, -1) : [_1] }
|
|
3130
3170
|
.uniq
|
|
3131
|
-
.
|
|
3171
|
+
.map { Atom[_1] }
|
|
3132
3172
|
synchronize do
|
|
3133
|
-
send_command("ENABLE
|
|
3173
|
+
send_command("ENABLE", *capabilities)
|
|
3134
3174
|
result = clear_responses("ENABLED").last || []
|
|
3135
|
-
@
|
|
3136
|
-
@utf8_strings ||= result.include? "IMAP4REV2"
|
|
3175
|
+
@enabled.merge(result.map(&:upcase))
|
|
3137
3176
|
result
|
|
3138
3177
|
end
|
|
3139
3178
|
end
|
|
3140
3179
|
|
|
3180
|
+
# Returns whether UTF-8 string encoding has been enabled for the connection.
|
|
3181
|
+
#
|
|
3182
|
+
# This is currently identical to calling #enabled? with +:utf8+.
|
|
3183
|
+
#
|
|
3184
|
+
# See #enable and #enabled?.
|
|
3185
|
+
def utf8_enabled?; enabled?(:utf8) end
|
|
3186
|
+
|
|
3187
|
+
# Returns whether +capability+ is in the set of #enabled capabilities,
|
|
3188
|
+
# matched case-insensitively.
|
|
3189
|
+
#
|
|
3190
|
+
# When +capability+ is +:utf8+, it matches if _either_
|
|
3191
|
+
# <tt>"UTF8=ACCEPT"</tt> or <tt>"IMAP4REV2"</tt> is enabled.
|
|
3192
|
+
#
|
|
3193
|
+
# See #enable and #utf8_enabled?.
|
|
3194
|
+
def enabled?(capability)
|
|
3195
|
+
case capability
|
|
3196
|
+
in String
|
|
3197
|
+
capability = capability.upcase
|
|
3198
|
+
synchronize { @enabled.include?(capability) }
|
|
3199
|
+
in Symbol
|
|
3200
|
+
capabilities = ENABLED_ALIASES[capability] or
|
|
3201
|
+
raise ArgumentError, "unknown capability alias: #{capability}"
|
|
3202
|
+
synchronize { @enabled.intersect?(capabilities) }
|
|
3203
|
+
else
|
|
3204
|
+
raise TypeError, "expected capability to be String or Symbol, " \
|
|
3205
|
+
"got %s" % [capability.class]
|
|
3206
|
+
end
|
|
3207
|
+
end
|
|
3208
|
+
|
|
3209
|
+
# Returns a set of enabled capabilities for the connection, as upper-cased
|
|
3210
|
+
# strings.
|
|
3211
|
+
#
|
|
3212
|
+
# See #enable and #enabled?.
|
|
3213
|
+
def enabled
|
|
3214
|
+
synchronize { @enabled.dup }
|
|
3215
|
+
end
|
|
3216
|
+
|
|
3141
3217
|
# Sends an {IDLE command [RFC2177 §3]}[https://www.rfc-editor.org/rfc/rfc6851#section-3]
|
|
3142
3218
|
# {[IMAP4rev2 §6.3.13]}[https://www.rfc-editor.org/rfc/rfc9051#section-6.3.13]
|
|
3143
3219
|
# that waits for notifications of new or expunged messages. Yields
|
|
@@ -3174,8 +3250,9 @@ module Net
|
|
|
3174
3250
|
|
|
3175
3251
|
synchronize do
|
|
3176
3252
|
tag = Thread.current[:net_imap_tag] = generate_tag
|
|
3177
|
-
|
|
3253
|
+
command = Command[tag:, name: "IDLE"]
|
|
3178
3254
|
put_string("#{tag} IDLE#{CRLF}")
|
|
3255
|
+
finish_sending_command(command)
|
|
3179
3256
|
|
|
3180
3257
|
begin
|
|
3181
3258
|
add_response_handler(&response_handler)
|
|
@@ -3183,7 +3260,7 @@ module Net
|
|
|
3183
3260
|
@idle_done_cond.wait(timeout)
|
|
3184
3261
|
@idle_done_cond = nil
|
|
3185
3262
|
if @receiver_thread_terminating
|
|
3186
|
-
|
|
3263
|
+
reraise @exception || Net::IMAP::Error.new("connection closed")
|
|
3187
3264
|
end
|
|
3188
3265
|
ensure
|
|
3189
3266
|
remove_response_handler(response_handler)
|
|
@@ -3192,6 +3269,9 @@ module Net
|
|
|
3192
3269
|
response = get_tagged_response(tag, "IDLE", idle_response_timeout)
|
|
3193
3270
|
end
|
|
3194
3271
|
end
|
|
3272
|
+
rescue InvalidResponseError
|
|
3273
|
+
disconnect
|
|
3274
|
+
raise
|
|
3195
3275
|
end
|
|
3196
3276
|
|
|
3197
3277
|
return response
|
|
@@ -3483,67 +3563,28 @@ module Net
|
|
|
3483
3563
|
end
|
|
3484
3564
|
|
|
3485
3565
|
def receive_responses
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
end
|
|
3491
|
-
begin
|
|
3492
|
-
resp = get_response
|
|
3493
|
-
rescue Exception => e
|
|
3494
|
-
synchronize do
|
|
3495
|
-
state_logout!
|
|
3496
|
-
@sock.close
|
|
3497
|
-
@exception = e
|
|
3498
|
-
end
|
|
3499
|
-
break
|
|
3500
|
-
end
|
|
3501
|
-
unless resp
|
|
3502
|
-
synchronize do
|
|
3503
|
-
@exception = EOFError.new("end of file reached")
|
|
3504
|
-
end
|
|
3505
|
-
break
|
|
3506
|
-
end
|
|
3507
|
-
begin
|
|
3508
|
-
synchronize do
|
|
3509
|
-
case resp
|
|
3510
|
-
when TaggedResponse
|
|
3511
|
-
@tagged_responses[resp.tag] = resp
|
|
3512
|
-
@tagged_response_arrival.broadcast
|
|
3513
|
-
case resp.tag
|
|
3514
|
-
when @logout_command_tag
|
|
3515
|
-
state_logout!
|
|
3516
|
-
return
|
|
3517
|
-
when @continued_command_tag
|
|
3518
|
-
@continuation_request_exception =
|
|
3519
|
-
RESPONSE_ERRORS[resp.name].new(resp)
|
|
3520
|
-
@continuation_request_arrival.signal
|
|
3521
|
-
end
|
|
3522
|
-
when UntaggedResponse
|
|
3523
|
-
record_untagged_response(resp)
|
|
3524
|
-
if resp.name == "BYE" && @logout_command_tag.nil?
|
|
3525
|
-
state_logout!
|
|
3526
|
-
@sock.close
|
|
3527
|
-
@exception = ByeResponseError.new(resp)
|
|
3528
|
-
connection_closed = true
|
|
3529
|
-
end
|
|
3530
|
-
when ContinuationRequest
|
|
3531
|
-
@continuation_request_arrival.signal
|
|
3532
|
-
end
|
|
3533
|
-
state_unselected! if resp in {data: {code: {name: "CLOSED"}}}
|
|
3534
|
-
@response_handlers.each do |handler|
|
|
3535
|
-
handler.call(resp)
|
|
3536
|
-
end
|
|
3537
|
-
end
|
|
3538
|
-
rescue Exception => e
|
|
3539
|
-
@exception = e
|
|
3540
|
-
synchronize do
|
|
3541
|
-
@tagged_response_arrival.broadcast
|
|
3542
|
-
@continuation_request_arrival.broadcast
|
|
3543
|
-
end
|
|
3544
|
-
end
|
|
3566
|
+
exception = nil
|
|
3567
|
+
loop do
|
|
3568
|
+
resp = get_response or raise EOFError, "end of file reached"
|
|
3569
|
+
handle_response(resp) or return
|
|
3545
3570
|
end
|
|
3571
|
+
rescue Exception => exception
|
|
3572
|
+
# NOTE: this rescue clause is only capturing the exception for the ensure
|
|
3573
|
+
# clause. Using `$!` in the ensure clause seems to trigger a weird
|
|
3574
|
+
# TruffleRuby bug: https://github.com/truffleruby/truffleruby/issues/4308
|
|
3575
|
+
#
|
|
3576
|
+
# We don't assign @exception directly here, because we want that to be
|
|
3577
|
+
# atomically synchronized with all of the other changes in `ensure`.
|
|
3578
|
+
raise
|
|
3579
|
+
ensure
|
|
3546
3580
|
synchronize do
|
|
3581
|
+
if exception
|
|
3582
|
+
# Handling exceptions here, not in a rescue clause, so the lock isn't
|
|
3583
|
+
# released and reacquired between rescue and ensure clauses.
|
|
3584
|
+
@exception ||= exception
|
|
3585
|
+
@sock.close
|
|
3586
|
+
end
|
|
3587
|
+
state_logout!
|
|
3547
3588
|
@receiver_thread_terminating = true
|
|
3548
3589
|
@tagged_response_arrival.broadcast
|
|
3549
3590
|
@continuation_request_arrival.broadcast
|
|
@@ -3551,36 +3592,6 @@ module Net
|
|
|
3551
3592
|
@idle_done_cond.signal
|
|
3552
3593
|
end
|
|
3553
3594
|
end
|
|
3554
|
-
ensure
|
|
3555
|
-
state_logout!
|
|
3556
|
-
end
|
|
3557
|
-
|
|
3558
|
-
def get_tagged_response(tag, cmd, timeout = nil)
|
|
3559
|
-
if timeout
|
|
3560
|
-
deadline = Time.now + timeout
|
|
3561
|
-
end
|
|
3562
|
-
until @tagged_responses.key?(tag)
|
|
3563
|
-
raise @exception if @exception
|
|
3564
|
-
if timeout
|
|
3565
|
-
timeout = deadline - Time.now
|
|
3566
|
-
if timeout <= 0
|
|
3567
|
-
return nil
|
|
3568
|
-
end
|
|
3569
|
-
end
|
|
3570
|
-
@tagged_response_arrival.wait(timeout)
|
|
3571
|
-
end
|
|
3572
|
-
resp = @tagged_responses.delete(tag)
|
|
3573
|
-
case resp.name
|
|
3574
|
-
when /\A(?:OK)\z/ni
|
|
3575
|
-
return resp
|
|
3576
|
-
when /\A(?:NO)\z/ni
|
|
3577
|
-
raise NoResponseError, resp
|
|
3578
|
-
when /\A(?:BAD)\z/ni
|
|
3579
|
-
raise BadResponseError, resp
|
|
3580
|
-
else
|
|
3581
|
-
disconnect
|
|
3582
|
-
raise InvalidResponseError, "invalid tagged resp: %p" % [resp.raw_data.chomp]
|
|
3583
|
-
end
|
|
3584
3595
|
end
|
|
3585
3596
|
|
|
3586
3597
|
def get_response
|
|
@@ -3593,6 +3604,47 @@ module Net
|
|
|
3593
3604
|
#############################
|
|
3594
3605
|
# built-in response handlers
|
|
3595
3606
|
|
|
3607
|
+
def handle_response(resp)
|
|
3608
|
+
connection_closed = false
|
|
3609
|
+
synchronize do
|
|
3610
|
+
case resp
|
|
3611
|
+
when TaggedResponse
|
|
3612
|
+
@tagged_responses[resp.tag] = resp
|
|
3613
|
+
@tagged_response_arrival.broadcast
|
|
3614
|
+
case resp.tag
|
|
3615
|
+
when @logout_command_tag
|
|
3616
|
+
state_logout!
|
|
3617
|
+
return
|
|
3618
|
+
when @continued_command_tag
|
|
3619
|
+
@continuation_request_exception =
|
|
3620
|
+
RESPONSE_ERRORS[resp.name].new(resp)
|
|
3621
|
+
@continuation_request_arrival.signal
|
|
3622
|
+
end
|
|
3623
|
+
when UntaggedResponse
|
|
3624
|
+
record_untagged_response(resp)
|
|
3625
|
+
if resp.name == "BYE" && @logout_command_tag.nil?
|
|
3626
|
+
state_logout!
|
|
3627
|
+
@sock.close
|
|
3628
|
+
@exception = ByeResponseError.new(resp)
|
|
3629
|
+
connection_closed = true
|
|
3630
|
+
end
|
|
3631
|
+
when ContinuationRequest
|
|
3632
|
+
@continuation_request_arrival.signal
|
|
3633
|
+
end
|
|
3634
|
+
state_unselected! if resp in {data: {code: {name: "CLOSED"}}}
|
|
3635
|
+
@response_handlers.each do |handler|
|
|
3636
|
+
handler.call(resp)
|
|
3637
|
+
end
|
|
3638
|
+
rescue Exception => e
|
|
3639
|
+
@exception = e
|
|
3640
|
+
@tagged_response_arrival.broadcast
|
|
3641
|
+
@continuation_request_arrival.broadcast
|
|
3642
|
+
ensure
|
|
3643
|
+
@exception = nil unless connection_closed
|
|
3644
|
+
end
|
|
3645
|
+
!connection_closed
|
|
3646
|
+
end
|
|
3647
|
+
|
|
3596
3648
|
# store name => [..., data]
|
|
3597
3649
|
def record_untagged_response(resp)
|
|
3598
3650
|
@responses[resp.name] << resp.data
|
|
@@ -3629,18 +3681,17 @@ module Net
|
|
|
3629
3681
|
end
|
|
3630
3682
|
|
|
3631
3683
|
def send_command(cmd, *args, &block)
|
|
3684
|
+
args.each do validate_data _1 end
|
|
3632
3685
|
synchronize do
|
|
3633
|
-
args.each do |i|
|
|
3634
|
-
validate_data(i)
|
|
3635
|
-
end
|
|
3636
3686
|
tag = generate_tag
|
|
3687
|
+
command = Command[tag:, name: cmd]
|
|
3637
3688
|
put_string(tag + " " + cmd)
|
|
3638
3689
|
args.each do |i|
|
|
3639
3690
|
put_string(" ")
|
|
3640
3691
|
send_data(i, tag)
|
|
3641
3692
|
end
|
|
3642
3693
|
@logout_command_tag = tag if cmd == "LOGOUT"
|
|
3643
|
-
|
|
3694
|
+
finish_sending_command(command)
|
|
3644
3695
|
add_response_handler(&block) if block
|
|
3645
3696
|
begin
|
|
3646
3697
|
put_string(CRLF)
|
|
@@ -3648,20 +3699,78 @@ module Net
|
|
|
3648
3699
|
ensure
|
|
3649
3700
|
remove_response_handler(block) if block
|
|
3650
3701
|
end
|
|
3702
|
+
rescue InvalidResponseError
|
|
3703
|
+
disconnect
|
|
3704
|
+
raise
|
|
3705
|
+
end
|
|
3706
|
+
end
|
|
3707
|
+
|
|
3708
|
+
# NOTE: This must be synchronized with sending the command's final CRLF and
|
|
3709
|
+
# adding any command-related response handlers.
|
|
3710
|
+
def finish_sending_command(command)
|
|
3711
|
+
if (response = @tagged_responses[command.tag])&.name&.casecmp?("OK")
|
|
3712
|
+
raise InvalidTaggedResponseError.new(:incomplete, command:, response:)
|
|
3651
3713
|
end
|
|
3652
|
-
rescue InvalidResponseError
|
|
3653
|
-
disconnect
|
|
3654
|
-
raise
|
|
3655
3714
|
end
|
|
3656
3715
|
|
|
3657
|
-
def
|
|
3658
|
-
|
|
3659
|
-
|
|
3660
|
-
|
|
3661
|
-
|
|
3662
|
-
|
|
3663
|
-
|
|
3664
|
-
|
|
3716
|
+
def get_tagged_response(tag, cmd, timeout = nil)
|
|
3717
|
+
if timeout
|
|
3718
|
+
deadline = Time.now + timeout
|
|
3719
|
+
end
|
|
3720
|
+
until @tagged_responses.key?(tag)
|
|
3721
|
+
reraise @exception
|
|
3722
|
+
if timeout
|
|
3723
|
+
timeout = deadline - Time.now
|
|
3724
|
+
if timeout <= 0
|
|
3725
|
+
return nil
|
|
3726
|
+
end
|
|
3727
|
+
end
|
|
3728
|
+
@tagged_response_arrival.wait(timeout)
|
|
3729
|
+
end
|
|
3730
|
+
resp = @tagged_responses.delete(tag)
|
|
3731
|
+
case resp.name
|
|
3732
|
+
when /\A(?:OK)\z/ni
|
|
3733
|
+
return resp
|
|
3734
|
+
when /\A(?:NO)\z/ni
|
|
3735
|
+
raise NoResponseError, resp
|
|
3736
|
+
when /\A(?:BAD)\z/ni
|
|
3737
|
+
raise BadResponseError, resp
|
|
3738
|
+
else
|
|
3739
|
+
disconnect
|
|
3740
|
+
raise InvalidResponseError, "invalid tagged resp: %p" % [resp.raw_data.chomp]
|
|
3741
|
+
end
|
|
3742
|
+
end
|
|
3743
|
+
|
|
3744
|
+
# Raises a copy of +exception+ with an updated +backtrace+ and +cause+, or
|
|
3745
|
+
# returns +nil+ when +exception+ is falsey.
|
|
3746
|
+
#
|
|
3747
|
+
# +backtrace+ is set to reflect the current caller.
|
|
3748
|
+
#
|
|
3749
|
+
# +cause+ is set to the original exception, _if_ the original has its own
|
|
3750
|
+
# backtrace or cause. Otherwise, the original was never raised and the
|
|
3751
|
+
# default cause is used.
|
|
3752
|
+
#
|
|
3753
|
+
# This is meant for exceptions that may have been created by another thread.
|
|
3754
|
+
# Raising them directly gives a misleading backtrace, making it hard to
|
|
3755
|
+
# discover where the errors originate. Although it is sometimes more
|
|
3756
|
+
# appropriate to raise a wrapping exception, that changes the API and forces
|
|
3757
|
+
# updates to users' `rescue` pattern matching.
|
|
3758
|
+
def reraise(exception)
|
|
3759
|
+
return unless exception
|
|
3760
|
+
copy = exception.dup
|
|
3761
|
+
# NOTE: set_backtrace(caller_locations) doesn't work for CRuby <= 3.3.
|
|
3762
|
+
# and set_backtrace(nil) doesn't work for TruffleRuby 34.0.0:
|
|
3763
|
+
# https://github.com/truffleruby/truffleruby/issues/4296
|
|
3764
|
+
if RUBY_ENGINE == "truffleruby"
|
|
3765
|
+
copy.set_backtrace caller_locations
|
|
3766
|
+
else
|
|
3767
|
+
copy.set_backtrace nil
|
|
3768
|
+
end
|
|
3769
|
+
if exception.cause || exception.backtrace
|
|
3770
|
+
raise copy, cause: exception
|
|
3771
|
+
else
|
|
3772
|
+
raise copy
|
|
3773
|
+
end
|
|
3665
3774
|
end
|
|
3666
3775
|
|
|
3667
3776
|
def generate_tag
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: net-imap
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.6.
|
|
4
|
+
version: 0.6.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Shugo Maeda
|
|
@@ -48,6 +48,7 @@ extra_rdoc_files: []
|
|
|
48
48
|
files:
|
|
49
49
|
- ".document"
|
|
50
50
|
- ".rdoc_options"
|
|
51
|
+
- ".simplecov"
|
|
51
52
|
- BSDL
|
|
52
53
|
- COPYING
|
|
53
54
|
- Gemfile
|
|
@@ -131,7 +132,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
131
132
|
- !ruby/object:Gem::Version
|
|
132
133
|
version: '0'
|
|
133
134
|
requirements: []
|
|
134
|
-
rubygems_version: 4.0.
|
|
135
|
+
rubygems_version: 4.0.17
|
|
135
136
|
specification_version: 4
|
|
136
137
|
summary: Ruby client api for Internet Message Access Protocol
|
|
137
138
|
test_files: []
|