net-imap 0.5.14 → 0.5.15

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: 33bccbb75eba778cb42fc5340afc2f10a899ca671123e8b538a39acbdf16bd1b
4
- data.tar.gz: c4252164f38a0f36b827fb32247500e95293880e2f8026f4b7ed04926614df41
3
+ metadata.gz: '0917528437aff2a931fa3b40d476ffbad713cf46655163dc21a6a7a2333f7a49'
4
+ data.tar.gz: 90b3bf050a4f403408e30eecfaf449069e55bc293297b482e627201821997e0c
5
5
  SHA512:
6
- metadata.gz: e682041f5c1f0e071578c0910f3eace9438064741cce16b148870c73137fd38926154269907f4948dec365e83b9115facff5ec21ac23072270ef39bab64cea10
7
- data.tar.gz: 8a04cac2ad54cd0bc4b2e2f5855bac16f571c3ab6aa0183caa1427ad7c420f8c3920e74b85871c22454dbd827c7772b24cfe96bcaf262222664c05f7483f5dbd
6
+ metadata.gz: c2aaf64941ac0537b7ba6da23ea501dacf438d9eb99581a2ca9906ad150bf5f55a7702141cb573e38306e3eb16000388acb4eb0dbc478727488bc21fa1c61288
7
+ data.tar.gz: 9d6b93b6c963e4e9c226044e1d8f9c4c5e09365ab57beaf1157128f85cc511402927d50033b0ad154b75914cf7191f2ec111dd240c566907203e4263a24f5e49
@@ -17,14 +17,15 @@ module Net
17
17
  when nil
18
18
  when String
19
19
  when Integer
20
- NumValidator.ensure_number(data)
20
+ # Covers modseq-valzer, which is the largest valid IMAP integer
21
+ if data.negative?
22
+ raise DataFormatError, "Integer argument must be unsigned: #{data}"
23
+ elsif 0xffff_ffff_ffff_ffff < data
24
+ raise DataFormatError, "Integer argument must fit in 64 bits: #{data}"
25
+ end
21
26
  when Array
22
- if data[0] == 'CHANGEDSINCE'
23
- NumValidator.ensure_mod_sequence_value(data[1])
24
- else
25
- data.each do |i|
26
- validate_data(i)
27
- end
27
+ data.each do |i|
28
+ validate_data(i)
28
29
  end
29
30
  when Time, Date, DateTime
30
31
  when Symbol
@@ -85,15 +86,23 @@ module Net
85
86
 
86
87
  # `non_sync` is an optional tri-state flag:
87
88
  # * `true` -> Force non-synchronizing `LITERAL+`/`LITERAL-` behavior.
88
- # TODO: raise or warn when capabilities don't allow non_sync.
89
+ # NOTE: raises DataFormatError when server doesn't support
90
+ # non-synchronizing literal, or literal is too large for LITERAL-.
89
91
  # * `false` -> Force normal synchronizing literal behavior.
90
92
  # * `nil` -> (default) Currently behaves like `false` (will be dynamic).
91
93
  # TODO: Dynamic, based on capabilities and bytesize.
92
94
  def send_literal(str, tag = nil, binary: false, non_sync: nil)
95
+ bytesize = str.bytesize
93
96
  synchronize do
97
+ if non_sync && !non_sync_literal_allowed?(bytesize)
98
+ # TODO: check in Printer, so we don't need to close the connection.
99
+ @sock.close
100
+ raise DataFormatError, "Connection closed: " \
101
+ "Cannot send non-synchronizing literal without known server support"
102
+ end
94
103
  prefix = "~" if binary
95
104
  plus = "+" if non_sync
96
- put_string("#{prefix}{#{str.bytesize}#{plus}}\r\n")
105
+ put_string("#{prefix}{#{bytesize}#{plus}}\r\n")
97
106
  if non_sync
98
107
  put_string(str)
99
108
  return
@@ -112,8 +121,18 @@ module Net
112
121
  end
113
122
  end
114
123
 
124
+ def non_sync_literal_allowed?(bytesize)
125
+ return unless capabilities_cached?
126
+ return "+" if capable?("LITERAL+")
127
+ return "-" if capable_literal_minus? && bytesize <= 4096
128
+ false
129
+ end
130
+
131
+ def capable_literal_minus? = capable?("LITERAL-") || capable?("IMAP4rev2")
132
+
133
+ # NOTE: +num+ should already be an Integer
115
134
  def send_number_data(num)
116
- put_string(num.to_s)
135
+ put_string(Integer(num).to_s)
117
136
  end
118
137
 
119
138
  def send_list_data(list, tag = nil)
@@ -148,36 +167,38 @@ module Net
148
167
  end
149
168
  end
150
169
 
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+.
170
+ # Represents IMAP +text+ or +quoted+ data, which share the same
171
+ # validations of decoded #data, and differ only in how they are formatted.
172
+ #
173
+ # +data+ may contain any 7-bit ASCII character except +NULL+, +CR+, or +LF+.
174
+ # Any multibyte +UTF-8+ character is also allowed when the connection
175
+ # supports UTF8: either +UTF8=ACCEPT+ or +IMAP4rev2+ have been enabled, or
176
+ # the server supports only +IMAP4rev2+ and not earlier IMAP revisions, or
177
+ # the server advertises +UTF8=ONLY+.
156
178
  #
157
- # NOTE: The current implementation does not validate whether the connection
158
- # currently supports UTF-8. Future versions may change.
179
+ # NOTE: This does not verify whether the connection supports UTF-8, but that
180
+ # may change in future versions.
159
181
  #
160
182
  # The string's bytes must be valid ASCII or valid UTF-8. The string's
161
183
  # reported encoding is ignored, but the string is _not_ transcoded.
162
- class RawText < CommandData # :nodoc:
184
+ class ValidNonLiteralData < CommandData
163
185
  def initialize(data:)
164
186
  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"))
187
+ unless data.encoding in Encoding::ASCII | Encoding::UTF_8
188
+ data = data.dup.force_encoding(data.ascii_only? ? "ASCII" : "UTF-8")
171
189
  end
190
+ data = -data
172
191
  super
173
192
  validate
174
193
  end
175
194
 
176
195
  def validate
177
- if data.include?("\0")
178
- raise DataFormatError, "NULL byte must be binary literal encoded"
196
+ if !(data.encoding in Encoding::ASCII | Encoding::UTF_8)
197
+ raise DataFormatError, "must use ASCII or UTF-8 encoding"
179
198
  elsif !data.valid_encoding?
180
199
  raise DataFormatError, "invalid UTF-8 must be literal encoded"
200
+ elsif data.include?("\0")
201
+ raise DataFormatError, "NULL byte must be binary literal encoded"
181
202
  elsif /[\r\n]/.match?(data)
182
203
  raise DataFormatError, "CR and LF bytes must be literal encoded"
183
204
  end
@@ -185,12 +206,27 @@ module Net
185
206
 
186
207
  def ascii_only? = data.ascii_only?
187
208
 
188
- def send_data(imap, tag) = imap.__send__(:put_string, data)
209
+ def send_data(imap, tag = nil) = imap.__send__(:put_string, formatted)
210
+ end
211
+
212
+ # Represents IMAP +text+ data, which covers everything in the IMAP grammar,
213
+ # except for +literal+, +literal8+, and the concluding +CRLF+.
214
+ #
215
+ # NOTE: The current implementation does not verify that the connection
216
+ # supports UTF-8. Future versions may validate this.
217
+ class RawText < ValidNonLiteralData # :nodoc:
218
+ # raw: no formatting necessary
219
+ alias formatted data
189
220
  end
190
221
 
191
222
  class RawData < CommandData # :nodoc:
192
223
  def initialize(data:)
193
- data = split_parts(data)
224
+ case data
225
+ in String then data = self.class.split(data)
226
+ in Array if data.all? { _1 in RawText | Literal }
227
+ else
228
+ raise TypeError, "expected String or Array[#{RawText} | #{Literal}]"
229
+ end
194
230
  super
195
231
  validate
196
232
  end
@@ -199,14 +235,16 @@ module Net
199
235
 
200
236
  def validate
201
237
  return unless data.last in RawText(data: text)
202
- if text.rindex(/~?\{[1-9]\d*\+?\}\z/n)
238
+ if text.rindex(/\{\d+\+?\}\z/n)
203
239
  raise DataFormatError, "RawData cannot end with literal continuation"
204
240
  end
205
241
  end
206
242
 
207
- private
208
-
209
- def split_parts(data)
243
+ # Splits an input +string+ into an array of RawText and Literal/Literal8.
244
+ #
245
+ # NOTE: unlike RawData#validate, this does not prevent the final RawText
246
+ # from ending with a literal prefix.
247
+ def self.split(data)
210
248
  data = data.b # dups and ensures BINARY encoding
211
249
  parts = []
212
250
  while data.match(/(~)?\{(0|[1-9]\d*)(\+)?\}\r\n/n)
@@ -220,7 +258,7 @@ module Net
220
258
  parts
221
259
  end
222
260
 
223
- def extract_literal(data, binary:, bytesize:, non_sync:)
261
+ def self.extract_literal(data, binary:, bytesize:, non_sync:)
224
262
  if data.bytesize < bytesize
225
263
  raise DataFormatError, "Too few bytes in string for literal, " \
226
264
  "expected: %s, remaining: %s" % [bytesize, data.bytesize]
@@ -228,6 +266,7 @@ module Net
228
266
  literal = data.byteslice(0, bytesize)
229
267
  (binary ? Literal8 : Literal).new(data: literal, non_sync:)
230
268
  end
269
+ private_class_method :extract_literal
231
270
  end
232
271
 
233
272
  class Atom < CommandData # :nodoc:
@@ -241,6 +280,8 @@ module Net
241
280
  or raise DataFormatError, "#{self.class} must be ASCII only"
242
281
  data.match?(ResponseParser::Patterns::ATOM_SPECIALS) \
243
282
  and raise DataFormatError, "#{self.class} must not contain atom-specials"
283
+ data.empty? \
284
+ and raise DataFormatError, "#{self.class} must not be empty"
244
285
  end
245
286
 
246
287
  def send_data(imap, tag)
@@ -254,10 +295,13 @@ module Net
254
295
  end
255
296
  end
256
297
 
257
- class QuotedString < CommandData # :nodoc:
258
- def send_data(imap, tag)
259
- imap.__send__(:send_quoted_string, data)
260
- end
298
+ # Represents a IMAP +quoted+ string, which can encode any valid ASCII or
299
+ # UTF-8 string, unless it contains any +CR+, +LF+, or +NULL+ bytes.
300
+ #
301
+ # NOTE: The current implementation does not verify that the connection
302
+ # supports UTF-8. Future versions may validate this.
303
+ class QuotedString < ValidNonLiteralData # :nodoc:
304
+ def formatted = %("#{data.gsub(/["\\]/, "\\\\\\&")}")
261
305
  end
262
306
 
263
307
  class Literal < Data.define(:data, :non_sync) # :nodoc:
@@ -2189,10 +2189,7 @@ module Net
2189
2189
  if $1
2190
2190
  return Token.new(T_SPACE, $+)
2191
2191
  elsif $2
2192
- len = $+.to_i
2193
- val = @str[@pos, len]
2194
- @pos += len
2195
- return Token.new(T_LITERAL8, val)
2192
+ literal_token($+, T_LITERAL8)
2196
2193
  elsif $3 && $7
2197
2194
  # greedily match ATOM, prefixed with NUMBER, NIL, or PLUS.
2198
2195
  return Token.new(T_ATOM, $3)
@@ -2220,10 +2217,7 @@ module Net
2220
2217
  elsif $15
2221
2218
  return Token.new(T_RBRA, $+)
2222
2219
  elsif $16
2223
- len = $+.to_i
2224
- val = @str[@pos, len]
2225
- @pos += len
2226
- return Token.new(T_LITERAL, val)
2220
+ literal_token($+)
2227
2221
  elsif $17
2228
2222
  return Token.new(T_PERCENT, $+)
2229
2223
  elsif $18
@@ -2249,10 +2243,7 @@ module Net
2249
2243
  elsif $4
2250
2244
  return Token.new(T_QUOTED, Patterns.unescape_quoted($+))
2251
2245
  elsif $5
2252
- len = $+.to_i
2253
- val = @str[@pos, len]
2254
- @pos += len
2255
- return Token.new(T_LITERAL, val)
2246
+ literal_token($+)
2256
2247
  elsif $6
2257
2248
  return Token.new(T_LPAR, $+)
2258
2249
  elsif $7
@@ -2267,6 +2258,23 @@ module Net
2267
2258
  else
2268
2259
  parse_error("invalid @lex_state - %s", @lex_state.inspect)
2269
2260
  end
2261
+ rescue DataFormatError => error
2262
+ parse_error error.message
2263
+ end
2264
+
2265
+ def literal_token(len, type = T_LITERAL)
2266
+ len = coerce_number64 len.to_i
2267
+ val = @str[@pos, len]
2268
+ @pos += len
2269
+ Token.new(type, val)
2270
+ end
2271
+
2272
+ # copied/adapted from NumValidator in v0.6
2273
+ def coerce_number64(num)
2274
+ int = num.to_i
2275
+ return int if 0 <= int && int <= 0x7fff_ffff_ffff_ffff
2276
+ raise DataFormatError,
2277
+ "number64 must be unsigned 63-bit integer: #{num}"
2270
2278
  end
2271
2279
 
2272
2280
  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)
@@ -35,7 +37,10 @@ module Net
35
37
  def line_done? = buff.end_with?(CRLF)
36
38
 
37
39
  def get_literal_size(buff)
38
- 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)
39
44
  end
40
45
 
41
46
  def read_line
@@ -74,6 +79,14 @@ module Net
74
79
  )
75
80
  end
76
81
 
82
+ # copied/adapted from NumValidator in v0.6
83
+ def coerce_number64(num)
84
+ int = num.to_i
85
+ return int if 0 <= int && int <= 0x7fff_ffff_ffff_ffff
86
+ raise DataFormatError,
87
+ "number64 must be unsigned 63-bit integer: #{num}"
88
+ end
89
+
77
90
  end
78
91
  end
79
92
  end
data/lib/net/imap.rb CHANGED
@@ -806,7 +806,7 @@ module Net
806
806
  # * {IMAP URLAUTH Authorization Mechanism Registry}[https://www.iana.org/assignments/urlauth-authorization-mechanism-registry/urlauth-authorization-mechanism-registry.xhtml]
807
807
  #
808
808
  class IMAP < Protocol
809
- VERSION = "0.5.14"
809
+ VERSION = "0.5.15"
810
810
 
811
811
  # Aliases for supported capabilities, to be used with the #enable command.
812
812
  ENABLE_ALIASES = {
@@ -1149,22 +1149,24 @@ module Net
1149
1149
 
1150
1150
  # Disconnects from the server.
1151
1151
  #
1152
- # Waits for receiver thread to close before returning. Slow or stuck
1153
- # response handlers can cause #disconnect to hang until they complete.
1152
+ # Waits for receiver thread to close before returning, except when called
1153
+ # from inside the connection mutex such as from a response handler. Slow or
1154
+ # stuck response handlers can cause #disconnect to hang until they complete.
1154
1155
  #
1155
1156
  # Related: #logout, #logout!
1156
1157
  def disconnect
1157
1158
  in_logout_state = try_state_logout?
1158
1159
  return if disconnected?
1160
+ in_receiver_thread = Thread.current == @receiver_thread
1159
1161
  begin
1160
1162
  @sock.to_io.shutdown
1161
1163
  rescue Errno::ENOTCONN
1162
1164
  # ignore `Errno::ENOTCONN: Socket is not connected' on some platforms.
1163
1165
  rescue Exception => e
1164
- @receiver_thread.raise(e)
1166
+ @receiver_thread.raise(e) unless in_receiver_thread
1165
1167
  end
1166
1168
  @sock.close
1167
- @receiver_thread.join
1169
+ @receiver_thread.join unless mon_owned? || in_receiver_thread
1168
1170
  raise e if e
1169
1171
  ensure
1170
1172
  # Try again after shutting down the receiver thread. With no reciever
@@ -2209,6 +2211,7 @@ module Net
2209
2211
  # provided as an array or a string.
2210
2212
  # See {"Argument translation"}[rdoc-ref:#search@Argument+translation]
2211
2213
  # and {"Search criteria"}[rdoc-ref:#search@Search+criteria], below.
2214
+ # <em>Please note</em> the warning for when +criteria+ is a String.
2212
2215
  #
2213
2216
  # +return+ options control what kind of information is returned about
2214
2217
  # messages matching the search +criteria+. Specifying +return+ should force
@@ -2619,7 +2622,8 @@ module Net
2619
2622
  # backward compatibility) but adds SearchResult#modseq when the +CONDSTORE+
2620
2623
  # capability has been enabled.
2621
2624
  #
2622
- # See #search for documentation of parameters.
2625
+ # See #search for documentation of parameters. <em>Please note</em> the
2626
+ # warning for when +criteria+ is a String.
2623
2627
  #
2624
2628
  # ==== Capabilities
2625
2629
  #
@@ -2705,7 +2709,8 @@ module Net
2705
2709
  # {SequenceSet[...]}[rdoc-ref:SequenceSet@Creating+sequence+sets].
2706
2710
  # (For message sequence numbers, use #fetch instead.)
2707
2711
  #
2708
- # +attr+ behaves the same as with #fetch.
2712
+ # +attr+ behaves the same as with #fetch. <em>Please note</em> the #fetch
2713
+ # warning on the +attr+ argument.
2709
2714
  # >>>
2710
2715
  # *Note:* Servers _MUST_ implicitly include the +UID+ message data item as
2711
2716
  # part of any +FETCH+ response caused by a +UID+ command, regardless of
@@ -2917,8 +2922,10 @@ module Net
2917
2922
 
2918
2923
  # Sends a {SORT command [RFC5256 §3]}[https://www.rfc-editor.org/rfc/rfc5256#section-3]
2919
2924
  # to search a mailbox for messages that match +search_keys+ and return an
2920
- # array of message sequence numbers, sorted by +sort_keys+. +search_keys+
2921
- # are interpreted the same as for #search.
2925
+ # array of message sequence numbers, sorted by +sort_keys+.
2926
+ #
2927
+ # +search_keys+ are interpreted the same as the +criteria+ argument for
2928
+ # #search. <em>Please note</em> the #search warning for String +criteria+.
2922
2929
  #
2923
2930
  #--
2924
2931
  # TODO: describe +sort_keys+
@@ -2943,8 +2950,10 @@ module Net
2943
2950
 
2944
2951
  # Sends a {UID SORT command [RFC5256 §3]}[https://www.rfc-editor.org/rfc/rfc5256#section-3]
2945
2952
  # to search a mailbox for messages that match +search_keys+ and return an
2946
- # array of unique identifiers, sorted by +sort_keys+. +search_keys+ are
2947
- # interpreted the same as for #search.
2953
+ # array of unique identifiers, sorted by +sort_keys+.
2954
+ #
2955
+ # +search_keys+ are interpreted the same as the +criteria+ argument for
2956
+ # #search. <em>Please note</em> the #search warning for String +criteria+.
2948
2957
  #
2949
2958
  # Related: #sort, #search, #uid_search, #thread, #uid_thread
2950
2959
  #
@@ -2958,8 +2967,10 @@ module Net
2958
2967
 
2959
2968
  # Sends a {THREAD command [RFC5256 §3]}[https://www.rfc-editor.org/rfc/rfc5256#section-3]
2960
2969
  # to search a mailbox and return message sequence numbers in threaded
2961
- # format, as a ThreadMember tree. +search_keys+ are interpreted the same as
2962
- # for #search.
2970
+ # format, as a ThreadMember tree.
2971
+ #
2972
+ # +search_keys+ are interpreted the same as the +criteria+ argument for
2973
+ # #search. <em>Please note</em> the #search warning for String +criteria+.
2963
2974
  #
2964
2975
  # The supported algorithms are:
2965
2976
  #
@@ -2985,6 +2996,9 @@ module Net
2985
2996
  # Similar to #thread, but returns unique identifiers instead of
2986
2997
  # message sequence numbers.
2987
2998
  #
2999
+ # +search_keys+ are interpreted the same as the +criteria+ argument for
3000
+ # #search. <em>Please note</em> the #search warning for String +criteria+.
3001
+ #
2988
3002
  # Related: #thread, #search, #uid_search, #sort, #uid_sort
2989
3003
  #
2990
3004
  # ==== Capabilities
@@ -3094,10 +3108,11 @@ module Net
3094
3108
  capabilities = capabilities
3095
3109
  .flatten
3096
3110
  .map {|e| ENABLE_ALIASES[e] || e }
3111
+ .flat_map { _1.is_a?(String) && !_1.empty? ? _1.split(/ /, -1) : [_1] }
3097
3112
  .uniq
3098
- .join(' ')
3113
+ .map { Atom[_1] }
3099
3114
  synchronize do
3100
- send_command("ENABLE #{capabilities}")
3115
+ send_command("ENABLE", *capabilities)
3101
3116
  result = clear_responses("ENABLED").last || []
3102
3117
  @utf8_strings ||= result.include? "UTF8=ACCEPT"
3103
3118
  @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.5.14
4
+ version: 0.5.15
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shugo Maeda
@@ -130,7 +130,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
130
130
  - !ruby/object:Gem::Version
131
131
  version: '0'
132
132
  requirements: []
133
- rubygems_version: 4.0.6
133
+ rubygems_version: 4.0.10
134
134
  specification_version: 4
135
135
  summary: Ruby client api for Internet Message Access Protocol
136
136
  test_files: []