net-imap 0.5.6 → 0.6.0

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.
data/lib/net/imap.rb CHANGED
@@ -43,10 +43,18 @@ 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
+ #
56
+ # See #connection_state.
57
+ #
50
58
  # === Sequence numbers and UIDs
51
59
  #
52
60
  # Messages have two sorts of identifiers: message sequence
@@ -199,6 +207,42 @@ module Net
199
207
  #
200
208
  # This script invokes the FETCH command and the SEARCH command concurrently.
201
209
  #
210
+ # When running multiple commands, care must be taken to avoid ambiguity. For
211
+ # example, SEARCH responses are ambiguous about which command they are
212
+ # responding to, so search commands should not run simultaneously, unless the
213
+ # server supports +ESEARCH+ {[RFC4731]}[https://rfc-editor.org/rfc/rfc4731] or
214
+ # IMAP4rev2[https://www.rfc-editor.org/rfc/rfc9051]. See {RFC9051
215
+ # §5.5}[https://www.rfc-editor.org/rfc/rfc9051.html#section-5.5] for
216
+ # other examples of command sequences which should not be pipelined.
217
+ #
218
+ # == Unbounded memory use
219
+ #
220
+ # Net::IMAP reads server responses in a separate receiver thread per client.
221
+ # Unhandled response data is saved to #responses, and response_handlers run
222
+ # inside the receiver thread. See the list of methods for {handling server
223
+ # responses}[rdoc-ref:Net::IMAP@Handling+server+responses], below.
224
+ #
225
+ # Because the receiver thread continuously reads and saves new responses, some
226
+ # scenarios must be careful to avoid unbounded memory use:
227
+ #
228
+ # * Commands such as #list or #fetch can have an enormous number of responses.
229
+ # * Commands such as #fetch can result in an enormous size per response.
230
+ # * Long-lived connections will gradually accumulate unsolicited server
231
+ # responses, especially +EXISTS+, +FETCH+, and +EXPUNGE+ responses.
232
+ # * A buggy or untrusted server could send inappropriate responses, which
233
+ # could be very numerous, very large, and very rapid.
234
+ #
235
+ # Use paginated or limited versions of commands whenever possible.
236
+ #
237
+ # Use Config#max_response_size to impose a limit on incoming server responses
238
+ # as they are being read. <em>This is especially important for untrusted
239
+ # servers.</em>
240
+ #
241
+ # Use #add_response_handler to handle responses after each one is received.
242
+ # Use the +response_handlers+ argument to ::new to assign response handlers
243
+ # before the receiver thread is started. Use #extract_responses,
244
+ # #clear_responses, or #responses (with a block) to prune responses.
245
+ #
202
246
  # == Errors
203
247
  #
204
248
  # An \IMAP server can send three different types of responses to indicate
@@ -260,8 +304,9 @@ module Net
260
304
  #
261
305
  # - Net::IMAP.new: Creates a new \IMAP client which connects immediately and
262
306
  # waits for a successful server greeting before the method returns.
307
+ # - #connection_state: Returns the connection state.
263
308
  # - #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.
309
+ # - #logout: Tells the server to end the session. Enters the +logout+ state.
265
310
  # - #disconnect: Disconnects the connection (without sending #logout first).
266
311
  # - #disconnected?: True if the connection has been closed.
267
312
  #
@@ -314,40 +359,39 @@ module Net
314
359
  #
315
360
  # - #capability: Returns the server's capabilities as an array of strings.
316
361
  #
317
- # <em>In general, #capable? should be used rather than explicitly sending a
318
- # +CAPABILITY+ command to the server.</em>
362
+ # <em>In general,</em> #capable? <em>should be used rather than explicitly
363
+ # sending a +CAPABILITY+ command to the server.</em>
319
364
  # - #noop: Allows the server to send unsolicited untagged #responses.
320
- # - #logout: Tells the server to end the session. Enters the "_logout_" state.
365
+ # - #logout: Tells the server to end the session. Enters the +logout+ state.
321
366
  #
322
367
  # ==== Not Authenticated state
323
368
  #
324
369
  # In addition to the commands for any state, the following commands are valid
325
- # in the "<em>not authenticated</em>" state:
370
+ # in the +not_authenticated+ state:
326
371
  #
327
372
  # - #starttls: Upgrades a clear-text connection to use TLS.
328
373
  #
329
374
  # <em>Requires the +STARTTLS+ capability.</em>
330
375
  # - #authenticate: Identifies the client to the server using the given
331
376
  # {SASL mechanism}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml]
332
- # and credentials. Enters the "_authenticated_" state.
377
+ # and credentials. Enters the +authenticated+ state.
333
378
  #
334
379
  # <em>The server should list <tt>"AUTH=#{mechanism}"</tt> capabilities for
335
380
  # supported mechanisms.</em>
336
381
  # - #login: Identifies the client to the server using a plain text password.
337
- # Using #authenticate is generally preferred. Enters the "_authenticated_"
338
- # state.
382
+ # Using #authenticate is preferred. Enters the +authenticated+ state.
339
383
  #
340
384
  # <em>The +LOGINDISABLED+ capability</em> <b>must NOT</b> <em>be listed.</em>
341
385
  #
342
386
  # ==== Authenticated state
343
387
  #
344
388
  # In addition to the commands for any state, the following commands are valid
345
- # in the "_authenticated_" state:
389
+ # in the +authenticated+ state:
346
390
  #
347
391
  # - #enable: Enables backwards incompatible server extensions.
348
392
  # <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.
393
+ # - #select: Open a mailbox and enter the +selected+ state.
394
+ # - #examine: Open a mailbox read-only, and enter the +selected+ state.
351
395
  # - #create: Creates a new mailbox.
352
396
  # - #delete: Permanently remove a mailbox.
353
397
  # - #rename: Change the name of a mailbox.
@@ -369,12 +413,12 @@ module Net
369
413
  #
370
414
  # ==== Selected state
371
415
  #
372
- # In addition to the commands for any state and the "_authenticated_"
373
- # commands, the following commands are valid in the "_selected_" state:
416
+ # In addition to the commands for any state and the +authenticated+
417
+ # commands, the following commands are valid in the +selected+ state:
374
418
  #
375
- # - #close: Closes the mailbox and returns to the "_authenticated_" state,
419
+ # - #close: Closes the mailbox and returns to the +authenticated+ state,
376
420
  # expunging deleted messages, unless the mailbox was opened as read-only.
377
- # - #unselect: Closes the mailbox and returns to the "_authenticated_" state,
421
+ # - #unselect: Closes the mailbox and returns to the +authenticated+ state,
378
422
  # without expunging any messages.
379
423
  # <em>Requires the +UNSELECT+ or +IMAP4rev2+ capability.</em>
380
424
  # - #expunge: Permanently removes messages which have the Deleted flag set.
@@ -395,7 +439,7 @@ module Net
395
439
  #
396
440
  # ==== Logout state
397
441
  #
398
- # No \IMAP commands are valid in the "_logout_" state. If the socket is still
442
+ # No \IMAP commands are valid in the +logout+ state. If the socket is still
399
443
  # open, Net::IMAP will close it after receiving server confirmation.
400
444
  # Exceptions will be raised by \IMAP commands that have already started and
401
445
  # are waiting for a response, as well as any that are called after logout.
@@ -449,7 +493,7 @@ module Net
449
493
  # ==== RFC3691: +UNSELECT+
450
494
  # Folded into IMAP4rev2[https://www.rfc-editor.org/rfc/rfc9051] and also included
451
495
  # above with {Core IMAP commands}[rdoc-ref:Net::IMAP@Core+IMAP+commands].
452
- # - #unselect: Closes the mailbox and returns to the "_authenticated_" state,
496
+ # - #unselect: Closes the mailbox and returns to the +authenticated+ state,
453
497
  # without expunging any messages.
454
498
  #
455
499
  # ==== RFC4314: +ACL+
@@ -744,7 +788,7 @@ module Net
744
788
  # * {IMAP URLAUTH Authorization Mechanism Registry}[https://www.iana.org/assignments/urlauth-authorization-mechanism-registry/urlauth-authorization-mechanism-registry.xhtml]
745
789
  #
746
790
  class IMAP < Protocol
747
- VERSION = "0.5.6"
791
+ VERSION = "0.6.0"
748
792
 
749
793
  # Aliases for supported capabilities, to be used with the #enable command.
750
794
  ENABLE_ALIASES = {
@@ -752,23 +796,41 @@ module Net
752
796
  "UTF8=ONLY" => "UTF8=ACCEPT",
753
797
  }.freeze
754
798
 
755
- autoload :SASL, File.expand_path("imap/sasl", __dir__)
756
- autoload :SASLAdapter, File.expand_path("imap/sasl_adapter", __dir__)
757
- autoload :StringPrep, File.expand_path("imap/stringprep", __dir__)
799
+ dir = File.expand_path("imap", __dir__)
800
+ autoload :ConnectionState, "#{dir}/connection_state"
801
+ autoload :ResponseReader, "#{dir}/response_reader"
802
+ autoload :SASL, "#{dir}/sasl"
803
+ autoload :SASLAdapter, "#{dir}/sasl_adapter"
804
+ autoload :SequenceSet, "#{dir}/sequence_set"
805
+ autoload :StringPrep, "#{dir}/stringprep"
758
806
 
759
807
  include MonitorMixin
760
- if defined?(OpenSSL::SSL)
761
- include OpenSSL
762
- include SSL
808
+
809
+ # :call-seq:
810
+ # Net::IMAP::SequenceSet(set = nil) -> SequenceSet
811
+ #
812
+ # Coerces +set+ into a SequenceSet, using either SequenceSet.try_convert or
813
+ # SequenceSet.new.
814
+ #
815
+ # * When +set+ is a SequenceSet, that same set is returned.
816
+ # * When +set+ responds to +to_sequence_set+, +set.to_sequence_set+ is
817
+ # returned.
818
+ # * Otherwise, returns the result from calling SequenceSet.new with +set+.
819
+ #
820
+ # Related: SequenceSet.try_convert, SequenceSet.new, SequenceSet::[]
821
+ def self.SequenceSet(set = nil)
822
+ SequenceSet.try_convert(set) || SequenceSet.new(set)
763
823
  end
764
824
 
765
825
  # Returns the global Config object
766
826
  def self.config; Config.global end
767
827
 
768
828
  # Returns the global debug mode.
829
+ # Delegates to {Net::IMAP.config.debug}[rdoc-ref:Config#debug].
769
830
  def self.debug; config.debug end
770
831
 
771
832
  # Sets the global debug mode.
833
+ # Delegates to {Net::IMAP.config.debug=}[rdoc-ref:Config#debug=].
772
834
  def self.debug=(val)
773
835
  config.debug = val
774
836
  end
@@ -789,7 +851,7 @@ module Net
789
851
  alias default_ssl_port default_tls_port
790
852
  end
791
853
 
792
- # Returns the initial greeting the server, an UntaggedResponse.
854
+ # Returns the initial greeting sent by the server, an UntaggedResponse.
793
855
  attr_reader :greeting
794
856
 
795
857
  # The client configuration. See Net::IMAP::Config.
@@ -798,13 +860,28 @@ module Net
798
860
  # Net::IMAP.config.
799
861
  attr_reader :config
800
862
 
801
- # Seconds to wait until a connection is opened.
802
- # If the IMAP object cannot open a connection within this time,
803
- # it raises a Net::OpenTimeout exception. The default value is 30 seconds.
804
- def open_timeout; config.open_timeout end
863
+ ##
864
+ # :attr_reader: open_timeout
865
+ # Seconds to wait until a connection is opened. Also used by #starttls.
866
+ # Delegates to {config.open_timeout}[rdoc-ref:Config#open_timeout].
805
867
 
868
+ ##
869
+ # :attr_reader: idle_response_timeout
806
870
  # Seconds to wait until an IDLE response is received.
807
- def idle_response_timeout; config.idle_response_timeout end
871
+ # Delegates to {config.idle_response_timeout}[rdoc-ref:Config#idle_response_timeout].
872
+
873
+ ##
874
+ # :attr_accessor: max_response_size
875
+ #
876
+ # The maximum allowed server response size, in bytes.
877
+ # Delegates to {config.max_response_size}[rdoc-ref:Config#max_response_size].
878
+
879
+ # :stopdoc:
880
+ def open_timeout; config.open_timeout end
881
+ def idle_response_timeout; config.idle_response_timeout end
882
+ def max_response_size; config.max_response_size end
883
+ def max_response_size=(val) config.max_response_size = val end
884
+ # :startdoc:
808
885
 
809
886
  # The hostname this client connected to
810
887
  attr_reader :host
@@ -827,6 +904,67 @@ module Net
827
904
  # Returns +false+ for a plaintext connection.
828
905
  attr_reader :ssl_ctx_params
829
906
 
907
+ # Returns the current connection state.
908
+ #
909
+ # Once an IMAP connection is established, the connection is in one of four
910
+ # states: +not_authenticated+, +authenticated+, +selected+, and +logout+.
911
+ # Most commands are valid only in certain states.
912
+ #
913
+ # The connection state object responds to +to_sym+ and +name+ with the name
914
+ # of the current connection state, as a Symbol or String. Future versions
915
+ # of +net-imap+ may store additional information on the state object.
916
+ #
917
+ # From {RFC9051}[https://www.rfc-editor.org/rfc/rfc9051#section-3]:
918
+ # +----------------------+
919
+ # |connection established|
920
+ # +----------------------+
921
+ # ||
922
+ # \/
923
+ # +--------------------------------------+
924
+ # | server greeting |
925
+ # +--------------------------------------+
926
+ # || (1) || (2) || (3)
927
+ # \/ || ||
928
+ # +-----------------+ || ||
929
+ # |Not Authenticated| || ||
930
+ # +-----------------+ || ||
931
+ # || (7) || (4) || ||
932
+ # || \/ \/ ||
933
+ # || +----------------+ ||
934
+ # || | Authenticated |<=++ ||
935
+ # || +----------------+ || ||
936
+ # || || (7) || (5) || (6) ||
937
+ # || || \/ || ||
938
+ # || || +--------+ || ||
939
+ # || || |Selected|==++ ||
940
+ # || || +--------+ ||
941
+ # || || || (7) ||
942
+ # \/ \/ \/ \/
943
+ # +--------------------------------------+
944
+ # | Logout |
945
+ # +--------------------------------------+
946
+ # ||
947
+ # \/
948
+ # +-------------------------------+
949
+ # |both sides close the connection|
950
+ # +-------------------------------+
951
+ #
952
+ # >>>
953
+ # Legend for the above diagram:
954
+ #
955
+ # 1. connection without pre-authentication (+OK+ #greeting)
956
+ # 2. pre-authenticated connection (+PREAUTH+ #greeting)
957
+ # 3. rejected connection (+BYE+ #greeting)
958
+ # 4. successful #login or #authenticate command
959
+ # 5. successful #select or #examine command
960
+ # 6. #close or #unselect command, unsolicited +CLOSED+ response code, or
961
+ # failed #select or #examine command
962
+ # 7. #logout command, server shutdown, or connection closed
963
+ #
964
+ # Before the server greeting, the state is +not_authenticated+.
965
+ # After the connection closes, the state remains +logout+.
966
+ attr_reader :connection_state
967
+
830
968
  # Creates a new Net::IMAP object and connects it to the specified
831
969
  # +host+.
832
970
  #
@@ -860,6 +998,12 @@ module Net
860
998
  #
861
999
  # See DeprecatedClientOptions.new for deprecated SSL arguments.
862
1000
  #
1001
+ # [response_handlers]
1002
+ # A list of response handlers to be added before the receiver thread is
1003
+ # started. This ensures every server response is handled, including the
1004
+ # #greeting. Note that the greeting is handled in the current thread, but
1005
+ # all other responses are handled in the receiver thread.
1006
+ #
863
1007
  # [config]
864
1008
  # A Net::IMAP::Config object to use as the basis for #config. By default,
865
1009
  # the global Net::IMAP.config is used.
@@ -931,7 +1075,7 @@ module Net
931
1075
  # [Net::IMAP::ByeResponseError]
932
1076
  # Connected to the host successfully, but it immediately said goodbye.
933
1077
  #
934
- def initialize(host, port: nil, ssl: nil,
1078
+ def initialize(host, port: nil, ssl: nil, response_handlers: nil,
935
1079
  config: Config.global, **config_options)
936
1080
  super()
937
1081
  # Config options
@@ -946,6 +1090,8 @@ module Net
946
1090
  @exception = nil
947
1091
  @greeting = nil
948
1092
  @capabilities = nil
1093
+ @tls_verified = false
1094
+ @connection_state = ConnectionState::NotAuthenticated.new
949
1095
 
950
1096
  # Client Protocol Receiver
951
1097
  @parser = ResponseParser.new(config: @config)
@@ -954,6 +1100,7 @@ module Net
954
1100
  @receiver_thread = nil
955
1101
  @receiver_thread_exception = nil
956
1102
  @receiver_thread_terminating = false
1103
+ response_handlers&.each do add_response_handler(_1) end
957
1104
 
958
1105
  # Client Protocol Sender (including state for currently running commands)
959
1106
  @tag_prefix = "RUBY"
@@ -967,8 +1114,8 @@ module Net
967
1114
  @logout_command_tag = nil
968
1115
 
969
1116
  # Connection
970
- @tls_verified = false
971
1117
  @sock = tcp_socket(@host, @port)
1118
+ @reader = ResponseReader.new(self, @sock)
972
1119
  start_tls_session if ssl_ctx
973
1120
  start_imap_connection
974
1121
  end
@@ -980,27 +1127,27 @@ module Net
980
1127
 
981
1128
  # Disconnects from the server.
982
1129
  #
1130
+ # Waits for receiver thread to close before returning. Slow or stuck
1131
+ # response handlers can cause #disconnect to hang until they complete.
1132
+ #
983
1133
  # Related: #logout, #logout!
984
1134
  def disconnect
1135
+ in_logout_state = try_state_logout?
985
1136
  return if disconnected?
986
1137
  begin
987
- begin
988
- # try to call SSL::SSLSocket#io.
989
- @sock.io.shutdown
990
- rescue NoMethodError
991
- # @sock is not an SSL::SSLSocket.
992
- @sock.shutdown
993
- end
1138
+ @sock.to_io.shutdown
994
1139
  rescue Errno::ENOTCONN
995
1140
  # ignore `Errno::ENOTCONN: Socket is not connected' on some platforms.
996
1141
  rescue Exception => e
997
1142
  @receiver_thread.raise(e)
998
1143
  end
1144
+ @sock.close
999
1145
  @receiver_thread.join
1000
- synchronize do
1001
- @sock.close
1002
- end
1003
1146
  raise e if e
1147
+ ensure
1148
+ # Try again after shutting down the receiver thread. With no reciever
1149
+ # left to wait for, any remaining locks should be _very_ brief.
1150
+ state_logout! unless in_logout_state
1004
1151
  end
1005
1152
 
1006
1153
  # Returns true if disconnected from the server.
@@ -1221,6 +1368,10 @@ module Net
1221
1368
  # both successful. Any error indicates that the connection has not been
1222
1369
  # secured.
1223
1370
  #
1371
+ # After the server agrees to start a TLS connection, this method waits up to
1372
+ # {config.open_timeout}[rdoc-ref:Config#open_timeout] before raising
1373
+ # +Net::OpenTimeout+.
1374
+ #
1224
1375
  # *Note:*
1225
1376
  # >>>
1226
1377
  # Any #response_handlers added before STARTTLS should be aware that the
@@ -1367,8 +1518,9 @@ module Net
1367
1518
  # completes. If the TaggedResponse to #authenticate includes updated
1368
1519
  # capabilities, they will be cached.
1369
1520
  def authenticate(*args, sasl_ir: config.sasl_ir, **props, &callback)
1521
+ sasl_ir = may_depend_on_capabilities_cached?(sasl_ir)
1370
1522
  sasl_adapter.authenticate(*args, sasl_ir: sasl_ir, **props, &callback)
1371
- .tap { @capabilities = capabilities_from_resp_code _1 }
1523
+ .tap do state_authenticated! _1 end
1372
1524
  end
1373
1525
 
1374
1526
  # Sends a {LOGIN command [IMAP4rev1 §6.2.3]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.2.3]
@@ -1402,7 +1554,7 @@ module Net
1402
1554
  raise LoginDisabledError
1403
1555
  end
1404
1556
  send_command("LOGIN", user, password)
1405
- .tap { @capabilities = capabilities_from_resp_code _1 }
1557
+ .tap do state_authenticated! _1 end
1406
1558
  end
1407
1559
 
1408
1560
  # Sends a {SELECT command [IMAP4rev1 §6.3.1]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.3.1]
@@ -1418,7 +1570,7 @@ module Net
1418
1570
  # When the +condstore+ keyword argument is true, the server is told to
1419
1571
  # enable the extension. If +mailbox+ supports persistence of mod-sequences,
1420
1572
  # the +HIGHESTMODSEQ+ ResponseCode will be sent as an untagged response to
1421
- # #select and all `FETCH` responses will include FetchData#modseq.
1573
+ # #select and all +FETCH+ responses will include FetchData#modseq.
1422
1574
  # Otherwise, the +NOMODSEQ+ ResponseCode will be sent.
1423
1575
  #
1424
1576
  # A Net::IMAP::NoResponseError is raised if the mailbox does not
@@ -1442,8 +1594,10 @@ module Net
1442
1594
  args = ["SELECT", mailbox]
1443
1595
  args << ["CONDSTORE"] if condstore
1444
1596
  synchronize do
1597
+ state_unselected! # implicitly closes current mailbox
1445
1598
  @responses.clear
1446
1599
  send_command(*args)
1600
+ .tap do state_selected! end
1447
1601
  end
1448
1602
  end
1449
1603
 
@@ -1460,8 +1614,10 @@ module Net
1460
1614
  args = ["EXAMINE", mailbox]
1461
1615
  args << ["CONDSTORE"] if condstore
1462
1616
  synchronize do
1617
+ state_unselected! # implicitly closes current mailbox
1463
1618
  @responses.clear
1464
1619
  send_command(*args)
1620
+ .tap do state_selected! end
1465
1621
  end
1466
1622
  end
1467
1623
 
@@ -1865,7 +2021,7 @@ module Net
1865
2021
  #
1866
2022
  # If +UIDPLUS+ [RFC4315[https://www.rfc-editor.org/rfc/rfc4315.html]] is
1867
2023
  # supported and the destination supports persistent UIDs, the server's
1868
- # response should include an +APPENDUID+ response code with UIDPlusData.
2024
+ # response should include an +APPENDUID+ response code with AppendUIDData.
1869
2025
  # This will report the UIDVALIDITY of the destination mailbox and the
1870
2026
  # assigned UID of the appended message.
1871
2027
  #
@@ -1900,6 +2056,7 @@ module Net
1900
2056
  # Related: #unselect
1901
2057
  def close
1902
2058
  send_command("CLOSE")
2059
+ .tap do state_authenticated! end
1903
2060
  end
1904
2061
 
1905
2062
  # Sends an {UNSELECT command [RFC3691 §2]}[https://www.rfc-editor.org/rfc/rfc3691#section-3]
@@ -1916,6 +2073,7 @@ module Net
1916
2073
  # [RFC3691[https://www.rfc-editor.org/rfc/rfc3691]].
1917
2074
  def unselect
1918
2075
  send_command("UNSELECT")
2076
+ .tap do state_authenticated! end
1919
2077
  end
1920
2078
 
1921
2079
  # call-seq:
@@ -1949,8 +2107,8 @@ module Net
1949
2107
  end
1950
2108
 
1951
2109
  # call-seq:
1952
- # uid_expunge{uid_set) -> array of message sequence numbers
1953
- # uid_expunge{uid_set) -> VanishedData of UIDs
2110
+ # uid_expunge(uid_set) -> array of message sequence numbers
2111
+ # uid_expunge(uid_set) -> VanishedData of UIDs
1954
2112
  #
1955
2113
  # Sends a {UID EXPUNGE command [RFC4315 §2.1]}[https://www.rfc-editor.org/rfc/rfc4315#section-2.1]
1956
2114
  # {[IMAP4rev2 §6.4.9]}[https://www.rfc-editor.org/rfc/rfc9051#section-6.4.9]
@@ -2514,6 +2672,7 @@ module Net
2514
2672
  # # fetch should return quickly and allocate little memory
2515
2673
  # results.size # => 0..500
2516
2674
  # break if results.empty?
2675
+ # results.sort_by!(&:uid) # server may return results out of order
2517
2676
  # next_uid_to_fetch = results.last.uid + 1
2518
2677
  # process results
2519
2678
  # end
@@ -2619,7 +2778,7 @@ module Net
2619
2778
  #
2620
2779
  # If +UIDPLUS+ [RFC4315[https://www.rfc-editor.org/rfc/rfc4315.html]] is
2621
2780
  # supported, the server's response should include a +COPYUID+ response code
2622
- # with UIDPlusData. This will report the UIDVALIDITY of the destination
2781
+ # with CopyUIDData. This will report the UIDVALIDITY of the destination
2623
2782
  # mailbox, the UID set of the source messages, and the assigned UID set of
2624
2783
  # the moved messages.
2625
2784
  #
@@ -2660,7 +2819,7 @@ module Net
2660
2819
  #
2661
2820
  # If +UIDPLUS+ [RFC4315[https://www.rfc-editor.org/rfc/rfc4315.html]] is
2662
2821
  # supported, the server's response should include a +COPYUID+ response code
2663
- # with UIDPlusData. This will report the UIDVALIDITY of the destination
2822
+ # with CopyUIDData. This will report the UIDVALIDITY of the destination
2664
2823
  # mailbox, the UID set of the source messages, and the assigned UID set of
2665
2824
  # the moved messages.
2666
2825
  #
@@ -2800,6 +2959,18 @@ module Net
2800
2959
  # command parameters defined by the extension will implicitly enable it.
2801
2960
  # See {[RFC7162 §3.1]}[https://www.rfc-editor.org/rfc/rfc7162.html#section-3.1].
2802
2961
  #
2962
+ # [+QRESYNC+ {[RFC7162]}[https://www.rfc-editor.org/rfc/rfc7162.html]]
2963
+ # *NOTE:* Enabling QRESYNC will replace +EXPUNGE+ with +VANISHED+, but
2964
+ # the extension arguments to #select, #examine, and #uid_fetch are not
2965
+ # supported yet.
2966
+ #
2967
+ # Adds quick resynchronization options to #select, #examine, and
2968
+ # #uid_fetch. +QRESYNC+ _must_ be explicitly enabled before using any of
2969
+ # the extension's command parameters. All +EXPUNGE+ responses will be
2970
+ # replaced with +VANISHED+ responses. Enabling +QRESYNC+ implicitly
2971
+ # enables +CONDSTORE+ as well.
2972
+ # See {[RFC7162 §3.2]}[https://www.rfc-editor.org/rfc/rfc7162.html#section-3.2].
2973
+ #
2803
2974
  # [+:utf8+ --- an alias for <tt>"UTF8=ACCEPT"</tt>]
2804
2975
  #
2805
2976
  # In a future release, <tt>enable(:utf8)</tt> will enable either
@@ -2917,8 +3088,8 @@ module Net
2917
3088
  raise @exception || Net::IMAP::Error.new("connection closed")
2918
3089
  end
2919
3090
  ensure
3091
+ remove_response_handler(response_handler)
2920
3092
  unless @receiver_thread_terminating
2921
- remove_response_handler(response_handler)
2922
3093
  put_string("DONE#{CRLF}")
2923
3094
  response = get_tagged_response(tag, "IDLE", idle_response_timeout)
2924
3095
  end
@@ -3146,6 +3317,10 @@ module Net
3146
3317
  # end
3147
3318
  # }
3148
3319
  #
3320
+ # Response handlers can also be added when the client is created before the
3321
+ # receiver thread is started, by the +response_handlers+ argument to ::new.
3322
+ # This ensures every server response is handled, including the #greeting.
3323
+ #
3149
3324
  # Related: #remove_response_handler, #response_handlers
3150
3325
  def add_response_handler(handler = nil, &block)
3151
3326
  raise ArgumentError, "two Procs are passed" if handler && block
@@ -3172,8 +3347,10 @@ module Net
3172
3347
  def start_imap_connection
3173
3348
  @greeting = get_server_greeting
3174
3349
  @capabilities = capabilities_from_resp_code @greeting
3350
+ @response_handlers.each do |handler| handler.call(@greeting) end
3175
3351
  @receiver_thread = start_receiver_thread
3176
3352
  rescue Exception
3353
+ state_logout!
3177
3354
  @sock.close
3178
3355
  raise
3179
3356
  end
@@ -3182,7 +3359,10 @@ module Net
3182
3359
  greeting = get_response
3183
3360
  raise Error, "No server greeting - connection closed" unless greeting
3184
3361
  record_untagged_response_code greeting
3185
- raise ByeResponseError, greeting if greeting.name == "BYE"
3362
+ case greeting.name
3363
+ when "PREAUTH" then state_authenticated!
3364
+ when "BYE" then state_logout!; raise ByeResponseError, greeting
3365
+ end
3186
3366
  greeting
3187
3367
  end
3188
3368
 
@@ -3214,6 +3394,7 @@ module Net
3214
3394
  resp = get_response
3215
3395
  rescue Exception => e
3216
3396
  synchronize do
3397
+ state_logout!
3217
3398
  @sock.close
3218
3399
  @exception = e
3219
3400
  end
@@ -3233,6 +3414,7 @@ module Net
3233
3414
  @tagged_response_arrival.broadcast
3234
3415
  case resp.tag
3235
3416
  when @logout_command_tag
3417
+ state_logout!
3236
3418
  return
3237
3419
  when @continued_command_tag
3238
3420
  @continuation_request_exception =
@@ -3242,6 +3424,7 @@ module Net
3242
3424
  when UntaggedResponse
3243
3425
  record_untagged_response(resp)
3244
3426
  if resp.name == "BYE" && @logout_command_tag.nil?
3427
+ state_logout!
3245
3428
  @sock.close
3246
3429
  @exception = ByeResponseError.new(resp)
3247
3430
  connection_closed = true
@@ -3249,6 +3432,7 @@ module Net
3249
3432
  when ContinuationRequest
3250
3433
  @continuation_request_arrival.signal
3251
3434
  end
3435
+ state_unselected! if resp in {data: {code: {name: "CLOSED"}}}
3252
3436
  @response_handlers.each do |handler|
3253
3437
  handler.call(resp)
3254
3438
  end
@@ -3269,6 +3453,8 @@ module Net
3269
3453
  @idle_done_cond.signal
3270
3454
  end
3271
3455
  end
3456
+ ensure
3457
+ state_logout!
3272
3458
  end
3273
3459
 
3274
3460
  def get_tagged_response(tag, cmd, timeout = nil)
@@ -3300,23 +3486,10 @@ module Net
3300
3486
  end
3301
3487
 
3302
3488
  def get_response
3303
- buff = String.new
3304
- while true
3305
- s = @sock.gets(CRLF)
3306
- break unless s
3307
- buff.concat(s)
3308
- if /\{(\d+)\}\r\n/n =~ s
3309
- s = @sock.read($1.to_i)
3310
- buff.concat(s)
3311
- else
3312
- break
3313
- end
3314
- end
3489
+ buff = @reader.read_response_buffer
3315
3490
  return nil if buff.length == 0
3316
- if config.debug?
3317
- $stderr.print(buff.gsub(/^/n, "S: "))
3318
- end
3319
- return @parser.parse(buff)
3491
+ $stderr.print(buff.gsub(/^/n, "S: ")) if config.debug?
3492
+ @parser.parse(buff)
3320
3493
  end
3321
3494
 
3322
3495
  #############################
@@ -3406,11 +3579,11 @@ module Net
3406
3579
  end
3407
3580
 
3408
3581
  def enforce_logindisabled?
3409
- if config.enforce_logindisabled == :when_capabilities_cached
3410
- capabilities_cached?
3411
- else
3412
- config.enforce_logindisabled
3413
- end
3582
+ may_depend_on_capabilities_cached?(config.enforce_logindisabled)
3583
+ end
3584
+
3585
+ def may_depend_on_capabilities_cached?(value)
3586
+ value == :when_capabilities_cached ? capabilities_cached? : value
3414
3587
  end
3415
3588
 
3416
3589
  def expunge_internal(...)
@@ -3509,6 +3682,9 @@ module Net
3509
3682
  end
3510
3683
 
3511
3684
  def fetch_internal(cmd, set, attr, mod = nil, partial: nil, changedsince: nil)
3685
+ if partial && !cmd.start_with?("UID ")
3686
+ raise ArgumentError, "partial can only be used with uid_fetch"
3687
+ end
3512
3688
  set = SequenceSet[set]
3513
3689
  if partial
3514
3690
  mod ||= []
@@ -3603,11 +3779,8 @@ module Net
3603
3779
  def build_ssl_ctx(ssl)
3604
3780
  if ssl
3605
3781
  params = (Hash.try_convert(ssl) || {}).freeze
3606
- context = SSLContext.new
3782
+ context = OpenSSL::SSL::SSLContext.new
3607
3783
  context.set_params(params)
3608
- if defined?(VerifyCallbackProc)
3609
- context.verify_callback = VerifyCallbackProc
3610
- end
3611
3784
  context.freeze
3612
3785
  [params, context]
3613
3786
  else
@@ -3619,16 +3792,54 @@ module Net
3619
3792
  raise "SSL extension not installed" unless defined?(OpenSSL::SSL)
3620
3793
  raise "already using SSL" if @sock.kind_of?(OpenSSL::SSL::SSLSocket)
3621
3794
  raise "cannot start TLS without SSLContext" unless ssl_ctx
3622
- @sock = SSLSocket.new(@sock, ssl_ctx)
3795
+ @sock = OpenSSL::SSL::SSLSocket.new(@sock, ssl_ctx)
3796
+ @reader = ResponseReader.new(self, @sock)
3623
3797
  @sock.sync_close = true
3624
3798
  @sock.hostname = @host if @sock.respond_to? :hostname=
3625
3799
  ssl_socket_connect(@sock, open_timeout)
3626
- if ssl_ctx.verify_mode != VERIFY_NONE
3800
+ if ssl_ctx.verify_mode != OpenSSL::SSL::VERIFY_NONE
3627
3801
  @sock.post_connection_check(@host)
3628
3802
  @tls_verified = true
3629
3803
  end
3630
3804
  end
3631
3805
 
3806
+ def state_authenticated!(resp = nil)
3807
+ synchronize do
3808
+ @capabilities = capabilities_from_resp_code resp if resp
3809
+ @connection_state = ConnectionState::Authenticated.new
3810
+ end
3811
+ end
3812
+
3813
+ def state_selected!
3814
+ synchronize do
3815
+ @connection_state = ConnectionState::Selected.new
3816
+ end
3817
+ end
3818
+
3819
+ def state_unselected!
3820
+ synchronize do
3821
+ state_authenticated! if connection_state.to_sym == :selected
3822
+ end
3823
+ end
3824
+
3825
+ def state_logout!
3826
+ return true if connection_state in [:logout, *]
3827
+ synchronize do
3828
+ return true if connection_state in [:logout, *]
3829
+ @connection_state = ConnectionState::Logout.new
3830
+ end
3831
+ end
3832
+
3833
+ # don't wait to aqcuire the lock
3834
+ def try_state_logout?
3835
+ return true if connection_state in [:logout, *]
3836
+ return false unless acquired_lock = mon_try_enter
3837
+ state_logout!
3838
+ true
3839
+ ensure
3840
+ mon_exit if acquired_lock
3841
+ end
3842
+
3632
3843
  def sasl_adapter
3633
3844
  SASLAdapter.new(self, &method(:send_command_with_continuations))
3634
3845
  end
@@ -3650,7 +3861,6 @@ require_relative "imap/errors"
3650
3861
  require_relative "imap/config"
3651
3862
  require_relative "imap/command_data"
3652
3863
  require_relative "imap/data_encoding"
3653
- require_relative "imap/data_lite"
3654
3864
  require_relative "imap/flags"
3655
3865
  require_relative "imap/response_data"
3656
3866
  require_relative "imap/response_parser"