net-imap 0.4.24 → 0.4.25

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9419630b908b12c7f89846682dae953e50376d89ebb3b7391b3426bb34480991
4
- data.tar.gz: 4558a77d38a4def28af960c201ca9359fcc828d5a316f76f95cf3c0e575702b6
3
+ metadata.gz: bf05b4f7aec9127531e83134b740620da216f847851f8ca464ef54e71e3431ad
4
+ data.tar.gz: 18361d6ee25983da0e4814d42a0ec851e10ce88789559e30778a945a7024a297
5
5
  SHA512:
6
- metadata.gz: 5f4baf570c8ed5732493ba801121ae092177fca3ab5f9162ef66f0db2a23e91c09fe740d8ddba52977e65737906a6735a8405bb94e923111970621e79b813141
7
- data.tar.gz: 624e1b8a76630bffa007391b303eab5729b20665d9a5242f58749e0127340ddd1a842eb94ca46da085ccfa91cc093367348586cde705b51fe3eff25882f8f096
6
+ metadata.gz: 697214c436f129746ad8986dde31e4b819854cf131472158c4aacf5a35ef665b8529528bd0a843ba17d9be105711b4163ecccfa525989d9fe0b3f807f4ae9d58
7
+ data.tar.gz: ef657a1dbfd5bc73fa317248ae19589a842f87eabfe966b43b026e9dceff68d510621d3e17136b266d22f68b85d20fc22b4ea67a482fdca193d8a5290c643abf
@@ -14,14 +14,15 @@ module Net
14
14
  when nil
15
15
  when String
16
16
  when Integer
17
- NumValidator.ensure_number(data)
17
+ # Covers modseq-valzer, which is the largest valid IMAP integer
18
+ if data.negative?
19
+ raise DataFormatError, "Integer argument must be unsigned: #{data}"
20
+ elsif 0xffff_ffff_ffff_ffff < data
21
+ raise DataFormatError, "Integer argument must fit in 64 bits: #{data}"
22
+ end
18
23
  when Array
19
- if data[0] == 'CHANGEDSINCE'
20
- NumValidator.ensure_mod_sequence_value(data[1])
21
- else
22
- data.each do |i|
23
- validate_data(i)
24
- end
24
+ data.each do |i|
25
+ validate_data(i)
25
26
  end
26
27
  when Time, Date, DateTime
27
28
  when Symbol
@@ -82,15 +83,23 @@ module Net
82
83
 
83
84
  # `non_sync` is an optional tri-state flag:
84
85
  # * `true` -> Force non-synchronizing `LITERAL+`/`LITERAL-` behavior.
85
- # TODO: raise or warn when capabilities don't allow non_sync.
86
+ # NOTE: raises DataFormatError when server doesn't support
87
+ # non-synchronizing literal, or literal is too large for LITERAL-.
86
88
  # * `false` -> Force normal synchronizing literal behavior.
87
89
  # * `nil` -> (default) Currently behaves like `false` (will be dynamic).
88
90
  # TODO: Dynamic, based on capabilities and bytesize.
89
91
  def send_literal(str, tag = nil, binary: false, non_sync: nil)
92
+ bytesize = str.bytesize
90
93
  synchronize do
94
+ if non_sync && !non_sync_literal_allowed?(bytesize)
95
+ # TODO: check in Printer, so we don't need to close the connection.
96
+ @sock.close
97
+ raise DataFormatError, "Connection closed: " \
98
+ "Cannot send non-synchronizing literal without known server support"
99
+ end
91
100
  prefix = "~" if binary
92
101
  plus = "+" if non_sync
93
- put_string("#{prefix}{#{str.bytesize}#{plus}}\r\n")
102
+ put_string("#{prefix}{#{bytesize}#{plus}}\r\n")
94
103
  if non_sync
95
104
  put_string(str)
96
105
  return
@@ -109,8 +118,18 @@ module Net
109
118
  end
110
119
  end
111
120
 
121
+ def non_sync_literal_allowed?(bytesize)
122
+ return unless capabilities_cached?
123
+ return "+" if capable?("LITERAL+")
124
+ return "-" if capable_literal_minus? && bytesize <= 4096
125
+ false
126
+ end
127
+
128
+ def capable_literal_minus?; capable?("LITERAL-") || capable?("IMAP4rev2") end
129
+
130
+ # NOTE: +num+ should already be an Integer
112
131
  def send_number_data(num)
113
- put_string(num.to_s)
132
+ put_string(Integer(num).to_s)
114
133
  end
115
134
 
116
135
  def send_list_data(list, tag = nil)
@@ -165,36 +184,38 @@ module Net
165
184
  end
166
185
  end
167
186
 
168
- # Represents IMAP +text+ data, which may contain any 7-bit ASCII character,
169
- # except for +NULL+, +CR+, or +LF+. +text+ is extended to allow any
170
- # multibyte +UTF-8+ character when either +UTF8=ACCEPT+ or +IMAP4rev2+ have
171
- # been enabled, or when the server supports only +IMAP4rev2+ and not earlier
172
- # IMAP revisions, or when the server advertises +UTF8=ONLY+.
187
+ # Represents IMAP +text+ or +quoted+ data, which share the same
188
+ # validations of decoded #data, and differ only in how they are formatted.
189
+ #
190
+ # +data+ may contain any 7-bit ASCII character except +NULL+, +CR+, or +LF+.
191
+ # Any multibyte +UTF-8+ character is also allowed when the connection
192
+ # supports UTF8: either +UTF8=ACCEPT+ or +IMAP4rev2+ have been enabled, or
193
+ # the server supports only +IMAP4rev2+ and not earlier IMAP revisions, or
194
+ # the server advertises +UTF8=ONLY+.
173
195
  #
174
- # NOTE: The current implementation does not validate whether the connection
175
- # currently supports UTF-8. Future versions may change.
196
+ # NOTE: This does not verify whether the connection supports UTF-8, but that
197
+ # may change in future versions.
176
198
  #
177
199
  # The string's bytes must be valid ASCII or valid UTF-8. The string's
178
200
  # reported encoding is ignored, but the string is _not_ transcoded.
179
- class RawText < CommandData # :nodoc:
201
+ class ValidNonLiteralData < CommandData
180
202
  def initialize(data:)
181
203
  data = String(data.to_str)
182
- data = if [Encoding::ASCII, Encoding::UTF_8].include?(data.encoding)
183
- -data
184
- elsif data.ascii_only?
185
- -(data.dup.force_encoding("ASCII"))
186
- else
187
- -(data.dup.force_encoding("UTF-8"))
204
+ unless [Encoding::ASCII, Encoding::UTF_8].include?(data.encoding)
205
+ data = data.dup.force_encoding(data.ascii_only? ? "ASCII" : "UTF-8")
188
206
  end
207
+ data = -data
189
208
  super
190
209
  validate
191
210
  end
192
211
 
193
212
  def validate
194
- if data.include?("\0")
195
- raise DataFormatError, "NULL byte must be binary literal encoded"
213
+ if ![Encoding::ASCII, Encoding::UTF_8].include?(data.encoding)
214
+ raise DataFormatError, "must use ASCII or UTF-8 encoding"
196
215
  elsif !data.valid_encoding?
197
216
  raise DataFormatError, "invalid UTF-8 must be literal encoded"
217
+ elsif data.include?("\0")
218
+ raise DataFormatError, "NULL byte must be binary literal encoded"
198
219
  elsif /[\r\n]/.match?(data)
199
220
  raise DataFormatError, "CR and LF bytes must be literal encoded"
200
221
  end
@@ -202,12 +223,31 @@ module Net
202
223
 
203
224
  def ascii_only?; data.ascii_only? end
204
225
 
205
- def send_data(imap, tag) imap.__send__(:put_string, data) end
226
+ def send_data(imap, tag = nil) imap.__send__(:put_string, formatted) end
227
+ end
228
+
229
+ # Represents IMAP +text+ data, which covers everything in the IMAP grammar,
230
+ # except for +literal+, +literal8+, and the concluding +CRLF+.
231
+ #
232
+ # NOTE: The current implementation does not verify that the connection
233
+ # supports UTF-8. Future versions may validate this.
234
+ class RawText < ValidNonLiteralData # :nodoc:
235
+ # raw: no formatting necessary
236
+ alias formatted data
206
237
  end
207
238
 
208
239
  class RawData < CommandData # :nodoc:
209
240
  def initialize(data:)
210
- data = split_parts(data)
241
+ case data
242
+ when String
243
+ data = self.class.split(data)
244
+ when Array
245
+ unless data.all? { |part| RawText === part || Literal === part }
246
+ raise TypeError, "expected String or Array[#{RawText} | #{Literal}]"
247
+ end
248
+ else
249
+ raise TypeError, "expected String or Array[#{RawText} | #{Literal}]"
250
+ end
211
251
  super
212
252
  validate
213
253
  end
@@ -217,14 +257,16 @@ module Net
217
257
  def validate
218
258
  return unless RawText === data.last
219
259
  text = data.last.data
220
- if text.rindex(/~?\{[1-9]\d*\+?\}\z/n)
260
+ if text.rindex(/\{\d+\+?\}\z/n)
221
261
  raise DataFormatError, "RawData cannot end with literal continuation"
222
262
  end
223
263
  end
224
264
 
225
- private
226
-
227
- def split_parts(data)
265
+ # Splits an input +string+ into an array of RawText and Literal/Literal8.
266
+ #
267
+ # NOTE: unlike RawData#validate, this does not prevent the final RawText
268
+ # from ending with a literal prefix.
269
+ def self.split(data)
228
270
  data = data.b # dups and ensures BINARY encoding
229
271
  parts = []
230
272
  while data.match(/(~)?\{(0|[1-9]\d*)(\+)?\}\r\n/n)
@@ -241,7 +283,7 @@ module Net
241
283
  parts
242
284
  end
243
285
 
244
- def extract_literal(data, binary:, bytesize:, non_sync:)
286
+ def self.extract_literal(data, binary:, bytesize:, non_sync:)
245
287
  if data.bytesize < bytesize
246
288
  raise DataFormatError, "Too few bytes in string for literal, " \
247
289
  "expected: %s, remaining: %s" % [bytesize, data.bytesize]
@@ -249,6 +291,7 @@ module Net
249
291
  literal = data.byteslice(0, bytesize)
250
292
  (binary ? Literal8 : Literal).new(data: literal, non_sync: non_sync)
251
293
  end
294
+ private_class_method :extract_literal
252
295
  end
253
296
 
254
297
  class Atom < CommandData # :nodoc:
@@ -262,6 +305,8 @@ module Net
262
305
  or raise DataFormatError, "#{self.class} must be ASCII only"
263
306
  data.match?(ResponseParser::Patterns::ATOM_SPECIALS) \
264
307
  and raise DataFormatError, "#{self.class} must not contain atom-specials"
308
+ data.empty? \
309
+ and raise DataFormatError, "#{self.class} must not be empty"
265
310
  end
266
311
 
267
312
  def send_data(imap, tag)
@@ -275,19 +320,13 @@ module Net
275
320
  end
276
321
  end
277
322
 
278
- class QuotedString # :nodoc:
279
- def send_data(imap, tag)
280
- imap.__send__(:send_quoted_string, @data)
281
- end
282
-
283
- def validate
284
- end
285
-
286
- private
287
-
288
- def initialize(data)
289
- @data = data
290
- end
323
+ # Represents a IMAP +quoted+ string, which can encode any valid ASCII or
324
+ # UTF-8 string, unless it contains any +CR+, +LF+, or +NULL+ bytes.
325
+ #
326
+ # NOTE: The current implementation does not verify that the connection
327
+ # supports UTF-8. Future versions may validate this.
328
+ class QuotedString < ValidNonLiteralData # :nodoc:
329
+ def formatted; %("#{data.gsub(/["\\]/, "\\\\\\&")}") end
291
330
  end
292
331
 
293
332
  class Literal # :nodoc:
@@ -2055,10 +2055,7 @@ module Net
2055
2055
  if $1
2056
2056
  return Token.new(T_SPACE, $+)
2057
2057
  elsif $2
2058
- len = $+.to_i
2059
- val = @str[@pos, len]
2060
- @pos += len
2061
- return Token.new(T_LITERAL8, val)
2058
+ literal_token($+, T_LITERAL8)
2062
2059
  elsif $3 && $7
2063
2060
  # greedily match ATOM, prefixed with NUMBER, NIL, or PLUS.
2064
2061
  return Token.new(T_ATOM, $3)
@@ -2086,10 +2083,7 @@ module Net
2086
2083
  elsif $15
2087
2084
  return Token.new(T_RBRA, $+)
2088
2085
  elsif $16
2089
- len = $+.to_i
2090
- val = @str[@pos, len]
2091
- @pos += len
2092
- return Token.new(T_LITERAL, val)
2086
+ literal_token($+)
2093
2087
  elsif $17
2094
2088
  return Token.new(T_PERCENT, $+)
2095
2089
  elsif $18
@@ -2115,10 +2109,7 @@ module Net
2115
2109
  elsif $4
2116
2110
  return Token.new(T_QUOTED, Patterns.unescape_quoted($+))
2117
2111
  elsif $5
2118
- len = $+.to_i
2119
- val = @str[@pos, len]
2120
- @pos += len
2121
- return Token.new(T_LITERAL, val)
2112
+ literal_token($+)
2122
2113
  elsif $6
2123
2114
  return Token.new(T_LPAR, $+)
2124
2115
  elsif $7
@@ -2133,6 +2124,23 @@ module Net
2133
2124
  else
2134
2125
  parse_error("invalid @lex_state - %s", @lex_state.inspect)
2135
2126
  end
2127
+ rescue DataFormatError => error
2128
+ parse_error error.message
2129
+ end
2130
+
2131
+ def literal_token(len, type = T_LITERAL)
2132
+ len = coerce_number64 len.to_i
2133
+ val = @str[@pos, len]
2134
+ @pos += len
2135
+ Token.new(type, val)
2136
+ end
2137
+
2138
+ # copied/adapted from NumValidator in v0.6
2139
+ def coerce_number64(num)
2140
+ int = num.to_i
2141
+ return int if 0 <= int && int <= 0x7fff_ffff_ffff_ffff
2142
+ raise DataFormatError,
2143
+ "number64 must be unsigned 63-bit integer: #{num}"
2136
2144
  end
2137
2145
 
2138
2146
  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)
@@ -31,12 +33,14 @@ module Net
31
33
 
32
34
  def bytes_read; buff.bytesize end
33
35
  def empty?; buff.empty? end
34
- def done?; line_done? && !get_literal_size end
35
36
  def done?; line_done? && !literal_size end
36
37
  def line_done?; buff.end_with?(CRLF) end
37
38
 
38
39
  def get_literal_size(buff)
39
- buff.end_with?("}\r\n") && buff.rindex(/\{(\d+)\}\r\n\z/n) && $1.to_i
40
+ buff.end_with?("}\r\n") && buff.rindex(/\{(\d+)\}\r\n\z/n) &&
41
+ coerce_number64($1)
42
+ rescue DataFormatError
43
+ raise DataFormatError, format("invalid response literal size (%s)", $1)
40
44
  end
41
45
 
42
46
  def read_line
@@ -77,6 +81,14 @@ module Net
77
81
  )
78
82
  end
79
83
 
84
+ # copied/adapted from NumValidator in v0.6
85
+ def coerce_number64(num)
86
+ int = num.to_i
87
+ return int if 0 <= int && int <= 0x7fff_ffff_ffff_ffff
88
+ raise DataFormatError,
89
+ "number64 must be unsigned 63-bit integer: #{num}"
90
+ end
91
+
80
92
  end
81
93
  end
82
94
  end
data/lib/net/imap.rb CHANGED
@@ -779,7 +779,7 @@ module Net
779
779
  # * {IMAP URLAUTH Authorization Mechanism Registry}[https://www.iana.org/assignments/urlauth-authorization-mechanism-registry/urlauth-authorization-mechanism-registry.xhtml]
780
780
  #
781
781
  class IMAP < Protocol
782
- VERSION = "0.4.24"
782
+ VERSION = "0.4.25"
783
783
 
784
784
  # Aliases for supported capabilities, to be used with the #enable command.
785
785
  ENABLE_ALIASES = {
@@ -1049,26 +1049,22 @@ module Net
1049
1049
 
1050
1050
  # Disconnects from the server.
1051
1051
  #
1052
+ # Waits for receiver thread to close before returning. Slow or stuck
1053
+ # response handlers can cause #disconnect to hang until they complete.
1054
+ #
1052
1055
  # Related: #logout, #logout!
1053
1056
  def disconnect
1054
1057
  return if disconnected?
1058
+ in_receiver_thread = Thread.current == @receiver_thread
1055
1059
  begin
1056
- begin
1057
- # try to call SSL::SSLSocket#io.
1058
- @sock.io.shutdown
1059
- rescue NoMethodError
1060
- # @sock is not an SSL::SSLSocket.
1061
- @sock.shutdown
1062
- end
1060
+ @sock.to_io.shutdown
1063
1061
  rescue Errno::ENOTCONN
1064
1062
  # ignore `Errno::ENOTCONN: Socket is not connected' on some platforms.
1065
1063
  rescue Exception => e
1066
- @receiver_thread.raise(e)
1067
- end
1068
- @receiver_thread.join
1069
- synchronize do
1070
- @sock.close
1064
+ @receiver_thread.raise(e) unless in_receiver_thread
1071
1065
  end
1066
+ @sock.close
1067
+ @receiver_thread.join unless mon_owned? || in_receiver_thread
1072
1068
  raise e if e
1073
1069
  end
1074
1070
 
@@ -2095,6 +2091,8 @@ module Net
2095
2091
  # string holding the entire search string, or a single-dimension array of
2096
2092
  # search keywords and arguments.
2097
2093
  #
2094
+ # <em>Please note</em> the warning (below) when +keys+ is a String.
2095
+ #
2098
2096
  # Returns a SearchResult object. SearchResult inherits from Array (for
2099
2097
  # backward compatibility) but adds SearchResult#modseq when the +CONDSTORE+
2100
2098
  # capability has been enabled.
@@ -2104,8 +2102,8 @@ module Net
2104
2102
  # ===== Search criteria
2105
2103
  #
2106
2104
  # >>>
2107
- # When +criteria+ is an Array, elements in the array will be validated and
2108
- # formatted. When +criteria+ is a String, it will be sent <em>with
2105
+ # When +keys+ is an Array, elements in the array will be validated and
2106
+ # formatted. When +keys+ is a String, it will be sent <em>with
2109
2107
  # minimal validation and no encoding or formatting</em>.
2110
2108
  #
2111
2109
  # <em>*WARNING:* Although CRLF is prohibited, this is vulnerable to other
@@ -2176,7 +2174,8 @@ module Net
2176
2174
  # backward compatibility) but adds SearchResult#modseq when the +CONDSTORE+
2177
2175
  # capability has been enabled.
2178
2176
  #
2179
- # See #search for documentation of search criteria.
2177
+ # See #search for documentation of parameters. <em>Please note</em> the
2178
+ # warning for when +keys+ is a String.
2180
2179
  def uid_search(keys, charset = nil)
2181
2180
  return search_internal("UID SEARCH", keys, charset)
2182
2181
  end
@@ -2253,6 +2252,8 @@ module Net
2253
2252
  # Similar to #fetch, but the +set+ parameter contains unique identifiers
2254
2253
  # instead of message sequence numbers.
2255
2254
  #
2255
+ # +attr+ behaves the same as with #fetch. <em>Please note</em> the #fetch
2256
+ # warning on the +attr+ argument.
2256
2257
  # >>>
2257
2258
  # *Note:* Servers _MUST_ implicitly include the +UID+ message data item as
2258
2259
  # part of any +FETCH+ response caused by a +UID+ command, regardless of
@@ -2405,8 +2406,10 @@ module Net
2405
2406
 
2406
2407
  # Sends a {SORT command [RFC5256 §3]}[https://www.rfc-editor.org/rfc/rfc5256#section-3]
2407
2408
  # to search a mailbox for messages that match +search_keys+ and return an
2408
- # array of message sequence numbers, sorted by +sort_keys+. +search_keys+
2409
- # are interpreted the same as for #search.
2409
+ # array of message sequence numbers, sorted by +sort_keys+.
2410
+ #
2411
+ # +search_keys+ are interpreted the same as the +criteria+ argument for
2412
+ # #search. <em>Please note</em> the #search warning for String +criteria+.
2410
2413
  #
2411
2414
  #--
2412
2415
  # TODO: describe +sort_keys+
@@ -2431,8 +2434,10 @@ module Net
2431
2434
 
2432
2435
  # Sends a {UID SORT command [RFC5256 §3]}[https://www.rfc-editor.org/rfc/rfc5256#section-3]
2433
2436
  # to search a mailbox for messages that match +search_keys+ and return an
2434
- # array of unique identifiers, sorted by +sort_keys+. +search_keys+ are
2435
- # interpreted the same as for #search.
2437
+ # array of unique identifiers, sorted by +sort_keys+.
2438
+ #
2439
+ # +search_keys+ are interpreted the same as the +criteria+ argument for
2440
+ # #search. <em>Please note</em> the #search warning for String +criteria+.
2436
2441
  #
2437
2442
  # Related: #sort, #search, #uid_search, #thread, #uid_thread
2438
2443
  #
@@ -2446,8 +2451,10 @@ module Net
2446
2451
 
2447
2452
  # Sends a {THREAD command [RFC5256 §3]}[https://www.rfc-editor.org/rfc/rfc5256#section-3]
2448
2453
  # to search a mailbox and return message sequence numbers in threaded
2449
- # format, as a ThreadMember tree. +search_keys+ are interpreted the same as
2450
- # for #search.
2454
+ # format, as a ThreadMember tree.
2455
+ #
2456
+ # +search_keys+ are interpreted the same as the +criteria+ argument for
2457
+ # #search. <em>Please note</em> the #search warning for String +criteria+.
2451
2458
  #
2452
2459
  # The supported algorithms are:
2453
2460
  #
@@ -2473,6 +2480,9 @@ module Net
2473
2480
  # Similar to #thread, but returns unique identifiers instead of
2474
2481
  # message sequence numbers.
2475
2482
  #
2483
+ # +search_keys+ are interpreted the same as the +criteria+ argument for
2484
+ # #search. <em>Please note</em> the #search warning for String +criteria+.
2485
+ #
2476
2486
  # Related: #thread, #search, #uid_search, #sort, #uid_sort
2477
2487
  #
2478
2488
  # ===== Capabilities
@@ -2560,10 +2570,11 @@ module Net
2560
2570
  capabilities = capabilities
2561
2571
  .flatten
2562
2572
  .map {|e| ENABLE_ALIASES[e] || e }
2573
+ .flat_map { _1.is_a?(String) && !_1.empty? ? _1.split(/ /, -1) : [_1] }
2563
2574
  .uniq
2564
- .join(' ')
2575
+ .map { Atom[_1] }
2565
2576
  synchronize do
2566
- send_command("ENABLE #{capabilities}")
2577
+ send_command("ENABLE", *capabilities)
2567
2578
  result = clear_responses("ENABLED").last || []
2568
2579
  @utf8_strings ||= result.include? "UTF8=ACCEPT"
2569
2580
  @utf8_strings ||= result.include? "IMAP4REV2"
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.4.24
4
+ version: 0.4.25
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shugo Maeda
@@ -125,7 +125,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
125
125
  - !ruby/object:Gem::Version
126
126
  version: '0'
127
127
  requirements: []
128
- rubygems_version: 4.0.6
128
+ rubygems_version: 4.0.10
129
129
  specification_version: 4
130
130
  summary: Ruby client api for Internet Message Access Protocol
131
131
  test_files: []