net-imap 0.4.18 → 0.4.20

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.
@@ -0,0 +1,326 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Net
4
+ class IMAP < Protocol
5
+
6
+ # *NOTE:* <em>UIDPlusData is deprecated and will be removed in the +0.6.0+
7
+ # release.</em> To use AppendUIDData and CopyUIDData before +0.6.0+, set
8
+ # Config#parser_use_deprecated_uidplus_data to +false+.
9
+ #
10
+ # UIDPlusData represents the ResponseCode#data that accompanies the
11
+ # +APPENDUID+ and +COPYUID+ {response codes}[rdoc-ref:ResponseCode].
12
+ #
13
+ # A server that supports +UIDPLUS+ should send UIDPlusData in response to
14
+ # the append[rdoc-ref:Net::IMAP#append], copy[rdoc-ref:Net::IMAP#copy],
15
+ # move[rdoc-ref:Net::IMAP#move], {uid copy}[rdoc-ref:Net::IMAP#uid_copy],
16
+ # and {uid move}[rdoc-ref:Net::IMAP#uid_move] commands---unless the
17
+ # destination mailbox reports +UIDNOTSTICKY+.
18
+ #
19
+ # Note that append[rdoc-ref:Net::IMAP#append], copy[rdoc-ref:Net::IMAP#copy]
20
+ # and {uid_copy}[rdoc-ref:Net::IMAP#uid_copy] return UIDPlusData in their
21
+ # TaggedResponse. But move[rdoc-ref:Net::IMAP#copy] and
22
+ # {uid_move}[rdoc-ref:Net::IMAP#uid_move] _should_ send UIDPlusData in an
23
+ # UntaggedResponse response before sending their TaggedResponse. However
24
+ # some servers do send UIDPlusData in the TaggedResponse for +MOVE+
25
+ # commands---this complies with the older +UIDPLUS+ specification but is
26
+ # discouraged by the +MOVE+ extension and disallowed by +IMAP4rev2+.
27
+ #
28
+ # == Required capability
29
+ # Requires either +UIDPLUS+ [RFC4315[https://www.rfc-editor.org/rfc/rfc4315]]
30
+ # or +IMAP4rev2+ capability.
31
+ #
32
+ class UIDPlusData < Struct.new(:uidvalidity, :source_uids, :assigned_uids)
33
+ ##
34
+ # method: uidvalidity
35
+ # :call-seq: uidvalidity -> nonzero uint32
36
+ #
37
+ # The UIDVALIDITY of the destination mailbox.
38
+
39
+ ##
40
+ # method: source_uids
41
+ # :call-seq: source_uids -> nil or an array of nonzero uint32
42
+ #
43
+ # The UIDs of the copied or moved messages.
44
+ #
45
+ # Note:: Returns +nil+ for Net::IMAP#append.
46
+
47
+ ##
48
+ # method: assigned_uids
49
+ # :call-seq: assigned_uids -> an array of nonzero uint32
50
+ #
51
+ # The newly assigned UIDs of the copied, moved, or appended messages.
52
+ #
53
+ # Note:: This always returns an array, even when it contains only one UID.
54
+
55
+ ##
56
+ # :call-seq: uid_mapping -> nil or a hash
57
+ #
58
+ # Returns a hash mapping each source UID to the newly assigned destination
59
+ # UID.
60
+ #
61
+ # Note:: Returns +nil+ for Net::IMAP#append.
62
+ def uid_mapping
63
+ source_uids&.zip(assigned_uids)&.to_h
64
+ end
65
+ end
66
+
67
+ # This replaces the `Data.define` polyfill that's used by net-imap 0.5.
68
+ class Data_define__uidvalidity___assigned_uids_ # :no-doc:
69
+ attr_reader :uidvalidity, :assigned_uids
70
+
71
+ def self.[](...) new(...) end
72
+ def self.new(uidvalidity = (args = false; nil),
73
+ assigned_uids = nil,
74
+ **kwargs)
75
+ if kwargs.empty?
76
+ super(uidvalidity: uidvalidity, assigned_uids: assigned_uids)
77
+ elsif !args
78
+ super
79
+ else
80
+ raise ArgumentError, "sent both positional and keyword args"
81
+ end
82
+ end
83
+
84
+ def ==(other)
85
+ self.class == other.class &&
86
+ self.uidvalidity == other.uidvalidity &&
87
+ self.assigned_uids == other.assigned_uids
88
+ end
89
+
90
+ def eql?(other)
91
+ self.class.eql?(other.class) &&
92
+ self.uidvalidity.eql?(other.uidvalidity) &&
93
+ self.assigned_uids.eql?(other.assigned_uids)
94
+ end
95
+
96
+ def hash; [self.class, uidvalidity, assigned_uids].hash end
97
+
98
+ def initialize(uidvalidity:, assigned_uids:)
99
+ @uidvalidity = uidvalidity
100
+ @assigned_uids = assigned_uids
101
+ freeze
102
+ end
103
+ end
104
+
105
+ # >>>
106
+ # *NOTE:* <em>AppendUIDData will replace UIDPlusData for +APPENDUID+ in the
107
+ # +0.6.0+ release.</em> To use AppendUIDData before +0.6.0+, set
108
+ # Config#parser_use_deprecated_uidplus_data to +false+.
109
+ #
110
+ # AppendUIDData represents the ResponseCode#data that accompanies the
111
+ # +APPENDUID+ {response code}[rdoc-ref:ResponseCode].
112
+ #
113
+ # A server that supports +UIDPLUS+ (or +IMAP4rev2+) should send
114
+ # AppendUIDData inside every TaggedResponse returned by the
115
+ # append[rdoc-ref:Net::IMAP#append] command---unless the target mailbox
116
+ # reports +UIDNOTSTICKY+.
117
+ #
118
+ # == Required capability
119
+ # Requires either +UIDPLUS+ [RFC4315[https://www.rfc-editor.org/rfc/rfc4315]]
120
+ # or +IMAP4rev2+ capability.
121
+ class AppendUIDData < Data_define__uidvalidity___assigned_uids_
122
+ def initialize(uidvalidity:, assigned_uids:)
123
+ uidvalidity = Integer(uidvalidity)
124
+ assigned_uids = SequenceSet[assigned_uids]
125
+ NumValidator.ensure_nz_number(uidvalidity)
126
+ if assigned_uids.include_star?
127
+ raise DataFormatError, "uid-set cannot contain '*'"
128
+ end
129
+ super
130
+ end
131
+
132
+ ##
133
+ # attr_reader: uidvalidity
134
+ # :call-seq: uidvalidity -> nonzero uint32
135
+ #
136
+ # The UIDVALIDITY of the destination mailbox.
137
+
138
+ ##
139
+ # attr_reader: assigned_uids
140
+ #
141
+ # A SequenceSet with the newly assigned UIDs of the appended messages.
142
+
143
+ # Returns the number of messages that have been appended.
144
+ def size
145
+ assigned_uids.count_with_duplicates
146
+ end
147
+ end
148
+
149
+ # This replaces the `Data.define` polyfill that's used by net-imap 0.5.
150
+ class Data_define__uidvalidity___source_uids___assigned_uids_ # :no-doc:
151
+ attr_reader :uidvalidity, :source_uids, :assigned_uids
152
+
153
+ def self.[](...) new(...) end
154
+ def self.new(uidvalidity = (args = false; nil),
155
+ source_uids = nil,
156
+ assigned_uids = nil,
157
+ **kwargs)
158
+ if kwargs.empty?
159
+ super(uidvalidity: uidvalidity,
160
+ source_uids: source_uids,
161
+ assigned_uids: assigned_uids)
162
+ elsif !args
163
+ super(**kwargs)
164
+ else
165
+ raise ArgumentError, "sent both positional and keyword args"
166
+ end
167
+ end
168
+
169
+ def initialize(uidvalidity:, source_uids:, assigned_uids:)
170
+ @uidvalidity = uidvalidity
171
+ @source_uids = source_uids
172
+ @assigned_uids = assigned_uids
173
+ freeze
174
+ end
175
+
176
+ def ==(other)
177
+ self.class == other.class &&
178
+ self.uidvalidity == other.uidvalidity &&
179
+ self.source_uids == other.source_uids
180
+ self.assigned_uids == other.assigned_uids
181
+ end
182
+
183
+ def eql?(other)
184
+ self.class.eql?(other.class) &&
185
+ self.uidvalidity.eql?(other.uidvalidity) &&
186
+ self.source_uids.eql?(other.source_uids)
187
+ self.assigned_uids.eql?(other.assigned_uids)
188
+ end
189
+
190
+ def hash; [self.class, uidvalidity, source_uids, assigned_uids].hash end
191
+ end
192
+
193
+ # >>>
194
+ # *NOTE:* <em>CopyUIDData will replace UIDPlusData for +COPYUID+ in the
195
+ # +0.6.0+ release.</em> To use CopyUIDData before +0.6.0+, set
196
+ # Config#parser_use_deprecated_uidplus_data to +false+.
197
+ #
198
+ # CopyUIDData represents the ResponseCode#data that accompanies the
199
+ # +COPYUID+ {response code}[rdoc-ref:ResponseCode].
200
+ #
201
+ # A server that supports +UIDPLUS+ (or +IMAP4rev2+) should send CopyUIDData
202
+ # in response to
203
+ # copy[rdoc-ref:Net::IMAP#copy], {uid_copy}[rdoc-ref:Net::IMAP#uid_copy],
204
+ # move[rdoc-ref:Net::IMAP#copy], and {uid_move}[rdoc-ref:Net::IMAP#uid_move]
205
+ # commands---unless the destination mailbox reports +UIDNOTSTICKY+.
206
+ #
207
+ # Note that copy[rdoc-ref:Net::IMAP#copy] and
208
+ # {uid_copy}[rdoc-ref:Net::IMAP#uid_copy] return CopyUIDData in their
209
+ # TaggedResponse. But move[rdoc-ref:Net::IMAP#copy] and
210
+ # {uid_move}[rdoc-ref:Net::IMAP#uid_move] _should_ send CopyUIDData in an
211
+ # UntaggedResponse response before sending their TaggedResponse. However
212
+ # some servers do send CopyUIDData in the TaggedResponse for +MOVE+
213
+ # commands---this complies with the older +UIDPLUS+ specification but is
214
+ # discouraged by the +MOVE+ extension and disallowed by +IMAP4rev2+.
215
+ #
216
+ # == Required capability
217
+ # Requires either +UIDPLUS+ [RFC4315[https://www.rfc-editor.org/rfc/rfc4315]]
218
+ # or +IMAP4rev2+ capability.
219
+ class CopyUIDData < Data_define__uidvalidity___source_uids___assigned_uids_
220
+ def initialize(uidvalidity:, source_uids:, assigned_uids:)
221
+ uidvalidity = Integer(uidvalidity)
222
+ source_uids = SequenceSet[source_uids]
223
+ assigned_uids = SequenceSet[assigned_uids]
224
+ NumValidator.ensure_nz_number(uidvalidity)
225
+ if source_uids.include_star? || assigned_uids.include_star?
226
+ raise DataFormatError, "uid-set cannot contain '*'"
227
+ elsif source_uids.count_with_duplicates != assigned_uids.count_with_duplicates
228
+ raise DataFormatError, "mismatched uid-set sizes for %s and %s" % [
229
+ source_uids, assigned_uids
230
+ ]
231
+ end
232
+ super
233
+ end
234
+
235
+ ##
236
+ # attr_reader: uidvalidity
237
+ #
238
+ # The +UIDVALIDITY+ of the destination mailbox (a nonzero unsigned 32 bit
239
+ # integer).
240
+
241
+ ##
242
+ # attr_reader: source_uids
243
+ #
244
+ # A SequenceSet with the original UIDs of the copied or moved messages.
245
+
246
+ ##
247
+ # attr_reader: assigned_uids
248
+ #
249
+ # A SequenceSet with the newly assigned UIDs of the copied or moved
250
+ # messages.
251
+
252
+ # Returns the number of messages that have been copied or moved.
253
+ # source_uids and the assigned_uids will both the same number of UIDs.
254
+ def size
255
+ assigned_uids.count_with_duplicates
256
+ end
257
+
258
+ # :call-seq:
259
+ # assigned_uid_for(source_uid) -> uid
260
+ # self[source_uid] -> uid
261
+ #
262
+ # Returns the UID in the destination mailbox for the message that was
263
+ # copied from +source_uid+ in the source mailbox.
264
+ #
265
+ # This is the reverse of #source_uid_for.
266
+ #
267
+ # Related: source_uid_for, each_uid_pair, uid_mapping
268
+ def assigned_uid_for(source_uid)
269
+ idx = source_uids.find_ordered_index(source_uid) and
270
+ assigned_uids.ordered_at(idx)
271
+ end
272
+ alias :[] :assigned_uid_for
273
+
274
+ # :call-seq:
275
+ # source_uid_for(assigned_uid) -> uid
276
+ #
277
+ # Returns the UID in the source mailbox for the message that was copied to
278
+ # +assigned_uid+ in the source mailbox.
279
+ #
280
+ # This is the reverse of #assigned_uid_for.
281
+ #
282
+ # Related: assigned_uid_for, each_uid_pair, uid_mapping
283
+ def source_uid_for(assigned_uid)
284
+ idx = assigned_uids.find_ordered_index(assigned_uid) and
285
+ source_uids.ordered_at(idx)
286
+ end
287
+
288
+ # Yields a pair of UIDs for each copied message. The first is the
289
+ # message's UID in the source mailbox and the second is the UID in the
290
+ # destination mailbox.
291
+ #
292
+ # Returns an enumerator when no block is given.
293
+ #
294
+ # Please note the warning on uid_mapping before calling methods like
295
+ # +to_h+ or +to_a+ on the returned enumerator.
296
+ #
297
+ # Related: uid_mapping, assigned_uid_for, source_uid_for
298
+ def each_uid_pair
299
+ return enum_for(__method__) unless block_given?
300
+ source_uids.each_ordered_number.lazy
301
+ .zip(assigned_uids.each_ordered_number.lazy) do
302
+ |source_uid, assigned_uid|
303
+ yield source_uid, assigned_uid
304
+ end
305
+ end
306
+ alias each_pair each_uid_pair
307
+ alias each each_uid_pair
308
+
309
+ # :call-seq: uid_mapping -> hash
310
+ #
311
+ # Returns a hash mapping each source UID to the newly assigned destination
312
+ # UID.
313
+ #
314
+ # <em>*Warning:*</em> The hash that is created may consume _much_ more
315
+ # memory than the data used to create it. When handling responses from an
316
+ # untrusted server, check #size before calling this method.
317
+ #
318
+ # Related: each_uid_pair, assigned_uid_for, source_uid_for
319
+ def uid_mapping
320
+ each_uid_pair.to_h
321
+ end
322
+
323
+ end
324
+
325
+ end
326
+ end
data/lib/net/imap.rb CHANGED
@@ -43,10 +43,16 @@ module Net
43
43
  # To work on the messages within a mailbox, the client must
44
44
  # first select that mailbox, using either #select or #examine
45
45
  # (for read-only access). Once the client has successfully
46
- # selected a mailbox, they enter the "_selected_" state, and that
46
+ # selected a mailbox, they enter the +selected+ state, and that
47
47
  # mailbox becomes the _current_ mailbox, on which mail-item
48
48
  # related commands implicitly operate.
49
49
  #
50
+ # === Connection state
51
+ #
52
+ # Once an IMAP connection is established, the connection is in one of four
53
+ # states: <tt>not authenticated</tt>, +authenticated+, +selected+, and
54
+ # +logout+. Most commands are valid only in certain states.
55
+ #
50
56
  # === Sequence numbers and UIDs
51
57
  #
52
58
  # Messages have two sorts of identifiers: message sequence
@@ -199,6 +205,42 @@ module Net
199
205
  #
200
206
  # This script invokes the FETCH command and the SEARCH command concurrently.
201
207
  #
208
+ # When running multiple commands, care must be taken to avoid ambiguity. For
209
+ # example, SEARCH responses are ambiguous about which command they are
210
+ # responding to, so search commands should not run simultaneously, unless the
211
+ # server supports +ESEARCH+ {[RFC4731]}[https://rfc-editor.org/rfc/rfc4731] or
212
+ # IMAP4rev2[https://www.rfc-editor.org/rfc/rfc9051]. See {RFC9051
213
+ # §5.5}[https://www.rfc-editor.org/rfc/rfc9051.html#section-5.5] for
214
+ # other examples of command sequences which should not be pipelined.
215
+ #
216
+ # == Unbounded memory use
217
+ #
218
+ # Net::IMAP reads server responses in a separate receiver thread per client.
219
+ # Unhandled response data is saved to #responses, and response_handlers run
220
+ # inside the receiver thread. See the list of methods for {handling server
221
+ # responses}[rdoc-ref:Net::IMAP@Handling+server+responses], below.
222
+ #
223
+ # Because the receiver thread continuously reads and saves new responses, some
224
+ # scenarios must be careful to avoid unbounded memory use:
225
+ #
226
+ # * Commands such as #list or #fetch can have an enormous number of responses.
227
+ # * Commands such as #fetch can result in an enormous size per response.
228
+ # * Long-lived connections will gradually accumulate unsolicited server
229
+ # responses, especially +EXISTS+, +FETCH+, and +EXPUNGE+ responses.
230
+ # * A buggy or untrusted server could send inappropriate responses, which
231
+ # could be very numerous, very large, and very rapid.
232
+ #
233
+ # Use paginated or limited versions of commands whenever possible.
234
+ #
235
+ # Use Config#max_response_size to impose a limit on incoming server responses
236
+ # as they are being read. <em>This is especially important for untrusted
237
+ # servers.</em>
238
+ #
239
+ # Use #add_response_handler to handle responses after each one is received.
240
+ # Use the +response_handlers+ argument to ::new to assign response handlers
241
+ # before the receiver thread is started. Use #extract_responses,
242
+ # #clear_responses, or #responses (with a block) to prune responses.
243
+ #
202
244
  # == Errors
203
245
  #
204
246
  # An \IMAP server can send three different types of responses to indicate
@@ -260,8 +302,9 @@ module Net
260
302
  #
261
303
  # - Net::IMAP.new: Creates a new \IMAP client which connects immediately and
262
304
  # waits for a successful server greeting before the method returns.
305
+ # - #connection_state: Returns the connection state.
263
306
  # - #starttls: Asks the server to upgrade a clear-text connection to use TLS.
264
- # - #logout: Tells the server to end the session. Enters the "_logout_" state.
307
+ # - #logout: Tells the server to end the session. Enters the +logout+ state.
265
308
  # - #disconnect: Disconnects the connection (without sending #logout first).
266
309
  # - #disconnected?: True if the connection has been closed.
267
310
  #
@@ -317,37 +360,36 @@ module Net
317
360
  # <em>In general, #capable? should be used rather than explicitly sending a
318
361
  # +CAPABILITY+ command to the server.</em>
319
362
  # - #noop: Allows the server to send unsolicited untagged #responses.
320
- # - #logout: Tells the server to end the session. Enters the "_logout_" state.
363
+ # - #logout: Tells the server to end the session. Enters the +logout+ state.
321
364
  #
322
365
  # ==== Not Authenticated state
323
366
  #
324
367
  # In addition to the commands for any state, the following commands are valid
325
- # in the "<em>not authenticated</em>" state:
368
+ # in the +not_authenticated+ state:
326
369
  #
327
370
  # - #starttls: Upgrades a clear-text connection to use TLS.
328
371
  #
329
372
  # <em>Requires the +STARTTLS+ capability.</em>
330
373
  # - #authenticate: Identifies the client to the server using the given
331
374
  # {SASL mechanism}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml]
332
- # and credentials. Enters the "_authenticated_" state.
375
+ # and credentials. Enters the +authenticated+ state.
333
376
  #
334
377
  # <em>The server should list <tt>"AUTH=#{mechanism}"</tt> capabilities for
335
378
  # supported mechanisms.</em>
336
379
  # - #login: Identifies the client to the server using a plain text password.
337
- # Using #authenticate is generally preferred. Enters the "_authenticated_"
338
- # state.
380
+ # Using #authenticate is preferred. Enters the +authenticated+ state.
339
381
  #
340
382
  # <em>The +LOGINDISABLED+ capability</em> <b>must NOT</b> <em>be listed.</em>
341
383
  #
342
384
  # ==== Authenticated state
343
385
  #
344
386
  # In addition to the commands for any state, the following commands are valid
345
- # in the "_authenticated_" state:
387
+ # in the +authenticated+ state:
346
388
  #
347
389
  # - #enable: Enables backwards incompatible server extensions.
348
390
  # <em>Requires the +ENABLE+ or +IMAP4rev2+ capability.</em>
349
- # - #select: Open a mailbox and enter the "_selected_" state.
350
- # - #examine: Open a mailbox read-only, and enter the "_selected_" state.
391
+ # - #select: Open a mailbox and enter the +selected+ state.
392
+ # - #examine: Open a mailbox read-only, and enter the +selected+ state.
351
393
  # - #create: Creates a new mailbox.
352
394
  # - #delete: Permanently remove a mailbox.
353
395
  # - #rename: Change the name of a mailbox.
@@ -369,12 +411,12 @@ module Net
369
411
  #
370
412
  # ==== Selected state
371
413
  #
372
- # In addition to the commands for any state and the "_authenticated_"
373
- # commands, the following commands are valid in the "_selected_" state:
414
+ # In addition to the commands for any state and the +authenticated+
415
+ # commands, the following commands are valid in the +selected+ state:
374
416
  #
375
- # - #close: Closes the mailbox and returns to the "_authenticated_" state,
417
+ # - #close: Closes the mailbox and returns to the +authenticated+ state,
376
418
  # expunging deleted messages, unless the mailbox was opened as read-only.
377
- # - #unselect: Closes the mailbox and returns to the "_authenticated_" state,
419
+ # - #unselect: Closes the mailbox and returns to the +authenticated+ state,
378
420
  # without expunging any messages.
379
421
  # <em>Requires the +UNSELECT+ or +IMAP4rev2+ capability.</em>
380
422
  # - #expunge: Permanently removes messages which have the Deleted flag set.
@@ -395,7 +437,7 @@ module Net
395
437
  #
396
438
  # ==== Logout state
397
439
  #
398
- # No \IMAP commands are valid in the "_logout_" state. If the socket is still
440
+ # No \IMAP commands are valid in the +logout+ state. If the socket is still
399
441
  # open, Net::IMAP will close it after receiving server confirmation.
400
442
  # Exceptions will be raised by \IMAP commands that have already started and
401
443
  # are waiting for a response, as well as any that are called after logout.
@@ -449,7 +491,7 @@ module Net
449
491
  # ==== RFC3691: +UNSELECT+
450
492
  # Folded into IMAP4rev2[https://tools.ietf.org/html/rfc9051] and also included
451
493
  # above with {Core IMAP commands}[rdoc-ref:Net::IMAP@Core+IMAP+commands].
452
- # - #unselect: Closes the mailbox and returns to the "_authenticated_" state,
494
+ # - #unselect: Closes the mailbox and returns to the +authenticated+ state,
453
495
  # without expunging any messages.
454
496
  #
455
497
  # ==== RFC4314: +ACL+
@@ -719,7 +761,7 @@ module Net
719
761
  # * {IMAP URLAUTH Authorization Mechanism Registry}[https://www.iana.org/assignments/urlauth-authorization-mechanism-registry/urlauth-authorization-mechanism-registry.xhtml]
720
762
  #
721
763
  class IMAP < Protocol
722
- VERSION = "0.4.18"
764
+ VERSION = "0.4.20"
723
765
 
724
766
  # Aliases for supported capabilities, to be used with the #enable command.
725
767
  ENABLE_ALIASES = {
@@ -727,6 +769,7 @@ module Net
727
769
  "UTF8=ONLY" => "UTF8=ACCEPT",
728
770
  }.freeze
729
771
 
772
+ autoload :ResponseReader, File.expand_path("imap/response_reader", __dir__)
730
773
  autoload :SASL, File.expand_path("imap/sasl", __dir__)
731
774
  autoload :SASLAdapter, File.expand_path("imap/sasl_adapter", __dir__)
732
775
  autoload :StringPrep, File.expand_path("imap/stringprep", __dir__)
@@ -741,9 +784,11 @@ module Net
741
784
  def self.config; Config.global end
742
785
 
743
786
  # Returns the global debug mode.
787
+ # Delegates to {Net::IMAP.config.debug}[rdoc-ref:Config#debug].
744
788
  def self.debug; config.debug end
745
789
 
746
790
  # Sets the global debug mode.
791
+ # Delegates to {Net::IMAP.config.debug=}[rdoc-ref:Config#debug=].
747
792
  def self.debug=(val)
748
793
  config.debug = val
749
794
  end
@@ -764,7 +809,7 @@ module Net
764
809
  alias default_ssl_port default_tls_port
765
810
  end
766
811
 
767
- # Returns the initial greeting the server, an UntaggedResponse.
812
+ # Returns the initial greeting sent by the server, an UntaggedResponse.
768
813
  attr_reader :greeting
769
814
 
770
815
  # The client configuration. See Net::IMAP::Config.
@@ -773,13 +818,28 @@ module Net
773
818
  # Net::IMAP.config.
774
819
  attr_reader :config
775
820
 
776
- # Seconds to wait until a connection is opened.
777
- # If the IMAP object cannot open a connection within this time,
778
- # it raises a Net::OpenTimeout exception. The default value is 30 seconds.
779
- def open_timeout; config.open_timeout end
821
+ ##
822
+ # :attr_reader: open_timeout
823
+ # Seconds to wait until a connection is opened. Also used by #starttls.
824
+ # Delegates to {config.open_timeout}[rdoc-ref:Config#open_timeout].
780
825
 
826
+ ##
827
+ # :attr_reader: idle_response_timeout
781
828
  # Seconds to wait until an IDLE response is received.
782
- def idle_response_timeout; config.idle_response_timeout end
829
+ # Delegates to {config.idle_response_timeout}[rdoc-ref:Config#idle_response_timeout].
830
+
831
+ ##
832
+ # :attr_accessor: max_response_size
833
+ #
834
+ # The maximum allowed server response size, in bytes.
835
+ # Delegates to {config.max_response_size}[rdoc-ref:Config#max_response_size].
836
+
837
+ # :stopdoc:
838
+ def open_timeout; config.open_timeout end
839
+ def idle_response_timeout; config.idle_response_timeout end
840
+ def max_response_size; config.max_response_size end
841
+ def max_response_size=(val) config.max_response_size = val end
842
+ # :startdoc:
783
843
 
784
844
  # The hostname this client connected to
785
845
  attr_reader :host
@@ -835,6 +895,12 @@ module Net
835
895
  #
836
896
  # See DeprecatedClientOptions.new for deprecated SSL arguments.
837
897
  #
898
+ # [response_handlers]
899
+ # A list of response handlers to be added before the receiver thread is
900
+ # started. This ensures every server response is handled, including the
901
+ # #greeting. Note that the greeting is handled in the current thread, but
902
+ # all other responses are handled in the receiver thread.
903
+ #
838
904
  # [config]
839
905
  # A Net::IMAP::Config object to use as the basis for #config. By default,
840
906
  # the global Net::IMAP.config is used.
@@ -906,7 +972,7 @@ module Net
906
972
  # [Net::IMAP::ByeResponseError]
907
973
  # Connected to the host successfully, but it immediately said goodbye.
908
974
  #
909
- def initialize(host, port: nil, ssl: nil,
975
+ def initialize(host, port: nil, ssl: nil, response_handlers: nil,
910
976
  config: Config.global, **config_options)
911
977
  super()
912
978
  # Config options
@@ -929,6 +995,7 @@ module Net
929
995
  @receiver_thread = nil
930
996
  @receiver_thread_exception = nil
931
997
  @receiver_thread_terminating = false
998
+ response_handlers&.each do add_response_handler(_1) end
932
999
 
933
1000
  # Client Protocol Sender (including state for currently running commands)
934
1001
  @tag_prefix = "RUBY"
@@ -944,6 +1011,7 @@ module Net
944
1011
  # Connection
945
1012
  @tls_verified = false
946
1013
  @sock = tcp_socket(@host, @port)
1014
+ @reader = ResponseReader.new(self, @sock)
947
1015
  start_tls_session if ssl_ctx
948
1016
  start_imap_connection
949
1017
 
@@ -1204,6 +1272,10 @@ module Net
1204
1272
  # both successful. Any error indicates that the connection has not been
1205
1273
  # secured.
1206
1274
  #
1275
+ # After the server agrees to start a TLS connection, this method waits up to
1276
+ # {config.open_timeout}[rdoc-ref:Config#open_timeout] before raising
1277
+ # +Net::OpenTimeout+.
1278
+ #
1207
1279
  # *Note:*
1208
1280
  # >>>
1209
1281
  # Any #response_handlers added before STARTTLS should be aware that the
@@ -1222,13 +1294,21 @@ module Net
1222
1294
  #
1223
1295
  def starttls(**options)
1224
1296
  @ssl_ctx_params, @ssl_ctx = build_ssl_ctx(options)
1225
- send_command("STARTTLS") do |resp|
1297
+ error = nil
1298
+ ok = send_command("STARTTLS") do |resp|
1226
1299
  if resp.kind_of?(TaggedResponse) && resp.name == "OK"
1227
1300
  clear_cached_capabilities
1228
1301
  clear_responses
1229
1302
  start_tls_session
1230
1303
  end
1304
+ rescue Exception => error
1305
+ raise # note that the error backtrace is in the receiver_thread
1231
1306
  end
1307
+ if error
1308
+ disconnect
1309
+ raise error
1310
+ end
1311
+ ok
1232
1312
  end
1233
1313
 
1234
1314
  # :call-seq:
@@ -2698,6 +2778,10 @@ module Net
2698
2778
  # end
2699
2779
  # }
2700
2780
  #
2781
+ # Response handlers can also be added when the client is created before the
2782
+ # receiver thread is started, by the +response_handlers+ argument to ::new.
2783
+ # This ensures every server response is handled, including the #greeting.
2784
+ #
2701
2785
  # Related: #remove_response_handler, #response_handlers
2702
2786
  def add_response_handler(handler = nil, &block)
2703
2787
  raise ArgumentError, "two Procs are passed" if handler && block
@@ -2724,6 +2808,7 @@ module Net
2724
2808
  def start_imap_connection
2725
2809
  @greeting = get_server_greeting
2726
2810
  @capabilities = capabilities_from_resp_code @greeting
2811
+ @response_handlers.each do |handler| handler.call(@greeting) end
2727
2812
  @receiver_thread = start_receiver_thread
2728
2813
  rescue Exception
2729
2814
  @sock.close
@@ -2852,23 +2937,10 @@ module Net
2852
2937
  end
2853
2938
 
2854
2939
  def get_response
2855
- buff = String.new
2856
- while true
2857
- s = @sock.gets(CRLF)
2858
- break unless s
2859
- buff.concat(s)
2860
- if /\{(\d+)\}\r\n/n =~ s
2861
- s = @sock.read($1.to_i)
2862
- buff.concat(s)
2863
- else
2864
- break
2865
- end
2866
- end
2940
+ buff = @reader.read_response_buffer
2867
2941
  return nil if buff.length == 0
2868
- if config.debug?
2869
- $stderr.print(buff.gsub(/^/n, "S: "))
2870
- end
2871
- return @parser.parse(buff)
2942
+ $stderr.print(buff.gsub(/^/n, "S: ")) if config.debug?
2943
+ @parser.parse(buff)
2872
2944
  end
2873
2945
 
2874
2946
  #############################
@@ -3069,6 +3141,7 @@ module Net
3069
3141
  raise "already using SSL" if @sock.kind_of?(OpenSSL::SSL::SSLSocket)
3070
3142
  raise "cannot start TLS without SSLContext" unless ssl_ctx
3071
3143
  @sock = SSLSocket.new(@sock, ssl_ctx)
3144
+ @reader = ResponseReader.new(self, @sock)
3072
3145
  @sock.sync_close = true
3073
3146
  @sock.hostname = @host if @sock.respond_to? :hostname=
3074
3147
  ssl_socket_connect(@sock, open_timeout)