net-imap 0.5.12 → 0.5.14
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/Gemfile +1 -1
- data/lib/net/imap/command_data.rb +155 -12
- data/lib/net/imap/config/attr_version_defaults.rb +93 -0
- data/lib/net/imap/config.rb +38 -90
- data/lib/net/imap/response_data.rb +20 -3
- data/lib/net/imap/response_reader.rb +11 -5
- data/lib/net/imap/sasl/scram_authenticator.rb +74 -0
- data/lib/net/imap/sequence_set.rb +17 -2
- data/lib/net/imap.rb +111 -34
- 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: 33bccbb75eba778cb42fc5340afc2f10a899ca671123e8b538a39acbdf16bd1b
|
|
4
|
+
data.tar.gz: c4252164f38a0f36b827fb32247500e95293880e2f8026f4b7ed04926614df41
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e682041f5c1f0e071578c0910f3eace9438064741cce16b148870c73137fd38926154269907f4948dec365e83b9115facff5ec21ac23072270ef39bab64cea10
|
|
7
|
+
data.tar.gz: 8a04cac2ad54cd0bc4b2e2f5855bac16f571c3ab6aa0183caa1427ad7c420f8c3920e74b85871c22454dbd827c7772b24cfe96bcaf262222664c05f7483f5dbd
|
data/Gemfile
CHANGED
|
@@ -5,6 +5,8 @@ require "date"
|
|
|
5
5
|
require_relative "errors"
|
|
6
6
|
require_relative "data_lite"
|
|
7
7
|
|
|
8
|
+
# :enddoc:
|
|
9
|
+
|
|
8
10
|
module Net
|
|
9
11
|
class IMAP < Protocol
|
|
10
12
|
|
|
@@ -26,6 +28,7 @@ module Net
|
|
|
26
28
|
end
|
|
27
29
|
when Time, Date, DateTime
|
|
28
30
|
when Symbol
|
|
31
|
+
Flag.validate(data)
|
|
29
32
|
else
|
|
30
33
|
data.validate
|
|
31
34
|
end
|
|
@@ -46,7 +49,7 @@ module Net
|
|
|
46
49
|
when Date
|
|
47
50
|
send_date_data(data)
|
|
48
51
|
when Symbol
|
|
49
|
-
|
|
52
|
+
Flag[data].send_data(self, tag)
|
|
50
53
|
else
|
|
51
54
|
data.send_data(self, tag)
|
|
52
55
|
end
|
|
@@ -78,9 +81,23 @@ module Net
|
|
|
78
81
|
put_string('"' + str.gsub(/["\\]/, "\\\\\\&") + '"')
|
|
79
82
|
end
|
|
80
83
|
|
|
81
|
-
def send_literal(
|
|
84
|
+
def send_binary_literal(*a, **kw); send_literal(*a, **kw, binary: true) end
|
|
85
|
+
|
|
86
|
+
# `non_sync` is an optional tri-state flag:
|
|
87
|
+
# * `true` -> Force non-synchronizing `LITERAL+`/`LITERAL-` behavior.
|
|
88
|
+
# TODO: raise or warn when capabilities don't allow non_sync.
|
|
89
|
+
# * `false` -> Force normal synchronizing literal behavior.
|
|
90
|
+
# * `nil` -> (default) Currently behaves like `false` (will be dynamic).
|
|
91
|
+
# TODO: Dynamic, based on capabilities and bytesize.
|
|
92
|
+
def send_literal(str, tag = nil, binary: false, non_sync: nil)
|
|
82
93
|
synchronize do
|
|
83
|
-
|
|
94
|
+
prefix = "~" if binary
|
|
95
|
+
plus = "+" if non_sync
|
|
96
|
+
put_string("#{prefix}{#{str.bytesize}#{plus}}\r\n")
|
|
97
|
+
if non_sync
|
|
98
|
+
put_string(str)
|
|
99
|
+
return
|
|
100
|
+
end
|
|
84
101
|
@continued_command_tag = tag
|
|
85
102
|
@continuation_request_exception = nil
|
|
86
103
|
begin
|
|
@@ -116,11 +133,13 @@ module Net
|
|
|
116
133
|
def send_date_data(date) put_string Net::IMAP.encode_date(date) end
|
|
117
134
|
def send_time_data(time) put_string Net::IMAP.encode_time(time) end
|
|
118
135
|
|
|
119
|
-
def send_symbol_data(symbol)
|
|
120
|
-
put_string("\\" + symbol.to_s)
|
|
121
|
-
end
|
|
122
|
-
|
|
123
136
|
CommandData = Data.define(:data) do # :nodoc:
|
|
137
|
+
def self.validate(...)
|
|
138
|
+
data = new(...)
|
|
139
|
+
data.validate
|
|
140
|
+
data
|
|
141
|
+
end
|
|
142
|
+
|
|
124
143
|
def send_data(imap, tag)
|
|
125
144
|
raise NoMethodError, "#{self.class} must implement #{__method__}"
|
|
126
145
|
end
|
|
@@ -129,15 +148,109 @@ module Net
|
|
|
129
148
|
end
|
|
130
149
|
end
|
|
131
150
|
|
|
151
|
+
# Represents IMAP +text+ data, which may contain any 7-bit ASCII character,
|
|
152
|
+
# except for +NULL+, +CR+, or +LF+. +text+ is extended to allow any
|
|
153
|
+
# multibyte +UTF-8+ character when either +UTF8=ACCEPT+ or +IMAP4rev2+ have
|
|
154
|
+
# been enabled, or when the server supports only +IMAP4rev2+ and not earlier
|
|
155
|
+
# IMAP revisions, or when the server advertises +UTF8=ONLY+.
|
|
156
|
+
#
|
|
157
|
+
# NOTE: The current implementation does not validate whether the connection
|
|
158
|
+
# currently supports UTF-8. Future versions may change.
|
|
159
|
+
#
|
|
160
|
+
# The string's bytes must be valid ASCII or valid UTF-8. The string's
|
|
161
|
+
# reported encoding is ignored, but the string is _not_ transcoded.
|
|
162
|
+
class RawText < CommandData # :nodoc:
|
|
163
|
+
def initialize(data:)
|
|
164
|
+
data = String(data.to_str)
|
|
165
|
+
data = if data.encoding in Encoding::ASCII | Encoding::UTF_8
|
|
166
|
+
-data
|
|
167
|
+
elsif data.ascii_only?
|
|
168
|
+
-(data.dup.force_encoding("ASCII"))
|
|
169
|
+
else
|
|
170
|
+
-(data.dup.force_encoding("UTF-8"))
|
|
171
|
+
end
|
|
172
|
+
super
|
|
173
|
+
validate
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def validate
|
|
177
|
+
if data.include?("\0")
|
|
178
|
+
raise DataFormatError, "NULL byte must be binary literal encoded"
|
|
179
|
+
elsif !data.valid_encoding?
|
|
180
|
+
raise DataFormatError, "invalid UTF-8 must be literal encoded"
|
|
181
|
+
elsif /[\r\n]/.match?(data)
|
|
182
|
+
raise DataFormatError, "CR and LF bytes must be literal encoded"
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def ascii_only? = data.ascii_only?
|
|
187
|
+
|
|
188
|
+
def send_data(imap, tag) = imap.__send__(:put_string, data)
|
|
189
|
+
end
|
|
190
|
+
|
|
132
191
|
class RawData < CommandData # :nodoc:
|
|
133
|
-
def
|
|
134
|
-
|
|
192
|
+
def initialize(data:)
|
|
193
|
+
data = split_parts(data)
|
|
194
|
+
super
|
|
195
|
+
validate
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def send_data(imap, tag) = data.each do _1.send_data(imap, tag) end
|
|
199
|
+
|
|
200
|
+
def validate
|
|
201
|
+
return unless data.last in RawText(data: text)
|
|
202
|
+
if text.rindex(/~?\{[1-9]\d*\+?\}\z/n)
|
|
203
|
+
raise DataFormatError, "RawData cannot end with literal continuation"
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
private
|
|
208
|
+
|
|
209
|
+
def split_parts(data)
|
|
210
|
+
data = data.b # dups and ensures BINARY encoding
|
|
211
|
+
parts = []
|
|
212
|
+
while data.match(/(~)?\{(0|[1-9]\d*)(\+)?\}\r\n/n)
|
|
213
|
+
text, binary, bytesize, non_sync, data = $`, !!$1, $2, !!$3, $'
|
|
214
|
+
bytesize = Integer bytesize, 10
|
|
215
|
+
parts << RawText[text] unless text.empty?
|
|
216
|
+
parts << extract_literal(data, binary:, bytesize:, non_sync:)
|
|
217
|
+
data[0, bytesize] = ""
|
|
218
|
+
end
|
|
219
|
+
parts << RawText[data] unless data.empty?
|
|
220
|
+
parts
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def extract_literal(data, binary:, bytesize:, non_sync:)
|
|
224
|
+
if data.bytesize < bytesize
|
|
225
|
+
raise DataFormatError, "Too few bytes in string for literal, " \
|
|
226
|
+
"expected: %s, remaining: %s" % [bytesize, data.bytesize]
|
|
227
|
+
end
|
|
228
|
+
literal = data.byteslice(0, bytesize)
|
|
229
|
+
(binary ? Literal8 : Literal).new(data: literal, non_sync:)
|
|
135
230
|
end
|
|
136
231
|
end
|
|
137
232
|
|
|
138
233
|
class Atom < CommandData # :nodoc:
|
|
234
|
+
def initialize(**)
|
|
235
|
+
super
|
|
236
|
+
validate
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
def validate
|
|
240
|
+
data.to_s.ascii_only? \
|
|
241
|
+
or raise DataFormatError, "#{self.class} must be ASCII only"
|
|
242
|
+
data.match?(ResponseParser::Patterns::ATOM_SPECIALS) \
|
|
243
|
+
and raise DataFormatError, "#{self.class} must not contain atom-specials"
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def send_data(imap, tag)
|
|
247
|
+
imap.__send__(:put_string, data.to_s)
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
class Flag < Atom # :nodoc:
|
|
139
252
|
def send_data(imap, tag)
|
|
140
|
-
imap.__send__(:put_string, data)
|
|
253
|
+
imap.__send__(:put_string, "\\#{data}")
|
|
141
254
|
end
|
|
142
255
|
end
|
|
143
256
|
|
|
@@ -147,9 +260,39 @@ module Net
|
|
|
147
260
|
end
|
|
148
261
|
end
|
|
149
262
|
|
|
150
|
-
class Literal <
|
|
263
|
+
class Literal < Data.define(:data, :non_sync) # :nodoc:
|
|
264
|
+
def self.validate(...)
|
|
265
|
+
data = new(...)
|
|
266
|
+
data.validate
|
|
267
|
+
data
|
|
268
|
+
end
|
|
269
|
+
|
|
270
|
+
def initialize(data:, non_sync: nil)
|
|
271
|
+
data = -String(data.to_str).b or
|
|
272
|
+
raise DataFormatError, "#{self.class} expects string input"
|
|
273
|
+
super
|
|
274
|
+
validate
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def bytesize; data.bytesize end
|
|
278
|
+
|
|
279
|
+
def validate
|
|
280
|
+
if data.include?("\0")
|
|
281
|
+
raise DataFormatError, "NULL byte not allowed in #{self.class}. " \
|
|
282
|
+
"Use #{Literal8} or a null-safe encoding."
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def send_data(imap, tag)
|
|
287
|
+
imap.__send__(:send_literal, data, tag, non_sync:)
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
class Literal8 < Literal # :nodoc:
|
|
292
|
+
def validate; nil end # all bytes are okay
|
|
293
|
+
|
|
151
294
|
def send_data(imap, tag)
|
|
152
|
-
imap.__send__(:
|
|
295
|
+
imap.__send__(:send_binary_literal, data, tag, non_sync:)
|
|
153
296
|
end
|
|
154
297
|
end
|
|
155
298
|
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "forwardable"
|
|
4
|
+
|
|
5
|
+
module Net
|
|
6
|
+
class IMAP
|
|
7
|
+
class Config
|
|
8
|
+
# >>>
|
|
9
|
+
# *NOTE:* This module is an internal implementation detail, with no
|
|
10
|
+
# guarantee of backward compatibility.
|
|
11
|
+
#
|
|
12
|
+
# Adds a +defaults+ parameter to +attr_accessor+, which is used to compile
|
|
13
|
+
# Config.version_defaults.
|
|
14
|
+
module AttrVersionDefaults
|
|
15
|
+
# The <tt>x.y</tt> part of Net::IMAP::VERSION, as a Rational number.
|
|
16
|
+
CURRENT_VERSION = VERSION.to_r
|
|
17
|
+
|
|
18
|
+
# The config version used for <tt>Config[:next]</tt>.
|
|
19
|
+
NEXT_VERSION = CURRENT_VERSION + 0.1r
|
|
20
|
+
|
|
21
|
+
# The config version used for <tt>Config[:future]</tt>.
|
|
22
|
+
FUTURE_VERSION = 1.0r
|
|
23
|
+
|
|
24
|
+
VERSIONS = ((0.0r..FUTURE_VERSION) % 0.1r).to_a.freeze
|
|
25
|
+
|
|
26
|
+
# See Config.version_defaults.
|
|
27
|
+
singleton_class.attr_reader :version_defaults
|
|
28
|
+
|
|
29
|
+
@version_defaults = Hash.new {|h, k|
|
|
30
|
+
# NOTE: String responds to both so the order is significant.
|
|
31
|
+
# And ignore non-numeric conversion to zero, because: "wat!?".to_r == 0
|
|
32
|
+
(h.fetch(k.to_r, nil) || h.fetch(k.to_f, nil) if k.is_a?(Numeric)) ||
|
|
33
|
+
(h.fetch(k.to_sym, nil) if k.respond_to?(:to_sym)) ||
|
|
34
|
+
(h.fetch(k.to_r, nil) if k.respond_to?(:to_r) && k.to_r != 0r) ||
|
|
35
|
+
(h.fetch(k.to_f, nil) if k.respond_to?(:to_f) && k.to_f != 0.0)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
# :stopdoc: internal APIs only
|
|
39
|
+
|
|
40
|
+
def attr_accessor(name, defaults: nil, default: (unset = true), **kw)
|
|
41
|
+
unless unset
|
|
42
|
+
version = DEFAULT_TO_INHERIT.include?(name) ? nil : 0.0r
|
|
43
|
+
defaults = { version => default }
|
|
44
|
+
end
|
|
45
|
+
defaults&.each_pair do |version, default|
|
|
46
|
+
AttrVersionDefaults.version_defaults[version] ||= {}
|
|
47
|
+
AttrVersionDefaults.version_defaults[version][name] = default
|
|
48
|
+
end
|
|
49
|
+
super(name, **kw)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.compile_default!
|
|
53
|
+
raise "Config.default already compiled" if Config.default
|
|
54
|
+
default = VERSIONS.select { _1 <= CURRENT_VERSION }
|
|
55
|
+
.filter_map { version_defaults[_1] }
|
|
56
|
+
.prepend(version_defaults.delete(nil))
|
|
57
|
+
.inject(&:merge)
|
|
58
|
+
Config.new(**default).freeze
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def self.compile_version_defaults!
|
|
62
|
+
# Temporarily assign Config.default, enabling #load_defaults(:default)
|
|
63
|
+
version_defaults[:default] = Config.default
|
|
64
|
+
# Use #load_defaults so some attributes are inherited from global.
|
|
65
|
+
version_defaults[:default] = Config.new.load_defaults(:default).freeze
|
|
66
|
+
version_defaults[0.0r] = Config[version_defaults.fetch(0.0r)]
|
|
67
|
+
|
|
68
|
+
VERSIONS.each_cons(2) do |prior, version|
|
|
69
|
+
updates = version_defaults[version]
|
|
70
|
+
version_defaults[version] = version_defaults[prior]
|
|
71
|
+
.then { updates ? _1.dup.update(**updates).freeze : _1 }
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Safe conversions one way only:
|
|
75
|
+
# 0.6r.to_f == 0.6 # => true
|
|
76
|
+
# 0.6 .to_r == 0.6r # => false
|
|
77
|
+
version_defaults.to_a.each do |k, v|
|
|
78
|
+
next unless k in Rational
|
|
79
|
+
version_defaults[k.to_f] = v
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
version_defaults[:original] = Config[0.0r]
|
|
83
|
+
version_defaults[:current] = Config[CURRENT_VERSION]
|
|
84
|
+
version_defaults[:next] = Config[NEXT_VERSION]
|
|
85
|
+
version_defaults[:future] = Config[FUTURE_VERSION]
|
|
86
|
+
|
|
87
|
+
version_defaults.freeze
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
data/lib/net/imap/config.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "config/attr_accessors"
|
|
4
4
|
require_relative "config/attr_inheritance"
|
|
5
5
|
require_relative "config/attr_type_coercion"
|
|
6
|
+
require_relative "config/attr_version_defaults"
|
|
6
7
|
|
|
7
8
|
module Net
|
|
8
9
|
class IMAP
|
|
@@ -141,15 +142,7 @@ module Net
|
|
|
141
142
|
# Net::IMAP::Config[0.5] == Net::IMAP::Config[0.5r] # => true
|
|
142
143
|
# Net::IMAP::Config["current"] == Net::IMAP::Config[:current] # => true
|
|
143
144
|
# Net::IMAP::Config["0.5.6"] == Net::IMAP::Config[0.5r] # => true
|
|
144
|
-
def self.version_defaults;
|
|
145
|
-
@version_defaults = Hash.new {|h, k|
|
|
146
|
-
# NOTE: String responds to both so the order is significant.
|
|
147
|
-
# And ignore non-numeric conversion to zero, because: "wat!?".to_r == 0
|
|
148
|
-
(h.fetch(k.to_r, nil) || h.fetch(k.to_f, nil) if k.is_a?(Numeric)) ||
|
|
149
|
-
(h.fetch(k.to_sym, nil) if k.respond_to?(:to_sym)) ||
|
|
150
|
-
(h.fetch(k.to_r, nil) if k.respond_to?(:to_r) && k.to_r != 0r) ||
|
|
151
|
-
(h.fetch(k.to_f, nil) if k.respond_to?(:to_f) && k.to_f != 0.0)
|
|
152
|
-
}
|
|
145
|
+
def self.version_defaults; AttrVersionDefaults.version_defaults end
|
|
153
146
|
|
|
154
147
|
# :call-seq:
|
|
155
148
|
# Net::IMAP::Config[number] -> versioned config
|
|
@@ -189,6 +182,7 @@ module Net
|
|
|
189
182
|
include AttrAccessors
|
|
190
183
|
include AttrInheritance
|
|
191
184
|
include AttrTypeCoercion
|
|
185
|
+
extend AttrVersionDefaults
|
|
192
186
|
|
|
193
187
|
# The debug mode (boolean). The default value is +false+.
|
|
194
188
|
#
|
|
@@ -200,7 +194,7 @@ module Net
|
|
|
200
194
|
#
|
|
201
195
|
# *NOTE:* Versioned default configs inherit #debug from Config.global, and
|
|
202
196
|
# #load_defaults will not override #debug.
|
|
203
|
-
attr_accessor :debug, type: :boolean
|
|
197
|
+
attr_accessor :debug, type: :boolean, default: false
|
|
204
198
|
|
|
205
199
|
# method: debug?
|
|
206
200
|
# :call-seq: debug? -> boolean
|
|
@@ -218,7 +212,7 @@ module Net
|
|
|
218
212
|
# See Net::IMAP.new and Net::IMAP#starttls.
|
|
219
213
|
#
|
|
220
214
|
# The default value is +30+ seconds.
|
|
221
|
-
attr_accessor :open_timeout, type: Integer
|
|
215
|
+
attr_accessor :open_timeout, type: Integer, default: 30
|
|
222
216
|
|
|
223
217
|
# Seconds to wait until an IDLE response is received, after
|
|
224
218
|
# the client asks to leave the IDLE state.
|
|
@@ -226,7 +220,7 @@ module Net
|
|
|
226
220
|
# See Net::IMAP#idle and Net::IMAP#idle_done.
|
|
227
221
|
#
|
|
228
222
|
# The default value is +5+ seconds.
|
|
229
|
-
attr_accessor :idle_response_timeout, type: Integer
|
|
223
|
+
attr_accessor :idle_response_timeout, type: Integer, default: 5
|
|
230
224
|
|
|
231
225
|
# Whether to use the +SASL-IR+ extension when the server and \SASL
|
|
232
226
|
# mechanism both support it. Can be overridden by the +sasl_ir+ keyword
|
|
@@ -242,7 +236,10 @@ module Net
|
|
|
242
236
|
#
|
|
243
237
|
# [+true+ <em>(default since +v0.4+)</em>]
|
|
244
238
|
# Use +SASL-IR+ when it is supported by the server and the mechanism.
|
|
245
|
-
attr_accessor :sasl_ir, type: :boolean
|
|
239
|
+
attr_accessor :sasl_ir, type: :boolean, defaults: {
|
|
240
|
+
0.0r => false,
|
|
241
|
+
0.4r => true,
|
|
242
|
+
}
|
|
246
243
|
|
|
247
244
|
# Controls the behavior of Net::IMAP#login when the +LOGINDISABLED+
|
|
248
245
|
# capability is present. When enforced, Net::IMAP will raise a
|
|
@@ -266,7 +263,10 @@ module Net
|
|
|
266
263
|
#
|
|
267
264
|
attr_accessor :enforce_logindisabled, type: Enum[
|
|
268
265
|
false, :when_capabilities_cached, true
|
|
269
|
-
]
|
|
266
|
+
], defaults: {
|
|
267
|
+
0.0r => false,
|
|
268
|
+
0.5r => true,
|
|
269
|
+
}
|
|
270
270
|
|
|
271
271
|
# The maximum allowed server response size. When +nil+, there is no limit
|
|
272
272
|
# on response size.
|
|
@@ -300,7 +300,10 @@ module Net
|
|
|
300
300
|
#
|
|
301
301
|
# * original: +nil+ <em>(no limit)</em>
|
|
302
302
|
# * +0.5+: 512 MiB
|
|
303
|
-
attr_accessor :max_response_size, type: Integer
|
|
303
|
+
attr_accessor :max_response_size, type: Integer?, defaults: {
|
|
304
|
+
0.0r => nil,
|
|
305
|
+
0.5r => 512 << 20, # 512 MiB
|
|
306
|
+
}
|
|
304
307
|
|
|
305
308
|
# Controls the behavior of Net::IMAP#responses when called without any
|
|
306
309
|
# arguments (+type+ or +block+).
|
|
@@ -330,7 +333,11 @@ module Net
|
|
|
330
333
|
# Note: #responses_without_args is an alias for #responses_without_block.
|
|
331
334
|
attr_accessor :responses_without_block, type: Enum[
|
|
332
335
|
:silence_deprecation_warning, :warn, :frozen_dup, :raise,
|
|
333
|
-
]
|
|
336
|
+
], defaults: {
|
|
337
|
+
0.0r => :silence_deprecation_warning,
|
|
338
|
+
0.5r => :warn,
|
|
339
|
+
0.6r => :frozen_dup,
|
|
340
|
+
}
|
|
334
341
|
|
|
335
342
|
alias responses_without_args responses_without_block # :nodoc:
|
|
336
343
|
alias responses_without_args= responses_without_block= # :nodoc:
|
|
@@ -375,7 +382,11 @@ module Net
|
|
|
375
382
|
# ResponseParser _only_ uses AppendUIDData and CopyUIDData.
|
|
376
383
|
attr_accessor :parser_use_deprecated_uidplus_data, type: Enum[
|
|
377
384
|
true, :up_to_max_size, false
|
|
378
|
-
]
|
|
385
|
+
], defaults: {
|
|
386
|
+
0.0r => true,
|
|
387
|
+
0.5r => :up_to_max_size,
|
|
388
|
+
0.6r => false,
|
|
389
|
+
}
|
|
379
390
|
|
|
380
391
|
# The maximum +uid-set+ size that ResponseParser will parse into
|
|
381
392
|
# deprecated UIDPlusData. This limit only applies when
|
|
@@ -399,7 +410,13 @@ module Net
|
|
|
399
410
|
# * +0.5+: <tt>100</tt>
|
|
400
411
|
# * +0.6+: <tt>0</tt>
|
|
401
412
|
#
|
|
402
|
-
attr_accessor :parser_max_deprecated_uidplus_data_size, type: Integer
|
|
413
|
+
attr_accessor :parser_max_deprecated_uidplus_data_size, type: Integer,
|
|
414
|
+
defaults: {
|
|
415
|
+
0.0r => 10_000,
|
|
416
|
+
0.4r => 1_000,
|
|
417
|
+
0.5r => 100,
|
|
418
|
+
0.6r => 0,
|
|
419
|
+
}
|
|
403
420
|
|
|
404
421
|
# Creates a new config object and initialize its attribute with +attrs+.
|
|
405
422
|
#
|
|
@@ -474,79 +491,10 @@ module Net
|
|
|
474
491
|
to_h.reject {|k,v| DEFAULT_TO_INHERIT.include?(k) }
|
|
475
492
|
end
|
|
476
493
|
|
|
477
|
-
@default =
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
idle_response_timeout: 5,
|
|
481
|
-
sasl_ir: true,
|
|
482
|
-
enforce_logindisabled: true,
|
|
483
|
-
max_response_size: 512 << 20, # 512 MiB
|
|
484
|
-
responses_without_block: :warn,
|
|
485
|
-
parser_use_deprecated_uidplus_data: :up_to_max_size,
|
|
486
|
-
parser_max_deprecated_uidplus_data_size: 100,
|
|
487
|
-
).freeze
|
|
488
|
-
|
|
489
|
-
@global = default.new
|
|
490
|
-
|
|
491
|
-
version_defaults[:default] = Config[default.send(:defaults_hash)]
|
|
492
|
-
|
|
493
|
-
version_defaults[0r] = Config[:default].dup.update(
|
|
494
|
-
sasl_ir: false,
|
|
495
|
-
responses_without_block: :silence_deprecation_warning,
|
|
496
|
-
enforce_logindisabled: false,
|
|
497
|
-
max_response_size: nil,
|
|
498
|
-
parser_use_deprecated_uidplus_data: true,
|
|
499
|
-
parser_max_deprecated_uidplus_data_size: 10_000,
|
|
500
|
-
).freeze
|
|
501
|
-
version_defaults[0.0r] = Config[0r]
|
|
502
|
-
version_defaults[0.1r] = Config[0r]
|
|
503
|
-
version_defaults[0.2r] = Config[0r]
|
|
504
|
-
version_defaults[0.3r] = Config[0r]
|
|
505
|
-
|
|
506
|
-
version_defaults[0.4r] = Config[0.3r].dup.update(
|
|
507
|
-
sasl_ir: true,
|
|
508
|
-
parser_max_deprecated_uidplus_data_size: 1000,
|
|
509
|
-
).freeze
|
|
494
|
+
@default = AttrVersionDefaults.compile_default!
|
|
495
|
+
@global = default.new
|
|
496
|
+
AttrVersionDefaults.compile_version_defaults!
|
|
510
497
|
|
|
511
|
-
version_defaults[0.5r] = Config[0.4r].dup.update(
|
|
512
|
-
enforce_logindisabled: true,
|
|
513
|
-
max_response_size: 512 << 20, # 512 MiB
|
|
514
|
-
responses_without_block: :warn,
|
|
515
|
-
parser_use_deprecated_uidplus_data: :up_to_max_size,
|
|
516
|
-
parser_max_deprecated_uidplus_data_size: 100,
|
|
517
|
-
).freeze
|
|
518
|
-
|
|
519
|
-
version_defaults[0.6r] = Config[0.5r].dup.update(
|
|
520
|
-
responses_without_block: :frozen_dup,
|
|
521
|
-
parser_use_deprecated_uidplus_data: false,
|
|
522
|
-
parser_max_deprecated_uidplus_data_size: 0,
|
|
523
|
-
).freeze
|
|
524
|
-
|
|
525
|
-
version_defaults[0.7r] = Config[0.6r].dup.update(
|
|
526
|
-
).freeze
|
|
527
|
-
|
|
528
|
-
# Safe conversions one way only:
|
|
529
|
-
# 0.6r.to_f == 0.6 # => true
|
|
530
|
-
# 0.6 .to_r == 0.6r # => false
|
|
531
|
-
version_defaults.to_a.each do |k, v|
|
|
532
|
-
next unless k in Rational
|
|
533
|
-
version_defaults[k.to_f] = v
|
|
534
|
-
end
|
|
535
|
-
|
|
536
|
-
current = VERSION.to_r
|
|
537
|
-
version_defaults[:original] = Config[0]
|
|
538
|
-
version_defaults[:current] = Config[current]
|
|
539
|
-
version_defaults[:next] = Config[current + 0.1r]
|
|
540
|
-
version_defaults[:future] = Config[current + 0.2r]
|
|
541
|
-
|
|
542
|
-
version_defaults.freeze
|
|
543
|
-
|
|
544
|
-
if ($VERBOSE || $DEBUG) && self[:current].to_h != self[:default].to_h
|
|
545
|
-
warn "Misconfigured Net::IMAP::Config[:current] => %p,\n" \
|
|
546
|
-
" not equal to Net::IMAP::Config[:default] => %p" % [
|
|
547
|
-
self[:current].to_h, self[:default].to_h
|
|
548
|
-
]
|
|
549
|
-
end
|
|
550
498
|
end
|
|
551
499
|
end
|
|
552
500
|
end
|
|
@@ -307,6 +307,14 @@ module Net
|
|
|
307
307
|
# because the server doesn't allow deletion of mailboxes with children.
|
|
308
308
|
# #data is +nil+.
|
|
309
309
|
#
|
|
310
|
+
# === <tt>QUOTA=RES-*</tt> response codes
|
|
311
|
+
# See {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208.html#section-4.3].
|
|
312
|
+
# * +OVERQUOTA+ (also in RFC5530[https://www.rfc-editor.org/rfc/rfc5530]),
|
|
313
|
+
# with a tagged +NO+ response to an +APPEND+/+COPY+/+MOVE+ command when
|
|
314
|
+
# the command would put the target mailbox over any quota, and with an
|
|
315
|
+
# untagged +NO+ when a mailbox exceeds a soft quota (which may be caused
|
|
316
|
+
# be external events). #data is +nil+.
|
|
317
|
+
#
|
|
310
318
|
# === +CONDSTORE+ extension
|
|
311
319
|
# See {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html].
|
|
312
320
|
# * +NOMODSEQ+, when selecting a mailbox that does not support
|
|
@@ -384,14 +392,23 @@ module Net
|
|
|
384
392
|
# and MailboxQuota objects.
|
|
385
393
|
#
|
|
386
394
|
# == Required capability
|
|
395
|
+
#
|
|
387
396
|
# Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
|
|
388
|
-
#
|
|
397
|
+
# or <tt>QUOTA=RES-STORAGE</tt>
|
|
398
|
+
# [RFC9208[https://www.rfc-editor.org/rfc/rfc9208]] capability.
|
|
389
399
|
class MailboxQuota < Struct.new(:mailbox, :usage, :quota)
|
|
390
400
|
##
|
|
391
401
|
# method: mailbox
|
|
392
402
|
# :call-seq: mailbox -> string
|
|
393
403
|
#
|
|
394
|
-
# The
|
|
404
|
+
# The quota root with the associated quota.
|
|
405
|
+
#
|
|
406
|
+
# NOTE: this was mistakenly named "mailbox". But the quota root's name may
|
|
407
|
+
# differ from the mailbox. A single quota root may cover multiple
|
|
408
|
+
# mailboxes, and a single mailbox may be governed by multiple quota roots.
|
|
409
|
+
|
|
410
|
+
# The quota root with the associated quota.
|
|
411
|
+
alias quota_root mailbox
|
|
395
412
|
|
|
396
413
|
##
|
|
397
414
|
# method: usage
|
|
@@ -403,7 +420,7 @@ module Net
|
|
|
403
420
|
# method: quota
|
|
404
421
|
# :call-seq: quota -> Integer
|
|
405
422
|
#
|
|
406
|
-
#
|
|
423
|
+
# Storage limit imposed on the mailbox.
|
|
407
424
|
#
|
|
408
425
|
end
|
|
409
426
|
|
|
@@ -8,6 +8,7 @@ module Net
|
|
|
8
8
|
|
|
9
9
|
def initialize(client, sock)
|
|
10
10
|
@client, @sock = client, sock
|
|
11
|
+
@buff = @literal_size = nil
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def read_response_buffer
|
|
@@ -15,13 +16,13 @@ module Net
|
|
|
15
16
|
catch :eof do
|
|
16
17
|
while true
|
|
17
18
|
read_line
|
|
18
|
-
break unless
|
|
19
|
+
break unless literal_size
|
|
19
20
|
read_literal
|
|
20
21
|
end
|
|
21
22
|
end
|
|
22
23
|
buff
|
|
23
24
|
ensure
|
|
24
|
-
@buff = nil
|
|
25
|
+
@buff = @literal_size = nil
|
|
25
26
|
end
|
|
26
27
|
|
|
27
28
|
private
|
|
@@ -30,13 +31,18 @@ module Net
|
|
|
30
31
|
|
|
31
32
|
def bytes_read = buff.bytesize
|
|
32
33
|
def empty? = buff.empty?
|
|
33
|
-
def done? = line_done? && !
|
|
34
|
+
def done? = line_done? && !literal_size
|
|
34
35
|
def line_done? = buff.end_with?(CRLF)
|
|
35
|
-
|
|
36
|
+
|
|
37
|
+
def get_literal_size(buff)
|
|
38
|
+
buff.end_with?("}\r\n") && buff.rindex(/\{(\d+)\}\r\n\z/n) && $1.to_i
|
|
39
|
+
end
|
|
36
40
|
|
|
37
41
|
def read_line
|
|
38
|
-
|
|
42
|
+
line = (@sock.gets(CRLF, read_limit) or throw :eof)
|
|
43
|
+
buff << line
|
|
39
44
|
max_response_remaining! unless line_done?
|
|
45
|
+
@literal_size = get_literal_size(line)
|
|
40
46
|
end
|
|
41
47
|
|
|
42
48
|
def read_literal
|
|
@@ -75,13 +75,19 @@ module Net
|
|
|
75
75
|
# * #password ― Password or passphrase associated with this #username.
|
|
76
76
|
# * _optional_ #authzid ― Alternate identity to act as or on behalf of.
|
|
77
77
|
# * _optional_ #min_iterations - Overrides the default value (4096).
|
|
78
|
+
# * _optional_ #max_iterations - Overrides the default value (2³¹ - 1).
|
|
78
79
|
#
|
|
79
80
|
# Any other keyword parameters are quietly ignored.
|
|
81
|
+
#
|
|
82
|
+
# *NOTE:* <em>It is the user's responsibility</em> to enforce minimum
|
|
83
|
+
# and maximum iteration counts that are appropriate for their security
|
|
84
|
+
# context.
|
|
80
85
|
def initialize(username_arg = nil, password_arg = nil,
|
|
81
86
|
authcid: nil, username: nil,
|
|
82
87
|
authzid: nil,
|
|
83
88
|
password: nil, secret: nil,
|
|
84
89
|
min_iterations: 4096, # see both RFC5802 and RFC7677
|
|
90
|
+
max_iterations: 2**31 - 1, # max int32
|
|
85
91
|
cnonce: nil, # must only be set in tests
|
|
86
92
|
**options)
|
|
87
93
|
@username = username || username_arg || authcid or
|
|
@@ -94,7 +100,22 @@ module Net
|
|
|
94
100
|
@min_iterations.positive? or
|
|
95
101
|
raise ArgumentError, "min_iterations must be positive"
|
|
96
102
|
|
|
103
|
+
@max_iterations = Integer max_iterations.to_int
|
|
104
|
+
@min_iterations <= @max_iterations or
|
|
105
|
+
raise ArgumentError, "max_iterations must be more than min_iterations"
|
|
106
|
+
|
|
97
107
|
@cnonce = cnonce || SecureRandom.base64(32)
|
|
108
|
+
|
|
109
|
+
# These attrs are set from the server challenges
|
|
110
|
+
@server_first_message = @snonce = @salt = @iterations = nil
|
|
111
|
+
@server_error = nil
|
|
112
|
+
|
|
113
|
+
# Memoized after @salt and @iterations have been sent.
|
|
114
|
+
@salted_password = @client_key = @server_key = nil
|
|
115
|
+
|
|
116
|
+
# These values are created and cached in response to server challenges
|
|
117
|
+
@client_first_message_bare = nil
|
|
118
|
+
@client_final_message_without_proof = nil
|
|
98
119
|
end
|
|
99
120
|
|
|
100
121
|
# Authentication identity: the identity that matches the #password.
|
|
@@ -127,8 +148,43 @@ module Net
|
|
|
127
148
|
|
|
128
149
|
# The minimal allowed iteration count. Lower #iterations will raise an
|
|
129
150
|
# Error.
|
|
151
|
+
#
|
|
152
|
+
# *WARNING:* The default value (4096) is set to match guidance from
|
|
153
|
+
# both {RFC5802}[https://www.rfc-editor.org/rfc/rfc5802#page-12]
|
|
154
|
+
# and RFC7677[https://www.rfc-editor.org/rfc/rfc7677#section-4], but
|
|
155
|
+
# {modern recommendations}[https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2]
|
|
156
|
+
# are significantly higher.
|
|
157
|
+
#
|
|
158
|
+
# It is ultimately the server's responsibility to securely store
|
|
159
|
+
# password hashes. While this parameter can alert the user to
|
|
160
|
+
# insecure password storage and prevent insecure authentication
|
|
161
|
+
# exchange, updating the iteration count generally requires resetting
|
|
162
|
+
# the password on the server.
|
|
130
163
|
attr_reader :min_iterations
|
|
131
164
|
|
|
165
|
+
# The maximal allowed iteration count. Higher #iterations will raise an
|
|
166
|
+
# Error.
|
|
167
|
+
#
|
|
168
|
+
# As noted in {RFC5802}[https://www.rfc-editor.org/rfc/rfc5802#section-9]
|
|
169
|
+
# >>>
|
|
170
|
+
# A hostile server can perform a computational denial-of-service
|
|
171
|
+
# attack on clients by sending a big iteration count value.
|
|
172
|
+
#
|
|
173
|
+
# *WARNING:* The default value is <tt>2³¹ - 1</tt>, the maximum signed
|
|
174
|
+
# 32-bit integer. This is large enough for the computation to take
|
|
175
|
+
# several minutes, and insufficient protection against hostile servers.
|
|
176
|
+
#
|
|
177
|
+
# Note that <tt>OpenSSL::KDF.pbkdf2_hmac</tt> is implemented by a
|
|
178
|
+
# blocking C function, and cannot be interrupted by +Timeout+ or
|
|
179
|
+
# <tt>Thread.raise</tt>. And it keeps the Global VM lock, as of v4.0 of
|
|
180
|
+
# the +openssl+ gem, so other ruby threads will not be able to run.
|
|
181
|
+
#
|
|
182
|
+
# <em>To prevent a denial of service attack,</em> this must be set to a
|
|
183
|
+
# safe value, depending on hardware and version of OpenSSL. <em>It is
|
|
184
|
+
# the user's responsibility</em> to enforce minimum and maximum
|
|
185
|
+
# iteration counts that are appropriate for their security context.
|
|
186
|
+
attr_reader :max_iterations
|
|
187
|
+
|
|
132
188
|
# The client nonce, generated by SecureRandom
|
|
133
189
|
attr_reader :cnonce
|
|
134
190
|
|
|
@@ -147,6 +203,15 @@ module Net
|
|
|
147
203
|
# Net::IMAP::NoResponseError.
|
|
148
204
|
attr_reader :server_error
|
|
149
205
|
|
|
206
|
+
# Memoized ScramAlgorithm#salted_password (needs #salt and #iterations)
|
|
207
|
+
def salted_password; @salted_password ||= compute_salted { super } end
|
|
208
|
+
|
|
209
|
+
# Memoized ScramAlgorithm#client_key (needs #salt and #iterations)
|
|
210
|
+
def client_key; @client_key ||= compute_salted { super } end
|
|
211
|
+
|
|
212
|
+
# Memoized ScramAlgorithm#server_key (needs #salt and #iterations)
|
|
213
|
+
def server_key; @server_key ||= compute_salted { super } end
|
|
214
|
+
|
|
150
215
|
# Returns a new OpenSSL::Digest object, set to the appropriate hash
|
|
151
216
|
# function for the chosen mechanism.
|
|
152
217
|
#
|
|
@@ -186,6 +251,13 @@ module Net
|
|
|
186
251
|
|
|
187
252
|
private
|
|
188
253
|
|
|
254
|
+
# Checks for +salt+ and +iterations+ before yielding
|
|
255
|
+
def compute_salted
|
|
256
|
+
salt in String or raise Error, "unknown salt"
|
|
257
|
+
iterations in Integer or raise Error, "unknown iterations"
|
|
258
|
+
yield
|
|
259
|
+
end
|
|
260
|
+
|
|
189
261
|
# Need to store this for auth_message
|
|
190
262
|
attr_reader :server_first_message
|
|
191
263
|
|
|
@@ -202,6 +274,8 @@ module Net
|
|
|
202
274
|
raise Error, "server did not send iteration count"
|
|
203
275
|
min_iterations <= iterations or
|
|
204
276
|
raise Error, "too few iterations: %d" % [iterations]
|
|
277
|
+
max_iterations.nil? || iterations <= max_iterations or
|
|
278
|
+
raise Error, "too many iterations: %d" % [iterations]
|
|
205
279
|
mext = sparams["m"] and
|
|
206
280
|
raise Error, "mandatory extension: %p" % [mext]
|
|
207
281
|
snonce.start_with? cnonce or
|
|
@@ -755,7 +755,13 @@ module Net
|
|
|
755
755
|
# Related: #min, #minmax, #slice
|
|
756
756
|
def max(count = nil, star: :*)
|
|
757
757
|
if count
|
|
758
|
-
|
|
758
|
+
# n.b: #cardinality has not been backported to 0.5
|
|
759
|
+
cardinality = @tuples.sum(@tuples.count) { _2 - _1 }
|
|
760
|
+
if cardinality <= count
|
|
761
|
+
frozen? ? self : dup
|
|
762
|
+
else
|
|
763
|
+
slice(-count..) || remain_frozen_empty
|
|
764
|
+
end
|
|
759
765
|
elsif (val = @tuples.last&.last)
|
|
760
766
|
val == STAR_INT ? star : val
|
|
761
767
|
end
|
|
@@ -1024,8 +1030,9 @@ module Net
|
|
|
1024
1030
|
# Related: #delete, #delete_at, #subtract, #difference, #disjoint?
|
|
1025
1031
|
def delete?(element)
|
|
1026
1032
|
modifying! # short-circuit before input_to_tuple
|
|
1033
|
+
element = input_try_convert(element)
|
|
1027
1034
|
tuple = input_to_tuple element
|
|
1028
|
-
if
|
|
1035
|
+
if number_input?(element)
|
|
1029
1036
|
return unless include_tuple? tuple
|
|
1030
1037
|
tuple_subtract tuple
|
|
1031
1038
|
normalize!
|
|
@@ -1790,6 +1797,14 @@ module Net
|
|
|
1790
1797
|
input
|
|
1791
1798
|
end
|
|
1792
1799
|
|
|
1800
|
+
# NOTE: input_try_convert must be called on input first
|
|
1801
|
+
def number_input?(input)
|
|
1802
|
+
case input
|
|
1803
|
+
when *STARS, Integer then true
|
|
1804
|
+
when String then !input.include?(/[:,]/)
|
|
1805
|
+
end
|
|
1806
|
+
end
|
|
1807
|
+
|
|
1793
1808
|
def range_to_tuple(range)
|
|
1794
1809
|
first = to_tuple_int(range.begin || 1)
|
|
1795
1810
|
last = to_tuple_int(range.end || :*)
|
data/lib/net/imap.rb
CHANGED
|
@@ -462,6 +462,9 @@ module Net
|
|
|
462
462
|
# +LITERAL-+, and +SPECIAL-USE+.</em>
|
|
463
463
|
#
|
|
464
464
|
# ==== RFC2087: +QUOTA+
|
|
465
|
+
# +NOTE:+ Only the +STORAGE+ quota resource type is currently supported.
|
|
466
|
+
# - Obsoleted by <tt>QUOTA=RES-*</tt> [RFC9208[https://www.rfc-editor.org/rfc/rfc9208]],
|
|
467
|
+
# although the commands are backward compatible.
|
|
465
468
|
# - #getquota: returns the resource usage and limits for a quota root
|
|
466
469
|
# - #getquotaroot: returns the list of quota roots for a mailbox, as well as
|
|
467
470
|
# their resource usage and limits.
|
|
@@ -578,6 +581,16 @@ module Net
|
|
|
578
581
|
# See FetchData#emailid and FetchData#emailid.
|
|
579
582
|
# - Updates #status with support for the +MAILBOXID+ status attribute.
|
|
580
583
|
#
|
|
584
|
+
# ==== RFC9208: <tt>QUOTA=RES-*</tt>
|
|
585
|
+
# +NOTE:+ Only the +STORAGE+ quota resource type is currently supported.
|
|
586
|
+
# - Obsoletes the +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
|
|
587
|
+
# extension and provides strict semantics for different resource types.
|
|
588
|
+
# - #getquota: returns the resource usage and limits for a quota root
|
|
589
|
+
# - #getquotaroot: returns the list of quota roots for a mailbox, as well as
|
|
590
|
+
# their resource usage and limits.
|
|
591
|
+
# - #setquota: sets the resource limits for a given quota root.
|
|
592
|
+
# - Updates #status with <tt>"DELETED"</tt> and +DELETED-STORAGE+ attributes.
|
|
593
|
+
#
|
|
581
594
|
# ==== RFC9394: +PARTIAL+
|
|
582
595
|
# - Updates #search, #uid_search with the +PARTIAL+ return option which adds
|
|
583
596
|
# ESearchResult#partial return data.
|
|
@@ -698,13 +711,12 @@ module Net
|
|
|
698
711
|
#
|
|
699
712
|
# === \IMAP Extensions
|
|
700
713
|
#
|
|
701
|
-
# [QUOTA[https://www.rfc-editor.org/rfc/
|
|
702
|
-
#
|
|
703
|
-
#
|
|
714
|
+
# [QUOTA[https://www.rfc-editor.org/rfc/rfc2087]]::
|
|
715
|
+
# Myers, J., "IMAP4 QUOTA extension", RFC 2087, DOI 10.17487/RFC2087,
|
|
716
|
+
# January 1997, <https://www.rfc-editor.org/info/rfc2087>.
|
|
704
717
|
#
|
|
705
|
-
#
|
|
706
|
-
#
|
|
707
|
-
# <em>Net::IMAP does not fully support the RFC9208 updates yet.</em>
|
|
718
|
+
# *NOTE*: _obsoleted_ by RFC9208[https://www.rfc-editor.org/rfc/rfc9208]
|
|
719
|
+
# (March 2022).
|
|
708
720
|
# [IDLE[https://www.rfc-editor.org/rfc/rfc2177]]::
|
|
709
721
|
# Leiba, B., "IMAP4 IDLE command", RFC 2177, DOI 10.17487/RFC2177,
|
|
710
722
|
# June 1997, <https://www.rfc-editor.org/info/rfc2177>.
|
|
@@ -756,6 +768,11 @@ module Net
|
|
|
756
768
|
# Gondwana, B., Ed., "IMAP Extension for Object Identifiers",
|
|
757
769
|
# RFC 8474, DOI 10.17487/RFC8474, September 2018,
|
|
758
770
|
# <https://www.rfc-editor.org/info/rfc8474>.
|
|
771
|
+
# [{QUOTA=RES-*}[https://www.rfc-editor.org/rfc/rfc9208]]::
|
|
772
|
+
# Melnikov, A., "IMAP QUOTA Extension", RFC 9208, DOI 10.17487/RFC9208,
|
|
773
|
+
# March 2022, <https://www.rfc-editor.org/info/rfc9208>.
|
|
774
|
+
#
|
|
775
|
+
# Obsoletes RFC2087[https://www.rfc-editor.org/rfc/rfc2087].
|
|
759
776
|
# [PARTIAL[https://www.rfc-editor.org/info/rfc9394]]::
|
|
760
777
|
# Melnikov, A., Achuthan, A., Nagulakonda, V., and L. Alves,
|
|
761
778
|
# "IMAP PARTIAL Extension for Paged SEARCH and FETCH", RFC 9394,
|
|
@@ -769,6 +786,7 @@ module Net
|
|
|
769
786
|
#
|
|
770
787
|
# === IANA registries
|
|
771
788
|
# * {IMAP Capabilities}[http://www.iana.org/assignments/imap4-capabilities]
|
|
789
|
+
# * {IMAP Quota Resource Types}[http://www.iana.org/assignments/imap4-capabilities#imap-capabilities-2]
|
|
772
790
|
# * {IMAP Response Codes}[https://www.iana.org/assignments/imap-response-codes/imap-response-codes.xhtml]
|
|
773
791
|
# * {IMAP Mailbox Name Attributes}[https://www.iana.org/assignments/imap-mailbox-name-attributes/imap-mailbox-name-attributes.xhtml]
|
|
774
792
|
# * {IMAP and JMAP Keywords}[https://www.iana.org/assignments/imap-jmap-keywords/imap-jmap-keywords.xhtml]
|
|
@@ -779,8 +797,8 @@ module Net
|
|
|
779
797
|
# * {GSSAPI/Kerberos/SASL Service Names}[https://www.iana.org/assignments/gssapi-service-names/gssapi-service-names.xhtml]:
|
|
780
798
|
# +imap+
|
|
781
799
|
# * {Character sets}[https://www.iana.org/assignments/character-sets/character-sets.xhtml]
|
|
800
|
+
#
|
|
782
801
|
# ==== For currently unsupported features:
|
|
783
|
-
# * {IMAP Quota Resource Types}[http://www.iana.org/assignments/imap4-capabilities#imap-capabilities-2]
|
|
784
802
|
# * {LIST-EXTENDED options and responses}[https://www.iana.org/assignments/imap-list-extended/imap-list-extended.xhtml]
|
|
785
803
|
# * {IMAP METADATA Server Entry and Mailbox Entry Registries}[https://www.iana.org/assignments/imap-metadata/imap-metadata.xhtml]
|
|
786
804
|
# * {IMAP ANNOTATE Extension Entries and Attributes}[https://www.iana.org/assignments/imap-annotate-extension/imap-annotate-extension.xhtml]
|
|
@@ -788,7 +806,7 @@ module Net
|
|
|
788
806
|
# * {IMAP URLAUTH Authorization Mechanism Registry}[https://www.iana.org/assignments/urlauth-authorization-mechanism-registry/urlauth-authorization-mechanism-registry.xhtml]
|
|
789
807
|
#
|
|
790
808
|
class IMAP < Protocol
|
|
791
|
-
VERSION = "0.5.
|
|
809
|
+
VERSION = "0.5.14"
|
|
792
810
|
|
|
793
811
|
# Aliases for supported capabilities, to be used with the #enable command.
|
|
794
812
|
ENABLE_ALIASES = {
|
|
@@ -1394,9 +1412,11 @@ module Net
|
|
|
1394
1412
|
#
|
|
1395
1413
|
def starttls(**options)
|
|
1396
1414
|
@ssl_ctx_params, @ssl_ctx = build_ssl_ctx(options)
|
|
1415
|
+
handled = false
|
|
1397
1416
|
error = nil
|
|
1398
1417
|
ok = send_command("STARTTLS") do |resp|
|
|
1399
1418
|
if resp.kind_of?(TaggedResponse) && resp.name == "OK"
|
|
1419
|
+
handled = true
|
|
1400
1420
|
clear_cached_capabilities
|
|
1401
1421
|
clear_responses
|
|
1402
1422
|
start_tls_session
|
|
@@ -1408,6 +1428,13 @@ module Net
|
|
|
1408
1428
|
disconnect
|
|
1409
1429
|
raise error
|
|
1410
1430
|
end
|
|
1431
|
+
unless handled
|
|
1432
|
+
disconnect
|
|
1433
|
+
raise InvalidResponseError,
|
|
1434
|
+
"STARTTLS handler was bypassed, although server responded %p" % [
|
|
1435
|
+
ok.raw_data.chomp
|
|
1436
|
+
]
|
|
1437
|
+
end
|
|
1411
1438
|
ok
|
|
1412
1439
|
end
|
|
1413
1440
|
|
|
@@ -1828,12 +1855,18 @@ module Net
|
|
|
1828
1855
|
# to both admin and user. If this mailbox exists, it returns an array
|
|
1829
1856
|
# containing objects of type MailboxQuotaRoot and MailboxQuota.
|
|
1830
1857
|
#
|
|
1858
|
+
# *NOTE:* Currently, Net::IMAP only supports +QUOTA+ responses with a single
|
|
1859
|
+
# resource type. This is usually +STORAGE+, but you may need to verify this
|
|
1860
|
+
# with UntaggedResponse#raw_data.
|
|
1861
|
+
#
|
|
1831
1862
|
# Related: #getquota, #setquota, MailboxQuotaRoot, MailboxQuota
|
|
1832
1863
|
#
|
|
1833
1864
|
# ==== Capabilities
|
|
1834
1865
|
#
|
|
1835
|
-
#
|
|
1836
|
-
#
|
|
1866
|
+
# Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
|
|
1867
|
+
# capability, or a capability prefixed with <tt>QUOTA=RES-*</tt>
|
|
1868
|
+
# {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208] for each supported
|
|
1869
|
+
# resource type.
|
|
1837
1870
|
def getquotaroot(mailbox)
|
|
1838
1871
|
synchronize do
|
|
1839
1872
|
send_command("GETQUOTAROOT", mailbox)
|
|
@@ -1845,41 +1878,59 @@ module Net
|
|
|
1845
1878
|
end
|
|
1846
1879
|
|
|
1847
1880
|
# Sends a {GETQUOTA command [RFC2087 §4.2]}[https://www.rfc-editor.org/rfc/rfc2087#section-4.2]
|
|
1848
|
-
#
|
|
1849
|
-
# containing a MailboxQuota object is returned.
|
|
1850
|
-
#
|
|
1881
|
+
# for the +quota_root+. If this quota root exists, then an array
|
|
1882
|
+
# containing a MailboxQuota object is returned.
|
|
1883
|
+
#
|
|
1884
|
+
# The names of quota roots that are applicable to a particular mailbox can
|
|
1885
|
+
# be discovered with #getquotaroot.
|
|
1886
|
+
#
|
|
1887
|
+
# *NOTE:* Currently, Net::IMAP only supports +QUOTA+ responses with a single
|
|
1888
|
+
# resource type. This is usually +STORAGE+, but you may need to verify this
|
|
1889
|
+
# with UntaggedResponse#raw_data.
|
|
1851
1890
|
#
|
|
1852
1891
|
# Related: #getquotaroot, #setquota, MailboxQuota
|
|
1853
1892
|
#
|
|
1854
1893
|
# ==== Capabilities
|
|
1855
1894
|
#
|
|
1856
|
-
#
|
|
1857
|
-
#
|
|
1858
|
-
|
|
1895
|
+
# Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
|
|
1896
|
+
# capability, or a capability prefixed with <tt>QUOTA=RES-*</tt>
|
|
1897
|
+
# {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208] for each supported
|
|
1898
|
+
# resource type.
|
|
1899
|
+
def getquota(quota_root)
|
|
1859
1900
|
synchronize do
|
|
1860
|
-
send_command("GETQUOTA",
|
|
1901
|
+
send_command("GETQUOTA", quota_root)
|
|
1861
1902
|
clear_responses("QUOTA")
|
|
1862
1903
|
end
|
|
1863
1904
|
end
|
|
1864
1905
|
|
|
1865
1906
|
# Sends a {SETQUOTA command [RFC2087 §4.1]}[https://www.rfc-editor.org/rfc/rfc2087#section-4.1]
|
|
1866
|
-
# along with the specified +
|
|
1867
|
-
# +
|
|
1868
|
-
#
|
|
1907
|
+
# along with the specified +quota_root+ and +storage_limit+. If
|
|
1908
|
+
# +storage_limit+ is +nil+, resource limits are unset for that quota root.
|
|
1909
|
+
# If +storage_limit+ is a number, it sets the +STORAGE+ resource limit.
|
|
1910
|
+
#
|
|
1911
|
+
# imap.setquota "#user/alice", 100
|
|
1912
|
+
# imap.getquota "#user/alice"
|
|
1913
|
+
# # => [#<struct Net::IMAP::MailboxQuota mailbox="#user/alice" usage=54 quota=100>]
|
|
1914
|
+
#
|
|
1915
|
+
# Typically one needs to be logged in as a server admin for this to work.
|
|
1916
|
+
#
|
|
1917
|
+
# *NOTE:* Currently, Net::IMAP only supports setting +STORAGE+ quota limits.
|
|
1869
1918
|
#
|
|
1870
1919
|
# Related: #getquota, #getquotaroot
|
|
1871
1920
|
#
|
|
1872
1921
|
# ==== Capabilities
|
|
1873
1922
|
#
|
|
1874
|
-
#
|
|
1875
|
-
#
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1923
|
+
# Requires +QUOTA+ [RFC2087[https://www.rfc-editor.org/rfc/rfc2087]]
|
|
1924
|
+
# capability, or a capability prefixed with <tt>QUOTA=RES-*</tt>
|
|
1925
|
+
# {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208] for each supported
|
|
1926
|
+
# resource type.
|
|
1927
|
+
def setquota(quota_root, storage_limit)
|
|
1928
|
+
if storage_limit.nil?
|
|
1929
|
+
list = []
|
|
1879
1930
|
else
|
|
1880
|
-
|
|
1931
|
+
list = ["STORAGE", Integer(storage_limit)]
|
|
1881
1932
|
end
|
|
1882
|
-
send_command("SETQUOTA",
|
|
1933
|
+
send_command("SETQUOTA", quota_root, list)
|
|
1883
1934
|
end
|
|
1884
1935
|
|
|
1885
1936
|
# Sends a {SETACL command [RFC4314 §3.1]}[https://www.rfc-editor.org/rfc/rfc4314#section-3.1]
|
|
@@ -1986,7 +2037,10 @@ module Net
|
|
|
1986
2037
|
# <tt>STATUS=SIZE</tt>
|
|
1987
2038
|
# {[RFC8483]}[https://www.rfc-editor.org/rfc/rfc8483.html].
|
|
1988
2039
|
#
|
|
1989
|
-
# +DELETED+
|
|
2040
|
+
# +DELETED+ must be supported when the server's capabilities includes
|
|
2041
|
+
# +IMAP4rev2+.
|
|
2042
|
+
# or <tt>QUOTA=RES-MESSAGES</tt>
|
|
2043
|
+
# {[RFC9208]}[https://www.rfc-editor.org/rfc/rfc9208.html].
|
|
1990
2044
|
#
|
|
1991
2045
|
# +HIGHESTMODSEQ+ requires the server's capabilities to include +CONDSTORE+
|
|
1992
2046
|
# {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html].
|
|
@@ -2267,11 +2321,11 @@ module Net
|
|
|
2267
2321
|
# Encoded as an \IMAP date (see ::encode_date).
|
|
2268
2322
|
#
|
|
2269
2323
|
# [When +criteria+ is a String]
|
|
2270
|
-
# +criteria+ will be sent
|
|
2271
|
-
#
|
|
2324
|
+
# +criteria+ will be sent to the server <em>with minimal validation and no
|
|
2325
|
+
# encoding or formatting</em>.
|
|
2272
2326
|
#
|
|
2273
|
-
# <em>*WARNING:*
|
|
2274
|
-
#
|
|
2327
|
+
# <em>*WARNING:* Although CRLF is prohibited, this is vulnerable to other
|
|
2328
|
+
# types of attribute injection attack if unvetted user input is used.</em>
|
|
2275
2329
|
#
|
|
2276
2330
|
# ==== Supported return options
|
|
2277
2331
|
#
|
|
@@ -2592,6 +2646,13 @@ module Net
|
|
|
2592
2646
|
#
|
|
2593
2647
|
# +attr+ is a list of attributes to fetch; see FetchStruct documentation for
|
|
2594
2648
|
# a list of supported attributes.
|
|
2649
|
+
# >>>
|
|
2650
|
+
# When +attr+ is a String, it will be sent <em>with minimal validation and
|
|
2651
|
+
# no encoding or formatting</em>. When +attr+ is an Array, each String in
|
|
2652
|
+
# +attr+ will be sent this way.
|
|
2653
|
+
#
|
|
2654
|
+
# <em>*WARNING:* Although CRLF is prohibited, this is vulnerable to other
|
|
2655
|
+
# types of attribute injection attack if unvetted user input is used.</em>
|
|
2595
2656
|
#
|
|
2596
2657
|
# +changedsince+ is an optional integer mod-sequence. It limits results to
|
|
2597
2658
|
# messages with a mod-sequence greater than +changedsince+.
|
|
@@ -2675,6 +2736,7 @@ module Net
|
|
|
2675
2736
|
# # fetch should return quickly and allocate little memory
|
|
2676
2737
|
# results.size # => 0..500
|
|
2677
2738
|
# break if results.empty?
|
|
2739
|
+
# results.sort_by!(&:uid) # server may return results out of order
|
|
2678
2740
|
# next_uid_to_fetch = results.last.uid + 1
|
|
2679
2741
|
# process results
|
|
2680
2742
|
# end
|
|
@@ -3079,6 +3141,7 @@ module Net
|
|
|
3079
3141
|
|
|
3080
3142
|
synchronize do
|
|
3081
3143
|
tag = Thread.current[:net_imap_tag] = generate_tag
|
|
3144
|
+
guard_against_tagged_response_skipping_handler!(tag, "IDLE")
|
|
3082
3145
|
put_string("#{tag} IDLE#{CRLF}")
|
|
3083
3146
|
|
|
3084
3147
|
begin
|
|
@@ -3232,7 +3295,7 @@ module Net
|
|
|
3232
3295
|
warn(RESPONSES_DEPRECATION_MSG, uplevel: 1, category: :deprecated)
|
|
3233
3296
|
when :frozen_dup
|
|
3234
3297
|
synchronize {
|
|
3235
|
-
responses = @responses.transform_values
|
|
3298
|
+
responses = @responses.transform_values { _1.dup.freeze }
|
|
3236
3299
|
responses.default_proc = nil
|
|
3237
3300
|
responses.default = [].freeze
|
|
3238
3301
|
return responses.freeze
|
|
@@ -3543,6 +3606,7 @@ module Net
|
|
|
3543
3606
|
put_string(" ")
|
|
3544
3607
|
send_data(i, tag)
|
|
3545
3608
|
end
|
|
3609
|
+
guard_against_tagged_response_skipping_handler!(tag, cmd)
|
|
3546
3610
|
put_string(CRLF)
|
|
3547
3611
|
if cmd == "LOGOUT"
|
|
3548
3612
|
@logout_command_tag = tag
|
|
@@ -3558,6 +3622,19 @@ module Net
|
|
|
3558
3622
|
end
|
|
3559
3623
|
end
|
|
3560
3624
|
end
|
|
3625
|
+
rescue InvalidResponseError
|
|
3626
|
+
disconnect
|
|
3627
|
+
raise
|
|
3628
|
+
end
|
|
3629
|
+
|
|
3630
|
+
def guard_against_tagged_response_skipping_handler!(tag, cmd)
|
|
3631
|
+
return unless (resp = @tagged_responses[tag])&.name&.upcase == "OK"
|
|
3632
|
+
raise InvalidResponseError, format(
|
|
3633
|
+
"Received tagged 'OK' to incomplete %s command (tag=%s). " \
|
|
3634
|
+
"This could indicate a malicious server, a man-in-the-middle, or " \
|
|
3635
|
+
"client-side command injection. Disconnecting.",
|
|
3636
|
+
cmd, tag
|
|
3637
|
+
)
|
|
3561
3638
|
end
|
|
3562
3639
|
|
|
3563
3640
|
def generate_tag
|
|
@@ -3711,7 +3788,7 @@ module Net
|
|
|
3711
3788
|
end
|
|
3712
3789
|
|
|
3713
3790
|
def store_internal(cmd, set, attr, flags, unchangedsince: nil)
|
|
3714
|
-
attr =
|
|
3791
|
+
attr = Atom.new(attr) if attr.instance_of?(String)
|
|
3715
3792
|
args = [SequenceSet.new(set)]
|
|
3716
3793
|
args << ["UNCHANGEDSINCE", Integer(unchangedsince)] if unchangedsince
|
|
3717
3794
|
args << attr << flags
|
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.5.
|
|
4
|
+
version: 0.5.14
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Shugo Maeda
|
|
@@ -60,6 +60,7 @@ files:
|
|
|
60
60
|
- lib/net/imap/config/attr_accessors.rb
|
|
61
61
|
- lib/net/imap/config/attr_inheritance.rb
|
|
62
62
|
- lib/net/imap/config/attr_type_coercion.rb
|
|
63
|
+
- lib/net/imap/config/attr_version_defaults.rb
|
|
63
64
|
- lib/net/imap/connection_state.rb
|
|
64
65
|
- lib/net/imap/data_encoding.rb
|
|
65
66
|
- lib/net/imap/data_lite.rb
|
|
@@ -129,7 +130,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
129
130
|
- !ruby/object:Gem::Version
|
|
130
131
|
version: '0'
|
|
131
132
|
requirements: []
|
|
132
|
-
rubygems_version:
|
|
133
|
+
rubygems_version: 4.0.6
|
|
133
134
|
specification_version: 4
|
|
134
135
|
summary: Ruby client api for Internet Message Access Protocol
|
|
135
136
|
test_files: []
|