@@ -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
@@ -260,8 +268,9 @@ module Net
260
268
#
261
269
# - Net::IMAP.new: Creates a new \IMAP client which connects immediately and
262
270
# waits for a successful server greeting before the method returns.
271
+ # - #connection_state: Returns the connection state.
263
272
# - #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.
273
+ # - #logout: Tells the server to end the session. Enters the +logout+ state.
265
274
# - #disconnect: Disconnects the connection (without sending #logout first).
266
275
# - #disconnected?: True if the connection has been closed.
267
276
#
@@ -317,37 +326,36 @@ module Net
317
326
# <em>In general, #capable? should be used rather than explicitly sending a
318
327
# +CAPABILITY+ command to the server.</em>
319
328
# - #noop: Allows the server to send unsolicited untagged #responses.
320
- # - #logout: Tells the server to end the session. Enters the "_logout_" state.
329
+ # - #logout: Tells the server to end the session. Enters the +logout+ state.
321
330
#
322
331
# ==== Not Authenticated state
323
332
#
324
333
# In addition to the commands for any state, the following commands are valid
325
- # in the "<em>not authenticated</em>" state:
334
+ # in the +not_authenticated+ state:
326
335
#
327
336
# - #starttls: Upgrades a clear-text connection to use TLS.
328
337
#
329
338
# <em>Requires the +STARTTLS+ capability.</em>
330
339
# - #authenticate: Identifies the client to the server using the given
331
340
# {SASL mechanism}[https://www.iana.org/assignments/sasl-mechanisms/sasl-mechanisms.xhtml]
332
- # and credentials. Enters the "_authenticated_" state.
341
+ # and credentials. Enters the +authenticated+ state.
333
342
#
334
343
# <em>The server should list <tt>"AUTH=#{mechanism}"</tt> capabilities for
335
344
# supported mechanisms.</em>
336
345
# - #login: Identifies the client to the server using a plain text password.
337
- # Using #authenticate is generally preferred. Enters the "_authenticated_"
338
- # state.
346
+ # Using #authenticate is preferred. Enters the +authenticated+ state.
339
347
#
340
348
# <em>The +LOGINDISABLED+ capability</em> <b>must NOT</b> <em>be listed.</em>
341
349
#
342
350
# ==== Authenticated state
343
351
#
344
352
# In addition to the commands for any state, the following commands are valid
345
- # in the "_authenticated_" state:
353
+ # in the +authenticated+ state:
346
354
#
347
355
# - #enable: Enables backwards incompatible server extensions.
348
356
# <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.
357
+ # - #select: Open a mailbox and enter the +selected+ state.
358
+ # - #examine: Open a mailbox read-only, and enter the +selected+ state.
351
359
# - #create: Creates a new mailbox.
352
360
# - #delete: Permanently remove a mailbox.
353
361
# - #rename: Change the name of a mailbox.
@@ -369,12 +377,12 @@ module Net
369
377
#
370
378
# ==== Selected state
371
379
#
372
- # In addition to the commands for any state and the "_authenticated_"
373
- # commands, the following commands are valid in the "_selected_" state:
380
+ # In addition to the commands for any state and the +authenticated+
381
+ # commands, the following commands are valid in the +selected+ state:
374
382
#
375
- # - #close: Closes the mailbox and returns to the "_authenticated_" state,
383
+ # - #close: Closes the mailbox and returns to the +authenticated+ state,
376
384
# expunging deleted messages, unless the mailbox was opened as read-only.
377
- # - #unselect: Closes the mailbox and returns to the "_authenticated_" state,
385
+ # - #unselect: Closes the mailbox and returns to the +authenticated+ state,
378
386
# without expunging any messages.
379
387
# <em>Requires the +UNSELECT+ or +IMAP4rev2+ capability.</em>
380
388
# - #expunge: Permanently removes messages which have the Deleted flag set.
@@ -395,7 +403,7 @@ module Net
395
403
#
396
404
# ==== Logout state
397
405
#
398
- # No \IMAP commands are valid in the "_logout_" state. If the socket is still
406
+ # No \IMAP commands are valid in the +logout+ state. If the socket is still
399
407
# open, Net::IMAP will close it after receiving server confirmation.
400
408
# Exceptions will be raised by \IMAP commands that have already started and
401
409
# are waiting for a response, as well as any that are called after logout.
@@ -449,7 +457,7 @@ module Net
449
457
# ==== RFC3691: +UNSELECT+
450
458
# Folded into IMAP4rev2[https://www.rfc-editor.org/rfc/rfc9051] and also included
451
459
# above with {Core IMAP commands}[rdoc-ref:Net::IMAP@Core+IMAP+commands].
452
- # - #unselect: Closes the mailbox and returns to the "_authenticated_" state,
460
+ # - #unselect: Closes the mailbox and returns to the +authenticated+ state,
453
461
# without expunging any messages.
454
462
#
455
463
# ==== RFC4314: +ACL+
@@ -752,9 +760,10 @@ class IMAP < Protocol
752
760
"UTF8=ONLY" => "UTF8=ACCEPT" ,
753
761
} . freeze
754
762
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__ )
763
+ autoload :ConnectionState , File . expand_path ( "imap/connection_state" , __dir__ )
764
+ autoload :SASL , File . expand_path ( "imap/sasl" , __dir__ )
765
+ autoload :SASLAdapter , File . expand_path ( "imap/sasl_adapter" , __dir__ )
766
+ autoload :StringPrep , File . expand_path ( "imap/stringprep" , __dir__ )
758
767
759
768
include MonitorMixin
760
769
if defined? ( OpenSSL ::SSL )
@@ -827,6 +836,67 @@ def idle_response_timeout; config.idle_response_timeout end
827
836
# Returns +false+ for a plaintext connection.
828
837
attr_reader :ssl_ctx_params
829
838
839
+ # Returns the current connection state.
840
+ #
841
+ # Once an IMAP connection is established, the connection is in one of four
842
+ # states: +not_authenticated+, +authenticated+, +selected+, and +logout+.
843
+ # Most commands are valid only in certain states.
844
+ #
845
+ # The connection state object responds to +to_sym+ and +name+ with the name
846
+ # of the current connection state, as a Symbol or String. Future versions
847
+ # of +net-imap+ may store additional information on the state object.
848
+ #
849
+ # From {RFC9051}[https://www.rfc-editor.org/rfc/rfc9051#section-3]:
850
+ # +----------------------+
851
+ # |connection established|
852
+ # +----------------------+
853
+ # ||
854
+ # \/
855
+ # +--------------------------------------+
856
+ # | server greeting |
857
+ # +--------------------------------------+
858
+ # || (1) || (2) || (3)
859
+ # \/ || ||
860
+ # +-----------------+ || ||
861
+ # |Not Authenticated| || ||
862
+ # +-----------------+ || ||
863
+ # || (7) || (4) || ||
864
+ # || \/ \/ ||
865
+ # || +----------------+ ||
866
+ # || | Authenticated |<=++ ||
867
+ # || +----------------+ || ||
868
+ # || || (7) || (5) || (6) ||
869
+ # || || \/ || ||
870
+ # || || +--------+ || ||
871
+ # || || |Selected|==++ ||
872
+ # || || +--------+ ||
873
+ # || || || (7) ||
874
+ # \/ \/ \/ \/
875
+ # +--------------------------------------+
876
+ # | Logout |
877
+ # +--------------------------------------+
878
+ # ||
879
+ # \/
880
+ # +-------------------------------+
881
+ # |both sides close the connection|
882
+ # +-------------------------------+
883
+ #
884
+ # >>>
885
+ # Legend for the above diagram:
886
+ #
887
+ # 1. connection without pre-authentication (+OK+ #greeting)
888
+ # 2. pre-authenticated connection (+PREAUTH+ #greeting)
889
+ # 3. rejected connection (+BYE+ #greeting)
890
+ # 4. successful #login or #authenticate command
891
+ # 5. successful #select or #examine command
892
+ # 6. #close or #unselect command, unsolicited +CLOSED+ response code, or
893
+ # failed #select or #examine command
894
+ # 7. #logout command, server shutdown, or connection closed
895
+ #
896
+ # Before the server greeting, the state is +not_authenticated+.
897
+ # After the connection closes, the state remains +logout+.
898
+ attr_reader :connection_state
899
+
830
900
# Creates a new Net::IMAP object and connects it to the specified
831
901
# +host+.
832
902
#
@@ -946,6 +1016,8 @@ def initialize(host, port: nil, ssl: nil,
946
1016
@exception = nil
947
1017
@greeting = nil
948
1018
@capabilities = nil
1019
+ @tls_verified = false
1020
+ @connection_state = ConnectionState ::NotAuthenticated . new
949
1021
950
1022
# Client Protocol Receiver
951
1023
@parser = ResponseParser . new ( config : @config )
@@ -967,7 +1039,6 @@ def initialize(host, port: nil, ssl: nil,
967
1039
@logout_command_tag = nil
968
1040
969
1041
# Connection
970
- @tls_verified = false
971
1042
@sock = tcp_socket ( @host , @port )
972
1043
start_tls_session if ssl_ctx
973
1044
start_imap_connection
@@ -983,6 +1054,7 @@ def tls_verified?; @tls_verified end
983
1054
# Related: #logout, #logout!
984
1055
def disconnect
985
1056
return if disconnected?
1057
+ state_logout!
986
1058
begin
987
1059
begin
988
1060
# try to call SSL::SSLSocket#io.
@@ -1368,7 +1440,7 @@ def starttls(**options)
1368
1440
# capabilities, they will be cached.
1369
1441
def authenticate ( *args , sasl_ir : config . sasl_ir , **props , &callback )
1370
1442
sasl_adapter . authenticate ( *args , sasl_ir : sasl_ir , **props , &callback )
1371
- . tap { @capabilities = capabilities_from_resp_code _1 }
1443
+ . tap do state_authenticated! _1 end
1372
1444
end
1373
1445
1374
1446
# Sends a {LOGIN command [IMAP4rev1 §6.2.3]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.2.3]
@@ -1402,7 +1474,7 @@ def login(user, password)
1402
1474
raise LoginDisabledError
1403
1475
end
1404
1476
send_command ( "LOGIN" , user , password )
1405
- . tap { @capabilities = capabilities_from_resp_code _1 }
1477
+ . tap do state_authenticated! _1 end
1406
1478
end
1407
1479
1408
1480
# Sends a {SELECT command [IMAP4rev1 §6.3.1]}[https://www.rfc-editor.org/rfc/rfc3501#section-6.3.1]
@@ -1442,8 +1514,10 @@ def select(mailbox, condstore: false)
1442
1514
args = [ "SELECT" , mailbox ]
1443
1515
args << [ "CONDSTORE" ] if condstore
1444
1516
synchronize do
1517
+ state_unselected! # implicitly closes current mailbox
1445
1518
@responses . clear
1446
1519
send_command ( *args )
1520
+ . tap do state_selected! end
1447
1521
end
1448
1522
end
1449
1523
@@ -1460,8 +1534,10 @@ def examine(mailbox, condstore: false)
1460
1534
args = [ "EXAMINE" , mailbox ]
1461
1535
args << [ "CONDSTORE" ] if condstore
1462
1536
synchronize do
1537
+ state_unselected! # implicitly closes current mailbox
1463
1538
@responses . clear
1464
1539
send_command ( *args )
1540
+ . tap do state_selected! end
1465
1541
end
1466
1542
end
1467
1543
@@ -1900,6 +1976,7 @@ def check
1900
1976
# Related: #unselect
1901
1977
def close
1902
1978
send_command ( "CLOSE" )
1979
+ . tap do state_authenticated! end
1903
1980
end
1904
1981
1905
1982
# Sends an {UNSELECT command [RFC3691 §2]}[https://www.rfc-editor.org/rfc/rfc3691#section-3]
@@ -1916,6 +1993,7 @@ def close
1916
1993
# [RFC3691[https://www.rfc-editor.org/rfc/rfc3691]].
1917
1994
def unselect
1918
1995
send_command ( "UNSELECT" )
1996
+ . tap do state_authenticated! end
1919
1997
end
1920
1998
1921
1999
# call-seq:
@@ -3174,6 +3252,7 @@ def start_imap_connection
3174
3252
@capabilities = capabilities_from_resp_code @greeting
3175
3253
@receiver_thread = start_receiver_thread
3176
3254
rescue Exception
3255
+ state_logout!
3177
3256
@sock . close
3178
3257
raise
3179
3258
end
@@ -3182,7 +3261,10 @@ def get_server_greeting
3182
3261
greeting = get_response
3183
3262
raise Error , "No server greeting - connection closed" unless greeting
3184
3263
record_untagged_response_code greeting
3185
- raise ByeResponseError , greeting if greeting . name == "BYE"
3264
+ case greeting . name
3265
+ when "PREAUTH" then state_authenticated!
3266
+ when "BYE" then state_logout! ; raise ByeResponseError , greeting
3267
+ end
3186
3268
greeting
3187
3269
end
3188
3270
@@ -3192,6 +3274,8 @@ def start_receiver_thread
3192
3274
rescue Exception => ex
3193
3275
@receiver_thread_exception = ex
3194
3276
# don't exit the thread with an exception
3277
+ ensure
3278
+ state_logout!
3195
3279
end
3196
3280
end
3197
3281
@@ -3214,6 +3298,7 @@ def receive_responses
3214
3298
resp = get_response
3215
3299
rescue Exception => e
3216
3300
synchronize do
3301
+ state_logout!
3217
3302
@sock . close
3218
3303
@exception = e
3219
3304
end
@@ -3233,6 +3318,7 @@ def receive_responses
3233
3318
@tagged_response_arrival . broadcast
3234
3319
case resp . tag
3235
3320
when @logout_command_tag
3321
+ state_logout!
3236
3322
return
3237
3323
when @continued_command_tag
3238
3324
@continuation_request_exception =
@@ -3242,13 +3328,15 @@ def receive_responses
3242
3328
when UntaggedResponse
3243
3329
record_untagged_response ( resp )
3244
3330
if resp . name == "BYE" && @logout_command_tag . nil?
3331
+ state_logout!
3245
3332
@sock . close
3246
3333
@exception = ByeResponseError . new ( resp )
3247
3334
connection_closed = true
3248
3335
end
3249
3336
when ContinuationRequest
3250
3337
@continuation_request_arrival . signal
3251
3338
end
3339
+ state_unselected! if resp in { data : { code : { name : "CLOSED" } } }
3252
3340
@response_handlers . each do |handler |
3253
3341
handler . call ( resp )
3254
3342
end
@@ -3629,6 +3717,29 @@ def start_tls_session
3629
3717
end
3630
3718
end
3631
3719
3720
+ def state_authenticated! ( resp = nil )
3721
+ synchronize do
3722
+ @capabilities = capabilities_from_resp_code resp if resp
3723
+ @connection_state = ConnectionState ::Authenticated . new
3724
+ end
3725
+ end
3726
+
3727
+ def state_selected!
3728
+ synchronize do
3729
+ @connection_state = ConnectionState ::Selected . new
3730
+ end
3731
+ end
3732
+
3733
+ def state_unselected!
3734
+ state_authenticated! if connection_state . to_sym == :selected
3735
+ end
3736
+
3737
+ def state_logout!
3738
+ synchronize do
3739
+ @connection_state = ConnectionState ::Logout . new
3740
+ end
3741
+ end
3742
+
3632
3743
def sasl_adapter
3633
3744
SASLAdapter . new ( self , &method ( :send_command_with_continuations ) )
3634
3745
end
0 commit comments