net-imap 0.6.4.1 → 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 +43 -16
- data/lib/net/imap/errors.rb +66 -1
- data/lib/net/imap.rb +194 -113
- 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,6 +15,9 @@ 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
|
|
20
23
|
if data.negative?
|
|
@@ -55,28 +58,39 @@ module Net
|
|
|
55
58
|
end
|
|
56
59
|
end
|
|
57
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.
|
|
58
71
|
def send_string_data(str, tag = nil)
|
|
59
72
|
if str.empty?
|
|
73
|
+
# same as send_quoted_string(str), but incompatible encoding is allowed
|
|
60
74
|
put_string('""')
|
|
61
|
-
elsif str.match?(
|
|
62
|
-
#
|
|
63
|
-
|
|
64
|
-
elsif !str.
|
|
65
|
-
if @utf8_strings
|
|
66
|
-
# quoted string
|
|
67
|
-
send_quoted_string(str)
|
|
68
|
-
else
|
|
69
|
-
# literal, because of non-ASCII bytes
|
|
70
|
-
send_literal(str, tag)
|
|
71
|
-
end
|
|
72
|
-
elsif str.match?(/[(){ \x00-\x1f\x7f%*"\\]/n)
|
|
73
|
-
# 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)
|
|
74
79
|
send_quoted_string(str)
|
|
75
80
|
else
|
|
76
|
-
|
|
81
|
+
send_literal(str, tag)
|
|
77
82
|
end
|
|
78
83
|
end
|
|
79
84
|
|
|
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?)
|
|
92
|
+
end
|
|
93
|
+
|
|
80
94
|
def send_quoted_string(str) = QuotedString.new(data: str).send_data(self)
|
|
81
95
|
|
|
82
96
|
def send_binary_literal(*, **) = send_literal(*, **, binary: true)
|
|
@@ -108,8 +122,7 @@ module Net
|
|
|
108
122
|
@continuation_request_exception = nil
|
|
109
123
|
begin
|
|
110
124
|
@continuation_request_arrival.wait
|
|
111
|
-
|
|
112
|
-
raise e if e
|
|
125
|
+
reraise @continuation_request_exception || @exception
|
|
113
126
|
put_string(str)
|
|
114
127
|
ensure
|
|
115
128
|
@continued_command_tag = nil
|
|
@@ -154,6 +167,9 @@ module Net
|
|
|
154
167
|
def send_date_data(date) put_string Net::IMAP.encode_date(date) end
|
|
155
168
|
def send_time_data(time) put_string Net::IMAP.encode_time(time) end
|
|
156
169
|
|
|
170
|
+
# NOTE: Currently for internal use only. The API is expected to change.
|
|
171
|
+
Command = Data.define(:tag, :name) # :nodoc:
|
|
172
|
+
|
|
157
173
|
CommandData = Data.define(:data) do # :nodoc:
|
|
158
174
|
def self.validate(...)
|
|
159
175
|
data = new(...)
|
|
@@ -271,6 +287,17 @@ module Net
|
|
|
271
287
|
private_class_method :extract_literal
|
|
272
288
|
end
|
|
273
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+
|
|
274
301
|
class Atom < CommandData # :nodoc:
|
|
275
302
|
def initialize(**)
|
|
276
303
|
super
|
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
|
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
|
|
|
@@ -1193,9 +1204,10 @@ module Net
|
|
|
1193
1204
|
# Waits for receiver thread to close before returning, except when called
|
|
1194
1205
|
# from inside the connection mutex such as from a response handler. Slow or
|
|
1195
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.
|
|
1196
1208
|
#
|
|
1197
1209
|
# Related: #logout, #logout!
|
|
1198
|
-
def disconnect
|
|
1210
|
+
def disconnect(timeout: nil)
|
|
1199
1211
|
in_logout_state = try_state_logout?
|
|
1200
1212
|
return if disconnected?
|
|
1201
1213
|
in_receiver_thread = Thread.current == @receiver_thread
|
|
@@ -1207,7 +1219,7 @@ module Net
|
|
|
1207
1219
|
@receiver_thread.raise(e) unless in_receiver_thread
|
|
1208
1220
|
end
|
|
1209
1221
|
@sock.close
|
|
1210
|
-
@receiver_thread.join unless mon_owned? || in_receiver_thread
|
|
1222
|
+
@receiver_thread.join(timeout) unless mon_owned? || in_receiver_thread
|
|
1211
1223
|
raise e if e
|
|
1212
1224
|
ensure
|
|
1213
1225
|
# Try again after shutting down the receiver thread. With no reciever
|
|
@@ -1469,7 +1481,7 @@ module Net
|
|
|
1469
1481
|
end
|
|
1470
1482
|
if error
|
|
1471
1483
|
disconnect
|
|
1472
|
-
|
|
1484
|
+
reraise error
|
|
1473
1485
|
end
|
|
1474
1486
|
unless handled
|
|
1475
1487
|
disconnect
|
|
@@ -3160,12 +3172,48 @@ module Net
|
|
|
3160
3172
|
synchronize do
|
|
3161
3173
|
send_command("ENABLE", *capabilities)
|
|
3162
3174
|
result = clear_responses("ENABLED").last || []
|
|
3163
|
-
@
|
|
3164
|
-
@utf8_strings ||= result.include? "IMAP4REV2"
|
|
3175
|
+
@enabled.merge(result.map(&:upcase))
|
|
3165
3176
|
result
|
|
3166
3177
|
end
|
|
3167
3178
|
end
|
|
3168
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
|
+
|
|
3169
3217
|
# Sends an {IDLE command [RFC2177 §3]}[https://www.rfc-editor.org/rfc/rfc6851#section-3]
|
|
3170
3218
|
# {[IMAP4rev2 §6.3.13]}[https://www.rfc-editor.org/rfc/rfc9051#section-6.3.13]
|
|
3171
3219
|
# that waits for notifications of new or expunged messages. Yields
|
|
@@ -3202,8 +3250,9 @@ module Net
|
|
|
3202
3250
|
|
|
3203
3251
|
synchronize do
|
|
3204
3252
|
tag = Thread.current[:net_imap_tag] = generate_tag
|
|
3205
|
-
|
|
3253
|
+
command = Command[tag:, name: "IDLE"]
|
|
3206
3254
|
put_string("#{tag} IDLE#{CRLF}")
|
|
3255
|
+
finish_sending_command(command)
|
|
3207
3256
|
|
|
3208
3257
|
begin
|
|
3209
3258
|
add_response_handler(&response_handler)
|
|
@@ -3211,7 +3260,7 @@ module Net
|
|
|
3211
3260
|
@idle_done_cond.wait(timeout)
|
|
3212
3261
|
@idle_done_cond = nil
|
|
3213
3262
|
if @receiver_thread_terminating
|
|
3214
|
-
|
|
3263
|
+
reraise @exception || Net::IMAP::Error.new("connection closed")
|
|
3215
3264
|
end
|
|
3216
3265
|
ensure
|
|
3217
3266
|
remove_response_handler(response_handler)
|
|
@@ -3220,6 +3269,9 @@ module Net
|
|
|
3220
3269
|
response = get_tagged_response(tag, "IDLE", idle_response_timeout)
|
|
3221
3270
|
end
|
|
3222
3271
|
end
|
|
3272
|
+
rescue InvalidResponseError
|
|
3273
|
+
disconnect
|
|
3274
|
+
raise
|
|
3223
3275
|
end
|
|
3224
3276
|
|
|
3225
3277
|
return response
|
|
@@ -3511,67 +3563,28 @@ module Net
|
|
|
3511
3563
|
end
|
|
3512
3564
|
|
|
3513
3565
|
def receive_responses
|
|
3514
|
-
|
|
3515
|
-
|
|
3516
|
-
|
|
3517
|
-
|
|
3518
|
-
end
|
|
3519
|
-
begin
|
|
3520
|
-
resp = get_response
|
|
3521
|
-
rescue Exception => e
|
|
3522
|
-
synchronize do
|
|
3523
|
-
state_logout!
|
|
3524
|
-
@sock.close
|
|
3525
|
-
@exception = e
|
|
3526
|
-
end
|
|
3527
|
-
break
|
|
3528
|
-
end
|
|
3529
|
-
unless resp
|
|
3530
|
-
synchronize do
|
|
3531
|
-
@exception = EOFError.new("end of file reached")
|
|
3532
|
-
end
|
|
3533
|
-
break
|
|
3534
|
-
end
|
|
3535
|
-
begin
|
|
3536
|
-
synchronize do
|
|
3537
|
-
case resp
|
|
3538
|
-
when TaggedResponse
|
|
3539
|
-
@tagged_responses[resp.tag] = resp
|
|
3540
|
-
@tagged_response_arrival.broadcast
|
|
3541
|
-
case resp.tag
|
|
3542
|
-
when @logout_command_tag
|
|
3543
|
-
state_logout!
|
|
3544
|
-
return
|
|
3545
|
-
when @continued_command_tag
|
|
3546
|
-
@continuation_request_exception =
|
|
3547
|
-
RESPONSE_ERRORS[resp.name].new(resp)
|
|
3548
|
-
@continuation_request_arrival.signal
|
|
3549
|
-
end
|
|
3550
|
-
when UntaggedResponse
|
|
3551
|
-
record_untagged_response(resp)
|
|
3552
|
-
if resp.name == "BYE" && @logout_command_tag.nil?
|
|
3553
|
-
state_logout!
|
|
3554
|
-
@sock.close
|
|
3555
|
-
@exception = ByeResponseError.new(resp)
|
|
3556
|
-
connection_closed = true
|
|
3557
|
-
end
|
|
3558
|
-
when ContinuationRequest
|
|
3559
|
-
@continuation_request_arrival.signal
|
|
3560
|
-
end
|
|
3561
|
-
state_unselected! if resp in {data: {code: {name: "CLOSED"}}}
|
|
3562
|
-
@response_handlers.each do |handler|
|
|
3563
|
-
handler.call(resp)
|
|
3564
|
-
end
|
|
3565
|
-
end
|
|
3566
|
-
rescue Exception => e
|
|
3567
|
-
@exception = e
|
|
3568
|
-
synchronize do
|
|
3569
|
-
@tagged_response_arrival.broadcast
|
|
3570
|
-
@continuation_request_arrival.broadcast
|
|
3571
|
-
end
|
|
3572
|
-
end
|
|
3566
|
+
exception = nil
|
|
3567
|
+
loop do
|
|
3568
|
+
resp = get_response or raise EOFError, "end of file reached"
|
|
3569
|
+
handle_response(resp) or return
|
|
3573
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
|
|
3574
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!
|
|
3575
3588
|
@receiver_thread_terminating = true
|
|
3576
3589
|
@tagged_response_arrival.broadcast
|
|
3577
3590
|
@continuation_request_arrival.broadcast
|
|
@@ -3579,36 +3592,6 @@ module Net
|
|
|
3579
3592
|
@idle_done_cond.signal
|
|
3580
3593
|
end
|
|
3581
3594
|
end
|
|
3582
|
-
ensure
|
|
3583
|
-
state_logout!
|
|
3584
|
-
end
|
|
3585
|
-
|
|
3586
|
-
def get_tagged_response(tag, cmd, timeout = nil)
|
|
3587
|
-
if timeout
|
|
3588
|
-
deadline = Time.now + timeout
|
|
3589
|
-
end
|
|
3590
|
-
until @tagged_responses.key?(tag)
|
|
3591
|
-
raise @exception if @exception
|
|
3592
|
-
if timeout
|
|
3593
|
-
timeout = deadline - Time.now
|
|
3594
|
-
if timeout <= 0
|
|
3595
|
-
return nil
|
|
3596
|
-
end
|
|
3597
|
-
end
|
|
3598
|
-
@tagged_response_arrival.wait(timeout)
|
|
3599
|
-
end
|
|
3600
|
-
resp = @tagged_responses.delete(tag)
|
|
3601
|
-
case resp.name
|
|
3602
|
-
when /\A(?:OK)\z/ni
|
|
3603
|
-
return resp
|
|
3604
|
-
when /\A(?:NO)\z/ni
|
|
3605
|
-
raise NoResponseError, resp
|
|
3606
|
-
when /\A(?:BAD)\z/ni
|
|
3607
|
-
raise BadResponseError, resp
|
|
3608
|
-
else
|
|
3609
|
-
disconnect
|
|
3610
|
-
raise InvalidResponseError, "invalid tagged resp: %p" % [resp.raw_data.chomp]
|
|
3611
|
-
end
|
|
3612
3595
|
end
|
|
3613
3596
|
|
|
3614
3597
|
def get_response
|
|
@@ -3621,6 +3604,47 @@ module Net
|
|
|
3621
3604
|
#############################
|
|
3622
3605
|
# built-in response handlers
|
|
3623
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
|
+
|
|
3624
3648
|
# store name => [..., data]
|
|
3625
3649
|
def record_untagged_response(resp)
|
|
3626
3650
|
@responses[resp.name] << resp.data
|
|
@@ -3657,18 +3681,17 @@ module Net
|
|
|
3657
3681
|
end
|
|
3658
3682
|
|
|
3659
3683
|
def send_command(cmd, *args, &block)
|
|
3684
|
+
args.each do validate_data _1 end
|
|
3660
3685
|
synchronize do
|
|
3661
|
-
args.each do |i|
|
|
3662
|
-
validate_data(i)
|
|
3663
|
-
end
|
|
3664
3686
|
tag = generate_tag
|
|
3687
|
+
command = Command[tag:, name: cmd]
|
|
3665
3688
|
put_string(tag + " " + cmd)
|
|
3666
3689
|
args.each do |i|
|
|
3667
3690
|
put_string(" ")
|
|
3668
3691
|
send_data(i, tag)
|
|
3669
3692
|
end
|
|
3670
3693
|
@logout_command_tag = tag if cmd == "LOGOUT"
|
|
3671
|
-
|
|
3694
|
+
finish_sending_command(command)
|
|
3672
3695
|
add_response_handler(&block) if block
|
|
3673
3696
|
begin
|
|
3674
3697
|
put_string(CRLF)
|
|
@@ -3676,20 +3699,78 @@ module Net
|
|
|
3676
3699
|
ensure
|
|
3677
3700
|
remove_response_handler(block) if block
|
|
3678
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:)
|
|
3713
|
+
end
|
|
3714
|
+
end
|
|
3715
|
+
|
|
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]
|
|
3679
3741
|
end
|
|
3680
|
-
rescue InvalidResponseError
|
|
3681
|
-
disconnect
|
|
3682
|
-
raise
|
|
3683
3742
|
end
|
|
3684
3743
|
|
|
3685
|
-
|
|
3686
|
-
|
|
3687
|
-
|
|
3688
|
-
|
|
3689
|
-
|
|
3690
|
-
|
|
3691
|
-
|
|
3692
|
-
|
|
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
|
|
3693
3774
|
end
|
|
3694
3775
|
|
|
3695
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: []
|