net-imap 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ee45560f32705f69d591b21df3d54372e69b444e3cb40f540f44b299c24a1803
4
- data.tar.gz: 2aa318c6367dee1ce530139e06c9b78b0cf7e8eeb9411ef873f290dda78b6273
3
+ metadata.gz: 9c96e58708687b3fb8ded9def89a66e3ca9d1d83717c76c1579928ed60fb2b52
4
+ data.tar.gz: 56c64b20eab875d49b03e7a18f46988850e38865fbd756b0e6db3d7ecaec5975
5
5
  SHA512:
6
- metadata.gz: f98f22799e9e1bff9c8f191d510688f52b7a7737de7fce8b76e42da1cfe8672a94cbb6c3a1eb24d7fce3d1855e6d809dfda52d91f2a67a4d216f66ba015960a9
7
- data.tar.gz: d51b6eb6901db8742ed404714cdd0f54c46cf965cbf2de0130cea863f41a84e0779aac631689c7b0913f77e0fecd2184a66054fc0699bde5d4825db7fb188829
6
+ metadata.gz: 6dc72eab65ac59b09328cad6d1ad9bcc86e4565f05416b6b7de952d2d9d4634f8f272b6e2bf36a0f386ae0acb46de506d28c3856e1a53058fa83f30edfd7275c
7
+ data.tar.gz: 6deb8133dd67519bd082ba287db7eda44500048ef30f6e25f43e6955ed4b82f96c690633375dad56af8590b578ea6df846422a8ae54d40e359bfd2795f2d4125
@@ -7,7 +7,7 @@ jobs:
7
7
  name: build (${{ matrix.ruby }} / ${{ matrix.os }})
8
8
  strategy:
9
9
  matrix:
10
- ruby: [ head, '3.0', '2.7' ]
10
+ ruby: [ head, '3.3', '3.2', '3.1', '3.0', '2.7' ]
11
11
  os: [ ubuntu-latest, macos-latest ]
12
12
  experimental: [false]
13
13
  include:
@@ -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
@@ -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
@@ -106,6 +106,41 @@ module Net
106
106
  #
107
107
  # This script invokes the FETCH command and the SEARCH command concurrently.
108
108
  #
109
+ # When running multiple commands, care must be taken to avoid ambiguity. For
110
+ # example, SEARCH responses are ambiguous about which command they are
111
+ # responding to, so search commands should not run simultaneously, unless the
112
+ # server supports +ESEARCH+ {[RFC4731]}[https://rfc-editor.org/rfc/rfc4731] or
113
+ # IMAP4rev2[https://www.rfc-editor.org/rfc/rfc9051]. See {RFC9051
114
+ # §5.5}[https://www.rfc-editor.org/rfc/rfc9051.html#section-5.5] for
115
+ # other examples of command sequences which should not be pipelined.
116
+ #
117
+ # == Unbounded memory use
118
+ #
119
+ # Net::IMAP reads server responses in a separate receiver thread per client.
120
+ # Unhandled response data is saved to #responses, and response_handlers run
121
+ # inside the receiver thread. See the list of methods for {handling server
122
+ # responses}[rdoc-ref:Net::IMAP@Handling+server+responses], below.
123
+ #
124
+ # Because the receiver thread continuously reads and saves new responses, some
125
+ # scenarios must be careful to avoid unbounded memory use:
126
+ #
127
+ # * Commands such as #list or #fetch can have an enormous number of responses.
128
+ # * Commands such as #fetch can result in an enormous size per response.
129
+ # * Long-lived connections will gradually accumulate unsolicited server
130
+ # responses, especially +EXISTS+, +FETCH+, and +EXPUNGE+ responses.
131
+ # * A buggy or untrusted server could send inappropriate responses, which
132
+ # could be very numerous, very large, and very rapid.
133
+ #
134
+ # Use paginated or limited versions of commands whenever possible.
135
+ #
136
+ # Use #max_response_size to impose a limit on incoming server responses
137
+ # as they are being read. <em>This is especially important for untrusted
138
+ # servers.</em>
139
+ #
140
+ # Use #add_response_handler to handle responses after each one is received.
141
+ # Use the +response_handlers+ argument to ::new to assign response handlers
142
+ # before the receiver thread is started.
143
+ #
109
144
  # == Errors
110
145
  #
111
146
  # An IMAP server can send three different types of responses to indicate
@@ -220,7 +255,9 @@ module Net
220
255
  # Unicode", RFC-2152[https://tools.ietf.org/html/rfc2152], May 1997.
221
256
  #
222
257
  class IMAP < Protocol
223
- VERSION = "0.2.3"
258
+ VERSION = "0.2.5"
259
+
260
+ autoload :ResponseReader, File.expand_path("imap/response_reader", __dir__)
224
261
 
225
262
  include MonitorMixin
226
263
  if defined?(OpenSSL::SSL)
@@ -251,6 +288,40 @@ module Net
251
288
  # Seconds to wait until an IDLE response is received.
252
289
  attr_reader :idle_response_timeout
253
290
 
291
+ # The maximum allowed server response size. When +nil+, there is no limit
292
+ # on response size.
293
+ #
294
+ # The default value is _unlimited_ (after +v0.5.8+, the default is 512 MiB).
295
+ # A _much_ lower value should be used with untrusted servers (for example,
296
+ # when connecting to a user-provided hostname). When using a lower limit,
297
+ # message bodies should be fetched in chunks rather than all at once.
298
+ #
299
+ # <em>Please Note:</em> this only limits the size per response. It does
300
+ # not prevent a flood of individual responses and it does not limit how
301
+ # many unhandled responses may be stored on the responses hash. See
302
+ # Net::IMAP@Unbounded+memory+use.
303
+ #
304
+ # Socket reads are limited to the maximum remaining bytes for the current
305
+ # response: max_response_size minus the bytes that have already been read.
306
+ # When the limit is reached, or reading a +literal+ _would_ go over the
307
+ # limit, ResponseTooLargeError is raised and the connection is closed.
308
+ # See also #socket_read_limit.
309
+ #
310
+ # Note that changes will not take effect immediately, because the receiver
311
+ # thread may already be waiting for the next response using the previous
312
+ # value. Net::IMAP#noop can force a response and enforce the new setting
313
+ # immediately.
314
+ #
315
+ # ==== Versioned Defaults
316
+ #
317
+ # Net::IMAP#max_response_size <em>was added in +v0.2.5+ and +v0.3.9+ as an
318
+ # attr_accessor, and in +v0.4.20+ and +v0.5.7+ as a delegator to a config
319
+ # attribute.</em>
320
+ #
321
+ # * original: +nil+ <em>(no limit)</em>
322
+ # * +0.5+: 512 MiB
323
+ attr_accessor :max_response_size
324
+
254
325
  # The thread to receive exceptions.
255
326
  attr_accessor :client_thread
256
327
 
@@ -955,6 +1026,11 @@ module Net
955
1026
  # end
956
1027
  # }
957
1028
  #
1029
+ # Response handlers can also be added when the client is created before the
1030
+ # receiver thread is started, by the +response_handlers+ argument to ::new.
1031
+ # This ensures every server response is handled, including the #greeting.
1032
+ #
1033
+ # Related: #remove_response_handler, #response_handlers
958
1034
  def add_response_handler(handler = nil, &block)
959
1035
  raise ArgumentError, "two Procs are passed" if handler && block
960
1036
  @response_handlers.push(block || handler)
@@ -1070,6 +1146,13 @@ module Net
1070
1146
  # OpenSSL::SSL::SSLContext#set_params as parameters.
1071
1147
  # open_timeout:: Seconds to wait until a connection is opened
1072
1148
  # idle_response_timeout:: Seconds to wait until an IDLE response is received
1149
+ # response_handlers:: A list of response handlers to be added before the
1150
+ # receiver thread is started. This ensures every server
1151
+ # response is handled, including the #greeting. Note
1152
+ # that the greeting is handled in the current thread,
1153
+ # but all other responses are handled in the receiver
1154
+ # thread.
1155
+ # max_response_size:: See #max_response_size.
1073
1156
  #
1074
1157
  # The most common errors are:
1075
1158
  #
@@ -1100,8 +1183,10 @@ module Net
1100
1183
  @tagno = 0
1101
1184
  @open_timeout = options[:open_timeout] || 30
1102
1185
  @idle_response_timeout = options[:idle_response_timeout] || 5
1186
+ @max_response_size = options[:max_response_size]
1103
1187
  @parser = ResponseParser.new
1104
1188
  @sock = tcp_socket(@host, @port)
1189
+ @reader = ResponseReader.new(self, @sock)
1105
1190
  begin
1106
1191
  if options[:ssl]
1107
1192
  start_tls_session(options[:ssl])
@@ -1112,6 +1197,7 @@ module Net
1112
1197
  @responses = Hash.new([].freeze)
1113
1198
  @tagged_responses = {}
1114
1199
  @response_handlers = []
1200
+ options[:response_handlers]&.each do |h| add_response_handler(h) end
1115
1201
  @tagged_response_arrival = new_cond
1116
1202
  @continued_command_tag = nil
1117
1203
  @continuation_request_arrival = new_cond
@@ -1128,6 +1214,7 @@ module Net
1128
1214
  if @greeting.name == "BYE"
1129
1215
  raise ByeResponseError, @greeting
1130
1216
  end
1217
+ @response_handlers.each do |handler| handler.call(@greeting) end
1131
1218
 
1132
1219
  @client_thread = Thread.current
1133
1220
  @receiver_thread = Thread.start {
@@ -1251,25 +1338,14 @@ module Net
1251
1338
  end
1252
1339
 
1253
1340
  def get_response
1254
- buff = String.new
1255
- while true
1256
- s = @sock.gets(CRLF)
1257
- break unless s
1258
- buff.concat(s)
1259
- if /\{(\d+)\}\r\n/n =~ s
1260
- s = @sock.read($1.to_i)
1261
- buff.concat(s)
1262
- else
1263
- break
1264
- end
1265
- end
1341
+ buff = @reader.read_response_buffer
1266
1342
  return nil if buff.length == 0
1267
- if @@debug
1268
- $stderr.print(buff.gsub(/^/n, "S: "))
1269
- end
1270
- return @parser.parse(buff)
1343
+ $stderr.print(buff.gsub(/^/n, "S: ")) if @@debug
1344
+ @parser.parse(buff)
1271
1345
  end
1272
1346
 
1347
+ #############################
1348
+
1273
1349
  def record_response(name, data)
1274
1350
  unless @responses.has_key?(name)
1275
1351
  @responses[name] = []
@@ -1447,6 +1523,7 @@ module Net
1447
1523
  context.verify_callback = VerifyCallbackProc
1448
1524
  end
1449
1525
  @sock = SSLSocket.new(@sock, context)
1526
+ @reader = ResponseReader.new(self, @sock)
1450
1527
  @sock.sync_close = true
1451
1528
  @sock.hostname = @host if @sock.respond_to? :hostname=
1452
1529
  ssl_socket_connect(@sock, @open_timeout)
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: net-imap
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.2.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Shugo Maeda
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2022-01-06 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: net-protocol
@@ -77,6 +76,7 @@ files:
77
76
  - lib/net/imap/flags.rb
78
77
  - lib/net/imap/response_data.rb
79
78
  - lib/net/imap/response_parser.rb
79
+ - lib/net/imap/response_reader.rb
80
80
  - net-imap.gemspec
81
81
  homepage: https://github.com/ruby/net-imap
82
82
  licenses:
@@ -85,7 +85,6 @@ licenses:
85
85
  metadata:
86
86
  homepage_uri: https://github.com/ruby/net-imap
87
87
  source_code_uri: https://github.com/ruby/net-imap
88
- post_install_message:
89
88
  rdoc_options: []
90
89
  require_paths:
91
90
  - lib
@@ -100,8 +99,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
100
99
  - !ruby/object:Gem::Version
101
100
  version: '0'
102
101
  requirements: []
103
- rubygems_version: 3.4.0.dev
104
- signing_key:
102
+ rubygems_version: 3.6.8
105
103
  specification_version: 4
106
104
  summary: Ruby client api for Internet Message Access Protocol
107
105
  test_files: []