net-imap 0.3.8 → 0.3.9

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: 1c0b89eb763616a2d446e7a145245fcc88906162cdb6e41e1f73b89ac54269fb
4
- data.tar.gz: aa19c272ab1142f1ebec8d40a7f56eeb348286188c154b9a4cfcd0819a2d4b18
3
+ metadata.gz: ac3c48a5e014382d7b05eda0c632b7924829fba35358881aca99cd88e36efe05
4
+ data.tar.gz: 73750ad71a15a08cbab8a92ff3630ada6dbbe804af3a3981f4e65298a64e101b
5
5
  SHA512:
6
- metadata.gz: 93819c8a2aa93f8e5e5737626981fadef9038e31afdec124f70ff6e29b4c233877031d64f3e19486e2976a713da40bed3e8cdf5d5939e953fa3b9cd864915ede
7
- data.tar.gz: f07545087c1d4c1faece211b902096782421fba4f5d81ceab07f8e4c5fed0f2e3918e4c4ba027231ab52038edf4b7dc3a0c4200bf0f1480addfa6042aaf7716e
6
+ metadata.gz: c1ba773bd3cc8dcd540e0c7c5ef7d63135191486660e1b5507921ed6954adb280f871a0d874838b4ef5c26d9f6d2f33d78931be7e7fd8ac8a381408a3666d1fe
7
+ data.tar.gz: 31c372f84bf66cb056a2933f85c2be65695f19b099ac119b5ee943e9d70d24c439d446af5b020cbe8cd4d961efd336d858e190f9b6e1a711571cf101603a0ff8
@@ -11,6 +11,40 @@ module Net
11
11
  class DataFormatError < Error
12
12
  end
13
13
 
14
+ # Error raised when the socket cannot be read, due to a configured limit.
15
+ class ResponseReadError < Error
16
+ end
17
+
18
+ # Error raised when a response is larger than IMAP#max_response_size.
19
+ class ResponseTooLargeError < ResponseReadError
20
+ attr_reader :bytes_read, :literal_size
21
+ attr_reader :max_response_size
22
+
23
+ def initialize(msg = nil, *args,
24
+ bytes_read: nil,
25
+ literal_size: nil,
26
+ max_response_size: nil,
27
+ **kwargs)
28
+ @bytes_read = bytes_read
29
+ @literal_size = literal_size
30
+ @max_response_size = max_response_size
31
+ msg ||= [
32
+ "Response size", response_size_msg, "exceeds max_response_size",
33
+ max_response_size && "(#{max_response_size}B)",
34
+ ].compact.join(" ")
35
+ return super(msg, *args) if kwargs.empty? # ruby 2.6 compatibility
36
+ super(msg, *args, **kwargs)
37
+ end
38
+
39
+ private
40
+
41
+ def response_size_msg
42
+ if bytes_read && literal_size
43
+ "(#{bytes_read}B read + #{literal_size}B literal)"
44
+ end
45
+ end
46
+ end
47
+
14
48
  # Error raised when a response from the server is non-parseable.
15
49
  class ResponseParseError < Error
16
50
  end
@@ -1382,7 +1382,7 @@ module Net
1382
1382
  when T_NUMBER then [Integer(token.value)]
1383
1383
  when T_ATOM
1384
1384
  entries = uid_set__ranges(token.value)
1385
- if (count = entries.sum(&:count)) > MAX_UID_SET_SIZE
1385
+ if (count = entries.sum(&:size)) > MAX_UID_SET_SIZE
1386
1386
  parse_error("uid-set is too large: %d > 10k", count)
1387
1387
  end
1388
1388
  entries.flat_map(&:to_a)
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ class IMAP
5
+ # See https://www.rfc-editor.org/rfc/rfc9051#section-2.2.2
6
+ class ResponseReader # :nodoc:
7
+ attr_reader :client
8
+
9
+ def initialize(client, sock)
10
+ @client, @sock = client, sock
11
+ end
12
+
13
+ def read_response_buffer
14
+ @buff = String.new
15
+ catch :eof do
16
+ while true
17
+ read_line
18
+ break unless (@literal_size = get_literal_size)
19
+ read_literal
20
+ end
21
+ end
22
+ buff
23
+ ensure
24
+ @buff = nil
25
+ end
26
+
27
+ private
28
+
29
+ attr_reader :buff, :literal_size
30
+
31
+ def bytes_read; buff.bytesize end
32
+ def empty?; buff.empty? end
33
+ def done?; line_done? && !get_literal_size end
34
+ def line_done?; buff.end_with?(CRLF) end
35
+ def get_literal_size; /\{(\d+)\}\r\n\z/n =~ buff && $1.to_i end
36
+
37
+ def read_line
38
+ buff << (@sock.gets(CRLF, read_limit) or throw :eof)
39
+ max_response_remaining! unless line_done?
40
+ end
41
+
42
+ def read_literal
43
+ # check before allocating memory for literal
44
+ max_response_remaining!
45
+ literal = String.new(capacity: literal_size)
46
+ buff << (@sock.read(read_limit(literal_size), literal) or throw :eof)
47
+ ensure
48
+ @literal_size = nil
49
+ end
50
+
51
+ def read_limit(limit = nil)
52
+ [limit, max_response_remaining!].compact.min
53
+ end
54
+
55
+ def max_response_size; client.max_response_size end
56
+ def max_response_remaining; max_response_size &.- bytes_read end
57
+ def response_too_large?; max_response_size &.< min_response_size end
58
+ def min_response_size; bytes_read + min_response_remaining end
59
+
60
+ def min_response_remaining
61
+ empty? ? 3 : done? ? 0 : (literal_size || 0) + 2
62
+ end
63
+
64
+ def max_response_remaining!
65
+ return max_response_remaining unless response_too_large?
66
+ raise ResponseTooLargeError.new(
67
+ max_response_size: max_response_size,
68
+ bytes_read: bytes_read,
69
+ literal_size: literal_size,
70
+ )
71
+ end
72
+
73
+ end
74
+ end
75
+ end
data/lib/net/imap.rb CHANGED
@@ -45,10 +45,16 @@ module Net
45
45
  # To work on the messages within a mailbox, the client must
46
46
  # first select that mailbox, using either #select or #examine
47
47
  # (for read-only access). Once the client has successfully
48
- # selected a mailbox, they enter the "_selected_" state, and that
48
+ # selected a mailbox, they enter the +selected+ state, and that
49
49
  # mailbox becomes the _current_ mailbox, on which mail-item
50
50
  # related commands implicitly operate.
51
51
  #
52
+ # === Connection state
53
+ #
54
+ # Once an IMAP connection is established, the connection is in one of four
55
+ # states: <tt>not authenticated</tt>, +authenticated+, +selected+, and
56
+ # +logout+. Most commands are valid only in certain states.
57
+ #
52
58
  # === Sequence numbers and UIDs
53
59
  #
54
60
  # Messages have two sorts of identifiers: message sequence
@@ -126,6 +132,41 @@ module Net
126
132
  #
127
133
  # This script invokes the FETCH command and the SEARCH command concurrently.
128
134
  #
135
+ # When running multiple commands, care must be taken to avoid ambiguity. For
136
+ # example, SEARCH responses are ambiguous about which command they are
137
+ # responding to, so search commands should not run simultaneously, unless the
138
+ # server supports +ESEARCH+ {[RFC4731]}[https://rfc-editor.org/rfc/rfc4731] or
139
+ # IMAP4rev2[https://www.rfc-editor.org/rfc/rfc9051]. See {RFC9051
140
+ # §5.5}[https://www.rfc-editor.org/rfc/rfc9051.html#section-5.5] for
141
+ # other examples of command sequences which should not be pipelined.
142
+ #
143
+ # == Unbounded memory use
144
+ #
145
+ # Net::IMAP reads server responses in a separate receiver thread per client.
146
+ # Unhandled response data is saved to #responses, and response_handlers run
147
+ # inside the receiver thread. See the list of methods for {handling server
148
+ # responses}[rdoc-ref:Net::IMAP@Handling+server+responses], below.
149
+ #
150
+ # Because the receiver thread continuously reads and saves new responses, some
151
+ # scenarios must be careful to avoid unbounded memory use:
152
+ #
153
+ # * Commands such as #list or #fetch can have an enormous number of responses.
154
+ # * Commands such as #fetch can result in an enormous size per response.
155
+ # * Long-lived connections will gradually accumulate unsolicited server
156
+ # responses, especially +EXISTS+, +FETCH+, and +EXPUNGE+ responses.
157
+ # * A buggy or untrusted server could send inappropriate responses, which
158
+ # could be very numerous, very large, and very rapid.
159
+ #
160
+ # Use paginated or limited versions of commands whenever possible.
161
+ #
162
+ # Use #max_response_size to impose a limit on incoming server responses
163
+ # as they are being read. <em>This is especially important for untrusted
164
+ # servers.</em>
165
+ #
166
+ # Use #add_response_handler to handle responses after each one is received.
167
+ # Use the +response_handlers+ argument to ::new to assign response handlers
168
+ # before the receiver thread is started.
169
+ #
129
170
  # == Errors
130
171
  #
131
172
  # An \IMAP server can send three different types of responses to indicate
@@ -187,7 +228,7 @@ module Net
187
228
  # - Net::IMAP.new: A new client connects immediately and waits for a
188
229
  # successful server greeting before returning the new client object.
189
230
  # - #starttls: Asks the server to upgrade a clear-text connection to use TLS.
190
- # - #logout: Tells the server to end the session. Enters the "_logout_" state.
231
+ # - #logout: Tells the server to end the session. Enters the +logout+ state.
191
232
  # - #disconnect: Disconnects the connection (without sending #logout first).
192
233
  # - #disconnected?: True if the connection has been closed.
193
234
  #
@@ -230,40 +271,39 @@ module Net
230
271
  # <em>Capabilities may change after</em> #starttls, #authenticate, or #login
231
272
  # <em>and cached capabilities must be reloaded.</em>
232
273
  # - #noop: Allows the server to send unsolicited untagged #responses.
233
- # - #logout: Tells the server to end the session. Enters the "_logout_" state.
274
+ # - #logout: Tells the server to end the session. Enters the +logout+ state.
234
275
  #
235
276
  # ==== \IMAP commands for the "Not Authenticated" state
236
277
  #
237
- # In addition to the universal commands, the following commands are valid in
238
- # the "<em>not authenticated</em>" state:
278
+ # In addition to the commands for any state, the following commands are valid
279
+ # in the +not_authenticated+ state:
239
280
  #
240
281
  # - #starttls: Upgrades a clear-text connection to use TLS.
241
282
  #
242
283
  # <em>Requires the +STARTTLS+ capability.</em>
243
- # - #authenticate: Identifies the client to the server using a {SASL
244
- # mechanism}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml].
245
- # Enters the "_authenticated_" state.
284
+ # - #authenticate: Identifies the client to the server using the given {SASL
285
+ # mechanism}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml]
286
+ # and credentials. Enters the +authenticated+ state.
246
287
  #
247
288
  # <em>Requires the <tt>AUTH=#{mechanism}</tt> capability for the chosen
248
289
  # mechanism.</em>
249
290
  # - #login: Identifies the client to the server using a plain text password.
250
- # Using #authenticate is generally preferred. Enters the "_authenticated_"
251
- # state.
291
+ # Using #authenticate is preferred. Enters the +authenticated+ state.
252
292
  #
253
293
  # <em>The +LOGINDISABLED+ capability</em> <b>must NOT</b> <em>be listed.</em>
254
294
  #
255
295
  # ==== \IMAP commands for the "Authenticated" state
256
296
  #
257
- # In addition to the universal commands, the following commands are valid in
258
- # the "_authenticated_" state:
297
+ # In addition to the commands for any state, the following commands are valid
298
+ # in the +authenticated+ state:
259
299
  #
260
300
  #--
261
301
  # - #enable: <em>Not implemented by Net::IMAP, yet.</em>
262
302
  #
263
303
  # <em>Requires the +ENABLE+ capability.</em>
264
304
  #++
265
- # - #select: Open a mailbox and enter the "_selected_" state.
266
- # - #examine: Open a mailbox read-only, and enter the "_selected_" state.
305
+ # - #select: Open a mailbox and enter the +selected+ state.
306
+ # - #examine: Open a mailbox read-only, and enter the +selected+ state.
267
307
  # - #create: Creates a new mailbox.
268
308
  # - #delete: Permanently remove a mailbox.
269
309
  # - #rename: Change the name of a mailbox.
@@ -289,12 +329,12 @@ module Net
289
329
  #
290
330
  # ==== \IMAP commands for the "Selected" state
291
331
  #
292
- # In addition to the universal commands and the "authenticated" commands, the
293
- # following commands are valid in the "_selected_" state:
332
+ # In addition to the commands for any state and the +authenticated+
333
+ # commands, the following commands are valid in the +selected+ state:
294
334
  #
295
- # - #close: Closes the mailbox and returns to the "_authenticated_" state,
335
+ # - #close: Closes the mailbox and returns to the +authenticated+ state,
296
336
  # expunging deleted messages, unless the mailbox was opened as read-only.
297
- # - #unselect: Closes the mailbox and returns to the "_authenticated_" state,
337
+ # - #unselect: Closes the mailbox and returns to the +authenticated+ state,
298
338
  # without expunging any messages.
299
339
  #
300
340
  # <em>Requires the +UNSELECT+ capability.</em>
@@ -384,7 +424,7 @@ module Net
384
424
  # ==== RFC3691: +UNSELECT+
385
425
  # Folded into IMAP4rev2[https://tools.ietf.org/html/rfc9051], so it is also
386
426
  # listed with {Core IMAP commands}[rdoc-ref:Net::IMAP@Core+IMAP+commands].
387
- # - #unselect: Closes the mailbox and returns to the "_authenticated_" state,
427
+ # - #unselect: Closes the mailbox and returns to the +authenticated+ state,
388
428
  # without expunging any messages.
389
429
  #
390
430
  # ==== RFC4314: +ACL+
@@ -699,7 +739,9 @@ module Net
699
739
  # * {Character sets}[https://www.iana.org/assignments/character-sets/character-sets.xhtml]
700
740
  #
701
741
  class IMAP < Protocol
702
- VERSION = "0.3.8"
742
+ VERSION = "0.3.9"
743
+
744
+ autoload :ResponseReader, File.expand_path("imap/response_reader", __dir__)
703
745
 
704
746
  include MonitorMixin
705
747
  if defined?(OpenSSL::SSL)
@@ -734,6 +776,40 @@ module Net
734
776
  # Seconds to wait until an IDLE response is received.
735
777
  attr_reader :idle_response_timeout
736
778
 
779
+ # The maximum allowed server response size. When +nil+, there is no limit
780
+ # on response size.
781
+ #
782
+ # The default value is _unlimited_ (after +v0.5.8+, the default is 512 MiB).
783
+ # A _much_ lower value should be used with untrusted servers (for example,
784
+ # when connecting to a user-provided hostname). When using a lower limit,
785
+ # message bodies should be fetched in chunks rather than all at once.
786
+ #
787
+ # <em>Please Note:</em> this only limits the size per response. It does
788
+ # not prevent a flood of individual responses and it does not limit how
789
+ # many unhandled responses may be stored on the responses hash. See
790
+ # Net::IMAP@Unbounded+memory+use.
791
+ #
792
+ # Socket reads are limited to the maximum remaining bytes for the current
793
+ # response: max_response_size minus the bytes that have already been read.
794
+ # When the limit is reached, or reading a +literal+ _would_ go over the
795
+ # limit, ResponseTooLargeError is raised and the connection is closed.
796
+ # See also #socket_read_limit.
797
+ #
798
+ # Note that changes will not take effect immediately, because the receiver
799
+ # thread may already be waiting for the next response using the previous
800
+ # value. Net::IMAP#noop can force a response and enforce the new setting
801
+ # immediately.
802
+ #
803
+ # ==== Versioned Defaults
804
+ #
805
+ # Net::IMAP#max_response_size <em>was added in +v0.2.5+ and +v0.3.9+ as an
806
+ # attr_accessor, and in +v0.4.20+ and +v0.5.7+ as a delegator to a config
807
+ # attribute.</em>
808
+ #
809
+ # * original: +nil+ <em>(no limit)</em>
810
+ # * +0.5+: 512 MiB
811
+ attr_accessor :max_response_size
812
+
737
813
  attr_accessor :client_thread # :nodoc:
738
814
 
739
815
  # Returns the debug mode.
@@ -1960,6 +2036,11 @@ module Net
1960
2036
  # end
1961
2037
  # }
1962
2038
  #
2039
+ # Response handlers can also be added when the client is created before the
2040
+ # receiver thread is started, by the +response_handlers+ argument to ::new.
2041
+ # This ensures every server response is handled, including the #greeting.
2042
+ #
2043
+ # Related: #remove_response_handler, #response_handlers
1963
2044
  def add_response_handler(handler = nil, &block)
1964
2045
  raise ArgumentError, "two Procs are passed" if handler && block
1965
2046
  @response_handlers.push(block || handler)
@@ -1995,6 +2076,13 @@ module Net
1995
2076
  # OpenSSL::SSL::SSLContext#set_params as parameters.
1996
2077
  # open_timeout:: Seconds to wait until a connection is opened
1997
2078
  # idle_response_timeout:: Seconds to wait until an IDLE response is received
2079
+ # response_handlers:: A list of response handlers to be added before the
2080
+ # receiver thread is started. This ensures every server
2081
+ # response is handled, including the #greeting. Note
2082
+ # that the greeting is handled in the current thread,
2083
+ # but all other responses are handled in the receiver
2084
+ # thread.
2085
+ # max_response_size:: See #max_response_size.
1998
2086
  #
1999
2087
  # The most common errors are:
2000
2088
  #
@@ -2025,8 +2113,10 @@ module Net
2025
2113
  @tagno = 0
2026
2114
  @open_timeout = options[:open_timeout] || 30
2027
2115
  @idle_response_timeout = options[:idle_response_timeout] || 5
2116
+ @max_response_size = options[:max_response_size]
2028
2117
  @parser = ResponseParser.new
2029
2118
  @sock = tcp_socket(@host, @port)
2119
+ @reader = ResponseReader.new(self, @sock)
2030
2120
  begin
2031
2121
  if options[:ssl]
2032
2122
  start_tls_session(options[:ssl])
@@ -2037,6 +2127,7 @@ module Net
2037
2127
  @responses = Hash.new([].freeze)
2038
2128
  @tagged_responses = {}
2039
2129
  @response_handlers = []
2130
+ options[:response_handlers]&.each do |h| add_response_handler(h) end
2040
2131
  @tagged_response_arrival = new_cond
2041
2132
  @continued_command_tag = nil
2042
2133
  @continuation_request_arrival = new_cond
@@ -2053,6 +2144,7 @@ module Net
2053
2144
  if @greeting.name == "BYE"
2054
2145
  raise ByeResponseError, @greeting
2055
2146
  end
2147
+ @response_handlers.each do |handler| handler.call(@greeting) end
2056
2148
 
2057
2149
  @client_thread = Thread.current
2058
2150
  @receiver_thread = Thread.start {
@@ -2176,25 +2268,14 @@ module Net
2176
2268
  end
2177
2269
 
2178
2270
  def get_response
2179
- buff = String.new
2180
- while true
2181
- s = @sock.gets(CRLF)
2182
- break unless s
2183
- buff.concat(s)
2184
- if /\{(\d+)\}\r\n/n =~ s
2185
- s = @sock.read($1.to_i)
2186
- buff.concat(s)
2187
- else
2188
- break
2189
- end
2190
- end
2271
+ buff = @reader.read_response_buffer
2191
2272
  return nil if buff.length == 0
2192
- if @@debug
2193
- $stderr.print(buff.gsub(/^/n, "S: "))
2194
- end
2195
- return @parser.parse(buff)
2273
+ $stderr.print(buff.gsub(/^/n, "S: ")) if @@debug
2274
+ @parser.parse(buff)
2196
2275
  end
2197
2276
 
2277
+ #############################
2278
+
2198
2279
  def record_response(name, data)
2199
2280
  unless @responses.has_key?(name)
2200
2281
  @responses[name] = []
@@ -2372,6 +2453,7 @@ module Net
2372
2453
  context.verify_callback = VerifyCallbackProc
2373
2454
  end
2374
2455
  @sock = SSLSocket.new(@sock, context)
2456
+ @reader = ResponseReader.new(self, @sock)
2375
2457
  @sock.sync_close = true
2376
2458
  @sock.hostname = @host if @sock.respond_to? :hostname=
2377
2459
  ssl_socket_connect(@sock, @open_timeout)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: net-imap
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.8
4
+ version: 0.3.9
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shugo Maeda
8
8
  - nicholas a. evans
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-02-07 00:00:00.000000000 Z
11
+ date: 1980-01-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: net-protocol
@@ -97,6 +97,7 @@ files:
97
97
  - lib/net/imap/flags.rb
98
98
  - lib/net/imap/response_data.rb
99
99
  - lib/net/imap/response_parser.rb
100
+ - lib/net/imap/response_reader.rb
100
101
  - lib/net/imap/sasl.rb
101
102
  - lib/net/imap/sasl/saslprep.rb
102
103
  - lib/net/imap/sasl/saslprep_tables.rb
@@ -128,7 +129,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
128
129
  - !ruby/object:Gem::Version
129
130
  version: '0'
130
131
  requirements: []
131
- rubygems_version: 3.6.2
132
+ rubygems_version: 3.6.8
132
133
  specification_version: 4
133
134
  summary: Ruby client api for Internet Message Access Protocol
134
135
  test_files: []